使用 Web Audio API 開發遊戲音訊

Boris Smus
Boris Smus

簡介

音訊是多媒體體驗中不可或缺的一環。如果你曾試過關掉聲音看電影,可能就會注意到這點。

遊戲也不例外!我對電玩遊戲最美好的回憶,就是音樂和音效。現在,在許多情況下,在玩過我最喜歡的遊戲近二十年後,我仍然無法忘記近藤浩治的薩爾達作品和Matt Uelmen的氛圍暗黑破壞神原聲帶。音效也同樣令人印象深刻,例如《魔獸爭霸》中一聽就知道的單位點擊回應,以及任天堂經典遊戲的音效。

遊戲音訊會帶來一些有趣的挑戰。為了創作令人信服的遊戲音樂,設計師需要根據玩家可能遇到的不可預測遊戲狀態進行調整。在實際情況下,遊戲的某些部分可能會持續不明的時間長度,音效可能會與環境互動,並以複雜的方式混合,例如房間效果和相對聲音定位。最後,系統可能會同時播放大量音效,這些音效必須同時播放,且不會造成效能降低。

網頁遊戲音訊

如果是簡單的遊戲,使用 <audio> 標記可能就足夠了。不過,許多瀏覽器的實作方式不佳,導致音訊故障和高延遲。由於供應商正努力改善各自的實作,因此這個問題應該只是暫時性的。如要快速瞭解 <audio> 標記的狀態,areweplayingyet.org 提供了一套實用的測試套件。

不過,深入研究 <audio> 標記規格後,就會發現許多事情無法透過該標記完成,這並不令人意外,因為該標記是專為媒體播放而設計。限制包括:

  • 無法對聲音訊號套用篩選器
  • 無法存取原始 PCM 資料
  • 沒有音源和聽者的位置和方向概念
  • 沒有精細的時間。

在本文其餘部分,我將深入探討其中一些主題,並以 Web Audio API 編寫的遊戲音訊為背景。如需這個 API 的簡介,請參閱入門教學課程。

背景音樂

遊戲通常會循環播放背景音樂。

如果循環短且可預測,可能會讓人感到厭煩。如果玩家卡在某個區域或關卡,且背景持續播放相同的音訊樣本,建議逐漸淡出音軌,避免玩家進一步感到挫折。另一種策略是混合不同強度的音效,並根據遊戲情境逐漸淡入淡出。

舉例來說,如果玩家在某個區域與史詩級魔王戰鬥,您可能會有多種混音,情緒範圍從營造氣氛、預示到緊張。音樂合成軟體通常可讓您根據一首曲子,選擇要匯出的音軌組合,匯出多個混音 (長度相同)。這樣一來,您就能保持內部一致性,避免在從一個音軌淡出到另一個音軌時,出現不協調的轉場效果。

Garageband

接著,您可以使用 Web Audio API,透過 XHR 使用 BufferLoader 類別等方式匯入所有這些樣本 (Web Audio API 簡介文章會深入探討這個主題)。載入音效需要時間,因此遊戲中使用的素材資源應在載入網頁時、關卡開始時,或在玩家遊玩時逐步載入。

接下來,您要為每個節點建立來源,並為每個來源建立增益節點,然後連結圖表。

完成後,您可以同時循環播放所有這些來源,由於長度相同,Web Audio API 會確保來源保持對齊。當角色接近或遠離最終魔王戰鬥時,遊戲可以使用如下的增益量演算法,改變鏈中每個節點的增益值:

