fix butterchurn init and cleanup

This commit is contained in:
jeffvli
2026-01-04 02:35:44 -08:00
parent 327875df6a
commit ff272a5385
2 changed files with 160 additions and 123 deletions
@@ -2110,8 +2110,6 @@ const ButterChurnCycleSettings = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { updateProperty, visualizer } = useUpdateButterchurn(); const { updateProperty, visualizer } = useUpdateButterchurn();
console.log(butterchurnPresets, 'number of presets');
const presetOptions = useMemo(() => { const presetOptions = useMemo(() => {
const presets = butterchurnPresets; const presets = butterchurnPresets;
return Object.keys(presets).map((presetName) => ({ return Object.keys(presets).map((presetName) => ({
@@ -24,121 +24,121 @@ const VisualizerInner = () => {
const { webAudio } = useWebAudio(); const { webAudio } = useWebAudio();
const canvasRef = createRef<HTMLCanvasElement>(); const canvasRef = createRef<HTMLCanvasElement>();
const containerRef = createRef<HTMLDivElement>(); const containerRef = createRef<HTMLDivElement>();
const [visualizer, setVisualizer] = useState<ButterchurnVisualizer>(); const visualizerRef = useRef<ButterchurnVisualizer | undefined>(undefined);
const isInitializedRef = useRef(false);
const [isVisualizerReady, setIsVisualizerReady] = useState(false);
const animationFrameRef = useRef<number | undefined>(undefined); const animationFrameRef = useRef<number | undefined>(undefined);
const resizeObserverRef = useRef<ResizeObserver | undefined>(undefined); const resizeObserverRef = useRef<ResizeObserver | undefined>(undefined);
const cycleTimerRef = useRef<NodeJS.Timeout | undefined>(undefined); const cycleTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
const cycleStartTimeRef = useRef<number | undefined>(undefined); const cycleStartTimeRef = useRef<number | undefined>(undefined);
const pauseTimerRef = useRef<NodeJS.Timeout | undefined>(undefined); const pauseTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
const initialPresetLoadedRef = useRef(false);
const butterchurnSettings = useSettingsStore((store) => store.visualizer.butterchurn); const butterchurnSettings = useSettingsStore((store) => store.visualizer.butterchurn);
const opacity = useSettingsStore((store) => store.visualizer.butterchurn.opacity); const opacity = useSettingsStore((store) => store.visualizer.butterchurn.opacity);
const { setSettings } = useSettingsStoreActions(); const { setSettings } = useSettingsStoreActions();
const playerStatus = usePlayerStatus(); const playerStatus = usePlayerStatus();
const isPlaying = playerStatus === PlayerStatus.PLAYING; const isPlaying = playerStatus === PlayerStatus.PLAYING;
const cleanupVisualizer = () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = undefined;
}
if (cycleTimerRef.current) {
clearInterval(cycleTimerRef.current);
cycleTimerRef.current = undefined;
}
if (pauseTimerRef.current) {
clearTimeout(pauseTimerRef.current);
pauseTimerRef.current = undefined;
}
if (resizeObserverRef.current) {
resizeObserverRef.current.disconnect();
resizeObserverRef.current = undefined;
}
visualizerRef.current = undefined;
isInitializedRef.current = false;
initialPresetLoadedRef.current = false;
setIsVisualizerReady(false);
};
// Initialize butterchurn instance
useEffect(() => { useEffect(() => {
const { context, gains } = webAudio || {}; const { context, gains } = webAudio || {};
if ( const canvas = canvasRef.current;
const container = containerRef.current;
const needsInitialization =
context && context &&
gains && gains &&
canvasRef.current && gains.length > 0 &&
containerRef.current && canvas &&
!visualizer && container &&
isPlaying isPlaying &&
) { (!isInitializedRef.current || !visualizerRef.current);
const canvas = canvasRef.current;
const container = containerRef.current;
const getDimensions = () => { if (!needsInitialization) {
const rect = container.getBoundingClientRect(); return;
return { }
height: rect.height || 600,
width: rect.width || 800, const getDimensions = () => {
}; const rect = container.getBoundingClientRect();
return {
height: rect.height || 600,
width: rect.width || 800,
}; };
};
let dimensions = getDimensions(); let dimensions = getDimensions();
// If dimensions are 0, wait for next frame // If dimensions are 0, wait for next frame
if (dimensions.width === 0 || dimensions.height === 0) { if (dimensions.width === 0 || dimensions.height === 0) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
dimensions = getDimensions(); dimensions = getDimensions();
if (dimensions.width > 0 && dimensions.height > 0) { if (dimensions.width > 0 && dimensions.height > 0) {
initializeVisualizer(dimensions.width, dimensions.height); initializeVisualizer(dimensions.width, dimensions.height);
}
});
} else {
initializeVisualizer(dimensions.width, dimensions.height);
}
function initializeVisualizer(width: number, height: number) {
if (!gains || gains.length === 0) return;
canvas.width = width;
canvas.height = height;
try {
const butterchurnInstance = butterchurn.createVisualizer(context, canvas, {
height,
width,
}) as ButterchurnVisualizer;
for (const gain of gains) {
butterchurnInstance.connectAudio(gain);
}
const presets = butterchurnPresets;
const presetNames = Object.keys(presets);
if (presetNames.length > 0) {
const presetName =
butterchurnSettings.currentPreset &&
presets[butterchurnSettings.currentPreset]
? butterchurnSettings.currentPreset
: presetNames[0];
const preset = presets[presetName];
butterchurnInstance.loadPreset(
preset,
butterchurnSettings.blendTime || 0.0,
);
// Initialize cycle timer
cycleStartTimeRef.current = Date.now();
}
setVisualizer(butterchurnInstance);
} catch (error) {
console.error('Failed to create butterchurn visualizer:', error);
} }
});
} else {
initializeVisualizer(dimensions.width, dimensions.height);
}
function initializeVisualizer(width: number, height: number) {
if (!gains || gains.length === 0 || !canvas || !context) return;
canvas.width = width;
canvas.height = height;
try {
const butterchurnInstance = butterchurn.createVisualizer(context, canvas, {
height,
width,
}) as ButterchurnVisualizer;
for (const gain of gains) {
butterchurnInstance.connectAudio(gain);
}
visualizerRef.current = butterchurnInstance;
isInitializedRef.current = true;
setIsVisualizerReady(true);
} catch (error) {
console.error('Failed to create butterchurn visualizer:', error);
isInitializedRef.current = false;
visualizerRef.current = undefined;
} }
} }
return () => { return () => {
if (animationFrameRef.current) { // Cleanup on unmount or when webAudio changes
cancelAnimationFrame(animationFrameRef.current); cleanupVisualizer();
animationFrameRef.current = undefined;
}
if (cycleTimerRef.current) {
clearInterval(cycleTimerRef.current);
cycleTimerRef.current = undefined;
}
if (pauseTimerRef.current) {
clearTimeout(pauseTimerRef.current);
pauseTimerRef.current = undefined;
}
if (resizeObserverRef.current) {
resizeObserverRef.current.disconnect();
resizeObserverRef.current = undefined;
}
if (visualizer) {
setVisualizer(undefined);
}
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [webAudio, canvasRef, containerRef, visualizer, isPlaying]); }, [webAudio, isPlaying]);
// Kill visualizer after 5 seconds of pause // Kill visualizer after 5 seconds of pause
useEffect(() => { useEffect(() => {
@@ -152,25 +152,11 @@ const VisualizerInner = () => {
} }
// Player is paused // Player is paused
if (!visualizer) return; if (!visualizerRef.current) return;
// Start 5-second timer // Start 5-second timer
pauseTimerRef.current = setTimeout(() => { pauseTimerRef.current = setTimeout(() => {
if (animationFrameRef.current) { cleanupVisualizer();
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = undefined;
}
if (cycleTimerRef.current) {
clearInterval(cycleTimerRef.current);
cycleTimerRef.current = undefined;
}
if (resizeObserverRef.current) {
resizeObserverRef.current.disconnect();
resizeObserverRef.current = undefined;
}
// Destroy visualizer
setVisualizer(undefined);
pauseTimerRef.current = undefined; pauseTimerRef.current = undefined;
}, 5000); }, 5000);
@@ -180,11 +166,12 @@ const VisualizerInner = () => {
pauseTimerRef.current = undefined; pauseTimerRef.current = undefined;
} }
}; };
}, [isPlaying, visualizer]); }, [isPlaying]);
// Handle resize // Handle resize
useEffect(() => { useEffect(() => {
const container = containerRef.current; const container = containerRef.current;
const visualizer = visualizerRef.current;
if (!container || !visualizer) return; if (!container || !visualizer) return;
const handleResize = () => { const handleResize = () => {
@@ -207,16 +194,45 @@ const VisualizerInner = () => {
return () => { return () => {
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
if (resizeObserverRef.current) {
resizeObserverRef.current.disconnect();
resizeObserverRef.current = undefined;
}
}; };
}, [visualizer, containerRef, canvasRef]); }, [isVisualizerReady, canvasRef, containerRef]);
// Load initial preset when visualizer is ready
useEffect(() => {
const visualizer = visualizerRef.current;
if (!visualizer || !isVisualizerReady || initialPresetLoadedRef.current) return;
const presets = butterchurnPresets;
const presetNames = Object.keys(presets);
if (presetNames.length > 0) {
const presetName =
butterchurnSettings.currentPreset && presets[butterchurnSettings.currentPreset]
? butterchurnSettings.currentPreset
: presetNames[0];
const preset = presets[presetName];
if (preset) {
visualizer.loadPreset(preset, butterchurnSettings.blendTime || 0.0);
cycleStartTimeRef.current = Date.now();
initialPresetLoadedRef.current = true;
}
}
}, [isVisualizerReady, butterchurnSettings.currentPreset, butterchurnSettings.blendTime]);
// Update preset when currentPreset or blendTime changes (but not when cycling) // Update preset when currentPreset or blendTime changes (but not when cycling)
const isCyclingRef = useRef(false); const isCyclingRef = useRef(false);
useEffect(() => { useEffect(() => {
if (!visualizer || !butterchurnSettings.currentPreset) return; const visualizer = visualizerRef.current;
if (!visualizer || !butterchurnSettings.currentPreset || !initialPresetLoadedRef.current)
return;
// Skip if we're currently cycling (to avoid recreating the visualizer) // Skip if we're currently cycling (to avoid reloading preset)
if (isCyclingRef.current) { if (isCyclingRef.current) {
isCyclingRef.current = false; isCyclingRef.current = false;
return; return;
@@ -230,12 +246,13 @@ const VisualizerInner = () => {
// Reset cycle timer when preset changes manually // Reset cycle timer when preset changes manually
cycleStartTimeRef.current = Date.now(); cycleStartTimeRef.current = Date.now();
} }
}, [visualizer, butterchurnSettings.currentPreset, butterchurnSettings.blendTime]); }, [butterchurnSettings.currentPreset, butterchurnSettings.blendTime]);
// Handle preset cycling // Handle preset cycling
useEffect(() => { useEffect(() => {
if (!visualizer || !butterchurnSettings.cyclePresets) { const visualizer = visualizerRef.current;
// Clear cycle timer if cycling is disabled if (!visualizer || !butterchurnSettings.cyclePresets || !initialPresetLoadedRef.current) {
// Clear cycle timer if cycling is disabled or visualizer not ready
if (cycleTimerRef.current) { if (cycleTimerRef.current) {
clearInterval(cycleTimerRef.current); clearInterval(cycleTimerRef.current);
cycleTimerRef.current = undefined; cycleTimerRef.current = undefined;
@@ -266,7 +283,8 @@ const VisualizerInner = () => {
cycleStartTimeRef.current = Date.now(); cycleStartTimeRef.current = Date.now();
const cycleToNextPreset = () => { const cycleToNextPreset = () => {
if (!visualizer) return; const currentVisualizer = visualizerRef.current;
if (!currentVisualizer) return;
const currentPresetName = butterchurnSettings.currentPreset; const currentPresetName = butterchurnSettings.currentPreset;
let nextPresetName: string; let nextPresetName: string;
@@ -298,7 +316,7 @@ const VisualizerInner = () => {
isCyclingRef.current = true; isCyclingRef.current = true;
// Load the preset with blending // Load the preset with blending
visualizer.loadPreset(nextPreset, currentSettings.blendTime || 0.0); currentVisualizer.loadPreset(nextPreset, currentSettings.blendTime || 0.0);
// Update currentPreset in settings // Update currentPreset in settings
setSettings({ setSettings({
@@ -331,18 +349,38 @@ const VisualizerInner = () => {
cycleTimerRef.current = undefined; cycleTimerRef.current = undefined;
} }
}; };
}, [visualizer, butterchurnSettings, setSettings]); }, [
isVisualizerReady,
butterchurnSettings.cyclePresets,
butterchurnSettings.cycleTime,
butterchurnSettings.includeAllPresets,
butterchurnSettings.selectedPresets,
butterchurnSettings.ignoredPresets,
butterchurnSettings.randomizeNextPreset,
butterchurnSettings.currentPreset,
setSettings,
]);
useEffect(() => { useEffect(() => {
if (!visualizer) return; const visualizer = visualizerRef.current;
if (!visualizer || !isVisualizerReady) return;
let lastFrameTime = 0; let lastFrameTime = 0;
const maxFPS = butterchurnSettings.maxFPS; const maxFPS = butterchurnSettings.maxFPS;
const minFrameInterval = maxFPS > 0 ? 1000 / maxFPS : 0; const minFrameInterval = maxFPS > 0 ? 1000 / maxFPS : 0;
const render = (currentTime: number) => { const render = (currentTime: number) => {
const currentVisualizer = visualizerRef.current;
if (!currentVisualizer) {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = undefined;
}
return;
}
if (maxFPS === 0 || currentTime - lastFrameTime >= minFrameInterval) { if (maxFPS === 0 || currentTime - lastFrameTime >= minFrameInterval) {
visualizer.render(); currentVisualizer.render();
lastFrameTime = currentTime; lastFrameTime = currentTime;
} }
animationFrameRef.current = requestAnimationFrame(render); animationFrameRef.current = requestAnimationFrame(render);
@@ -353,13 +391,14 @@ const VisualizerInner = () => {
return () => { return () => {
if (animationFrameRef.current) { if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current); cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = undefined;
} }
}; };
}, [visualizer, butterchurnSettings.maxFPS]); }, [isVisualizerReady, butterchurnSettings.maxFPS]);
// Render container when playing (for initialization) or when visualizer exists // Render container when playing (for initialization) or when visualizer exists
// Canvas must always be rendered when container is rendered so refs are available // Canvas must always be rendered when container is rendered so refs are available
const shouldRenderContainer = isPlaying || visualizer; const shouldRenderContainer = isPlaying || isVisualizerReady;
if (!shouldRenderContainer) { if (!shouldRenderContainer) {
return null; return null;
@@ -369,10 +408,10 @@ const VisualizerInner = () => {
<div <div
className={styles.container} className={styles.container}
ref={containerRef} ref={containerRef}
style={{ opacity: visualizer ? opacity : 0 }} style={{ opacity: isVisualizerReady ? opacity : 0 }}
> >
<canvas className={styles.canvas} ref={canvasRef} /> <canvas className={styles.canvas} ref={canvasRef} />
{visualizer && butterchurnSettings.currentPreset && ( {isVisualizerReady && butterchurnSettings.currentPreset && (
<Text className={styles['preset-overlay']} isNoSelect size="sm"> <Text className={styles['preset-overlay']} isNoSelect size="sm">
{butterchurnSettings.currentPreset} {butterchurnSettings.currentPreset}
</Text> </Text>