barts.work
aChord — app screenshot
Live web

aChord

A chord sheet that transposes itself, hears your guitar, and works with no signal in a church basement.

aChord is a chord transposer for people who play from paste-in chord sheets. Drop in a song with chords sitting above the lyrics, hit the arrows, and every chord moves — while the lyrics stay exactly where they were. It detects the key, lays it out on an interactive circle of fifths, suggests the harmony that fits it, draws the fingering for guitar or ukulele, tunes your instrument through the device microphone, and keeps your songs and setlists on the device. It installs to a home screen and runs with no network at all.

That last part is the whole point. The place I actually need this is a room with thick walls and no reception, ten minutes before people start singing.

How it was built

The interesting work is in src/engine/ — about 1,250 lines of pure TypeScript with no React anywhere near it, which is why all 468 tests run in under two seconds.

  • The parser is the piece everything else depends on. A chord sheet is an ambiguous document: a line reading Am C F G is chords, but Dad drove me starts with something that looks like a D chord. The parser scores each line on how much of it parses as valid chord tokens before deciding whether it’s a chord line or a lyric line, so transposing never rewrites your words.
  • Transposition works in pitch classes rather than string substitution, so slash chords, extensions, and inversions survive the trip. There’s a simplify pass for when you want Cmaj7 to just be C because you’re teaching the song to someone who’s been playing for a month.
  • Sharps by default. C# not Db, unless you flip it. The preference persists, because having to re-flip it every session is precisely the kind of small friction that makes you stop using a tool.
  • The tuner implements YIN pitch detection over the Web Audio API rather than plain autocorrelation — autocorrelation is octave-happy and will confidently tell you a low E is an E an octave up. YIN’s cumulative mean normalized difference function is what makes the reading stable enough to trust.
  • Key detection scores the chord set against every major and minor key and reports the best match, which then feeds the harmony view’s suggestions.

Where it runs

It’s a Cloudflare Worker serving static assets — no server, no database, no accounts, no analytics, no network calls after first load. Your songs live in your browser’s localStorage and nowhere else. Deploying it is wrangler deploy against a wrangler.jsonc and about twenty lines of Worker whose only job is folding the alias hostnames onto one canonical address — browser storage is per-origin, so several live hostnames would mean several unrelated song libraries for the same person. The whole build is 340 KB on disk.

Lessons learned

  • The ambiguity is in the input, not the algorithm. I started out trying to write a stricter chord regex and kept breaking real songs. The fix wasn’t a better pattern for what a chord looks like — it was accepting that you can only classify a line by looking at all of it at once, and scoring it, rather than deciding token by token.
  • Pick the pitch-detection algorithm before you build the UI around it. The tuner’s needle felt broken for a day before I understood the problem was octave errors in autocorrelation, not smoothing in the display. I’d been tuning the wrong layer.
  • Offline-first is a deployment decision as much as a code one. The service worker was the easy half. The half that mattered was refusing to add anything — no fonts from a CDN, no analytics beacon, no “just this one” API call — that would quietly turn a working app in a dead-zone room into a spinner.
  • A green test suite tells you what you thought to check. 125 tests passed while the ukulele returned the wrong chord for every fingering it had to generate — ask for F♯, get E♭ major — because nothing ever converted a diagram back into the notes it actually sounds. The fix wasn’t a better test for that one bug; it was testing the property instead of the example, sweeping all twelve roots across every quality. That one file found 149 wrong diagrams on its first run.
  • Update on the user’s terms, not the app’s. The service worker was set to update silently, which is wrong for something you open ten minutes before a service: it can swap the app out from under you mid-song. Each build now stamps itself with its commit and writes the same value to /version.json; the running app polls that when it is online, and offers “new version available” with a button. Nothing changes until you press it.
  • The feature I’d already fixed was still switched off. The tuner sat behind a commented-out tab reading “hidden until pitch detection is fixed” long after swapping autocorrelation for YIN had fixed it. detectPitch had no tests, so I never had the evidence to trust it again. Thirty synthesized waveforms later it reports 0.0 cents on every string, and the tab is back.