简体   繁体   中英

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?

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM