import { test, expect } from "bun:test"; import { quantize, snapTicks } from "../../src/pipeline/quantize.ts"; import type { TimedNote } from "../../src/core/types.ts"; const t = (onset: number, offset: number, pitch: number): TimedNote => ({ onset, offset, pitch }); const g = { bpm: 120, firstDownbeat: 0.1, beatsPerBar: 4 }; test("snaps onsets and durations to the 16th grid relative to the first downbeat", () => { const q = quantize([t(0.1 + 0.5 + 0.02, 0.1 + 1.0, 60), t(0.1 + 0.75 - 0.015, 0.1 + 1.0, 64)], g); expect(q.map((n) => [n.tick, n.dur, n.pitch])).toEqual([[960, 960, 60], [1440, 480, 64]]); }); test("drops notes before the first downbeat", () => { const q = quantize([t(0.0, 0.3, 60), t(0.1, 0.4, 62)], g); expect(q.map((n) => n.pitch)).toEqual([62]); }); test("merges chord notes within 35 ms, with one consistent duration", () => { const q = quantize([t(0.6, 1.6, 60), t(0.625, 1.1, 64), t(0.63, 1.6, 67)], g); expect(new Set(q.map((n) => n.tick)).size).toBe(1); expect(new Set(q.map((n) => n.dur)).size).toBe(1); expect(q).toHaveLength(3); }); test("collapses exact duplicates, enforces min duration", () => { const q = quantize([t(0.1, 0.101, 60), t(0.1, 0.101, 60), t(0.225, 3, 62)], g); expect(q.map((n) => [n.tick, n.pitch])).toEqual([[0, 60], [240, 62]]); expect(q[0]!.dur).toBe(240); for (const n of q) { expect(n.tick % 240).toBe(0); expect(n.dur % 240).toBe(0); expect(n.dur).toBeGreaterThanOrEqual(240); } }); test("snapTicks matches quantize ticks, incl. chord clusters and adjacent repeated pitches", () => { // two same-pitch notes 20 ms apart merge; the next repeat is on the adjacent 16th (125 ms at 120 bpm) const notes = [t(0.1, 0.2, 60), t(0.12, 0.2, 60), t(0.225, 0.3, 60)]; expect(snapTicks(notes, g)).toEqual([0, 0, 240]); expect(quantize(notes, g).map((n) => n.tick)).toEqual([0, 240]); });