I wanted to build a synth. Not a toy—an actual instrument that sounds good and feels good to play. The TB-303 seemed like the obvious choice. It's iconic, its architecture is well-documented, and honestly, I just wanted to hear that squelch in something I made myself.
The Basics
If you've never looked under the hood of a 303, here's the short version: it's a single oscillator (sawtooth or square wave) running through a resonant lowpass filter with an envelope that controls both the amplitude and the filter cutoff. That's it. The magic is in how those simple pieces interact.
The Web Audio API gives you all of this out of the box. OscillatorNode for the waveform, BiquadFilterNode for the filter, GainNode for amplitude. Wire them together and you've got a signal chain.
The Sequencer
A 303 without a sequencer is just a monosynth. The step sequencer is where the personality comes from—those rests, accents, and slides that turn a simple pattern into something that grooves.
I built a 16-step sequencer where each step has:
- • Note (C through B)
- • Octave (-1 to +2)
- • Gate (on/off)
- • Accent (boosts amplitude and filter envelope)
- • Slide (portamento to the next note)
The tricky part was timing. JavaScript's setInterval isn't accurate enough for music—it drifts. But it's fine for triggering audio events as long as you're scheduling them ahead using the Web Audio clock, which is sample-accurate.
I ended up using recursive setTimeout instead of setInterval. Why? Because it reads the current BPM fresh on every tick. You can turn the tempo knob while the sequencer is running and it responds immediately—no pause, no stutter.
The Filter
This is where things got interesting. The 303's filter is the sound. That resonant squelch when the envelope opens up, the way it screams when you push the resonance—that's what people are paying thousands of dollars for when they buy vintage hardware.
The Web Audio BiquadFilterNode isn't a perfect replica, but it gets close enough. The key is the envelope modulation: when a note triggers, I schedule a rapid sweep from the base cutoff frequency up to a peak (controlled by the Env Mod knob), then back down over the decay time.
The accent adds extra intensity—both to the amplitude and to how far the filter opens. Accented notes cut through the mix. That's intentional; that's how you program groove into a pattern.
Real-Time Control
I spent way too long on the knobs. They needed to feel right—smooth drag response, visual feedback, values that update in real-time without any lag.
The trick is useRef. React state is great for UI updates, but it's too slow for audio. Every audio parameter has a ref that gets synced on every render. The audio callback reads from refs, not state. That way, when you drag a knob, the sound changes immediately—not on the next React render cycle.
I also had to use cancelScheduledValues() on the filter's AudioParam before setting new values. Otherwise, the envelope automation would fight with the knob changes and you'd get weird jumps.
The Drums
An acid line needs a beat. I added an optional drum machine with two patterns: a straight four-on-the-floor techno kick, and a breakbeat with spaced-out kicks and snares on the backbeat.
The drums are fully synthesized—no samples. The kick is an oscillator with a pitch envelope that drops from 150Hz to 40Hz over about 100ms. The snare is filtered noise plus a short triangle wave body. The hi-hat is highpass-filtered noise with a fast decay.
Simple synthesis, but it sounds like drums. That's all you need.
Variable Length
One feature I added that the original 303 doesn't have: variable sequence length. You can set the pattern anywhere from 1 to 16 steps. The synth loops at that length while the drums keep running on 16 steps.
Set it to 3 or 5 steps and you get polyrhythmic patterns against the 4/4 drums. Set it to 1 and you get a single repeating note—perfect for filter sweeps.
The Visualizer
I routed the audio through an AnalyserNode and drew the waveform to a canvas. It's a simple time-domain visualization—just the raw waveform oscillating. But it makes the synth feel alive. You can see the filter opening up on accented notes.
Export to DAW
A web synth is fun, but what if you want to use it in a real production? I added two export options:
- • Record Audio: Uses the
MediaRecorderAPI to capture the synth output as a WebM file. The elapsed time displays while recording, and when you stop, you can preview the recording before downloading. - • Export MIDI: Generates a standard MIDI file from the current pattern. Import it into Ableton, Logic, or any DAW and use your own soft synths.
The MIDI export writes a proper Type 0 file with tempo metadata and variable-length delta times. It maps the 303 notes to MIDI, preserving accent velocity and approximate slide timing. Not perfect, but enough to capture your patterns.
What I Learned
The Web Audio API is more capable than most people realize. You can build real instruments with it—not just toys. The main challenges are timing (use the audio clock, not JavaScript timers) and real-time control (use refs, not state).
The 303 is simple on paper but deep in practice. A single oscillator, a single filter, a step sequencer. But the interaction between accent, slide, decay, and resonance creates infinite variation. Constraints breed creativity.
The synth is live at /projects/acid-synth. Press space to play. Drag the knobs. Record your jams or export MIDI to your DAW. It's free, it runs in your browser, and yes—it squelches.