StreamLevels: matching mic levels before you go live
StreamLevels lets co-streamers compare mic levels in a shared lobby before going live. Only the numbers travel, no audio. Here's why I built it.
Last month I hopped on a co-stream and spent the first ten minutes sounding like I was broadcasting from inside a fridge. My guest was crisp and loud. I was a distant mumble. Nobody caught it until chat did. Classic.
Here's the thing about co-streaming: there's no shared reference for "are we even in the same range?" before you go live. Everyone has different mics, different interfaces, different gain staging. You each glance at your own meter in OBS, decide it looks fine, and hit Live. Then one of you is clipping and the other is a whisper, and you don't find out until it's already on tape.
So I built StreamLevels. It's dead simple: you spin up a lobby, send the link to whoever you're streaming with, and you all watch each other's mic levels on one screen in real time. Speak normally, nudge your gain until everyone's bars sit in the same zone, then go live. That's the whole app.
The one rule I cared about: no audio ever leaves your machine. Only the level number travels.
The browser was lying to me
First version, I got the meters drawing, wired up the lobby, tested it with a second laptop. And it worked... too well. Both meters magically sat at the same level no matter what I did to the gain. Cranked it, dropped it, didn't matter. The bars barely moved.
Took me an embarrassingly long time to figure out why. Chrome's getUserMedia turns on autoGainControl by default. The browser was helpfully normalizing my input level before I ever saw it, which is the one thing a level-matching tool absolutely cannot have. My app was measuring Chrome's idea of a good level, not mine.
The fix is three booleans: echoCancellation, noiseSuppression, and autoGainControl, all off. Now the meter shows your actual gain, warts and all. If you're running hot, you look hot. That's the entire point.
The best feature is the one I didn't build
The obvious way to let people "hear each other check levels" is to send audio. WebRTC, peer connections, the whole circus. I almost did it.
Then I realized I didn't need to. Co-streamers already have Discord or whatever open for the actual talking. The only thing I needed to move across the network was a single number, your level in dBFS, about thirty times a second. So that's all I send. A tiny JSON payload over a WebSocket. No SDP, no TURN servers, no media negotiation, no "allow this site to record your audio and send it to a stranger."
And the privacy line writes itself, because it isn't marketing. It's just true. Your audio never leaves your machine because there is no code path that would carry it. I literally can't see or hear you. I get a number.
That constraint made everything smaller. The backend is a Cloudflare Durable Object per room. Rooms hibernate when idle and dissolve the moment the last person closes the tab. No database of recordings to secure, because there are no recordings. No accounts. Refusing to send audio didn't cost me a feature. It deleted half the app and made the rest more honest.
What surprised me
I went in assuming the hard part would be the metering math or the realtime networking. It wasn't. The hard part was getting the browser to hand me an honest signal in the first place. Everything downstream (the smoothed bars, the peak-hold markers, the "possibly muted" nudge when someone's been silent too long) was easy once the input was real.
That's the lesson I keep relearning on side projects: the tricky bit is almost never where you brace for it. Budget your worry for the boring layer at the bottom.
If you co-stream, try it before your next one. Open streamlevels.app, make a lobby, throw the link to your co-host. Takes about fifteen seconds, and you'll never go live mismatched again.
And if you do, come tell me on X. Bonus points if you find a bug.
Now go match your levels.