What Sample Rate and Channels Do Before MP3 to MIDI Detection

A channel average, a 16 kHz resample, and the anti-alias filter that is not there.

Short answer: before the detector runs, every file is decoded, averaged down to one channel, and resampled to 16 kHz with plain linear interpolation. I measured all three steps. The sample rate of your file changes nothing - 8 kHz, 44.1 kHz and 96 kHz sources gave identical note lists. The channel count does: because the channels are averaged rather than one of them picked, a stereo file whose two sides are out of phase averages to silence and returns nothing. And the resampler has no anti-alias filter, so anything above 8 kHz folds back into the analysis - a pure 15 kHz tone, far above the 2000 Hz ceiling, came back as a B5 note at 991 Hz.

This page is the third in a series that takes the converter apart. The pitch detection article covers what the detector does with a frame; this one covers what happens to your audio before a frame exists. Everything below is read out of the page's own source or measured by me, and the last measurement was made on the live page itself, not on a copy of it.

The three steps that run before the detector

The analysis is a few hundred lines of plain JavaScript inside a single HTML file, so the preprocessing is readable in full. Two short functions do all of it.

function toMono(buf) {
  var n = buf.length;
  var chs = buf.numberOfChannels;
  var out = new Float32Array(n);
  for (var c = 0; c < chs; c++) {
    var d = buf.getChannelData(c);
    for (var i = 0; i < n; i++) { out[i] += d[i]; }
  }
  if (chs > 1) { for (var j = 0; j < n; j++) { out[j] /= chs; } }
  return out;
}
function resample(src, srcRate, dstRate) {
  if (dstRate === srcRate) { return src; }
  var ratio = srcRate / dstRate;
  var n = Math.max(1, Math.floor(src.length / ratio));
  var out = new Float32Array(n);
  for (var i = 0; i < n; i++) {
    var pos = i * ratio;
    var i0 = Math.floor(pos);
    var i1 = Math.min(i0 + 1, src.length - 1);
    var f = pos - i0;
    out[i] = src[i0] * (1 - f) + src[i1] * f;
  }
  return out;
}

They are called in this order, in the main run:

var mono = resample(toMono(audioBuffer), audioBuffer.sampleRate, TARGET_RATE);
var notes = framesToNotes(mono, TARGET_RATE, threshold, minMs);

with TARGET_RATE = 16000. Three things are worth pulling out of that.

  1. The channels are averaged, not picked. The loop adds every channel together and then divides by the channel count. A stereo file is not converted using the left channel; it is converted using the arithmetic mean of both.
  2. The resample is linear interpolation between two neighbouring samples. No windowed-sinc kernel, no polyphase filter bank, and nothing at all that removes energy above the new Nyquist frequency. That last part matters, and it has its own section below.
  3. If your file is already 16 kHz, no resampling happens at all. The first line of resample returns the buffer untouched when the rates match. That shortcut turns out to be the way to sidestep the aliasing problem, which is also covered below.

One thing I want to be careful about: the source does not say why 16 kHz was chosen. What the source does show is that 16 kHz gives a Nyquist limit of 8000 Hz, four times the 2000 Hz ceiling the detector can actually report. So the resample is not what sets the pitch range - the lag window is. I am reading the number, not the intent.

Sample rate: measured, it changes nothing

I generated the same eight-note C major scale - C4 to C5, half a second per note, amplitude 0.5 - at six different sample rates, and ran each one through the page's own toMono and resample and then its own detector, at the default sensitivity.

Source rateSamples inAfter resampleNotes foundNote list
8,000 Hz32,00064,0008C4 D4 E4 F4 G4 A4 B4 C5
16,000 Hz64,00064,0008C4 D4 E4 F4 G4 A4 B4 C5
22,050 Hz88,20064,0008C4 D4 E4 F4 G4 A4 B4 C5
44,100 Hz176,40064,0008C4 D4 E4 F4 G4 A4 B4 C5
48,000 Hz192,00064,0008C4 D4 E4 F4 G4 A4 B4 C5
96,000 Hz384,00064,0008C4 D4 E4 F4 G4 A4 B4 C5

Not just the same notes - the same start times, to the millisecond: 0.000, 0.496, 0.992, 1.488, 2.000, 2.496, 2.992, 3.488 seconds, and the same velocity of 100 on all eight. A 12-times difference in sample rate produced byte-identical note data.

That is less surprising once you notice that every row collapses to the same 64,000 samples - 4.0 seconds at 16 kHz - before the detector sees anything. The resampling step throws away the extra resolution the higher rates were carrying. If you have been exporting a 96 kHz master because you assumed the converter would make use of it, it does not.

