Chrome 64 在 Web Audio API 中引入了备受期待的新功能 - AudioWorklet。本文向希望使用 JavaScript 代码创建自定义音频处理器的用户介绍其概念和用法。请查看 GitHub 上的实时演示。另外,您还可以阅读本系列的下一篇文章:音频 Worklet 设计模式,阅读此文章可能会对构建高级音频应用感兴趣。
背景:ScriptProcessorNode
Web Audio API 中的音频处理功能在独立于主界面线程的线程中运行,因此运行顺畅。为了在 JavaScript 中实现自定义音频处理,Web Audio API 提议了 ScriptProcessorNode,它使用事件处理脚本在主界面线程中调用用户脚本。
此设计存在两个问题:事件处理在设计上是异步的,并且代码执行在主线程上执行。前一种会导致延迟,而后一种会加压通常因各种界面和 DOM 相关任务而拥挤的主线程,从而导致界面“卡顿”或音频“干扰”。由于这一基本设计缺陷,ScriptProcessorNode
已从规范中废弃,取而代之的是 AudioWorklet。
概念
音频 Worklet 可以很好地将用户提供的 JavaScript 代码全部保留在音频处理线程中,也就是说,它不必跳转到主线程来处理音频。这意味着用户提供的脚本代码可以与其他内置 AudioNode 一起在音频呈现线程 (AudioWorkletGlobalScope) 上运行,这可确保零额外延迟并实现同步渲染。
注册和实例化
使用 Audio Worklet 包括两部分: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()
调用加载和注册处理器定义。包括 Audio 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()
中的 promise 将被解析,以通知用户类定义已准备好在主全局范围内使用。
自定义 AudioParam
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 的控件。例如,基于字符串的 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>
增益处理器.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);
这涵盖了音频 Worklet 系统的基础知识。Chrome WebAudio 团队的 GitHub 代码库中提供了实时演示。
功能转换:从实验性版本到稳定版
对于 Chrome 66 或更高版本,音频 Worklet 默认处于启用状态。在 Chrome 64 和 65 中,该功能位于实验标志后面。