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);
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);
}
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
}
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 + '%';
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();
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
});
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();
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)