Chrome 64 中新增了 Web Audio API 中备受期待的新功能 - AudioWorklet。在本课程中,您将学习使用 JavaScript 代码创建自定义音频处理器的概念和用法。查看实时演示。如需了解如何构建高级音频应用,不妨阅读本系列的下一篇文章:音频 Worklet 设计模式。
背景:ScriptProcessorNode
Web Audio API 中的音频处理在与主界面线程分离的线程中运行,因此可以顺畅运行。为了在 JavaScript 中实现自定义音频处理,Web Audio API 提出了 ScriptProcessorNode,该节点使用事件处理脚本在主界面线程中调用用户脚本。
这种设计存在两个问题:事件处理是异步的,并且代码执行在主线程上进行。前者会导致延迟,后者会给主线程带来压力,主线程通常会被各种与界面和 DOM 相关的任务挤满,导致界面“卡顿”或音频“故障”。由于这个根本性的设计缺陷,ScriptProcessorNode
已从规范中废弃,并替换为 AudioWorklet。
概念
Audio Worklet 会将用户提供的 JavaScript 代码全部保留在音频处理线程中。也就是说,它不必跳转到主线程来处理音频。这意味着,用户提供的脚本代码会与其他内置 AudioNodes
一起在音频渲染线程 (AudioWorkletGlobalScope
) 上运行,从而确保没有额外的延迟和同步渲染。
注册和实例化
使用音频 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()
调用加载和注册处理器定义。Worklet API(包括 Audio Worklet)仅在安全上下文中可用,因此使用它们的网页必须通过 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()
中的 promise,以通知用户类定义已准备好在主全局范围内使用。
自定义音频参数
AudioNode 的一个实用功能是可使用 AudioParam
进行可调度的参数自动化。AudioWorkletNode 可以使用这些参数来获取可自动以音频速率控制的公开参数。
您可以通过设置一组 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 模块。这为音频 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);
本部分介绍了音频 Worklet 系统的基础知识。如需查看实时演示,请访问 Chrome WebAudio 团队的 GitHub 代码库。
功能转换:从实验性转换为稳定版
Chrome 66 或更高版本默认启用音频 Worklet。在 Chrome 64 和 65 中,该功能需要启用实验性标志才能使用。