音频 Worklet 现在默认可用

Hongchan Choi

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 作用域示意图
图 1

注册和实例化

使用音频 Worklet 分为两个部分:AudioWorkletProcessorAudioWorkletNode。这比使用 ScriptProcessorNode 更复杂,但必须这样做才能为开发者提供自定义音频处理的低级功能。AudioWorkletProcessor 表示使用 JavaScript 代码编写的实际音频处理器,并且位于 AudioWorkletGlobalScope 中。AudioWorkletNodeAudioWorkletProcessor 的对应项,负责处理与主线程中其他 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 可以使用这些参数来获取可自动以音频速率控制的公开参数。

音频 worklet 节点和处理器图
图 2

您可以通过设置一组 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 属性。为此,AudioWorkletNodeAudioWorkletProcessor 配备了用于双向通信的 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!');
});
/* "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

下面是一个基于 AudioWorkletNodeAudioWorkletProcessor 构建的完整 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 中,该功能需要启用实验性标志才能使用。