MP3 to MIDI Settings: What Each Control Actually Changes

Four controls, and only two of them decide which notes exist. I changed each one on the live converter and read back the notes, the copied text and the .mid bytes.

Short answer: Confidence and Shortest note are applied inside pitch detection, so they change which notes exist and therefore change everything downstream. Tempo (BPM) changes only the downloaded .mid - the tick grid and the tempo event - and never the length of what you hear: the same file converted at BPM 20, 60, 120, 200 and 300 all plays back at 2.03 seconds. Transpose changes the .mid and the copied note list, but not the piano roll and not the Pitch range readout - which still said C4 - C5 after I transposed the file up an octave. Two of these controls are also less well behaved than their labels suggest: Confidence is not a percentage (the slider maps to a YIN threshold of 0.05 to 0.50, and it is not monotonic), and typing a BPM below about 3.58 writes a corrupt tempo value, because a MIDI tempo event is only three bytes long.

Everything below is either read out of this page's own source or measured by me while the live page ran. Where a number comes from a real conversion in a real browser, I say so. I did not change a single line of the converter to run these tests - the controls were set through the DOM and the buttons were clicked for real.

The one table that matters

Each control is applied at a different point in the pipeline, and that is the whole reason they behave so differently. This is what I confirmed by changing one control at a time and reading back every output:

ControlNotes detectedPiano rollPitch range Copied note listDownloaded .mid
Tempo (BPM) nonononoyes
Confidence yesyesyesyesyes
Transpose nononoyesyes
Shortest note yesyesyesyesyes

The evidence for the "no" columns, in order:

Confidence is not a percentage

The label reads like a quality dial, but it is a threshold on a normalised difference function. The page takes the slider value and flips it:

var threshold = 1 - parseFloat(sensEl.value);   // higher confidence = stricter

The slider is declared min="0.5" max="0.95" step="0.05", so there are exactly ten positions, and the threshold behind them spans 0.50 down to 0.05. The default 0.85 is a threshold of 0.15. There is no setting that means "0" and no setting that means "1".

On clean monophonic audio it changes almost nothing

I fed an eight-note C major scale (with three harmonics and a light noise floor) through all ten positions. Every single one returned the same eight notes, C4 through C5. What does move is where the page decides a note has ended - a stricter threshold cuts the tail off sooner. On the live page, the first note's duration went:

ConfidenceThresholdNotes foundPitch range First note (start - end)
0.500.508C4 - C5 0.000 - 0.448 s
0.85 (default)0.158C4 - C5 0.000 - 0.432 s
0.950.058C4 - C5 0.000 - 0.416 s

So on easy material the slider is close to cosmetic. That is worth knowing before you spend an evening tuning it.

On hard material it is not monotonic

The interesting case is a tone whose fundamental is weaker than its second harmonic - the classic recipe for an octave error. I used 440 Hz at amplitude 0.36 with a 220 Hz fundamental at 0.12, so the fundamental sits at one third of the harmonic. Sweeping the slider across all ten positions gave this - the full ten-position sweep ran against this page's own code, and the 0.50, 0.80 and 0.85 rows were then reproduced on the live page:

ConfidenceThresholdNotes foundPitch rangeVerdict
0.500.501A4 - A4octave too high
0.55 - 0.750.45 - 0.251A4 - A4 octave too high
0.800.2010A3 - A4 alternating A4 / A3
0.850.151A3 - A3 correct
0.90 - 0.950.10 - 0.051A3 - A3correct

Two things to take from that. First, loosening the slider made the answer worse, not better - the most permissive setting is the one that reports the wrong octave. Second, the response is not smooth: at exactly one position the detector flip-flops between the two octaves frame by frame and hands you ten notes instead of one. The copied note list shows it plainly, A4, A3, A4, A3, all the way down.

Practical reading: if a result looks wrong, try tightening Confidence before loosening it. And if you get a spray of very short notes alternating between two octaves, that is this behaviour, not a broken file - move the slider off that position rather than editing ten notes by hand. Where a slider can rescue an octave error in the other direction, and where it cannot, is covered in how to check a converted MIDI is correct.

