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.
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:
| Control | Notes detected | Piano roll | Pitch range | Copied note list | Downloaded .mid |
|---|---|---|---|---|---|
| Tempo (BPM) | no | no | no | no | yes |
| Confidence | yes | yes | yes | yes | yes |
| Transpose | no | no | no | yes | yes |
| Shortest note | yes | yes | yes | yes | yes |
The evidence for the "no" columns, in order:
0.000 0.496, 0.512 1.024,
1.024 1.536, 1.536 2.032) and identical readouts. Only the .mid bytes
changed.C4 - C5; at Transpose +12 it still reported C4 - C5, while the copied
note list moved from C4 E4 G4 C5 to C5 E5 G5 C6 and the .mid note numbers moved from
60/64/67/72 to 72/76/79/84.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".
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:
| Confidence | Threshold | Notes found | Pitch range | First note (start - end) |
|---|---|---|---|---|
| 0.50 | 0.50 | 8 | C4 - C5 | 0.000 - 0.448 s |
| 0.85 (default) | 0.15 | 8 | C4 - C5 | 0.000 - 0.432 s |
| 0.95 | 0.05 | 8 | C4 - 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.
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:
| Confidence | Threshold | Notes found | Pitch range | Verdict |
|---|---|---|---|---|
| 0.50 | 0.50 | 1 | A4 - A4 | octave too high |
| 0.55 - 0.75 | 0.45 - 0.25 | 1 | A4 - A4 | octave too high |
| 0.80 | 0.20 | 10 | A3 - A4 | alternating A4 / A3 |
| 0.85 | 0.15 | 1 | A3 - A3 | correct |
| 0.90 - 0.95 | 0.10 - 0.05 | 1 | A3 - A3 | correct |
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.
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 file | Page reports | In frame steps |
|---|---|---|
| 20 ms | 0.016 s | 1 |
| 40 ms | 0.048 s | 3 |
| 60 ms | 0.080 s | 5 |
| 80 ms | 0.096 s | 6 |
| 100 ms | 0.128 s | 8 |
| 160 ms | 0.176 s | 11 |
| 320 ms | 0.336 s | 21 |
| 640 ms | 0.656 s | 41 |
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.
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 note | Notes kept | Notes kept (live page) |
|---|---|---|
| 0 ms | 8 | 8 |
| 20 - 40 ms | 7 | - |
| 50 - 80 ms | 6 | 6 at 60 ms |
| 100 - 120 ms | 4 | - |
| 160 ms | 3 | - |
| 200 ms | 2 | - |
| 400 ms | 1 | 1 |
| 800 - 1000 ms | 0 | - |
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.
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 length | Frames | Notes at Shortest note 0 | Notes at Shortest note 60 |
|---|---|---|---|
| 1023 samples (63.9 ms) | 0 | 0 | 0 |
| 1024 samples (64.0 ms) | 1 | 1 (16 ms long) | 0 |
| 1280 samples (80.0 ms) | 2 | 1 | 0 |
| 1536 samples (96.0 ms) | 3 | 1 | 0 |
| 1792 samples (112.0 ms) | 4 | 1 | 1 |
| 3000 samples (187.5 ms) | 8 | 1 | 1 |
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.
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:
| BPM | Ticks per second | First note-on tick | Last event tick | Playback length |
|---|---|---|---|---|
| 20 | 160 | 0 | 325 | 2.0313 s |
| 60 | 480 | 0 | 975 | 2.0313 s |
| 120 | 960 | 0 | 1951 | 2.0323 s |
| 200 | 1600 | 0 | 3251 | 2.0319 s |
| 300 | 2400 | 0 | 4877 | 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.
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 box | Value used | Tempo bytes written | Microseconds per quarter note | BPM the file claims | Playback length |
|---|---|---|---|---|---|
| 120 | 120 | 7, 161, 32 | 500,000 | 120.000 | 2.0323 s |
| 20 | 20 | 45, 198, 192 | 3,000,000 | 20.000 | 2.0312 s |
| 300 | 300 | 3, 13, 64 | 200,000 | 300.000 | 2.0321 s |
| 4 | 4 | 228, 225, 192 | 15,000,000 | 4.000 | 2.0312 s |
| 3 | 3 | 49, 45, 0 | 3,222,784 | 18.617 | 0.3290 s |
| 2 | 2 | 201, 195, 128 | 13,222,784 | 4.538 | 0.9091 s |
| 1 | 1 | 147, 135, 0 | 9,668,352 | 6.206 | 0.3223 s |
| 0 | 120 | 7, 161, 32 | 500,000 | 120.000 | 2.0323 s |
| abc | 120 | 7, 161, 32 | 500,000 | 120.000 | 2.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 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:
| Source | Detected | Transpose | Pitch range readout | Copied note list | .mid note number |
|---|---|---|---|---|---|
| 62 Hz tone | B1 (35) | -36 | B1 - B1 | ? | 0 |
| 1900 Hz tone | A#6 (94) | +36 | A#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.
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.
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.