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]>
73 lines
3.2 KiB
TypeScript
73 lines
3.2 KiB
TypeScript
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");
|
||
});
|