深刻淺出 Web Audio Api

題圖:Egor Khomiakovcss

注:本文同時發佈在 知乎專欄html

什麼是 Web Audio Api

首先引用一下 MDN 上對 Web Audio Api 的一段描述:html5

The Web Audio API involves handling audio operations inside an audio context, and has been designed to allow modular routing. Basic audio operations are performed with audio nodes, which are linked together to form an audio routing graph.node

大體的意思就是 Web Audio API 須要在音頻上下文中處理音頻的操做,並具備模塊化路由的特色。基本的音頻操做是經過音頻節點來執行的,這些音頻節點被鏈接在一塊兒造成音頻路由圖。webpack

咱們能夠從上面這段文字中提取出幾個關鍵詞:git

  • 音頻上下文
  • 音頻節點
  • 模塊化
  • 音頻圖

我將會以這些關鍵詞爲開始,慢慢介紹什麼是 Web Audio Api,如何使用 Web Audio Api 來處理音頻等等。github

音頻上下文(AudioContext)

音頻中的 AudioContext 能夠類比於 canvas 中的 context,其中包含了一系列用來處理音頻的 API,簡而言之,就是能夠用來控制音頻的各類行爲,好比播放、暫停、音量大小等等等等。建立音頻的 context 比建立 canvascontext 簡單多了(考慮代碼的簡潔性,下面代碼都不考慮瀏覽器的兼容狀況):web

const audioContext = new AudioContext();複製代碼

在繼續瞭解 AudioContext 以前,咱們先來回顧一下,平時咱們是如何播放音頻的:ajax

<audio autoplay src="path/to/music.mp3"></audio>複製代碼

或者:canvas

const audio = new Audio();
audio.autoplay = true;
audio.src = 'path/to/music.mp3';複製代碼

沒錯,很是簡單的幾行代碼就實現了音頻的播放,可是這種方式播放的音頻,只能控制播放、暫停等等一些簡單的操做。可是若是咱們想要控制音頻更「高級」的屬性呢,好比聲道的合併與分割、混響、音調、聲相控制和音頻振幅壓縮等等,能夠作到嗎?答案固然是確定的,一切都基於 AudioContext。咱們以最簡單的栗子來了解一下 AudioContext 的用法:

const URL = 'path/to/music.mp3';
const audioContext = new AudioContext();
const playAudio = function (buffer) {
    const source = audioContext.createBufferSource();
    source.buffer = buffer;
    source.connect(audioContext.destination);
    source.start();
};
const getBuffer = function (url) {
    const request = new XMLHttpRequest();
    return new Promise((resolve, reject) => {
        request.open('GET', url, true);
        request.responseType = 'arraybuffer';
        request.onload = () => {
            audioContext.decodeAudioData(request.response, buffer => buffer ? resolve(buffer) : reject('decoding error'));
        };
        request.onerror = error => reject(error);
        request.send();
    });
};
const buffer = await getBuffer(URL);
buffer && playAudio(buffer);複製代碼

別方,這個栗子真的是最簡單的栗子了(儘可能寫得簡短易懂了),其實仔細看下,代碼無非就作了三件事:

  • 經過 ajax 把音頻數據請求下來;
  • 經過 audioContext.decodeAudioData() 方法把音頻數據轉換成咱們所須要的 buffer 格式;
  • 經過 playAudio() 方法把音頻播放出來。

你沒猜錯,達到效果和剛剛提到的播放音頻的方式一毛同樣。這裏須要重點講一下 playAudio 這個函數,我提取出了三個關鍵點:

  • source
  • connect
  • destination

你能夠試着以這種方式來理解這三個關鍵點:首先咱們經過 audioContext.createBufferSource() 方法建立了一個「容器」 source 並裝入接收進來的「水」 buffer;其次經過「管道」 connect 把它和「出口」 destination 鏈接起來;最終「出口」 destination 「流」出來的就是咱們所聽到的音頻了。不知道這麼講,你們有沒有比較好理解。

AudioContext
AudioContext

或者也能夠拿 webpack 的配置文件來類比:

module.exports = {
    // source.buffer
    entry: 'main.js',
    // destination
    output: {
        filename: 'app.js',
        path: '/path/to/dist',
    },
};複製代碼

sourcedestination 分別至關於配置中的入口文件和輸出文件,而 connect 至關於 webpack 內置的默認 loader,負責把源代碼 buffer 生成到輸出文件中。