// Assume gains is an array of AudioGainNode, normVal is the intensity
// between 0 and 1.
var value = normVal - (gains.length - 1);
// First reset gains on all nodes.
for (var i = 0; i < gains.length; i++) {
    gains[i].gain.value = 0;
}
// Decide which two nodes we are currently between, and do an equal
// power crossfade between them.
var leftNode = Math.floor(value);
// Normalize the value between 0 and 1.
var x = value - leftNode;
var gain1 = Math.cos(x - 0.5*Math.PI);
var gain2 = Math.cos((1.0 - x) - 0.5*Math.PI);
// Set the two gains accordingly.
gains[leftNode].gain.value = gain1;
// Check to make sure that there's a right node.
if (leftNode < gains.length - 1) {
    // If there is, adjust its gain.
    gains[leftNode + 1].gain.value = gain2;
}

在上述方法中,兩個來源會同時播放,我們使用等功率曲線在兩者之間進行交叉淡化 (如簡介所述)。

現今許多遊戲開發人員會使用 <audio> 標記來播放背景音樂,因為這項功能非常適合串流內容。現在,您可以將 <audio> 標記中的內容帶入 Web Audio 情境。

這項技術相當實用,因為 <audio> 標記可與串流內容搭配使用,讓您立即播放背景音樂,不必等待全部下載完成。將串流帶入 Web Audio API 後,您就可以操控或分析串流。以下範例會對透過 <audio> 標記播放的音樂套用低通濾波器:

var audioElement = document.querySelector('audio');
var mediaSourceNode = context.createMediaElementSource(audioElement);
// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
mediaSourceNode.connect(filter);
filter.connect(context.destination);

如要進一步瞭解如何整合 <audio> 標記與 Web Audio API,請參閱這篇簡短文章。

音效

遊戲通常會根據使用者輸入內容或遊戲狀態變更,播放音效。不過,音效和背景音樂一樣,很快就會讓人感到厭煩。為避免這種情況,建議您準備一組相似但不同的音效。這可能是腳步聲樣本的輕微變化,也可能是劇烈變化,例如《魔獸爭霸系列》中點選單位時的反應。

遊戲音效的另一個重要特點是,可以同時播放多個音效。假設你正在拍攝槍戰場景,多名演員拿著機關槍射擊。每把機槍每秒會發射多次,導致同時播放數十個音效。Web Audio API 的一大亮點,就是能同時播放多個來源的音訊,且時間精準。

以下範例會建立多個音源,並錯開播放時間,藉此使用多個個別子彈樣本建立機關槍連發音效。

var time = context.currentTime;
for (var i = 0; i < rounds; i++) {
    var source = this.makeSource(this.buffers[M4A1]);
    source.noteOn(time + i - interval);
}

如果遊戲中所有機關槍的聲音都一模一樣,那就太無趣了。當然,聲音會根據與目標的距離和相對位置而有所不同 (稍後會詳細說明),但這可能還不夠。幸好,Web Audio API 提供兩種方法,可輕鬆調整上述範例:

  1. 子彈發射的時間間隔略有不同
  2. 改變每個樣本的 playbackRate (也會改變音高),更逼真地模擬現實世界的隨機性。

如需更貼近實際情況的技術範例,請參閱撞球桌示範,其中使用隨機取樣和不同的 playbackRate,讓球碰撞的聲音更有趣。

3D 定位音效

遊戲通常設定在具有幾何屬性的世界中,無論是 2D 還是 3D。在這種情況下,立體聲定位音訊可以大幅提升體驗的沉浸感。幸好,Web Audio API 內建硬體加速定位音訊功能,使用起來相當簡單。順帶一提,請務必使用立體聲喇叭 (最好是耳機),才能聽出下列範例的效果。

在上述範例中,畫布中間有一個聽眾 (人形圖示),滑鼠會影響音源 (喇叭圖示) 的位置。以上是使用 AudioPannerNode 達到這類效果的簡單範例。上述範例的基本概念是透過設定音訊來源的位置,回應滑鼠移動,如下所示:

PositionSample.prototype.changePosition = function(position) {
    // Position coordinates are in normalized canvas coordinates
    // with -0.5 < x, y < 0.5
    if (position) {
    if (!this.isPlaying) {
        this.play();
    }
    var mul = 2;
    var x = position.x / this.size.width;
    var y = -position.y / this.size.height;
    this.panner.setPosition(x - mul, y - mul, -0.5);
    } else {
    this.stop();
    }
};

