import * as Tone from 'tone'; import { PatternKeyframe, ChordKeyframe, MuteKeyframe, AutomationPoint, LayerName, } from '@/types/audio'; import { DrumMachine } from './drumMachine'; import { ChordEngine } from './chordEngine'; import { drumPatterns, chordProgressions } from './patterns'; type ScheduledEventId = number; interface ScheduledEvents { drumPatterns: ScheduledEventId[]; chordProgressions: ScheduledEventId[]; muteEvents: Record; automationEvents: Record; } export class TimelineScheduler { private transport = Tone.getTransport(); private events: ScheduledEvents = { drumPatterns: [], chordProgressions: [], muteEvents: { drums: [], chords: [], ambient: [] }, automationEvents: { drums: [], chords: [], ambient: [] }, }; private drumMachine: DrumMachine | null = null; private chordEngine: ChordEngine | null = null; private volumeCallback: ((layer: LayerName, value: number) => void) | null = null; private muteCallback: ((layer: LayerName, muted: boolean) => void) | null = null; setDrumMachine(dm: DrumMachine | null): void { this.drumMachine = dm; } setChordEngine(ce: ChordEngine | null): void { this.chordEngine = ce; } setVolumeCallback(cb: (layer: LayerName, value: number) => void): void { this.volumeCallback = cb; } setMuteCallback(cb: (layer: LayerName, muted: boolean) => void): void { this.muteCallback = cb; } scheduleDrumKeyframes(keyframes: PatternKeyframe[]): void { this.clearDrumEvents(); const sorted = [...keyframes].sort((a, b) => a.bar - b.bar); sorted.forEach((kf) => { const eventId = this.transport.schedule((time) => { if (this.drumMachine && drumPatterns[kf.patternIndex]) { this.drumMachine.setPattern(drumPatterns[kf.patternIndex]); } }, `${kf.bar}:0:0`); this.events.drumPatterns.push(eventId); }); } scheduleChordKeyframes(keyframes: ChordKeyframe[]): void { this.clearChordEvents(); const sorted = [...keyframes].sort((a, b) => a.bar - b.bar); sorted.forEach((kf) => { const eventId = this.transport.schedule((time) => { if (this.chordEngine && chordProgressions[kf.progressionIndex]) { this.chordEngine.setProgression(chordProgressions[kf.progressionIndex]); } }, `${kf.bar}:0:0`); this.events.chordProgressions.push(eventId); }); } scheduleMuteKeyframes(layer: LayerName, keyframes: MuteKeyframe[]): void { this.clearMuteEvents(layer); const sorted = [...keyframes].sort((a, b) => a.bar - b.bar); sorted.forEach((kf) => { const eventId = this.transport.schedule((time) => { this.muteCallback?.(layer, kf.muted); }, `${kf.bar}:0:0`); this.events.muteEvents[layer].push(eventId); }); } scheduleVolumeAutomation(layer: LayerName, points: AutomationPoint[]): void { this.clearAutomationEvents(layer); if (points.length === 0) return; const sorted = [...points].sort((a, b) => { if (a.bar !== b.bar) return a.bar - b.bar; return a.beat - b.beat; }); for (let i = 0; i < sorted.length; i++) { const current = sorted[i]; const next = sorted[i + 1]; if (!next) { const eventId = this.transport.schedule((time) => { this.volumeCallback?.(layer, current.value); }, `${current.bar}:${current.beat}:0`); this.events.automationEvents[layer].push(eventId); continue; } const startTime = this.transport.toSeconds(`${current.bar}:${current.beat}:0`); const endTime = this.transport.toSeconds(`${next.bar}:${next.beat}:0`); const duration = endTime - startTime; const steps = Math.max(1, Math.floor(duration * 16)); const stepDuration = duration / steps; for (let step = 0; step <= steps; step++) { const t = step / steps; const value = current.value + (next.value - current.value) * t; const scheduleTime = startTime + step * stepDuration; const eventId = this.transport.schedule((time) => { this.volumeCallback?.(layer, value); }, scheduleTime); this.events.automationEvents[layer].push(eventId); } } } private clearDrumEvents(): void { this.events.drumPatterns.forEach((id) => this.transport.clear(id)); this.events.drumPatterns = []; } private clearChordEvents(): void { this.events.chordProgressions.forEach((id) => this.transport.clear(id)); this.events.chordProgressions = []; } private clearMuteEvents(layer: LayerName): void { this.events.muteEvents[layer].forEach((id) => this.transport.clear(id)); this.events.muteEvents[layer] = []; } private clearAutomationEvents(layer: LayerName): void { this.events.automationEvents[layer].forEach((id) => this.transport.clear(id)); this.events.automationEvents[layer] = []; } clearAll(): void { this.clearDrumEvents(); this.clearChordEvents(); const layers: LayerName[] = ['drums', 'chords', 'ambient']; layers.forEach((l) => { this.clearMuteEvents(l); this.clearAutomationEvents(l); }); } dispose(): void { this.clearAll(); this.drumMachine = null; this.chordEngine = null; this.volumeCallback = null; this.muteCallback = null; } }