Web Audio API 第1章 基礎篇

池中物王二狗發表於2024-03-14

webaudioapi

Web Audio API 第1章 基礎篇

我查了一下 Web Audio API 蝙蝠書居然在 2013 年就出版了

我又看了一下我的“豆瓣讀書”頻道內,這本書加入到“在讀”標籤是在 2021 年了

一直沒有堅持看這本書的原因有兩點,

一是本書是英文版的,不像中文看的那麼流暢

二是在前端開發業務中一直沒有遇到複雜到需要用到 Web Audio API 的場景

這不在 2023 年末業務上就遇上了需要處理音訊的業務麼,督促自己看下去,順便翻譯了

因為此書已是 10 年前寫的了,程式碼有些可能比較古早,我將會用 JavaScript 重新實現相關 demo

demo 會放在 https://github.com/willian12345/WebAudioAPI/tree/master/examples

我並非音樂相關專業人士,所以對一些專業用語可能翻的沒那麼專業,但這不重要,重要的就是先過一遍

對 Web Audio API 能做啥有個印象,用到的時候再仔細深入

總之沒用的知識又可以增加了!!
譯者 (王二狗 Sheldon 池中物) 注

正文開始

此章將描述從何處入手 Web Audio API, 哪些瀏覽器支援,如何檢測哪些 API 可用,什麼是音訊視覺化,音訊節點是什麼,如何將音訊節點組合連線在一起,介紹一些基礎的音訊節點型別,最後載入音訊並播放

Audio 音訊在網頁中的歷史小知識

最早在網頁中播放音訊使用的是 標籤,當有人訪問網頁時,網頁創作者使用此標籤就可自動播放背景音訊。這個特性只能 IE 瀏覽器上可用,它從未被標準化或被其它瀏覽器所實現過。網景瀏覽器實現了一個類似的 標籤, 提供了相似的播放功能。

Flash 最早在網頁中實現了跨瀏覽器播放音訊的功能,但它有個比較大的缺點就是需要以外掛方式執行。最近瀏覽器開發商都投向了 HTML5

儘管在網頁上播放音訊不再需要外掛了,但

  • 沒有精確的可控定時器
  • 一次可播放的音訊數量太少
  • 沒有可靠的預緩衝能力
  • 無法應用實時音訊特效
  • 沒有可用的方法對音訊進行分析

他們多次嘗試推出強大的 audio API 來解決我上面提到的這些音訊限制。推出的這些 API 中值得注意的就是火狐瀏覽器設計的 Audio Data API 原型實現。Mozilla 試圖用

相比於 Audio Data API, Web Audio API 使用了全新的模式,完全獨立於

遊戲與互動

音訊是互動體驗的重要組成部分。如果你不信,試著觀看一部關掉聲音的電影。

在遊戲中更是如此!我最愛的電子遊戲令我記憶最深的就是其中的音樂與音效。就算過了將近20年,塞爾達和暗黑破壞神的音效在我的腦海中仍然揮之不去。
無論是暴雪的《魔獸爭霸》和《星際爭霸》系列中的圈點士兵音效還是任天堂經典遊戲中的各種音效,這些精心設計的遊戲音效立馬就可以被識別出來。

音效在遊戲外的應用也同樣重要。它們在命令列工具內互動之始就被應用於 UI 的互動上,當輸出出錯時就發出“嗶”的一聲。同樣被應用現代互動 UI 上,一般用於提醒功能,鈴聲,也應用於音影片通訊比如 Skype。像 Google Now 和 Siri 這樣的助手軟體同樣提供了豐富的音效反饋。當我們深入發掘這個世界,通用計算裝置,語音和手勢互動等可脫離螢幕的互動方式更加的依賴於音訊的反饋。最後,對於視障視弱的計算機使用者來說,音訊提示,語音合成與識別提供了最主要的使用者體驗原則

可互動的音訊也代表著一些有趣的挑戰。為了建立合適的遊戲音訊,設計師需要調整好遊戲玩家所有不可預知的狀態。在實踐中,遊戲中某部分的時長可能是未知的,音效與周邊環境互動產生更復雜的混音,需要環境特效音且取決於相關的音訊位置。最終可能同一時刻播放著一堆音訊,全都得組合在一起的音效即保證質量又不影響渲染效能

Audio Context

