Andrew Shiau

Practice room

Type
Web instrument
Pitch
McLeod pitch method, asserted to half a cent from C2 to C7
Microphone
Read in the page and discarded. Nothing is recorded or sent.

A chromatic tuner and a metronome, side by side on one screen. Pitch detection runs in the browser; the microphone signal never leaves it.

Open the instrument. The tuner reads the offset in cents, the metronome takes a typed tempo or a tapped one, and on a phone you swipe between them — or add it to your home screen.

The detector that failed first

The obvious pitch detector is autocorrelation, and it failed its own tests. The raw correlation tapers with lag, which dragged a 65 Hz sine 22 cents sharp. Normalizing the taper out made every multiple of the true period score alike, so the picker took nine times the period and read three octaves flat.

What shipped is the McLeod pitch method. It bounds the difference function to ±1 at every lag and takes the first key maximum within 10% of the best as the period. On synthesized buffers it holds within 0.35 cents from C2 to C7, including a plucked string whose second harmonic is louder than its fundamental. That is the case that makes FFT-peak tuners read a low E an octave high.

It breaks one octave further up. At C8 the period is 11 samples, and on the plucked case the doubled-period lobe scores 1.00 against the true one's 0.88 — under the 0.9 threshold, so the picker takes the octave below. Nothing this tuner is for plays a C8, so the bound is stated at C7 and the tests stop there.

What the tests assert

The unit suite runs the math on synthesized buffers: 62 tests over the note grid, the scheduler, tap tempo, and the detector's bounds. They check the sign of the reading as well as its size. A tuner with a flipped sign passes every "about 23 cents" assertion and sends you the wrong way on every string.

The 50 end-to-end tests play known sine waves into Chromium's fake microphone, one browser per input frequency, because the fake mic is a launch flag. 440 Hz must read A4 at 0 cents. 446 Hz must read sharp with a plus sign. Recalibrating A4 to 446 must then move that same input to 0, which is the control arm: a calibration that is stored but never reaches the detector passes the first two and fails this one.

The bleed test is the one that taught me most, because three versions of it passed against the broken tuner before one could fail. The fake microphone plays a file and cannot hear the page's speaker, so the test builds the room: every click also routes into a gain node the test wires back to the analyser. Version one fed it the steady 440 Hz sine the other tuner tests use — a continuous tone masks the click, so the bug never appeared. Version two used a pulsed note and caught the first bug, then passed the broken fix, because a clean synthetic room has no noise floor and a click in silence reads exactly on its own frequency. Version three added noise and compared against a quiet baseline, which the noise made wide enough to swallow the leak.

What finally worked was giving up on note names. Both logs — every sound scheduled, every reading painted — carry audio-clock timestamps, so the assertion is arithmetic: no reading may be painted from a window the speaker was sounding in. That holds whatever the click smears to. A test that names the wrong answers can only catch the ones you thought of.

The tuner heard the metronome

Run both at once with the speakers up and the tuner reads the metronome. The click is a pure sine, so it is the easiest thing in the room to find the pitch of: handed one, the detector returns its frequency to within 0.1 cents — still dead on at a tenth of the level. Between notes the readout printed C6 on the beat and G6 on the downbeat, perfectly in tune, and from 140 bpm up it never cleared, because the dash needs six consecutive silent frames and a beat at 140 leaves five. The tuner was working. That was the problem.

The page knows what its own speaker is doing, so the tuner now drops any reading taken while a click it scheduled was sounding. A dropped frame draws nothing, which means the last real reading stands — the right answer for a beat that interrupts a held note.

The first version of that gate also required the pitch to be near the click's own frequency, on the theory that throwing away good frames is wasteful. Deploying it is what proved it wrong: the live tuner still printed C6, now at 1021 Hz — the 1046.5 Hz click, 35 cents off and outside the window. A microphone has a noise floor, and noise moves the period the detector locks onto: the same click reads 25 to 46 cents off under a quiet hiss, and with a room tone in the window it smears past 900 cents or drops to a subharmonic. No tolerance contains that. The time is exact, so the frequency test was deleted — one constant and one branch fewer than the version that was cleverer.

The cost is stated rather than hidden: each tick blinds the tuner for about 150 ms, so at fast subdivisions — 200 bpm in sixteenths, a tick every 75 ms — the readout holds its last value instead of tracking. That limit is a test, so it cannot quietly change.

One screen, two instruments

This is the only page on the site that leaves the document measure. On a wide screen the tuner and metronome sit in equal halves, both running. Under 900px the halves become two swipe pages, and the indicator is the site's own pair: a disc for the page you are on, a ring for the other. Every control treatment came off a proof sheet where the options ran live on one simulated signal.

Five metronomes, one bad moment

Tapping a tempo over a running metronome used to tear it: every tap re-anchored the tick grid mid-lookahead, and the pendulum recomputed its swing against the new tempo. The fix was chosen from a sheet of five working metronomes, each running the real engine with a different policy for the same moment. The first is the bug, kept honest; the others turn at the next beat, at the next bar, hand the beat to the tapping hand, or stop. Sound on — start one and tap a faster tempo over it.

Each option clicks and swings for real. Two more rounds refined the fourth: taps take the beat over and sound as they land, the arm finishes its sweep and rests against its side while you tap, and the metronome returns on the downbeat your hand implied.

Buttery, measured

The shipped takeover still felt rough in two ways: an occasional doubled click under the finger, and a stutter in the strike flashes at the arc's ends. Both were clock bugs. Stopping the scheduler cannot silence clicks it has already scheduled into its 100 ms lookahead — those oscillators are started against the audio clock and play over the tap's own sound — and the flash side was being recomputed from old beat timestamps at the new tempo, so a tempo change could fire the same end twice.

"Feels rough" is not a bug report a test can hold, so the suite grew a harness that makes smoothness a number. The page logs every sound that actually reaches the speaker, with cancelled clicks removed, and a per-frame sampler reads the pendulum's rotation and which end is flashing. Four tests assert the feel: no two beat clicks within 60 ms of each other through a tap barrage and its comeback, the same under ten taps hammered at 150 ms, flash sides in strict alternation across a mid-run tempo change, and no frame-to-frame jump over 40° — with no exemptions clause.

Run once, the harness passed. Run six times with retries off, it failed three different ways, and each failure was a real mechanism: a sweep that rescaled mid-flight when the tempo changed under it, a re-anchored beat landing mid-sweep and flipping the arm back toward the end it had just left, and a leftover settle animation from one takeover snapping the next run's arm a full 108°. A sweep now locks its duration at its own strike, starts from the arm's drawn angle, absorbs beats that land mid-flight, and every settle loop carries a generation counter so a stale one exits instead of moving the arm. After the fourth fix the harness passed six consecutive no-retry runs.

Pitch detectionMcLeod pitch method on a 2048-sample window: normalized square difference, key-maxima picking, parabolic interpolation.
TimingA lookahead scheduler on the audio clock. setInterval wakes it; the audio thread keeps time.
SoundsThree synthesized sine pings. G6 opens the bar, C6 marks the beat, G5 sits under it. No samples.
PrivacyThe microphone signal is read in the page and discarded. Nothing is recorded, stored, or sent.
BleedThe tuner disowns any reading taken while the metronome's own click was sounding, by timestamp rather than by pitch.
ApparatusWeb Audio API, no runtime dependencies. 62 unit tests on the math; 50 end-to-end tests against a fake microphone, re-run against the live site after every deploy.