// The clip bench's bounded playback, as arithmetic. // // Run with: pnpm test:scripts import assert from "node:assert/strict"; import test from "node:test"; import { MAX_DECODE_SPAN, MUTE_FADE, bufferSchedule, covers, decodeSpan, elementShouldStop, muteRamp, parseMuteFrom, playheadAt, wavHeader, } from "./playback.mjs"; const near = (a, b, eps = 1e-9) => assert.ok(Math.abs(a - b) <= eps, `${a} ≉ ${b}`); test("decodeSpan: a short window decodes whole, whatever is being played", () => { assert.deepEqual(decodeSpan({ from: 24019.6, to: 24032.6 }, { from: 24022.6, to: 24029.6 }), { from: 24019.6, to: 24032.6, }); // Exactly at the cap is still whole. assert.deepEqual(decodeSpan({ from: 0, to: MAX_DECODE_SPAN }, { from: 5, to: 6 }), { from: 0, to: MAX_DECODE_SPAN, }); }); test("decodeSpan: a long window decodes the selection plus a margin, clamped and capped", () => { const whole = { from: 0, to: 7200 }; assert.deepEqual(decodeSpan(whole, { from: 3600, to: 3610 }), { from: 3570, to: 3640 }); // Clamped at the recording's start. assert.deepEqual(decodeSpan(whole, { from: 10, to: 20 }), { from: 0, to: 50 }); // Clamped at its end. assert.deepEqual(decodeSpan(whole, { from: 7190, to: 7200 }), { from: 7160, to: 7200 }); // A selection wider than the cap: from its start, as much as fits. const wide = decodeSpan(whole, { from: 1000, to: 1300 }); assert.deepEqual(wide, { from: 1000, to: 1000 + MAX_DECODE_SPAN }); // ...and never past the window's end. assert.deepEqual(decodeSpan(whole, { from: 7150, to: 7400 }), { from: 7120, to: 7200 }); assert.deepEqual(decodeSpan(whole, { from: 7000, to: 7400 }), { from: 7000, to: 7000 + MAX_DECODE_SPAN }); }); test("covers: the decoded span holds the whole range, with a hair of tolerance", () => { const span = { from: 10, to: 20 }; assert.equal(covers(span, 10, 20), true); assert.equal(covers(span, 12, 19.999), true); assert.equal(covers(span, 9.9, 15), false); assert.equal(covers(span, 15, 20.1), false); assert.equal(covers(null, 1, 2), false); }); test("bufferSchedule: offset into the buffer, source seconds to play, and how long that takes", () => { const span = { from: 24019.6, to: 24032.6 }; const s = bufferSchedule(span, 13, 24022.6, 24029.6, 1); near(s.offset, 3, 1e-6); near(s.duration, 7, 1e-6); near(s.wall, 7, 1e-6); // Speed changes how long it takes, not what is played. const fast = bufferSchedule(span, 13, 24022.6, 24029.6, 1.5); near(fast.offset, 3, 1e-6); near(fast.duration, 7, 1e-6); near(fast.wall, 7 / 1.5, 1e-6); const slow = bufferSchedule(span, 13, 24022.6, 24029.6, 0.75); near(slow.wall, 7 / 0.75, 1e-6); // A range running past the buffer stops at the buffer. near(bufferSchedule(span, 13, 24030, 24040, 1).duration, 2.6, 1e-6); // Nothing of the range in the buffer. assert.equal(bufferSchedule(span, 13, 24040, 24050, 1), null); // A nonsense rate is read as 1, not as a division by zero. near(bufferSchedule(span, 13, 24022.6, 24029.6, 0).wall, 7, 1e-6); }); test("playheadAt: from the audio clock, in source seconds, held at the end", () => { near(playheadAt(100, 5, 5, 1, 4), 100); near(playheadAt(100, 5, 6, 1, 4), 101); near(playheadAt(100, 5, 6, 2, 4), 102); // Before the scheduled start (the lead): at the start, not before it. near(playheadAt(100, 5, 4.98, 1, 4), 100); // Past the end: held at the end. near(playheadAt(100, 5, 60, 1, 4), 104); }); test("muteRamp: when the mark silences a playback, in wall seconds from its start", () => { assert.equal(muteRamp(null, 10, 20), null); assert.equal(muteRamp(undefined, 10, 20), null); // At or past the end of the range: nothing to mute in it. assert.equal(muteRamp(20, 10, 20), null); assert.equal(muteRamp(25, 10, 20), null); // At or before the start: silent throughout, the picture still plays. assert.deepEqual(muteRamp(10, 10, 20), { at: 0, fade: 0 }); assert.deepEqual(muteRamp(5, 10, 20), { at: 0, fade: 0 }); // Inside: full level until the fade, which ENDS at the mark (the build's). assert.equal(MUTE_FADE, 0.04); const m = muteRamp(16, 10, 20, 1); near(m.at, 6 - MUTE_FADE); near(m.fade, MUTE_FADE); near(m.at + m.fade, 6); // At 2x the mark arrives in half the time, and so does the fade. const f = muteRamp(16, 10, 20, 2); near(f.at, (6 - MUTE_FADE) / 2); near(f.fade, MUTE_FADE / 2); // A range starting inside the fade ramps from its first sample to the mark. const g = muteRamp(16, 15.98, 20, 1); near(g.at, 0); near(g.fade, 0.02); }); test("elementShouldStop: stops when the end is under half a frame away, at any rate", () => { const frame = 1 / 60; assert.equal(elementShouldStop(9.9, 10, 1, frame), false); assert.equal(elementShouldStop(10 - frame / 2 - 0.001, 10, 1, frame), false); assert.equal(elementShouldStop(10 - frame / 2 + 0.001, 10, 1, frame), true); assert.equal(elementShouldStop(10.2, 10, 1, frame), true); // At 2x a frame covers twice the source, so the stop comes a frame earlier. assert.equal(elementShouldStop(10 - frame + 0.001, 10, 2, frame), true); }); test("parseMuteFrom: a number inside the clip, rounded like an edge, or null to delete", () => { assert.equal(parseMuteFrom(null, 10, 20), null); assert.equal(parseMuteFrom("", 10, 20), null); assert.equal(parseMuteFrom(15.456, 10, 20), 15.46); assert.equal(parseMuteFrom(" 15.5 ", 10, 20), 15.5); assert.equal(parseMuteFrom(10, 10, 20), 10); assert.equal(parseMuteFrom(20, 10, 20), 20); // The 0.02 tolerance an edge gets, clamped back inside. assert.equal(parseMuteFrom(20.01, 10, 20), 20); assert.equal(parseMuteFrom(9.99, 10, 20), 10); assert.throws(() => parseMuteFrom(20.5, 10, 20), /must lie inside the clip 10–20/); assert.throws(() => parseMuteFrom(9, 10, 20), /must lie inside/); assert.throws(() => parseMuteFrom("soon", 10, 20), /must be a number/); assert.throws(() => parseMuteFrom(true, 10, 20), /must be a number/); assert.throws(() => parseMuteFrom(Number.NaN, 10, 20), /must be a number/); }); test("wavHeader: 44 bytes of RIFF/WAVE for interleaved 16-bit PCM", () => { const h = wavHeader({ channels: 2, sampleRate: 48000, dataBytes: 192000 }); assert.equal(h.length, 44); const v = new DataView(h.buffer); const tag = (at) => String.fromCharCode(...h.slice(at, at + 4)); assert.equal(tag(0), "RIFF"); assert.equal(v.getUint32(4, true), 36 + 192000); assert.equal(tag(8), "WAVE"); assert.equal(tag(12), "fmt "); assert.equal(v.getUint16(20, true), 1); assert.equal(v.getUint16(22, true), 2); assert.equal(v.getUint32(24, true), 48000); assert.equal(v.getUint32(28, true), 48000 * 4); assert.equal(v.getUint16(32, true), 4); assert.equal(v.getUint16(34, true), 16); assert.equal(tag(36), "data"); assert.equal(v.getUint32(40, true), 192000); });