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>(); 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): 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 { 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(); 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); } }