Field Notes

Horizonte: engineering a music player where the record has mass

How I turned each album's audio into the physical constants of its world, and wrote a test that fails when those worlds start to look alike.

September 28, 2026 · Mariana Castro

Most music visualizers make the same promise: the screen moves when the music moves. They keep it, and that's the problem. With the sound off, every record looks like every other record. With the sound on, they all look like the same record at different volumes. Horizonte came out of a question that reacting to audio can't answer: why should two albums feel different when nothing is playing? My own first answer fell into the same trap. Measured the way a person looks, its albums correlated 0.99 with each other.

The problem

Reacting to live audio is the easy half. An AnalyserNode hands you a spectrum sixty times a second, and anything wired to it will pulse on the beat. It always looks cheap, and it says nothing about the record. A sphere that swells with the bass knows how loud this second is, not what this album is.

The usual alternatives go the other way. A player shows the cover and a progress bar, and the record's identity is reduced to an image file. Or a generative scene is tuned by hand until it feels right, and the feeling can't be traced back to anything in the audio.

What was missing was a world whose shape is a measurement of the record: something you could point at and ask which property of the music produced it.

The idea: music has mass, and mass bends space

Horizonte is a single full-screen fragment shader. The typography, the album art and the dust are drawn into two flat canvases, and a gravitational lens bends both with the same field. There is no 3D scene and no interface floating on top: the artist's name and the cover are deformed by the same mass.

The design rests on one split: identity is measured and constant, reaction is live and bounded. Each album's audio is analysed once, offline, and what a record is becomes the constants of its world. Loudness sets the weight of the type and the scale of the body. Brightness sets how flat the body lies and how hard the light cuts its rim. Duration sets how heavy it is to travel past. Playback only perturbs those constants.

Even the limit on that perturbation is measured. How far live audio may bend the world depends on the record's own dynamic range: about 5% for the most compressed album in the catalogue, about 19% for the most dynamic. Before, it was one hard-coded number for everyone. And if the listener asks the system for reduced motion, the live perturbation drops to zero: the world keeps its shape and stops reacting.

The engineering decision: one contract, two producers

Every descriptor is normalised against fixed absolute anchors, such as loudness between −32 and −12 dB or brightness between 200 and 2,600 Hz, never against the other albums. Stretching each measurement across the collection's range would be more convenient, and it would mean that adding one record changes every other record's world. Here a signature depends only on its own audio.

That determinism had to survive a second producer. The curated catalogue is measured offline by a Python script. A listener can also drop their own files into the page, and the same analysis runs in the browser, in a Web Worker, with a hand-written FFT and nothing uploaded. Both produce the same AlbumSignature, and past that point the engine never asks where a record came from.

"The same" had to be checkable. Fed identical PCM, the TypeScript port and the Python original produce the same base64 envelope string on every album measured and 0.0000% difference on every scalar. End to end, though, from the real audio files, the first comparison was 20.5% off in loudness. The DSP wasn't the cause. The two tools fold stereo into mono differently:

afconvert, catalogue   (L + R) / √2    equal-power   −3 dB
Web Audio, browser     0.5 · (L + R)   spec          −6 dB

Almost 3 dB of systematic difference, from a line nobody writes on purpose. Aligning the browser with the tool that produced the catalogue brought every derived field constant within 0.55% of its range. A conformance vector with its own audio excerpts now runs in CI, so the two producers can't drift apart quietly.

Measuring what the eye measures

Every mapping in the sensory map had a reason on its own. Loudness to weight made sense; brightness to flatness made sense. Each one was validated by checking that its parameter actually travelled across its range. None of that measured what a person sees.

When I measured the composition as an image, the result was hard to argue with. Every mapping had the form scalar × fixed shape: it scaled a composition that had already been decided. Rasterised and blurred to the equivalent of a glance, the albums correlated 0.99 with each other, and 44% of the ink was identical on every one of them. The identity was the colour.

The fix changed the model, not the numbers. One pure module now turns a signature into the anatomy of its world. Dynamics sculpts the contour and tears gaps between tracks. Pulse stratifies the ring into shells. Duration scatters the body into satellites. The ring, the layout, hit-testing, the engine and the shader all read that same geometry. What stays fixed is a set of relations, not a drawing: there is one dark mass, the album's time is a closed circuit you can point at, angle is duration, and the name weighs what the record weighs.

The guardrail is a test that looks the way a person looks. It rasterises each album's silhouette from the same geometry functions the renderer uses, never a parallel model that could diverge. These are the same fourteen albums as that rasteriser draws them, each in the same position in both grids:

The same fourteen albums rasterised by the perception test. Above, the previous geometry: fourteen near-identical segmented rings. Below, the current morphology: fourteen distinct shapes, with sculpted contours, shells, gaps and satellites.

