WebVR開發教程——Web Audio實現3D音效

blank

WebVR開發教程——Web Audio實現3D音效

在VR開發中,除了圖形視覺渲染,音頻處理是重要的一環,好的音頻處理可以欺騙用戶的聽覺,達到身臨其境的效果,本文主要介紹WebVR音頻是如何開發的。

VR Audio

VR音頻的輸出硬件主要是耳機,根據音頻源與場景之間的關係,可將VR音頻分為兩類:靜態音頻和空間化音頻(audio spatialization)。

靜態音頻

這類音頻作用於整個VR場景,可簡單的理解成背景音樂,音頻輸出是靜態的,比如微風雨滴聲、鬧市聲等充斥整個場景的背景音效。
對於環境音效的開發,我們可以簡單的使用<audio>標籤進行循環播放。

空間化音頻

音頻作用在空間的實體上,具有發聲體和聽者的位置關係,音頻輸出會根據發聲體與用戶的距離、方向動態變化,它模擬了現實中聲音的傳播方式,具有空間感。

實現原理:在虛擬場景中,通過調節音頻的振幅來描述發聲體與聽者之間的距離,再通過調節左右通道(audio channel)之間的差異,控制左右耳機喇叭輸出,來描述發聲體相對聽者的方位。

  • 從發聲體與用戶兩點間的距離來看,如距離越遠,音頻音量(振幅)應越小;
  • 從發聲體與用戶的方向來看,如發聲體位於聽者左側,則音頻輸出的左聲道應比右聲道音量大。

blank
3D立體音效原理

形如音頻空間化此類稍複雜的音頻的處理,可通過Web Audio API來實現。

Web Audio API 簡介

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

blank
Web Audio處理流程

Web Audio處理流程可以比喻成一個加工廠對聲源的加工,這個加工廠由多個加工模塊AudioNode連接而成,音頻源經過一系列的處理加工後,被輸送至揚聲器。

AudioContext

類似於canvascontext上下文環境,它代表了一個audio加工廠控制中心,負責各個audioNode的創建和組合,通過new AudioContext()的方式創建。

AudioNode

AudioNode音頻節點,則是加工廠的加工模塊, 按照功能可分為三類:輸入結點、處理結點、輸出結點。每個結點都擁有connect方法連接下一個節點,將音頻輸出到下一個模塊。

  • 輸入結點主要負責加載解碼音頻源,比如獲取二進制音頻源的BufferSourceNode 、獲取<audio>音頻源的MediaElementSourceNode等;
  • 處理結點主要對音頻數據進行計算處理,比如處理音頻振幅的GainNode等;
  • 輸出結點則將音頻輸出至揚聲器或耳機, AudioContext.destination便是默認的輸出節點。

一個簡單的音頻處理流程只需要分為四步:

  1. 創建音頻上下文
  2. 創建並初始化輸入結點、處理結點
  3. 將輸入結點、處理結點、輸出結點進行有連接
  4. 動態修改結點屬性以輸出不同音效

參考以下代碼:

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的實現主要通過AudioListenerPannerNode結點配合,這兩個對象可以根據空間方位訊息動態處理音頻源,並輸出左右聲道。

  • 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)。

blank
Forward向量與Up向量
  • 在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聽者,即用戶。

blank
Listener-Speaker的一對多關係

一個VR場景音頻角色由一個Listener和多個Speaker組成,於是筆者將PannerNodeAudioListener進行獨立封裝,整合為Speaker類和Listener對象。

PS:這裡沿用前幾期three.js開發WebVR的方式,可參考《WebVR開發——標準教程》

Speaker實現

Speaker類代表發聲體,主要做了以下事情:

  1. 初始化階段加載解析音頻源,創建並連接輸入結點、處理結點、輸出結點
  2. 提供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方法,傳入小汽車的位置、用戶的位置和旋轉矩陣,更新音頻空間訊息。

範例: yonechen.github.io/WebV
源碼: github.com/YoneChen/Web

小結

本文主要講解了WebVR應用音頻空間化的實現步驟,核心是運用了Web Audio API的PannerNodeAudioListener兩個對象處理音頻源,文末展示了VR Audio的一個簡單代碼例子,three.js本身也提供了完善的音頻空間化支持,可以參考PositinalAudio
更多文章可關注

WebVR開發教程——深度剖析關於WebVR的開發調試方案以及原理機制
WebVR開發教程——標准入門使用Three.js開發WebVR場景的入門教程

What do you think?

Written by marketer

blank

展望2018 年JavaScript Testing

blank

Lodash 源碼中的那些迷人的細節