Build story
Building a spatial groovebox in the browser
How a Python script for making "8D audio" turned into seven browser instruments that all play through one binaural 3D engine — no plugins, no backend, nothing uploaded.
🎧 Put headphones on before reading this — then hit the demo button on the landing page. A drum beat will circle your head. Everything below exists to make more of that feeling.
It started as a Python script
"8D audio" is the YouTube trick where a track swirls around your head. I wanted to make my own, so I built a Python tool: load a track, define a path around the listener, convolve it through HRTF filters (the measured ear-and-head responses that fool your brain into hearing direction), add distance, Doppler and reverb, render a WAV. It worked.
Then I tried porting it to the browser and found something
annoying: the Web Audio API's native PannerNode — one line of
setup — sounded better than my carefully wired KEMAR SOFA
convolution. The browser ships a real-time binaural renderer in
every tab, and almost nothing uses it. I killed my own
bring-your-own-HRTF feature, froze the Python as a proof of
concept, and rebuilt around the discovery.
The idea that organised everything
An 8D converter is a commodity. The interesting realisation was that once you have a spatialisation engine, everything can play through it. So instead of one converter, the project became a suite of instruments where "where is this sound around your head?" is a performance control, like a fader:
- 8D Audio — draw a path on a sphere; a track follows it (the original tool).
- Drum Machine — a step sequencer where each voice can sit at, or orbit, its own position.
- Bassline — a 303-style acid synth that can circle the head beat-synced.
- Sampler Pads — auto-chop a track; every pad fires from its own direction.
- Spatial FX — a feedback delay whose echoes physically fly around you.
- Scratch Deck — two turntables with EQ, key-lock, loops, and per-deck spatialisation.
- Jam Room — drums and bass on one master clock, each instrument a draggable dot on a radar around your head.
All of it is client-side: Astro static pages, strict TypeScript, the Web Audio API, deployed on Cloudflare Pages. Your audio never leaves your machine.
The hardest problem: scratching a record that's also time-stretched
The deck is where the engineering got real. Two features collide head-on:
Scratching needs a playhead you can grab — drag the
platter backwards and the audio must run in reverse, instantly,
with zero latency. The Web Audio API's
AudioBufferSourceNode can't do any of that: it can't seek, can't
reverse, can't scrub. So the deck runs on a custom
AudioWorklet that holds the sample data and advances a floating-
point playhead by a rate the main thread drives — rate 1 is
normal playback, negative is reverse, 0 is a held platter. Jog
wheel angular velocity becomes rate; all smoothing is
setTargetAtTime on the audio clock so nothing clicks.
Key-lock needs the opposite: change tempo without changing pitch, which means time-stretching — I implemented WSOLA (waveform-similarity overlap-add) as a streaming processor, with a unit test that verifies the pitch actually holds when the tempo moves. But a time-stretcher fundamentally cannot scratch: it needs a window of future audio, and "future" is undefined when the playhead is being dragged backwards at varying speed.
The resolution is a graceful handoff. Key-lock runs only for sane forward rates (0.5–2.0×). The moment you grab the platter, scratch backwards, or slam the brake, the worklet falls back to plain varispeed — so scratching pitches naturally, the way vinyl does — and when you let go it crossfades back into the key-locked stream. The stretch path costs about 10 ms of latency; the scratch path stays at zero. You stop noticing the seam, which is the point.
Smaller things I'm fond of
- Drum hits are short transients, so spatial orbits don't animate the panner — each hit's position is set at its scheduled time, sample-accurately. Orbits render identically offline, so the exported WAV is the performance.
-
Everything musical runs on one look-ahead scheduler on the audio
clock, not
setTimeoutguesswork — patterns keep perfect time even with the tab backgrounded. - Drum exports render an extra decay tail and wrap it back onto the start, so loops are gapless at exactly the musical length.
- Patterns share as URLs — the whole session state fits in a base64 hash. No accounts, no database, nothing stored anywhere.
What I learned
The browser is a legitimate instrument platform. Between
PannerNode's built-in binaural rendering, AudioWorklets for
custom DSP, and OfflineAudioContext for faster-than-realtime
export, the only real constraints I hit were API shape, not
capability. And the best architectural decision was made by
deleting code: the day the native HRTF beat my hand-rolled
convolution was the day the project stopped being a port and
started being a product.
Try the suite — it's free, it's instant, and the hi-hat really does circle your head.