Audio to MIDI timing drift: why the notes land off the grid.
Notes come out of this converter stamped against a fixed 120 BPM, 480-tick-per-quarter grid, so a part recorded at any other tempo arrives with the right internal spacing and the wrong bar positions. The millisecond errors on top of that are not a constant offset either — in the test below, two 220 Hz notes of the same length, in the same file, were stamped 0 ms and roughly 10 ms before their true onsets, and a third note was about 20 ms early.
That second point costs people an afternoon: nudge everything by the same amount and you fix one note while pushing the others further off.
The selected audio is decoded in your browser; it is not uploaded to a server.
The grid the file is written on
Everything becomes ticks at 120 BPM, whatever your recording was doing.
The export routine in converter.js multiplies each note's start and end by 120/60 × 480 — 960 ticks per second, one tick per 1.0417 ms — and writes a format-0 single track at 480 ticks per quarter note. What it does not write is a tempo meta event: the only events are note-on, note-off and end-of-track, so 120 BPM exists purely as the conversion factor and the playing tempo is the DAW's own default.
That is harmless at 120 BPM and not harmless anywhere else, because the grid does not stretch — it reinterprets.
| Your part is at | One bar lasts | A four-bar phrase reads as |
|---|---|---|
| 96 BPM | 2.50 s | 5.0 bars at 120 BPM |
| 120 BPM | 2.00 s | 4.0 bars — correct |
| 140 BPM | 1.71 s | 3.4 bars at 120 BPM |
What these numbers are. Bar and phrase lengths here are arithmetic on the 120 BPM / 480 PPQ constants read from converter.js; nothing was imported into a DAW and timed.
Measured here, 2026-11-05
Three notes in one file came back with three different start errors.
We fed the detector one 1.64-second buffer: 220 Hz for 600 ms, 330 Hz for 200 ms, 220 Hz for 600 ms, separated by 120 ms of silence, gain 0.5, 3 ms fades, mono at 44.1 kHz. Two of those notes share a pitch and a length and sit at different points in the file. What the detector stamped, against where they begin:
| Note in the buffer | True onset | Stamped start |
|---|---|---|
| 220 Hz · 600 ms | 0.000 s | 0.00 s — no error |
| 330 Hz · 200 ms | 0.720 s | 0.70 s — about 20 ms early |
| 220 Hz · 600 ms | 1.040 s | 1.03 s — about 10 ms early |
The mechanism is in the frame loop, and it is the code the site ships. Frames are 1,024 samples — 23.2 ms at 44.1 kHz — advancing in hops of 256 samples, 5.8 ms. Each frame is judged whole, and a note is stamped with the start of the earliest frame whose energy clears the level threshold. A window that opens before the attack but still ends after it already holds a few milliseconds of the note, and at gain 0.5 that fragment is loud enough to qualify. So the stamp can land up to a full window ahead of the real attack, and how far ahead depends on where the onset falls inside that window. The first 220 Hz note begins exactly on a frame boundary and reads exactly; the second begins mid-window and reads about 10 ms early.
Two more consequences. Sensitivity does not move the stamps: at all three settings the same three notes printed identical starts of 0.00, 0.70 and 1.03, because the threshold decides whether a frame qualifies, not when it begins. And the end of the file is cut: the loop only runs while a whole frame fits inside the buffer, so the last note's end printed 1.61 s where the audio runs to 1.64 s.
How to reproduce it. Harness scripts/run-missing-notes.js, section A, the 200 ms row. It re-implements detectNotes from converter.js line for line and prints the geometry it used. Any buffer built the same way gives the same stamps. The 330 Hz note also reads one semitone high (MIDI 65 rather than 64), a separate effect covered on the missing-notes page; it does not affect the timing readings.
What it means for you. A 20 ms lead is not a constant you can subtract. Inside one short file the error ran from nothing to 20 ms, so a single global nudge cannot be right for more than one note at a time.
Diagnosis before repair
Ragged or uniform? The answer decides the fix.
Put the original audio and the MIDI on two tracks with the same start, zoom to one bar, and look at the gaps between attacks. If every note is off by the same amount you have a uniform offset — a tempo mismatch like the first table, or round-trip latency. Nudge the track; that repair is cheap and correct.
If the gaps themselves change note to note, you have the second problem, and nudging makes half of it worse. On a piano roll zoomed to one beat, 20 ms at 120 BPM is 4% of a beat — small, but visible as a note sitting clearly ahead of the line.
Quantising is the reflex, and at 120 BPM a 1/64 grid is 31.25 ms — coarser than the error you are trying to remove, so a note 20 ms early lands on the nearest line only by luck. Quantising tidies the picture but replaces the measurement with a grid. If you want the performance's own timing, leave it and fix pitches first; the step-by-step workflow puts timing last for this reason.
Order of operations
Set the tempo first, or you are measuring the wrong thing.
Because the file carries no tempo event, establish the tempo of the source, not of the project you have open. If you do not know it, measure it from the recording first — the BPM counter exists for that step — then set the project tempo and import. Skip it and every later judgement about "the conversion is off" is really a judgement about two tempos.
After that, the remaining errors are the frame-level ones measured above plus whatever the pitch side contributes. Check pitch first, because a wrong note reads as a timing error when you scan a piano roll quickly; the accuracy checklist covers the sequence.
When to ignore all of it
Sustained material does not care about 20 ms.
A pad, a held string line, a slow vocal phrase — 20 ms of lead is inaudible against an attack that takes 300 ms to develop. Percussive material is the opposite: 20 ms on a kick is the difference between tight and sloppy, and it is where a frame-based pitch detector is the wrong tool. The drum workflow treats attacks as transients instead, a different measurement with a different error profile.
FAQ
Timing questions
Why are all my converted notes slightly early?
Each note is stamped with the start of the 23.2 ms frame that first clears the level threshold, so the stamp can sit up to a frame before the real attack. In our test the lead ran from 0 to 20 ms inside one 1.64-second file, so it does not look like a consistent shift.
Does the exported MIDI contain a tempo?
No. The writer converts seconds to ticks at 120 BPM and 480 ticks per quarter, but writes no tempo meta event — only note-on, note-off and end-of-track. Set the tempo in your project before judging the alignment.
Can I fix the timing with the sensitivity setting?
No. All three settings printed identical start times for the same three notes. Sensitivity changes the energy threshold that decides whether a frame counts as a note, not when the frame begins.
Is this timing good enough for drums?
Not for tight programming. A 20 ms lead is audible on a transient, and the detector is estimating a pitch rather than locating an attack. Use a transient-based workflow and check the result against the original.