|
|
|
|
@@ -10,20 +10,28 @@ export interface MultiTrackPlayerState {
|
|
|
|
|
duration: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface TrackLevel {
|
|
|
|
|
trackId: string;
|
|
|
|
|
level: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
|
|
|
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
|
|
|
const [currentTime, setCurrentTime] = useState(0);
|
|
|
|
|
const [duration, setDuration] = useState(0);
|
|
|
|
|
const [trackLevels, setTrackLevels] = useState<Record<string, number>>({});
|
|
|
|
|
|
|
|
|
|
const audioContextRef = useRef<AudioContext | null>(null);
|
|
|
|
|
const sourceNodesRef = useRef<AudioBufferSourceNode[]>([]);
|
|
|
|
|
const gainNodesRef = useRef<GainNode[]>([]);
|
|
|
|
|
const panNodesRef = useRef<StereoPannerNode[]>([]);
|
|
|
|
|
const analyserNodesRef = useRef<AnalyserNode[]>([]);
|
|
|
|
|
const effectNodesRef = useRef<EffectNodeInfo[][]>([]); // Effect nodes per track
|
|
|
|
|
const masterGainNodeRef = useRef<GainNode | null>(null);
|
|
|
|
|
const startTimeRef = useRef<number>(0);
|
|
|
|
|
const pausedAtRef = useRef<number>(0);
|
|
|
|
|
const animationFrameRef = useRef<number | null>(null);
|
|
|
|
|
const levelMonitorFrameRef = useRef<number | null>(null);
|
|
|
|
|
const tracksRef = useRef<Track[]>(tracks); // Always keep latest tracks
|
|
|
|
|
|
|
|
|
|
// Keep tracksRef in sync with tracks prop
|
|
|
|
|
@@ -42,6 +50,36 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
|
|
|
|
|
setDuration(maxDuration);
|
|
|
|
|
}, [tracks]);
|
|
|
|
|
|
|
|
|
|
// Monitor playback levels for all tracks
|
|
|
|
|
const monitorPlaybackLevels = useCallback(() => {
|
|
|
|
|
if (!isPlaying || analyserNodesRef.current.length === 0) return;
|
|
|
|
|
|
|
|
|
|
const levels: Record<string, number> = {};
|
|
|
|
|
|
|
|
|
|
analyserNodesRef.current.forEach((analyser, index) => {
|
|
|
|
|
const track = tracksRef.current[index];
|
|
|
|
|
if (!track) return;
|
|
|
|
|
|
|
|
|
|
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
|
|
|
analyser.getByteTimeDomainData(dataArray);
|
|
|
|
|
|
|
|
|
|
// Calculate RMS level
|
|
|
|
|
let sum = 0;
|
|
|
|
|
for (let i = 0; i < dataArray.length; i++) {
|
|
|
|
|
const normalized = (dataArray[i] - 128) / 128;
|
|
|
|
|
sum += normalized * normalized;
|
|
|
|
|
}
|
|
|
|
|
const rms = Math.sqrt(sum / dataArray.length);
|
|
|
|
|
levels[track.id] = rms;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setTrackLevels(levels);
|
|
|
|
|
|
|
|
|
|
if (isPlaying) {
|
|
|
|
|
levelMonitorFrameRef.current = requestAnimationFrame(monitorPlaybackLevels);
|
|
|
|
|
}
|
|
|
|
|
}, [isPlaying]);
|
|
|
|
|
|
|
|
|
|
const updatePlaybackPosition = useCallback(() => {
|
|
|
|
|
if (!audioContextRef.current) return;
|
|
|
|
|
|
|
|
|
|
@@ -87,6 +125,7 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
|
|
|
|
|
sourceNodesRef.current = [];
|
|
|
|
|
gainNodesRef.current = [];
|
|
|
|
|
panNodesRef.current = [];
|
|
|
|
|
analyserNodesRef.current = [];
|
|
|
|
|
effectNodesRef.current = [];
|
|
|
|
|
|
|
|
|
|
// Create master gain node
|
|
|
|
|
@@ -104,6 +143,9 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
|
|
|
|
|
|
|
|
|
|
const gainNode = audioContext.createGain();
|
|
|
|
|
const panNode = audioContext.createStereoPanner();
|
|
|
|
|
const analyserNode = audioContext.createAnalyser();
|
|
|
|
|
analyserNode.fftSize = 256;
|
|
|
|
|
analyserNode.smoothingTimeConstant = 0.3;
|
|
|
|
|
|
|
|
|
|
// Set gain based on track volume and solo/mute state
|
|
|
|
|
const trackGain = getTrackGain(track, tracks);
|
|
|
|
|
@@ -112,7 +154,7 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
|
|
|
|
|
// Set pan
|
|
|
|
|
panNode.pan.setValueAtTime(track.pan, audioContext.currentTime);
|
|
|
|
|
|
|
|
|
|
// Connect: source -> gain -> pan -> effects -> master gain -> destination
|
|
|
|
|
// Connect: source -> gain -> pan -> effects -> analyser -> master gain -> destination
|
|
|
|
|
source.connect(gainNode);
|
|
|
|
|
gainNode.connect(panNode);
|
|
|
|
|
|
|
|
|
|
@@ -123,7 +165,10 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
|
|
|
|
|
console.log('[MultiTrackPlayer] Number of effects:', track.effectChain.effects.length);
|
|
|
|
|
console.log('[MultiTrackPlayer] Effects:', track.effectChain.effects);
|
|
|
|
|
const { outputNode, effectNodes } = applyEffectChain(audioContext, panNode, track.effectChain);
|
|
|
|
|
outputNode.connect(masterGain);
|
|
|
|
|
|
|
|
|
|
// Insert analyser after effects, before master gain
|
|
|
|
|
outputNode.connect(analyserNode);
|
|
|
|
|
analyserNode.connect(masterGain);
|
|
|
|
|
console.log('[MultiTrackPlayer] Effect output connected with', effectNodes.length, 'effect nodes');
|
|
|
|
|
|
|
|
|
|
// Start playback from current position
|
|
|
|
|
@@ -133,6 +178,7 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
|
|
|
|
|
sourceNodesRef.current.push(source);
|
|
|
|
|
gainNodesRef.current.push(gainNode);
|
|
|
|
|
panNodesRef.current.push(panNode);
|
|
|
|
|
analyserNodesRef.current.push(analyserNode);
|
|
|
|
|
effectNodesRef.current.push(effectNodes);
|
|
|
|
|
|
|
|
|
|
// Handle ended event
|
|
|
|
|
@@ -148,7 +194,8 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
|
|
|
|
|
startTimeRef.current = audioContext.currentTime;
|
|
|
|
|
setIsPlaying(true);
|
|
|
|
|
updatePlaybackPosition();
|
|
|
|
|
}, [tracks, duration, masterVolume, updatePlaybackPosition]);
|
|
|
|
|
monitorPlaybackLevels();
|
|
|
|
|
}, [tracks, duration, masterVolume, updatePlaybackPosition, monitorPlaybackLevels]);
|
|
|
|
|
|
|
|
|
|
const pause = useCallback(() => {
|
|
|
|
|
if (!audioContextRef.current || !isPlaying) return;
|
|
|
|
|
@@ -174,6 +221,14 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
|
|
|
|
|
cancelAnimationFrame(animationFrameRef.current);
|
|
|
|
|
animationFrameRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (levelMonitorFrameRef.current) {
|
|
|
|
|
cancelAnimationFrame(levelMonitorFrameRef.current);
|
|
|
|
|
levelMonitorFrameRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear track levels
|
|
|
|
|
setTrackLevels({});
|
|
|
|
|
}, [isPlaying, duration]);
|
|
|
|
|
|
|
|
|
|
const stop = useCallback(() => {
|
|
|
|
|
@@ -314,6 +369,7 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
|
|
|
|
|
sourceNodesRef.current = [];
|
|
|
|
|
gainNodesRef.current = [];
|
|
|
|
|
panNodesRef.current = [];
|
|
|
|
|
analyserNodesRef.current = [];
|
|
|
|
|
effectNodesRef.current = [];
|
|
|
|
|
|
|
|
|
|
// Create master gain node
|
|
|
|
|
@@ -331,6 +387,9 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
|
|
|
|
|
|
|
|
|
|
const gainNode = audioContext.createGain();
|
|
|
|
|
const panNode = audioContext.createStereoPanner();
|
|
|
|
|
const analyserNode = audioContext.createAnalyser();
|
|
|
|
|
analyserNode.fftSize = 256;
|
|
|
|
|
analyserNode.smoothingTimeConstant = 0.3;
|
|
|
|
|
|
|
|
|
|
// Set gain based on track volume and solo/mute state
|
|
|
|
|
const trackGain = getTrackGain(track, latestTracks);
|
|
|
|
|
@@ -339,13 +398,14 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
|
|
|
|
|
// Set pan
|
|
|
|
|
panNode.pan.setValueAtTime(track.pan, audioContext.currentTime);
|
|
|
|
|
|
|
|
|
|
// Connect: source -> gain -> pan -> effects -> master gain -> destination
|
|
|
|
|
// Connect: source -> gain -> pan -> effects -> analyser -> master gain -> destination
|
|
|
|
|
source.connect(gainNode);
|
|
|
|
|
gainNode.connect(panNode);
|
|
|
|
|
|
|
|
|
|
// Apply effect chain
|
|
|
|
|
const { outputNode, effectNodes } = applyEffectChain(audioContext, panNode, track.effectChain);
|
|
|
|
|
outputNode.connect(masterGain);
|
|
|
|
|
outputNode.connect(analyserNode);
|
|
|
|
|
analyserNode.connect(masterGain);
|
|
|
|
|
|
|
|
|
|
// Start playback from current position
|
|
|
|
|
source.start(0, pausedAtRef.current);
|
|
|
|
|
@@ -354,6 +414,7 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
|
|
|
|
|
sourceNodesRef.current.push(source);
|
|
|
|
|
gainNodesRef.current.push(gainNode);
|
|
|
|
|
panNodesRef.current.push(panNode);
|
|
|
|
|
analyserNodesRef.current.push(analyserNode);
|
|
|
|
|
effectNodesRef.current.push(effectNodes);
|
|
|
|
|
|
|
|
|
|
// Handle ended event
|
|
|
|
|
@@ -391,11 +452,12 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
|
|
|
|
|
animationFrameRef.current = requestAnimationFrame(updatePosition);
|
|
|
|
|
};
|
|
|
|
|
updatePosition();
|
|
|
|
|
monitorPlaybackLevels();
|
|
|
|
|
}, 10);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
previousEffectStructureRef.current = currentStructure;
|
|
|
|
|
}, [tracks, isPlaying, duration, masterVolume]);
|
|
|
|
|
}, [tracks, isPlaying, duration, masterVolume, monitorPlaybackLevels]);
|
|
|
|
|
|
|
|
|
|
// Stop playback when all tracks are deleted
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
@@ -454,6 +516,9 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
|
|
|
|
|
if (animationFrameRef.current) {
|
|
|
|
|
cancelAnimationFrame(animationFrameRef.current);
|
|
|
|
|
}
|
|
|
|
|
if (levelMonitorFrameRef.current) {
|
|
|
|
|
cancelAnimationFrame(levelMonitorFrameRef.current);
|
|
|
|
|
}
|
|
|
|
|
sourceNodesRef.current.forEach(node => {
|
|
|
|
|
try {
|
|
|
|
|
node.stop();
|
|
|
|
|
@@ -464,6 +529,7 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
|
|
|
|
|
});
|
|
|
|
|
gainNodesRef.current.forEach(node => node.disconnect());
|
|
|
|
|
panNodesRef.current.forEach(node => node.disconnect());
|
|
|
|
|
analyserNodesRef.current.forEach(node => node.disconnect());
|
|
|
|
|
if (masterGainNodeRef.current) {
|
|
|
|
|
masterGainNodeRef.current.disconnect();
|
|
|
|
|
}
|
|
|
|
|
@@ -474,6 +540,7 @@ export function useMultiTrackPlayer(tracks: Track[], masterVolume: number = 1) {
|
|
|
|
|
isPlaying,
|
|
|
|
|
currentTime,
|
|
|
|
|
duration,
|
|
|
|
|
trackLevels,
|
|
|
|
|
play,
|
|
|
|
|
pause,
|
|
|
|
|
stop,
|
|
|
|
|
|