Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -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);
}
}