Search code examples
javascriptaudioweb-audio-api

Web Audio API creating a Peak Meter with AnalyserNode


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?


Solution

  • 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>