重點理解這三個關鍵點的關係

注意:Audio 和 Web Audio 是不同的,它們之間的關係大概像這樣:

Web audio API and Audio
Web audio API and Audio

Audio:

  • 簡單的音頻播放器;
  • 「單線程」的音頻;

Web Audio:

  • 音頻合成;
  • 能夠作音頻的各類處理;
  • 遊戲或可交互應用中的環繞音效;
  • 可視化音頻等等等等。

音頻節點(AudioNode)

到這裏,你們應該大體知道了如何經過 AudioContext 去控制音頻的播放。可是會發現寫了這麼一大堆作的事情和前面提到的一行代碼的所作的事情沒什麼區別(<audio autoplay src="path/to/music.mp3"></audio>),那麼 AudioContext 具體是如何去處理咱們前面所提到的那些「高級」的功能呢?就是咱們接下來正要了解的 音頻節點

那麼什麼是音頻節點呢?能夠把它理解爲是經過「管道」 connect 鏈接在「容器」source 和「出口」 destination 之間一系列的音頻「處理器」。AudioContext 提供了許多「處理器」用來處理音頻,好比音量「處理器」 GainNode、延時「處理器」 DelayNode 或聲道合併「處理器」 ChannelMergerNode 等等。

前面所提到的「管道」 connect 也是由音頻節點 AudioNode 提供的,因此你猜的沒錯,「容器」 source 也是一種音頻節點。

const source = audioContext.createBufferSource();
console.log(source instanceof AudioNode); // true複製代碼

AudioNode 還提供了一系列的方法和屬性:

  • .context (read only): audioContext 的引用
  • .channelCount: 聲道數
  • .connect(): 鏈接另一個音頻節點
  • .start(): 開始播放
  • .stop(): 中止播放

更多詳細介紹可訪問 MDN 文檔

GainNode

GainNode
GainNode

前面有提到音頻處理是經過一個個「處理器」來處理的,那麼在實際應用中怎麼把咱們想要的「處理器」裝上去呢?

Don't BB, show me the code:

const source = audioContext.createBufferSource();
const gainNode = audioContext.createGain();
const buffer = await getBuffer(URL);

source.buffer = buffer;
source.connect(gainNode);
gainNode.connect(source.destination);

const updateVolume = volume => gainNode.gain.value = volume;複製代碼

能夠發現和上面提到的 playAudio 方法很像,區別只是 source 不直接 connect 到 source.destination,而是先 connect 到 gainNode,而後再經過 gainNode connect 到 source.destination。這樣其實就把「音量處理器」裝載上去了,此時咱們經過更新 gainNode.gain.value 的值(0 - 1 之間)就能夠控制音量的大小了。

Full Demo

BiquadFilterNode(waiting for perfection)

BiquadFilterNode
BiquadFilterNode

不知道怎麼翻譯這個「處理器」,暫且叫作低階濾波器吧,簡單來講它就是一個經過過濾音頻的數字信號進而達到控制 音調 的音頻節點。把它裝上:

const filterNode = audioContext.createBiquadFilter();
// ...
source.connect(filterNode);
filterNode.connect(source.destination);

const updateFrequency = frequency => filterNode.frequency.value = frequency;複製代碼

這樣一來咱們就能夠經過 updateFrequency() 方法來控制音頻的音調(頻率)了。固然,除了 frequency 咱們還能夠調整的屬性還有(MDN Docs):

  • .Q: quality factor;
  • .type: lowpass, highpass, bandpass, lowshelf, highshelf, peaking, notch, allpass;
  • .detune: detuning of the frequency in cents.

Full Demo

PannerNode

咱們能夠調用 PannerNode.setPosition() 方法來作出很是有意思的 3D 環繞音效:

<input type="range" name="rangeX" value="0" max="10" min="-10">複製代碼
const rangeX = document.querySelector('input[name="rangeX"]');
const source = audioContext.createBufferSource();
const pannerNode = audioContext.createPanner();

source.connect(pannerNode);
pannerNode.connect(source.destination);

rangeX.addEventListener('input', () => pannerNode.setPosition(rangeX.value, 0, 0));複製代碼

仍是老方法「裝上」 PannerNode 「處理器」,而後經過監聽 range 控件的 input 事件,經過 .setPosition() 方法更新 聲源相對於聽音者的位置,這裏我只簡單的更新了聲源相對於聽音者的 X 方向上的距離,當值爲負值時,聲音在左邊,反之則在右邊。

