- 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]>
72 lines
3.3 KiB
TypeScript
72 lines
3.3 KiB
TypeScript
import { test, expect, describe } from "bun:test";
|
|
import { dragAudio, dragMidi, hitAnchor, anchorAtCursor, MIN_GAP_SEC } from "../../../src/align/page/edit.ts";
|
|
import { makeWarp, type Anchor } from "../../../src/align/warp.ts";
|
|
|
|
const m = (midiSec: number, audioSec: number): Anchor => ({ midiSec, audioSec, kind: "manual" });
|
|
const a = (midiSec: number, audioSec: number): Anchor => ({ midiSec, audioSec, kind: "auto" });
|
|
const base = [m(10, 11), m(20, 22), m(30, 32)];
|
|
|
|
describe("dragAudio (recording end)", () => {
|
|
test("moves audioSec, keeps midiSec, returns the anchor's new index", () => {
|
|
const r = dragAudio(base, 1, 23);
|
|
expect(r.anchors[r.index]).toEqual(m(20, 23));
|
|
expect(r.anchors).toHaveLength(3);
|
|
});
|
|
test("cannot cross manual neighbours", () => {
|
|
expect(dragAudio(base, 1, 5).anchors[1]!.audioSec).toBeCloseTo(11 + MIN_GAP_SEC, 9);
|
|
expect(dragAudio(base, 1, 99).anchors[1]!.audioSec).toBeCloseTo(32 - MIN_GAP_SEC, 9);
|
|
});
|
|
test("the first anchor cannot be dragged before the start of the recording", () => {
|
|
expect(dragAudio(base, 0, -3).anchors[0]!.audioSec).toBe(0);
|
|
});
|
|
test("dragging an auto anchor makes it manual and drops the autos in its stretches", () => {
|
|
const list = [m(0, 0), a(5, 5.2), a(10, 10.3), a(15, 15.1), m(20, 20)];
|
|
const r = dragAudio(list, 2, 10.5);
|
|
expect(r.anchors).toEqual([m(0, 0), m(10, 10.5), m(20, 20)]);
|
|
expect(r.index).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("dragMidi (MIDI end): the grabbed MIDI moment follows the mouse, audioSec stays", () => {
|
|
for (const [grab, to] of [[22, 23.5], [22, 20.5], [21, 25], [24, 21]] as const) {
|
|
test(`grab at ${grab}s, drag to ${to}s`, () => {
|
|
const m0 = makeWarp(base).toMidi(grab);
|
|
const r = dragMidi(base, 1, m0, to);
|
|
expect(r.anchors[r.index]!.audioSec).toBe(22);
|
|
expect(makeWarp(r.anchors).toAudio(m0)).toBeCloseTo(to, 6);
|
|
});
|
|
}
|
|
test("no drag = no change", () => {
|
|
const r = dragMidi(base, 1, 20, 22);
|
|
expect(r.anchors[1]!.midiSec).toBeCloseTo(20, 6);
|
|
});
|
|
test("clamped between manual neighbours' MIDI times", () => {
|
|
const r = dragMidi(base, 1, 20, 31.9);
|
|
expect(r.anchors[1]!.midiSec).toBeGreaterThanOrEqual(10 + MIN_GAP_SEC - 1e-9);
|
|
const s = dragMidi(base, 1, 20, 11.1);
|
|
expect(s.anchors[1]!.midiSec).toBeLessThanOrEqual(30 - MIN_GAP_SEC + 1e-9);
|
|
});
|
|
test("works for the first and only anchor (edge extrapolation)", () => {
|
|
const r = dragMidi([m(10, 11)], 0, 10, 12);
|
|
expect(makeWarp(r.anchors).toAudio(10)).toBeCloseTo(12, 6);
|
|
expect(r.anchors[0]!.audioSec).toBe(11);
|
|
const f = dragMidi(base, 0, 10, 12);
|
|
expect(makeWarp(f.anchors).toAudio(10)).toBeCloseTo(12, 6);
|
|
});
|
|
});
|
|
|
|
test("hitAnchor picks the nearest anchor line within tolerance", () => {
|
|
const v = { start: 0, pxPerSec: 10, width: 500 };
|
|
expect(hitAnchor(base, v, 221, 6)).toBe(1);
|
|
expect(hitAnchor(base, v, 113, 6)).toBe(0);
|
|
expect(hitAnchor(base, v, 160, 6)).toBe(-1);
|
|
});
|
|
|
|
test("anchorAtCursor adds a manual anchor where the current warp maps the cursor", () => {
|
|
const r = anchorAtCursor(base, 27);
|
|
const k = r.anchors.findIndex((x) => x.audioSec === 27);
|
|
expect(r.index).toBe(k);
|
|
expect(r.anchors[k]).toEqual(m(makeWarp(base).toMidi(27), 27));
|
|
expect(() => anchorAtCursor(base, 22)).toThrow(); // already an anchor there
|
|
});
|