Files
STSheets/tests/dsp/resample.test.ts
T
Lucas WintherandClaude Opus 5.5 f0249995cf fix(align): review round 1 for the alignment page
- server: collapse leading slashes so //host/x is never proxied off-host
  or redirected cross-origin; 1 MB body limit; max 10k anchors; a corrupt
  alignment.json no longer 500s /api/songs (song falls back to auto with
  a warning); comments on cookie scope and the loopback inner server
- warp (ruling A6): adding/moving a manual anchor removes autos only
  within 10 s (MIDI) of the new/old position, never past a manual, plus
  any auto that would break audio monotonicity
- page: first anchor clamped at >= 0 s; ruler scrubbing seeks on release;
  wording for the new auto-removal rule; removed-count excludes the
  dragged anchor; shows server warnings
- manifest: unique tmp name for alignment.json; clear error for a
  corrupt file
- resample test: speed asserted as a ratio to the direct path

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:48:32 +01:00

42 lines
2.0 KiB
TypeScript

import { test, expect } from "bun:test";
import { resample } from "../../src/dsp/resample.ts";
// Reference: the direct windowed-sinc evaluation (the original implementation), used to check the cached path is exact.
function reference(x: Float32Array, from: number, to: number): Float32Array {
const sinc = (v: number) => (v === 0 ? 1 : Math.sin(Math.PI * v) / (Math.PI * v));
const ratio = to / from, outLen = Math.round(x.length * ratio), out = new Float32Array(outLen);
const cutoff = Math.min(1, ratio), half = 16 / cutoff;
for (let j = 0; j < outLen; j++) {
const t = j / ratio;
const lo = Math.max(0, Math.ceil(t - half)), hi = Math.min(x.length - 1, Math.floor(t + half));
let acc = 0, wsum = 0;
for (let i = lo; i <= hi; i++) {
const d = i - t;
const w = 0.42 + 0.5 * Math.cos((Math.PI * d) / half) + 0.08 * Math.cos((2 * Math.PI * d) / half);
const k = cutoff * sinc(cutoff * d) * w;
acc += x[i]! * k; wsum += k;
}
out[j] = wsum !== 0 ? acc / wsum : 0;
}
return out;
}
const noise = (n: number) => { let s = 1; return Float32Array.from({ length: n }, () => ((s = (s * 16807) % 2147483647) / 2147483647) * 2 - 1); };
test("2:1 decimation is bit-identical to the direct evaluation and much faster", () => {
const x = noise(44100);
expect(Array.from(resample(x, 44100, 22050))).toEqual(Array.from(reference(x, 44100, 22050)));
const big = noise(44100 * 10);
const time = (f: () => unknown) => { const t0 = performance.now(); f(); return performance.now() - t0; };
time(() => resample(big, 44100, 22050)); // warm-up (JIT)
const fast = time(() => resample(big, 44100, 22050)), slow = time(() => reference(big, 44100, 22050));
// measured ~40x; a 4x margin keeps this robust on a loaded machine
expect(fast * 4).toBeLessThan(slow);
});
test("non-integer ratios are unchanged", () => {
const x = noise(4800);
expect(Array.from(resample(x, 48000, 44100))).toEqual(Array.from(reference(x, 48000, 44100)));
expect(Array.from(resample(x, 22050, 44100))).toEqual(Array.from(reference(x, 22050, 44100)));
});