The Web Audio API is one of the most powerful and least understood browser APIs. It can synthesize sounds, process audio in real time, and maintain sample-accurate timing. Building a drum machine exercises all of these capabilities and teaches you patterns that apply to any audio application in the browser.
Why the Web Audio API exists
Before the Web Audio API, browser audio was limited to the <audio> element. You could play a file. That was it. No precise timing, no layering, no effects, no synthesis.
The Web Audio API provides a modular routing graph where audio sources connect through processing nodes to a destination (your speakers). Think of it like a signal chain in a recording studio.
const ctx = new AudioContext();
// Source -> Gain -> Destination
const source = ctx.createBufferSource();
const gain = ctx.createGain();
source.connect(gain);
gain.connect(ctx.destination);
Timing is everything
The fundamental challenge of a drum machine is accurate timing. Using setInterval or setTimeout for timing musical events does not work because JavaScript's event loop provides approximately 4-16ms timing accuracy. At 120 BPM, a sixteenth note is 125ms. A 16ms jitter is audible as a "sloppy" beat.
The Web Audio API solves this with its own high-precision clock (ctx.currentTime) that runs on the audio thread, independent of the main JavaScript thread.
The pattern is a "lookahead scheduler":
const LOOKAHEAD = 25; // ms to look ahead
const SCHEDULE_AHEAD = 0.1; // seconds to schedule in advance
let nextNoteTime = 0;
let currentStep = 0;
function scheduler() {
while (nextNoteTime < ctx.currentTime + SCHEDULE_AHEAD) {
scheduleNote(currentStep, nextNoteTime);
advanceStep();
}
setTimeout(scheduler, LOOKAHEAD);
}
The scheduler runs every 25ms (using setTimeout's imprecise timing) and schedules notes using the precise ctx.currentTime. As long as the lookahead window is larger than the setTimeout jitter, every note is scheduled with sample-accurate timing.
Loading and playing samples
Drum machines use recorded samples (kick, snare, hi-hat, etc.). Load them as AudioBuffers:
async function loadSample(url) {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
return ctx.decodeAudioData(arrayBuffer);
}
function playSample(buffer, time) {
const source = ctx.createBufferSource();
source.buffer = buffer;
source.connect(ctx.destination);
source.start(time);
}
Each createBufferSource() is single-use. You cannot replay a source node; create a new one each time. This feels wasteful but is by design -- the browser optimizes this internally.
The step sequencer pattern
A drum machine typically has 16 steps (one bar of sixteenth notes at 4/4 time). Each instrument has a row of 16 buttons. If a button is active, the sample plays on that step.
const pattern = {
kick: [1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0],
snare: [0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0],
hihat: [1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0],
};
function scheduleNote(step, time) {
for (const [instrument, steps] of Object.entries(pattern)) {
if (steps[step]) {
playSample(samples[instrument], time);
}
}
}
Adding feel
A perfectly quantized beat sounds robotic. Real drummers have micro-variations in timing and velocity. Add human feel by introducing small random variations:
function humanize(time, amount = 0.01) {
return time + (Math.random() - 0.5) * amount;
}
function playWithVelocity(buffer, time, velocity) {
const source = ctx.createBufferSource();
const gain = ctx.createGain();
gain.gain.value = velocity;
source.buffer = buffer;
source.connect(gain);
gain.connect(ctx.destination);
source.start(time);
}
Velocity variation of +/- 20% and timing variation of +/- 10ms creates a noticeable improvement in feel without sounding sloppy.
The tool
For a ready-to-use browser drum machine with preset patterns, adjustable tempo, and multiple kit sounds, I built one at zovo.one/free-tools/drum-machine. It implements all the patterns described above with a visual step sequencer interface.
I'm Michael Lip. I build free developer tools at zovo.one. 500+ tools, all private, all free.
Top comments (0)