Files
STSheets/tests/align/fit.test.ts
T
Lucas WintherandClaude Opus 5.5 894fd9b9ef feat(align): alignment web page
Token-protected Bun.serve server (bun run align [--host] [--port]
[--url-host] [--root]) with the layout-A page: recording and warped-MIDI
lanes on one zoomable timeline, draggable anchors (recording end / MIDI
end), stretch tempo labels with warnings, fit-flagged windows, Web Audio
playback with mix choice, auto-align, save, reset, undo. API per the task
brief; transcode/peaks cache under reference/<id>/. Adds bun run align
auto <id>, writeSmf (tests), encodeWav, recordingOnsets/fitFromOnsets,
fitSummary and stretch tempo helpers.

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

73 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from "bun:test";
import { fitQuality, fitSummary } from "../../src/align/fit.ts";
test("perfect fit: match rate 1, residual 0, one window per 30 s", () => {
const on = [1, 2, 3, 31, 32, 61];
const r = fitQuality(on, on, 75);
expect(r.matchRate).toBe(1);
expect(r.medianResidualMs).toBe(0);
expect(r.windows.map((w) => w.startSec)).toEqual([0, 30, 60]);
expect(r.windows.every((w) => !w.flagged && w.matchRate === 1)).toBe(true);
});
test("match within ±70 ms only; residual is median |audio − midi| of matched onsets", () => {
const midi = [1, 2, 3, 4];
const audio = [1.01, 2.03, 3.069, 4.2]; // last is 200 ms away: unmatched
const r = fitQuality(audio, midi, 10);
expect(r.matchRate).toBe(0.75);
expect(r.medianResidualMs!).toBeCloseTo(30, 6);
});
test("nearest recording onset is used and may be shared by several MIDI onsets", () => {
const r = fitQuality([1.0], [0.98, 1.0, 1.02], 5);
expect(r.matchRate).toBe(1);
expect(r.medianResidualMs!).toBeCloseTo(20, 6);
});
test("empty window gives nulls and is not flagged", () => {
const r = fitQuality([1, 2, 65], [1, 2, 65], 90);
expect(r.windows).toHaveLength(3);
const w = r.windows[1]!;
expect(w).toEqual({ startSec: 30, matchRate: null, medianResidualMs: null, flagged: false });
});
test("no MIDI onsets at all gives overall nulls", () => {
const r = fitQuality([1, 2], [], 10);
expect(r.matchRate).toBeNull();
expect(r.medianResidualMs).toBeNull();
expect(r.windows).toEqual([{ startSec: 0, matchRate: null, medianResidualMs: null, flagged: false }]);
});
test("window flagged when match rate < 0.5", () => {
const midi = [1, 2, 3, 4, 31, 32];
const audio = [1, 31, 32]; // window 0: 1/4 matched
const r = fitQuality(audio, midi, 60);
expect(r.windows[0]!.matchRate).toBe(0.25);
expect(r.windows[0]!.flagged).toBe(true);
expect(r.windows[1]!.flagged).toBe(false);
expect(r.matchRate).toBe(0.5);
});
test("window flagged when median residual > 60 ms even with full match", () => {
const midi = [1, 2, 3];
const r = fitQuality(midi.map((t) => t + 0.065), midi, 10);
expect(r.matchRate).toBe(1);
expect(r.windows[0]!.medianResidualMs!).toBeCloseTo(65, 6);
expect(r.windows[0]!.flagged).toBe(true);
});
test("unsorted and duplicate MIDI onsets: duplicates count once", () => {
const r = fitQuality([2, 1], [2, 1, 1, 1], 5);
expect(r.matchRate).toBe(1);
const s = fitQuality([1], [1, 1, 1, 3], 5);
expect(s.matchRate).toBe(0.5); // unique onsets {1, 3}
});
test("fitSummary: one line with match rate, median residual and flagged windows", () => {
const w = (startSec: number, flagged: boolean) => ({ startSec, matchRate: 0.9, medianResidualMs: 10, flagged });
expect(fitSummary({ matchRate: 0.8234, medianResidualMs: 14.24, windows: [w(0, false), w(30, true), w(60, false), w(90, true)] }))
.toBe("82.3% of MIDI onsets matched · median residual 14.2 ms · 2/4 windows flagged (0:30, 1:30)");
expect(fitSummary({ matchRate: 1, medianResidualMs: 3, windows: [w(0, false)] })).toBe("100.0% of MIDI onsets matched · median residual 3.0 ms · 0/1 windows flagged");
expect(fitSummary({ matchRate: null, medianResidualMs: null, windows: [] })).toBe("no scored MIDI onsets");
});