Recording Into an Audio to MIDI Converter

The page asks for { audio: true }. Your browser answers with echo cancellation, noise suppression and automatic gain.

Short answer: this converter's Record button asks the browser for the microphone with getUserMedia({ audio: true }) and nothing else. Every other setting is left to the browser, and in Chrome that means echo cancellation, noise suppression and automatic gain control all switched on - the same chain used for voice calls, never adjusted for music. The recording is written as Opus inside WebM at 48 kHz mono, about 80 kbps in my measurement. The 80 MB limit is checked against that compressed file, so the microphone path allows about 140 minutes where a stereo WAV upload allows 7.9 minutes.

Everything here is read out of this page's own source or measured by me while it ran. Two of the numbers come from a browser microphone that was not a real microphone - I say exactly which, and why, further down.

The whole path, in the order it happens

The microphone code is short enough to read in full. Pressing the button does this:

navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) {
  var chunks = [];
  var mr = new MediaRecorder(stream);
  ...
  mr.onstop = function () {
    stream.getTracks().forEach(function (t) { t.stop(); });
    var blob = new Blob(chunks, { type: mr.mimeType || 'audio/webm' });
    if (blob.size > MAX_BYTES) { /* dropped */ }
    ...
    fr.readAsArrayBuffer(blob);
  };
  mr.start();
});

Then the recording is handed to the same decode() the file path uses, and from there it is the identical pipeline: decode, average to one channel, resample to 16 kHz, run YIN. Nothing downstream knows or cares whether the audio arrived from a file or from a microphone.

Four things in that snippet are worth naming.

  1. The constraints object is one word long. No sample rate, no channel count, no echoCancellation, no noiseSuppression, no autoGainControl.
  2. The recorder is built with no options either - no mimeType, no audioBitsPerSecond.
  3. The size check runs on the compressed blob, before decoding. MAX_BYTES is 80 × 1024 × 1024 = 83,886,080 bytes, the same cap the file path uses - but applied to a very different kind of file.
  4. The microphone is released when you stop. stream.getTracks().forEach(t => t.stop()) runs on stop, so the recording light should go out.

What { audio: true } actually got you

I intercepted the page's own call and read back what the track was really running with. Not the defaults I assume, not the documentation - the values from that call, on that track:

SettingValue the browser appliedWhat it is for
echoCancellationtrueRemoving speaker output leaking into the mic
noiseSuppressiontrueSuppressing steady background noise
autoGainControltrueLevelling loudness automatically
channelCount1Mono capture
sampleRate48000Hz
latency0.01Seconds
voiceIsolationfalseIsolating a voice from everything else

I ran this twice, on two separate browser instances, and got identical values both times.

All three of those processors exist because of video calls. Echo cancellation is for speaker output bleeding into a microphone. Noise suppression is tuned to make speech intelligible against steady background noise. Automatic gain control exists so that whoever is speaking is always about the same loudness, whether they lean in or lean back. None of the three was designed with a sustained musical tone in mind, and a musical note is close to the worst case for all of them: it is continuous, it is periodic, and its loudness is usually the thing you are trying to preserve.

The important part is not that these are on. It is that the page never asked for them and never turned them off. Asking for { audio: true } is asking the browser to decide.

They were all optional

I also read the track's capabilities - the range of values the browser would have accepted. Every one of those three processors can be turned off:

CapabilityValues accepted
autoGainControltrue, false
noiseSuppressiontrue, false
echoCancellationtrue, false, "remote-only", "all"
voiceIsolationtrue, false
channelCountmin 1, max 2
sampleRatemin 44100, max 48000

So this is not a browser limitation. The page could have asked for clean audio and did not. I am not claiming the result would have been better if it had - I could not measure that, and I explain why below - but the choice was available and was not taken.

What the recorder writes

new MediaRecorder(stream) with no options, so the container and codec are whatever the browser picks. In Chrome that is:

PropertyMeasured
MIME type chosenaudio/webm;codecs=opus
Audio in the fileOpus, 48,000 Hz, mono
Bytes per second, melodic take10,007 B/s (about 80 kbps)
Bytes per second, continuous tone15,000 B/s (about 120 kbps)

I also asked the browser which recording targets it would accept at all. The answer is narrower than most people expect:

TypeRecordable here?
audio/webmyes
audio/webm;codecs=opusyes
audio/mp4yes
audio/mp4;codecs=mp4a.40.2yes
audio/ogg;codecs=opusno
audio/wavno
audio/mpegno
audio/flacno

There is no way to get a lossless recording out of this button. Whatever you play, the converter analyses an Opus decode of it.

The size limit, in minutes this time

Because the cap is applied to the compressed blob, the microphone path buys enormously more time than the file path does. A 13.70-second recording produced 137,103 bytes, which is 10,007 bytes per second. Against 83,886,080 bytes that is 8,383 seconds, or about 140 minutes.

InputBytes per secondWhat 80 MB holds
Microphone, Opus, melodic10,007139.7 minutes
Microphone, Opus, continuous tone15,00093.2 minutes
WAV, 44.1 kHz stereo176,4007.9 minutes
MP3, 320 kbps40,02034.9 minutes

The WAV and MP3 rows are carried over from the article on what the limits actually are; the microphone rows are measured here. The practical upshot is that the recording path is not where you will hit the wall. At the analysis speed I measured there - roughly 50 times real time - 140 minutes of audio would take about three minutes to process.

The same melody, two ways in

I wanted to know what the microphone path costs you, so I fed the same eight-note melody in twice: once as a WAV file, once through the Record button. Both runs went all the way through the live page, and I took the .mid the page actually wrote on each run and read it back.

As a WAV fileThrough the microphone
Notes found88
Pitch rangeC4 - C5C4 - C5
Reported duration12.0 s13.7 s
MIDI file size105 bytes105 bytes

The pitches survived intact. The note start times drifted by 0 to 31 milliseconds - the MIDI tick here is 1.04 ms, and the detector's own frame hop is 16 ms, so most of that drift is a single frame and the worst case is two. The velocities did not survive intact:

NoteLevel in the sourceVelocity, WAVVelocity, microphone
C40.035757
D40.056958
E40.088672
F40.1210093
G40.20100100
A40.30100100
B40.45100100
C50.60100100

The quiet half of the melody came back 7 to 14 steps softer. The loud half is identical, and that is not reassuring - it is identical because velocity saturates at 100 well before the loud end, a flattening that is documented in how to check whether a conversion is correct. So the microphone path did not damage the top of the range; it had nowhere left to damage.

I want to be precise about what this table does and does not show. Something in the microphone path cost the quiet notes 7 to 14 velocity steps. I could not determine which part - the browser's processing, the Opus encoding, or the playback level of the input I used. I tried to isolate the browser's contribution and could not get a clean measurement, so I am not attributing it. The number is real; the cause is not established.

What this means in practice

What I did not test