A few months ago I set out to build a drum-practice app that runs entirely in the browser, grades your timing in real time, and works offline. It became GrooveSteps, a free drum trainer. Here is how the core pieces actually work, in case you are building anything rhythm or audio related on the web.
Scheduling audio with a look-ahead clock
The naive approach to a metronome, setInterval firing a sound, drifts badly because timers are not sample-accurate. The fix is the pattern Chris Wilson described years ago: a look-ahead scheduler. A setInterval wakes up every ~25 ms, looks a small window into the future, and schedules any notes that fall inside it directly on the Web Audio clock with AudioContext.currentTime and oscillator.start(time).
const lookahead = 0.1; // seconds
function scheduler() {
while (nextNoteTime < audioCtx.currentTime + lookahead) {
scheduleNote(nextNoteTime);
nextNoteTime += 60.0 / bpm / stepsPerBeat;
}
}
setInterval(scheduler, 25);
Because the timing lives on the audio hardware clock, the click stays rock-steady even when the main thread is busy.
Scoring timing in real time
Every scheduled beat registers an expected hit time. When the player taps, I compare the input time to the nearest expected time and bucket it: perfect within 30 ms, good within 60 ms, ok within 100 ms, otherwise a miss. That yields an accuracy percentage, a combo counter, and a letter grade. The interesting wrinkle is latency calibration: browsers and Bluetooth add unpredictable output delay, so a short tap test measures the player's average signed error once and stores an offset that is subtracted on every later judgment.
Talking to a real drum kit with Web MIDI
This surprised people the most: an actual electronic drum kit plugs in over USB and plays the app. The Web MIDI API exposes inputs, and a General MIDI note map turns pad hits into the right sounds.
const access = await navigator.requestMIDIAccess();
for (const input of access.inputs.values()) {
input.onmidimessage = (e) => {
const [status, note, velocity] = e.data;
if ((status & 0xf0) === 0x90 && velocity > 0) triggerPad(GM_MAP[note]);
};
}
Making it installable and offline
A service worker precaches the shell and samples, and the manifest makes it installable, so the trainer runs as an offline Progressive Web App on a phone with no connection. The one gotcha worth repeating: never cache non-OK responses, or you can poison the offline cache with a 404.
Takeaways
- Use a look-ahead scheduler, never raw timers, for anything musical.
- Calibrate latency once instead of fighting it forever.
- Web MIDI is underused and genuinely fun for instrument apps.
If you want to poke at the result, the drum trainer, rudiments, and play-along grooves are all free at groovesteps.com, and I wrote up the rhythm terms in a drum and music glossary. Happy to answer questions about the audio engine.
Top comments (0)