Files
skills/docs/media/audio/midi/src/clock/clock.md
2026-05-29 08:20:56 +01:00

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 = 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

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.