Web Audio 處理空間化的注意事項:

  • 聽眾預設位於原點 (0, 0, 0)。
  • Web Audio 位置 API 沒有單位,因此我加入了乘數,讓示範音效聽起來更佳。
  • Web Audio 使用 y 軸向上的笛卡兒座標 (與大多數電腦圖形系統相反)。因此,我在上述程式碼片段中交換了 Y 軸

精確:音錐

位置模型非常強大且相當先進,主要以 OpenAL 為基礎。詳情請參閱上述連結規格的第 3 和第 4 節。

位置模式

Web Audio API 情境中附加了一個 AudioListener,可透過位置和方向在空間中設定。每個來源都可以透過 AudioPannerNode 傳遞,將輸入音訊空間化。平移節點具有位置和方向,以及距離和方向模型。

距離模型會根據與來源的距離指定增益量,而方向模型則可透過指定內錐和外錐來設定,內錐和外錐會決定聽者在內錐內、內錐和外錐之間或外錐外時的增益量 (通常為負值)。

var panner = context.createPanner();
panner.coneOuterGain = 0.5;
panner.coneOuterAngle = 180;
panner.coneInnerAngle = 0;

雖然我的範例是 2D,但這個模型可以輕鬆推廣到 3D。如需 3D 空間音訊範例,請參閱這個位置範例。除了位置之外,Web Audio 音效模型也可以選擇加入都卜勒效應的速度。這個範例更詳細地說明都卜勒效應。

如要進一步瞭解這個主題,請參閱這篇關於 [混合位置音訊和 WebGL][webgl] 的詳細教學課程。

會議室特效和濾鏡

實際上,聲音的感知方式很大程度上取決於聆聽聲音的房間。同樣是嘎吱作響的門,在地下室和大型開放式大廳聽起來會截然不同。高製作價值的遊戲會想要模仿這些效果,因為為每個環境建立一組獨立的樣本成本過高,而且會導致更多資產和更大量的遊戲資料。

簡單來說,原始聲音與實際聲音之間的差異,在音訊術語中稱為「脈衝響應」。這些脈衝響應可以精心錄製,事實上,有些網站會託管許多這類預先錄製的脈衝響應檔案 (儲存為音訊),方便您使用。

如要進一步瞭解如何從特定環境建立脈衝響應,請詳閱 Web Audio API 規格中「Convolution」部分的「Recording Setup」一節。

更重要的是,Web Audio API 提供了一種簡單的方法,可以使用 ConvolverNode 將這些脈衝響應套用到我們的聲音。

// Make a source node for the sample.
var source = context.createBufferSource();
source.buffer = this.buffer;
// Make a convolver node for the impulse response.
var convolver = context.createConvolver();
convolver.buffer = this.impulseResponseBuffer;
// Connect the graph.
source.connect(convolver);
convolver.connect(context.destination);

此外,您也可以在 Web Audio API 規格頁面上查看房間效果示範,以及這個範例,讓您控制乾 (原始) 和濕 (透過卷積器處理) 混音的爵士標準。

最後倒數

假設您已建構遊戲、設定位置音訊,現在圖表中有大量 AudioNode 同時播放。很好,但還有一點需要考量:

由於多個音效只是堆疊在一起,沒有經過正規化,因此可能會超出喇叭的音量上限。就像圖片超出畫布邊界一樣,如果波形超過最大門檻,聲音也會被截斷,產生明顯的失真。波形大致如下:

剪輯

以下是實際剪輯範例。波形不佳:

剪輯

請務必留意上述刺耳的失真音效,或過於柔和的混音,以免聽眾必須調高音量。如果遇到這種情況,請務必修正!

偵測削波

