DEV Community

Joseph Anady
Joseph Anady

Posted on

Building a real-time, offline drum trainer in the browser with Web Audio and Web MIDI

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);
Enter fullscreen mode Exit fullscreen mode

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]);
  };
}
Enter fullscreen mode Exit fullscreen mode

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)