Bass · Low-frequency range
Bass lines do not come back wrong. They come back as one note.
A four-string bass in standard tuning runs from E1 at 41.2 Hz to G2 at 98 Hz. The detector declares a 55 Hz search floor, so most of that range looks covered. It is not. At 44.1 kHz the analysis frame is 1,024 samples and the lag search stops at half the frame — 512 samples — so the longest period the loop can even attempt is 86.13 Hz. Measured, the failure is worse than that number suggests: every open string on the bass, played as a clean sine or as a plucked note with harmonics, came back as MIDI 89 — a single F6 around 1,423 Hz — at all three sensitivity settings.
There is a workaround that works, and it is arithmetic rather than a setting: shift the audio up until the fundamental clears roughly 165 Hz, convert, then move the exported notes back down. For the low strings that is two octaves up and 24 semitones back. The measurements behind both claims are below.
The selected audio is decoded in your browser; it is not uploaded to a server.
Where the range actually ends
The declared floor is 55 Hz. The reachable floor is 86.13 Hz.
Two numbers in the code decide how low the detector can look, and they disagree. minFreq is 55, which sets maxLag = floor(44100 / 55) = 801. But the same line caps that value at frameSize / 2. At 44.1 kHz the frame is 2^floor(log2(44100 / 27)) = 1024 samples, so the cap is 512 — and 512 is the smaller number, so 512 is what the loop uses. 44100 / 512 = 86.13 Hz. Nothing below that can be found, regardless of what the constant says.
That is arithmetic from the loop bounds, not a measurement. What the detector actually returns is a separate question, and it is the one that matters when you drop a bass take into the converter:
| Played | Fundamental | Correct MIDI | Clean sine | Plucked (8 harmonics) |
|---|---|---|---|---|
| E1 · 4th string open | 41.20 Hz | 28 | MIDI 89 · 1 note | MIDI 89 · 1 note |
| A1 · 3rd string open | 55.00 Hz | 33 | MIDI 89 · 1 note | MIDI 89 · 1 note |
| D2 · 2nd string open | 73.42 Hz | 38 | MIDI 89 · 1 note | MIDI 89 · 1 note |
| E2 · 12th fret on E1 | 82.41 Hz | 40 | MIDI 89 · 1 note | MIDI 89 · 1 note |
| G2 · 1st string open | 98.00 Hz | 43 | MIDI 89 · 1 note | MIDI 89 · 1 note |
| A2 · 12th fret on A1 | 110.00 Hz | 45 | MIDI 89 · 1 note | MIDI 89 · 1 note |
| D3 · well above the bass | 146.83 Hz | 50 | MIDI 89 · 1 note | MIDI 50 + a stray 89 |
| E3 · reference | 164.81 Hz | 52 | MIDI 52 · 1 note | MIDI 52 · 1 note |
The pattern is not "the low notes are a bit off". Every frequency from 41.2 to 110 Hz collapsed to the same note, MIDI 89, and the per-frame readings explain why: in 341 of 341 analysed frames the best correlation sat at lag 31 — the shortest lag the loop is allowed to test, which corresponds to 1,422.6 Hz. A low sine changes so slowly that its correlation at a very short lag is still close to 1, so the first lag wins and the true period is never preferred. That single mechanism produces the identical wrong answer for E1, A1, D2, E2 and G2 — which is why a bass line does not sound transposed or detuned after conversion, it sounds like one held high note.
Note where the crossover sits: 110 Hz still fails, 146.83 Hz is recovered only with harmonics present (the clean sine still returned 89), and 164.81 Hz is correct as either signal. The practical threshold is around 165 Hz, not the 86 Hz arithmetic floor. Above the floor a note can be searched; it is not guaranteed to be chosen.
Test conditions. 2-second mono signals at 44.1 kHz, generated in Node and fed through a line-by-line replica of the detector in converter.js: frame size 1,024, hop 256, lag range 31–512, RMS thresholds [0.009, 0.015, 0.023] for sensitivity levels 1/2/3, minimum note duration [0.055, 0.075, 0.11] s, MIDI clamp 28–96. Two signal types per frequency: a pure sine at amplitude 0.5 with 80 ms fades, and a plucked model built from eight harmonics at 1/n amplitude with per-harmonic decay and a 5 ms attack. All three sensitivity levels produced identical note counts and pitches in every row above. These are synthesized signals, not a recorded bass. A real bass take adds string noise, finger attack, fret buzz and room, none of which this model contains — the failure mode it demonstrates is in the lag search and will not be fixed by those extra components, but the exact readings from a real instrument could differ. Log: localmidi-private/logs/bass-lowfreq-2026-11-09.txt.
The workaround, measured
Shift the take up until the fundamental clears 165 Hz, then transpose the MIDI back down.
The same plucked signal, moved up one octave at a time before conversion. The shift factor and the transposition you owe afterwards are the same number:
| Original | Shift | Fundamental sent in | Returned | Transpose back |
|---|---|---|---|---|
| E1 · 41.2 Hz | ×1 | 41.2 Hz | MIDI 89 — wrong | — |
| E1 · 41.2 Hz | ×2 | 82.4 Hz | MIDI 89 — wrong | — |
| E1 · 41.2 Hz | ×4 | 164.8 Hz | MIDI 52 (E3) — correct | −24 semitones |
| A1 · 55.0 Hz | ×2 | 110.0 Hz | MIDI 89 — wrong | — |
| A1 · 55.0 Hz | ×4 | 220.0 Hz | MIDI 57 (A3) — correct | −24 semitones |
| G2 · 98.0 Hz | ×1 | 98.0 Hz | MIDI 89 — wrong | — |
| G2 · 98.0 Hz | ×2 | 196.0 Hz | MIDI 55 (G3) — correct | −12 semitones |
Two details worth knowing before you rely on this. First, the returned pitch class is right but the register is not — you get E3, not E1 — so the transposition afterwards is mandatory, not cosmetic. Second, the readings at the boundary wobble: at 164.8 Hz the mean measured pitch drifted between 166.1 and 177.2 Hz across sensitivity levels while the quantised note stayed at MIDI 52. The note survives; the frame-level frequency estimate is noisy right at the edge, which is why shifting well past 165 Hz (rather than exactly to it) is safer.
The order that avoids extra work: pitch the audio up two octaves in your DAW (varispeed or a pitch shifter — either is fine, you are only moving the analysis), run the conversion, drop the exported MIDI two octaves down, then check the result against the original. Doing the transposition before conversion and the reverse after means the timing grid is untouched, so nothing needs re-aligning. If you shift the audio with varispeed, the take gets shorter and faster, so convert first, transpose second, and never compare the shifted file's duration to the original's.
One thing this does not fix: a bass part is still a single line. Slides, ghost notes and two-string figures will not survive any better after shifting, because the detector reduces each frame to one pitch. For why that is a limit of the estimator rather than a bug, see the polyphonic guide.
In practice
What to send the converter, and what to fix afterwards.
- Shift, do not transpose the notes. Pitching the audio up is what moves the fundamental into the searchable band. Transposing a MIDI file after a failed conversion cannot recover information the detector never had — you would be transposing MIDI 89, not your bass line.
- Two octaves is the safe default for the E and A strings, one for the G. E1, A1, D2 and E2 all needed ×4; G2 needed ×2. Shifting the whole take by two octaves puts G2 at 392 Hz, which is still well inside the range, so a single uniform shift works for a part that uses all four strings.
- Expect stray high notes at the attack. In the D3 run the plucked signal returned the correct MIDI 50 plus a spurious MIDI 89 in the opening frames, and the clean sine returned only the 89. Short spurious notes at the start of a phrase are an artefact of the same lag-31 preference; delete them rather than chasing them with the sensitivity slider.
- Sensitivity will not rescue a low fundamental. All three levels returned MIDI 89 for every frequency from 41.2 to 110 Hz. The slider changes the RMS threshold and the minimum note duration, neither of which is what is failing here.
- Missing notes are a separate problem. Once the pitch is in range, short notes under 55–110 ms can still be filtered out. That is covered on the missing notes page.
If the pitches are wrong after shifting — a note a semitone off rather than an octave off — that is the integer-lag rounding described on the wrong notes page, and it is a different failure with a different fix. For the general conversion sequence, including what the exported file contains, see how to convert audio to MIDI.
FAQ
Questions that come up with bass takes.
Why did my whole bass line turn into one high note?
Every frame picked the shortest lag the search allows (31 samples ≈ 1,423 Hz) instead of the true period, because a low fundamental correlates almost as well at a very short lag as at its own. The note count collapses and the pitch is identical for every string, which is why it sounds like a single held note rather than a wrong melody.
Is the 55 Hz limit real?
No. minFreq = 55 would allow a lag of 801 samples, but the same line caps the lag at half the 1,024-sample frame, so the loop stops at 512 — 86.13 Hz. Measured, even notes above that floor (98 and 110 Hz) failed, so the effective limit for reliable results is closer to 165 Hz.
Can I just use a low-pass or an EQ to help it?
Filtering changes which harmonics are present, not where the lag search is allowed to look. It cannot move a 41 Hz fundamental above 86 Hz. The only fix that addresses the cause is shifting the audio up before conversion.
How far up should I shift?
Until the lowest note in the part is around 165 Hz or higher. In practice: two octaves for anything on the E or A string, one octave for parts that stay on the G string. Then transpose the exported MIDI down by the same amount.
Does this apply to a five-string or a detuned bass?
More strongly. A low B on a five-string is about 30.9 Hz and drop-D tuning puts the fourth string at 36.7 Hz — both further below the floor than E1, so they fail the same way and need the same shift.