Shortest note works on a 16 ms grid

This one is more predictable, because it inherits the frame grid. Detection runs on 1024-sample frames with a 256-sample hop at 16 kHz, so a note can only start or end every 256 / 16000 = 16 ms. A note's duration is therefore always a whole number of frame steps, and nothing in between is representable.

I built a file with notes of 20, 40, 60, 80, 100, 160, 320 and 640 ms, separated by 200 ms of silence, and read the durations back off the page's own note list:

Note in the filePage reportsIn frame steps
20 ms0.016 s1
40 ms0.048 s3
60 ms0.080 s5
80 ms0.096 s6
100 ms0.128 s8
160 ms0.176 s11
320 ms0.336 s21
640 ms0.656 s41

Every duration is a whole number of 16 ms steps, and the shortest note in the file does not get shorter than one step. A 640 ms note reads back as 656 ms.

What the setting does, step by step

The filter is a single comparison applied after notes are grouped:

if (durMs < minMs) { continue; }

It is a cut, not a trim: a note shorter than the threshold disappears entirely, it is not lengthened to meet it. On that same file the count of surviving notes went:

Shortest noteNotes keptNotes kept (live page)
0 ms88
20 - 40 ms7-
50 - 80 ms66 at 60 ms
100 - 120 ms4-
160 ms3-
200 ms2-
400 ms11
800 - 1000 ms0-

Note the shape of that. The default of 60 ms keeps six of the eight notes; dropping it to 0 keeps all eight, including a 16 ms blip that is probably noise. Raising it to 400 ms throws away everything but one note. The control is a blunt instrument and the useful range is narrow.

The shortest audio that can produce any note at all

Because notes are built out of frames, there is a floor on how short your audio can be before the converter has nothing to work with. Frames are only produced while a full 1024-sample window fits, so the first frame needs 1024 samples and each further frame needs another 256. I sliced a steady 440 Hz tone at increasing lengths and counted:

Audio lengthFramesNotes at Shortest note 0 Notes at Shortest note 60
1023 samples (63.9 ms)000
1024 samples (64.0 ms)11 (16 ms long) 0
1280 samples (80.0 ms)210
1536 samples (96.0 ms)310
1792 samples (112.0 ms)411
3000 samples (187.5 ms)811

So at the default settings a clip shorter than 112 ms cannot produce a note no matter what it contains, and a clip shorter than 64 ms cannot produce one even with the filter switched off. If you are converting a short one-shot sample and getting an empty result, that is the first thing to check - and the reason is arithmetic, not the material.

Tempo changes the grid, never the length

The .mid is written at 480 ticks per quarter note, and BPM decides how many ticks fit in a second:

var ticksPerSec = (bpm / 60) * 480;

Every note time is converted into ticks at that rate, and a tempo event carrying 60000000 / bpm microseconds per quarter note is written alongside it. The two cancel out, so a higher BPM gives you a finer grid in exactly the proportion that makes playback land in the same place. Measured on a four-note, 2.03-second file:

BPMTicks per secondFirst note-on tickLast event tick Playback length
201600325 2.0313 s
604800975 2.0313 s
12096001951 2.0323 s
200160003251 2.0319 s
300240004877 2.0321 s

The length column is computed from the file itself - last tick multiplied by the tempo event actually written, divided by the division. It is flat to within a millisecond. BPM is a notation choice, not a speed control, and the reason it is there at all is so the grid lines up with the bars of your project. The full byte-level breakdown of that file is in what is inside the downloaded .mid file.

The tempo event is only three bytes, and that bites

A MIDI tempo event carries a 24-bit value, so the largest tempo it can hold is 16,777,215 microseconds per quarter note. The page computes the value and then writes the low three bytes:

var mpq = Math.round(60000000 / bpm);
pushAll([(mpq >> 16) & 0xFF, (mpq >> 8) & 0xFF, mpq & 0xFF]);

