開始使用 Web Audio API

Boris Smus
Boris Smus

在 HTML5 <audio> 元素推出之前,您必須使用 Flash 或其他外掛程式,才能讓網頁發出聲音。雖然網路上的音訊不再需要外掛程式,但音訊標記會為實作複雜的遊戲和互動應用程式帶來重大限制。

Web Audio API 是高階 JavaScript API,可在網路應用程式中處理及合成音訊。這個 API 的目標是納入現代遊戲音訊引擎的功能,以及現代電腦音訊製作應用程式中的部分混合、處理和篩選工作。以下將簡要介紹如何使用這個強大的 API。

AudioContext 用於管理及播放所有音效。如要使用 Web Audio API 產生音訊,請建立一或多個音源,然後將其連結至 AudioContext 執行個體提供的音訊目的地。這類連線不必是直接連線,也可以經過任意數量的中繼 AudioNodes,這些中繼可做為音訊信號的處理模組。如要進一步瞭解這項路由,請參閱 Web Audio 規格

單一 AudioContext 例項可支援多個音訊輸入和複雜的音訊圖表,因此我們只需要為每個建立的音訊應用程式提供其中一個。

下列程式碼片段會建立 AudioContext

var context;
window
.addEventListener('load', init, false);
function init() {
   
try {
    context
= new AudioContext();
   
}
   
catch(e) {
    alert
('Web Audio API is not supported in this browser');
   
}
}

如果是較舊的 WebKit 瀏覽器,請使用 webkit 前置字串,就像 webkitAudioContext 一樣。

許多有趣的 Web Audio API 功能 (例如建立 AudioNode 和解碼音訊檔案資料) 都是 AudioContext 的方法。

載入音效

Web Audio API 會使用 AudioBuffer 處理短至中等長度的音訊。基本做法是使用 XMLHttpRequest 擷取音訊檔案。

API 支援以多種格式載入音訊檔案資料,例如 WAV、MP3、AAC、OGG 和「其他」。瀏覽器支援的不同音訊格式各有不同

以下程式碼片段示範如何載入音訊樣本:

var dogBarkingBuffer = null;
var context = new AudioContext();

function loadDogSound(url) {
   
var request = new XMLHttpRequest();
    request
.open('GET', url, true);
    request
.responseType = 'arraybuffer';

   
// Decode asynchronously
    request
.onload = function() {
    context
.decodeAudioData(request.response, function(buffer) {
        dogBarkingBuffer
= buffer;
   
}, onError);
   
}
    request
.send();
}

音訊檔案資料為二進位資料 (而非文字),因此我們將要求的 responseType 設為 'arraybuffer'。如要進一步瞭解 ArrayBuffers,請參閱這篇關於 XHR2 的文章

收到 (未解碼) 音訊檔案資料後,可以保留資料以供日後解碼,也可以立即使用 AudioContext decodeAudioData() 方法解碼。這個方法會擷取儲存在 request.response 中的音訊檔案資料 ArrayBuffer,並以非同步方式解碼 (不會封鎖主要 JavaScript 執行緒)。

decodeAudioData() 完成後,會呼叫回呼函式,以 AudioBuffer 的形式提供解碼的 PCM 音訊資料。

播放音效

簡單的音訊圖表
簡單的音訊圖表

載入一或多個 AudioBuffers 後,我們就可以播放聲音了。假設我們剛載入含有狗吠聲的 AudioBuffer,且載入作業已完成。接著,我們可以使用以下程式碼播放這個緩衝區。

var context = new AudioContext();

function playSound(buffer) {
   
var source = context.createBufferSource(); // creates a sound source
    source
.buffer = buffer;                    // tell the source which sound to play
    source
.connect(context.destination);       // connect the source to the context's destination (the speakers)
    source
.noteOn(0);                          // play the source now
}

每當使用者按下鍵或用滑鼠點選某個項目時,系統就會呼叫這個 playSound() 函式。

noteOn(time) 函式可讓您輕鬆為遊戲和其他時間敏感的應用程式安排精確的音效播放時間。不過,為了讓這項排程正常運作,請務必預先載入音訊緩衝區。

將 Web Audio API 抽象化

當然,最好建立更通用的載入系統,而非硬式編碼載入這項特定音效。針對音訊應用程式或遊戲會使用的許多短至中長音訊,有許多處理方式。以下介紹一種使用 BufferLoader 的方法 (不屬於網路標準)。

以下是如何使用 BufferLoader 類別的範例。我們來建立兩個 AudioBuffers,並在載入完成後同時播放。

window.onload = init;
var context;
var bufferLoader;

