# Synthetic Benchmark Harness Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Generate original tier-1 songs as Guitar Pro files, render them to audio, run a baseline browser-portable transcription pipeline on the audio, and score the output against the source file. **Architecture:** One shared symbolic `Song` model is written to alphaTex and turned into Guitar Pro / MIDI / audio by alphaTab. The pipeline (Demucs-6s ONNX → Basic Pitch ONNX / heuristic drums → tempo grid → quantise → tab assignment) produces timed notes plus a `Song` that goes back out through the same alphaTex writer. A scorer compares timed notes to the ground truth extracted from the GP file. **Tech Stack:** Bun 1.3 + TypeScript (strict), `@coderline/alphatab@1.8.4` (MPL-2.0), `onnxruntime-node@1.31.1` (MIT), `@spotify/basic-pitch@1.0.1` post-processing only (Apache-2.0), ffmpeg CLI (mp3 encode only), models: `Kani95/htdemucs-6s-ort` (Apache-2.0, sha256 `35cb61d4d35173ff1d16df7393f41f05651ffdfe13d3c10e06abc26708bd6933`), Spotify `nmp.onnx` (Apache-2.0). **Spec:** `docs/superpowers/specs/2026-10-30-synthetic-benchmark-design.md` ## Global Constraints - Bun + TypeScript for everything; follow `CLAUDE.md` (bun test, `Bun.file`/`Bun.write`, `Bun.$`, `bunx`). No Python on any path in this plan. - Models run via `onnxruntime-node`; code touching ORT must only use APIs that also exist in `onnxruntime-web` (`InferenceSession.create`, `Tensor`, `session.run`). - CPU only. Use `executionProviders: ["cpu"]`. - Pipeline code (`src/pipeline/**`, `src/dsp/**`, `src/score/**`) must not import `node:fs`/`Bun.*`; file I/O lives in `src/bench/**`, `src/audio/wav.ts`, `src/audio/mp3.ts`. (Keeps the pipeline browser-portable.) - Generated material is original; no reference songs are recreated. - Tier-1 benchmark songs: at most two guitars, standard tuning `[64,59,55,50,45,40]` (MIDI, highest string first), 4/4. - Pitch score: onset tolerance ±50 ms, offsets ignored, string ignored; computed on timed notes **before** quantisation. - Song pass: merged-guitar pitch F1 ≥ 0.85. Tier pass: median song passes. - Rhythm alignment allows tempo factor ½×, 1×, 2× and whole-beat bar offsets; factor/offset reported separately. - Every result is labelled with its oracle flags (`oracleStems`, `oracleTempo`, `oracleNotes`). - Reports order instruments: guitar, vocals, then bass, drums, keys. - Generated audio, `data/`, `runs/`, `models/*.onnx` are git-ignored; `results/*.md` + `results/*.json` summaries are committed. - SoundFont: alphaTab's bundled `sonivox.sf2` (Apache-2.0). - Commits end with `Co-Authored-By: Claude Opus 5.5 `. ## Out of Scope for This Plan - The optional Python comparison track (spec: "optional") — deferred to a follow-up plan once baseline numbers exist. - Website UI, YouTube ingestion, later tiers. ## Review Focus 1. **Durations that aren't a single note value or that cross a barline** (e.g. 5 sixteenths, a whole note starting on beat 3) — writer must emit tied notes so the GP round trip preserves every onset and pitch. Test in Task 2. 2. **Silent or absent stems** (no vocals in t1a; a stem Demucs leaves near-silent) — transcribers return `[]`, scorer reports `null` metrics (not NaN) and the report shows "n/a". Tests in Tasks 6, 8. 3. **Audio length not a multiple of the Demucs segment, or shorter than one segment** — separation output length must equal input length exactly. Test in Task 7. 4. **Pitches outside the instrument's fret range** (octave errors from transcription) — tab assigner drops them, counts them, never throws. Test in Task 11. 5. **Synth onset latency** — rendered audio must put note energy within 20 ms of ground-truth onset, otherwise every score is silently shifted. Test in Task 4. --- ## File Structure ``` scripts/fetch-models.ts download + sha256-verify models into models/ src/core/types.ts Song, Track, ScoreNote, TimedNote, TruthTrack, Role, Grid, Audio src/core/time.ts tick/second conversion (TPQ = 960) src/core/rng.ts seeded PRNG src/score/alphatex.ts songToAlphaTex(song) src/score/gp.ts songToScore / songToGp / truthFromGp (alphaTab) src/gen/harmony.ts keys, progressions, guitar chord shapes src/gen/parts.ts per-role part generators src/gen/tiers.ts tier specs t1a..t1d src/gen/generate.ts generateSong(tier, seed) src/audio/wav.ts read/write WAV, toMono, resample src/audio/mp3.ts ffmpeg encode src/audio/render.ts per-track stem rendering via alphaSynth src/audio/fx.ts drive, chorus, reverb, pan, gain src/audio/mix.ts mix stems src/dsp/fft.ts radix-2 FFT, torch-compatible stft/istft src/pipeline/separate.ts Demucs 6s ONNX src/pipeline/transcribe.ts Basic Pitch ONNX → TimedNote[] src/pipeline/drums.ts heuristic kick/snare/hat transcriber src/pipeline/tempo.ts tempo + downbeat grid src/pipeline/quantize.ts TimedNote[] → ScoreNote[] src/pipeline/tab.ts string/fret assignment (Viterbi) src/pipeline/run.ts orchestrates pipeline incl. oracle modes src/eval/notes.ts note matching / F1 / octave errors src/eval/rhythm.ts grid alignment + rhythm score src/eval/score.ts scoreSong src/bench/dataset.ts build song dirs on disk src/bench/report.ts markdown + JSON tier report src/bench/cli.ts `bun run bench …` entry tests/** bun tests mirroring src ``` On-disk layout: `data///{song.json, truth.gp, stems/.wav, mix.wav, mix.mp3}`; `runs////{notes.json, out.gp, score.json}`; `results/.{md,json}`. --- ### Task 1: Scaffolding, core types, time, RNG, model fetch **Files:** - Create: `src/core/types.ts`, `src/core/time.ts`, `src/core/rng.ts`, `scripts/fetch-models.ts` - Modify: `package.json` (scripts), `.gitignore`, delete `index.ts` - Test: `tests/core/time.test.ts`, `tests/core/rng.test.ts` **Interfaces:** - Produces (`src/core/types.ts`): ```ts export type Role = "guitar" | "bass" | "keys" | "vocals" | "drums"; export type FxPreset = "none" | "clean" | "clean-chorus" | "light-drive"; export interface ScoreNote { tick: number; dur: number; pitch: number; string?: number; fret?: number } // string numbering: 1 = highest-pitched string (alphaTex convention). Drums: pitch = GM drum number. export interface Track { name: string; role: Role; program: number; tuning?: number[]; pan: number; fx: FxPreset; gainDb: number; notes: ScoreNote[] } export interface Song { title: string; tier: string; seed: number; bpm: number; timeSig: [number, number]; bars: number; key: string; tracks: Track[] } export interface TimedNote { onset: number; offset: number; pitch: number; velocity?: number; string?: number; fret?: number } export interface TruthTrack { name: string; role: Role; tuning?: number[]; notes: TimedNote[] } export interface Grid { bpm: number; firstDownbeat: number; beatsPerBar: number } export interface Audio { sampleRate: number; channels: Float32Array[] } export const TPQ = 960; export const GUITAR_STANDARD = [64, 59, 55, 50, 45, 40]; export const BASS_STANDARD = [43, 38, 33, 28]; ``` - Produces: `ticksToSeconds(tick: number, bpm: number): number`, `secondsToTicks(s: number, bpm: number): number` in `src/core/time.ts`; `createRng(seed: number): Rng` with `next(): number` (0..1), `int(lo: number, hi: number): number` (inclusive), `pick(a: readonly T[]): T`, `chance(p: number): boolean` in `src/core/rng.ts` (mulberry32). - [ ] **Step 1: Write failing tests** ```ts test("ticks/seconds", () => { expect(ticksToSeconds(960, 60)).toBeCloseTo(1); expect(ticksToSeconds(3840, 120)).toBeCloseTo(2); expect(secondsToTicks(0.5, 120)).toBeCloseTo(960); }); test("rng deterministic", () => { const a = createRng(42), b = createRng(42), c = createRng(43); const sa = [a.next(), a.next(), a.next()]; expect([b.next(), b.next(), b.next()]).toEqual(sa); expect(c.next()).not.toBe(sa[0]); const r = createRng(1); for (let i = 0; i < 1000; i++) { const v = r.int(3, 5); expect(v >= 3 && v <= 5).toBe(true); } }); ``` - [ ] **Step 2:** `bun test tests/core` → FAIL (modules missing). - [ ] **Step 3:** Implement types, time, rng. `package.json` scripts: `"test": "bun test"`, `"typecheck": "bunx tsc --noEmit"`, `"fetch-models": "bun scripts/fetch-models.ts"`, `"bench": "bun src/bench/cli.ts"`. `.gitignore` add `data/`, `runs/`, `models/**/*.onnx`. `scripts/fetch-models.ts` downloads `https://huggingface.co/Kani95/htdemucs-6s-ort/resolve/main/htdemucs_6s.onnx` → `models/demucs/htdemucs_6s.onnx` and `https://github.com/spotify/basic-pitch/raw/main/basic_pitch/saved_models/icassp_2022/nmp.onnx` → `models/basic-pitch/nmp.onnx`, skips existing files whose sha256 matches, fails loudly on mismatch (Demucs hash above; record the nmp hash from the current file on first run as a constant). - [ ] **Step 4:** `bun test tests/core` → PASS; `bun run typecheck` → no errors. - [ ] **Step 5:** Commit `feat: core types, time, rng, model fetch`. ### Task 2: alphaTex writer and Guitar Pro round trip **Files:** - Create: `src/score/alphatex.ts`, `src/score/gp.ts` - Test: `tests/score/roundtrip.test.ts` **Interfaces:** - Consumes: Task 1 types. - Produces: `songToAlphaTex(song: Song): string`; `songToScore(song: Song): alphaTab.model.Score`; `songToGp(song: Song): Uint8Array` (Gp7Exporter); `truthFromGp(bytes: Uint8Array): { bpm: number; timeSig: [number, number]; tracks: TruthTrack[] }`; `songToMidi(song: Song): alphaTab.midi.MidiFile`. Writer rules (the implementer cannot infer these): - Header `\title`, `\tempo `, `\ts `; per track `\track ""`, `\instrument ` (drums: `\instrument percussion \articulation defaults`, notes written as GM numbers), guitar/bass: `\staff {tabs}` + `\tuning (...)` written as note names highest string first, notes as `fret.string`; keys/vocals: `\staff {score}`, notes as note names (e.g. `C#4`). - Each track is one voice. Notes with equal `tick` form a chord beat; their beat duration is the shortest `dur` in the group, clipped to the next onset in the track. Gaps become rests. - Durations are decomposed greedily into {1, 2, 4, 8, 16, 32} with optional single dots, split at barlines, joined with ties (`-` tie syntax on the continuation note; verify alphaTex tie syntax against alphaTab 1.8 docs/diagnostics). - `truthFromGp` uses absolute tick = `masterBar.start + beat.playbackStart`, seconds via `ticksToSeconds` with the score tempo (tier 1 is constant tempo). Tied continuation notes are merged into the original note (one TimedNote). String converted to our numbering: `ours = stringCount - model.string + 1`. Drum pitch = `track.percussionArticulations[n.percussionArticulation].outputMidiNumber`. Role is stored as the alphaTex track short name (`\track "" ""`) and recovered from `track.shortName`. - [ ] **Step 1: Write failing tests** — build a hand-made `Song` (bpm 90) with: a guitar track containing a C-major chord at tick 0 (5 notes with string/fret), a single note of `dur = 5 * 240` (5 sixteenths), and a whole note starting at beat 3 of bar 1 (crosses barline); a vocals track with a rest gap; a drums track with kick 36 + hat 42 together and a snare 38. Assertions: ```ts const truth = truthFromGp(songToGp(song)); // every source note appears exactly once with same pitch and onset (±1 ms), ties merged: expect(sortNotes(truth.tracks[0].notes).map(n => [n.pitch, round3(n.onset)])).toEqual(expectedGuitar); expect(truth.tracks[0].notes.find(n => n.pitch === tiedPitch)!.offset - onset).toBeCloseTo(4 * 60/90, 3); expect(truth.tracks[0].notes.every(n => n.string! >= 1 && n.string! <= 6)).toBe(true); expect(truth.tracks.find(t => t.role === "drums")!.notes.map(n => n.pitch).sort()).toEqual([36, 38, 42]); expect(truth.tracks.map(t => t.role)).toEqual(["guitar", "vocals", "drums"]); ``` plus: alphaTex parsing emits no error diagnostics (capture `Logger` or use the importer's diagnostics API) for this song. - [ ] **Step 2:** `bun test tests/score` → FAIL. - [ ] **Step 3:** Implement writer + gp.ts. - [ ] **Step 4:** `bun test tests/score` → PASS. - [ ] **Step 5:** Commit `feat: alphaTex writer and GP round trip`. ### Task 3: Tier-1 song generator **Files:** - Create: `src/gen/harmony.ts`, `src/gen/parts.ts`, `src/gen/tiers.ts`, `src/gen/generate.ts` - Test: `tests/gen/generate.test.ts` **Interfaces:** - Consumes: types, rng, `songToGp`/`truthFromGp` (test only). - Produces: `type TierId = "t1a" | "t1b" | "t1c" | "t1d"`; `TIERS: Record`; `generateSong(tier: TierId, seed: number): Song`. Tier content (decided): | Tier | Tracks | Notes | |---|---|---| | t1a | 1 clean guitar | solo; 66–96 bpm; mix of strummed open chords and arpeggios; one short single-note melodic phrase allowed | | t1b | guitar + bass + drums | bass on chord roots (quarters/halves); drums: kick/snare/closed-hat groove, crash on section starts | | t1c | t1b + keys (pad/piano chords) + vocals (program 53 "Voice Oohs", monophonic melody in C4–G5) | keys voiced in C3–C5 | | t1d | t1c + second guitar | guitars get distinct roles (strum vs arpeggio/lead line), pans −0.6 / +0.6, one `clean`, one `clean-chorus` or `light-drive` | Common: 4/4, 16 bars split into sections (e.g. A A B A, 4 bars each, repeats identical), keys from {C, G, D, A, E, F} major + relative minors, progressions from a fixed list of pop progressions (I–V–vi–IV, vi–IV–I–V, I–vi–IV–V, I–IV–vi–V). Guitar chords come from an open-chord shape dictionary in `harmony.ts` (each shape = per-string fret or mute, highest string first) so every guitar note carries a valid `string`/`fret`; arpeggios draw from the same shape. Guitar programs: 25 (steel) or 27 (clean electric). Bass program 33, keys 0 or 89, drums percussion. - [ ] **Step 1: Write failing tests** ```ts for (const tier of ["t1a","t1b","t1c","t1d"] as const) for (const seed of [1,2,3]) { const s = generateSong(tier, seed); expect(generateSong(tier, seed)).toEqual(s); // deterministic const guitars = s.tracks.filter(t => t.role === "guitar"); expect(guitars.length).toBe(tier === "t1d" ? 2 : 1); for (const g of guitars) for (const n of g.notes) expect(GUITAR_STANDARD[n.string! - 1]! + n.fret!).toBe(n.pitch); expect(s.tracks.map(t => t.role).sort()).toEqual(EXPECTED_ROLES[tier]); const end = s.bars * 4 * TPQ; for (const t of s.tracks) for (const n of t.notes) expect(n.tick + n.dur).toBeLessThanOrEqual(end); expect(truthFromGp(songToGp(s)).tracks.length).toBe(s.tracks.length); // valid GP } const d = generateSong("t1d", 7); const [g1, g2] = d.tracks.filter(t => t.role === "guitar"); expect(Math.sign(g1!.pan)).not.toBe(Math.sign(g2!.pan)); expect(generateSong("t1a", 1)).not.toEqual(generateSong("t1a", 2)); ``` - [ ] **Step 2:** `bun test tests/gen` → FAIL. - [ ] **Step 3:** Implement. - [ ] **Step 4:** `bun test tests/gen` → PASS. - [ ] **Step 5:** Commit `feat: tier-1 song generator`. ### Task 4: Audio I/O, stem rendering, effects, mixing **Files:** - Create: `src/audio/wav.ts`, `src/audio/mp3.ts`, `src/audio/render.ts`, `src/audio/fx.ts`, `src/audio/mix.ts` - Test: `tests/audio/audio.test.ts` **Interfaces:** - Consumes: `songToMidi`, `songToScore`, types. - Produces: `readWav(path: string): Promise