Enter Audio Worklet

Enter Audio Worklet

Tags
WebMedia
WebAudio
Published
Apr 9, 2021
原文:https://developers.google.com/web/updates/2017/12/audio-worklet#concepts
 
Chrome 从 64 版本开始在 Web Audio API 中带来了新功能 - AudioWorklet。 本文章主要是为那些渴望使用 JavaScript 构建自定义音频处理器的人介绍其概念和用法。 请查看 GitHub 上的实时演示。 同样该系列的下一篇 Audio Worklet Design Pattern 也会是构建高级音频程序的一篇有意思的文章。

背景:ScriptProcessorNode

Web Audio API 中的音频处理,和主 UI 线程是不同的线程,因此运行流畅。 为了在 JavaScript 中启用自定义音频处理,Web Audio API 推出 ScriptProcessorNode,它通过 event handler 在主 UI 线程中调用用户脚本。
这种设计里有两个问题:事件处理是异步设计的,代码执行发生在主线程上。 前者会导致延迟,后者给处理各种 UI 和 DOM 相关任务的主线程带来压力,导致 UI“卡顿”或音频“毛刺”。 由于这个基础的设计缺陷,ScriptProcessorNode 从规范中被废弃,取而代之的是 AudioWorklet

概念

AudioWorklet 很好地将用户提供的 JavaScript 代码保存在音频处理线程中——也就是说,它不必跳到主线程来处理音频。 这意味着用户提供的代码可以与其他内置 AudioNodes 一起在音频渲染线程 (AudioWorkletGlobalScope) 上运行,从而确保零额外延迟和同步渲染。
notion image

注册和实例化

使用 AudioWorklet由两部分组成:AudioWorkletProcessorAudioWorkletNode。 这比使用 ScriptProcessorNode 更复杂,但是为了给开发者提供低阶的能力来自定义处理音频是必须的。 AudioWorkletProcessor 代表用 JavaScript 代码编写的实际音频处理器,它位于 AudioWorkletGlobalScope 中。 AudioWorkletNodeAudioWorkletProcessor 对应的,负责与主线程中其他 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对象和processor的字符串名称。processor定义可以通过新的 AudioWorklet 对象的 addModule() 调用加载和注册。 包括 AudioWorklet 在内的 Worklet 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);
processor.js
AudioWorkletGlobalScope 中的 registerProcessor() 方法采用一个字符串作为即将注册的processor名称和类定义。 在全局作用域内完成代码赋值后, AudioWorklet.addModule()promiseresolve 则表明知全局作用域里,类定义完成可以使用。
 

自定义音频参数

AudioNodes 的一项有用功能,是使用 AudioParams 可以自动来调度参数。 AudioWorkletNodes 可以使用这些来获取可以自动控制音频速率的公开参数。
notion image
通过设置一组 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.
    }
  }
}
my-worklet-processor.js

AudioWorkletProcessor.process() 方法

实际的音频处理发生在 AudioWorkletProcessorprocess() 回调方法中,它必须由用户在类定义中实现。 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 的控件。例如,可以使用基于字符串的类型属性来控制自定义筛选器。为此,AudioWorkletNode 和 AudioWorkletProcessor 配备了用于双向通信的 MessagePort。任何类型的自定义数据都可以通过此通道进行交换。
notion image
可以通过以下方式访问 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 模块。这就为如何利用音频工作站系统开辟了无数的可能性。
 

Eg: 构建 GainNode

下面是构建在 AudioWorkletNode 和 AudioWorkletProcessor 之上的 GainNode 的完整示例。
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);

Experimental to Stable

在 Chrome66或更高版本中启用了 AudioWorklet。在 Chrome64和 Chrome65 中,这个功能位在 experimental flag。

相关信息

  1. Chrome 66 release date: April 17, 2018