原文:https://developers.google.com/web/updates/2017/12/audio-worklet#concepts
Chrome 从 64 版本开始在 Web Audio API 中带来了新功能 - AudioWorklet。 本文章主要是为那些渴望使用 JavaScript 构建自定义音频处理器的人介绍其概念和用法。 请查看 GitHub 上的实时演示。 同样该系列的下一篇 Audio Worklet Design Pattern 也会是构建高级音频程序的一篇有意思的文章。
背景:ScriptProcessorNode
Web Audio API 中的音频处理,和主 UI 线程是不同的线程,因此运行流畅。 为了在 JavaScript 中启用自定义音频处理,Web Audio API 推出
ScriptProcessorNode
,它通过 event handler 在主 UI 线程中调用用户脚本。这种设计里有两个问题:事件处理是异步设计的,代码执行发生在主线程上。 前者会导致延迟,后者给处理各种 UI 和 DOM 相关任务的主线程带来压力,导致 UI“卡顿”或音频“毛刺”。 由于这个基础的设计缺陷,
ScriptProcessorNode
从规范中被废弃,取而代之的是 AudioWorklet
。概念
AudioWorklet
很好地将用户提供的 JavaScript 代码保存在音频处理线程中——也就是说,它不必跳到主线程来处理音频。 这意味着用户提供的代码可以与其他内置 AudioNodes
一起在音频渲染线程 (AudioWorkletGlobalScope
) 上运行,从而确保零额外延迟和同步渲染。注册和实例化
使用
AudioWorklet
由两部分组成: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
对象和processor
的字符串名称。processor
定义可以通过新的 AudioWorklet
对象的 addModule()
调用加载和注册。 包括 AudioWorklet
在内的 Worklet 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()
方法采用一个字符串作为即将注册的processor
名称和类定义。 在全局作用域内完成代码赋值后, AudioWorklet.addModule()
的promise
在resolve
则表明知全局作用域里,类定义完成可以使用。自定义音频参数
AudioNodes
的一项有用功能,是使用 AudioParams
可以自动来调度参数。 AudioWorkletNodes
可以使用这些来获取可以自动控制音频速率的公开参数。通过设置一组
AudioParamDescriptor
,可以在 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 的控件。例如,可以使用基于字符串的类型属性来控制自定义筛选器。为此,AudioWorkletNode 和 AudioWorkletProcessor 配备了用于双向通信的 MessagePort。任何类型的自定义数据都可以通过此通道进行交换。
可以通过以下方式访问 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 模块。这就为如何利用音频工作站系统开辟了无数的可能性。
Eg: 构建 GainNode
下面是构建在 AudioWorkletNode 和 AudioWorkletProcessor 之上的 GainNode 的完整示例。
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);
Experimental to Stable
在 Chrome66或更高版本中启用了 AudioWorklet。在 Chrome64和 Chrome65 中,这个功能位在 experimental flag。
相关信息
- Chrome 66 release date: April 17, 2018