Lofi_Generator/lib/audio/timelineScheduler.ts
Nicholai d3158e4c6a feat(timeline-mixer): WIP timeline and mixer components
work in progress implementation of:
- mixer with channel strips, faders, pan knobs, level meters
- timeline with ruler, playhead, sections, keyframe tracks
- pattern and progression pickers for drums/chords
- automation lanes and mute tracks
- loop bracket for loop region selection
- export modal placeholder

known issues:
- drum pattern changes don't update audio engine
- timeline keyframes not connected to scheduler
- some UI bugs remain

this is a checkpoint commit for further iteration
2026-01-20 18:22:10 -07:00

177 lines
5.3 KiB
TypeScript

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<LayerName, ScheduledEventId[]>;
automationEvents: Record<LayerName, ScheduledEventId[]>;
}
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;
}
}