The 8 kHz row is worth a second look. An 8 kHz file has a Nyquist limit of 4 kHz, and the detector's ceiling is 2000 Hz, so there is still headroom. It works - but only just, and any real content up there would already have been damaged by the low sample rate before the converter ever saw it.

Stereo is averaged, and that has a failure mode

Because toMono sums the channels and divides by the count, the result depends on how the two sides relate to each other. I ran the same scale through five channel arrangements:

InputPeak after mixdownNotes foundWhat happened
Mono (reference)0.50008Baseline
Stereo, L = R0.50008Identical to mono, as expected
Stereo, R silent0.25008Half the level, same eight notes
Stereo, R at half level0.37508Same eight notes
Stereo, L and R out of phase0.00000Total cancellation - no notes at all

The inverted case is the one to remember. The left channel is the scale; the right channel is the same scale multiplied by minus one. Each channel on its own is a perfectly good signal. Averaged, every sample pair sums to zero, and the converter is handed two seconds of digital silence.

This is not a hypothetical. Out-of-phase stereo shows up in real material - a badly wired cable, a stereo-widening plugin pushed too far, a doubled take that was phase-flipped, a mid-side decode gone wrong. It is easy to miss because the file plays back fine in one ear and fine in the other, and often sounds wider than the original. The converter reports zero notes, and the status line says the conversion finished. There is no error, because from the inside there is nothing wrong: the audio really is silent.

The other two rows are the quieter lesson. Halving the level - one channel silent - did not change a single note or a single velocity value. That is not the mixdown being clever; it is the velocity curve being flat at the top, which is documented in the how to check a conversion article.

One more stereo case worth knowing: I fed 220 Hz into the left channel and 440 Hz into the right. The averaged signal contains both tones. The output was one note, A3 - the lower one. This is the monophonic limitation showing up in the mixdown stage: two pitches in, one pitch out, decided before the detector ever sees a frame.

Bit depth: measured, no effect down to 8 bits

Bit depth sets how much dynamic range the file can carry, so it is a reasonable thing to suspect. I quantised the same scale to six different word lengths - uniform quantisation referenced to full scale - and ran each through the detector.

Signal level24-bit16-bit8-bit6-bit4-bit
Peaking at 0.508 notes8 notes8 notes8 notes8 notes
Peaking at 0.108 notes8 notes8 notes8 notes8 notes
Peaking at 0.038 notes8 notes8 notes8 notes0 notes

Every note list was identical, all eight notes, all velocity 100, right down to 4-bit at normal level. The only failure is in the bottom-right cell: a signal peaking at 3 percent of full scale has so little room above the quantisation step at 4 bits that every sample rounds to zero, and there is nothing left to analyse.

The reason bit depth is so quiet here is that the audio pipeline is floating point. The browser's decoder hands the page 32-bit floats regardless of what the file contained, so by the time the detector runs, the original word length is already gone - it survives only as quantisation noise, and a clean, reasonably loud recording has plenty of margin over that. Bit depth is a real quality decision for archiving and for quiet material, but it is not what decides whether a conversion works.

The filter that is not there

Now the part that actually bites. Look again at what resample does when the rates differ: it walks the source at a fractional step and linearly interpolates between the two nearest samples. That is it. There is no step that removes frequencies which cannot be represented at the new rate.

When you sample a signal at 16 kHz, the highest frequency it can carry is 8000 Hz. Anything above that does not disappear - it reflects back down. The arithmetic is one line:

folded frequency = | f - 16000 * round(f / 16000) |

A 15,000 Hz component folds to |15000 - 16000| = 1000 Hz. That is squarely inside the detector's 60-2000 Hz window. The detector has no way to tell it apart from a real 1000 Hz tone, because at 16 kHz the two are literally the same samples.

I fed pure sine tones from 14 kHz to 18 kHz through the page's own pipeline at 44.1 kHz. The detector reported a note for every single one:

Tone fed inFold arithmetic predictsReported noteReported Hz
14,000 Hz2000 HzB62009.01
14,250 Hz1750 HzA61778.97
14,500 Hz1500 HzF#61493.55
14,750 Hz1250 HzD#61267.54
15,000 Hz1000 HzB5991.08
15,250 Hz750 HzG5769.53
15,500 Hz500 HzB4488.70
15,750 Hz250 HzC4260.11
16,250 Hz250 HzC#4275.59
16,500 Hz500 HzC#5545.28
16,750 Hz750 HzG5768.30
17,000 Hz1000 HzB5990.94
17,250 Hz1250 HzD#61277.12
17,500 Hz1500 HzG5761.68
17,750 Hz1750 HzA#61812.44
18,000 Hz2000 HzB62002.50

