用 AudioWorklet 来做白噪声

Posted on Thu 30 September 2021 in Journal

AudioNode 是什么

在 WebRTC 中 AudioNode 表示一个音频处理模块。它可以是用来表示一个音频源,一个音频目标,或者中间处理模块(例如 BiquadFilterNode , ConvolverNode, 或 GainNode)

每一个 AudioNode 都有一个输入和输出,多个音频节点连接在一起来构建一个音频处理图。这个图包含在一个 AudioContext , 每一个 AudioNode 只能隶属于一个 AudioContext.

源节点有零个输入但有一个或多个输出,可用于生成声音。另一方面,目标节点没有输出;相反,它的所有输入都直接在扬声器(或音频上下文使用的任何音频输出设备)上播放。此外,还有具有输入和输出的处理节点。完成的确切处理因一个 AudioNode 而异,但通常,一个节点读取其输入,进行一些与音频相关的处理,并为其输出生成新值,或让音频通过(例如在 AnalyserNode 中,其中处理的结果单独访问)。

图中的节点越多,延迟就越高。例如,如果您的图形有 500 毫秒的延迟,那么当源节点播放声音时,需要半秒时间才能在您的扬声器上听到该声音(或者甚至更长,因为底层音频设备的延迟)。因此,如果您需要具有交互式音频,请保持图形尽可能小,并将用户控制的音频节点放在图形的末尾。例如,音量控制 (GainNode) 应该是最后一个节点,以便音量更改立即生效。

每个输入和输出都有给定数量的通道。例如,单声道音频有一个通道,而立体声音频有两个通道。 Web Audio API 将根据需要对通道数量进行上混或下混;有关详细信息,请查看网络音频规范。

AudioWorklet 是什么

Web Audio API 的 AudioWorkletNode 接口代表用户定义的 AudioNode 的基类,它可以与其他节点一起连接到音频路由图。 它有一个关联的 AudioWorkletProcessor,它在 Web 音频渲染线程中进行实际的音频处理。

以一个白噪声生成器 NoiseGenerator 为例

Example 1: Noise generator node

  • noise-generator.html
const context = new AudioContext();
const demoCode = async (context) => {
        await context.audioWorklet.addModule('noise-generator.js');
        const modulator = new OscillatorNode(context);
        const modGain = new GainNode(context);
        const noiseGenerator = new AudioWorkletNode(context, 'noise-generator');
        noiseGenerator.connect(context.destination);

        // Connect the oscillator to 'amplitude' AudioParam.
        const paramAmp = noiseGenerator.parameters.get('amplitude');
        modulator.connect(modGain).connect(paramAmp);

        modulator.frequency.value = 0.5;
        modGain.gain.value = 0.75;
        modulator.start();
      };
}
document.getElementById("startButton").addEventListener("click", demoNode);
  • NoiseGenerator: noise-generator.js
**
 * A noise generator with a gain AudioParam.
 *
 * @class NoiseGenerator
 * @extends AudioWorkletProcessor
 */
class NoiseGenerator extends AudioWorkletProcessor {
  static get parameterDescriptors() {
    return [{name: 'amplitude', defaultValue: 0.25, minValue: 0, maxValue: 1}];
  }

  process(inputs, outputs, parameters) {
    const output = outputs[0];
    const amplitude = parameters.amplitude;
    const isAmplitudeConstant = amplitude.length === 1;

    for (let channel = 0; channel < output.length; ++channel) {
      const outputChannel = output[channel];
      for (let i = 0; i < outputChannel.length; ++i) {
        // This loop can branch out based on AudioParam array length, but
        // here we took a simple approach for the demonstration purpose.
        outputChannel[i] = 2 * (Math.random() - 0.5) *
            (isAmplitudeConstant ? amplitude[0] : amplitude[i]);
      }
    }

    return true;
  }
}

registerProcessor('noise-generator', NoiseGenerator);

Example 2: Gain Node

  • gain-processor.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>
  • 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);