forked from averyfelts/Lofi_Generator
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
267 lines
6.7 KiB
TypeScript
267 lines
6.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
import { EngineState, LayerName, MeterLevels, LoopRegion } from '@/types/audio';
|
|
|
|
const defaultState: 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,
|
|
},
|
|
};
|
|
|
|
const defaultMeterLevels: MeterLevels = {
|
|
drums: 0,
|
|
chords: 0,
|
|
ambient: 0,
|
|
master: 0,
|
|
};
|
|
|
|
const defaultPans: Record<LayerName, number> = {
|
|
drums: 0,
|
|
chords: 0,
|
|
ambient: 0,
|
|
};
|
|
|
|
const defaultSoloed: Record<LayerName, boolean> = {
|
|
drums: false,
|
|
chords: false,
|
|
ambient: false,
|
|
};
|
|
|
|
export function useAudioEngine() {
|
|
const [state, setState] = useState<EngineState>(defaultState);
|
|
const [currentStep, setCurrentStep] = useState(0);
|
|
const [meterLevels, setMeterLevels] = useState<MeterLevels>(defaultMeterLevels);
|
|
const [pans, setPans] = useState<Record<LayerName, number>>(defaultPans);
|
|
const [soloed, setSoloed] = useState<Record<LayerName, boolean>>(defaultSoloed);
|
|
const [playheadPosition, setPlayheadPosition] = useState({ bar: 0, beat: 0 });
|
|
|
|
const engineRef = useRef<typeof import('@/lib/audio/audioEngine').default | null>(null);
|
|
const isInitializingRef = useRef(false);
|
|
|
|
// Dynamically import the audio engine (client-side only)
|
|
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;
|
|
}, []);
|
|
|
|
// Initialize engine and set up callbacks
|
|
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);
|
|
},
|
|
onBarChange: (bar, beat) => {
|
|
setPlayheadPosition({ bar, beat });
|
|
},
|
|
onMeterUpdate: (levels) => {
|
|
setMeterLevels(levels);
|
|
},
|
|
});
|
|
|
|
await engine.initialize();
|
|
setState(engine.getState());
|
|
} finally {
|
|
isInitializingRef.current = false;
|
|
}
|
|
}, [getEngine]);
|
|
|
|
// Play/pause toggle
|
|
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]);
|
|
|
|
// Stop playback
|
|
const stop = useCallback(async () => {
|
|
const engine = await getEngine();
|
|
if (!engine) return;
|
|
engine.stop();
|
|
setCurrentStep(0);
|
|
}, [getEngine]);
|
|
|
|
// Generate new beat
|
|
const generateNewBeat = useCallback(async () => {
|
|
const engine = await getEngine();
|
|
if (!engine) return;
|
|
|
|
if (!state.isInitialized) {
|
|
await initialize();
|
|
}
|
|
|
|
engine.generateNewBeat();
|
|
}, [getEngine, state.isInitialized, initialize]);
|
|
|
|
// Set BPM
|
|
const setBpm = useCallback(async (bpm: number) => {
|
|
const engine = await getEngine();
|
|
if (!engine) return;
|
|
engine.setBpm(bpm);
|
|
}, [getEngine]);
|
|
|
|
// Set swing
|
|
const setSwing = useCallback(async (swing: number) => {
|
|
const engine = await getEngine();
|
|
if (!engine) return;
|
|
engine.setSwing(swing);
|
|
}, [getEngine]);
|
|
|
|
// Set master volume
|
|
const setMasterVolume = useCallback(async (volume: number) => {
|
|
const engine = await getEngine();
|
|
if (!engine) return;
|
|
engine.setMasterVolume(volume);
|
|
}, [getEngine]);
|
|
|
|
// Set layer volume
|
|
const setLayerVolume = useCallback(async (layer: LayerName, volume: number) => {
|
|
const engine = await getEngine();
|
|
if (!engine) return;
|
|
engine.setLayerVolume(layer, volume);
|
|
}, [getEngine]);
|
|
|
|
// Toggle layer mute
|
|
const toggleMute = useCallback(async (layer: LayerName) => {
|
|
const engine = await getEngine();
|
|
if (!engine) return;
|
|
engine.toggleMute(layer);
|
|
}, [getEngine]);
|
|
|
|
// Set muted state directly
|
|
const setMuted = useCallback(async (layer: LayerName, muted: boolean) => {
|
|
const engine = await getEngine();
|
|
if (!engine) return;
|
|
engine.setMuted(layer, muted);
|
|
}, [getEngine]);
|
|
|
|
// Set pan
|
|
const setPan = useCallback(async (layer: LayerName, value: number) => {
|
|
const engine = await getEngine();
|
|
if (!engine) return;
|
|
engine.setPan(layer, value);
|
|
setPans((prev) => ({ ...prev, [layer]: value }));
|
|
}, [getEngine]);
|
|
|
|
// Set solo
|
|
const setSolo = useCallback(async (layer: LayerName, enabled: boolean) => {
|
|
const engine = await getEngine();
|
|
if (!engine) return;
|
|
engine.setSolo(layer, enabled);
|
|
setSoloed(engine.getSoloState());
|
|
}, [getEngine]);
|
|
|
|
// Toggle solo
|
|
const toggleSolo = useCallback(async (layer: LayerName) => {
|
|
const engine = await getEngine();
|
|
if (!engine) return;
|
|
const current = engine.getSoloState();
|
|
engine.setSolo(layer, !current[layer]);
|
|
setSoloed(engine.getSoloState());
|
|
}, [getEngine]);
|
|
|
|
// Seek to position
|
|
const seek = useCallback(async (bar: number, beat: number = 0) => {
|
|
const engine = await getEngine();
|
|
if (!engine) return;
|
|
engine.seek(bar, beat);
|
|
setPlayheadPosition({ bar, beat });
|
|
}, [getEngine]);
|
|
|
|
// Set duration
|
|
const setDuration = useCallback(async (bars: number) => {
|
|
const engine = await getEngine();
|
|
if (!engine) return;
|
|
engine.setDuration(bars);
|
|
}, [getEngine]);
|
|
|
|
// Set loop region
|
|
const setLoopRegion = useCallback(
|
|
async (start: number, end: number, enabled: boolean) => {
|
|
const engine = await getEngine();
|
|
if (!engine) return;
|
|
engine.setLoopRegion(start, end, enabled);
|
|
},
|
|
[getEngine]
|
|
);
|
|
|
|
// Get meter levels on demand
|
|
const getMeterLevels = useCallback(async (): Promise<MeterLevels> => {
|
|
const engine = await getEngine();
|
|
if (!engine) return defaultMeterLevels;
|
|
return engine.getMeterLevels();
|
|
}, [getEngine]);
|
|
|
|
// Cleanup on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
// Don't dispose on unmount to allow seamless navigation
|
|
// The engine is a singleton that persists
|
|
};
|
|
}, []);
|
|
|
|
return {
|
|
state,
|
|
currentStep,
|
|
meterLevels,
|
|
pans,
|
|
soloed,
|
|
playheadPosition,
|
|
initialize,
|
|
togglePlayback,
|
|
stop,
|
|
generateNewBeat,
|
|
setBpm,
|
|
setSwing,
|
|
setMasterVolume,
|
|
setLayerVolume,
|
|
toggleMute,
|
|
setMuted,
|
|
setPan,
|
|
setSolo,
|
|
toggleSolo,
|
|
seek,
|
|
setDuration,
|
|
setLoopRegion,
|
|
getMeterLevels,
|
|
};
|
|
}
|