A real voice, not a synthesiser
Every note is a recorded WAV of a person saying it. Browser speech synthesis reads “A flat” a different way on every platform; a recording says it the same way every time.
Ear training · 2022 · Vanilla JavaScript
A hands-free practice partner for the piano. It speaks a random black‑key note — A flat, F sharp, B flat — then waits while you find it. No teacher, no screen to watch, no page to turn.
Space plays & pauses
Why it exists
A student can read G♭ off a page and still freeze when someone says it out loud. The gap is recall, not theory — and it is widest on the black keys, because every one of them answers to two names.
This drills both halves at once. You hear a name, and you have to know which physical key it belongs to before the next one arrives. Because the prompt is audio, your eyes stay on your hands where they are useful.
How a session runs
Slow leaves four seconds of silence between calls, Fast runs them nearly back to back. Change it mid‑session and the engine re‑times on the spot rather than stacking a second timer.
All ten recordings were fetched while you were reading this, so the first note speaks immediately. From here on the session never touches the network again.
The name appears as the voice says it and the matching key lights up — but that is confirmation, not instruction. The session works just as well with your eyes shut.
What it does well
Every note is a recorded WAV of a person saying it. Browser speech synthesis reads “A flat” a different way on every platform; a recording says it the same way every time.
All ten enharmonic spellings sit in the same rotation, so G♯ comes up exactly as often as A♭. You learn the key, not one label for it.
Five, three or one second between calls. Switching clears the pending timer instead of leaving it to fire alongside the new one — the difference between a metronome and a mess.
The audio carries the whole session. That is what lets a student keep both hands on the keys and both eyes on their fingering.
The page prefetches all ten files and waits for canplaythrough on each one. After that the browser serves them from cache, so a dropped connection mid‑practice changes nothing.
One HTML file, one stylesheet, and about 170 lines of plain JavaScript. Nothing to install, nothing to compile, nothing to break in four years’ time.
Under the hood
Playing a sound is one line. Making ten of them arrive on a steady, adjustable pulse — while the display keeps up — is where the work went.
// The next note is scheduled by the previous one finishing,
// never by a repeating interval that can drift.
audio.onended = function () {
if (!isPlaying) return;
var i = Math.round(Math.random() * 100) % filesCount;
playingKey = filesArray[i];
// Each recording carries ~500 ms of air at both ends,
// so the requested gap is corrected for it.
var gap = gapBetween - audioFilePadding * 2;
if (justStarted) { gap = 100; justStarted = false; }
playTimer = setTimeout(function () {
if (!isPlaying) return; // paused while we waited
audio.src = filePath + playingKey + extension;
audio.play();
announceText(musicalFormat(playingKey));
}, gap);
};
Each note schedules the next one from the previous onended event. A setInterval would drift the moment one file took longer to decode, and the drift compounds over a ten‑minute session.
The recordings have roughly half a second of room tone at each end. The engine subtracts it, so “three seconds” means three seconds of actual silence to think in — not three seconds minus the padding.
Before painting a name, the engine checks the element’s readyState against HAVE_FUTURE_DATA and re‑checks every 10 ms until it clears. Text that arrives early gives the answer away.
Play and pause are a single SVG <path> whose d attribute is interpolated by an <animate> element. No icon swap, no second asset, no layout shift.
A pending setTimeout outlives the click that stops playback, so the callback re‑checks the play state before it touches the audio element. Without that guard, one more note escapes after every pause.
The note library
The complete set, exactly as the engine sees it. Select any card to hear the recording that plays during a session.
WAV · 44.1 kHz · 16‑bit stereo · durations measured from the source files
Specifications
| Audio format | WAV · PCM · 44,100 Hz · 16‑bit · stereo |
|---|---|
| Clip length | 1.39 s – 1.97 s per note |
| Library size | 10 files · 2.7 MB total |
| Tempo presets | Slow 5 s · Medium 3 s · Fast 1 s between calls |
| Runtime | Vanilla JavaScript · no framework · no build step |
| Dependencies | None |
| Network | One prefetch pass on load, then fully offline |
| Requirements | Any browser with <audio> · visualiser needs Web Audio and degrades quietly |
| Accessibility | Keyboard transport, visible focus, live region, reduced‑motion support |
Available for front-end work
Piano Practice was one of my first client projects — a small brief, taken seriously. I build interfaces where the details are load‑bearing: timing, audio, motion, and all the parts a user feels without ever noticing. If that is the kind of work you need, I would like to hear about it.