01The answer is a number of button presses
Two short recordings: start somewhere comfortable and walk down, then start somewhere comfortable and walk up. Out comes your range — and then the thing you can act on, which is a key-button count.
That framing decides what the tool refuses to do. It does not recommend songs, for three independently sufficient reasons: per-song melodic range data means karaoke MIDI, which is licensed; two people with identical ranges want to sing entirely different things; and it is unnecessary, because one number lets you check any song already in your head. It also does not print a key signature, because a key does not determine range — two songs in the same key can sit an octave apart — and because the key button changes it anyway. Outputting a value that one button press invalidates would be self-contradictory.
02Width is the invariant, the centre is free
Transposition shifts an entire melody by a constant. That single fact organises the whole tool:
- Width is invariant. A song spanning two octaves is impossible for a singer spanning one and a half — in every key. This is the only absolute constraint.
- The centre is a free variable. Where the melody sits is exactly what the key button moves, so it is not a constraint at all. It is the answer: one integer.
Working it out explicitly: with your range and the song's, the workable transpositions are the integers k with k ≥ yourLow − songLow and k ≤ yourHigh − songHigh. Non-empty exactly when your width is at least the song's. I checked it against a range of F2–D4:
| Song | Your width | Song width | Workable keys |
|---|---|---|---|
| 18 semitones, sitting low | 21 | 18 | +4 to +6 |
| 21 semitones, exact fit | 21 | 21 | −3 only |
| 24 semitones | 21 | 24 | none, at any key |
The machine's limit is what makes "can't sing it" mean anything
Korean karaoke machines transpose by one semitone per press, up to ±6 — and TJ and Kumyoung are the same. That has a pleasant consequence: the conversion table is machine-independent, thirteen rows from −6 to +6, with no "which machine are you at?" question anywhere in the interface. Since ±6 is twelve semitones, the total amount a machine can move a song is exactly one octave.
And because the floor is −6 rather than infinity, a single number defines the boundary:
No song database, no taste, one number. And it deliberately uses the stable top note, not the highest note you can produce — using the latter would mean telling someone they can sing a song on the strength of a note they can hit once and cannot hold. That is the confidently-wrong claim this whole family of projects keeps trying not to make.
03A note you cannot hold is not in your range
The naive measurement is absolute range, lowest producible to highest producible. It is the wrong measurement: the bottom fills with vocal fry and the top with whistle tones, neither of which can carry a song.
So a note only counts if it is held — a stretch of frames staying inside a narrow band for long enough, with enough energy and enough tracker confidence. That criterion is musically honest, and it has two properties that turned out to matter more than the definition itself.
The parent project needed an expensive Viterbi octave-correction stage, because octave errors wreck a pitch curve. Here, an octave error is a jump, and a jump breaks the hold. The sustain criterion discards them for free. So the correction layer is not ported — it is dropped, and the replacement is simpler than the original.
The same applies to the rest of the parent's machinery. Octave folding would flatten the top note the singer is deliberately reaching for; spike removal would erase a sustained high note as an outlier; semitones-relative-to-your-median is meaningless when absolute pitch is the product. Roughly 20% of the engine is reused, not 90%.
The second property: the criterion never asks what kind of voice produced the note. Falsetto passes if it is held with enough energy, and that is correct — plenty of songs are sung in falsetto. The test is whether you can hold it, not how you made it. Section 08 is that sentence being paid off.
The user never has to judge
Two design choices remove all musical skill from the procedure. You are never told to sing a specific pitch — the instruction is "a little lower than last time", and the tool measures how far you actually moved, so uneven steps and skipped steps are fine. And you are never asked "is this your limit?" You keep going and the criterion breaks first: near the limit a voice does not stop cleanly, it wobbles and thins and fades, which is precisely what is being measured. Two consecutive failed steps ends the take.
There is also no audio output at all. Playing a reference tone would let the microphone pick it up and read it as the singer, and it is unnecessary anyway since no specific pitch is ever requested. Timing comes from a progress bar instead.
04Punish the drift, not the wobble
To split "stable" from "merely reachable", the same recording is scored against two threshold sets, strict inside loose. The interesting question is what "unstable" should mean, and the obvious answer is wrong.
The obvious answer is scatter around the mean. But vibrato is scatter around the mean, and trained singers have more of it. Penalising scatter hands the narrowest ranges to the best singers.
So a held stretch is split into trend and residual, and the penalty goes on the trend: vibrato oscillates around a stable centre and drifts almost not at all, while a note at the edge of your range sags. Measured, the separation is clean — vibrato drifts at about 0.06 semitones per second, a sagging limit note at 0.79 to 0.94. Residual is left entirely unpunished at both thresholds.
And the gap between the two ranges is itself a finding worth printing. A wide gap between your stable top note and your maximum top note means you can reach notes you cannot yet hold — there is room to train. A narrow gap means you are already using everything you have. No single range number can say either.
05Writing the test first found a hole in my own spec
The regression suite existed before the implementation, and on the first day it surfaced three things. Two were implementation bugs. One was worse.
The threshold was silently 13% looser than written. A pitch tracker starts reporting a note once a good part of its analysis window contains it, so counting frames overstates duration — measured, by a constant 40 ms at a 64 ms window: true durations of 270 / 290 / 300 / 400 ms read as 310 / 330 / 340 / 440. Uncorrected, a spec saying "300 ms" accepts 260 ms notes and every range comes out inflated. The correction is pinned by boundary tests, because it is tied to the window length and must be re-measured if that changes.
Duration plus bandwidth cannot exclude a glissando. Not "does not" — cannot. Any band, traversed slowly enough, satisfies both. A two-second sweep across three semitones registered as three separate held notes.
The missing ingredient was drift, which I had introduced only as a refinement for the strict threshold. It was not a refinement. It was a necessary condition for the loose threshold too, and without it the basic definition of "a held note" was unsound.
06Synthetic signals proved the accuracy and lied about the thresholds
Synthetic tones are the strongest verification in this whole family of projects, because they supply an absolute right answer. A 440 Hz sine must read as A4. Nothing in the intonation work had that.
I re-checked the mapping while writing this page, including the boundary where Korean karaoke naming rolls over, which sits at C exactly like scientific notation:
| Input | Scientific | Korean karaoke | Error |
|---|---|---|---|
| 82.41 Hz | E2 | 0옥타브 미 | +0.1 ¢ |
| 440 Hz | A4 | 2옥타브 라 | +0.0 ¢ |
| 493.88 Hz | B4 | 2옥타브 시 | −0.0 ¢ |
| 523.25 Hz | C5 | 3옥타브 도 | −0.0 ¢ |
Then a real person sang into it. It captured two notes, and the stable range was empty.
Reading the raw frames, the synthetic signals had been an order of magnitude out on every threshold that mattered:
| Property of a held note | Synthetic sine | Actual voice |
|---|---|---|
| Band occupied | under 0.1 st | 0.78 – 0.94 st |
| Drift | 0.06 st/s | 0.64 – −0.95 st/s |
| Level below the recording's peak | ≈ 0 | 26.4 dB |
Three causes, and the third is the one that stings:
- Takes were ending after 3.6 seconds. The pacing bar started with the recording, so the first squares — while the singer was still drawing breath — counted as failures, and two consecutive failures ended the take before it began. Failures now only count once a first note has been captured.
- One pop destroyed the energy reference. The threshold was relative to the recording's peak, and a single-frame breath noise sat 26 dB above the singing, disqualifying more than half of it. The reference is now the 90th percentile of voiced frames — which is what "the level of your voice" meant in the first place.
- The band was cutting vibrato. A max-minus-min test inside a ±0.5 semitone band is, in effect, a scatter test — the exact thing section 04 promises not to do. The spec said in writing not to punish vibrato, and the implementation punished vibrato. The band went to ±0.75.
The lesson is narrow and I want to state it precisely. The synthetic tones did prove something real and worth having: 440 Hz reads as A4 to within half a cent. But "how much wobble still counts as holding a note" is a question about human voices, and on that question the synthetic signals returned a confident, wrong answer.
07The test passed because the test was not real
Widening the band reopened the glissando leak, so I added what seemed like an elegant discriminator. A sliding note keeps sliding until it hits the edge of the band; a held note ends because the singer runs out of breath and the sound stops. Record why each stretch ended, and refuse band-terminated stretches at the strict threshold.
The observation is true. Applied to this procedure it is completely wrong, and the second real session made that obvious: captured notes went from 2 to 16, and only 5 passed the strict threshold. My new gate was rejecting the other eleven.
This procedure is continuous. You hold a note, slide to the next, hold again — there is no silence in between. So almost every held note ends by hitting the band edge, because the singer moved on. The only stretches that ended in silence were the five times the singer paused for breath, and those were exactly the five that passed.
And here is why it survived a green test suite: my regression fixtures put 200 ms of silence between steps. That never happens. Eighty-five assertions, then ninety-nine, and not one of them modelled the shape of the actual procedure.
Writing the tests before the implementation was right and it paid for itself in section 05. But writing tests first and having them model reality are two different problems, and passing the second is not implied by passing the first. Where synthetic input carries an absolute right answer, the proof is real. Where it encodes my assumption about how the thing is used, a pass guarantees nothing at all.
The actual fix was smaller and better than the gate. A held stretch begins with the tail of a slide in and ends with the tail of a slide out, so measuring the trend across the whole stretch measures the moving, not the holding. Trimming 15% from each end and measuring the middle 70% separates the two cases cleanly:
| Drift measured over | A held note (true sag −0.15 st/s) | A 0.82 st/s glissando |
|---|---|---|
| The whole stretch | −0.41 to −0.74 | 0.82 |
| The middle 70% | −0.06 to −0.24 | 0.82 |
The fixture library gained a generator for continuous steps with no silence — the shape of the real procedure — and the suite now asserts that ending at the band edge is normal and a stable range must still come out.
08The gap that looked like a bug and was a person
Third session. Captured notes 18, nine of them stable, stable range F2–D4. But the maximum top note was B4, sitting nine semitones above the next note down, with nothing in between — and the empty slot was B3, of which B4 is exactly double.
A textbook octave error. I was ready to call it one.
I asked instead. The singer confirmed that the voice had flipped thin at the top. It was falsetto. B4 was real, and the empty region between D4 and B4 was not a measurement failure — it was a register break.
Three things had already been designed correctly for this and I nearly overrode all of them:
- The criterion admits falsetto, because it asks whether the note was held, not how it was produced. B4 passed the loose threshold and failed the strict one — sorted by sustainability, not by technique.
- The two-layer range kept it out of the tool's claim. The absolute ceiling was computed from the stable D4, giving G#4. A falsetto note reachable once did not get to contaminate the answer.
- The "room to train" reading was literally correct. A nine-semitone gap means notes you can reach but not hold, which is exactly what a falsetto ceiling is.
I proposed flagging it: "if there is a gap between the maximum and stable top notes, warn that the reading may be wrong." That feature would have told users that their real falsetto range was a suspected error — the same confidently-wrong claim this project keeps avoiding, dressed up as a caution.
Section 07's lesson was that a test which does not model reality proves nothing. This one runs the other way: a gap in the data does not mean the measurement is broken. People are shaped like that. If anything goes in that space it can only be a statement of fact — "no held notes were found between these" — because whether it is a register break or an error is not something this tool can know.
09What one measurement cannot tell you
The same singer, minutes apart, same microphone, same room, produced maximum top notes of D4 and A3. Five semitones apart.
So "your top note is X" from a single session is not reproducible, and the tool should not pretend otherwise. That is the same wall the parent project hit from a different direction, and it has the same resolution: repeated measurement over time, with the person's own noise floor shown next to the trend so that variation is not read as improvement.
Two more limitations worth stating plainly. Every threshold in the table came from three sessions with one voice — they are calibrated, not validated, and a different voice may need different numbers. And one leak is knowingly open: a glissando slow enough not to saturate the band still reads as a slightly sagging held note. It is confined to the maximum range, never the stable one, so nothing the tool actually claims — the shareable top note, the absolute ceiling, the conversion table — passes through it. The regression suite pins that limitation as a passing test, so the day someone fixes it, the line breaks and says so.
Try it. Two short recordings, no musical training needed, and a number you can use at the machine tonight.
Open Vocal Range →