import { test, expect } from "bun:test"; import { features, pickOnsets } from "../../src/align/features.ts"; import type { Audio } from "../../src/core/types.ts"; const SR = 44100; function tone(freqs: number[], sec: number): Audio { const x = new Float32Array(Math.round(sec * SR)); for (let i = 0; i < x.length; i++) for (const f of freqs) x[i]! += 0.3 * Math.sin((2 * Math.PI * f * i) / SR); return { sampleRate: SR, channels: [x] }; } test("features rate and length", () => { const f = features(tone([440], 3), 10); expect(f.rate).toBe(10); expect(Math.abs(f.chroma.length - 31)).toBeLessThanOrEqual(1); expect(f.onset.length).toBe(f.chroma.length); expect(f.chroma[0]!.length).toBe(12); const g = features(tone([440], 3), 50); expect(Math.abs(g.chroma.length - 151)).toBeLessThanOrEqual(1); }); test("chroma peaks at the right pitch class and is unit-norm", () => { const f = features(tone([440], 2), 10); // A -> pitch class 9 const c = f.chroma[10]!; let best = 0; for (let k = 1; k < 12; k++) if (c[k]! > c[best]!) best = k; expect(best).toBe(9); expect(Math.hypot(...c)).toBeCloseTo(1, 4); const fe = features(tone([261.63, 329.63, 392], 2), 10).chroma[10]!; // C E G const top = [...fe].map((v, k) => [v, k] as const).sort((a, b) => b[0] - a[0]).slice(0, 3).map((e) => e[1]).sort((a, b) => a - b); expect(top).toEqual([0, 4, 7]); }); test("silent frames stay zero", () => { const f = features({ sampleRate: SR, channels: [new Float32Array(SR * 2)] }, 10); for (const c of f.chroma) expect(c.every((v) => v === 0)).toBe(true); }); test("pickOnsets finds a click train within 10 ms", () => { const x = new Float32Array(SR * 4); const times = [0.5, 1.2, 2.0, 2.7, 3.3]; for (const t of times) { const s = Math.round(t * SR); for (let k = 0; k < 200; k++) x[s + k] = Math.sin(k * 0.8) * Math.exp(-k / 40) * 0.8; } const f = features({ sampleRate: SR, channels: [x] }, 50); const got = pickOnsets(f.onset, f.rate); expect(got.length).toBe(times.length); times.forEach((t, k) => expect(Math.abs(got[k]! - t)).toBeLessThanOrEqual(0.01)); }); test("pickOnsets enforces 50 ms min gap and ignores flat input", () => { const o = new Float32Array(100); o[20] = 5; o[22] = 6; o[60] = 5; expect(pickOnsets(o, 100)).toEqual([0.22, 0.6]); expect(pickOnsets(new Float32Array(100), 100)).toEqual([]); });