Web Audio API 是建立在 audio context 音訊上下文的概念之上的。音訊上下文是描述音訊所有節點的,這些節點定義了音訊流是如何從起點(一般是音訊檔案)到目的地(一般就是你的揚聲器)。當音訊透過每個節點時,音訊的屬性可被修改和檢視。最簡單的音訊上下文就是起始節點到結束節點的直連如圖 1-1

圖 1-1

一個音訊上下文可能會非常複雜,在音訊的起點與結束節點之間包含眾多的音訊節點(圖1-2)進行任意眾多的高階合成或分析。

圖 1-1 和 1-2 方塊表示音訊節點。箭頭代表節點之間的連線。這些節點經常擁有眾多的輸入和輸出連線。預設情況下,如果一個音訊節點擁有多個輸入源,Web Audio API 會簡單的混成一個音訊訊號。

音訊節點圖的概念並不新鮮,它來源於一些流行的音訊處理框架,如 Apple 的 CoreAudio, 它提供了類似的音訊處理 影像 API。它本身的概念很久遠了,可追溯到1960年代的早期處理音訊,比如 Moog 模組化混成系統(Moog modular synthesizer systems)。

圖 1-2

初始化音訊上下文

Web Audio API 現已被 Chrome 和 Safari 瀏覽器實現(包含 IOS 6 手機版 Safari )並且透過 JavaScript 開放給了網頁開發者。在這些瀏覽器上,音訊上下文建立時需要加上 webkit 字首,你不能直接使用 new AudioContext 而應該使用 new webkitAudioContext 建立。然而在未來在 API 足夠穩定且多數瀏覽器供應商都實現了後字首可去掉。Mozilla 已宣佈在火狐瀏覽器上實現 Web Audio API,Opera 也開始參與進工作組。記得這一點即可,下面是通用的相容寫法:

var contextClass = (window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.oAudioContext || window.msAudioContext);
if (contextClass) {
  // Web Audio API is available. 
  var context = new contextClass();
} else {
  // Web Audio API is not available. Ask the user to use a supported browser.
}

一個音訊上下文即可支援多個音訊的輸入與複雜的音訊圖譜,一般來講,一個應用使用一個音訊上下文即可。音訊上下文的例項包含了眾多的方法用於建立音訊節點以及操作全域性音訊屬性。幸運的是這些方法不需要加字首,且相對穩定。 API 仍在變更,所以還是要小心會有大的變化。

音訊節點的型別

音訊上下文主要的一個功能就是建立新的音訊節點。廣義上來講,一般包含以下幾個節點型別:

  • 源節點(Source nodes)
    音源,如音訊緩衝,直播音訊輸入,

  • 修飾節點(Modification nodes)
    Filters, convolvers, panners, JS 處理器 等等

  • 分析節點(Analysis nodes)
    分析器, JS 處理器

  • 結束節點(Destination nodes)
    音訊輸出和結束處理快取

音訊源不限於音訊檔案,可用直播音訊流或麥克風,

連線音訊圖

使用 connect() 方法可以將任意音訊節點輸出連線至任意其它的音訊節點。在以下例子,我們連線了音源節點的輸出到 gain 節點,並將其輸出連線到了結束節點:

// Create the source.
var source = context.createBufferSource(); 
// Create the gain node.
var gain = context.createGain();
// Connect source to filter, filter to destination.
source.connect(gain);
gain.connect(context.destination);

注意,context.destination 是一個特殊的節點,一般為系統的預設音訊輸出。上述程式碼形成的音訊圖如下 1-3:

圖 1-3

一旦像上面這樣我們連線了音訊圖就可以動態改變它。我們可以呼叫 node.disconnect(outputNumber) 來斷開節點連線。例如,重新直連源音訊節點至結束音訊節點,避開中間 gain 節點 程式碼如下:

source.disconnect(0);
gain.disconnect(0);
source.connect(context.destination);

模組化路由的力量

在很多遊戲中,通常要將多個音源最終混成一個聲音。這些音源包含了背景音樂,遊戲音效,UI反饋音,在多人遊戲中還有其它玩家的聊天音訊。Web Audio API 的一個非常重要的特性就是它允許你單獨且完整的控制某個音或者組合在一起控制。音訊圖看起來應該像下面這樣圖 1-4:

