Ear training · 2022 · Vanilla JavaScript

A voice calls the note.
Your hands answer.

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.

Standby Called0 Time0:00
Press play to begin

Space plays & pauses

Two octaves · the five black keys are the whole syllabus Lit key = the note being called

10 Spoken notes
3 Tempo presets
0 Dependencies
2.7MB Total payload

Why it exists

Sight‑reading trains the eyes. Nothing was training the ear.

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.

C same key D
D same key E
F same key G
G same key A
A same key B

How a session runs

Three steps, then stop looking at the screen.

Step 01

Set the tempo

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.

Step 02

Press play

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.

Step 03

Play what you hear

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

Small tool, deliberate decisions.

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.

Both names for every black key

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.

Three tempos that actually re‑time

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.

Nothing to watch

The audio carries the whole session. That is what lets a student keep both hands on the keys and both eyes on their fingering.

Offline after the first load

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.

No framework, no build step

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

The hard part was timing, not audio.

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.

script.js — the scheduling loop
// 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);
};

Chained, not intervalled

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 gap knows about the silence

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.

The display waits for the voice

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.

One path, two icons

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.

Pause has to mean pause

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

Ten recordings. Five keys. Every name.

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

The whole thing, on one line each.

Audio formatWAV · PCM · 44,100 Hz · 16‑bit · stereo
Clip length1.39 s – 1.97 s per note
Library size10 files · 2.7 MB total
Tempo presetsSlow 5 s · Medium 3 s · Fast 1 s between calls
RuntimeVanilla JavaScript · no framework · no build step
DependenciesNone
NetworkOne prefetch pass on load, then fully offline
RequirementsAny browser with <audio> · visualiser needs Web Audio and degrades quietly
AccessibilityKeyboard transport, visible focus, live region, reduced‑motion support

Available for front-end work

Need a front end built with this much care?

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.