84 lines
2.5 KiB
TypeScript
84 lines
2.5 KiB
TypeScript
import type { TransportLike, TransportTime } from "@suaveplan/audio-transport";
|
|
import { getControllerOtel } from "../base/otel.js";
|
|
import type { SequencerOptions, StepCallback } from "./sequencer.types.js";
|
|
|
|
/**
|
|
* Transport-synced step sequencer. Every step fires through
|
|
* `Transport.scheduleRepeat`; no `setInterval`/`setTimeout`/`Date.now`
|
|
* appears anywhere on the timing path.
|
|
*/
|
|
export class Sequencer {
|
|
private readonly transport: TransportLike;
|
|
private readonly stepDuration: TransportTime;
|
|
private steps: boolean[] = [];
|
|
private readonly stepListeners = new Map<number, Set<StepCallback>>();
|
|
private repeatId: number | undefined;
|
|
private cursor = 0;
|
|
private bpm = 120;
|
|
|
|
constructor(options: SequencerOptions) {
|
|
this.transport = options.transport;
|
|
this.stepDuration = options.stepDuration ?? "16n";
|
|
}
|
|
|
|
public setTempo(bpm: number): void {
|
|
this.bpm = bpm;
|
|
this.transport.bpm.setValueAtTime(bpm, 0);
|
|
}
|
|
|
|
public getTempo(): number {
|
|
return this.bpm;
|
|
}
|
|
|
|
public setSteps(steps: ReadonlyArray<boolean>): void {
|
|
this.steps = steps.slice();
|
|
}
|
|
|
|
public setStep(index: number, active: boolean): void {
|
|
while (this.steps.length <= index) this.steps.push(false);
|
|
this.steps[index] = active;
|
|
}
|
|
|
|
public getSteps(): ReadonlyArray<boolean> {
|
|
return this.steps.slice();
|
|
}
|
|
|
|
public start(): void {
|
|
if (this.repeatId !== undefined) return;
|
|
this.repeatId = this.transport.scheduleRepeat(
|
|
(time) => this.tick(time),
|
|
this.stepDuration
|
|
);
|
|
}
|
|
|
|
public stop(): void {
|
|
if (this.repeatId === undefined) return;
|
|
this.transport.cancel(this.repeatId);
|
|
this.repeatId = undefined;
|
|
}
|
|
|
|
public reset(): void {
|
|
this.cursor = 0;
|
|
}
|
|
|
|
public onStep(index: number, callback: StepCallback): () => void {
|
|
const bucket = this.stepListeners.get(index) ?? new Set<StepCallback>();
|
|
bucket.add(callback);
|
|
this.stepListeners.set(index, bucket);
|
|
return () => bucket.delete(callback);
|
|
}
|
|
|
|
private tick(time: number): void {
|
|
const otel = getControllerOtel();
|
|
otel.sequencerSteps.add(1);
|
|
const idx = this.cursor % Math.max(this.steps.length, 1);
|
|
if (this.steps[idx]) {
|
|
const bucket = this.stepListeners.get(idx);
|
|
if (bucket) {
|
|
for (const fn of bucket) fn(time);
|
|
}
|
|
}
|
|
this.cursor = (this.cursor + 1) % Math.max(this.steps.length, 1);
|
|
}
|
|
}
|