WebVR開發(fā)——Web Audio實現(xiàn)3D音效

在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ā)聲體位于聽者左側,則音頻輸出的左聲道應比右聲道音量大羞迷。


    3D立體音效原理

形如音頻空間化此類稍復雜的音頻的處理界轩,可通過Web Audio API來實現(xiàn)。

Web Audio API 簡介

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


Web Audio處理流程可以比喻成一個加工廠對聲源的加工与殃,這個加工廠由多個加工模塊AudioNode連接而成,音頻源經(jīng)過一系列的處理加工后碍现,被輸送至揚聲器幅疼。

AudioContext

類似于canvascontext上下文環(huán)境,它代表了一個audio加工廠控制中心昼接,負責各個audioNode的創(chuàng)建和組合爽篷,通過new AudioContext()的方式創(chuàng)建。

AudioNode

AudioNode音頻節(jié)點慢睡,則是加工廠的加工模塊逐工, 按照功能可分為三類:輸入結點、處理結點漂辐、輸出結點泪喊。每個結點都擁有connect方法連接下一個節(jié)點,將音頻輸出到下一個模塊髓涯。

  • 輸入結點主要負責加載解碼音頻源袒啼,比如獲取二進制音頻源的BufferSourceNode、獲取<audio>音頻源的MediaElementSourceNode等纬纪;
  • 處理結點主要對音頻數(shù)據(jù)進行計算處理蚓再,比如處理音頻振幅的GainNode等;
  • 輸出結點則將音頻輸出至揚聲器或耳機包各,AudioContext.destination便是默認的輸出節(jié)點摘仅。

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

  1. 創(chuàng)建音頻上下文
  2. 創(chuàng)建并初始化輸入結點、處理結點
  3. 將輸入結點问畅、處理結點娃属、輸出結點進行有連接
  4. 動態(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)主要通過AudioListenerPannerNode結點配合豺旬,這兩個對象可以根據(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)宇植。


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

Listener-Speaker的一對多關系

一個VR場景音頻角色由一個Listener和多個Speaker組成底哗,于是筆者將PannerNodeAudioListener進行獨立封裝,整合為Speaker類和Listener對象锚沸。
PS:這里沿用前幾期three.js開發(fā)WebVR的方式跋选,可參考《WebVR開發(fā)——標準教程》

Speaker實現(xiàn)

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

  1. 初始化階段加載解析音頻源哗蜈,創(chuàng)建并連接輸入結點前标、處理結點、輸出結點
  2. 提供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的PannerNodeAudioListener兩個對象處理音頻源锰扶,文末展示了VR Audio的一個簡單代碼例子献酗,three.js本身也提供了完善的音頻空間化支持,可以參考PositinalAudio坷牛。
最近筆者正在實現(xiàn)WebVR多人聊天室罕偎,下期文章圍繞此展開,敬請期待~
更多文章可關注WebXR技術莊園
WebVR開發(fā)教程——交互事件(二)使用Gamepad
WebVR開發(fā)教程——深度剖析 關于WebVR的開發(fā)調(diào)試方案以及原理機制
WebVR開發(fā)教程——標準入門 使用Three.js開發(fā)WebVR場景的入門教程

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末京闰,一起剝皮案震驚了整個濱河市颜及,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蹂楣,老刑警劉巖俏站,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異痊土,居然都是意外死亡肄扎,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門赁酝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來犯祠,“玉大人,你說我怎么就攤上這事酌呆『庠兀” “怎么了?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵隙袁,是天一觀的道長痰娱。 經(jīng)常有香客問我,道長菩收,這世上最難降的妖魔是什么梨睁? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮娜饵,結果婚禮上坡贺,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好拴念,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著褐缠,像睡著了一般政鼠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上队魏,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天公般,我揣著相機與錄音,去河邊找鬼胡桨。 笑死官帘,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的昧谊。 我是一名探鬼主播刽虹,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼呢诬!你這毒婦竟也來了涌哲?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤尚镰,失蹤者是張志新(化名)和其女友劉穎阀圾,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體狗唉,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡初烘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了分俯。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肾筐。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖澳迫,靈堂內(nèi)的尸體忽然破棺而出局齿,到底是詐尸還是另有隱情,我是刑警寧澤橄登,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布抓歼,位于F島的核電站,受9級特大地震影響拢锹,放射性物質(zhì)發(fā)生泄漏谣妻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一卒稳、第九天 我趴在偏房一處隱蔽的房頂上張望蹋半。 院中可真熱鬧,春花似錦充坑、人聲如沸减江。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辈灼。三九已至份企,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間巡莹,已是汗流浹背司志。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留降宅,地道東北人骂远。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像腰根,于是被迫代替她去往敵國和親激才。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348