function init() {
    context
= new AudioContext();

    bufferLoader
= new BufferLoader(
    context
,
   
[
       
'../sounds/hyper-reality/br-jam-loop.wav',
       
'../sounds/hyper-reality/laughter.wav',
   
],
    finishedLoading
   
);

    bufferLoader
.load();
}

function finishedLoading(bufferList) {
   
// Create two sources and play them both together.
   
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
.noteOn(0);
    source2
.noteOn(0);
}

處理時間:播放有節奏的音效

Web Audio API 可讓開發人員精確安排播放作業。為了示範這項功能,我們來設定簡單的節奏音軌。以下可能是最廣為人知的鼓組模式:

簡單的搖滾鼓聲節奏
簡單的搖滾鼓聲模式

每個八分音符會播放一個高帽,而踢腳和小鼓會以 4/4 拍的速度交替播放。

假設我們已載入 kicksnarehihat 緩衝區,這時要執行此操作的程式碼很簡單:

for (var bar = 0; bar < 2; bar++) {
   
var time = startTime + bar * 8 * eighthNoteTime;
   
// Play the bass (kick) drum on beats 1, 5
    playSound
(kick, time);
    playSound
(kick, time + 4 * eighthNoteTime);

   
// Play the snare drum on beats 3, 7
    playSound
(snare, time + 2 * eighthNoteTime);
    playSound
(snare, time + 6 * eighthNoteTime);

   
// Play the hi-hat every eighth note.
   
for (var i = 0; i < 8; ++i) {
    playSound
(hihat, time + i * eighthNoteTime);
   
}
}

這裡只會重複一次,而非無限循環,就像在樂譜中看到的一樣。playSound 函式是一種方法,可在指定時間播放緩衝區,如下所示:

function playSound(buffer, time) {
   
var source = context.createBufferSource();
    source
.buffer = buffer;
    source
.connect(context.destination);
    source
.noteOn(time);
}

變更音效音量

您可能會想對聲音執行最基本的操作之一,也就是變更音量。使用 Web Audio API,我們可以透過 AudioGainNode 將來源路由至其目的地,以便操控音量:

含有增益節點的音訊圖表
含增益節點的音訊圖表

您可以按照下列步驟設定此連線:

// Create a gain node.
var gainNode = context.createGainNode();
// Connect the source to the gain node.
source
.connect(gainNode);
// Connect the gain node to the destination.
gainNode
.connect(context.destination);

設定圖表後,您可以透過下列方式操控 gainNode.gain.value,以程式輔助方式變更音量:

// Reduce the volume.
gainNode
.gain.value = 0.5;

兩個音效之間的淡出淡入效果

現在,假設我們有一個稍微複雜的情況,我們要播放多個音效,但希望在兩者之間進行交叉淡出淡入。這是類似 DJ 的應用程式中常見的情況,我們有兩個轉盤,並希望能夠從一個音源平移到另一個音源。

您可以使用下列音訊圖表完成這項操作:

音訊圖表,其中兩個來源透過增益節點連接
音訊圖表,其中兩個來源透過增益節點連接

如要設定這項功能,只需建立兩個 AudioGainNodes,並透過節點連接每個來源,使用類似以下的函式:

function createSource(buffer) {
   
var source = context.createBufferSource();
   
// Create a gain node.
   
var gainNode = context.createGainNode();
    source
.buffer = buffer;
   
// Turn on looping.
    source
.loop = true;
   
// Connect source to gain.
    source
.connect(gainNode);
   
// Connect gain to destination.
    gainNode
.connect(context.destination);

   
return {
    source
: source,
    gainNode
: gainNode
   
};
}

等效功率交叉淡出

在樣本之間滑動時,粗略的線性交叉淡出方法會顯示音量下降。

線性交叉淡出
線性交錯

為解決這個問題,我們使用等功率曲線,其中對應的增益曲線是非線性的,並在較高的振幅處相交。這麼做可盡量減少音訊區域之間的音量落差,讓音量可能略有差異的區域之間,產生更均勻的轉場效果。

等功率交叉淡出。
等功率交錯淡出效果

播放清單交叉淡出效果

另一個常見的跨淡音處理應用程式是音樂播放器應用程式。當歌曲變更時,我們希望淡出目前的曲目,並淡入新曲目,以免轉場效果生硬。如要這樣做,請將交叉淡出效果排定在未來。雖然我們可以使用 setTimeout 進行排程,但這並不精確。透過 Web Audio API,我們可以使用 AudioParam 介面,為參數排定日後的值,例如 AudioGainNode 的增益值。

因此,在播放清單中,我們可以透過在目前播放的曲目結束前稍早的時間,為目前播放的曲目安排衰減增益,並為下一個曲目安排增益增益,藉此在曲目之間進行轉換:

function playHelper(bufferNow, bufferLater) {
   
var playNow = createSource(bufferNow);
   
var source = playNow.source;
   
var gainNode = playNow.gainNode;
   
var duration = bufferNow.duration;
   
var currTime = context.currentTime;
   
// Fade the playNow track in.
    gainNode
.gain.linearRampToValueAtTime(0, currTime);
    gainNode
.gain.linearRampToValueAtTime(1, currTime + ctx.FADE_TIME);
   
// Play the playNow track.
    source
.noteOn(0);
   
// At the end of the track, fade it out.
    gainNode
.gain.linearRampToValueAtTime(1, currTime + duration-ctx.FADE_TIME);
    gainNode
.gain.linearRampToValueAtTime(0, currTime + duration);
   
// Schedule a recursive track change with the tracks swapped.
   
var recurse = arguments.callee;
    ctx
.timer = setTimeout(function() {
    recurse
(bufferLater, bufferNow);
   
}, (duration - ctx.FADE_TIME) - 1000);
}

Web Audio API 提供一組方便的 RampToValue 方法,可逐步變更參數的值,例如 linearRampToValueAtTimeexponentialRampToValueAtTime

您可以從內建的線性和指數函式 (如上所述) 中挑選轉場時間函式,但也可以使用 setValueCurveAtTime 函式,透過陣列值指定自己的值曲線。

為音訊套用簡單的濾鏡效果

含有 BiquadFilterNode 的音訊圖表
含有 BiquadFilterNode 的音訊圖表

Web Audio API 可讓您將音訊從一個音訊節點傳送至另一個音訊節點,建立可能複雜的處理器鏈,為您的聲音形式添加複雜的效果。

其中一種方法是在音訊來源和目的地之間放置 BiquadFilterNode。這類音訊節點可執行各種低階濾鏡,用於建立圖形均衡器,甚至更複雜的效果,主要用於選取要強調或抑制音訊頻譜的哪些部分。

支援的篩選器類型包括:

  • 低通濾波器
  • 高通濾波器
  • 帶通濾波器
  • 低檔案櫃篩選器
  • 高書架篩選器
  • 峰值篩選器
  • 缺口濾鏡
  • 全部通過篩選器

所有篩選器都包含參數,可用來指定某些增益量、套用篩選器的頻率,以及品質因子。低通濾波器會保留較低的頻率範圍,但捨棄較高的頻率。截斷點取決於頻率值,而 Q 因子沒有單位,可決定圖形的形狀。增益只會影響特定濾波器,例如低置和峰值濾波器,而不會影響這個低通濾波器。

讓我們設定簡單的低通濾波器,只從音訊樣本中擷取基音:

// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
source
.connect(filter);
filter
.connect(context.destination);
// Create and specify parameters for the low-pass filter.
filter
.type = 0; // Low-pass filter. See BiquadFilterNode docs
filter
.frequency.value = 440; // Set cutoff to 440 HZ
// Playback the sound.
source
.noteOn(0);

一般來說,頻率控制項需要經過調整,才能以對數刻度運作,因為人類聽覺本身就是以相同原則運作 (也就是 A4 是 440hz,A5 是 880hz)。詳情請參閱上方原始碼連結中的 FilterSample.changeFrequency 函式。

最後,請注意,範例程式碼可讓您連結及中斷濾鏡,以動態方式變更 AudioContext 圖表。我們可以呼叫 node.disconnect(outputNumber) 將 AudioNode 從圖表中移除。舉例來說,如要將圖表重新導向,從經過篩選器改為直接連線,我們可以執行以下操作:

// Disconnect the source and filter.
source
.disconnect(0);
filter
.disconnect(0);
// Connect the source directly.
source
.connect(context.destination);

進一步聆聽

我們已介紹 API 的基本概念,包括載入和播放音訊樣本。我們使用增益節點和篩選器建立音訊圖表,並安排音效和音訊參數調整,以便啟用一些常見的音效。到目前為止,您已準備好建構一些精彩的網路音訊應用程式!

如要尋找靈感,許多開發人員已使用 Web Audio API 製作出精彩作品。我最喜歡的功能包括:

  • AudioJedit:瀏覽器內的音訊拼接工具,可使用 SoundCloud 永久連結。
  • ToneCraft,這是一個音效排序器,可透過堆疊 3D 區塊來建立音效。
  • Plink,這是一款使用 Web Audio 和 Web Sockets 的協作音樂製作遊戲。