first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
# clock/clock
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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.