Lofi_Generator/hooks/useAudioEngine.ts
Avery Felts 26d66f329c Add song structure generation and fix volume controls
- Fix volume sliders by tracking currentVolume separately from mute state
- Add song structure system with intro, verse, bridge, chorus, outro sections
- Implement automatic section transitions during playback based on duration
- Add 30-second loading bar with progress animation during beat generation
- Update instrument box colors (grey drums, red bass, green piano, baby blue brass)
- Add custom trumpet SVG icon for brass section
- Set duration slider max to 3 minutes
- Fix TypeScript type errors in audio engine classes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 23:30:33 -07:00

197 lines
4.6 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback, useRef } from 'react';
import { EngineState, LayerName, Genre } from '@/types/audio';
const defaultState: EngineState = {
isPlaying: false,
isInitialized: false,
bpm: 90,
swing: 0.15,
currentStep: 0,
currentSection: 'intro',
genre: 'hiphop',
duration: 3,
instruments: {
drums: 'acoustic',
bass: 'synth',
brass: 'trumpet',
piano: 'grand',
chords: 'pad',
ambient: 'rain',
},
volumes: {
master: 0.8,
drums: 0.8,
bass: 0.6,
brass: 0.4,
piano: 0.5,
chords: 0.6,
ambient: 0.4,
},
muted: {
drums: false,
bass: false,
brass: true,
piano: true,
chords: false,
ambient: false,
},
};
export function useAudioEngine() {
const [state, setState] = useState<EngineState>(defaultState);
const [currentStep, setCurrentStep] = useState(0);
const engineRef = useRef<typeof import('@/lib/audio/audioEngine').default | null>(null);
const isInitializingRef = useRef(false);
const getEngine = useCallback(async () => {
if (typeof window === 'undefined') return null;
if (!engineRef.current) {
const { default: audioEngine } = await import('@/lib/audio/audioEngine');
engineRef.current = audioEngine;
}
return engineRef.current;
}, []);
const initialize = useCallback(async () => {
if (isInitializingRef.current) return;
isInitializingRef.current = true;
try {
const engine = await getEngine();
if (!engine) return;
engine.setCallbacks({
onStepChange: (step) => {
setCurrentStep(step);
},
onStateChange: (newState) => {
setState(newState);
},
});
await engine.initialize();
setState(engine.getState());
} finally {
isInitializingRef.current = false;
}
}, [getEngine]);
const togglePlayback = useCallback(async () => {
const engine = await getEngine();
if (!engine) return;
if (!state.isInitialized) {
await initialize();
}
const currentState = engine.getState();
if (currentState.isPlaying) {
engine.pause();
} else {
await engine.play();
}
}, [getEngine, state.isInitialized, initialize]);
const stop = useCallback(async () => {
const engine = await getEngine();
if (!engine) return;
engine.stop();
setCurrentStep(0);
}, [getEngine]);
const generateNewBeat = useCallback(async () => {
const engine = await getEngine();
if (!engine) return;
if (!state.isInitialized) {
await initialize();
}
engine.generateNewBeat();
}, [getEngine, state.isInitialized, initialize]);
const setGenre = useCallback(async (genre: Genre) => {
const engine = await getEngine();
if (!engine) return;
if (!state.isInitialized) {
await initialize();
}
engine.setGenre(genre);
}, [getEngine, state.isInitialized, initialize]);
const setDuration = useCallback(async (minutes: number) => {
const engine = await getEngine();
if (!engine) return;
engine.setDuration(minutes);
}, [getEngine]);
const setInstrument = useCallback(async (layer: LayerName, instrument: string) => {
const engine = await getEngine();
if (!engine) return;
if (!state.isInitialized) {
await initialize();
}
engine.setInstrument(layer, instrument);
}, [getEngine, state.isInitialized, initialize]);
const setBpm = useCallback(async (bpm: number) => {
const engine = await getEngine();
if (!engine) return;
engine.setBpm(bpm);
}, [getEngine]);
const setSwing = useCallback(async (swing: number) => {
const engine = await getEngine();
if (!engine) return;
engine.setSwing(swing);
}, [getEngine]);
const setMasterVolume = useCallback(async (volume: number) => {
const engine = await getEngine();
if (!engine) return;
engine.setMasterVolume(volume);
}, [getEngine]);
const setLayerVolume = useCallback(async (layer: LayerName, volume: number) => {
const engine = await getEngine();
if (!engine) return;
engine.setLayerVolume(layer, volume);
}, [getEngine]);
const toggleMute = useCallback(async (layer: LayerName) => {
const engine = await getEngine();
if (!engine) return;
engine.toggleMute(layer);
}, [getEngine]);
useEffect(() => {
return () => {
// Don't dispose on unmount
};
}, []);
return {
state,
currentStep,
initialize,
togglePlayback,
stop,
generateNewBeat,
setGenre,
setDuration,
setInstrument,
setBpm,
setSwing,
setMasterVolume,
setLayerVolume,
toggleMute,
};
}