Chrome 64 提供 Web Audio API 中備受期待的新功能 - AudioWorklet。您將在這裡瞭解相關概念和用法,以便使用 JavaScript 程式碼建立自訂音訊處理器。請參閱直播示範。系列文章的下一篇文章「音訊工作區設計模式」或許會是建構進階音訊應用程式的有趣讀物。
背景:ScriptProcessorNode
Web Audio API 中的音訊處理作業會在主 UI 執行緒以外的獨立執行緒中執行,因此運作順暢。為在 JavaScript 中啟用自訂音訊處理功能,Web Audio API 提出了 ScriptProcessorNode,這個節點會使用事件處理常式,在主要 UI 執行緒中叫用使用者指令碼。
這項設計有兩個問題:事件處理是異步設計,且程式碼執行作業會在主執行緒上發生。前者會導致延遲,後者會對主要執行緒施加壓力,而主要執行緒通常會擠滿各種 UI 和 DOM 相關工作,導致 UI 出現「卡頓」或音訊出現「異常」。由於這個基本設計缺陷,ScriptProcessorNode
已從規格中淘汰,並由 AudioWorklet 取代。
概念
Audio Worklet 會將使用者提供的 JavaScript 程式碼保留在音訊處理執行緒中。也就是說,它不必跳轉至主執行緒即可處理音訊。這表示使用者提供的指令碼會在音訊轉譯執行緒 (AudioWorkletGlobalScope
) 上執行,並與其他內建 AudioNodes
搭配運作,確保沒有額外的延遲和同步轉譯。
註冊和例項化
使用 Audio Worklet 包含兩個部分:AudioWorkletProcessor
和 AudioWorkletNode
。這比使用 ScriptProcessorNode 更複雜,但必須提供開發人員自訂音訊處理的低階功能。AudioWorkletProcessor
代表以 JavaScript 程式碼編寫的實際音訊處理器,且位於 AudioWorkletGlobalScope
中。AudioWorkletNode
是 AudioWorkletProcessor
的對應項目,負責處理主執行緒中與其他 AudioNodes
的連線。這個函式會在主要全域範圍中公開,並像一般 AudioNode
一樣運作。
以下是兩個程式碼片段,分別示範註冊和例項化。
// The code in the main global scope.
class MyWorkletNode extends AudioWorkletNode {
constructor(context) {
super(context, 'my-worklet-processor');
}
}
let context = new AudioContext();
context.audioWorklet.addModule('processors.js').then(() => {
let node = new MyWorkletNode(context);
});
如要建立 AudioWorkletNode
,您必須新增 AudioContext 物件和處理器名稱做為字串。您可以透過新的 Audio Worklet 物件的 addModule()
呼叫,載入及註冊處理器定義。音訊工作區 API 等工作區 API 只能在安全環境中使用,因此使用這些 API 的網頁必須透過 HTTPS 提供,但 http://localhost
在本機測試時視為安全。
您可以將 AudioWorkletNode
設為子類別,定義由在 worklet 上執行的處理器支援的自訂節點。
// This is the "processors.js" file, evaluated in AudioWorkletGlobalScope
// upon audioWorklet.addModule() call in the main global scope.
class MyWorkletProcessor extends AudioWorkletProcessor {
constructor() {
super();
}
process(inputs, outputs, parameters) {
// audio processing code here.
}
}
registerProcessor('my-worklet-processor', MyWorkletProcessor);
AudioWorkletGlobalScope
中的 registerProcessor()
方法會使用字串,用於註冊的處理器名稱和類別定義。在全球範圍中完成指令碼評估後,系統會解析來自 AudioWorklet.addModule()
的承諾,通知使用者類別定義已準備好在主要全球範圍中使用。
自訂音訊參數
AudioNodes 的其中一個實用功能,就是可透過 AudioParam
排程參數自動化。AudioWorkletNodes 可以使用這些參數,取得可自動以音訊速率控制的公開參數。
您可以設定一組 AudioParamDescriptor
,在 AudioWorkletProcessor
類別定義中宣告使用者定義的音訊參數。基礎 WebAudio 引擎會在建構 AudioWorkletNode 時擷取這項資訊,然後相應地建立 AudioParam
物件並連結至節點。
/* A separate script file, like "my-worklet-processor.js" */
class MyWorkletProcessor extends AudioWorkletProcessor {
// Static getter to define AudioParam objects in this custom processor.
static get parameterDescriptors() {
return [{
name: 'myParam',
defaultValue: 0.707
}];
}
constructor() { super(); }
process(inputs, outputs, parameters) {
// |myParamValues| is a Float32Array of either 1 or 128 audio samples
// calculated by WebAudio engine from regular AudioParam operations.
// (automation methods, setter) Without any AudioParam change, this array
// would be a single value of 0.707.
const myParamValues = parameters.myParam;
if (myParamValues.length === 1) {
// |myParam| has been a constant value for the current render quantum,
// which can be accessed by |myParamValues[0]|.
} else {
// |myParam| has been changed and |myParamValues| has 128 values.
}
}
}
AudioWorkletProcessor.process()
方法
實際的音訊處理作業會在 AudioWorkletProcessor
的 process()
回呼方法中執行。使用者必須在類別定義中實作此方法。WebAudio 引擎會以非同步方式叫用這個函式,以便提供輸入內容和參數,並擷取輸出內容。
/* AudioWorkletProcessor.process() method */
process(inputs, outputs, parameters) {
// The processor may have multiple inputs and outputs. Get the first input and
// output.
const input = inputs[0];
const output = outputs[0];
// Each input or output may have multiple channels. Get the first channel.
const inputChannel0 = input[0];
const outputChannel0 = output[0];
// Get the parameter value array.
const myParamValues = parameters.myParam;
// if |myParam| has been a constant value during this render quantum, the
// length of the array would be 1.
if (myParamValues.length === 1) {
// Simple gain (multiplication) processing over a render quantum
// (128 samples). This processor only supports the mono channel.
for (let i = 0; i < inputChannel0.length; ++i) {
outputChannel0[i] = inputChannel0[i] * myParamValues[0];
}
} else {
for (let i = 0; i < inputChannel0.length; ++i) {
outputChannel0[i] = inputChannel0[i] * myParamValues[i];
}
}
// To keep this processor alive.
return true;
}
此外,process()
方法的傳回值可用於控制 AudioWorkletNode
的生命週期,方便開發人員管理記憶體占用空間。從 process()
方法傳回 false
會將處理器標示為不活動,而 WebAudio
引擎也不會再叫用該方法。為確保處理器持續運作,方法必須傳回 true
。否則,系統最終會收集節點和處理器組合的垃圾。
使用 MessagePort 進行雙向通訊
有時,自訂 AudioWorkletNode
會想要公開不會對應至 AudioParam
的控制項,例如用於控制自訂篩選器的字串型 type
屬性。為此目的和其他用途,AudioWorkletNode
和 AudioWorkletProcessor
都配備了 MessagePort
,可進行雙向通訊。您可以透過這個管道交換任何類型的自訂資料。
您可以使用節點和處理器的 .port
屬性存取 MessagePort。節點的 port.postMessage()
方法會將訊息傳送至關聯處理器的 port.onmessage
處理常式,反之亦然。
/* The code in the main global scope. */
context.audioWorklet.addModule('processors.js').then(() => {
let node = new AudioWorkletNode(context, 'port-processor');
node.port.onmessage = (event) => {
// Handling data from the processor.
console.log(event.data);
};
node.port.postMessage('Hello!');
});
/* "processors.js" file. */
class PortProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.port.onmessage = (event) => {
// Handling data from the node.
console.log(event.data);
};
this.port.postMessage('Hi!');
}
process(inputs, outputs, parameters) {
// Do nothing, producing silent output.
return true;
}
}
registerProcessor('port-processor', PortProcessor);
MessagePort
支援可轉移功能,可讓您透過執行緒邊界傳輸資料儲存空間或 WASM 模組。這為 Audio Worklet 系統的使用方式開啟了無數的可能性。
操作說明:建構 GainNode
以下是建構在 AudioWorkletNode
和 AudioWorkletProcessor
之上的完整 GainNode 範例。
index.html
檔案:
<!doctype html>
<html>
<script>
const context = new AudioContext();
// Loads module script with AudioWorklet.
context.audioWorklet.addModule('gain-processor.js').then(() => {
let oscillator = new OscillatorNode(context);
// After the resolution of module loading, an AudioWorkletNode can be
// constructed.
let gainWorkletNode = new AudioWorkletNode(context, 'gain-processor');
// AudioWorkletNode can be interoperable with other native AudioNodes.
oscillator.connect(gainWorkletNode).connect(context.destination);
oscillator.start();
});
</script>
</html>
gain-processor.js
檔案:
class GainProcessor extends AudioWorkletProcessor {
// Custom AudioParams can be defined with this static getter.
static get parameterDescriptors() {
return [{ name: 'gain', defaultValue: 1 }];
}
constructor() {
// The super constructor call is required.
super();
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const gain = parameters.gain;
for (let channel = 0; channel < input.length; ++channel) {
const inputChannel = input[channel];
const outputChannel = output[channel];
if (gain.length === 1) {
for (let i = 0; i < inputChannel.length; ++i)
outputChannel[i] = inputChannel[i] * gain[0];
} else {
for (let i = 0; i < inputChannel.length; ++i)
outputChannel[i] = inputChannel[i] * gain[i];
}
}
return true;
}
}
registerProcessor('gain-processor', GainProcessor);
這篇文章將介紹 Audio Worklet 系統的基本概念。您可以在 Chrome WebAudio 團隊的 GitHub 存放區中查看實際操作示範。
功能轉換:從實驗版轉換為穩定版
Chrome 66 以上版本預設會啟用 Audio Worklet。在 Chrome 64 和 65 中,這項功能是實驗性功能旗標的後端。