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 setTimeout guesswork — 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.