WebVR開發教程——Web Audio實現3D音效
在VR開發中,除了圖形視覺渲染,音頻處理是重要的一環,好的音頻處理可以欺騙用戶的聽覺,達到身臨其境的效果,本文主要介紹WebVR音頻是如何開發的。
VR Audio
VR音頻的輸出硬件主要是耳機,根據音頻源與場景之間的關係,可將VR音頻分為兩類:靜態音頻和空間化音頻(audio spatialization)。
靜態音頻
這類音頻作用於整個VR場景,可簡單的理解成背景音樂,音頻輸出是靜態的,比如微風雨滴聲、鬧市聲等充斥整個場景的背景音效。
對於環境音效的開發,我們可以簡單的使用<audio>標籤進行循環播放。
空間化音頻
音頻作用在空間的實體上,具有發聲體和聽者的位置關係,音頻輸出會根據發聲體與用戶的距離、方向動態變化,它模擬了現實中聲音的傳播方式,具有空間感。
實現原理:在虛擬場景中,通過調節音頻的振幅來描述發聲體與聽者之間的距離,再通過調節左右通道(audio channel)之間的差異,控制左右耳機喇叭輸出,來描述發聲體相對聽者的方位。
- 從發聲體與用戶兩點間的距離來看,如距離越遠,音頻音量(振幅)應越小;
- 從發聲體與用戶的方向來看,如發聲體位於聽者左側,則音頻輸出的左聲道應比右聲道音量大。

形如音頻空間化此類稍複雜的音頻的處理,可通過Web Audio API來實現。
Web Audio API 簡介
Web Audio API提供了一個功能強大的音頻處理系統,允許我們在瀏覽器中通過js來實時控制處理音頻,比如音頻可視化、音頻混合等。

Web Audio處理流程可以比喻成一個加工廠對聲源的加工,這個加工廠由多個加工模塊AudioNode
連接而成,音頻源經過一系列的處理加工後,被輸送至揚聲器。
AudioContext
類似於canvas
的context
上下文環境,它代表了一個audio加工廠控制中心,負責各個audioNode的創建和組合,通過new AudioContext()
的方式創建。
AudioNode
AudioNode音頻節點,則是加工廠的加工模塊, 按照功能可分為三類:輸入結點、處理結點、輸出結點。每個結點都擁有connect
方法連接下一個節點,將音頻輸出到下一個模塊。
- 輸入結點主要負責加載解碼音頻源,比如獲取二進制音頻源的
BufferSourceNode
、獲取<audio>音頻源的MediaElementSourceNode
等; - 處理結點主要對音頻數據進行計算處理,比如處理音頻振幅的
GainNode
等; - 輸出結點則將音頻輸出至揚聲器或耳機,
AudioContext.destination
便是默認的輸出節點。
一個簡單的音頻處理流程只需要分為四步:
- 創建音頻上下文
- 創建並初始化輸入結點、處理結點
- 將輸入結點、處理結點、輸出結點進行有連接
- 動態修改結點屬性以輸出不同音效
參考以下代碼:
constmyAudio=document.querySelector('audio');constaudioCtx=newAudioContext();//創建音頻上下文//創建輸入結點,解碼audio標籤的音頻源;創建處理結點,處理音頻constsource=audioCtx.createMediaElementSource(myAudio);constgainNode=audioCtx.createGain();//創建GainNode結點控制音頻振幅//將輸入結點、處理結點、輸出結點兩兩相連source.connect(gainNode);//將輸入結點連接到gainNode處理結點gainNode.connect(audioCtx.destination);//將gainNode連接到destination輸出節點//通過動態改變結點屬性產生不同音效source.start(0);//播放音頻gainNode.gain.value=val;//設置音量
理解了Web Audio的開發流程,接下來看看如何在WebVR中實現Audio Spatialization,這裡VR場景使用three.js進行開發。
實現空間化音頻
Audio Spatialization的實現主要通過AudioListener
和PannerNode
結點配合,這兩個對象可以根據空間方位訊息動態處理音頻源,並輸出左右聲道。
AudioListener
對象代表三維空間中的聽者(用戶),通過AudioContext.listener
屬性獲取;-
PannerNode
對象指的是三維空間中的發聲體,通過AudioContext.createPanner()
創建。
我們需要初始化這兩個對象,並將空間方位訊息作為入參動態傳給它們。
設置PannerNode
constmyAudio=document.querySelector('audio');constaudioCtx=newAudioContext();//創建音頻上下文constsource=audioCtx.createMediaElementSource(myAudio);//設置PannerNode位置constpanner=audioCtx.createPannerNode();panner.setPosition(speaker.position.x,speaker.position.y,speaker.position.z);//將發聲體坐標傳給PannerNodesource.connect(panner);//將輸入結點連接到PannerNode處理結點panner.connect(audioCtx.destination);// PannerNode連接至輸出結點source.start(0);//播放音頻
設置AudioListener
VR用戶頭顯最多有6-Dof:position位置3-Dof系統和orientation方向3-Dof系統,我們需要將這6-Dof的訊息傳入AudioListener,由它為我們處理音頻數據。
對於用戶位置數據,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
獲取相機的旋轉(四元數)矩陣,與初始向量相乘,得到當前Up向量和Forward向量,代碼如下:
//計算當前listener的forward向量letforward=newTHREE.Vector3(0,0,-1);forward.applyQuaternion(camera.quaternion);// forward初始向量與camera四元數矩陣相乘,得到當前的forward向量forward.normalize();//向量歸一//賦值給AudioListener的forward分量listener.forwardX.value=forward.x;listener.forwardY.value=forward.y;listener.forwardZ.value=forward.z;//計算當前listener的up向量letup=newTHREE.Vector3(0,1,0);up.applyQuaternion(camera.quaternion);// up初始向量與camera四元數矩陣相乘,得到當前的up向量up.normalize();//向量歸一//賦值給AudioListener的up分量listener.upX.value=up.x;listener.upY.value=up.y;listener.upZ.value=up.z;
WebVR實現音頻角色
在VR場景中,根據音頻的發起方和接收方可以分為兩個角色:Speaker發聲體與Listener聽者,即用戶。