從技術角度來看,當任何聲道中的訊號值超出有效範圍 (即 -1 到 1 之間) 時,就會發生削波。偵測到這種情況時,建議您提供視覺回饋,如要確實做到這一點,請在圖表中加入 JavaScriptAudioNode。音訊圖的設定如下:

// Assume entire sound output is being piped through the mix node.
var meter = context.createJavaScriptNode(2048, 1, 1);
meter.onaudioprocess = processAudio;
mix.connect(meter);
meter.connect(context.destination);

您可以在下列 processAudio 處理常式中偵測到裁剪:

function processAudio(e) {
    var buffer = e.inputBuffer.getChannelData(0);

    var isClipping = false;
    // Iterate through buffer to check if any of the |values| exceeds 1.
    for (var i = 0; i < buffer.length; i++) {
    var absValue = Math.abs(buffer[i]);
    if (absValue >= 1) {
        isClipping = true;
        break;
    }
    }
}

一般來說,基於效能考量,請小心不要過度使用 JavaScriptAudioNode。在這種情況下,計量的替代實作方式可能會在轉譯時,根據 requestAnimationFrame 判斷,輪詢音訊圖中的 RealtimeAnalyserNode,以取得 getByteFrequencyData。這種做法更有效率,但會遺漏大部分訊號 (包括可能剪輯的位置),因為轉譯作業每秒最多發生 60 次,而音訊訊號的變化速度更快。

由於剪輯偵測非常重要,我們未來可能會看到內建的 MeterNode Web Audio API 節點。

防止剪輯

只要調整主要 AudioGainNode 的增益,就能將混音降低到不會發生削波的程度。不過,在實際操作中,由於遊戲中播放的聲音可能取決於多種因素,因此可能很難決定主增益值,以防止所有狀態的削波。一般來說,您應該調整增益,以預測最壞的情況,但這更像是一門藝術,而非科學。

加點糖

壓縮器常用於音樂和遊戲製作,可讓訊號更平滑,並控制整體訊號中的尖峰。在 Web Audio 世界中,這項功能可透過 DynamicsCompressorNode 實現,您可以將其插入音訊圖中,以獲得更響亮、更豐富、更飽滿的聲音,並有助於剪輯。直接引用規格,這個節點

一般來說,使用動態壓縮是個好主意,尤其是在遊戲設定中,如前所述,您不確定會播放哪些聲音以及何時播放。DinahMoe 實驗室的 Plink 就是一個很好的例子,因為播放的聲音完全取決於你和其他參與者。壓縮器在大多數情況下都很有用,但少數情況除外,例如處理經過精心母帶處理、音質已調整到「恰到好處」的音軌。

如要實作這項功能,只要在音訊圖中加入 DynamicsCompressorNode 即可,通常是目的地前的最後一個節點:

// Assume the output is all going through the mix node.
var compressor = context.createDynamicsCompressor();
mix.connect(compressor);
compressor.connect(context.destination);

如要進一步瞭解動態壓縮,請參閱這篇維基百科文章,其中提供豐富資訊。

總之,請仔細聆聽是否有削波現象,並插入主增益節點來防止這種情況。接著使用動態壓縮器節點,讓整體混音更緊湊。音訊圖表大致如下:

最終結果

結論

以上就是我認為使用 Web Audio API 開發遊戲音訊時,最重要的幾個面向。運用這些技術,就能直接在瀏覽器中打造真正引人入勝的音訊體驗。在結束之前,我想提供一個瀏覽器專屬的提示:如果分頁進入背景,請務必使用 Page Visibility API 暫停音效,否則可能會造成使用者不悅。

如要進一步瞭解 Web Audio,請參閱入門文章。如有疑問,請查看網頁音訊常見問題,看看是否已有解答。最後,如有其他問題,請前往 Stack Overflow 提問,並加上 web-audio 標記。

在結束之前,我想分享一些現今實際遊戲中,Web Audio API 的精彩應用: