用HTML5 Audio API開發(fā)游戲音樂
注意:本文討論的API尚未最終確定,仍在不斷變化。請在自己的項目中謹(jǐn)慎使用。
介 紹
音頻在很大程度上使得多媒體體驗非常引人注目。如果你曾經(jīng)嘗試在關(guān)閉聲音的情況下看電影,你就很可能已經(jīng)注意到了這一點。
游戲也不例外!我最喜愛的視頻游戲的回憶里包含了音樂和聲效。在二十年后的今天,大多情況下,當(dāng)玩我最愛的游戲時,我仍然不能把“塞爾達(dá)”里近藤浩二的樂曲和馬特大氣的暗黑配樂從我的頭腦里驅(qū)逐掉。這同樣適用于音效,例如魔獸里單位實時點擊的響應(yīng),以及任天堂的經(jīng)典例子。
游戲的音頻提出了一些有趣的挑戰(zhàn)。要創(chuàng)建令人著迷的游戲音樂,設(shè)計人員需要調(diào)節(jié)潛在的不可預(yù)知的狀態(tài)。實際上,部分游戲能持續(xù)未知的時間長度,聲音可以與環(huán)境互動,并以復(fù)雜的方式混合起來,例如室內(nèi)效果和相對聲音定位。最后,可能有大量的一次播放聲效,這需要不錯的混合效果和在渲染時沒有性能損失。
網(wǎng)頁上的游戲音頻
簡單的游戲使用<audio>標(biāo)簽可能就足夠了。然而,許多瀏覽器提供的簡陋實現(xiàn)導(dǎo)致音頻毛刺和高延遲的出現(xiàn)。這可能只是暫時性的問題,因為廠商們都在努力改進(jìn)各自的實現(xiàn)。要了解<audio>標(biāo)簽的支持情況,我們可以使用areweplayingyet.org所提供的優(yōu)秀測試工具。
一旦深入<audio>標(biāo)簽規(guī)范,就會清楚了解到有很多事情根本不能用它實現(xiàn)。這并不奇怪,因為它主要被設(shè)計來支持多媒體播放。這些限制包括:
- 無法為聲音信號使用濾波器
- 無法訪問原始的PCM(宇捷:即WAV)數(shù)據(jù)
- 沒有來源和聽眾位置、方向的概念
- 沒有細(xì)粒度的計時
在下文中,我將深入介紹一些用WebAudio API編寫游戲音頻方面的內(nèi)容。在入門教程里可以了解到此API的簡單介紹。
背景音樂
游戲里往往有循環(huán)播放的背景音樂。例如,一個背景音軌如下:
如果你的循環(huán)音樂很短并且已知,會相當(dāng)?shù)臒┤?。?dāng)玩家被困在一個區(qū)域或者關(guān)卡上,會同時連續(xù)播放相同的背景音樂,我們可能需要逐漸淡出來防止讓玩家厭煩。另一種策略是,根據(jù)游戲中的上下文,把不同的音效強度通過逐漸的淡入淡出混合起來。
如果你的玩家在一個史詩般的BOSS關(guān)卡里,可能需要對幾個不同的情緒范圍進(jìn)行混音,例如從藝術(shù)氛圍到有心理暗示的氛圍再到激烈的氛圍。音樂合成軟件通常允許你通過選擇音軌集合來導(dǎo)出幾種混音(它們具有同樣長度)。這樣音軌之間就有某種內(nèi)部一致性,避免出現(xiàn)從一個音軌切換到另一個時出現(xiàn)不和諧的轉(zhuǎn)換過渡。
然后,利用WebAudio API,你可以使用某些類例如BufferLoader通過XHR導(dǎo)入所有這些音效樣本(這在介紹網(wǎng)絡(luò)音頻API的文章中進(jìn)行了深入介紹)。加載音效需要時間,所以這些在游戲中使用的音效在每一關(guān)開始時,應(yīng)該在頁面加載時同時載入,或者在播放器播放時增量加載。
接下來,你需要為每個節(jié)點創(chuàng)建一個源,并為每個源創(chuàng)建一個增益節(jié)點,連接圖如下:
完成之后,你可以在一個循環(huán)中同時回放這些音效源,因為它們都具有相同的長度,WebAudio API將保證它們保持一致。由于最后的BOSS戰(zhàn)時音效風(fēng)格會變得相近或更不同,游戲可以使用類似于下面的增量算法來改變鏈中各節(jié)點對應(yīng)的增益值:
- // 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;
- }
在上述方法中,有兩個音效源同時播放,我們使用同等功率的曲線(如介紹所述)從它們之間淡入淡出。下面的示例使用了這一策略,演示的背景音樂在魔獸爭霸2的主題上逐漸增強:
缺少的環(huán)節(jié):Web Audio的Audio標(biāo)簽
現(xiàn)在許多游戲開發(fā)商為背景音樂使用<audio>標(biāo)簽,因為它非常適合流媒體內(nèi)容?,F(xiàn)在你可以通過<audio>標(biāo)簽把內(nèi)容帶入網(wǎng)絡(luò)音頻的上下文。
<audio>標(biāo)簽支持流媒體相當(dāng)有用,因為它可以讓你立即播放背景音樂,而無須等待下載所有內(nèi)容。在網(wǎng)絡(luò)音頻API支持音頻流之后,你可以操作或分析它們。下面的例子為通過<audio>標(biāo)簽播放的音樂使用了一個低通濾波器:
- 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);
關(guān)于<audio>標(biāo)簽和網(wǎng)絡(luò)音頻API整合更多的討論,可以看看這篇短文。
音 效
游戲經(jīng)常在響應(yīng)用戶輸入或者游戲狀態(tài)改變時播放聲音效果。但是像背景音樂一樣,音效可以很快的讓用戶厭倦。 為了避免這種情況,最好有一個音效池放置相似但是不同的音效。 這可以從輕微變化到急劇變化間通過固定長度來過渡,像魔獸系列里點擊各單位的時候。
游戲音效的另外一個關(guān)鍵點是可以同時有多個。想象一下,你與多個演員拍攝槍戰(zhàn)時。每個機槍每秒觸發(fā)多次,造成幾十個音效同時播放。從多個源同時播放音效,還要對音效源精確計時,是網(wǎng)絡(luò)音頻API真正的亮點。
下面的例子演示了由多個單獨子彈樣本組成的機槍,其創(chuàng)建了多個播放時間錯開的聲源。
- var time = context.currentTime;
- for (var i = 0; i < rounds; i++) {
- var source = this.makeSource(this.buffers[M4A1]);
- source.noteOn(time + i * interval);
- }
下面是這個代碼的效果:
如果你覺得聲音太響了,我感到抱歉。我們將在后面的章節(jié)討論測量和動態(tài)壓縮。
現(xiàn)在,如果你游戲里所有的機槍都像這樣響起,那將相當(dāng)無聊。當(dāng)然,它們會基于目標(biāo)的距離和相對位置而有所差異(稍后討論),但即使這樣做可能還不夠。幸運的是,網(wǎng)絡(luò)音頻API提供了對上面的示例進(jìn)行輕松調(diào)整的方式,主要有兩種:
- 發(fā)射子彈時間上微妙的變化
- 改變每個音效的播放速率(同時改變音高),以更好地模擬現(xiàn)實世界中的隨機性。
這兩種方法的效果如下:
對于這些技術(shù)在現(xiàn)實生活中的實際例子,可以看看臺球桌的演示 ,它采用了隨機抽樣和變化的播放速率來表現(xiàn)更有趣的球的碰撞聲。
3D定位音效
游戲往往設(shè)定在一個2D或者3D的世界里。在這樣的情況下,立體定位的音頻可以大大增加沉浸感的體驗。幸運的是,網(wǎng)絡(luò)音頻API帶來了內(nèi)置硬件加速的位置音頻特性,可以直接的使用。 順便說一下,你應(yīng)該確保有立體聲揚聲器(最好是耳機)來運行下面的例子。 在下面的示例中,你可以通過在畫布上滾動鼠標(biāo)滾輪來更改聲源的角度。
上面的例子中,有一個監(jiān)聽者在畫布正中(人的圖標(biāo)),同時鼠標(biāo)控制聲源(喇叭圖標(biāo))的位置,這是使用AudioPannerNode實現(xiàn)這種效果的簡單例子。它的基本思想是通過設(shè)置音頻信號源的位置響應(yīng)鼠標(biāo)的移動,如下所示:
- 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();
- }
- };
關(guān)于網(wǎng)絡(luò)音頻空間化處理需要了解的事情:
- 監(jiān)聽者默認(rèn)在原點(0,0,0)。
- 網(wǎng)絡(luò)音頻位置API沒有單位,所以我引入了一個乘數(shù)使得演示的聲效更好。
- 網(wǎng)絡(luò)音頻采用Y-型直角坐標(biāo)系(和大多數(shù)計算機圖形系統(tǒng)相反)。 這就是為什么我在上面的代碼片段進(jìn)行了y軸的變換。
高級:音錐
定位模型非常強大,而且相當(dāng)先進(jìn),主要基于OpenAL。詳細(xì)信息請查看上述規(guī)范的第3和第4節(jié)。
在有單一的AudioListener連接到網(wǎng)絡(luò)音頻API的情況下,它可以通過位置和方向配置空間。每個源可以通過一個AudioPannerNode(音頻聲像節(jié)點)來使得音頻輸入空間化。聲像節(jié)點有位置和方向,以及距離和方向性模型。
距離模型指定的增益取決于和源的接近程度,而方向模型可以通過指定內(nèi)外錐來配置,以決定監(jiān)聽者在內(nèi)部錐里,在內(nèi)外錐之間,或在外部錐之外時增益的大?。ㄍǔ樨?fù)值)。
- var panner = context.createPanner();
- panner.coneOuterGain = 0.5;
- panner.coneOuterAngle = 180;
- panner.coneInnerAngle = 0;
雖然我的例子在2D空間,但是這種模式很容易推廣到三維。例如3D聲音空間化的例子可以看看這個位置演示。另外對于位置來說,網(wǎng)絡(luò)音頻模型也可以選擇多普勒頻移的速度。這個例子展示了多普勒效應(yīng)的詳細(xì)信息。
關(guān)于這一主題的更多信息,可以閱讀混合定位音頻和WebGL的詳細(xì)教程 。
室內(nèi)效果和濾波器
在現(xiàn)實中,聲音被感覺的方式很大程度上取決于聲音所在的房間。相同吱吱作響的門在地下室與大型的開放式大廳里相比會發(fā)出相當(dāng)不同的聲音。高產(chǎn)值的游戲?qū)7逻@些影響,因為為每個環(huán)境創(chuàng)建一套獨立的音效是相當(dāng)昂貴的,并且會產(chǎn)生相當(dāng)多的材料和大量的游戲數(shù)據(jù)。
嚴(yán)格地說,描述原始聲音和現(xiàn)實中所聽到之間不同的音頻術(shù)語是脈沖響應(yīng)。這些脈沖響應(yīng)可以被精心錄制,其實也有網(wǎng)站為了方便你的使用存放了許多這種預(yù)先錄制的脈沖響應(yīng)文件(作為音頻方式存儲)。
對于如何從一個給定的環(huán)境創(chuàng)建脈沖響應(yīng)的更多信息,可以通讀網(wǎng)絡(luò)音頻API規(guī)范卷積部分的“錄音設(shè)置”一節(jié)。
更重要的是針對我們的目標(biāo),網(wǎng)絡(luò)音頻API提供了一個簡單的方法來在我們的聲音里應(yīng)用脈沖響應(yīng),即通過使用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);
下面的示例展示了一些不同脈沖響應(yīng)下的軍事演講:
還可以看看網(wǎng)絡(luò)音頻API規(guī)范頁面上的房間效果演示,以及這個讓你控制通過一個偉大的爵士標(biāo)準(zhǔn)混合干(原料)和濕(通過卷積處理)的例子。
最后的倒計時
現(xiàn)在你已經(jīng)創(chuàng)建了一個游戲,添加了位置音頻,而且現(xiàn)在在你的圖里有大量的同時播放的AudioNodes。 太棒了,但是還有一件事要考慮:
由于多種聲音互相疊加起來播放,你可能會發(fā)現(xiàn)在某種情況下,聲音超過了揚聲器的最大承受能力。就像圖像超出了畫布邊界的情況一樣,聲音也會在波形超過最大閾值時進(jìn)行削波,導(dǎo)致明顯的失真。波形看起來會像下面這樣:
這里有一個真實削波的例子。波形看起來相當(dāng)糟糕:
聽起來也很糟糕:
聽到像上面這樣嚴(yán)重扭曲的音樂是很嚴(yán)重的事,或者與此相反,過分的混合會迫使聽眾調(diào)大音量。如果你現(xiàn)在有這種情況,你真的需要立即解決它!
檢測削波
從技術(shù)角度看,削波發(fā)生在任何一個通道的信號值超出有效范圍即-1和1之間時。一旦檢測到削波反生時,視覺反饋會非常有用。要可靠的實現(xiàn)這點,可以把JavaScriptAudioNode放到你的圖里。音頻圖將會按如下進(jìn)行設(shè)置:
- // 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來檢測。這個方法更有效,但會錯過多數(shù)信號(包括有可能削波的地方),因為渲染最多發(fā)生60次,而音頻信號的變化更為迅速。
因為削波的檢測非常重要,未來我們很可能將看到網(wǎng)絡(luò)音頻API節(jié)點內(nèi)置MeterNode。
防止削波
通過調(diào)整主要AudioGainNode的增益,你可以控制混音的水平來防止削波。 然而在實踐中,因為你游戲中所播放的聲音可能取決于大量因素,所以決定主增益值來防止所有情況下的削波是相當(dāng)困難的。在通常情況下,你應(yīng)該調(diào)整增益來預(yù)期最壞的情況,但這是一門藝術(shù),而不是科學(xué)。
要知道這是具體如何實現(xiàn)的,下面是一個示例,在此你可以調(diào)整主增益。如果增益設(shè)置過高,會導(dǎo)致聲音削波。監(jiān)視器會變成紅色來給出削波的視覺反饋。下面的音響生態(tài)環(huán)境是Disco Dan的混音作品,原曲是由Yasunori Mitsuda所做的偉大的“超時空之輪”。
加一點糖
音樂和游戲制作中經(jīng)常使用效果器來平滑信號和控制尖峰。此功能在網(wǎng)絡(luò)音頻世界里可以通過DynamicsCompressorNode 來實現(xiàn),可以在你的音頻圖加入一個更響亮,更豐富,更飽滿的音色,這也有利于削波。直接引用規(guī)范里的話,這個節(jié)點
“...降低了信號最響亮部分的體積,并提升了最柔軟部分的音量... 尤其重要的是在游戲和音樂應(yīng)用里,當(dāng)大量獨立的聲音播放時,控制信號整體水平,并有助于避免削波。”
使用動態(tài)壓縮通常來說是一個好主意,尤其是在游戲的設(shè)置里,正如前面所討論的一樣,你并不知道到底此時什么聲音將會何時播放。DinahMoe實驗室的Plink是很好的例子,因為聲音的回放完全取決于你和其他參與者。效果器在大多數(shù)情況下是有用的,除了一些罕見的情況外,而這種情況下你可以使用已經(jīng)精心調(diào)整過,并且聽起來“恰到好處”的曲目。
它的實現(xiàn)是一件簡單的事情,只需要在你的音頻圖里把DynamicsCompressorNode作為目標(biāo)前的最后一個節(jié)點添加進(jìn)去。
- // Assume the output is all going through the mix node.
- var compressor = context.createDynamicsCompressor();
- mix.connect(compressor);
- compressor.connect(context.destination);
關(guān)于動態(tài)壓縮的更多細(xì)節(jié),Wikipedia上的這篇文章非常翔實。
總結(jié)一下,仔細(xì)檢查削波,通過插入主增益節(jié)點來防止它的出現(xiàn)。然后使用動態(tài)效果器節(jié)點來收緊整個混音。你的音頻圖可能看起來像這樣:
結(jié) 論
以上內(nèi)容涵蓋了我認(rèn)為使用網(wǎng)絡(luò)音頻API來開發(fā)游戲音樂最重要的方面。有了這些技術(shù),可以在你的瀏覽器上構(gòu)建真正有吸引力的音頻體驗。在我結(jié)束本文之前,給你一個提示:如果你的瀏覽器標(biāo)簽使用page visibility API切換到了后臺,一定要讓聲音暫停,否則你會為用戶提供一個潛在的令人厭煩的體驗。 對于關(guān)于網(wǎng)絡(luò)音頻的其他信息,需要在入門的文章進(jìn)行更多了解。如果你有問題,看看它是否已經(jīng)在網(wǎng)絡(luò)音頻FAQ里得到解答。最后,如果你有其他問題,可以在Stack Overflow上的web-audio標(biāo)簽下提問。 在本文結(jié)束前,讓我為你展示網(wǎng)絡(luò)音頻API現(xiàn)在在實際游戲里的用途:
譯自:http://www.html5rocks.com/en/tutorials/webaudio/games/
原文鏈接:http://blog.csdn.net/hfahe/article/details/7443276
【編輯推薦】