进入音频 Worklet

Hongchan Choi

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) 上运行,这可确保零额外延迟并实现同步渲染。

主全局作用域和音频 Worklet 作用域图
图 1

注册和实例化

使用 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 可以使用它们获取可以自动按音频速率控制的公开参数。

音频 Worklet 节点和处理器示意图
图 2

可以通过设置一组 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。任何类型的自定义数据都可以通过此渠道交换。

Fig.2
图 2

您可以通过节点和处理器上的 .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 中,该功能位于实验标志后面。