Every mixtape tool makes a JPEG. This one bends the pitch.
Side B plays your tracks through a tape chain built live in the Web Audio API: wow and flutter from a delay line that refuses to hold still, saturation, a hiss floor, and reels turned by the audio clock.
A cassette never plays a note at quite the pitch it recorded. The motor drifts a fraction of a percent, the capstan jitters against the pinch roller, and the whole song sags and shivers around where it should be. That instability is most of what people mean when they say a tape sounds warm. You can rebuild it in a browser tab with no audio file at all. You take a delay line, and you refuse to let its length hold still.
Search "mixtape maker" and you get a wall of cover-art generators: pick a font, pick a gradient, export a JPEG, post it next to a streaming link. The audio is never touched. It's still the service's file playing through the service's player, and the tape is a picture of a tape. But the thing that made a mixtape a mixtape was never the j-card. It was that the songs came out sounding different on the other end. Side B is a cassette maker that works the other way round: drop in tracks, press record, and you hear them the way a worn deck would have played them.
Pitch is the derivative of delay
Here's the part worth stealing even if you never open the app. Take a delay line set to 20 milliseconds. Leave it there and it does nothing interesting: the output is the input, late. Now start stretching the delay, lengthening it at one percent per second. Samples now leave the line slightly slower than they entered it, and the pitch drops by roughly one percent. Shorten it and the pitch rises. The playback rate is one minus the rate of change of the delay, which means a delay line whose length you keep moving is a pitch shifter. Chorus and flanger pedals have run on this since the seventies.
So the tape transport is two oscillators
summed into a delay node's delayTime. A slow deep one, under a couple of hertz, is wow: the
motor and the reel eccentricity dragging the whole song around. A fast shallow one, up in the
single-digit hertz, is flutter: the capstan and roller chattering. The two are different
artifacts of different parts of a machine, and summing them is what keeps the result from
sounding like a single tidy vibrato.
The rest of the chain is short. Saturation is a waveshaper holding a cubic soft-clip curve: magnetic tape doesn't hit a wall and square off the way digital clipping does, it leans into saturation gradually, so transients round over and pick up harmonics instead of tearing. Hiss is filtered noise, level matched to sit under the program material, running continuously so the quiet passages read as tape rather than as a file that stopped. All of it is nodes on a live graph. Drag a slider and the sound changes under the playhead, because there is no rendered file to wait for and nothing pre-baked anywhere in it.
The reels can't lie
The cassette is drawn on canvas, and every frame asks AudioContext.currentTime where the
audio actually is. That's the whole reason it's canvas and not a looping GIF. Rotation and the
take-up ratio are computed from playback position, so the picture cannot drift away from the
sound over a four-minute track the way a free-running animation will.
The physics is a nice detail on its own. Tape moves past the head at a constant linear speed, so the angular speed of a reel is that speed divided by its radius. The take-up reel starts on a bare hub, spinning fast, and visibly slows as it fills, while the supply reel does the opposite. Nobody notices this consciously. Everybody notices when it's wrong.
The honest limit is the share link. State (tracks, every effect setting, your hand-drawn cover) encodes into the URL itself, which is what lets the whole thing be a static site with no backend, no database and no accounts. Built-in tracks are synthesized in code, so a link rebuilds them exactly on a stranger's machine. Your own files can't ride in a URL, so a tape made from them asks whoever opens it to supply the audio. That was the trade for not building an upload bucket, and I'd make it again.
Try it
Open Side B, click two of the built-in tracks into the tray, and press record. The full chain is already on, so the first thing you hear is the tape. Then find the wow and flutter depth and push it further than is reasonable while the song is playing: that's the delay line stretching, live. Doodle something on the j-card label, press Share tape, and send the link. The source is on GitHub.
This post is part of the build log: every app my automated factory ships gets written up here, honestly. Browse everything at apps.charliekrug.com. Comments are open below.
Loading comments…