使用 Web Audio API 開發遊戲音訊

Boris Smus
Boris Smus

簡介

音訊是促成多媒體體驗的重要因素如果你以前曾嘗試以靜音模式觀看電影 大概可能已經注意到這點

遊戲也不例外!我最渴望到的電玩遊戲回憶 就是音樂與音效但在很多情況下,我都沒辦法在玩自己喜愛的將近二十年後,獲得 Koji Kondo 的 Zelda 組合Matt Uelmen 大氣的 Diablo 聲帶。同樣的集中精神亦適用於音效,例如:Warcraft 的即時可辨識單元點擊回應,以及任天堂經典名詞的範例。

遊戲音訊存在一些有趣的挑戰。為了創造有說服力的遊戲音樂,設計人員必須做出調整,讓玩家發現自己可能難以預測的遊戲狀態。實際上,遊戲的部分內容可能會長時間停留,且聲音可能會與環境互動,並以複雜的方式混入,例如房間效果和相對音效定位。最後,一次會播放大量音訊,這些聲音需要聲音良好並轉譯,而無需對效能造成負面影響。

網路上的遊戲音訊

如果是簡單的遊戲,使用 <audio> 標記或許就夠了;然而,許多瀏覽器提供的實作方式都不佳,因此可能會導致音訊故障和高延遲。這可能是暫時性問題,因為供應商正努力改善適用的實作方式。如要深入瞭解 <audio> 標記的狀態,您可以在 areweplayingyet.org 找到適合的測試套件。

不過深入探討 <audio> 標記規格後,您已清楚知道許多事情,主要是無法針對媒體播放而完成,這並不令人意外。相關限制包括:

  • 無法對音效訊號套用濾鏡
  • 無法存取原始 PCM 資料
  • 沒有來源和事件監聽器的位置和方向概念
  • 沒有精確的時間,

在本文的其他部分中,我將深入探討使用 Web Audio API 編寫的遊戲音訊,並深入探討其中幾項主題。如需這個 API 的簡介,請參閱入門教學課程

背景音樂

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

如果迴圈很短,而且容易預測,可能會非常惱人。如果玩家卡在某個區域或關卡,且在背景中持續播放相同的樣本,那麼您可以逐漸淡出音軌,避免造成進一步的困擾。另一個策略是結合各種強度,並根據遊戲情境,逐漸漸入一個強度。

舉例來說,如果您的玩家所處區域有史詩級的 Boss 戰,您可能會擁有各種不同的情感組合,包括大氣、絕緣點子到緊張刺激的情緒。音樂合成軟體通常可讓您挑選要匯出的一組音軌,藉此根據一個片段匯出多個長度相同的混音。如此一來,您會具備一些內部一致性,避免在從一個測試群組交替淡出時,出現不愉快的轉場效果。

車庫

接著使用 Web Audio API,即可透過 XHR 使用 BufferLoader 類別等方式匯入所有範例 (詳情請參閱簡介 Web Audio API 文章)。載入音效需要時間,因此遊戲中使用的資產應在網頁載入時、關卡開始時載入,或在播放器播放時以漸進方式載入。

接下來,您必須為每個節點建立來源、為每個來源建立來源節點,並連結圖表。

完成這項操作後,您就可以在迴圈中同時播放所有來源,而由於這些來源的長度相同,因此 Web Audio API 保證會保持對齊。角色在最終 Bos 戰鬥中較早或更遠時,遊戲可以利用以下提升金額演算法,為鏈中各節點的增益值產生不同的獲利值:

// 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> 標記的內容帶入網路音訊結構定義中。

這個技巧很實用,因為 <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. 調整每個樣本的播放速率 (同時也改變音調),以更妥善地模擬真實世界的隨機性。

如需這些技術的實際實際範例,請參閱集區資料表示範,其中採用隨機取樣和不同的播放速率,以提供更有趣的球形衝突音效。

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();
    }
};

關於網路音訊空間化處理方式的須知事項:

  • 根據預設,事件監聽器會位於起點 (0, 0, 0)。
  • 網路音訊定位 API 是無單位的 API,因此我導入了調節係數,藉此讓示範音訊的品質更佳。
  • 網路音訊使用以 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 規格「卷積」中的「錄製設定」一節。

更重要的是,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 規格頁面上的房間效果示範,以及此範例,可讓您控制混音 (原始) 和潮濕 (透過共通式) 混合的出色爵士標準。

最終倒數計時

因此,您已建構遊戲、設定位置音訊,現在圖形中就有大量的 AudioNodes,所有音訊都會同時播放。太好了,但還有一個需要考量的地方:

由於多個聲音只堆疊在沒有正規化的情況下,因此可能會發生超過喇叭能力的門檻時。就像圖片超出畫布邊界,如果波形超出閾值,聲音也可能會裁剪,進而產生不同的扭曲。波形如下所示:

裁剪

以下是剪輯實際運作的範例。波形看起來不佳:

裁剪

請務必聆聽類似上述的強烈失真音色,或聆聽過度音調的混音,強迫事件監聽器提升音量。如果發生這種情況,就必須修正問題!

偵測裁剪

就技術層面而言,如果任何管道中的信號值超出有效範圍 (介於 -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。在這種情況下,另一種計量模式可在轉譯時間輪詢 getByteFrequencyData 的音訊圖中的 RealtimeAnalyserNode (由 requestAnimationFrame 決定)。這個方法較有效率,但會錯過大部分訊號 (包括可能剪輯音訊的位置),因為算繪最多每秒發生 60 倍,而音訊訊號的變快變化速度則越快。

由於片段偵測極為重要,因此我們日後可能會發現內建的 MeterNode Web Audio API 節點。

防止裁剪

透過調整主要 AudioGainNode 的增益,即可將您的混音賦予一個防止剪輯的層級。然而,在實務上,由於遊戲中要播放的音效取決於許多因素,因此很難決定哪些主要利益值會防止在所有狀態間複製。一般來說,您應該微調收益金額,以預測最糟糕的情況,但這比科學還多。

添加一點糖

壓縮工具常用於音樂和遊戲製作中,以流暢地處理訊號,並控制整體訊號的遽增情形。這項功能可在網路音訊世界中透過 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 開發遊戲音訊時最重要的層面。有了這些技術,您就能直接在瀏覽器中打造真正引人入勝的音訊體驗。在登出前,我先為您提供瀏覽器專屬的提示:請務必透過頁面瀏覽權限 API,在分頁移至背景時暫停音效,否則可能會讓使用者感到困擾。

如要進一步瞭解網路音訊,請參閱更多簡介入門指南。如有任何問題,請參閱網路音訊常見問題一文,瞭解是否已獲得解答。 最後,如果您有其他問題,請在 Stack Overflow 上使用 web-audio 標記提問。

登出前,我想在實際遊戲中介紹 Web Audio API 的超棒之處: