開始使用 Web Audio API

Boris Smus
Boris Smus

在 HTML5 <audio> 元素之前,必須使用 Flash 或其他外掛程式才能中斷網頁靜音。雖然網路上的音訊不再需要外掛程式,但是音訊標記在實作複雜遊戲和互動式應用程式方面都有重大限制。

Web Audio API 是高階 JavaScript API,用於處理及合成網頁應用程式中的音訊。這個 API 的目標,在於納入現代遊戲音訊引擎所提供的功能,以及現代電腦音訊製作應用程式中的一些混合、處理及篩選工作。接著會稍微介紹這個強大的 API

開始使用 AudioContext

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

單一 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 功能,例如建立 AudioNodes 以及解碼音訊檔案資料,都是 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),將 AudioNodes 與圖表中斷連結。舉例來說,如要將圖表從篩選器重新轉送至直接連線,可以執行下列步驟:

// 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 是一款使用網路音訊和 WebSocket 的協作音樂製作遊戲。