Open my home page, click the track under Now playing, and a cassette slides out. It plays. The level meter jumps. And the big chrome blob in the hero starts dancing: it swells with the bass, flinches on every kick, and every little star on the page jumps with it.
None of that is necessary. A portfolio needs a name, some work and a way to get in touch. This one has a tape deck wired to a 3D shader.
Here's how it works, from the sound to the shapes.
Where the sound comes from
The Now playing line is real. It shows what's playing on my own music server at home, the same one Cassette talks to. The page asks every five seconds. If the last thing I played is more than five minutes old, it counts as not playing, so the line never lies about what I'm doing right now.
The obvious next step would be to stream that track to you. I don't, for two reasons.
The first is legal. Playing my library to anyone who opens a web page is a public performance of music I don't own the rights to. Owning a file is not the same as being allowed to broadcast it. (I've written about what owning means before. It applies here too.)
The second is that it would mean opening my music server to the whole internet, for a decoration.
So the tape plays something else: the 30-second preview the store publishes for the same song. Those previews exist to be played to strangers. That's their whole job.
Finding the right one is harder than it sounds. A search for "artist title" works most of the time. A generic title like The Long Way Home gets buried under famous songs with the same name. So the search falls back in steps: first the artist and the title together, then the artist's songs matched against the title, then songs with that title matched against the artist. First hit wins. If none of them match, the tape stays quiet rather than playing the wrong song.
The trap
The browser can analyse any sound it plays. You plug the audio element into a Web Audio graph, add an analyser, and read the waveform sixty times a second.
Except when the audio comes from another site. Then the browser still plays it, perfectly, through your speakers. But the analyser gets nothing but silence. It's a privacy rule: a page shouldn't be able to read the content of something it fetched from someone else's server. Sensible. Also invisible. No error, no warning. Just a flat line and a blob that won't move.
The fix is to not ask for the audio from someone else. Both the search and the audio go through my own server, so to the browser it's all coming from the same place. As a bonus, your browser never talks to Apple directly. It talks to me, and I talk to them.
Hearing the beat
Once the signal is readable, there are two things to pull out of it.
The level meter is the easy one. For each channel, measure how loud the waveform is right now, and let the needle fall back slowly instead of snapping down. That decay is what makes fourteen segments of LEDs read like a real meter instead of flickering lights.
The beat is the interesting one. The analyser splits the sound into frequency bands. I only look at the lowest handful, where the kick drum and the bass live, and average them into one number: how much bass is there right now.
A fixed threshold doesn't work. A quiet acoustic track never crosses it, a loud club track never drops below it. So the page keeps a running average of the bass, and a beat is a moment when the bass jumps well above its own recent average:
bassAvg += (bass - bassAvg) * 0.05;
const beat =
bass > 0.45 && // there is some bass at all
bass > bassAvg * 1.18 && // and it just jumped
now - lastBeat > 260; // and we didn't just fire
Three conditions, and it adapts to every song on its own. It's not a real beat tracker. It doesn't know about tempo or bars. It doesn't need to. It only has to feel right, and it does.
One event, many listeners
The tape doesn't know the blob exists. It doesn't know about the stars either.
Every frame while the sound plays, it sends one event to the page with three values: the level, the bass, and whether this frame is a beat. Anything that wants to react listens for it.
The blob uses the bass as a dial. More bass, more agitation on the surface, and the whole shape grows by a few percent. Every beat gives it a kick, the same spring it uses when you poke it, and sends a ripple across the side facing you.
The stars only care about beats. Each one jumps to 1.4 times its size and settles back over half a second. Not all at once: each one starts a few milliseconds apart, in a random order. In sync, it looks like a light blinking. Slightly out of sync, it looks like a room reacting.
Because it's just an event, anything I add later can join in without the tape changing a line.
The shapes, and the lines that shouldn't be there
The blob isn't always a sphere. Every twenty or thirty seconds it melts into something else: a heart, a star or a moon, all puffed up like balloons. It always goes back through the sphere on the way, so it never passes through anything flat or broken in between.
The first version of the heart and the star had a problem: thin lines across the chrome, like creases in foil.
The shader was finding each shape the brute-force way. For every point on the sphere, walk outward along that direction, sixteen steps, until you hit the surface of the heart. It works. Until a direction hits the surface twice, like the dip at the top of the heart. Or skims past a concave corner, like the inside of a star's points. The search jumps from one answer to another, and on a mirror-like material every jump shows as a line.
The fix was to stop searching. Each outline is now a short series of waves around the centre, computed once, with the fine detail damped so every point and every dip comes out rounded. The shader just adds the waves up. No search, no jumps, nothing that can crease. And it went from sixteen steps per point to about a dozen multiplications.
The moon couldn't work that way at all. A crescent can't be described from its own centre, because a line from the middle goes straight out through the gap. So it's built differently: a tube, bent. Unbent, that tube is exactly the sphere. Bend it, stretch it a little, thin it out, and the sphere curls into a crescent.
Why bother
Because the details are the job.
Nobody hires a developer for the parts that work. They hire one for how far he goes on the parts nobody asked for. The silence from the analyser, the song with a generic title, the line across the chrome: none of those were in any spec. Each one was a small thing that was wrong, and fixing it is the whole difference between something that works and something that feels right.
That's what I do in apps, too. This one just happens to dance.
Go open the tape. Put on something with a kick.
Top comments (0)