import test from "node:test"; import assert from "node:assert/strict"; import { Stft, fft } from "../src/analysis/spectrogram.ts"; import { encodeWav, scaleForPlayback } from "../src/audio/player.ts"; test("FFT of a sine peaks at the right bin", () => { const n = 1024, re = new Float32Array(n), im = new Float32Array(n); for (let i = 0; i < n; i++) re[i] = Math.sin((2 * Math.PI * 64 * i) / n); fft(re, im); let best = 0; for (let b = 1; b < n / 2; b++) if (Math.hypot(re[b], im[b]) > Math.hypot(re[best], im[best])) best = b; assert.equal(best, 64); assert.ok(Math.abs(Math.hypot(re[64], im[64]) - n / 2) < 1e-2); }); test("incremental STFT tracks a chirp's centroid", () => { const fs = 44100, st = new Stft(fs, 1024, 256); const sig = new Float32Array(fs); for (let i = 0; i < fs; i++) { const t = i / fs; sig[i] = Math.sin(2 * Math.PI * (500 * t + 2000 * t * t)); } let cols = 0; for (let s = 0; s < fs; s += 1000) cols += st.push(sig.subarray(s, Math.min(fs, s + 1000))); assert.equal(cols, st.columns.length); assert.ok(cols > 150); const early = st.centroid[5], late = st.centroid[cols - 5]; assert.ok(early > 400 && early < 1200 && late > 3500 && late < 5500, `${early} ${late}`); }); test("WAV encoding and playback scaling", () => { const p = new Float32Array([0, 2, -4, 1]); const s = scaleForPlayback(p, true); assert.ok(Math.abs(Math.max(...Array.from(s).map(Math.abs)) - 0.9) < 1e-6); const blob = encodeWav(s, 44100); assert.equal(blob.size, 44 + 8); }); test("band summary reports staggered onsets for a chirp", async () => { const { bandSummary } = await import("../src/analysis/spectrogram.ts"); const fs = 44100, st = new Stft(fs, 1024, 256), sig = new Float32Array(fs); for (let i = 0; i < fs; i++) { const t = i / fs; sig[i] = Math.sin(2 * Math.PI * (200 * t + 4000 * t * t)); } st.push(sig); const s = bandSummary(st); const b250 = s.bands.find((b) => b.lo === 250)!, b2k = s.bands.find((b) => b.lo === 2000)!; assert.ok(b250.onsetMs < b2k.onsetMs, `${b250.onsetMs} vs ${b2k.onsetMs}`); assert.ok(s.centroidHz.length > 3 && s.centroidHz[0].hz < s.centroidHz[s.centroidHz.length - 1].hz); }); test("movie store quantizes, times and seeks frames", async () => { const { Movie, movieEvery } = await import("../src/analysis/movie.ts"); const fs = 44100, N = 50; assert.equal(movieEvery(fs, 2000), 22); const m = new Movie(N, fs, { fps: 2000, budgetBytes: 1e6 }); for (let f = 0; f < 10; f++) { const w = new Float32Array(N); for (let n = 0; n < N; n++) w[n] = 1e-3 * Math.sin(n + f); m.push(w, f * 22); } assert.equal(m.length, 10); assert.equal(m.bytes, 10 * N * 2); assert.ok(Math.abs(m.duration - (9 * 22) / fs) < 1e-12); // dequantization is accurate to about 1e-4 of the frame's peak const back = m.read(3); for (let n = 0; n < N; n++) assert.ok(Math.abs(back[n] - 1e-3 * Math.sin(n + 3)) < 1e-3 * 1e-4); assert.equal(m.indexAtTime(0), 0); assert.equal(m.indexAtTime((3 * 22) / fs), 3); assert.equal(m.indexAtTime(10), 9); // the budget stops capture rather than growing without bound const small = new Movie(N, fs, { fps: 2000, budgetBytes: 3 * N * 2 }); for (let f = 0; f < 10; f++) small.push(new Float32Array(N), f * 22); assert.equal(small.length, 3); assert.ok(small.full); });