Retiring Sonogram — and keeping the part that mattered
I'm retiring Sonogram, my WebGL music visualizer. The audio analysis underneath it — waveforms, EBU R128 loudness, key and tempo — lives on in 1-bit. What I kept, and what I let go.
I’m retiring Sonogram.
If you never saw it: Sonogram was a little web app I built for turning a piece of
music into a piece of art. You’d upload a track, it would analyze the audio with
librosa, and then render the result through one of four WebGL visualizers —
spectral blooms, chroma rings, energy fields, that kind of thing. You could share
the result as a PNG to a public gallery, and each image carried a quiet
steganographic signature so it could be traced back to its source. It ran on
fly.io, it had a Python/FastAPI back end, and it was genuinely fun to make.
It’s also, I’ve come to accept, a completely different animal from the thing I actually care about building these days.
Two products, one of them a tourist
My main project is 1-bit: a bit-exact iOS music player for people who care about their files — DSD, hi-res FLAC, the whole audiophile bestiary — paired with 1-bit-bridge, a small pure-Go companion server that streams a library over the network without mangling a single byte. The entire philosophy of that ecosystem is don’t touch the bits. No transcoding, no “enhancement,” no backend that sees your music. What leaves the server is byte-for-byte what was on disk.
Sonogram is the opposite in almost every way: a public gallery, a backend that ingests your uploads, a rendering pipeline whose whole point is to transform the audio into something new. There’s nothing wrong with that — it’s just not the same product, and maintaining a second, philosophically-opposed stack “because I made it once” is how side projects quietly rot.
So instead of letting it rot, I did the more interesting thing: I asked what, exactly, was worth keeping.
The visualizers were the show. The analysis was the substance.
Here’s the thing about Sonogram that surprised me when I went back to read my own code. The flashy part — the four visualizers — was maybe fifteen percent of the value. The part that actually did the work was the quiet offline audio analysis underneath: the code that listened to a track and figured out its shape, its loudness, its key, its tempo. The visualizers were just one way to draw that analysis. But the analysis itself is useful to any music player, drawings or not.
A scrubber that shows you the waveform of the song you’re seeking through. Volume that stays consistent as you move between a quietly-mastered jazz record and a brick-walled pop single. A little “Em · 92 BPM” badge for the tracks whose tags never bothered to say. None of that needs a public gallery or a WebGL shader. It just needs the ear.
So that’s what I migrated. Not the art — the ear.
What moved into the bridge (and up to the player)
All of this now lives in 1-bit-bridge as an optional, offline analysis pass, and surfaces additively in the 1-bit app:
-
Waveform / peak envelope. The bridge decodes a track once, reduces it to a compact peak envelope (tiny binary sidecar, ~5 KB for a four-minute song), and the iOS player renders it as a real scrubber waveform instead of a blank seek bar.
-
Loudness → ReplayGain. Proper EBU R128 / ITU-R BS.1770-4 integrated loudness — K-weighting, the full two-stage gating, the works — converted to a ReplayGain value so playback volume is consistent across your library. I cross-checked the numbers against
ffmpeg’s own R128 meter and they land within a fraction of a dB. Crucially, this is applied as gain at playback, never as an edit to the file. -
Key and tempo. Musical key via Krumhansl-Schmuckler chroma correlation; tempo via a spectral-flux onset envelope and autocorrelation (with a perceptual prior that keeps it from confidently reporting half or double the real BPM). These are honestly labeled estimates, and a real curated tag in the file always wins.
The discipline that made this fit the 1-bit philosophy instead of violating it:
- It’s offline. The bridge computes everything once, ahead of time, and caches it. There’s no on-the-fly DSP in the playback path. The player still receives the original file, bit-for-bit.
- It’s additive. New fields on the wire, no protocol bump, older clients simply ignore what they don’t recognize.
- It only reads. The analysis decodes the samples to learn about the music. It never rewrites what you hear. That’s the whole line, and it holds.
I deployed it this week and watched it chew through a real library — Abdullah Ibrahim records coming back with sensible keys and tempos in the high-80s to mid-130s, loudness values that actually reflect how those albums were mastered. The ear works.
What I let go
For the record, the things that did not make the trip: the four visualizers, the public gallery, the share flow, the steganographic PNG signing, the moderation tooling, the SEO and sitemaps, and the entire Python/fly.io deployment. All of it. That’s the part of retiring a project that’s genuinely freeing — you’re not just deleting code, you’re deciding, on purpose, what you’re no longer going to carry.
One last look
Before I shut it down, I ran a single track through the Studio over and over —
the same song into a different visualizer each time. Here’s what librosa and a
pile of shaders made of it.








That’s the show I’m switching off. The ear underneath it keeps going.
The lesson I keep relearning
A side project’s value is rarely where you thought it was when you built it. I made Sonogram for the pictures. I’m keeping it for the math. Retiring something doesn’t have to mean throwing it all away — sometimes the right move is to find the load-bearing fifteen percent, give it a better home, and let the rest go with a clean conscience.
Sonogram, you were a good time. The ear lives on in 1-bit.
Technical note for the curious: the analysis lives in internal/analyze in
1-bit-bridge — a sox-driven streaming decode fanned out to a peak envelope, an
R128 loudness meter, and a single STFT feeding both the key chroma and the tempo
onset envelope. One decode, four answers, all cached. It’s feature-gated and off
by default; flip it on per-library if you want it.