你能夠這麼去理解 PannerNode,它把你(聽音者)置身於一個四面八方都很是空曠安靜的空間中,其中還有一個音響(聲源),而 .setPosition() 方法就是用來控制 音響 在空間中 相對於你(聽音者) 的位置的,因此上面這段代碼能夠控制聲源在你左右倆耳邊來回晃動(帶上耳機)。

Full Demo

固然,對於 PannerNode 來講,還有許多屬性可使得 3D 環繞音效聽上去更逼真,好比:

  • .distanceModel: 控制音量變化的方式,有 3 種可能的值:linear, inverseexponential
  • .maxDistance: 表示 聲源聽音者 之間的最大距離,超出這個距離後,聽音者將再也不能聽到聲音;
  • .rolloffFactor: 表示當 聲源 遠離 聽音者 的時候,音量以多快的速率減少;

這裏只列舉了經常使用的幾個,若是想進一步瞭解 PannerNode 能作什麼的話,能夠查閱 MDN 上的 文檔

多個音頻源

前面有提到過,在 AudioContext 中能夠同時使用多個「處理器」去處理一個音頻源,那麼多個音頻源 source 能夠同時輸出嗎?答案固然也是確定的,在 AudioContext 中能夠有多個音頻處理通道,它們之間互不影響:

cross fading
cross fading

const sourceOne = audioContext.createBufferSource();
const sourceTwo = audioContext.createBufferSource();
const gainNodeOne = audioContext.createGain();
const gainNodeTwo = audioContext.createGain();

sourceOne.connect(gainNodeOne);
sourceTwo.connect(gainNodeTwo);
gainNodeOne.connect(audioContext.destination);
gainNodeTwo.connect(audioContext.destination);複製代碼

Full Demo

模塊化(Modular)

Modular
Modular

經過前面 音頻節點 的介紹,相信大家已經感覺到了 Web Audio 的模塊化設計了,它提供了一種很是方便的方式來爲音頻裝上(connect)不一樣的「處理器」 AudioNode。不只一個音頻源可使用多個「處理器」,而多個音頻源也能夠合併爲一個「輸出」 destination

得益於 Web Audio 的模塊化設計,除了上面提到的模塊(AudioNode),它還提供了很是多的可配置的、高階的、開箱即用的模塊。因此經過使用這些模塊,咱們徹底能夠建立出功能豐富的音頻處理應用。

若是你對 AudioContextAudioNode 之間的關係尚未一個比較清晰的概念的話,就和前面一開始所說的那樣,把它們和 webpack 和 loader 作類比,AudioContext 和 webpack 至關於一個「環境」,模塊(AudioNodeloader)能夠很方便在「環境」中處理數據源(AudioContext 中的 buffer 或 webpack 中的 js, css, image 等靜態資源),對好比下:

module.exports = {
    entry: {
        // 多音頻源合併爲一個輸出
        app: ['main.js'], // source.buffer
        vender: ['vender'], // source.buffer
    },
    output: { // source.destination
        filename: 'app.js',
        path: '/path/to/dist',
    },
    // AudioNode
    module: {
        rules: [{
            // source.buffer
            test: /\.(scss|css)$/,
            // AudioNode: GainNode, BiquadFilterNode, PannerNode ...
            use: ['style-loader', 'css-loader', 'sass-loader'],
        }],
    },
};複製代碼

再次發現,Web Audio Api 和 webpack 的設計理念如此的類似。

音頻圖(Audio Graph)

Audio Graph
Audio Graph

An audio graph is a set of interconnected audio nodes.

如今咱們知道了,音頻的處理都是經過 音頻節點 來處理的,而多個音頻節點 connect 到一塊兒就造成了 音頻導向圖(Audio Routing Graph),簡而言之就是多個相互鏈接在一塊兒的音頻節點。

總結

本文展現的僅僅只是 Web Audio 衆多 API 中的冰山一角,若是想更深刻了解 Web Audio 的話,建議能夠去查閱相關文檔。儘管如此,利用上面介紹的一些 API 也足夠作出一些有意思的音樂效果來了。

參考資料

  1. Web Audio Api - MDN
  2. Getting Started with Web Audio API
相關文章
相關標籤/搜索