Files
suaveplan/packages/media/audio/audio-controllers/tests/fake-transport.ts
2026-05-27 23:47:53 +01:00

132 lines
3.7 KiB
TypeScript

/**
* Real `TransportLike` implementation backed by an in-memory event log.
* Used in tests so step/arpeggio callbacks can be driven deterministically
* without spinning a real wall-clock scheduler. This is a real object that
* satisfies the `TransportLike` contract — not a runner mock.
*/
import type {
BarsBeatsSixteenths,
SignalLike,
TransportCallback,
TransportLike,
TransportTime,
} from "@suaveplan/audio-transport";
interface ScheduledRepeat {
readonly id: number;
readonly callback: TransportCallback;
readonly interval: TransportTime;
}
interface ScheduledOnce {
readonly id: number;
readonly callback: TransportCallback;
readonly time: number;
}
const NOOP_BBS: BarsBeatsSixteenths = "0:0:0";
function makeFakeSignal(): SignalLike {
let v = 120;
const points: { time: number; value: number; type: "set" }[] = [];
const sig: SignalLike = {
get points() {
return points.slice();
},
get initialValue() {
return 120;
},
get value() {
return v;
},
setValueAtTime(value: number, time: number) {
v = value;
points.push({ time, value, type: "set" });
},
linearRampToValueAtTime(value: number) {
v = value;
},
exponentialRampToValueAtTime(value: number) {
v = value;
},
getValueAtTime() {
return v;
},
cancelScheduledValues() {
points.length = 0;
},
};
return sig;
}
export class FakeTransport implements TransportLike {
public bpm: SignalLike = makeFakeSignal();
public readonly ppq = 192;
public readonly timeSignature = 4;
public swing = 0;
public swingSubdivision: TransportTime = "8n";
public loop = false;
public loopStart: TransportTime = 0;
public loopEnd: TransportTime = 0;
public state: "stopped" | "playing" | "paused" = "stopped";
public position: BarsBeatsSixteenths = NOOP_BBS;
public ticks = 0;
public seconds = 0;
public readonly repeats = new Map<number, ScheduledRepeat>();
public readonly onces = new Map<number, ScheduledOnce>();
private nextId = 1;
start(): void {
this.state = "playing";
}
stop(): void {
this.state = "stopped";
}
pause(): void {
this.state = "paused";
}
schedule(callback: TransportCallback, time: TransportTime): number {
return this.scheduleOnce(callback, time);
}
scheduleOnce(callback: TransportCallback, time: TransportTime): number {
const id = this.nextId++;
const num = typeof time === "number" ? time : 0;
this.onces.set(id, { id, callback, time: num });
return id;
}
scheduleRepeat(
callback: TransportCallback,
interval: TransportTime
): number {
const id = this.nextId++;
this.repeats.set(id, { id, callback, interval });
return id;
}
cancel(eventId: number): void {
this.repeats.delete(eventId);
this.onces.delete(eventId);
}
cancelAfter(_time: TransportTime): void {
// Not used by audio-controllers tests.
}
toSeconds(time: TransportTime): number {
return typeof time === "number" ? time / this.ppq : 0;
}
toTicks(time: TransportTime): number {
return typeof time === "number" ? time * this.ppq : 0;
}
/** Test helper: drive every active repeat once. */
tickAll(time = 0): void {
for (const r of this.repeats.values()) r.callback(time);
}
/** Test helper: fire every scheduled `scheduleOnce`. */
flushOnces(): void {
for (const o of this.onces.values()) o.callback(o.time);
this.onces.clear();
}
}