/** * CTP — Click Track Protocol * * A JSON format for describing the tempo map of a song so that a * metronomic click track (WAV) can be generated deterministically. * Version: 1.0 */ // ─── Time signature ─────────────────────────────────────────────────────────── export interface TimeSignature { /** Beats per bar (top number). e.g. 4 */ numerator: number; /** Note value of one beat (bottom number). e.g. 4 = quarter note */ denominator: number; } // ─── Section transitions ────────────────────────────────────────────────────── /** * "step" — tempo changes instantly at the first beat of this section. * "ramp" — tempo interpolates linearly beat-by-beat from bpm_start to bpm_end * over the duration of this section. Requires bpm_start + bpm_end. */ export type SectionTransition = "step" | "ramp"; // ─── Sections ───────────────────────────────────────────────────────────────── /** A constant-tempo section. */ export interface StepSection { /** Human-readable label, e.g. "Intro", "Verse 1", "Chorus". */ label: string; /** The bar number (1-based) at which this section begins. */ start_bar: number; /** Beats per minute for the entire section. */ bpm: number; time_signature: TimeSignature; transition: "step"; } /** A section where tempo ramps linearly from bpm_start to bpm_end. */ export interface RampSection { label: string; start_bar: number; /** Starting BPM (at the first beat of this section). */ bpm_start: number; /** Ending BPM (reached at the last beat of this section, held until next). */ bpm_end: number; time_signature: TimeSignature; transition: "ramp"; } export type CTPSection = StepSection | RampSection; // ─── Count-in ───────────────────────────────────────────────────────────────── export interface CountIn { /** Whether to prepend a count-in before bar 1. */ enabled: boolean; /** Number of count-in bars. Typically 1 or 2. */ bars: number; /** * When true, uses the BPM and time signature of the first section. * When false (future), a separate tempo could be specified. */ use_first_section_tempo: boolean; } // ─── Metadata ───────────────────────────────────────────────────────────────── export interface CTPMetadata { /** Song title */ title: string; /** Artist or band name */ artist: string; /** MusicBrainz Recording ID (UUID). Null when MBID is unknown. */ mbid: string | null; /** Total song duration in seconds (excluding count-in). */ duration_seconds: number; /** Username or handle of the contributor. */ contributed_by: string; /** Whether a human editor has verified this tempo map against the recording. */ verified: boolean; /** ISO 8601 creation timestamp. */ created_at: string; } // ─── Root document ──────────────────────────────────────────────────────────── export interface CTPDocument { /** Protocol version. Currently "1.0". */ version: "1.0"; metadata: CTPMetadata; count_in: CountIn; /** * Ordered list of sections. Must contain at least one entry. * Sections must be ordered by start_bar ascending, with no gaps. * The first section must have start_bar === 1. */ sections: CTPSection[]; } // ─── Derived helpers ────────────────────────────────────────────────────────── /** Returns true if a section uses constant tempo (step transition). */ export function isStepSection(s: CTPSection): s is StepSection { return s.transition === "step"; } /** Returns true if a section ramps tempo. */ export function isRampSection(s: CTPSection): s is RampSection { return s.transition === "ramp"; } /** * Returns the starting BPM of a section regardless of transition type. * For step sections this is simply `bpm`. For ramp sections it is `bpm_start`. */ export function sectionStartBpm(s: CTPSection): number { return isStepSection(s) ? s.bpm : s.bpm_start; }