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

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

VR Audio

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

靜態音頻

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

空間化音頻

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

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

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

3D立體音效原理

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

Web Audio API 簡介

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

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

AudioContext

相似於canvascontext上下文環境,它表明了一個audio加工廠控制中心,負責各個audioNode的建立和組合,經過new AudioContext()的方式建立。瀏覽器

AudioNode

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

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

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

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

參考如下代碼:

const myAudio = document.querySelector('audio');
const audioCtx = new AudioContext(); // 建立音頻上下文
 // 建立輸入結點,解碼audio標籤的音頻源;建立處理結點,處理音頻
const source = audioCtx.createMediaElementSource(myAudio);
const gainNode = 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

const myAudio = document.querySelector('audio');
const audioCtx = new AudioContext(); // 建立音頻上下文
const source = audioCtx.createMediaElementSource(myAudio);

const panner = audioCtx.createPannerNode();
panner.setPosition(speaker.position.x, speaker.position.y, speaker.position.z); // 將發聲體座標傳給PannerNode

source.connect(panner); // 將輸入結點鏈接到PannerNode處理結點
panner.connect(audioCtx.destination); 
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)。

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向量
    let forward = new THREE.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向量
    let up = new THREE.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聽者,即用戶。
Listener-Speaker的一對多關係
一個VR場景音頻角色由一個Listener和多個Speaker組成,因而筆者將PannerNodeAudioListener進行獨立封裝,整合爲Speaker類和Listener對象。
PS:這裏沿用前幾期three.js開發WebVR的方式,可參考《WebVR開發——標準教程》

Speaker實現

Speaker類表明發聲體,主要作了如下事情:

  1. 初始化階段加載解析音頻源,建立並鏈接輸入結點、處理結點、輸出結點
  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; // 設置音頻循環播放
        this.source.connect(this.panner); // 將輸入結點連至PannerNode
        this.panner.connect(ctx.destination); // 將PannerNode連至輸出結點
        this._processAudio(); // 異步函數,請求與加載音頻數據
    }
    update(position) {
        const { panner } = this;
        panner.setPosition(position.x, position.y, position.z); // 將發聲體座標傳給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); // 解碼音頻數據
            source.buffer = buffer; // 將解碼數據賦值給BufferSourceNode輸入結點
            source.start(0); // 播放音頻
        } catch(err) {
            console.err(err);
        }
    }
}

這裏初始化的流程跟前面略有不一樣,這裏使用的是fetch請求音頻文件,經過BufferSourceNode輸入結點解析音頻數據。
update方法傳入發聲體position,設置PannerNode位置。

Listener實現

Listener對象表明聽者,提供update公用方法,在每幀中傳入AudioListener的位置和方向。

// 建立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;
    // 計算當前listener的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;
    // 計算當前listener的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和四元數矩陣,設置AudioListener位置、方向。

接下來,將Listener和Speaker引入到WebVR應用中,下面例子描述了這樣一個簡陋場景:一輛狂響喇叭的汽車從你身旁通過,並駛向遠方。

class WebVRApp {
...
  start() {
      const { scene, camera } = this;
      ... // 建立燈光、地面
      // 建立一輛簡陋小車
      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(); // 建立AudioContext上下文
      Listener.init(ctx); // 初始化listener
      this.car_speaker = new Speaker(ctx,'audio/horn.wav'); // 建立speaker,傳入上下文和音頻路徑

  }
}

首先在start方法建立小汽車,接着初始化Listener並建立一個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方法中,更新小汽車的位置,並調用Speaker和Listener的update方法,傳入小汽車的位置、用戶的位置和旋轉矩陣,更新音頻空間信息。
示例地址:https://yonechen.github.io/We...(須要支持es7的瀏覽器如新版chrome,太懶沒作打包編譯?)
源碼地址:https://github.com/YoneChen/W...

小結

本文主要講解了WebVR應用音頻空間化的實現步驟,核心是運用了Web Audio API的PannerNodeAudioListener兩個對象處理音頻源,文末展現了VR Audio的一個簡單代碼例子,three.js自己也提供了完善的音頻空間化支持,能夠參考PositinalAudio
最近筆者正在實現WebVR多人聊天室,下期文章圍繞此展開,敬請期待~
更多文章可關注WebVR技術莊園
WebVR開發教程——交互事件(二)使用Gamepad
WebVR開發教程——深度剖析 關於WebVR的開發調試方案以及原理機制
WebVR開發教程——標準入門 使用Three.js開發WebVR場景的入門教程

相關文章
相關標籤/搜索