我們分別使用 gain 音訊節點將分開的聲道音源聯合在一起,並使用一個主 gain 節點來控制它們。 透過這樣的設定,非常方便按需精準控制單獨的聲道級別。例如,非常多的使用者在玩遊戲過程中更喜歡把背景音關掉。

重要理論:什麼是聲音?

就物理上而言,聲音是一種縱波(有時也被稱為壓力波)它們透過空氣或其它媒介傳播。 音源產生是由於空氣內分子之間的振動與碰撞。它們的聚散導致了不同區域的高低壓。如果你有能力凍結時間那麼就可以觀察到聲波影像,可能類似於圖 1-5

圖 1-5

圖 1-6

數學上來講,聲音可被表示為函式,透過時間軸上的壓力範圍值。 圖 1-6 展示了函式影像。 可以看到它模擬的是圖 1-5 ,值高的地方表示粒子稠密的區域(高壓),值低則表示粒子稀疏的區域(低壓)

首次捕獲並重建聲音的能力得追溯到 20 世紀初了。麥克風捕獲壓力波並轉化成電子訊號,(舉例)用 +5 伏電壓代表最高壓,-5 伏電壓代表最低壓。與之相反,音訊播放器獲取這些電壓並將其轉換回壓力波,這樣我們才能聽到。

無論我們是在分析聲音還是合成聲音, 對於音訊程式設計者來說感興趣的位元資訊都在黑盒內操作音訊訊號圖 1-7 。早期操作音訊這個空間被模擬濾波器和其他硬體佔據,按照今天的標準,這些硬體會被認為是過時的。現在有更多數字處理裝置代替老的模擬裝置。但在使用軟體處理音訊前,我們需要先將聲音轉換成電腦能處理的資訊

圖 1-7 聲音的記錄與回放

重要理論:什麼是數字聲音?

我們可以這樣做,對模擬訊號按時間進行一定頻率的取樣,對每個訊號進行編碼,編成一個數字。 對模擬訊號的取樣被稱為取樣率。一般音訊軟體處理通常取樣率為44.1kHz。這意味著每一秒對聲音進行記錄 44100 個數值。這些數值限定在某個範圍內。每個值通常分配一定數量的位,稱為位深度(bit depth)。對於大多數數字音訊錄製,包括CD,位深度 16 對於大多數人來說足夠了。

對於音樂發燒友來說,24位深度也夠了,已經有足夠的精度,使用再高的位深度人耳已很難區別開來了。

模擬訊號轉數字訊號被稱為數字化(或取樣),可抽象為 圖 1-8

圖 1-8

在圖 1-8 ,數字化訊號的版本的長條與模擬訊號版本光滑的曲線看起來區別非常大。隨著取樣率增高與取樣深度的增加區別會越來越小(藍色部分)。然而這些值的增加也意謂著更多儲存內空間的付出。為了節約空間,電話系統通常使用的取樣率低至 8 kHz, 因為使人類聲音清晰可聽的頻率範圍遠遠小於我們可聽到的全部頻率範圍.

對於聲音的數字化,計算機通常會像對待長數字的陣列一樣處理。這樣的編碼被稱為脈衝編碼調製 PCM(pulse-code modulation)。因為計算機非常善於處理陣列,PCM 對於大多數數字音訊應用來說是一個非常強大的基元。在 Web Audio API 的世界,長數字的陣列的聲音被表示為音訊緩衝(AudioBuffer). 音訊緩衝可以儲存多條音訊通道(通常在立體聲中,包含左聲道和右聲道)被表示為標準化從 -1 到 1 後的浮點數陣列。同樣的訊號當然也可以被表示為整形陣列,在 16 位長度下,範圍從 -215 至 215 - 1。

重要理論:音訊編碼格式

原始的音訊 PCM 格式非常巨大, 它會造成額外的儲存浪費,在下載時也會造成額外的頻寬浪費。正因如此,音訊通常會儲存為壓縮後的格式。有兩種型別的壓縮方式:有失真壓縮和無失真壓縮。無失真壓縮(如, FLAC)保證在壓縮與解壓後位元位資訊完全相同。有失真壓縮(如,MP3) 利用人類聽覺特性會丟棄掉部人類聽不到的分位元位資訊用於節約儲存空間。有失真壓縮對於大多數人來說足夠用了,音樂發燒友除外。

