DEV Community

The Tap Tempo
The Tap Tempo

Posted on

The Math (and Code) Behind Converting BPM to Milliseconds

Why Every Audio Developer Eventually Needs This Formula

If you've ever built anything involving rhythm — a metronome, a delay effect, a rhythm game, a DAW plugin, or even a CSS animation that needs to "feel" like it's on-beat — you'll run into the same wall: your code operates in milliseconds, but music operates in BPM (beats per minute). At some point you have to convert between the two, and getting it slightly wrong is the difference between an effect that locks to the groove and one that feels off.

This post walks through the core formula, the code that implements it, and the edge cases (dotted notes, triplets, LFO sync) that trip people up.

The Core Formula

There are 60,000 milliseconds in a minute. If a song has a tempo of BPM beats per minute, then one beat (a quarter note, in standard time) lasts:

quarterNoteMs = 60000 / BPM

That's it. Everything else in this post is a variation on that one line.

js
function bpmToMs(bpm) {
return 60000 / bpm;
}

bpmToMs(120); // 500 — a quarter note at 120 BPM is exactly half a second
Extending to Other Note Values

Every standard note duration is a simple multiple or fraction of the quarter note:

js
function noteDurations(bpm) {
const quarter = 60000 / bpm;
return {
whole: quarter * 4,
half: quarter * 2,
quarter,
eighth: quarter / 2,
sixteenth: quarter / 4,
thirtySecond: quarter / 8,
dottedQuarter: quarter * 1.5,
dottedEighth: (quarter / 2) * 1.5,
quarterTriplet: quarter * (2 / 3),
eighthTriplet: (quarter / 2) * (2 / 3),
};
}

console.log(noteDurations(120));
// { whole: 2000, half: 1000, quarter: 500, eighth: 250, sixteenth: 125,
// thirtySecond: 62.5, dottedQuarter: 750, dottedEighth: 375,
// quarterTriplet: 333.33, eighthTriplet: 166.67 }

Two subtleties that are easy to get backwards:

Dotted notes are 1.5× their undotted value (a dot adds half the note's original duration).
Triplets divide a beat into three instead of two, so a triplet note is 2/3 the duration of its straight counterpart — not 1/3.
Where This Actually Gets Used

This isn't just music-theory trivia — it maps directly onto real audio programming tasks:

Use case What you're really doing
Delay effect / echo plugin Setting delayTimeMs so repeats land on a subdivision
Reverb pre-delay Offsetting the reverb tail so it doesn't smear transients
LFO / tremolo rate Converting a note duration into a frequency in Hz
Rhythm game timing windows Defining how wide a "hit window" is, in ms, per difficulty
CSS/Canvas beat-synced animation setInterval/requestAnimationFrame timed to a groove
Converting a Note Duration to an LFO Frequency

If you're driving a Web Audio OscillatorNode or any modulation source, you often need Hz, not ms. The conversion is just the reciprocal:

js
function noteToHz(ms) {
const seconds = ms / 1000;
return 1 / seconds;
}

noteToHz(bpmToMs(128)); // ≈ 2.13 Hz — one quarter-note cycle per beat at 128 BPM
Going the Other Direction: MS Back to BPM

Sometimes you have the millisecond value first — say, a delay time that already "feels right" — and you want to know what tempo it implies:

js
function msToBpm(ms, noteFraction = 1) {
// noteFraction: 1 = quarter note, 0.5 = eighth, 1.5 = dotted quarter, etc.
const quarterMs = ms / noteFraction;
return 60000 / quarterMs;
}

msToBpm(300, 0.5); // 100 — a 300ms eighth note implies 100 BPM

This reverse lookup is genuinely useful when you're reverse-engineering the tempo of a sample or matching a pre-recorded delay loop to a new track.

The Part Most Tutorials Skip: Precision and Drift

A few things that will bite you in production code:

Use performance.now(), not Date.now(), for anything measuring intervals in the browser. Date.now() has coarser resolution and is subject to system clock adjustments; performance.now() gives you a monotonic, sub-millisecond clock.
Sample-rate limits matter for very short intervals. At a 44.1kHz sample rate, 1ms ≈ 44 samples. If you're calculating sub-10ms delay times (like tight slapback echo), round carefully — aggressive rounding compounds into audible drift over many repeats, especially with feedback.
Don't truncate decimals early. 60000 / 128 = 468.75. If you round that to 469 before further math (like converting to Hz), your LFO will drift out of phase over a long session. Keep full float precision until the final assignment.
Putting It Together: A Minimal Tempo Utility

Here's a small, dependency-free utility that covers the common cases:

js
const TempoUtils = {
bpmToMs(bpm, subdivision = 1) {
return (60000 / bpm) * subdivision;
},
msToHz(ms) {
return 1000 / ms;
},
msToBpm(ms, subdivision = 1) {
return 60000 / (ms / subdivision);
},
};

// Dotted eighth delay at 120 BPM (subdivision = 0.5 * 1.5 = 0.75)
TempoUtils.bpmToMs(120, 0.75); // 375

Subdivision multipliers, for reference: whole = 4, half = 2, quarter = 1, eighth = 0.5, sixteenth = 0.25, dotted-X = X × 1.5, X-triplet = X × (2/3).

Try It Without Writing Any Code

If you just need the numbers and don't want to wire this up yourself, I built a free calculator that does exactly this — BPM to ms, dotted notes, triplets, and a genre-based delay/reverb preset chart: BPM to Milliseconds Calculator. It's part of a small toolkit I've been building — including a tap tempo BPM finder that uses the same interval-averaging approach described above, and a Delay & Reverb Time Calculator for the full effect-chain workflow.

Wrap-Up

The formula is one line: 60000 / BPM. Everything else — dotted notes, triplets, LFO Hz conversion, reverse lookups — is just careful multiplication and remembering which direction you're dividing. If you're building anything rhythm-adjacent, this small utility will save you from the "why does my delay feel almost right but not quite" debugging session everyone eventually has.

If you've solved beat-detection or tempo-sync problems in an interesting way — especially audio-based BPM detection from a raw waveform rather than manual tapping — I'd love to hear about your approach in the comments.

Top comments (0)