Web Audio API 第6章 高階主題

池中物王二狗發表於2024-04-30

高階主題

這一章涵蓋了非常重要的主題,但比本書的其他部分稍微複雜一些。 我們會深入對聲音新增音效,完全不透過任何音訊緩衝來計算合成音效, 模擬不同聲音環境的效果,還有關於空 3D 空間音訊。

重要理論:雙二階濾波器

一個濾波可以增強或減弱聲音訊譜的某些部分。 直觀地,在頻域上它可以被表示為一個圖表被稱為“頻率響應圖”(見圖 6-1)。在每一個頻率上,對於每一個頻率,圖形的值越高,表示頻率範圍的那一部分越受重視。向下傾斜的圖表更多地強調低頻,而較少強調高頻。

Web Audio 濾鏡可配置3個引數: gain, frequency 和 質量因子( 常稱為 Q)。這些引數全部會不同程度影響頻率響應圖。

有很多種濾鏡可以用來達到特定的效果:

  • Low-pass 濾波
    使聲音更低沉

  • High-pass 濾波器
    使聲音更微小

  • Band-pass 濾波器
    截掉低點和高點(例如,電話濾波器)

  • Low-shelf 濾波器
    影響聲音中的低音量(如立體聲上的低音旋鈕)

  • Peaking 濾波器
    影響聲音中音的數量(如立體聲上的中音旋鈕)

  • Notch 濾波器
    去除窄頻率範圍內不需要的聲音

  • All-pass 濾波器
    建立相位效果

image

圖 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 可用的濾波器型別放到一起的頻率反應圖。

image

圖 6-2 帶引數的低通濾波器的頻率響應圖

用程式生成聲音

到目前為止,我們假定你遊戲中用到的都是靜態的聲音。音訊設計師自己建立並處理了一堆音訊資源,你負責根據當前條件使用一些引數控制播放這些音訊(舉例,房間內的背景音和音訊資源位置與聽眾)。這種實現方式有以下缺點:

  1. 聲音檔案可能會非常大。在網頁中尤其不好,與在本地磁碟載入不同,通常是透過網路載入的(特別是第一次載入時), 簡直慢了一個數量級。

  2. 就算擁有多眾多資源和變和簡單的變形,變化種類還是有限。

  3. 你需要透過搜尋音效庫來找到資產,然後可能還要擔心版權問題.另外,很有可能,任何給定的聲音效果已經在其他應用 程式中使用過,所以您的使用者會產生意想不到的關聯

我們完全可以利用程式使用 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:

  1. 聽者與資源的位置與方向

  2. 與源音錐(描述定向聲音響度的模型稱為音錐)相關聯的引數

  3. 源和聽者的相對速度

譯者注: 音錐,描述定向聲音的響度的模型,正確設計音錐可以給應用程式增加戲劇性的效果。 例如,可以將聲源放置在房間的中心,將其方向設定為走廊中開啟的門。 然後設定內部錐體的角度,使其擴充套件到門道的寬度,使外部圓錐稍微寬一點,最後將外部錐體音量設定為聽不見。 沿著走廊移動的聽眾只有在門口附近才會開始聽到聲音。 當聽眾在開啟的門前經過時,聲音將是最響亮的。

沒有方向的聲音在所有方向的給定距離處具有相同的振幅。 具有方向的聲音在方向方向上響亮。 描述定向聲音響度的模型稱為音錐。 音錐由內部 (或內部) 錐和外部 (或外部) 錐組成。外錐角必須始終等於或大於內錐角。

音錐解釋引自 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.

image

圖 6-3 二維空間裡的調音器和聽者示意圖

一旦你指定了一個內錐體和一個外錐體,你最終會把空間分成三個部分,如圖 6-3 所示:

  1. Inner cone
  2. Outer cone
  3. 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池中物 (willian12345@126.com)

相關文章