在VR開發(fā)中食寡,除了圖形視覺渲染典格,音頻處理是重要的一環(huán),好的音頻處理可以欺騙用戶的聽覺阱佛,達到身臨其境的效果帖汞,本文主要介紹WebVR音頻是如何開發(fā)的。
VR Audio
VR音頻的輸出硬件主要是耳機凑术,根據(jù)音頻源與場景之間的關系翩蘸,可將VR音頻分為兩類:靜態(tài)音頻和空間化音頻(audio spatialization)。
靜態(tài)音頻
這類音頻作用于整個VR場景淮逊,可簡單的理解成背景音樂鹿鳖,音頻輸出是靜態(tài)的,比如微風雨滴聲壮莹、鬧市聲等充斥整個場景的背景音效。
對于環(huán)境音效的開發(fā)姻檀,我們可以簡單的使用<audio>標簽進行循環(huán)播放命满。
空間化音頻
音頻作用在空間的實體上,具有發(fā)聲體和聽者的位置關系绣版,音頻輸出會根據(jù)發(fā)聲體與用戶的距離胶台、方向動態(tài)變化,它模擬了現(xiàn)實中聲音的傳播方式杂抽,具有空間感诈唬。
實現(xiàn)原理:在虛擬場景中,通過調(diào)節(jié)音頻的振幅來描述發(fā)聲體與聽者之間的距離缩麸,再通過調(diào)節(jié)左右通道(audio channel)之間的差異铸磅,控制左右耳機喇叭輸出,來描述發(fā)聲體相對聽者的方位。
- 從發(fā)聲體與用戶兩點間的距離來看阅仔,如距離越遠吹散,音頻音量(振幅)應越小八酒;
-
從發(fā)聲體與用戶的方向來看空民,如發(fā)聲體位于聽者左側,則音頻輸出的左聲道應比右聲道音量大羞迷。
形如音頻空間化此類稍復雜的音頻的處理界轩,可通過Web Audio API來實現(xiàn)。
Web Audio API 簡介
Web Audio API提供了一個功能強大的音頻處理系統(tǒng)衔瓮,允許我們在瀏覽器中通過js來實時控制處理音頻浊猾,比如音頻可視化、音頻混合等报辱。
Web Audio處理流程可以比喻成一個加工廠對聲源的加工与殃,這個加工廠由多個加工模塊AudioNode
連接而成,音頻源經(jīng)過一系列的處理加工后碍现,被輸送至揚聲器幅疼。
AudioContext
類似于canvas
的context
上下文環(huán)境,它代表了一個audio加工廠控制中心昼接,負責各個audioNode的創(chuàng)建和組合爽篷,通過new AudioContext()
的方式創(chuàng)建。
AudioNode
AudioNode音頻節(jié)點慢睡,則是加工廠的加工模塊逐工, 按照功能可分為三類:輸入結點、處理結點漂辐、輸出結點泪喊。每個結點都擁有connect
方法連接下一個節(jié)點,將音頻輸出到下一個模塊髓涯。
- 輸入結點主要負責加載解碼音頻源袒啼,比如獲取二進制音頻源的
BufferSourceNode
、獲取<audio>音頻源的MediaElementSourceNode
等纬纪; - 處理結點主要對音頻數(shù)據(jù)進行計算處理蚓再,比如處理音頻振幅的
GainNode
等; - 輸出結點則將音頻輸出至揚聲器或耳機包各,
AudioContext.destination
便是默認的輸出節(jié)點摘仅。
一個簡單的音頻處理流程只需要分為四步:
- 創(chuàng)建音頻上下文
- 創(chuàng)建并初始化輸入結點、處理結點
- 將輸入結點问畅、處理結點娃属、輸出結點進行有連接
- 動態(tài)修改結點屬性以輸出不同音效
參考以下代碼:
const myAudio = document.querySelector('audio');
const audioCtx = new AudioContext(); // 創(chuàng)建音頻上下文
// 創(chuàng)建輸入結點六荒,解碼audio標簽的音頻源;創(chuàng)建處理結點膳犹,處理音頻
const source = audioCtx.createMediaElementSource(myAudio);
const gainNode = audioCtx.createGain(); // 創(chuàng)建GainNode結點控制音頻振幅
// 將輸入結點恬吕、處理結點、輸出結點兩兩相連
source.connect(gainNode); // 將輸入結點連接到gainNode處理結點
gainNode.connect(audioCtx.destination); // 將gainNode連接到destination輸出節(jié)點
// 通過動態(tài)改變結點屬性產(chǎn)生不同音效
source.start(0); // 播放音頻
gainNode.gain.value = val; // 設置音量
理解了Web Audio的開發(fā)流程须床,接下來看看如何在WebVR中實現(xiàn)Audio Spatialization铐料,這里VR場景使用three.js進行開發(fā)。
實現(xiàn)空間化音頻
Audio Spatialization的實現(xiàn)主要通過AudioListener
和PannerNode
結點配合豺旬,這兩個對象可以根據(jù)空間方位信息動態(tài)處理音頻源钠惩,并輸出左右聲道。
-
AudioListener
對象代表三維空間中的聽者(用戶)族阅,通過AudioContext.listener
屬性獲嚷恕; -
PannerNode
對象指的是三維空間中的發(fā)聲體坦刀,通過AudioContext.createPanner()
創(chuàng)建愧沟。
我們需要初始化這兩個對象,并將空間方位信息作為入?yún)討B(tài)傳給它們鲤遥。
設置PannerNode
const myAudio = document.querySelector('audio');
const audioCtx = new AudioContext(); // 創(chuàng)建音頻上下文
const source = audioCtx.createMediaElementSource(myAudio);
const panner = audioCtx.createPannerNode();
panner.setPosition(speaker.position.x, speaker.position.y, speaker.position.z); // 將發(fā)聲體坐標傳給PannerNode
source.connect(panner); // 將輸入結點連接到PannerNode處理結點
panner.connect(audioCtx.destination);
source.start(0); // 播放音頻
設置AudioListener
VR用戶頭顯最多有6-Dof:position位置3-Dof系統(tǒng)和orientation方向3-Dof系統(tǒng)沐寺,我們需要將這6-Dof的信息傳入AudioListener,由它為我們處理音頻數(shù)據(jù)盖奈。
對于用戶位置數(shù)據(jù),AudioListener提供了三個位置屬性:positionX
,positionY
,positionZ
钢坦,它分別代表聽者當前位置的xyz坐標究孕,我們可將用戶在場景中的位置(一般用camera的position)賦值給這三個屬性。
// 為listener設置position
const listener = audioCtx.listener;
listener.positionX = camera.position.x;
listener.positionY = camera.position.y;
listener.positionZ = camera.position.z;
除了傳入用戶的位置爹凹,我們還需要將用戶的視角方向信息傳給AudioListener
厨诸,具體是給AudioListener的Forward向量三個分量forwardX
,forwardY
,forwardZ
和Up向量三個分量upX
,upY
,upZ
賦值。
- Forward向量沿著鼻子方向指向前禾酱,默認是(0,0,-1)泳猬;
-
Up向量沿著頭頂方向指向上,默認是(0,1,0)宇植。
- 在VR場景中,當用戶轉動頭部改變視角時埋心,up向量或forward向量會隨之改變指郁,但兩者始終垂直。
Up向量 = Camera.旋轉矩陣 × [0,1,0]
Forward向量 = Camera.旋轉矩陣 × [0,0,-1]
參照上方公式拷呆,這里的camera是three.js的camera闲坎,指代用戶的頭部疫粥,通過camera.quaternion
獲取相機的旋轉(四元數(shù))矩陣,與初始向量相乘腰懂,得到當前Up向量和Forward向量梗逮,代碼如下:
// 計算當前l(fā)istener的forward向量
let forward = new THREE.Vector3(0,0,-1);
forward.applyQuaternion(camera.quaternion); // forward初始向量與camera四元數(shù)矩陣相乘,得到當前的forward向量
forward.normalize(); // 向量歸一
// 賦值給AudioListener的forward分量
listener.forwardX.value = forward.x;
listener.forwardY.value = forward.y;
listener.forwardZ.value = forward.z;
// 計算當前l(fā)istener的up向量
let up = new THREE.Vector3(0,1,0);
up.applyQuaternion(camera.quaternion); // up初始向量與camera四元數(shù)矩陣相乘绣溜,得到當前的up向量
up.normalize(); // 向量歸一
// 賦值給AudioListener的up分量
listener.upX.value = up.x;
listener.upY.value = up.y;
listener.upZ.value = up.z;
WebVR實現(xiàn)音頻角色
在VR場景中慷彤,根據(jù)音頻的發(fā)起方和接收方可以分為兩個角色:Speaker發(fā)聲體與Listener聽者,即用戶怖喻。
一個VR場景音頻角色由一個Listener和多個Speaker組成底哗,于是筆者將
PannerNode
和AudioListener
進行獨立封裝,整合為Speaker類和Listener對象锚沸。PS:這里沿用前幾期three.js開發(fā)WebVR的方式跋选,可參考《WebVR開發(fā)——標準教程》
Speaker實現(xiàn)
Speaker類代表發(fā)聲體,主要做了以下事情:
- 初始化階段加載解析音頻源哗蜈,創(chuàng)建并連接輸入結點前标、處理結點、輸出結點
- 提供
update
公用方法距潘,在每一幀中更新PannerNode位置炼列。
class Speaker {
constructor(ctx,path) {
this.path = path;
this.ctx = ctx;
this.source = ctx.createBufferSource();
this.panner = ctx.createPanner();
this.source.loop = true; // 設置音頻循環(huán)播放
this.source.connect(this.panner); // 將輸入結點連至PannerNode
this.panner.connect(ctx.destination); // 將PannerNode連至輸出結點
this._processAudio(); // 異步函數(shù),請求與加載音頻數(shù)據(jù)
}
update(position) {
const { panner } = this;
panner.setPosition(position.x, position.y, position.z); // 將發(fā)聲體坐標傳給PannerNode
}
_loadAudio(path) {
// 使用fetch請求音頻文件
return fetch(path).then(res => res.arrayBuffer());
}
async _processAudio() {
const { path, ctx, source } = this;
try {
const data = await this._loadAudio(path); // 異步請求音頻
const buffer = await ctx.decodeAudioData(data); // 解碼音頻數(shù)據(jù)
source.buffer = buffer; // 將解碼數(shù)據(jù)賦值給BufferSourceNode輸入結點
source.start(0); // 播放音頻
} catch(err) {
console.err(err);
}
}
}
這里初始化的流程跟前面略有不同绽昼,這里使用的是fetch請求音頻文件唯鸭,通過BufferSourceNode
輸入結點解析音頻數(shù)據(jù)。
update
方法傳入發(fā)聲體position硅确,設置PannerNode
位置目溉。
Listener實現(xiàn)
Listener對象代表聽者,提供update
公用方法菱农,在每幀中傳入AudioListener
的位置和方向缭付。
// 創(chuàng)建Listener對象
const Listener = {
init(ctx) {
this.ctx = ctx;
this.listener = this.ctx.listener;
},
update(position,quaternion) {
const { listener } = this;
listener.positionX = position.x;
listener.positionY = position.y;
listener.positionZ = position.z;
// 計算當前l(fā)istener的forward向量
let forward = new THREE.Vector3(0,0,-1);
forward.applyQuaternion(quaternion);
forward.normalize();
listener.forwardX.value = forward.x;
listener.forwardY.value = forward.y;
listener.forwardZ.value = forward.z;
// 計算當前l(fā)istener的up向量
let up = new THREE.Vector3(0,1,0);
up.applyQuaternion(quaternion);
up.normalize();
listener.upX.value = up.x;
listener.upY.value = up.y;
listener.upZ.value = up.z;
}
}
這里只是簡單的將AudioListener
作一層封裝,update方法傳入camera的position和四元數(shù)矩陣循未,設置AudioListener
位置陷猫、方向。
接下來的妖,將Listener和Speaker引入到WebVR應用中绣檬,下面例子描述了這樣一個簡陋場景:一輛狂響喇叭的汽車從你身旁經(jīng)過,并駛向遠方嫂粟。
class WebVRApp {
...
start() {
const { scene, camera } = this;
... // 創(chuàng)建燈光娇未、地面
// 創(chuàng)建一輛簡陋小車
const geometry = new THREE.CubeGeometry(4, 3, 5);
const material = new THREE.MeshLambertMaterial({ color: 0xef6500 });
this.car = new THREE.Mesh(geometry, material);
this.car.position.set(-12, 2, -100);
scene.add(this.car);
const ctx = new AudioContext(); // 創(chuàng)建AudioContext上下文
Listener.init(ctx); // 初始化listener
this.car_speaker = new Speaker(ctx,'audio/horn.wav'); // 創(chuàng)建speaker星虹,傳入上下文和音頻路徑
}
}
首先在start
方法創(chuàng)建小汽車零抬,接著初始化Listener并創(chuàng)建一個Speaker镊讼。
class WebVRApp {
...
update() {
const { scene, camera, renderer} = this;
// 啟動渲染
this.car.position.z += 0.4;
this.car_speaker.update(this.car.position); // 更新speaker位置
Listener.update(camera.position, camera.quaternion); // 更新Listener位置以及頭部朝向
renderer.render(scene, camera);
}
}
new WebVRApp();
在動畫渲染update
方法中,更新小汽車的位置平夜,并調(diào)用Speaker和Listener的update方法蝶棋,傳入小汽車的位置、用戶的位置和旋轉矩陣忽妒,更新音頻空間信息玩裙。
示例地址:https://yonechen.github.io/WebVR-helloworld/examples/3d-audio.html
源碼地址:https://github.com/YoneChen/WebVR-helloworld/blob/master/examples/3d-audio.html
小結
本文主要講解了WebVR應用音頻空間化的實現(xiàn)步驟,核心是運用了Web Audio API的PannerNode
和AudioListener
兩個對象處理音頻源锰扶,文末展示了VR Audio的一個簡單代碼例子献酗,three.js本身也提供了完善的音頻空間化支持,可以參考PositinalAudio坷牛。
最近筆者正在實現(xiàn)WebVR多人聊天室罕偎,下期文章圍繞此展開,敬請期待~
更多文章可關注WebXR技術莊園
WebVR開發(fā)教程——交互事件(二)使用Gamepad
WebVR開發(fā)教程——深度剖析 關于WebVR的開發(fā)調(diào)試方案以及原理機制
WebVR開發(fā)教程——標準入門 使用Three.js開發(fā)WebVR場景的入門教程