import * as Tone from 'tone'; import { DrumMachine } from './drumMachine'; import { ChordEngine } from './chordEngine'; import { AmbientLayer } from './ambientLayer'; import { EngineState, AudioEngineCallbacks, LayerName, MeterLevels, LoopRegion, } from '@/types/audio'; class AudioEngine { private static instance: AudioEngine | null = null; private drumMachine: DrumMachine | null = null; private chordEngine: ChordEngine | null = null; private ambientLayer: AmbientLayer | null = null; private masterGain: Tone.Gain | null = null; private masterCompressor: Tone.Compressor | null = null; private masterLimiter: Tone.Limiter | null = null; private masterReverb: Tone.Reverb | null = null; // Meters for level detection private drumMeter: Tone.Meter | null = null; private chordMeter: Tone.Meter | null = null; private ambientMeter: Tone.Meter | null = null; private masterMeter: Tone.Meter | null = null; // Panners for stereo positioning private drumPanner: Tone.Panner | null = null; private chordPanner: Tone.Panner | null = null; private ambientPanner: Tone.Panner | null = null; // Solo state tracking private soloState: Record = { drums: false, chords: false, ambient: false, }; // Pre-solo volume states for restoration private preSoloMuted: Record | null = null; // Timeline state private durationBars: number = 16; private loopRegion: LoopRegion = { start: 0, end: 16, enabled: false }; private meterAnimationId: number | null = null; private barTrackerId: number | null = null; private callbacks: AudioEngineCallbacks = {}; private state: EngineState = { isPlaying: false, isInitialized: false, bpm: 78, swing: 0.12, currentStep: 0, volumes: { master: 0.8, drums: 0.8, chords: 0.6, ambient: 0.4, }, muted: { drums: false, chords: false, ambient: false, }, }; private constructor() {} static getInstance(): AudioEngine { if (!AudioEngine.instance) { AudioEngine.instance = new AudioEngine(); } return AudioEngine.instance; } async initialize(): Promise { if (this.state.isInitialized) return; // Start Tone.js audio context (requires user gesture) await Tone.start(); // Set up transport Tone.getTransport().bpm.value = this.state.bpm; Tone.getTransport().swing = this.state.swing; Tone.getTransport().swingSubdivision = '16n'; // Create meters this.drumMeter = new Tone.Meter({ smoothing: 0.8 }); this.chordMeter = new Tone.Meter({ smoothing: 0.8 }); this.ambientMeter = new Tone.Meter({ smoothing: 0.8 }); this.masterMeter = new Tone.Meter({ smoothing: 0.8 }); // Create panners this.drumPanner = new Tone.Panner(0); this.chordPanner = new Tone.Panner(0); this.ambientPanner = new Tone.Panner(0); // Master chain this.masterGain = new Tone.Gain(this.state.volumes.master); this.masterCompressor = new Tone.Compressor({ threshold: -20, ratio: 4, attack: 0.003, release: 0.25, }); this.masterLimiter = new Tone.Limiter(-1); this.masterReverb = new Tone.Reverb({ decay: 2, wet: 0.15, }); // Chain: gain -> reverb -> compressor -> limiter -> meter -> destination this.masterGain.connect(this.masterReverb); this.masterReverb.connect(this.masterCompressor); this.masterCompressor.connect(this.masterLimiter); this.masterLimiter.connect(this.masterMeter); this.masterMeter.toDestination(); // Initialize layers with panners and meters in chain this.drumMachine = new DrumMachine(this.drumPanner); this.chordEngine = new ChordEngine(this.chordPanner); this.ambientLayer = new AmbientLayer(this.ambientPanner); // Connect panners -> meters -> master this.drumPanner.connect(this.drumMeter); this.drumMeter.connect(this.masterGain); this.chordPanner.connect(this.chordMeter); this.chordMeter.connect(this.masterGain); this.ambientPanner.connect(this.ambientMeter); this.ambientMeter.connect(this.masterGain); // Create sequences this.drumMachine.createSequence((step) => { this.state.currentStep = step; this.callbacks.onStepChange?.(step); }); this.chordEngine.createSequence(); // Start meter animation loop this.startMeterLoop(); // Start bar tracking this.startBarTracking(); this.state.isInitialized = true; this.notifyStateChange(); } setCallbacks(callbacks: AudioEngineCallbacks): void { this.callbacks = callbacks; } private notifyStateChange(): void { this.callbacks.onStateChange?.({ ...this.state }); } async play(): Promise { if (!this.state.isInitialized) { await this.initialize(); } this.ambientLayer?.start(); Tone.getTransport().start(); this.state.isPlaying = true; this.notifyStateChange(); } pause(): void { Tone.getTransport().pause(); this.ambientLayer?.stop(); this.state.isPlaying = false; this.notifyStateChange(); } stop(): void { Tone.getTransport().stop(); this.ambientLayer?.stop(); this.state.isPlaying = false; this.state.currentStep = 0; this.notifyStateChange(); } generateNewBeat(): void { this.drumMachine?.randomize(); this.chordEngine?.randomize(); this.notifyStateChange(); } setBpm(bpm: number): void { this.state.bpm = Math.max(60, Math.min(100, bpm)); Tone.getTransport().bpm.value = this.state.bpm; this.notifyStateChange(); } setSwing(swing: number): void { this.state.swing = Math.max(0, Math.min(0.5, swing)); Tone.getTransport().swing = this.state.swing; this.notifyStateChange(); } setMasterVolume(volume: number): void { this.state.volumes.master = Math.max(0, Math.min(1, volume)); this.masterGain?.gain.rampTo(this.state.volumes.master, 0.1); this.notifyStateChange(); } setLayerVolume(layer: LayerName, volume: number): void { const normalizedVolume = Math.max(0, Math.min(1, volume)); this.state.volumes[layer] = normalizedVolume; switch (layer) { case 'drums': this.drumMachine?.setVolume(normalizedVolume); break; case 'chords': this.chordEngine?.setVolume(normalizedVolume); break; case 'ambient': this.ambientLayer?.setVolume(normalizedVolume); break; } this.notifyStateChange(); } toggleMute(layer: LayerName): void { this.state.muted[layer] = !this.state.muted[layer]; switch (layer) { case 'drums': this.drumMachine?.mute(this.state.muted[layer]); break; case 'chords': this.chordEngine?.mute(this.state.muted[layer]); break; case 'ambient': this.ambientLayer?.mute(this.state.muted[layer]); break; } this.notifyStateChange(); } setMuted(layer: LayerName, muted: boolean): void { this.state.muted[layer] = muted; switch (layer) { case 'drums': this.drumMachine?.mute(muted); break; case 'chords': this.chordEngine?.mute(muted); break; case 'ambient': this.ambientLayer?.mute(muted); break; } this.notifyStateChange(); } getState(): EngineState { return { ...this.state }; } // Meter methods private startMeterLoop(): void { const updateMeters = () => { if (this.state.isPlaying && this.callbacks.onMeterUpdate) { this.callbacks.onMeterUpdate(this.getMeterLevels()); } this.meterAnimationId = requestAnimationFrame(updateMeters); }; this.meterAnimationId = requestAnimationFrame(updateMeters); } private stopMeterLoop(): void { if (this.meterAnimationId !== null) { cancelAnimationFrame(this.meterAnimationId); this.meterAnimationId = null; } } getMeterLevels(): MeterLevels { const normalize = (val: number | number[]): number => { const v = typeof val === 'number' ? val : val[0] ?? -Infinity; const db = Math.max(-60, Math.min(0, v)); return (db + 60) / 60; }; return { drums: normalize(this.drumMeter?.getValue() ?? -Infinity), chords: normalize(this.chordMeter?.getValue() ?? -Infinity), ambient: normalize(this.ambientMeter?.getValue() ?? -Infinity), master: normalize(this.masterMeter?.getValue() ?? -Infinity), }; } // Panner methods setPan(layer: LayerName, value: number): void { const normalizedPan = Math.max(-1, Math.min(1, value)); switch (layer) { case 'drums': this.drumPanner?.pan.rampTo(normalizedPan, 0.1); break; case 'chords': this.chordPanner?.pan.rampTo(normalizedPan, 0.1); break; case 'ambient': this.ambientPanner?.pan.rampTo(normalizedPan, 0.1); break; } } getPan(layer: LayerName): number { switch (layer) { case 'drums': return this.drumPanner?.pan.value ?? 0; case 'chords': return this.chordPanner?.pan.value ?? 0; case 'ambient': return this.ambientPanner?.pan.value ?? 0; } } // Solo methods setSolo(layer: LayerName, enabled: boolean): void { const hadAnySolo = Object.values(this.soloState).some(Boolean); this.soloState[layer] = enabled; const hasAnySolo = Object.values(this.soloState).some(Boolean); if (!hadAnySolo && hasAnySolo) { this.preSoloMuted = { ...this.state.muted }; } if (hasAnySolo) { const layers: LayerName[] = ['drums', 'chords', 'ambient']; layers.forEach((l) => { const shouldMute = !this.soloState[l]; this.setMuted(l, shouldMute); }); } else if (hadAnySolo && !hasAnySolo && this.preSoloMuted) { const layers: LayerName[] = ['drums', 'chords', 'ambient']; layers.forEach((l) => { this.setMuted(l, this.preSoloMuted![l]); }); this.preSoloMuted = null; } } getSoloState(): Record { return { ...this.soloState }; } // Timeline methods private startBarTracking(): void { if (this.barTrackerId !== null) { Tone.getTransport().clear(this.barTrackerId); } this.barTrackerId = Tone.getTransport().scheduleRepeat((time) => { const position = Tone.getTransport().position; const [bars, beats] = String(position).split(':').map(Number); Tone.getDraw().schedule(() => { this.callbacks.onBarChange?.(bars, beats); }, time); }, '4n'); } seek(bar: number, beat: number = 0): void { const position = `${bar}:${beat}:0`; Tone.getTransport().position = position; this.callbacks.onBarChange?.(bar, beat); } setDuration(bars: number): void { this.durationBars = Math.max(4, Math.min(128, bars)); } getDuration(): number { return this.durationBars; } setLoopRegion(start: number, end: number, enabled: boolean): void { this.loopRegion = { start, end, enabled }; if (enabled) { Tone.getTransport().setLoopPoints(`${start}:0:0`, `${end}:0:0`); Tone.getTransport().loop = true; } else { Tone.getTransport().loop = false; } } getLoopRegion(): LoopRegion { return { ...this.loopRegion }; } getPlaybackPosition(): { bar: number; beat: number; sixteenth: number } { const position = String(Tone.getTransport().position); const [bars, beats, sixteenths] = position.split(':').map(Number); return { bar: bars || 0, beat: beats || 0, sixteenth: sixteenths || 0 }; } // Pattern access methods getDrumMachine(): DrumMachine | null { return this.drumMachine; } getChordEngine(): ChordEngine | null { return this.chordEngine; } dispose(): void { this.stop(); this.stopMeterLoop(); if (this.barTrackerId !== null) { Tone.getTransport().clear(this.barTrackerId); this.barTrackerId = null; } this.drumMachine?.dispose(); this.chordEngine?.dispose(); this.ambientLayer?.dispose(); this.masterGain?.dispose(); this.masterCompressor?.dispose(); this.masterLimiter?.dispose(); this.masterReverb?.dispose(); // Dispose new components this.drumMeter?.dispose(); this.chordMeter?.dispose(); this.ambientMeter?.dispose(); this.masterMeter?.dispose(); this.drumPanner?.dispose(); this.chordPanner?.dispose(); this.ambientPanner?.dispose(); this.drumMachine = null; this.chordEngine = null; this.ambientLayer = null; this.masterGain = null; this.masterCompressor = null; this.masterLimiter = null; this.masterReverb = null; this.drumMeter = null; this.chordMeter = null; this.ambientMeter = null; this.masterMeter = null; this.drumPanner = null; this.chordPanner = null; this.ambientPanner = null; this.state.isInitialized = false; this.state.isPlaying = false; AudioEngine.instance = null; } } export const audioEngine = AudioEngine.getInstance(); export default audioEngine;