- Add 4 genres: Hip Hop, Classical, Trap, Pop with unique patterns - Add new instrument layers: Bass, Brass, Piano - Each layer now has 4 instrument variations to choose from - Add genre-specific drum patterns, chord progressions, and melodies - Add duration control (1-10 minutes) - Rename app to "Beat Generator" with modern gradient header - Redesign UI with 2-column instrument grid layout - Add color-coded accent for each instrument section Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
196 lines
4.5 KiB
TypeScript
196 lines
4.5 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,
|
|
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,
|
|
};
|
|
}
|