I wanted something simple: open a tab, allow the microphone, and see roughly how loud the room is. No app install, no account, no audio leaving the device. That turned out to be a nice little lesson in how the Web Audio API actually works, and also in how honest you have to be about what a phone microphone can and cannot tell you.
Here's what I learned along the way.
1. Turn off the "helpful" processing
Browsers try to make your voice sound good on calls. For measuring sound, that's exactly what you don't want. Echo cancellation, noise suppression and auto gain will quietly flatten the very thing you're trying to measure.
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: false,
noiseSuppression: false,
autoGainControl: false,
},
});
const ctx = new AudioContext();
const source = ctx.createMediaStreamSource(stream);
const analyser = ctx.createAnalyser();
analyser.fftSize = 2048;
source.connect(analyser);
Some devices ignore these constraints anyway, which is the first hint that readings will vary between phones.
2. RMS, not peaks
A single sample tells you almost nothing. What you want is the average energy over a short window, which is the root mean square (RMS):
const buf = new Float32Array(analyser.fftSize);
function readLevel() {
analyser.getFloatTimeDomainData(buf);
let sum = 0;
for (const s of buf) sum += s * s;
const rms = Math.sqrt(sum / buf.length);
return 20 * Math.log10(Math.max(rms, 1e-8)); // dBFS, always <= 0
}
That number is in dBFS — decibels relative to the loudest value the microphone can digitise. It is not the same as the dB you see on a decibel chart (dB SPL). To get anywhere near dB SPL you need an offset, and that offset is different for every microphone.
3. The calibration problem is the real problem
This was the part that changed how I think about these tools. Two phones in the same room can disagree by 10 dB or more. The microphone, the preamp, the case, even which mic the browser picked all matter.
So the practical approach is:
apply a sensible default offset,
let the user adjust it against a known reference (a real meter, if they have one),
and clearly say the result is an estimate, not a certified measurement.
If you need a number for a dispute, a workplace or a legal question, you need a Class 1 or Class 2 sound level meter. A browser can help you notice patterns; it can't replace that.
4. Use requestAnimationFrame, and clean up
let running = true;
function tick() {
if (!running) return;
render(readLevel());
requestAnimationFrame(tick);
}
tick();
function stop() {
running = false;
stream.getTracks().forEach((t) => t.stop()); // turns off the mic indicator
ctx.close();
}
Forgetting track.stop() leaves the recording indicator on, and users notice. It's a small trust thing, but it matters.
Making the numbers mean something
A reading of "62 dB" is useless if you don't know what 62 dB sounds like. Pairing the meter with a plain reference scale — whisper, conversation, traffic, concert — made it far more useful than any amount of visual polish.
If you want to see the result of this approach, I put it together as a browser-based sound level meter alongside a decibel chart with everyday examples. Everything runs locally; nothing is recorded or uploaded.
Curious how others have handled calibration across devices — have you found anything better than a manual offset?
Top comments (0)