Chrome 64 在 Web Audio API 中加入了備受期待的新功能:AudioWorklet。本文將介紹如何使用 JavaScript 程式碼建立自訂音訊處理器的概念和用法。請前往 GitHub 查看即時示範。此外,下一篇系列文章「Audio Worklet Design Pattern」文章或許也會探討如何建構進階音訊應用程式。
背景:ScriptProcessorNode
Web Audio API 中的音訊處理會在主要 UI 執行緒外的獨立執行緒中執行,因此可順暢執行。為了在 JavaScript 中啟用自訂音訊處理功能,Web Audio API 提議使用 ScriptProcessorNode,此指令碼會使用事件處理常式叫用主 UI 執行緒中的使用者指令碼。
這項設計有兩個問題:事件處理為非同步設計,而程式碼執行作業是在主執行緒上執行。前者會引發延遲時間,後者則對主要執行緒設有壓力,因為這類工作常因各種 UI 和 DOM 相關工作而擁擠,導致 UI 無法「卡頓」或音訊「故障」。由於這項基本設計瑕疵,ScriptProcessorNode
已從規格中淘汰,並替換為 AudioWorklet。
概念
Audio Worklet 可將使用者提供的 JavaScript 程式碼妥善保存在音訊處理執行緒中,也就是說,它無須跳到主執行緒來處理音訊。這表示使用者提供的指令碼程式碼會與其他內建 AudioNode 一起在音訊轉譯執行緒 (AudioWorkletGlobalScope) 上執行,確保不會發生額外延遲並同步轉譯。
註冊與建立例項
使用音訊小程式包含兩個部分:AudioWorkletProcessor 和 AudioWorkletNode。這比使用 ScriptProcessorNode 還複雜,但為了讓開發人員擁有低階自訂音訊處理能力,這是必要行為。AudioWorkletProcessor 代表以 JavaScript 程式碼編寫的實際音訊處理器,並位於 AudioWorkletGlobalScope。AudioWorkletNode 是 AudioWorkletProcessor 的對應產品,會處理與主執行緒中其他 AudioNode 之間的連線。它會在主要全域範圍和功能中公開,就像一般的 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()
呼叫,載入及註冊處理器定義。包含音訊 Worklet 的 Worklet API 只能在安全環境中提供,因此使用這類 API 的網頁必須透過 HTTPS 提供,儘管 http://localhost
被視為在本機測試的安全。
另外值得注意的是,您可以將 AudioWorkletNode 設為子類別,定義自訂節點由在 Worklet 上執行的處理器支援。
// This is "processor.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()
的承諾,通知使用者類別定義已準備好用於主要全域範圍。
自訂 AudioParam
AudioNode 是可排程的參數自動化作業,其中一項實用的 AudioNodes。AudioWorkletNode 可以使用這些參數取得可自動以音訊速率控制的已公開參數。
您可以設定一組 AudioParamDescriptors,藉此在 AudioWorkletProcessor 類別定義中宣告使用者定義的 AudioParams。基礎 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 的雙向通訊
有時自訂 AudioWorkletNodes 會想公開未對應至 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!');
});
/* "processor.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 支援 Transferable,讓您可以在執行緒邊界上轉移資料儲存空間或 WASM 模組。這開啟了 Audio Worklet 系統的運用方式,造成各種可能。
逐步操作說明:建構 GainNode
以下提供在 AudioWorkletNode 和 AudioWorkletProcessor 基礎上建構的 GainNode 完整範例。
Index.html
<!doctype html>
<html>
<script>
const context = new AudioContext();
// Loads module script via 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>
get-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 以上版本會預設啟用「音訊小程式」。在 Chrome 64 和 65 版中 這個功能則位於實驗性旗標後面