import { test, expect } from "bun:test"; import { fitQuality, fitSummary, FLAG_MIN_PRECISION, FLAG_MIN_LIFT, MIN_REC_ONSETS, SHIFT_MARGIN, type FitReport, type FitWindow } from "../../src/align/fit.ts"; const grid = (from: number, to: number, step: number) => { const o: number[] = []; for (let t = from; t < to - 1e-9; t += step) o.push(Math.round(t * 1000) / 1000); return o; }; test("constants match ruling A7", () => { expect(FLAG_MIN_PRECISION).toBe(0.6); expect(FLAG_MIN_LIFT).toBe(0.25); expect(MIN_REC_ONSETS).toBe(8); expect(SHIFT_MARGIN).toBe(0.05); }); test("precision = share of recording onsets with a MIDI onset within ±70 ms; audibleRate = the reverse (info)", () => { // recording hears every 2nd MIDI onset (dense score, sparse attacks) plus 2 extra attacks out of the score const midi = grid(0.5, 20.5, 0.25); // 80 onsets const rec = [...midi.filter((_, k) => k % 2 === 0), 3.13, 7.63]; // 40 + 2 const r = fitQuality(rec, midi, 20.5); expect(r.precision).toBeCloseTo(40 / 42, 6); expect(r.audibleRate).toBeCloseTo(0.5, 6); expect(r.medianResidualMs).toBe(0); expect(r.windows).toHaveLength(1); expect(r.windows[0]!.recOnsets).toBe(42); // low audibleRate alone does not flag (that was the old, miscalibrated rule) expect(r.windows[0]!.flagged).toBe(false); }); test("lift: precision minus mean precision at off-grid shifts (+0.11, +0.23, +0.37 s)", () => { const midi = grid(1, 29, 1); // one onset per second: shifts of 0.11–0.37 s never land const r = fitQuality(midi, midi, 30); expect(r.precision).toBe(1); expect(r.lift).toBeCloseTo(1, 6); expect(r.windows[0]!.lift).toBeCloseTo(1, 6); // a dense MIDI grid (every 0.12 s) matches anything: precision 1 but no lift -> flagged const dense = grid(0, 30, 0.12); const rec = grid(0.5, 29.5, 0.5).map((t) => t + 0.031); const d = fitQuality(rec, dense, 30); expect(d.precision).toBe(1); expect(d.lift!).toBeLessThan(FLAG_MIN_LIFT); expect(d.windows[0]!.flagged).toBe(true); expect(d.windows[0]!.reasons.join(" ")).toContain("lift"); }); test("window flagged when precision < 0.6; null and unflagged with < 8 recording onsets", () => { const midi = grid(1, 89, 1); const rec = [ ...grid(1, 11, 1), // window 0: 10 onsets, all on the score ...grid(31.5, 41.5, 1), // window 1: 10 onsets, all half-way between score onsets 61, 62, 63, // window 2: only 3 onsets ]; const r = fitQuality(rec, midi, 90); expect(r.windows.map((w) => w.startSec)).toEqual([0, 30, 60]); expect(r.windows[0]!.precision).toBe(1); expect(r.windows[0]!.flagged).toBe(false); expect(r.windows[1]!.precision).toBe(0); expect(r.windows[1]!.flagged).toBe(true); expect(r.windows[1]!.reasons[0]).toContain("precision"); const w2 = r.windows[2]!; expect(w2.recOnsets).toBe(3); expect(w2.precision).toBeNull(); expect(w2.lift).toBeNull(); expect(w2.flagged).toBe(false); }); test("no MIDI onsets: overall nulls, nothing flagged", () => { const r = fitQuality([1, 2], [], 10); expect(r.precision).toBeNull(); expect(r.audibleRate).toBeNull(); expect(r.lift).toBeNull(); expect(r.windows.every((w) => !w.flagged)).toBe(true); }); test("residual: median |recording − nearest MIDI| over matched recording onsets", () => { const midi = grid(1, 11, 1); const r = fitQuality(midi.map((t, k) => t + (k % 2 ? 0.02 : 0.04)), midi, 12); expect(r.medianResidualMs!).toBeCloseTo(30, 6); }); test("beat-shift check: flags a window where a shifted symbolic chroma beats the current one by the margin", () => { const rate = 10, n = 600; // 60 s, two windows const pc = (k: number) => { const c = new Float32Array(12); c[k % 12] = 1; return c; }; // chord changes every second (one "beat"); recording = sequence s(t); current MIDI = s shifted by one beat in window 1 const seq = (t: number) => pc(Math.floor(t / rate) * 5); const recording = Array.from({ length: n }, (_, t) => seq(t)); const current = Array.from({ length: n }, (_, t) => (t < 300 ? seq(t) : seq(t - rate))); const plus1 = Array.from({ length: n }, (_, t) => seq(t - rate)), minus1 = Array.from({ length: n }, (_, t) => (t < 300 ? seq(t - rate) : seq(t))); const onsets = grid(0.5, 59.5, 0.5); const r = fitQuality(onsets, onsets, 60, { shift: { rate, recording, current, shifts: [{ label: "+1 beat", chroma: plus1 }, { label: "−1 beat", chroma: minus1 }] } }); expect(r.windows[0]!.beatShift).toBeNull(); expect(r.windows[0]!.flagged).toBe(false); expect(r.windows[1]!.beatShift?.label).toBe("−1 beat"); expect(r.windows[1]!.beatShift!.gain).toBeGreaterThan(SHIFT_MARGIN); expect(r.windows[1]!.flagged).toBe(true); expect(r.windows[1]!.reasons).toContain("beat shift suspected: −1 beat"); }); test("fitSummary: precision, lift, audible rate, residual, flagged windows with reasons, stretch warnings", () => { const w = (startSec: number, reasons: string[]): FitWindow => ({ startSec, recOnsets: 20, precision: 0.9, lift: 0.7, audibleRate: 0.4, medianResidualMs: 10, beatShift: null, flagged: reasons.length > 0, reasons }); const f: FitReport = { precision: 0.8234, lift: 0.6512, audibleRate: 0.3811, medianResidualMs: 14.24, windows: [w(0, []), w(30, ["beat shift suspected: +1 beat"]), w(60, []), w(90, ["precision 41%"])], stretches: { total: 40, warned: 3, over15: 1 }, }; expect(fitSummary(f)).toBe("precision 82.3% (lift +0.65) · MIDI onsets audible as attacks 38.1% · median residual 14.2 ms · 2/4 windows flagged (0:30 beat shift suspected: +1 beat; 1:30 precision 41%) · stretches: 3/40 warned, 1 > 15%"); expect(fitSummary({ ...f, windows: [w(0, [])], stretches: undefined })).toBe("precision 82.3% (lift +0.65) · MIDI onsets audible as attacks 38.1% · median residual 14.2 ms · 0/1 windows flagged"); expect(fitSummary({ precision: null, lift: null, audibleRate: null, medianResidualMs: null, windows: [] })).toBe("no scored MIDI onsets"); });