高級主題
這一章涵蓋了非常重要的主題,但比本書的其他部分稍微複雜一些。 我們會深入對聲音添加音效,完全不通過任何音頻緩衝來計算合成音效, 模擬不同聲音環境的效果,還有關於空 3D 空間音頻。
重要理論:雙二階濾波器
一個濾波可以增強或減弱聲音頻譜的某些部分。 直觀地,在頻域上它可以被表示爲一個圖表被稱爲“頻率響應圖”(見圖 6-1)。在每一個頻率上,對於每一個頻率,圖形的值越高,表示頻率範圍的那一部分越受重視。向下傾斜的圖表更多地強調低頻,而較少強調高頻。
Web Audio 濾鏡可配置3個參數: gain, frequency 和 質量因子( 常稱爲 Q)。這些參數全部會不同程度影響頻率響應圖。
有很多種濾鏡可以用來達到特定的效果:
-
Low-pass 濾波
使聲音更低沉 -
High-pass 濾波器
使聲音更微小 -
Band-pass 濾波器
截掉低點和高點(例如,電話濾波器) -
Low-shelf 濾波器
影響聲音中的低音量(如立體聲上的低音旋鈕) -
Peaking 濾波器
影響聲音中音的數量(如立體聲上的中音旋鈕) -
Notch 濾波器
去除窄頻率範圍內不需要的聲音 -
All-pass 濾波器
創建相位效果
圖 6-1 低通濾波器的頻率響應圖
所有這些雙二元濾波器(biquad filter)都源於一個共同的數學模型,並且都可以用圖形表示, 就像低通濾波器(low-pass filter) 一樣(圖 6-1)。 關於更多的濾波器細節參考對數學要求更高的這本書《Real Sound Synthesis for Interactive》作者 Perry R. Cook。 如果你對音頻底層原理感興趣的話我強烈推薦你閱讀它。
簡單來說:濾波器節點可以對音頻信號進行多種類型的濾波處理,包括低通濾波、高通濾波和帶通濾波等。低通濾波可以過濾掉某個臨界點以上的高頻信號,只讓低頻信號通過;高通濾波則相反,過濾掉低頻信號,只讓高頻信號通過;帶通濾波則是允許某個特定頻段的信號通過。
通過濾波器添加效果
要使用 Web Audio API ,我們可以通過應用上面提到過的 BiquadFilterNodes。
這個類型的音頻節點,在創建均衡器並以有趣的方式操縱聲音時應用非常普遍。讓我們設置一個簡單的低通濾波器(low-pass filter) 在一個聲音例子中用它過濾掉低頻噪聲:
// Create a filter
var filter = context.createBiquadFilter();
// Note: the Web Audio spec is moving from constants to strings. // filter.type = 'lowpass';
filter.type = filter.LOWPASS;
filter.frequency.value = 100;
// Connect the source to it, and the filter to the destination.
各 filter demo 可參考 https://github.com/willian12345/WebAudioAPI/tree/master/examples/ch06/filters-demo.html
譯者注:現在 filter.LOWPASS 已不存在,需要直接傳入字符串,如:'lowpass'
詳實話:我試着聽了一下所有的 filter 效果,嗯,怎麼說呢,有效果,但我也就聽個響 -_-!!
BiquadFilterNode 支持所有常用的二階過濾器類型。我們可以使用與前一節中討論的相同的參數配置這些節點,並且還可以通過在節點上使用get FrequencyResponse方法來可視化頻率響應圖。給定一個頻率數組,該函數返回對應於每個頻率的響應幅度數組。
Chris Wilson 和 Chris Rogers 非常好的可視化例子,將所有 Web Audio API 可用的濾波器類型放到一起的頻率反應圖。
圖 6-2 帶參數的低通濾波器的頻率響應圖
用程序生成聲音
到目前爲止,我們假定你遊戲中用到的都是靜態的聲音。音頻設計師自己創建並處理了一堆音頻資源,你負責根據當前條件使用一些參數控制播放這些音頻(舉例,房間內的背景音和音頻資源位置與聽衆)。這種實現方式有以下缺點:
-
聲音文件可能會非常大。在網頁中尤其不好,與在本地磁盤加載不同,通常是通過網絡加載的(特別是第一次加載時), 簡直慢了一個數量級。
-
就算擁有多衆多資源和變和簡單的變形,變化種類還是有限。
-
你需要通過搜索音效庫來找到資產,然後可能還要擔心版權問題.另外,很有可能,任何給定的聲音效果已經在其他應用 程序中使用過,所以您的用戶會產生意想不到的關聯
我們完全可以利用程序使用 Web Audio API 來直接生成聲音。舉個例子,讓我們來模擬一下槍開火的聲音。我們從一個白器噪聲的衝級區開始,它使用 ScriptProcessorNode 生成如下:
function WhiteNoiseScript() {
this.node = context.createScriptProcessor(1024, 1, 2);
this.node.onaudioprocess = this.process;
}
WhiteNoiseScript.prototype.process = function(e) {
var L = e.outputBuffer.getChannelData(0);
var R = e.outputBuffer.getChannelData(1);
for (var i = 0; i < L.length; i++) {
L[i] = ((Math.random() * 2) - 1);
R[i] = L[i];
}
};
上面的代碼實現不夠高效,因爲 JavaScript 需要不斷動態地創建白噪音流,爲了增強效率, 我們可以以程序化的方式生成白噪聲的單聲道音頻緩衝,如下所示:
function WhiteNoiseGenerated(callback) {
// Generate a 5 second white noise buffer.
var lengthInSamples = 5 * context.sampleRate;
var buffer = context.createBuffer(1, lengthInSamples, context.sampleRate);
var data = buffer.getChannelData(0);
for (var i = 0; i < lengthInSamples; i++) {
data[i] = ((Math.random() * 2) - 1);
}
// Create a source node from the buffer.
this.node = context.createBufferSource();
this.node.buffer = buffer;
this.node.loop = true;
this.node.start(0);
}
接下來,我們可以在一個封裝好的函數中模擬槍射擊的各個階段——攻擊、衰減和釋放:
function Envelope() {
this.node = context.createGain()
this.node.gain.value = 0;
}
Envelope.prototype.addEventToQueue = function() {
this.node.gain.linearRampToValueAtTime(0, context.currentTime);
this.node.gain.linearRampToValueAtTime(1, context.currentTime + 0.001);
this.node.gain.linearRampToValueAtTime(0.3, context.currentTime + 0.101);
this.node.gain.linearRampToValueAtTime(0, context.currentTime + 0.500);
}
最後,我們可以將聲音輸出連接到一個濾波器,以模擬距離
this.voices = [];
this.voiceIndex = 0;
var noise = new WhiteNoise();
var filter = context.createBiquadFilter();
filter.type = 0;
filter.Q.value = 1;
filter.frequency.value = 800;
// Initialize multiple voices.
for (var i = 0; i < VOICE_COUNT; i++) {
var voice = new Envelope();
noise.connect(voice.node);
voice.connect(filter);
this.voices.push(voice);
}
var gainMaster = context.createGainNode();
gainMaster.gain.value = 5;
filter.connect(gainMaster);
gainMaster.connect(context.destination);
正如您所看到的,這種方法非常強大,但很快就會變得複雜,超出了本書的範圍. 更多關於聲音的處理與生成可參考 Andy Farnell 的《Practical Synthetic Sound Design tutorials》
原文中給了一個代碼例子鏈接已失效,上面的這部分代碼是無法直接運行的可以參考我修改這部分後的代碼:
https://github.com/willian12345/WebAudioAPI/tree/master/examples/ch06/gun-effect-demo.html
房間音效
在聲音從源頭到我們耳朵之前, 它會在牆,建築物,傢俱, 地毯還有其它物體間反覆碰撞。 每一次碰撞都會改變一些聲音的屬性。例如,你在室外拍手和你在大教堂內拍手的聲音會有很大的不同, 在大教堂內聲音會有多數秒鐘的多重回音。高質量的遊戲旨在模仿這些效果。爲每個聲音環境創建單獨的樣本集通常是非常昂貴的,因爲這需要音頻設計師付出大量的努力,以及大量的音頻資源,會造成遊戲資源數據也會非常大。
Web Audio API 提供了一個叫 ConvolverNode 音頻節點用於模擬各種聲音環境。您可以從卷積引擎中獲得的效果示例包括合唱效果、混響和類似電話的語音。製作房間效果的想法是在房間裏引用播放一個聲音,記錄下來, 然後(打個比方)取原始聲音和錄製聲音之間的差異。 這樣做的結果是一個脈衝響應,它捕捉到房間對聲音的影響.這些脈衝響應是在非常特殊的工作室環境中精心記錄的,你自己做這件事需要認真的投入.幸運的是,有些網站託管了許多預先錄製的脈衝響應文件(以音頻文件的形式存儲),方便使用.
利用 ConvolverNode 節點 Web Audio API 提供了簡便的方式將脈衝響應應用到你的聲音上。這個音頻節點接收一個脈衝響應緩衝, 它是一個加載了脈衝響應的常規 AudioBuffer。卷積器實際上是一個非常複雜的濾波器(如 BiquadFilterNode),但不是從一組效果類型中進行選擇,而是可以用任意濾波器響應配置它。
譯者注:在音頻領域中,脈衝響應常用於模擬和重現不同的聲音環境,如演唱廳、錄音棚、房間等。通過獲取特定環境的脈衝響應,可以創建一個模型或仿真,使得音頻信號經過該模型處理後,能夠模擬出與原始環境類似的聲音效果。脈衝響應可以反映出系統對不同頻率的音頻信號的處理方式,包括頻率響應、時域特性和空間特性等
var impulseResponseBuffer = null;
function loadImpulseResponse() {
loadBuffer('impulse.wav', function(buffer) {
impulseResponseBuffer = buffer;
});
}
function play() {
// 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();
// Set the impulse response buffer.
convolver.buffer = impulseResponseBuffer;
// Connect graph.
source.connect(convolver);
convolver.connect(context.destination);
}
卷積節點通過計算卷積來“smushed”輸入聲音及其脈衝響應, 一個數學上加強函數。結果聽起來好像是在記錄脈衝響應的房間裏產生的。在實踐中,通常將原始聲音(稱爲幹混音)與卷積聲音(稱爲溼混音)混合在一起是有意義的,並使用等功率交叉漸變來控制您想要應用的效果的多少.
當然你也可以自己合成這些脈衝反應,但這個主題超出了本書的範圍。
譯者注:我在網上下載了一個免費的 impulse.wav 來自:http://www.cksde.com/p_6_250.htm
效果相當好,實現了一個模擬迴響的效果可參考:https://github.com/willian12345/WebAudioAPI/tree/master/examples/ch06/impulse-demo.html
空間聲音
遊戲通常被設定在一個有多物體位置的空間世界, 無論是2D 還是3D. 如果是這樣的話,空間化音頻可以大大增加體驗的沉浸感. 很幸運地是,Web Audio API 自帶 空間化音頻的特性(立體聲現在用)使用起來很簡單。
你試聽一下看空間音頻,推薦用立體音響(更好的方式當然是耳機)。這將使您更好地瞭解左右通道是如何通過空間化方法進行轉換的。
Web Audio API模型有三個方面的複雜性,其中許多概念借鑑於OpenAL:
-
聽者與資源的位置與方向
-
與源音錐(描述定向聲音響度的模型稱爲音錐)相關聯的參數
-
源和聽者的相對速度
譯者注: 音錐,描述定向聲音的響度的模型,正確設計音錐可以給應用程序增加戲劇性的效果。 例如,可以將聲源放置在房間的中心,將其方向設置爲走廊中打開的門。 然後設置內部錐體的角度,使其擴展到門道的寬度,使外部圓錐稍微寬一點,最後將外部錐體音量設置爲聽不見。 沿着走廊移動的聽衆只有在門口附近纔會開始聽到聲音。 當聽衆在打開的門前經過時,聲音將是最響亮的。
沒有方向的聲音在所有方向的給定距離處具有相同的振幅。 具有方向的聲音在方向方向上響亮。 描述定向聲音響度的模型稱爲音錐。 音錐由內部 (或內部) 錐和外部 (或外部) 錐組成。外錐角必須始終等於或大於內錐角。
音錐解釋引自 https://learn.microsoft.com/zh-cn/windows/win32/xaudio2/sound-cones
Web Audio API 上下文中有一個監聽器(audiollistener),可以通過位置和方向參數在空間中進行配置。每個源都可以通過一個panner節點(AudioPannerNode)傳遞,該節點對輸入音頻進行空間化。
基於音源和聽者的相對位置,Web Audio API 計算出正確的增益修改。
一些需要提前知曉的設定。首先聽者的原始位置座標默認爲(0, 0, 0)。 位置API座標是無單位的,所以在實踐中,需要一些乘數調整使其如你預期的那樣。其次,方向特殊指向的單位向量。最後,在此座標空間內,y 朝向是向上的,這與大多數計算機圖形系統正好相反。
知道了這些設定,下面是一個通過 (PannerNode) 在 2D 空間改變音源節點位置的例子:
// Position the listener at the origin (the default, just added for the sake of being explicit)
context.listener.setPosition(0, 0, 0);
// Position the panner node.
// Assume X and Y are in screen coordinates and the listener is at screen center.
var panner = context.createPanner();
var centerX = WIDTH/2;
var centerY = HEIGHT/2;
var x = (X - centerX) / WIDTH;
// The y coordinate is flipped to match the canvas coordinate space.
var y = (Y - centerY) / HEIGHT;
// Place the z coordinate slightly in behind the listener.
var z = -0.5;
// Tweak multiplier as necessary.
var scaleFactor = 2;
panner.setPosition(x * scaleFactor, y * scaleFactor, z);
// Convert angle into a unit vector.
panner.setOrientation(Math.cos(angle), -Math.sin(angle), 1);
// Connect the node you want to spatialize to a panner.
source.connect(panner);
除了考慮相對位置和方向外,每個源都有一個可配置的音頻錐,如圖 6-3.
圖 6-3 二維空間裏的調音器和聽者示意圖
一旦你指定了一個內錐體和一個外錐體,你最終會把空間分成三個部分,如圖 6-3 所示:
- Inner cone
- Outer cone
- Neither cone
每個子空間都有一個增益乘法器,作爲位置模型的額外提示。例如,要模擬目標聲音,我們可能需要以下配置:
panner.coneInnerAngle = 5;
panner.coneOuterAngle = 10;
panner.coneGain = 0.5;
panner.coneOuterGain = 0.2;
分散的聲音可能有一組非常不同的參數。全向源有一個360度的內錐,其方位對空間化沒有影響:
panner.coneInnerAngle = 180;
panner.coneGain = 0.5;
除了位置、方向和音錐,聲源和聽者也可以指定速度。這個值對於模擬多普勒效應引起的音高變化是很重要的
用 JavaScript 處理
一般來說,Web Audio API 目的是提供足夠的基原(大多是通過 音頻節點)能力用於處理音頻任務。這些模塊是用c++編寫的,比用JavaScript編寫的代碼要快得多。
然而, 此 API 還提供了一個叫 ScriptProcessorNode 的節點,讓網頁開發者直接使用 JavaScript 來合成和處理音頻。例如,通過此種方式繼承實現自定義的 DSP 數字信號處理器, 或一些圖像概念的教學app。
開始前先創建一個 ScriptProcessorNode。此節點以 chunks 形式處理聲音,通過傳遞指定 bufferSize 給此節點,值必須是2的冪。最好使用更大的緩衝區,因爲如果主線程忙於其他事情(如頁面重新佈局、垃圾收集或JavaScript回調),它可以爲您提供更多的安全餘量,以防止出現故障:
// Create a ScriptProcessorNode.
var processor = context.createScriptProcessor(2048);
// Assign the onProcess function to be called for every buffer.
processor.onaudioprocess = onProcess;
// Assuming source exists, connect it to a script processor.
source.connect(processor);
譯者注:createScriptProcessor 已廢棄,用 AudioWorklets 取代
一旦將音頻數據導入 JavaScript 函數後,可以通過檢測輸入緩衝區來分析輸入的音頻流,或者通過修改輸出緩衝區直接更改輸出。例如,我們可以通過實現下面的腳本處理器輕鬆地交換左右通道:
function onProcess(e) {
var leftIn = e.inputBuffer.getChannelData(0);
var rightIn = e.inputBuffer.getChannelData(1);
var leftOut = e.outputBuffer.getChannelData(0);
var rightOut = e.outputBuffer.getChannelData(1);
for (var i = 0; i < leftIn.length; i++) {
// Flip left and right channels.
leftOut[i] = rightIn[i];
rightOut[i] = leftIn[i];
}
}
需要注意的是,你不應該在生產環境下使用這種方式實現聲道的切換。因爲使用 ChannelMergerNode 的 ChannelSplitterNode 更高效。在另一個例子中,我們可以混入一些隨機噪聲。通過對信號添加一些簡單的隨機位置。通過完全隨機信號,我們可以得到白噪聲,這個在很多應用中非常有用。
function onProcess(e) {
var leftOut = e.outputBuffer.getChannelData(0);
var rightOut = e.outputBuffer.getChannelData(1);
for (var i = 0; i < leftOut.length; i++) {
// Add some noise
leftOut[i] += (Math.random() - 0.5) * NOISE_FACTOR;
rightOut[i] += (Math.random() - 0.5) * NOISE_FACTOR;
}
}
最主要的問題還是在於性能。用 Javascript 去實現相比於瀏覽器內置的實現要慢的多的多。
注:轉載請註明出處博客園:王二狗Sheldon池中物 ([email protected])