The test then blurs each silhouette by visual angle rather than screen pixels, the equivalent of a glance, and correlates every pair:

At a glance Previous geometry Current Limit
Mean correlation between albums 0.992 0.539 ≤ 0.72
Most similar pair of musically distant albums 0.998 0.867 ≤ 0.90
Ink shared by every album 44.3% 0.1% ≤ 10%
Visual distance following musical distance 0.44 0.51 ≥ 0.25

Each limit sits between the old measurement and the new one: loose enough for a new record to join, tight enough to reject what came before. Four albums have entered the catalogue since the limits were set, and the test still passes. One case in the suite runs the old geometry through the same rasteriser and requires it to fail. A guardrail the previous version would pass isn't a guardrail.

The last row is the most revealing. The old composition wasn't wrong about which albums were alike; the ordering was already there. It had squeezed every difference into a sliver too thin to see. The guardrail doesn't demand that every pair look different, either. Two records that measure alike should look alike. What it forbids is two musically distant records converging.

A descriptor has to earn its axis

Adding a new dimension to a record's identity is tempting, and most candidates turn out to be an existing one under another name. Across the catalogue, spectral rolloff correlates 0.995 with brightness. The bass ratio correlates −0.98. Brightness variation between tracks, −0.86. Each was measured on the real catalogue and rejected.

The first hypothesis for rhythm was onset density: count attacks per second. It correlated 0.825 with brightness and was unstable, with tracks from the same record ranging from 0.17 to 5.87 attacks per second. Counting attacks measures how percussive the timbre is. What survived was the autocorrelation of the onset envelope between 30 and 240 BPM, after whitening each frequency bin against its own recent peak: not how many attacks there are, but whether they fall on a grid. The first half of each album against the second correlates 0.994. It's also immune to mastering. Crushing a signal with an aggressive limiter drops normalised dynamics to 0.00 and moves pulse by 0.004. Compression destroys the size of an attack, not the moment it happens.

What survived is pinned by 710 automated tests: parity between the two producers, the perception guardrail on desktop, tablet and mobile viewports down to 320 px, and the listening rules of the ring, where pausing never erases anything and skipping ahead leaves a gap for the part you didn't hear.

What I left behind and what I'd do differently

Some of Horizonte's most useful results are negative, and they stay documented next to the code that replaced them.

One experiment let the whole world slowly dilate with the record's accumulated charge, a signal on the scale of minutes rather than milliseconds. Even with the gain pushed until the channel saturated almost constantly, flipping up to 6% of the frame's pixels, the change stayed invisible. Crown, core and satellites moved together, so nothing stood still to act as a ruler, and vision read the scene as the same scene. The lesson generalised: a change is only legible at a cold glance if it breaks against a neighbour that didn't change. Twenty-two pixels of uniform dilation vanish; a seven-pixel step in a continuous line doesn't. Today the ring behaves as a material instead: it yields locally where the music presses, relaxes back, and keeps a scar where the load passed its yield point.

Other things stay open on purpose. The 512-point envelope published for each album is decimated without an anti-aliasing filter, so shifting the audio by a single sample (45 µs) can visibly change it. Fixing that would change the signature of every curated album, and those are the calibrated reference of the product, so it's recorded as a known limitation and pinned by a test. Two live channels, the jet and the blur, still sit outside the per-album ceiling; measured, the most compressed album's jet swings slightly more than the most dynamic one's, so the order inverts. Capping them would also erase the most visible sign that the world is alive during playback. That trade-off is documented, not solved.

If I started again, I'd measure in image space from the first mapping. The convergence lived exactly in the gap between validating parameters and validating the screen. And I'd make every descriptor answer three questions before it earned an axis:

  1. Is it new information, or an existing axis under another name?
  2. Is it stable within the same record?
  3. Can someone see it at a glance?

What this project taught me

Building Horizonte wasn't about making music look good on screen. It was about turning a feeling, that two records are different, into something with a traceable cause: this contour comes from this album's dynamics, this twist in the field from its pulse, this ceiling from its dynamic range.

The most important lesson was that every decision can be defensible and the whole can still be wrong. Nothing in Horizonte's parameter space was ever broken; the convergence only existed where a person looks. So the unit of verification has to be what the person actually sees, measured the way they see it, with a test strict enough to reject the version that came before.

See it running

Horizonte inside e-world: sixteen tracks as sixteen sectors, three shells from its pulse, satellites from its 75 minutes.

Open the collection and move between albums before playing anything: each one already has its own shape, weight and light. Then drop a record of your own. The same analysis runs in your browser, nothing leaves your device, and the world it builds is measured from that record alone.


Written by Mariana Castro, Full-Stack Developer & Tech Lead in Fortaleza, Brazil. I build tools that turn complex processes into legible, trustworthy and auditable systems.

GitHub · LinkedIn · marianacastro.dev