feat: initial scaffold for ClickTrack monorepo

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>
This commit is contained in:
AJ Avezzano
2026-04-01 11:14:46 -04:00
commit 5b772655c6
31 changed files with 2762 additions and 0 deletions

121
lib/ctp/schema.ts Normal file
View File

@@ -0,0 +1,121 @@
/**
* 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;
}