一個VR場景音頻角色由一個Listener和多個Speaker組成,於是筆者將PannerNode
和AudioListener
進行獨立封裝,整合為Speaker類和Listener對象。
PS:這裡沿用前幾期three.js開發WebVR的方式,可參考《WebVR開發——標準教程》
Speaker實現
Speaker類代表發聲體,主要做了以下事情:
- 初始化階段加載解析音頻源,創建並連接輸入結點、處理結點、輸出結點
- 提供
update
公用方法,在每一幀中更新PannerNode位置。
classSpeaker{constructor(ctx,path){this.path=path;this.ctx=ctx;this.source=ctx.createBufferSource();this.panner=ctx.createPanner();this.source.loop=true;//設置音頻循環播放this.source.connect(this.panner);//將輸入結點連至PannerNodethis.panner.connect(ctx.destination);//將PannerNode連至輸出結點this._processAudio();//異步函數,請求與加載音頻數據}update(position){const{panner}=this;panner.setPosition(position.x,position.y,position.z);//將發聲體坐標傳給PannerNode}_loadAudio(path){//使用fetch請求音頻文件returnfetch(path).then(res=>res.arrayBuffer());}async_processAudio(){const{path,ctx,source}=this;try{constdata=awaitthis._loadAudio(path);//異步請求音頻constbuffer=awaitctx.decodeAudioData(data);//解碼音頻數據source.buffer=buffer;//將解碼數據賦值給BufferSourceNode輸入結點source.start(0);//播放音頻}catch(err){console.err(err);}}}
這裡初始化的流程跟前面略有不同,這裡使用的是fetch請求音頻文件,通過BufferSourceNode
輸入結點解析音頻數據。update
方法傳入發聲體position,設置PannerNode
位置。
Listener實現
Listener對象代表聽者,提供update
公用方法,在每幀中傳入AudioListener
的位置和方向。
//創建Listener對象constListener={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;//計算當前listener的forward向量letforward=newTHREE.Vector3(0,0,-1);forward.applyQuaternion(quaternion);forward.normalize();listener.forwardX.value=forward.x;listener.forwardY.value=forward.y;listener.forwardZ.value=forward.z;//計算當前listener的up向量letup=newTHREE.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和四元數矩陣,設置AudioListener
位置、方向。
接下來,將Listener和Speaker引入到WebVR應用中,下面例子描述了這樣一個簡陋場景:一輛狂響喇叭的汽車從你身旁經過,並駛向遠方。
classWebVRApp{...start(){const{scene,camera}=this;...//創建燈光、地面//創建一輛簡陋小車constgeometry=newTHREE.CubeGeometry(4,3,5);constmaterial=newTHREE.MeshLambertMaterial({color:0xef6500});this.car=newTHREE.Mesh(geometry,material);this.car.position.set(-12,2,-100);scene.add(this.car);constctx=newAudioContext();//創建AudioContext上下文Listener.init(ctx);//初始化listenerthis.car_speaker=newSpeaker(ctx,'audio/horn.wav');//創建speaker,傳入上下文和音頻路徑}}
首先在start
方法創建小汽車,接著初始化Listener並創建一個Speaker。
classWebVRApp{...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);}}newWebVRApp();
在動畫渲染update
方法中,更新小汽車的位置,並調用Speaker和Listener的update方法,傳入小汽車的位置、用戶的位置和旋轉矩陣,更新音頻空間訊息。
範例: https:// yonechen.github.io/WebV R-helloworld/examples/3d-audio.html
源碼: https:// github.com/YoneChen/Web VR-helloworld/blob/master/examples/3d-audio.html
小結
本文主要講解了WebVR應用音頻空間化的實現步驟,核心是運用了Web Audio API的PannerNode
和AudioListener
兩個對象處理音頻源,文末展示了VR Audio的一個簡單代碼例子,three.js本身也提供了完善的音頻空間化支持,可以參考PositinalAudio 。
更多文章可關注
WebVR開發教程——深度剖析關於WebVR的開發調試方案以及原理機制
WebVR開發教程——標准入門使用Three.js開發WebVR場景的入門教程