The reported pitch follows the fold arithmetic closely across the middle of the window. Two rows do not match cleanly and I am leaving them in rather than tidying them away: 17,500 Hz came back at 761.68 Hz instead of 1500 Hz, exactly an octave low, which is the same second-period failure the pitch detection article documents above the ceiling. The resampled artifact is not always a clean tone, so the detector sometimes halves it.

Two control runs pin the cause down. I took the identical 15 kHz tone and removed everything above 7.8 kHz in the frequency domain before resampling it. Same file, same pipeline, same settings: zero notes. And when I fed the page a 220 Hz tone on its own, at either 44.1 kHz or 16 kHz, it reported A3 at 220 Hz in both cases. So the B5 is not a quirk of the detector and not a quirk of 15 kHz - it is produced by the resampler, and removing the energy above the new Nyquist removes it.

It is not only 14-18 kHz

The fold formula puts content between 14 kHz and 18 kHz into the 60-2000 Hz window. But tones outside that window still produced notes too, because a folded component that lands at 3 kHz or 6 kHz is still a strong periodic signal, and the detector's lag search will happily lock onto a sub-multiple of it. Every tone I tried from 8 kHz to 20 kHz returned a note. A few examples from outside the window:

Tone fed inFolds toReported
8,250 Hz7750 HzD5 at 592.89 Hz
10,000 Hz6000 HzB6 at 2003.82 Hz
12,000 Hz4000 HzB6 at 2008.34 Hz
19,500 Hz3500 HzG4 at 389.66 Hz

None of those reported pitches has any relationship to the tone that went in. This is the part I would flag hardest: the tool's stated range is 60-2000 Hz, and it is easy to read that as "everything outside the range is ignored". It is not. Content outside the range is not ignored; it is folded, and then it is believed.

How loud does it have to be?

I swept the level of a 15 kHz tone down to find where it stops mattering:

15 kHz levelLevel in dBFSResult
0.030-30.5 dBB5 at 991 Hz
0.020-34.0 dBB5 at 991 Hz
0.016-35.9 dBB5 at 991 Hz
0.014-37.1 dBB5 at 991 Hz
0.012-38.4 dBNo notes
0.010-40.0 dBNo notes

The artifact survives down to about -37 dBFS. That is not a tiny amount of energy. Cymbal wash, sibilance, string bow noise, tape hiss and dither all live in that region, and plenty of them sit well above it.

Does it break a real note?

A pure 15 kHz tone is a laboratory signal. The question that matters is whether high-frequency content disturbs a note that is actually there. I built a more realistic source: A3 at 220 Hz with harmonics two through eight at amplitude 1/k, then added a 15 kHz component at four different levels.

15 kHz levelRelative to the fundamentalReported
none-A3 at 220.01 Hz - correct
0.02-23.5 dBA3 at 220.00 Hz - correct
0.05-15.6 dBA3 at 219.98 Hz - correct
0.10-9.5 dBA3 at 219.93 Hz - correct
0.20-3.5 dBA2 at 110.51 Hz - one octave low

Up to about 10 dB below the fundamental, the high-frequency content is harmless. At 3.5 dB below, the whole conversion drops an octave: the file says A3, the MIDI says A2.

Confirmed on the live page

Everything above was measured with the page's functions extracted and run outside the browser. That is the same code, but it is not the same thing as the page. So I made real WAV files, loaded easyaudiotomidi.com in a browser, set the file input, pressed Convert, and read the numbers the page printed:

File uploadedNotes foundPitch range shown
44.1 kHz, A3 + harmonics1A3 - A3
44.1 kHz, same + 15 kHz at -3.5 dB1A2 - A2
44.1 kHz, same + 15 kHz at -23.5 dB1A3 - A3
16 kHz, the -3.5 dB file resampled first1A3 - A3

The live page reports A2 for the third file and A3 for the fourth, exactly as the offline measurements predicted. This is not a theoretical property of the code - it is what the page on this site does with a file you hand it today.

What this means in practice

The other side of the same coin - how much the input bitrate moves the result, as opposed to the sample rate - was measured separately in 128 kbps vs 320 kbps MP3 to MIDI, and the answer there was also less dramatic than you might expect.

What I did not test