网络音频 API 使用 AnalyserNode 创建峰值计

Web Audio API creating a Peak Meter with AnalyserNode

实施 Peak Meter like those in Logic Pro with the Web Audio API AnalyserNode 的正确方法是什么?

我知道 AnalyserNode.getFloatFrequencyData() returns 分贝值,但如何组合这些值才能在仪表中显示?您是否像下面的代码示例一样取最大值(其中 analyserData 来自 getFloatFrequencyData():

let peak = -Infinity;
for (let i = 0; i < analyserData.length; i++) {
  const x = analyserData[i];
  if (x > peak) {
    peak = x;
  }
}

检查仅取最大值的一些输出使得这看起来不是正确的方法。我错了吗?

或者,改用 ScriptProcessorNode 会更好吗?这种方法有何不同?

Do you just take the maximum value

对于峰值计,是的。对于 VU 表,在测量功率以及模拟表的弹道方面有各种各样的考虑因素。还有 RMS 功率计量。

在数字领域,您会发现峰值计对许多任务最有用,而且迄今为止最容易计算。

任何给定样本集的峰值是该集中的最高绝对值。不过,首先,您需要那组样本。如果您调用 getFloatFrequencyData(),您得到的不是样本值,而是频谱。你想要的是 getFloatTimeDomainData()。此数据是样本的低分辨率表示。也就是说,您的 window 中可能有 4096 个样本,但您的分析器可能配置了 256 个桶...因此这 4096 个样本将被重新采样为 256 个样本。这对于计量任务来说通常是可以接受的。

从那里开始,只需 Math.max(-Math.min(samples), Math.max(samples)) 即可获得绝对值的最大值。

假设您想要更高分辨率的峰值计。为此,您需要获得所有原始样品。这就是 ScriptProcessorNode 派上用场的地方。您可以访问实际示例数据。

基本上,对于此任务,AnalyserNode 速度更快,但分辨率略低。 ScriptProcessorNode 慢得多,但分辨率稍高。

如果你在一帧中取getFloatFrequencyData()的最大值,那么你测量的是音频功率在单一频率(取其有最强大)。您实际想要测量的是 any 频率处的峰值 — 换句话说,您想要 not 使用频率数据,但未处理的样本没有分成频率仓。

问题是您必须自己计算分贝功率。这是相当简单的算术:你取一些样本(一个或多个),对它们进行平方,然后取平均值。请注意,即使是“峰值”仪表也可能会进行平均 - 只是在更短的时间范围内。

这是一个完整的例子。 (警告:发出声音。)

document.getElementById('start').addEventListener('click', () => {
  const context = new(window.AudioContext || window.webkitAudioContext)();

  const oscillator = context.createOscillator();
  oscillator.type = 'square';
  oscillator.frequency.value = 440;
  oscillator.start();

  const gain1 = context.createGain();

  const analyser = context.createAnalyser();

  // Reduce output level to not hurt your ears.
  const gain2 = context.createGain();
  gain2.gain.value = 0.01;

  oscillator.connect(gain1);
  gain1.connect(analyser);
  analyser.connect(gain2);
  gain2.connect(context.destination);

  function displayNumber(id, value) {
    const meter = document.getElementById(id + '-level');
    const text = document.getElementById(id + '-level-text');
    text.textContent = value.toFixed(2);
    meter.value = isFinite(value) ? value : meter.min;
  }

  // Time domain samples are always provided with the count of
  // fftSize even though there is no FFT involved.
  // (Note that fftSize can only have particular values, not an
  // arbitrary integer.)
  analyser.fftSize = 2048;
  const sampleBuffer = new Float32Array(analyser.fftSize);

  function loop() {
    // Vary power of input to analyser. Linear in amplitude, so
    // nonlinear in dB power.
    gain1.gain.value = 0.5 * (1 + Math.sin(Date.now() / 4e2));

    analyser.getFloatTimeDomainData(sampleBuffer);

    // Compute average power over the interval.
    let sumOfSquares = 0;
    for (let i = 0; i < sampleBuffer.length; i++) {
      sumOfSquares += sampleBuffer[i] ** 2;
    }
    const avgPowerDecibels = 10 * Math.log10(sumOfSquares / sampleBuffer.length);

    // Compute peak instantaneous power over the interval.
    let peakInstantaneousPower = 0;
    for (let i = 0; i < sampleBuffer.length; i++) {
      const power = sampleBuffer[i] ** 2;
      peakInstantaneousPower = Math.max(power, peakInstantaneousPower);
    }
    const peakInstantaneousPowerDecibels = 10 * Math.log10(peakInstantaneousPower);

    // Note that you should then add or subtract as appropriate to
    // get the _reference level_ suitable for your application.

    // Display value.
    displayNumber('avg', avgPowerDecibels);
    displayNumber('inst', peakInstantaneousPowerDecibels);

    requestAnimationFrame(loop);
  }
  loop();
});
<button id="start">Start</button>

<p>
  Short average
  <meter id="avg-level" min="-100" max="10" value="-100"></meter>
  <span id="avg-level-text">—</span> dB
</p>

<p>
  Instantaneous
  <meter id="inst-level" min="-100" max="10" value="-100"></meter>
  <span id="inst-level-text">—</span> dB
</p>