✳ OMNIVIEWER Voice recorder Voice toolkit ← back

What the browser does to your voice

Three pieces of signal processing run on your microphone by default, in every browser, and almost nobody is shown them. They are the right choice for a video call and the wrong one for a recording — and they are why a browser recording of rain, or a guitar, or a room, comes out sounding wrong in a way people struggle to name. Here is what each one does, how to see whether yours were actually turned off, and why a browser cannot hand you an MP3 without help.

On this page The three switches Seeing what you got Why no MP3 What two hours weighs Pitch without speed

The three switches

When a page calls getUserMedia({ audio: true }), it does not get your microphone. It gets your microphone after three pieces of DSP, all of which default to on:

ConstraintWhat it doesWhat it costs
echoCancellation Subtracts what your speakers are playing from what the microphone hears, so the far end does not hear itself. It is an adaptive filter fitted to a room, and it takes a good deal of that room with it. Reverb, ambience and anything arriving late all suffer.
noiseSuppression A denoiser tuned for speech: it decides what is a voice and attenuates the rest. It is very good at its job and its job is not your job. Rain, applause, traffic, a guitar and a cat all read as noise, and are removed.
autoGainControl Continuously adjusts the input gain so a quiet talker and a loud one arrive at the same level. It moves the thing you are measuring. Quiet passages are pushed up, loud ones pulled down, and a fade becomes a plateau.

None of this is a browser behaving badly. The overwhelming majority of getUserMedia calls on the web are a video call, and for a video call these three are unambiguously correct. The problem is only that they are invisible, and that the minority case — recording something — is the one where they do damage.

Turning them off is a matter of asking: { audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false } }. Asking is not getting, though, which is the next section.

Asked, supported, got

A media track will answer three different questions about itself, with three different methods, and they routinely disagree:

A constraint expressed as a bare value or as { ideal: … } is a preference. The browser may ignore it, and it will not tell you that it did — the call resolves, you get a track, and the only evidence is that getSettings() disagrees with what you sent. Only { exact: … } forces the issue, and it does so by failing the whole call with an OverconstrainedError rather than by giving you a track you did not want.

So the honest question is never “did I turn noise suppression off” — it is “did the browser agree”. Put the three side by side and the answer is one row:

Setting              Asked          Supported     Got
Echo cancellation    exactly no     yes · no      on   ≠
Noise suppression    exactly no     yes · no      off
Auto gain control    exactly no     yes · no      off

That first row is the whole point. OmniViewer’s voice CAPABILITIES tab builds exactly this table from your own microphone, flagging every disagreement, and its camera equivalent does the same for a webcam — where the classic version of the same surprise is asking for 1920×1080 and quietly being handed 640×480.

Before you press allow, a page can already call enumerateDevices() and count your microphones. What it cannot do is name them: labels are empty strings until a grant. That gap is a deliberate mitigation, and it is visible — the device list on the capabilities tab looks different before and after the prompt.

Why a browser will not give you an MP3

MediaRecorder writes whatever container the platform ships an encoder for. In practice that is WebM with Opus nearly everywhere and MP4 with AAC on Safari. It is not MP3, on any browser, and MediaRecorder.isTypeSupported('audio/mpeg') is false everywhere. This is a licensing history rather than a technical one, and it long outlived the patents.

So a browser recorder has three options. Hand you a .webm and call it done, which is what most do. Upload the capture to a server to be transcoded, which means your recording leaves your machine. Or encode it locally — which is now perfectly possible, because a real ffmpeg compiled to WebAssembly runs in a worker at a useful speed, with libmp3lame in the build.

OmniViewer takes the third. When you stop a recording on /voice/record, the WebM or MP4 the browser captured is transcoded to MP3 in the page. The core is about 31 MB and is fetched once, the first time you ever stop a recording — never on page load, and never on any path that merely reads a file. Nothing is uploaded to do it.

What two hours actually weighs

Constant-bitrate MP3 has the rare virtue of being exactly predictable: the bitrate is kilobits per second, decimal, and the file is that arithmetic plus a few hundred bytes of ID3. There is no reason for a recorder to make you guess, and yet almost all of them do.

PresetSettingsPer hourTwo hours
Voice64 kbps mono, 32 kHz27.5 MB54.9 MB
Standard128 kbps mono, 44.1 kHz54.9 MB109.9 MB
High192 kbps stereo, 44.1 kHz82.4 MB164.8 MB
Studio320 kbps stereo, 48 kHz137.3 MB274.7 MB

For speech, 64 kbps mono at 32 kHz is not a compromise worth agonising over: the octave it discards sits above almost everything a voice produces, and it makes a two-hour interview small enough to email.

Pitch without speed

The last thing a browser will not do for you is shift pitch. Web Audio can change playback rateplaybackRate on a buffer source — and that moves pitch and duration together. It is the tape trick, and it is why the chipmunk effect also makes the recording shorter.

Moving one without the other takes two steps. First stretch time by the pitch ratio, cutting the signal into overlapping windowed grains and laying them back down at a wider spacing. Then resample the result by that same ratio, which pulls the duration back to where it started and multiplies every frequency by the ratio on the way.

The catch is in the first step. Grains laid down at a new spacing no longer line up with each other’s waveform, and the phase cancellation that follows is the hollow, flanged sound of a cheap time-stretch. WSOLA fixes it by sliding each grain a little — a few hundred samples — to wherever it best correlates with what has already been written. That single search is most of the difference between a voice that sounds processed and one that sounds broken, and OmniViewer’s TRANSFORM tab does it in about forty lines of plain JavaScript, with no library.

Everything in TRANSFORM is rendered offline, against the recording you already have, through an OfflineAudioContext. That makes it faster than real time, exactly repeatable, and undoable — the original buffer is never touched, so comparing the two is only a question of which one you press play on.
Record something →One button, up to two hours, out comes an MP3. Nothing is uploaded. Read your microphone →Asked, supported and got, side by side, with the disagreements flagged. Bend a recording →Twenty effects, including pitch shifts that hold their duration. The camera version →The same three questions, asked of a webcam instead.