Anything above that ceiling is silently truncated rather than clamped, and 60000000 / 16,777,215 is 3.5765. So every BPM below about 3.58 writes a wrong number. The input is declared min="20" max="300", but nothing in the script enforces those bounds - the only guard is parseInt(bpmEl.value, 10) || 120, which catches zero and empty and nothing else. I typed the values in and read the bytes the live page wrote:

Typed into the BPM boxValue usedTempo bytes written Microseconds per quarter noteBPM the file claimsPlayback length
1201207, 161, 32 500,000120.0002.0323 s
202045, 198, 192 3,000,00020.0002.0312 s
3003003, 13, 64 200,000300.0002.0321 s
44228, 225, 192 15,000,0004.0002.0312 s
3349, 45, 0 3,222,78418.617 0.3290 s
22201, 195, 128 13,222,7844.538 0.9091 s
11147, 135, 0 9,668,3526.206 0.3223 s
01207, 161, 32 500,000120.0002.0323 s
abc1207, 161, 32 500,000120.0002.0323 s

Three things in that table are worth pausing on. Typing 0 silently becomes 120 - a number input left empty reads as "", and "" || 120 is 120. Typing abc does the same, because the number field refuses the text and ends up empty. And the wrapped values are not even ordered: BPM 3 produces a file that claims 18.6 BPM, while BPM 2 produces one that claims 4.5. The 1 and 3 rows are the shortest files of the set (65 bytes against 69) simply because fewer ticks means fewer bytes of variable-length delta time.

Practical reading: leave BPM inside 20 to 300 and this never comes up. If you do type something odd, check the file rather than the box - a DAW will show you a tempo you never asked for, and the audio will be the wrong length. The playback-length column above is what a player actually does with the bytes, not what the page intended.

Transpose stops at the edges, quietly

Transpose is applied at write time, in two places: the note numbers in the .mid, and the note names in the copied text. It is added, then clamped, and the two paths disagree about what to do when the result falls outside the 0 to 127 MIDI range.

// .mid writer
var m = n.note + transpose;
if (m < 0) { m = 0; }
if (m > 127) { m = 127; }

// copied note list
noteName(n.note + transpose)

// and noteName itself
function noteName(m) {
  if (m < 0 || m > 127) { return '?'; }
  ...
}

So the .mid quietly pins the note to the edge of the range, while the copied list prints a question mark. Both of these are real conversions on the live page:

SourceDetectedTransposePitch range readout Copied note list.mid note number
62 Hz toneB1 (35)-36B1 - B1 ?0
1900 Hz toneA#6 (94)+36A#6 - A#6 ?127

The readout is the tell: it still reports the untransposed pitch, so a range of B1 - B1 next to a question mark in the list is exactly what out-of-range transposition looks like. Inside the range the two agree perfectly - transposing that four-note file by +12 moved the .mid note numbers to 72, 76, 79 and 84 and the list to C5, E5, G5 and C6, and both parsers I used to read the file agreed on every value.

One more thing the note list does

The copied text is fixed-width, space separated, with three decimals on all three time columns:

#  Start(s)  End(s)  Dur(s)  Vel  Note
1  0.000  0.496  0.496  100  C4
2  0.512  1.024  0.512  100  E4

Those are the detector's raw seconds, not the tick values that went into the .mid, so the list and the file will not always agree to the last digit once BPM rounding is applied. And the Duration readout at the top of the results is the length of the decoded audio, not the span of the notes - a 3.0-second file whose last note ends at 2.816 seconds still shows 3.0 s.

Nothing re-runs until you press Convert

Changing a control does not recompute anything. The page reads all four values at the moment you press Convert to MIDI and not before, so editing a box and looking at the results is a guaranteed way to mislead yourself. I changed Transpose, BPM, Confidence and Shortest note in turn after a conversion and the canvas fingerprint stayed identical through all four; the notes, the list and the .mid only changed once Convert was pressed again.

Clear is not a reset either. It empties the stored file and hides the results panel, but it leaves the four controls exactly where you put them and does not wipe the canvas underneath. If you are starting a fresh test, set the controls yourself.

Suggested starting points

Honest boundaries