使用 Web Audio API 開發遊戲音訊

Boris Smus
Boris Smus

簡介

音訊是打造引人入勝的多媒體體驗的關鍵。如果你曾經在觀看電影時關閉音效 應該就會注意到這種情況

遊戲也不例外!我對音樂和音效最深刻的回憶,是來自於玩電玩遊戲時的體驗。如今,在許多情況下,我玩過最愛的遊戲後,幾乎 20 年過去了,我仍無法忘記 Koji Kondo 的《薩爾達傳說》曲目Matt Uelmen 的《Diablo》氛圍配樂。這種活潑風格適用於音效,例如《Warcraft 讓機器可立即辨識的單位點擊回應,以及任天堂經典歌曲的樣本。

遊戲音效有幾項有趣的挑戰。為了製作令人信服的遊戲音樂,設計師必須調整玩家可能處於的不可預測遊戲狀態。實際上,遊戲的部分內容可能會持續一段未知的時間,聲音可以與環境互動,並以複雜的方式混合,例如房間效果和相對聲音位置。最後,可能會同時播放大量音訊,但這些聲音需要搭配良好音訊並進行算繪,不會降低效能。

網路上的遊戲音訊

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

不過,深入瞭解 <audio> 標記規格後,您會發現許多事情根本無法透過 <audio> 標記完成,這一點毫不令人意外,因為 <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 整合,請參閱這篇簡短文章

音效

遊戲通常會根據使用者輸入內容或遊戲狀態變更,播放音效。不過,就像背景音樂一樣,音效很快就會讓人感到厭煩。為避免這種情況,通常會提供一組類似但不同的音效。這可能會從輕微的腳步樣本變化,到劇烈的變化,如在 Warcraft 系列中點選單位時所見。

遊戲中的音效另一個重要特徵,是可以同時播放多個音效。假設你正在進行槍戰,有多名演員開著機關槍。每部機槍每秒會發射多次,導致同時播放數十個音效。同時播放多個精確時間的來源聲音,是 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 (也變更音調),以便更準確模擬現實世界的隨機性。

如需這些技術實際運作情況的更多實際範例,請參閱撞球桌示範,該示範使用隨機取樣和變化播放速率,產生更有趣的球碰撞聲。

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 空間化聲音的範例,請參閱位置範例。除了位置之外,網路音訊音效模型也可讓您選擇加入人員位移的速度。這個範例會更詳細地顯示多普勒效應

如要進一步瞭解這個主題,請參閱 [混合位置音訊和 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 規格頁面中的會議室效果示範,以及這個範例,可讓您控制乾燥 (原始) 和潮濕 (透過 Convolver 處理) 完美的爵士樂標準。

最後倒數計時

因此,您已經建構了遊戲、設定位置音訊,現在圖表中有許多 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 決定的計量模式,可能會在轉譯時輪詢 getByteFrequencyData 的音訊圖表中的 RealtimeAnalyserNode。這種方法效率較高,但會遺漏大部分的訊號 (包括可能出現剪輯的部分),因為算繪作業最多每秒執行 60 次,而音訊訊號的變化速度遠快於此。

由於片段偵測功能非常重要,我們日後可能會看到內建的 MeterNode Web Audio API 節點。

避免裁剪

只要調整主 AudioGainNode 的增益,就能將混音調降至可避免剪輯的程度。然而,在實務上,由於遊戲中的音效可能取決於許多因素,因此很難決定防止所有狀態遭到破解的主增值值。一般來說,您應調整增益,以便預測最糟的情況,但這更像是一種藝術,而非科學。

加入少許糖

壓縮器通常用於音樂和遊戲製作,用於平滑信號,並控制整體信號中的尖峰。這項功能可透過 DynamicsCompressorNode 在 Web Audio 環境中使用,並插入音訊圖表中可產生更大、更飽滿、更飽滿的音效,而且還提供剪輯方面的協助。直接引用規格,這個節點

使用動態壓縮通常是不錯的做法,特別是在遊戲設定中,因為如前所述,您並不知道會播放什麼聲音和何時播放。DinahMoe 研究室的「Plink」就是很好的範例,因為播放的音效完全取決於您和其他參與者。壓縮器在大多數情況下都很實用,但在某些罕見情況下,你可能會遇到經過精心母帶處理的音軌,這些音軌已經經過調音,聽起來「剛剛好」。

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

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

如要進一步瞭解動態壓縮,請參閱 這篇 Wikipedia 文章,其中提供許多實用資訊。

總結來說,請仔細聆聽剪裁,並插入主要執行個體增益節點。接著,使用動態壓縮器節點來加強整個混音。音訊圖表可能會如下所示:

最終結果

結論

以上就是我認為使用 Web Audio API 開發遊戲音訊時,最重要的面向。透過這些技巧,您可以在瀏覽器中打造真正引人入勝的音訊體驗。在我登出前,我想為您提供瀏覽器專屬提示:如果分頁使用頁面瀏覽權限 API 進入背景,請務必暫停音效,否則可能會讓使用者感到困擾。

如要進一步瞭解 Web Audio,請參閱更深入的入門文章。如果有任何問題,請查看Web Audio 常見問題,看看是否已獲得解答。最後,如果您還有其他問題,請在 Stack Overflow 中使用 web-audio 標記提出問題。

在結束本篇文章之前,讓我為您介紹一些在現今遊戲中使用 Web Audio API 的絕佳用途: