From ff272a5385c5f67a054f7af305b6e865b8885091 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 4 Jan 2026 02:35:44 -0800 Subject: [PATCH] fix butterchurn init and cleanup --- .../visualizer-settings-form.tsx | 2 - .../components/butternchurn/visualizer.tsx | 281 ++++++++++-------- 2 files changed, 160 insertions(+), 123 deletions(-) diff --git a/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx b/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx index 5143edb08..d19cb75b6 100644 --- a/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx +++ b/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx @@ -2110,8 +2110,6 @@ const ButterChurnCycleSettings = () => { const { t } = useTranslation(); const { updateProperty, visualizer } = useUpdateButterchurn(); - console.log(butterchurnPresets, 'number of presets'); - const presetOptions = useMemo(() => { const presets = butterchurnPresets; return Object.keys(presets).map((presetName) => ({ diff --git a/src/renderer/features/visualizer/components/butternchurn/visualizer.tsx b/src/renderer/features/visualizer/components/butternchurn/visualizer.tsx index dbe759742..8208e9be4 100644 --- a/src/renderer/features/visualizer/components/butternchurn/visualizer.tsx +++ b/src/renderer/features/visualizer/components/butternchurn/visualizer.tsx @@ -24,121 +24,121 @@ const VisualizerInner = () => { const { webAudio } = useWebAudio(); const canvasRef = createRef(); const containerRef = createRef(); - const [visualizer, setVisualizer] = useState(); + const visualizerRef = useRef(undefined); + const isInitializedRef = useRef(false); + const [isVisualizerReady, setIsVisualizerReady] = useState(false); const animationFrameRef = useRef(undefined); const resizeObserverRef = useRef(undefined); const cycleTimerRef = useRef(undefined); const cycleStartTimeRef = useRef(undefined); const pauseTimerRef = useRef(undefined); + const initialPresetLoadedRef = useRef(false); const butterchurnSettings = useSettingsStore((store) => store.visualizer.butterchurn); const opacity = useSettingsStore((store) => store.visualizer.butterchurn.opacity); const { setSettings } = useSettingsStoreActions(); const playerStatus = usePlayerStatus(); 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(() => { const { context, gains } = webAudio || {}; - if ( + const canvas = canvasRef.current; + const container = containerRef.current; + + const needsInitialization = context && gains && - canvasRef.current && - containerRef.current && - !visualizer && - isPlaying - ) { - const canvas = canvasRef.current; - const container = containerRef.current; + gains.length > 0 && + canvas && + container && + isPlaying && + (!isInitializedRef.current || !visualizerRef.current); - const getDimensions = () => { - const rect = container.getBoundingClientRect(); - return { - height: rect.height || 600, - width: rect.width || 800, - }; + if (!needsInitialization) { + return; + } + + 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.width === 0 || dimensions.height === 0) { - requestAnimationFrame(() => { - dimensions = getDimensions(); - if (dimensions.width > 0 && dimensions.height > 0) { - 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); + // If dimensions are 0, wait for next frame + if (dimensions.width === 0 || dimensions.height === 0) { + requestAnimationFrame(() => { + dimensions = getDimensions(); + if (dimensions.width > 0 && dimensions.height > 0) { + initializeVisualizer(dimensions.width, dimensions.height); } + }); + } 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 () => { - 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; - } - - if (visualizer) { - setVisualizer(undefined); - } + // Cleanup on unmount or when webAudio changes + cleanupVisualizer(); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [webAudio, canvasRef, containerRef, visualizer, isPlaying]); + }, [webAudio, isPlaying]); // Kill visualizer after 5 seconds of pause useEffect(() => { @@ -152,25 +152,11 @@ const VisualizerInner = () => { } // Player is paused - if (!visualizer) return; + if (!visualizerRef.current) return; // Start 5-second timer pauseTimerRef.current = setTimeout(() => { - if (animationFrameRef.current) { - 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); + cleanupVisualizer(); pauseTimerRef.current = undefined; }, 5000); @@ -180,11 +166,12 @@ const VisualizerInner = () => { pauseTimerRef.current = undefined; } }; - }, [isPlaying, visualizer]); + }, [isPlaying]); // Handle resize useEffect(() => { const container = containerRef.current; + const visualizer = visualizerRef.current; if (!container || !visualizer) return; const handleResize = () => { @@ -207,16 +194,45 @@ const VisualizerInner = () => { return () => { 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) const isCyclingRef = useRef(false); 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) { isCyclingRef.current = false; return; @@ -230,12 +246,13 @@ const VisualizerInner = () => { // Reset cycle timer when preset changes manually cycleStartTimeRef.current = Date.now(); } - }, [visualizer, butterchurnSettings.currentPreset, butterchurnSettings.blendTime]); + }, [butterchurnSettings.currentPreset, butterchurnSettings.blendTime]); // Handle preset cycling useEffect(() => { - if (!visualizer || !butterchurnSettings.cyclePresets) { - // Clear cycle timer if cycling is disabled + const visualizer = visualizerRef.current; + if (!visualizer || !butterchurnSettings.cyclePresets || !initialPresetLoadedRef.current) { + // Clear cycle timer if cycling is disabled or visualizer not ready if (cycleTimerRef.current) { clearInterval(cycleTimerRef.current); cycleTimerRef.current = undefined; @@ -266,7 +283,8 @@ const VisualizerInner = () => { cycleStartTimeRef.current = Date.now(); const cycleToNextPreset = () => { - if (!visualizer) return; + const currentVisualizer = visualizerRef.current; + if (!currentVisualizer) return; const currentPresetName = butterchurnSettings.currentPreset; let nextPresetName: string; @@ -298,7 +316,7 @@ const VisualizerInner = () => { isCyclingRef.current = true; // Load the preset with blending - visualizer.loadPreset(nextPreset, currentSettings.blendTime || 0.0); + currentVisualizer.loadPreset(nextPreset, currentSettings.blendTime || 0.0); // Update currentPreset in settings setSettings({ @@ -331,18 +349,38 @@ const VisualizerInner = () => { cycleTimerRef.current = undefined; } }; - }, [visualizer, butterchurnSettings, setSettings]); + }, [ + isVisualizerReady, + butterchurnSettings.cyclePresets, + butterchurnSettings.cycleTime, + butterchurnSettings.includeAllPresets, + butterchurnSettings.selectedPresets, + butterchurnSettings.ignoredPresets, + butterchurnSettings.randomizeNextPreset, + butterchurnSettings.currentPreset, + setSettings, + ]); useEffect(() => { - if (!visualizer) return; + const visualizer = visualizerRef.current; + if (!visualizer || !isVisualizerReady) return; let lastFrameTime = 0; const maxFPS = butterchurnSettings.maxFPS; const minFrameInterval = maxFPS > 0 ? 1000 / maxFPS : 0; 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) { - visualizer.render(); + currentVisualizer.render(); lastFrameTime = currentTime; } animationFrameRef.current = requestAnimationFrame(render); @@ -353,13 +391,14 @@ const VisualizerInner = () => { return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = undefined; } }; - }, [visualizer, butterchurnSettings.maxFPS]); + }, [isVisualizerReady, butterchurnSettings.maxFPS]); // Render container when playing (for initialization) or when visualizer exists // Canvas must always be rendered when container is rendered so refs are available - const shouldRenderContainer = isPlaying || visualizer; + const shouldRenderContainer = isPlaying || isVisualizerReady; if (!shouldRenderContainer) { return null; @@ -369,10 +408,10 @@ const VisualizerInner = () => {
- {visualizer && butterchurnSettings.currentPreset && ( + {isVisualizerReady && butterchurnSettings.currentPreset && ( {butterchurnSettings.currentPreset}