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.
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:
| Constraint | What it does | What 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.
{ 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:
getConstraints()— what was asked for. Your own request, echoed back.getCapabilities()— what the hardware says it can do.getSettings()— what the track actually got.
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.
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.
| Preset | Settings | Per hour | Two hours |
|---|---|---|---|
| Voice | 64 kbps mono, 32 kHz | 27.5 MB | 54.9 MB |
| Standard | 128 kbps mono, 44.1 kHz | 54.9 MB | 109.9 MB |
| High | 192 kbps stereo, 44.1 kHz | 82.4 MB | 164.8 MB |
| Studio | 320 kbps stereo, 48 kHz | 137.3 MB | 274.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 rate — playbackRate 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.
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.