# 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 = 24` and `TICKS_PER_BEAT` alias 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 ```ts import { bpmToClock, clockToBpm } from "@suaveplan/midi"; const tickMs = bpmToClock(120); // ~20.833 ms const bpm = clockToBpm([tickMs, tickMs, tickMs]); // ≈ 120 ``` ## Usage: Advanced ```ts 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.