import { test, expect, beforeAll } from "bun:test"; import { alignmentFit, autoAlign, bandedWithRetry, barTimes } from "../../src/align/auto.ts"; import { makeWarp, type Anchor, type Warp } from "../../src/align/warp.ts"; import type { RefTrack, Smf } from "../../src/align/smf.ts"; import type { Audio, Role, TruthTrack } from "../../src/core/types.ts"; import { buildSong, loadSongDir } from "../../src/bench/dataset.ts"; const SF2 = new Uint8Array(await Bun.file("node_modules/@coderline/alphatab/dist/soundfont/sonivox.sf2").arrayBuffer()); const BIG = 60_000; let song: Awaited>; beforeAll(async () => { song = await loadSongDir(await buildSong("t1c", 1)); }, BIG); /** In-memory Smf from truth tracks: `map` turns truth (= recording) time into MIDI time; `reps` tiles the song. */ function smfFrom(truth: TruthTrack[], programs: number[], bpm: number, map: (t: number) => number, reps = 1, period = 0): Smf { const tracks: RefTrack[] = truth.map((t, i) => ({ index: i + 1, name: t.name, program: programs[i]!, isDrums: t.role === "drums", notes: Array.from({ length: reps }, (_, r) => t.notes.map((n) => ({ onset: map(n.onset + r * period), offset: map(n.offset + r * period), pitch: n.pitch, velocity: n.velocity ?? 96, channel: t.role === "drums" ? 9 : i, }))).flat(), })); const durationSec = Math.max(...tracks.flatMap((t) => t.notes.map((n) => n.offset))); return { durationSec, tracks, tempoMap: [{ sec: 0, bpm }], timeSigs: [{ sec: 0, num: 4, den: 4 }] }; } const rolesOf = (truth: TruthTrack[]): Record => Object.fromEntries(truth.map((t) => [t.name, t.role])); const durOf = (a: Audio) => a.channels[0]!.length / a.sampleRate; const median = (xs: number[]) => { const s = [...xs].sort((a, b) => a - b); return s[s.length >> 1]!; }; const p90 = (xs: number[]) => { const s = [...xs].sort((a, b) => a - b); return s[Math.floor(s.length * 0.9)]!; }; const debug = (msg: string) => { if (process.env.ALIGN_DEBUG) console.log(msg); }; const strictlyMonotone = (A: Anchor[]) => A.every((a, k) => k === 0 || (a.midiSec > A[k - 1]!.midiSec && a.audioSec > A[k - 1]!.audioSec)); /** Known distortion recording time -> MIDI time: +0.4 s, then bars 4–10 at ×1.06 tempo, then ≈ −0.8 s (continuous). */ function distortion(bpm: number): Warp { const bar = (4 * 60) / bpm, b1 = 4 * bar, b2 = 10 * bar; const m2 = b1 + 0.4 + (b2 - b1) / 1.06; return makeWarp([ { midiSec: 0, audioSec: 0.4, kind: "manual" }, { midiSec: b1, audioSec: b1 + 0.4, kind: "manual" }, { midiSec: b2, audioSec: m2, kind: "manual" }, { midiSec: b2 + 100, audioSec: m2 + 100, kind: "manual" }, ]); // toAudio: recording -> MIDI, toMidi: MIDI -> recording (the truth the aligner must recover) } function recoveryErrors(smf: Smf, got: Anchor[], trueMidiToAudio: (m: number) => number, audioDur: number): number[] { const w = makeWarp(got); const onsets = [...new Set(smf.tracks.flatMap((t) => t.notes.map((n) => n.onset)))]; return onsets.filter((m) => { const a = trueMidiToAudio(m); return a >= 0 && a <= audioDur; }) .map((m) => Math.abs(w.toAudio(m) - trueMidiToAudio(m))); } test("recovers a known piecewise warp: median error < 40 ms, fit match rate > 0.7", () => { const D = distortion(song.song.bpm); const smf = smfFrom(song.truth.tracks, song.song.tracks.map((t) => t.program), song.song.bpm, D.toAudio); const t0 = performance.now(); const { anchors, fit } = autoAlign(song.mix, smf, rolesOf(song.truth.tracks), [], SF2); const ms = performance.now() - t0; const err = recoveryErrors(smf, anchors, D.toMidi, durOf(song.mix)); debug(`synthetic warp: ${anchors.length} anchors, median err ${(median(err) * 1000).toFixed(1)} ms, p90 ${(p90(err) * 1000).toFixed(1)} ms, fit ${fit.matchRate?.toFixed(3)} / ${fit.medianResidualMs?.toFixed(1)} ms, ${ms.toFixed(0)} ms`); expect(anchors.every((a) => a.kind === "auto")).toBe(true); expect(median(err)).toBeLessThan(0.04); expect(p90(err)).toBeLessThan(0.08); expect(fit.matchRate!).toBeGreaterThan(0.7); expect(fit.windows.length).toBe(2); expect(anchors.every((a) => a.audioSec > 0 || a.midiSec === 0)).toBe(true); }, BIG); test("a correct manual anchor mid-song is honoured exactly", () => { const D = distortion(song.song.bpm); const smf = smfFrom(song.truth.tracks, song.song.tracks.map((t) => t.program), song.song.bpm, D.toAudio); const manual: Anchor = { midiSec: D.toAudio(30), audioSec: 30, kind: "manual" }; const { anchors } = autoAlign(song.mix, smf, rolesOf(song.truth.tracks), [manual], SF2); expect(anchors.filter((a) => a.kind === "manual")).toEqual([manual]); expect(makeWarp(anchors).toAudio(manual.midiSec)).toBe(30); // no auto anchor within 1 s (MIDI time) of the manual one expect(anchors.some((a) => a.kind === "auto" && Math.abs(a.midiSec - manual.midiSec) < 1)).toBe(false); expect(anchors.length).toBeGreaterThan(5); const err = recoveryErrors(smf, anchors, D.toMidi, durOf(song.mix)); expect(median(err)).toBeLessThan(0.04); }, BIG); test("a silent intro in the recording does not drag the path", () => { const pad = 4 * song.mix.sampleRate; const mix: Audio = { sampleRate: song.mix.sampleRate, channels: song.mix.channels.map((c) => { const o = new Float32Array(c.length + pad); o.set(c, pad); return o; }) }; const smf = smfFrom(song.truth.tracks, song.song.tracks.map((t) => t.program), song.song.bpm, (t) => t); const { anchors, fit } = autoAlign(mix, smf, rolesOf(song.truth.tracks), [], SF2); const err = recoveryErrors(smf, anchors, (m) => m + 4, durOf(mix)); expect(median(err)).toBeLessThan(0.04); expect(fit.matchRate!).toBeGreaterThan(0.7); }, BIG); test("5-minute input aligns in < 15 s", () => { const reps = 5, period = durOf(song.mix); const mix: Audio = { sampleRate: song.mix.sampleRate, channels: song.mix.channels.map((c) => { const o = new Float32Array(c.length * reps); for (let r = 0; r < reps; r++) o.set(c, r * c.length); return o; }) }; const smf = smfFrom(song.truth.tracks, song.song.tracks.map((t) => t.program), song.song.bpm, (t) => t + 0.3, reps, period); const t0 = performance.now(); const { anchors, fit } = autoAlign(mix, smf, rolesOf(song.truth.tracks), [], SF2); const sec = (performance.now() - t0) / 1000; debug(`5-min (${durOf(mix).toFixed(0)} s) input: ${sec.toFixed(2)} s, ${anchors.length} anchors, fit ${fit.matchRate?.toFixed(3)}`); expect(durOf(mix)).toBeGreaterThan(290); expect(sec).toBeLessThan(15); expect(fit.windows.length).toBe(10); }, BIG); test("barTimes follows tempo and time-signature changes", () => { const smf: Smf = { durationSec: 10, tracks: [], tempoMap: [{ sec: 0, bpm: 120 }, { sec: 4, bpm: 60 }], timeSigs: [{ sec: 0, num: 4, den: 4 }, { sec: 4, num: 3, den: 4 }], }; // 4/4 @120: bars of 2 s at 0, 2; then 3/4 @60: bars of 3 s at 4, 7 expect(barTimes(smf)).toEqual([0, 2, 4, 7]); expect(barTimes({ durationSec: 3, tracks: [], tempoMap: [{ sec: 0, bpm: 120 }], timeSigs: [] })).toEqual([0, 2]); // default 4/4 expect(barTimes({ durationSec: 4, tracks: [], tempoMap: [{ sec: 0, bpm: 120 }], timeSigs: [{ sec: 0, num: 6, den: 8 }] })).toEqual([0, 1.5, 3]); }); test("fine pass widens the band when the end cell is unreachable, then gives up with null", () => { const cost = (i: number, j: number) => Math.abs(i - j); // centre jumps by 120 frames mid-way: ±25 and ±50 leave a gap between rows, ±100 bridges it const jump = (i: number) => (i < 200 ? i : i + 120); const p = bandedWithRetry(cost, 400, 600, jump); expect(p).not.toBeNull(); expect(p![p!.length - 1]).toEqual([399, 599]); // a 400-frame jump defeats even ±2 s (±100 frames) expect(bandedWithRetry(cost, 400, 1000, (i) => (i < 200 ? i : i + 400))).toBeNull(); }); test("fit exposes a bad alignment instead of hiding it", () => { const D = distortion(song.song.bpm); const smf = smfFrom(song.truth.tracks, song.song.tracks.map((t) => t.program), song.song.bpm, D.toAudio); const roles = rolesOf(song.truth.tracks); const bad = alignmentFit(song.mix, smf, roles, []); // identity: off by 0.4–0.8 s throughout expect(bad.matchRate!).toBeLessThan(0.5); // the −0.8 s section is exposed; the +0.4 s section is not, as 0.4 s is ≈ 40 ms from two 16ths at 68 bpm (grid alias) expect(bad.windows[1]!.flagged).toBe(true); const good = alignmentFit(song.mix, smf, roles, autoAlign(song.mix, smf, roles, [], SF2).anchors); expect(good.windows.some((w) => w.flagged)).toBe(false); // unmapped tracks are excluded: with no scored roles there is nothing to measure expect(alignmentFit(song.mix, smf, Object.fromEntries(Object.keys(roles).map((k) => [k, null])), []).matchRate).toBeNull(); }, BIG); test("a deliberately wrong manual anchor (+0.3 s) is kept exactly and the result stays monotonic", () => { const D = distortion(song.song.bpm); const smf = smfFrom(song.truth.tracks, song.song.tracks.map((t) => t.program), song.song.bpm, D.toAudio); const manual: Anchor = { midiSec: D.toAudio(30), audioSec: 30.3, kind: "manual" }; const { anchors } = autoAlign(song.mix, smf, rolesOf(song.truth.tracks), [manual], SF2); expect(anchors.filter((a) => a.kind === "manual")).toEqual([manual]); expect(makeWarp(anchors).toAudio(manual.midiSec)).toBe(30.3); expect(strictlyMonotone(anchors)).toBe(true); expect(anchors.every((a) => a.audioSec > 0 || a.midiSec === 0)).toBe(true); // away from the bad anchor (> 4 s), the autos still follow the truth const far = anchors.filter((a) => a.kind === "auto" && Math.abs(a.audioSec - 30) > 4); expect(far.length).toBeGreaterThan(5); expect(median(far.map((a) => Math.abs(a.audioSec - D.toMidi(a.midiSec))))).toBeLessThan(0.04); }, BIG); test("when every fine band fails, the coarse-path fallback still yields auto anchors", () => { const D = distortion(song.song.bpm); const smf = smfFrom(song.truth.tracks, song.song.tracks.map((t) => t.program), song.song.bpm, D.toAudio); const manual: Anchor = { midiSec: D.toAudio(30), audioSec: 30, kind: "manual" }; const { anchors, fit } = autoAlign(song.mix, smf, rolesOf(song.truth.tracks), [manual], SF2, { fineBandsSec: [] }); const autos = anchors.filter((a) => a.kind === "auto"); // both segments (before and after the manual anchor) get anchors expect(autos.filter((a) => a.audioSec < 30).length).toBeGreaterThan(3); expect(autos.filter((a) => a.audioSec > 30).length).toBeGreaterThan(3); expect(strictlyMonotone(anchors)).toBe(true); const err = recoveryErrors(smf, anchors, D.toMidi, durOf(song.mix)); debug(`coarse fallback: ${autos.length} autos, median err ${(median(err) * 1000).toFixed(1)} ms, p90 ${(p90(err) * 1000).toFixed(1)} ms, fit ${fit.matchRate?.toFixed(3)}`); expect(median(err)).toBeLessThan(0.06); // 10 Hz resolution: coarser than the fine pass, still hand-correctable expect(p90(err)).toBeLessThan(0.12); }, BIG);