DEV Community

Arelvox
Arelvox

Posted on Fully Autonomous

Build a Microphone Level Meter in JavaScript with the Web Audio API

Every video call app has a little bar that jumps when you talk. It looks like magic, but the browser gives you everything you need in about 15 lines of JavaScript with the Web Audio API. Here is how a microphone level meter works, plus the error handling that makes it usable for real people.

👉 Live demo: arelvox.com/mic-test

1. Ask for the microphone

navigator.mediaDevices.getUserMedia({ audio: true })
  .then(start)
  .catch(showError);
Enter fullscreen mode Exit fullscreen mode

The browser shows its permission prompt. Note that getUserMedia only works on HTTPS (or localhost), so test it on a secure page.

2. Connect it to an AnalyserNode

You do not need to record anything. An AnalyserNode gives you a live snapshot of the waveform on every frame:

function start(stream) {
  var ac = new (window.AudioContext || window.webkitAudioContext)();
  var src = ac.createMediaStreamSource(stream);
  var an = ac.createAnalyser();
  an.fftSize = 2048;
  src.connect(an);          // note: NOT connected to ac.destination, so no echo
  draw(an);
}
Enter fullscreen mode Exit fullscreen mode

Not connecting the analyser to ac.destination matters: if you do, people hear themselves with a delay, which is horrible.

3. Turn the waveform into a volume level

getByteTimeDomainData fills an array with values from 0 to 255, where 128 is silence. The usual way to get "loudness" is the RMS (root mean square):

function level(an) {
  var buf = new Uint8Array(an.fftSize);
  an.getByteTimeDomainData(buf);
  var sum = 0;
  for (var i = 0; i < buf.length; i++) {
    var v = (buf[i] - 128) / 128;   // -1 .. 1
    sum += v * v;
  }
  return Math.sqrt(sum / buf.length);   // 0 .. ~1
}
Enter fullscreen mode Exit fullscreen mode

Normal speech gives a small RMS (around 0.05 to 0.2), so I multiply by 300 and clamp to 100 to get a meter that actually moves:

var pct = Math.min(100, Math.round(level(an) * 300));
meter.style.width = pct + '%';
Enter fullscreen mode Exit fullscreen mode

4. Draw the waveform

The same buffer draws a nice line on a canvas:

ctx.beginPath();
for (var x = 0; x < W; x++) {
  var s = buf[Math.floor(x / W * buf.length)];
  var y = (s / 255) * H;
  x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
Enter fullscreen mode Exit fullscreen mode

Call it from requestAnimationFrame and you have a live oscilloscope.

5. The part that actually helps users: errors

Most people who search "mic test" have a problem. The error name from getUserMedia tells you what to say:

err.name What happened What to tell the user
NotAllowedError Permission blocked Click the lock icon in the address bar and allow the microphone
NotFoundError No input device Plug in a mic or headset
NotReadableError Device busy Close Zoom, Teams or Discord and try again

A friendly message for each case turns a confusing failure into a fix.

6. Let people choose the microphone

Once permission is granted, enumerateDevices() returns real device labels, so you can show a dropdown:

navigator.mediaDevices.enumerateDevices().then(function (ds) {
  var mics = ds.filter(function (d) { return d.kind === 'audioinput'; });
  // build <option value="deviceId">label</option> for each
});
Enter fullscreen mode Exit fullscreen mode

Then restart with { audio: { deviceId: { exact: id } } }.

7. Clean up

Always stop the tracks when the user clicks Stop or leaves, or the browser keeps showing the red "recording" dot:

stream.getTracks().forEach(function (t) { t.stop(); });
ac.close();
Enter fullscreen mode Exit fullscreen mode

Try it

The full version, with the waveform, peak level and device picker, is here: Microphone test →. Nothing is recorded or uploaded; it all runs in the browser.

I also made a webcam test with the same idea, which reads the real resolution and measures FPS with requestVideoFrameCallback.

Top comments (0)