import * as Tone from 'tone'; import { DrumMachine } from './drumMachine'; import { ChordEngine } from './chordEngine'; import { AmbientLayer } from './ambientLayer'; import { EngineState, AudioEngineCallbacks, LayerName } 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; 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'; // 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 -> destination this.masterGain.connect(this.masterReverb); this.masterReverb.connect(this.masterCompressor); this.masterCompressor.connect(this.masterLimiter); this.masterLimiter.toDestination(); // Initialize layers this.drumMachine = new DrumMachine(this.masterGain); this.chordEngine = new ChordEngine(this.masterGain); this.ambientLayer = new AmbientLayer(this.masterGain); // Create sequences this.drumMachine.createSequence((step) => { this.state.currentStep = step; this.callbacks.onStepChange?.(step); }); this.chordEngine.createSequence(); 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 }; } dispose(): void { this.stop(); this.drumMachine?.dispose(); this.chordEngine?.dispose(); this.ambientLayer?.dispose(); this.masterGain?.dispose(); this.masterCompressor?.dispose(); this.masterLimiter?.dispose(); this.masterReverb?.dispose(); this.drumMachine = null; this.chordEngine = null; this.ambientLayer = null; this.masterGain = null; this.masterCompressor = null; this.masterLimiter = null; this.masterReverb = null; this.state.isInitialized = false; this.state.isPlaying = false; AudioEngine.instance = null; } } export const audioEngine = AudioEngine.getInstance(); export default audioEngine;