import { expect, test } from "bun:test"; import { matchNotes, maxAssignment } from "../../src/eval/notes.ts"; import type { TimedNote } from "../../src/core/types.ts"; const n = (onset: number, pitch: number, string?: number): TimedNote => ({ onset, offset: onset + 0.2, pitch, string }); const ref = [n(1.0, 60), n(1.5, 62), n(2.0, 64)]; test("tp / octave errors", () => { const est = [n(1.03, 60), n(1.5, 74), n(2.2, 64)]; expect(matchNotes(ref, est).tp).toBe(1); expect(matchNotes(ref, est).octaveErrors).toBe(1); }); test("perfect and empty cases", () => { expect(matchNotes(ref, ref).f1).toBe(1); expect(matchNotes([], []).f1).toBeNull(); expect(matchNotes(ref, []).recall).toBe(0); expect(matchNotes(ref, []).precision).toBeNull(); expect(matchNotes([], ref).recall).toBeNull(); expect(matchNotes([], ref).f1).toBe(0); }); test("matching is maximum, not greedy", () => { // greedy nearest would pair r1-e1 (0.00) leaving r2 unmatched; optimum pairs r1-e2, r2-e1 const r = [n(1.0, 60), n(1.04, 60)]; const e = [n(1.0, 60), n(0.96, 60)]; expect(matchNotes(r, e, { onsetTol: 0.045 }).tp).toBe(2); const r2 = [n(0.0, 60), n(0.08, 60)]; const e2 = [n(0.04, 60), n(-0.01, 60)]; expect(matchNotes(r2, e2, { onsetTol: 0.05 }).tp).toBe(2); }); test("pitchString key", () => { expect(matchNotes([n(1, 60, 1)], [n(1, 60, 2)], { key: "pitchString" }).tp).toBe(0); expect(matchNotes([n(1, 60, 1)], [n(1, 60, 2)]).tp).toBe(1); }); test("maxAssignment", () => { expect(maxAssignment([[5, 4], [4, 0]])).toEqual([1, 0]); expect(maxAssignment([[3], [5]])).toEqual([-1, 0]); });