Initial Commit
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user