Full self-hosted click track generator for cover bands. Core technical pieces implemented: - CTP (Click Track Protocol) TypeScript schema, Zod validator, and WAV renderer (44.1 kHz, 16-bit PCM, accented downbeats, ramp sections) - MusicBrainz API client with 1 req/s rate limiting - PostgreSQL schema (songs, tempo_maps, registry_sync_log) with triggers - Git registry sync logic (clone/pull → validate CTP → upsert DB) - Next.js 14 App Router: search page, track page, API routes (/api/songs, /api/tracks, /api/generate) - UI components: SearchBar, SongResult, TempoMapEditor, ClickTrackPlayer (Web Audio API in-browser playback + WAV download) - Docker Compose stack: app + postgres + redis + nginx + registry-sync - Multi-stage Dockerfile with standalone Next.js output - .env.example documenting all configuration variables - README with setup instructions, CTP format spec, and API reference Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
122 lines
4.7 KiB
TypeScript
122 lines
4.7 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|