開始使用 Web Audio API

Boris Smus
Boris Smus

在 HTML5 <audio> 元素之前,需要使用 Flash 或其他外掛程式來打破網路的無聲。雖然網路上的音訊不再需要外掛程式,但音訊標記會對實作複雜遊戲和互動應用程式造成重大限制。

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

開始使用 AudioContext

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 為 440 Hz,A5 為 880 hz)。詳情請參閱上方原始碼連結中的 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 的協作音樂製作遊戲。