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>
264 lines
7.0 KiB
Markdown
264 lines
7.0 KiB
Markdown
# ClickTrack
|
||
|
||
A self-hosted, open-source click track generator for cover bands.
|
||
|
||
Search any song, browse community-contributed tempo maps, and download a metronomic WAV file — accented downbeats, time signature changes, and tempo ramps included.
|
||
|
||
---
|
||
|
||
## Features
|
||
|
||
- **Song search** via MusicBrainz, cached locally in PostgreSQL
|
||
- **Community tempo maps** in the open CTP (Click Track Protocol) format
|
||
- **WAV generation** — 44.1 kHz, 16-bit PCM, mono; 880 Hz accented / 440 Hz unaccented clicks
|
||
- **Count-in** — configurable 1–8 bar count-in prepended before bar 1
|
||
- **Tempo ramps** — linear BPM interpolation beat-by-beat for ritardando/accelerando sections
|
||
- **Federated registry** — pull CTP files from any public Git repo on a cron schedule
|
||
- **Self-hosted** — single `docker compose up` gets you running
|
||
|
||
---
|
||
|
||
## Quick Start (Docker)
|
||
|
||
### 1. Clone and configure
|
||
|
||
```bash
|
||
git clone https://github.com/your-org/clicktrack.git
|
||
cd clicktrack
|
||
cp .env.example .env
|
||
# Edit .env — at minimum set a strong POSTGRES_PASSWORD
|
||
```
|
||
|
||
### 2. Start the stack
|
||
|
||
```bash
|
||
docker compose up -d --build
|
||
```
|
||
|
||
This starts:
|
||
| Container | Role |
|
||
|---|---|
|
||
| `app` | Next.js 14 (port 3000, proxied via nginx) |
|
||
| `postgres` | PostgreSQL 16 with persistent volume |
|
||
| `redis` | Redis 7 for caching |
|
||
| `nginx` | Reverse proxy on ports 80/443 |
|
||
|
||
### 3. Open the app
|
||
|
||
Navigate to `http://localhost` (or your server's IP/domain).
|
||
|
||
### 4. Enable registry sync (optional)
|
||
|
||
Set `REGISTRY_REPO` in `.env` to a public GitHub repo URL containing CTP files, then:
|
||
|
||
```bash
|
||
docker compose --profile registry up -d
|
||
```
|
||
|
||
The `registry-sync` container will pull and import CTP files every `REGISTRY_SYNC_INTERVAL` seconds (default: 1 hour).
|
||
|
||
---
|
||
|
||
## Development Setup
|
||
|
||
```bash
|
||
# Prerequisites: Node 20+, a local PostgreSQL 16, Redis 7
|
||
cp .env.example .env
|
||
# Update DATABASE_URL and REDIS_URL to point at your local services
|
||
npm install
|
||
|
||
# Apply schema
|
||
psql $DATABASE_URL -f lib/db/schema.sql
|
||
|
||
# Start dev server with hot reload
|
||
npm run dev
|
||
```
|
||
|
||
Or with Docker (hot-reload via volume mount):
|
||
|
||
```bash
|
||
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
|
||
```
|
||
|
||
---
|
||
|
||
## CTP — Click Track Protocol
|
||
|
||
CTP is an open JSON format for describing a song's complete tempo map. A CTP file is a plain `.ctp.json` file that can be version-controlled and shared.
|
||
|
||
### Full example
|
||
|
||
```json
|
||
{
|
||
"version": "1.0",
|
||
"metadata": {
|
||
"title": "Bohemian Rhapsody",
|
||
"artist": "Queen",
|
||
"mbid": "b1a9c0e0-9c4a-4a6b-8b5a-1234567890ab",
|
||
"duration_seconds": 354,
|
||
"contributed_by": "freddie_fan",
|
||
"verified": false,
|
||
"created_at": "2026-04-01T00:00:00Z"
|
||
},
|
||
"count_in": {
|
||
"enabled": true,
|
||
"bars": 1,
|
||
"use_first_section_tempo": true
|
||
},
|
||
"sections": [
|
||
{
|
||
"label": "Intro (Ballad)",
|
||
"start_bar": 1,
|
||
"bpm": 72.0,
|
||
"time_signature": { "numerator": 4, "denominator": 4 },
|
||
"transition": "step"
|
||
},
|
||
{
|
||
"label": "Rock Section",
|
||
"start_bar": 45,
|
||
"bpm": 152.0,
|
||
"time_signature": { "numerator": 4, "denominator": 4 },
|
||
"transition": "step"
|
||
},
|
||
{
|
||
"label": "Outro (slowing)",
|
||
"start_bar": 120,
|
||
"bpm_start": 152.0,
|
||
"bpm_end": 72.0,
|
||
"time_signature": { "numerator": 4, "denominator": 4 },
|
||
"transition": "ramp"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### Schema rules
|
||
|
||
| Field | Type | Notes |
|
||
|---|---|---|
|
||
| `version` | `"1.0"` | Must be exactly `"1.0"` |
|
||
| `metadata.mbid` | UUID or `null` | MusicBrainz Recording ID |
|
||
| `metadata.duration_seconds` | number | Total song duration (excluding count-in) |
|
||
| `count_in.bars` | 1–8 | Number of count-in bars |
|
||
| `sections[*].start_bar` | integer ≥ 1 | Must start at 1, strictly ascending |
|
||
| `sections[*].transition` | `"step"` \| `"ramp"` | Step = instant change; ramp = linear interpolation |
|
||
| `sections[*].bpm` | 20–400 | Only for `step` sections |
|
||
| `sections[*].bpm_start` / `bpm_end` | 20–400 | Only for `ramp` sections |
|
||
| `sections[*].time_signature.denominator` | 1, 2, 4, 8, 16, 32 | Must be a power of 2 |
|
||
|
||
### Click sounds
|
||
|
||
| Beat | Frequency | Amplitude |
|
||
|---|---|---|
|
||
| Beat 1 (downbeat) | 880 Hz | Accented |
|
||
| Other beats | 440 Hz | Normal |
|
||
|
||
Both use a 12 ms sine wave with exponential decay (`e^(-300t)`).
|
||
|
||
---
|
||
|
||
## Community Registry
|
||
|
||
The registry is a public Git repository containing `.ctp.json` files organised by artist:
|
||
|
||
```
|
||
registry-repo/
|
||
q/
|
||
queen/
|
||
b1a9c0e0-9c4a-4a6b-8b5a-1234567890ab.ctp.json # Bohemian Rhapsody
|
||
t/
|
||
the-beatles/
|
||
...
|
||
```
|
||
|
||
### Contributing a tempo map
|
||
|
||
1. Fork the community registry repo.
|
||
2. Create a `.ctp.json` file for your song. Use the schema above.
|
||
3. Validate your file: paste the JSON into a CTP validator (coming soon) or use the API:
|
||
```bash
|
||
curl -X POST http://localhost/api/tracks \
|
||
-H "Content-Type: application/json" \
|
||
-d @your-song.ctp.json
|
||
```
|
||
4. Open a pull request to the registry repo.
|
||
|
||
Once merged, any ClickTrack instance syncing that registry will import your map automatically.
|
||
|
||
---
|
||
|
||
## API Reference
|
||
|
||
### `GET /api/songs?q=<query>&limit=<n>`
|
||
|
||
Search for songs. Hits local DB first, falls back to MusicBrainz.
|
||
|
||
**Response:**
|
||
```json
|
||
{ "songs": [...], "total": 5 }
|
||
```
|
||
|
||
### `GET /api/tracks?mbid=<uuid>`
|
||
|
||
List all community tempo maps for a song.
|
||
|
||
### `POST /api/tracks`
|
||
|
||
Submit a new CTP document. Body must be a valid CTP JSON.
|
||
|
||
**Response:** `201 Created` with the stored map record.
|
||
|
||
### `GET /api/generate?id=<tempo-map-uuid>&count_in=true`
|
||
|
||
Generate and download a WAV click track.
|
||
|
||
- `id` — UUID of the tempo map (from `/api/tracks`)
|
||
- `count_in` — `true` or `false` (default: `true`)
|
||
|
||
Returns `audio/wav` with `Content-Disposition: attachment`.
|
||
|
||
---
|
||
|
||
## Architecture
|
||
|
||
```
|
||
Browser
|
||
└── Next.js App Router (app/)
|
||
├── (web)/page.tsx — song search
|
||
├── (web)/track/[id]/ — track page + player
|
||
└── api/
|
||
├── songs/ — search + MB integration
|
||
├── tracks/ — CTP CRUD
|
||
└── generate/ — WAV rendering
|
||
|
||
lib/
|
||
├── ctp/
|
||
│ ├── schema.ts — TypeScript types
|
||
│ ├── validate.ts — Zod validation
|
||
│ └── render.ts — CTP → WAV (Node.js)
|
||
├── db/client.ts — pg Pool + query helpers
|
||
├── musicbrainz/client.ts — rate-limited MB API
|
||
└── registry/sync.ts — Git registry pull + upsert
|
||
```
|
||
|
||
---
|
||
|
||
## Environment Variables
|
||
|
||
See [`.env.example`](.env.example) for all variables with descriptions.
|
||
|
||
| Variable | Required | Default | Description |
|
||
|---|---|---|---|
|
||
| `DATABASE_URL` | Yes | — | PostgreSQL connection string |
|
||
| `REDIS_URL` | Yes | — | Redis connection URL |
|
||
| `NEXT_PUBLIC_APP_NAME` | No | `ClickTrack` | UI display name |
|
||
| `REGISTRY_REPO` | No | — | GitHub repo URL for CTP registry |
|
||
| `REGISTRY_SYNC_INTERVAL` | No | `3600` | Sync interval in seconds |
|
||
| `MUSICBRAINZ_USER_AGENT` | No | built-in | User-Agent for MB API requests |
|
||
|
||
---
|
||
|
||
## License
|
||
|
||
MIT
|