import { describe, expect, it } from "@suaveplan/testing/runner"; import { type FrameChord, matchChord, mergeRuns, smoothLabels, } from "./chord-matching.js"; function chromaFor(pcs: number[]): Float32Array { const v = new Float32Array(12); for (const pc of pcs) v[pc] = 1; return v; } describe("matchChord", () => { it("matches a C major triad to C major", { timeout: 5000 }, () => { const r = matchChord(chromaFor([0, 4, 7])); expect(r.root).toBe("C"); expect(r.quality).toBe("major"); expect(r.confidence).toBeGreaterThan(0); }); it("matches an A minor triad to A minor", { timeout: 5000 }, () => { const r = matchChord(chromaFor([9, 0, 4])); expect(r.root).toBe("A"); expect(r.quality).toBe("minor"); }); it("matches a diminished triad", { timeout: 5000 }, () => { const r = matchChord(chromaFor([0, 3, 6])); expect(r.quality).toBe("dim"); }); it("matches an augmented triad", { timeout: 5000 }, () => { const r = matchChord(chromaFor([0, 4, 8])); expect(r.quality).toBe("aug"); }); it("zero chroma returns the default with zero confidence", { timeout: 5000, }, () => { const r = matchChord(new Float32Array(12)); expect(r.confidence).toBe(0); }); }); describe("smoothLabels", () => { it("collapses a single odd label between two identical neighbours", { timeout: 5000, }, () => { const c: FrameChord[] = [ { root: "C", quality: "major", confidence: 0.9 }, { root: "G", quality: "major", confidence: 0.4 }, { root: "C", quality: "major", confidence: 0.9 }, ]; const s = smoothLabels(c); expect(s[1]?.root).toBe("C"); }); it("leaves stable runs unchanged", { timeout: 5000 }, () => { const c: FrameChord[] = [ { root: "C", quality: "major", confidence: 0.9 }, { root: "C", quality: "major", confidence: 0.9 }, ]; expect(smoothLabels(c)).toEqual(c); }); }); describe("mergeRuns", () => { it("merges adjacent identical labels into single chord records", { timeout: 5000, }, () => { const labels: FrameChord[] = [ { root: "C", quality: "major", confidence: 0.9 }, { root: "C", quality: "major", confidence: 0.95 }, { root: "G", quality: "major", confidence: 0.8 }, { root: "G", quality: "major", confidence: 0.85 }, ]; const merged = mergeRuns(labels, { hopSeconds: 0.1, minDuration: 0 }); expect(merged.length).toBe(2); expect(merged[0]?.root).toBe("C"); expect(merged[1]?.root).toBe("G"); expect(merged[0]?.timestamp).toBeLessThan(merged[1]?.timestamp ?? 0); }); it("drops runs shorter than minDuration", { timeout: 5000 }, () => { const labels: FrameChord[] = [ { root: "C", quality: "major", confidence: 0.9 }, { root: "G", quality: "major", confidence: 0.4 }, { root: "G", quality: "major", confidence: 0.4 }, { root: "C", quality: "major", confidence: 0.9 }, ]; // Smoothing keeps middle G run (length 2), C bookends not — but // minDuration=0.3 with hopSeconds=0.1 drops the single-frame C bookends. const merged = mergeRuns(labels, { hopSeconds: 0.1, minDuration: 0.3 }); // Each pure run is too short — all dropped or none survive. expect(merged.every((c) => c.duration >= 0.3)).toBe(true); }); it("returns empty array for empty labels", { timeout: 5000 }, () => { expect(mergeRuns([], { hopSeconds: 0.1, minDuration: 0 })).toEqual([]); }); it("emits a time-ordered smoothed chord progression", { timeout: 5000, }, () => { const labels: FrameChord[] = [ { root: "C", quality: "major", confidence: 0.9 }, { root: "C", quality: "major", confidence: 0.9 }, { root: "C", quality: "major", confidence: 0.9 }, { root: "G", quality: "major", confidence: 0.9 }, { root: "G", quality: "major", confidence: 0.9 }, { root: "G", quality: "major", confidence: 0.9 }, ]; const merged = mergeRuns(labels, { hopSeconds: 0.1, minDuration: 0 }); expect(merged.map((c) => c.root)).toEqual(["C", "G"]); for (let i = 1; i < merged.length; i++) { const prev = merged[i - 1]; const cur = merged[i]; expect((cur?.timestamp ?? 0) > (prev?.timestamp ?? 0)).toBe(true); } }); it("coalesces non-adjacent identical labels merged by smoothing", { timeout: 5000, }, () => { const labels: FrameChord[] = [ { root: "C", quality: "major", confidence: 0.5 }, { root: "C", quality: "major", confidence: 0.5 }, { root: "G", quality: "major", confidence: 0.4 }, { root: "C", quality: "major", confidence: 0.5 }, { root: "C", quality: "major", confidence: 0.5 }, ]; const merged = mergeRuns(labels, { hopSeconds: 0.1, minDuration: 0 }); // smoothing converts the lone G to a C — should coalesce to a single C run. expect(merged.length).toBe(1); expect(merged[0]?.root).toBe("C"); }); });