3.0 KiB
clock/clock
(no submodules)
Purpose
Helpers for converting between MIDI Clock ticks, beats, and tempo. MIDI 1.0 defines 24 pulses per quarter note (PPQN = 24); every other unit derives from that constant. This module is the canonical place those derivations live so downstream sequencers and beat-aligned UI code do not re-implement them (and get the rounding subtly wrong).
bpmToClock returns the per-tick interval in milliseconds for a given tempo. clockToBpm is the inverse: given a window of observed inter-tick intervals (from a hardware Clock source), it returns the estimated BPM. The two compose into a stable tempo-follower when the input window is large enough to average out jitter.
Features
PPQN = 24andTICKS_PER_BEATalias for clarity at call sites.bpmToClock,clockToBpm— bidirectional conversion.beatsToTicks,ticksToBeats— beat-grid conversions for sequencer code.- Aggressive input validation — empty arrays, non-positive intervals, NaN, infinity all throw
MidiError.
Usage: Basic
import { bpmToClock, clockToBpm } from "@suaveplan/midi";
const tickMs = bpmToClock(120); // ~20.833 ms
const bpm = clockToBpm([tickMs, tickMs, tickMs]); // ≈ 120
Usage: Advanced
import { clockToBpm, PPQN } from "@suaveplan/midi";
// Build a rolling-window tempo follower from incoming Clock messages.
class TempoFollower {
private readonly windowSize = PPQN * 4; // average over a full bar
private intervals: number[] = [];
private lastTickMs: number | undefined;
onClock(nowMs: number) {
if (this.lastTickMs !== undefined) {
this.intervals.push(nowMs - this.lastTickMs);
if (this.intervals.length > this.windowSize) this.intervals.shift();
}
this.lastTickMs = nowMs;
}
bpm(): number | undefined {
return this.intervals.length >= this.windowSize / 2
? clockToBpm(this.intervals)
: undefined;
}
}
API Reference
PPQN: 24— MIDI Clock pulses per quarter note.TICKS_PER_BEAT: 24— alias of PPQN for readability.bpmToClock(bpm: number): number— milliseconds per tick. Throws on non-positive input.clockToBpm(intervalsMs: readonly number[]): number— averages a window of intervals to a BPM. Throws on empty / invalid input.beatsToTicks(beats: number): number— round-to-integer tick count. Throws on negative input.ticksToBeats(ticks: number): number— rational beat count (no rounding).
Implementation Notes
clockToBpm uses the arithmetic mean rather than the median because the input window is expected to be small (~24..96 entries) and the median's k-th-order-statistic cost dwarfs the precision benefit. Validation runs before the sum to fail fast: a single bad interval rejects the whole batch rather than silently skewing the result. beatsToTicks uses Math.round so half-beat inputs land on the nearest tick — Math.floor would systematically slip ahead of the beat, Math.ceil behind it.