通常度量音訊壓縮量被稱作位元率,它代表著重放每秒音訊所需要的位元位數量。位元率越高單位時間內可利用的資料就越多所需的壓縮就越少(更精確)。通常有失真壓縮格式,比如 MP3 定義為它們的位元率(一般為 128 到 192 Kb/s)。有損格式的編解碼有可能使用任意的位元率。舉個例子,電話人聲音訊通常使用 8Kb/s MP3 格式。一些格式支援可變位元率如 OGG 格式。位元率會隨著時間變化而變。注意此處的位元率別和取樣率或位元深度混淆了

瀏覽器對不同音訊格式的支援差別很大。一般來說,如果是在瀏覽器上, Web Audio API 的實現與

Firefox Chrommium 現在是支援 mp3 格式的。

聲音的載入與播放

Web Audio API 將緩衝與音源(source)節點區別的很清晰。這樣的架構有利於解構音訊資源與播放狀態。以唱片機為例,緩衝區就像唱片,而源就像播放指標,而在 Web Audio API 的世界中你可以同時在多個播放指標處播放同一個唱片。由於許多應用程式涉及同一緩衝區的多個版本同時播放,因此這種模式是必不可少的。舉個例子,
如果您希望快速連續地發出多個彈跳球的聲音,則只需要載入一次彈跳緩衝並安排多個音源(source)。

在 Web Audio API 載入一個音訊樣本,我們可以使用一個 XMLHttpRequest 載入並對載入的結果用context.decodeAudioData進行音訊解碼處理。這些都是非同步的不會阻塞主 UI 程序:

var request = new XMLHttpRequest(); 
request.open('GET', url, true); 
request.responseType = 'arraybuffer';
// Decode asynchronously
request.onload = function() {
  context.decodeAudioData(request.response, function(theBuffer) { 
    buffer = theBuffer;
  }, onError);
}
request.send();

此程式碼較老,用 fetch 請求資料更簡單,我的 demo 裡就是用 fetch 代替了 XMLHttpRequest

音訊緩衝只是播放源的一種。其它播放源包括直接來自麥克風或其它音源輸入裝置,或者

一旦你載入了緩衝,你可以為它建立一個(source node)源節點(AudioBufferSource Node),把它連線到音訊圖並在源節點(source node)呼叫 start(0)。結束播放呼叫 stop(0).注意,這兩個函式在當前音訊上下文的座標系統呼叫都需要消耗時間(見第2章)

function playSound(buffer) {
  var source = context.createBufferSource(); 
  source.buffer = buffer; 
  source.connect(context.destination); 
  source.start(0);
}

遊戲中通常都有迴圈播放的背景音樂。然而,需要注意不要過度重複你的選擇:如果玩家被困在了某地或某個關卡,同樣的樣本在背景中不斷播放,為了防止玩家沮喪,背景音聲道的逐漸淡出可能是值得考慮的。另一種策略是根據遊戲情境將不同強度的元素混合在一起(文章後續會提到)。

將它們整合在一起

如你所見以上的程式碼,Web Audio API 需要一些初始化的程式設定。在真正的遊戲中,可以考慮圍繞 Web Audio API 執行 JavaScript 抽象。例如後續的 BufferClass 類。它將所有東西都整合進一個簡單的載入器上,載入器提供了設定路徑,返回音訊緩衝的功能。以下是如何使用 BufferLoader 類的程式碼:

window.onload = init; 
var context;
var bufferLoader;
function init() {
  context = new webkitAudioContext();
  bufferLoader = new BufferLoader( context,
            [
              '../sounds/hyper-reality/br-jam-loop.wav',
              '../sounds/hyper-reality/laughter.wav',
            ],
            finishedLoading
        );
  bufferLoader.load();
}

function finishedLoading(bufferList) {
    // 建立兩具音源並把它們整合到一起播放
    var source1 = context.createBufferSource();
    var source2 = context.createBufferSource(); 
    source1.buffer = bufferList[0];
    source2.buffer = bufferList[1];
    source1.connect(context.destination);
    source2.connect(context.destination);
    source1.start(0);
    source2.start(0);
}

BufferLoader 類的實現可參 https://github.com/willian12345/WebAudioAPI/blob/master/examples/Bufferloader.js

音訊播放例子可參考 https://github.com/willian12345/WebAudioAPI/blob/master/examples/ch01/index.html


注:轉載請註明出處部落格園:王二狗Sheldon池中物 (willian12345@126.com)

相關文章