I built BPM Tool because I was tired of pausing a session to do tempo math. I wanted a fast, single-screen utility that answers “what is a dotted eighth at 128 BPM?” and then goes a little further: swing, groove, MIDI, and a quick way to hear the feel.

Update: BPM Tool has been rebranded and expanded into GrooveLab, a broader timing + groove toolbox. The live tool and source have moved.

What it does

  • Converts BPM into ms and Hz values for common note divisions
  • Visualises groove and swing timing
  • Lets you audition patterns with a synthesized drum kit
  • Exports the result as MIDI

Why it exists

Most DAWs give you sync values (quarter, eighth, triplet). That is great until you need something odd or a precise millisecond value for an effect that only speaks ms or Hz.

Examples where this comes up constantly:

  • Delay throws that must land exactly on the beat
  • Reverb pre-delay to push a sound forward or back
  • Sidechain release times for the right pump
  • LFO rates when a plugin wants Hz, not note values
  • Compressor attack/release when groove matters

The translation layer between musical intent and parameter units is the whole point of the tool.

The swing math

Swing sounds simple until you implement it. You are shifting the off-beat later while keeping the total duration the same. The cleanest solution I found is ratio-based:

// Convert swing percent into a duration ratio (clamped 50–75%)
const swing = Math.max(50, Math.min(75, swingPercent)) / 100;
const ratio = swing / (1 - swing);
const pairDuration = quarterNote;
const onBeatDuration = (pairDuration * ratio) / (1 + ratio);
const offBeatDuration = pairDuration - onBeatDuration;

At 50% swing you get straight timing. At 66.67% you get a triplet feel. I clamp to a practical range and only apply swing to the hi-hats so the kick/snare stay locked.

Synthesizing drums instead of loading samples

To keep the app snappy (and avoid file size or licensing issues), the drum kit is synthesized in real time with Tone.js:

  • Kick: MembraneSynth with a pitch envelope
  • Snare: NoiseSynth through a bandpass filter
  • Hi-hat: MetalSynth with a short decay

This keeps load time near zero and still gives you enough character to feel the groove.

MIDI export gotchas

MIDI drum maps are strict. Drums belong on channel 10 (0-indexed as 9), and each sound is tied to a pitch (36 = kick, 38 = snare, 42 = closed hat).

The tricky part is translating continuous timing into discrete MIDI ticks. The playback engine is flexible, but MIDI files want exact integers. That meant keeping the groove math deterministic so the export and the auditioned pattern stay aligned.

In practice, I generate drum notes by groove type (straight, swing, triplet), then dump them into a channel-10 track. Kick/snare stay on quarter notes, hats are the only subdivision that changes. The same note map drives both playback and export so there is no mismatch.

const notes = generateDrumNotes(bpm, grooveType, swingPercent, bars);
notes.forEach((note) => {
  track.addNote({
    midi: note.pitch,
    time: note.time,
    duration: note.duration,
    velocity: note.velocity,
  });
});

State without a state library

The app is small enough that React hooks do the job:

  • useState for core values
  • useContext for global UI state
  • a useLocalStorage hook for bookmarks and theme

No Redux, no Zustand. It stays simple until it needs to be complex.

The equation that powers everything

Quarter Note (ms) = 60,000 / BPM

From there:

  • Half note = quarter x 2
  • Eighth note = quarter / 2
  • Dotted = value x 1.5
  • Triplet = value x (2/3)
  • Hz = 1000 / ms

Nothing fancy. Just fast access to the numbers you always end up needing.

What I learned

Building a tool for musicians as a musician is different. You can feel the friction in your own workflow, which makes scope decisions easier.

The hardest part was stopping. Everyone has a “one more feature” suggestion: polyrhythms, tempo ramps, odd metres. I kept the focus tight: translate tempo into timing, then support that core with groove visualisation, playback, and export.

That is the whole product.


Built with Next.js, React, TypeScript, Tone.js