音频 Worklet 现在默认可用

Hongchan Choi

Chrome 64 为 Web Audio API 推出了备受期待的新功能: AudioWorklet。本单元将向您介绍创建 自定义音频处理器和 JavaScript 代码。请参阅 现场演示。 本系列的下一篇文章音频 Worklet 设计模式 可能是构建高级音频应用的有趣读物。

背景:ScriptProcessorNode

Web Audio API 中的音频处理在独立于主线程的线程中运行 界面线程,以便它顺畅运行。要在 在 JavaScript 中,Web Audio API 建议了一个 ScriptProcessorNode, 事件处理脚本,以便在主界面线程中调用用户脚本。

这种设计存在两个问题:事件处理是异步的 而且代码执行在主线程上执行。前者 导致延迟,而后者会加大 通常与各种界面和 DOM 相关的任务拥挤,导致出现上述两个界面 更改为“卡顿”或音频出现“故障”。由于存在这个基本的设计缺陷 规范中废弃了 ScriptProcessorNode, 替换为 AudioWorklet。

概念

音频 Worklet 将用户提供的 JavaScript 代码 音频处理线程。也就是说,无需跳转到主页面, 线程处理音频。这意味着用户提供的脚本代码 在音频渲染线程 (AudioWorkletGlobalScope) 中与其他资源一起 内置 AudioNodes,可确保零额外延迟和同步 呈现。

<ph type="x-smartling-placeholder">
</ph> 主全局范围和音频 Worklet 范围图 <ph type="x-smartling-placeholder">
</ph> Fig.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 在内的 Worklet API 仅在 安全上下文,因此 虽然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() 的承诺将得到解决,通知用户 表明类定义已准备好在主全局作用域中使用。

自定义音频参数

AudioNode 的一项实用功能是可调度参数 使用 AudioParam 实现自动化。AudioWorkletNode 可以使用 可以自动以音频速率控制的公开参数。

<ph type="x-smartling-placeholder">
</ph> 音频 Worklet 节点和处理器示意图 <ph type="x-smartling-placeholder">
</ph> Fig.2

用户定义的音频参数可以在 AudioWorkletProcessor 中声明 定义一组 AudioParamDescriptor。通过 底层 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() 方法

实际的音频处理发生在process() AudioWorkletProcessor。它必须由类中的用户来实现 定义。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 用于双向通信。任何类型的自定义数据 都可通过该渠道进行交换

<ph type="x-smartling-placeholder">
</ph> Fig.2 <ph type="x-smartling-placeholder">
</ph> Fig.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

下面是 GainNode 基于 AudioWorkletNodeAudioWorkletProcessor

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 中,该功能位于实验性标记之后。