Real Chords, Real Hardware

June 28, 2026 Audio Analysis • Self-Hosted Infra • Production Hardening

The Tool That Was Lying

The Song Analyzer looked finished from the outside. Paste a YouTube link, get back a key, a tempo, a chord progression synced to playback. It had a real redesign, a real player, real-feeling UI. What it didn't have was a real answer. Underneath, a Cloudflare Worker was generating plausible-looking chords at random. Every song got a different fake progression. It worked as a demo. It was never going to work as a tool.

Today we replaced the lie with a pipeline.

What Real Analysis Actually Takes

Chord detection isn't a lookup, it's signal processing, and signal processing wants CPU time a Cloudflare Worker will never give you. The new backend — chord-server/, a small Flask service — does the work a Worker can't: yt-dlp pulls the audio, ffmpeg converts it to wav, and librosa does the actual listening. chroma_cqt reduces the waveform to 12 pitch-class bins per frame; correlating the mean against Krumhansl-Schmuckler major/minor key profiles picks the key. beat_track finds the tempo. And the chords themselves come from template matching — 24 binary triad templates, major and minor across all twelve roots, compared against 2-second chroma windows by cosine similarity.

None of this is exotic. It's the same family of techniques tools like Chordify built a business on. The honest caveat: template matching catches triads, not 7ths or sus chords or slash chords, and it has rough edges on heavily distorted or vocal-only sections. Good enough to be true instead of fake. Upgrading to something like madmom's ML-based recognition is a real future option if the gap matters in practice.

Putting It on the Open Internet

A Flask service on a home machine isn't reachable from outside that machine, and it shouldn't be — no port-forwarding, no public IP exposed to the world. Cloudflare Tunnel solves that properly: cloudflared runs as a local service and opens an outbound-only connection to Cloudflare's edge, which then proxies api.methodictruth.com straight to localhost:5005. Nothing inbound ever touches the box directly.

Getting there meant three rounds of systemd archaeology. sudo cloudflared service install runs as root, which doesn't see a regular user's ~/.cloudflared/config.yml — the config and tunnel credentials had to be copied to /etc/cloudflared/ explicitly. Then the generated unit's Type=notify contract bit us: cloudflared's QUIC handshake took longer than the default TimeoutStartSec=15, so systemd kept killing it mid-negotiation and restarting it in a loop. Bumping that to 60 seconds let it actually finish connecting. Small, specific, the kind of failure that looks alarming in a log and is fixed by reading the log correctly.

End to end, it works exactly like any other API: a curl against https://api.methodictruth.com comes back with real chords, a real key, a real tempo, for whatever song you actually gave it.

The Honest Tradeoff

This is the one feature on the entire site where "the site" and "the infrastructure" are different things. Every other page is static, served from Cloudflare's edge, effectively free to scale. The Song Analyzer's real work happens on one physical machine I own, that has to be powered on, running Python. If that box is off, this one feature fails — gracefully, the rest of the site is unaffected, but it fails.

Worse, the first version of the deployment ran Flask's development server: one request at a time, no concurrency, 10-30 seconds of download-and-analyze per request. Fine for a quiet personal site. It would not have survived a real concurrent spike — a few dozen simultaneous requests would have simply queued behind each other.

Hardening It Before It Needed To Be Hardened

Three fixes, all shipped the same day, before any of this became an actual incident instead of a known risk:

A permanent disk cache, keyed by video ID. A song's chords, key, and BPM don't change. The first analysis writes cache/<video_id>.json; every repeat request for that song is a disk read instead of a re-download and a re-analysis. This is the single highest-leverage fix — most real traffic to a tool like this is the same handful of songs requested over and over, and now those are instant.

gunicorn instead of the dev server. Four worker processes instead of one, so four different songs can be analyzed in genuine parallel — separate processes, no GIL contention — bounded by CPU core count instead of by "one at a time." A 120-second timeout keeps gunicorn from killing a worker mid-analysis, since its default 30-second timeout is shorter than a single chord analysis can take.

A per-video dedup lock. If two people request the same uncached song within the same few seconds, the second request doesn't trigger a second download — it waits on an fcntl.flock keyed to that video ID, then reads the result the first request just wrote. This works across gunicorn's separate worker processes, not just within one of them. Requests for different songs never block each other.

What This Doesn't Solve, On Purpose

None of this turns one consumer machine into a data center. The actual remaining ceiling is many people requesting many different, never-before-analyzed songs at the exact same moment — that still queues, bounded by however many CPU cores are available. For the traffic a personal site actually gets, that ceiling is comfortably out of reach. Building past it now would be solving a problem that doesn't exist yet.

The interesting part is what the next step looks like if it ever does become real, and it isn't a rewrite. Cloudflare Tunnel already supports running multiple connectors behind the same hostname — a second machine, one of the other desktops sitting around, could join as a second connector with zero application code changes. The architecture is already shaped for that. A cloud VPS is the other option, but it trades something real — hardware already paid for — for something else real: uptime that doesn't depend on whether a machine in a house has power and internet. That's not a question of capability, it's a question of which tradeoff matters more, and it doesn't need answering today.

Ownership of compute showed up here the same way it showed up when we wired the local inference tier onto the mesh in May: the constraint isn't "can this be done," it's "what's the cheapest fix that's actually true to the problem you have." Cache what repeats. Parallelize what's independent. Lock what would otherwise race. Scale the hardware only when the traffic, not the anxiety about traffic, says to.