import { describe, test, expect } from "bun:test"; import { TARGET_SAMPLE_RATE, downmixToMono, resampleLinear, } from "../src/lib/audio"; // Pure helpers only — no browser APIs, no network, no model weights. describe("downmixToMono", () => { test("throws on zero channels", () => { expect(() => downmixToMono([])).toThrow(); }); test("single channel is copied, not aliased", () => { const channel = Float32Array.from([0.1, -0.5, 1]); const mono = downmixToMono([channel]); expect(Array.from(mono)).toEqual(Array.from(channel)); mono[0] = 42; expect(channel[0]).toBeCloseTo(0.1); }); test("averages stereo channels", () => { const left = Float32Array.from([1, 0, -1, 0.5]); const right = Float32Array.from([0, 0, 1, 0.5]); const mono = downmixToMono([left, right]); expect(Array.from(mono)).toEqual([0.5, 0, 0, 0.5]); }); test("truncates to the shortest channel", () => { const a = Float32Array.from([1, 1, 1]); const b = Float32Array.from([1, 1]); expect(downmixToMono([a, b]).length).toBe(2); }); }); describe("resampleLinear", () => { test("rejects non-positive rates", () => { const input = new Float32Array(4); expect(() => resampleLinear(input, 0, 16_000)).toThrow(); expect(() => resampleLinear(input, 44_100, -1)).toThrow(); }); test("identity when rates match (copy, not alias)", () => { const input = Float32Array.from([0.25, -0.25]); const out = resampleLinear(input, 16_000, 16_000); expect(Array.from(out)).toEqual([0.25, -0.25]); out[0] = 9; expect(input[0]).toBeCloseTo(0.25); }); test("empty input stays empty", () => { expect(resampleLinear(new Float32Array(0), 44_100, 16_000).length).toBe(0); }); test("output length scales with the rate ratio", () => { const oneSecondAt48k = new Float32Array(48_000); const out = resampleLinear(oneSecondAt48k, 48_000, TARGET_SAMPLE_RATE); expect(out.length).toBe(TARGET_SAMPLE_RATE); }); test("preserves a constant (DC) signal", () => { const input = new Float32Array(1000).fill(0.7); const out = resampleLinear(input, 44_100, 16_000); for (const sample of out) { expect(sample).toBeCloseTo(0.7, 5); } }); test("downsampling a linear ramp stays a linear ramp", () => { const n = 800; const ramp = new Float32Array(n); for (let i = 0; i < n; i++) ramp[i] = i / (n - 1); const out = resampleLinear(ramp, 32_000, 16_000); // Linear interpolation reproduces a linear function exactly at every // sampled position: out[i] ≈ (i * step) / (n - 1). const step = 32_000 / 16_000; for (let i = 0; i < out.length; i++) { const expected = Math.min((i * step) / (n - 1), 1); expect(out[i]!).toBeCloseTo(expected, 5); } }); test("upsampling interpolates between neighbours", () => { const input = Float32Array.from([0, 1]); const out = resampleLinear(input, 1, 2); expect(out.length).toBe(4); expect(out[0]).toBeCloseTo(0); expect(out[1]).toBeCloseTo(0.5); expect(out[2]).toBeCloseTo(1); // clamped at the last sample expect(out[3]).toBeCloseTo(1); }); });