最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Web Audio API creating a Peak Meter with AnalyserNode - Stack Overflow

programmeradmin2浏览0评论

What is the correct way to implement a Peak Meter like those in Logic Pro with the Web Audio API AnalyserNode?

I know AnalyserNode.getFloatFrequencyData() returns decibel values, but how do you combine those values to get the one to be displayed in the meter? Do you just take the maximum value like in the following code sample (where analyserData comes from getFloatFrequencyData():

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

Inspecting some output from just taking the max makes it look like this is not the correct approach. Am I wrong?

Alternatively, would it be a better idea to use a ScriptProcessorNode instead? How would that approach differ?

What is the correct way to implement a Peak Meter like those in Logic Pro with the Web Audio API AnalyserNode?

I know AnalyserNode.getFloatFrequencyData() returns decibel values, but how do you combine those values to get the one to be displayed in the meter? Do you just take the maximum value like in the following code sample (where analyserData comes from getFloatFrequencyData():

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

Inspecting some output from just taking the max makes it look like this is not the correct approach. Am I wrong?

Alternatively, would it be a better idea to use a ScriptProcessorNode instead? How would that approach differ?

Share Improve this question edited Mar 21, 2023 at 15:33 Marcel 15.7k22 gold badges103 silver badges164 bronze badges asked Jun 5, 2017 at 0:48 CalebmerCalebmer 2,8608 gold badges30 silver badges44 bronze badges 1
  • 1 @Kaiido AudioWorkers are implemented now? According to MDN, they aren't... In the mean time, there's nothing wrong with ScriptProcessorNode. – Brad Commented Jun 5, 2017 at 1:43
Add a comment  | 

2 Answers 2

Reset to default 17

If you take the maximum of getFloatFrequencyData()'s results in one frame, then what you are measuring is the audio power at a single frequency (whichever one has the most power). What you actually want to measure is the peak at any frequency — in other words, you want to not use the frequency data, but the unprocessed samples not separated into frequency bins.

The catch is that you'll have to compute the decibels power yourself. This is fairly simple arithmetic: you take some number of samples (one or more), square them, and average them. Note that even a “peak” meter may be doing averaging — just on a much shorter time scale.

Here's a complete example. (Warning: produces sound.)

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>

Do you just take the maximum value

For a peak meter, yes. For a VU meter, there's all sorts of considerations in measuring the power, as well as the ballistics of an analog meter. There's also RMS power metering.

In digital land, you'll find a peak meter to be most useful for many tasks, and by far the easiest to compute.

A peak for any given set of samples is the highest absolute value in the set. First though, you need that set of samples. If you call getFloatFrequencyData(), you're not getting sample values, you're getting the spectrum. What you want instead is getFloatTimeDomainData(). This data is a low resolution representation of the samples. That is, you might have 4096 samples in your window, but your analyser might be configured with 256 buckets... so those 4096 samples will be resampled down to 256 samples. This is generally acceptable for a metering task.

From there, it's just Math.max(-Math.min(samples), Math.max(samples)) to get the max of the absolute value.

Suppose you wanted a higher resolution peak meter. For that, you need all the raw samples you can get. That's where a ScriptProcessorNode comes in handy. You get access to the actual sample data.

Basically, for this task, AnalyserNode is much faster, but slightly lower resolution. ScriptProcessorNode is much slower, but slightly higher resolution.

发布评论

评论列表(0)

  1. 暂无评论