diff --git a/bc-demo.html b/bc-demo.html new file mode 100644 index 0000000..0932107 --- /dev/null +++ b/bc-demo.html @@ -0,0 +1,241 @@ + + + + + + + Butterchurn Demo Fixed + + + + + + + +
+
+ + +
+ +
+ + + + +
+ + +
+ + + + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html index b567c91..25dd978 100644 --- a/index.html +++ b/index.html @@ -1600,6 +1600,44 @@ style="font-size: 0.9rem; min-width: 3em; text-align: right">60% + + + + + diff --git a/js/audio-context.js b/js/audio-context.js index 61ca4b8..000aa7a 100644 --- a/js/audio-context.js +++ b/js/audio-context.js @@ -163,7 +163,13 @@ class AudioContextManager { // Disconnect everything first this.source.disconnect(); this.outputNode.disconnect(); - this.analyser.disconnect(); + + // Only disconnect destination from analyser to preserve other taps (like Butterchurn) + try { + this.analyser.disconnect(this.audioContext.destination); + } catch (e) { + // Ignore if not connected + } if (this.isEQEnabled && this.filters.length > 0) { // EQ enabled: source -> EQ filters -> output -> analyser -> destination diff --git a/js/settings.js b/js/settings.js index 2d8d8dc..23ae14a 100644 --- a/js/settings.js +++ b/js/settings.js @@ -857,6 +857,62 @@ export function initializeSettings(scrobbler, player, api, ui) { const visualizerSmartIntensitySetting = document.getElementById('visualizer-smart-intensity-setting'); const visualizerSensitivitySetting = document.getElementById('visualizer-sensitivity-setting'); const visualizerPresetSetting = document.getElementById('visualizer-preset-setting'); + const visualizerPresetSelect = document.getElementById('visualizer-preset-select'); + + // Butterchurn Settings Elements + const butterchurnCycleSetting = document.getElementById('butterchurn-cycle-setting'); + const butterchurnDurationSetting = document.getElementById('butterchurn-duration-setting'); + const butterchurnRandomizeSetting = document.getElementById('butterchurn-randomize-setting'); + const butterchurnSpecificPresetSetting = document.getElementById('butterchurn-specific-preset-setting'); + const butterchurnSpecificPresetSelect = document.getElementById('butterchurn-specific-preset-select'); + const butterchurnCycleToggle = document.getElementById('butterchurn-cycle-toggle'); + const butterchurnDurationInput = document.getElementById('butterchurn-duration-input'); + const butterchurnRandomizeToggle = document.getElementById('butterchurn-randomize-toggle'); + + const updateButterchurnSettingsVisibility = () => { + const isEnabled = visualizerEnabledToggle ? visualizerEnabledToggle.checked : false; + const isButterchurn = visualizerPresetSelect ? visualizerPresetSelect.value === 'butterchurn' : false; + const show = isEnabled && isButterchurn; + + if (butterchurnCycleSetting) butterchurnCycleSetting.style.display = show ? 'flex' : 'none'; + if (butterchurnSpecificPresetSetting) butterchurnSpecificPresetSetting.style.display = show ? 'flex' : 'none'; + + // Cycle duration and randomize only show if cycle is enabled + const isCycleEnabled = butterchurnCycleToggle ? butterchurnCycleToggle.checked : false; + const showSubSettings = show && isCycleEnabled; + + if (butterchurnDurationSetting) butterchurnDurationSetting.style.display = showSubSettings ? 'flex' : 'none'; + if (butterchurnRandomizeSetting) butterchurnRandomizeSetting.style.display = showSubSettings ? 'flex' : 'none'; + + // Populate preset list if visible + if (show && ui && ui.visualizer && ui.visualizer.presets['butterchurn']) { + const preset = ui.visualizer.presets['butterchurn']; + const select = butterchurnSpecificPresetSelect; + + // Only populate if needed (to avoid resetting selection or heavy DOM ops) + if (select && select.options.length <= 1 && preset.getPresetNames && preset.getPresetNames().length > 0) { + const names = preset.getPresetNames(); + select.innerHTML = ''; + names.forEach(name => { + const option = document.createElement('option'); + option.value = name; + option.textContent = name; + select.appendChild(option); + }); + + // Select current + if (preset.getCurrentPresetName) { + select.value = preset.getCurrentPresetName(); + } + } else if (select && preset.getCurrentPresetName) { + // Just update selection if list already populated + const current = preset.getCurrentPresetName(); + if (select.value !== current) { + select.value = current; + } + } + } + }; const updateVisualizerSettingsVisibility = (enabled) => { const display = enabled ? 'flex' : 'none'; @@ -864,10 +920,19 @@ export function initializeSettings(scrobbler, player, api, ui) { if (visualizerSmartIntensitySetting) visualizerSmartIntensitySetting.style.display = display; if (visualizerSensitivitySetting) visualizerSensitivitySetting.style.display = display; if (visualizerPresetSetting) visualizerPresetSetting.style.display = display; + + // Also update Butterchurn specific visibility + updateButterchurnSettingsVisibility(); }; + // Initialize preset select value early so visibility logic works correctly on load + if (visualizerPresetSelect) { + visualizerPresetSelect.value = visualizerSettings.getPreset(); + } + if (visualizerEnabledToggle) { visualizerEnabledToggle.checked = visualizerSettings.isEnabled(); + updateVisualizerSettingsVisibility(visualizerEnabledToggle.checked); visualizerEnabledToggle.addEventListener('change', (e) => { @@ -877,21 +942,57 @@ export function initializeSettings(scrobbler, player, api, ui) { } // Visualizer Preset Select - const visualizerPresetSelect = document.getElementById('visualizer-preset-select'); if (visualizerPresetSelect) { - visualizerPresetSelect.value = visualizerSettings.getPreset(); + // value set above visualizerPresetSelect.addEventListener('change', (e) => { const val = e.target.value; visualizerSettings.setPreset(val); - // Assuming 'ui' has access to 'visualizer' instance or we need to find it - // 'ui' is passed to initializeSettings. - // In ui.js, 'visualizer' is a property of UIRenderer. if (ui && ui.visualizer) { ui.visualizer.setPreset(val); } + updateButterchurnSettingsVisibility(); }); } + if (butterchurnCycleToggle) { + butterchurnCycleToggle.checked = visualizerSettings.isButterchurnCycleEnabled(); + butterchurnCycleToggle.addEventListener('change', (e) => { + visualizerSettings.setButterchurnCycleEnabled(e.target.checked); + updateButterchurnSettingsVisibility(); + }); + } + + if (butterchurnDurationInput) { + butterchurnDurationInput.value = visualizerSettings.getButterchurnCycleDuration(); + butterchurnDurationInput.addEventListener('change', (e) => { + let val = parseInt(e.target.value, 10); + if (isNaN(val) || val < 5) val = 5; + if (val > 300) val = 300; + e.target.value = val; + visualizerSettings.setButterchurnCycleDuration(val); + }); + } + + if (butterchurnRandomizeToggle) { + butterchurnRandomizeToggle.checked = visualizerSettings.isButterchurnRandomizeEnabled(); + butterchurnRandomizeToggle.addEventListener('change', (e) => { + visualizerSettings.setButterchurnRandomizeEnabled(e.target.checked); + }); + } + + if (butterchurnSpecificPresetSelect) { + butterchurnSpecificPresetSelect.addEventListener('change', (e) => { + if (ui && ui.visualizer && ui.visualizer.presets['butterchurn']) { + ui.visualizer.presets['butterchurn'].loadPreset(e.target.value); + } + }); + } + + // Refresh settings when presets are loaded asynchronously + window.addEventListener('butterchurn-presets-loaded', () => { + updateButterchurnSettingsVisibility(); + }); + // Visualizer Mode Select const visualizerModeSelect = document.getElementById('visualizer-mode-select'); if (visualizerModeSelect) { diff --git a/js/storage.js b/js/storage.js index b8988bd..88f79bd 100644 --- a/js/storage.js +++ b/js/storage.js @@ -701,7 +701,7 @@ export const visualizerSettings = { localStorage.setItem(this.SMART_INTENSITY_KEY, enabled); }, - // Butterchurn preset cycle duration in seconds (0 = disabled) + // Butterchurn preset cycle duration in seconds getButterchurnCycleDuration() { try { const val = localStorage.getItem(this.BUTTERCHURN_CYCLE_KEY); @@ -714,6 +714,32 @@ export const visualizerSettings = { setButterchurnCycleDuration(seconds) { localStorage.setItem(this.BUTTERCHURN_CYCLE_KEY, seconds.toString()); }, + + // Butterchurn cycle enabled + isButterchurnCycleEnabled() { + try { + return localStorage.getItem('butterchurn-cycle-enabled') !== 'false'; + } catch { + return true; + } + }, + + setButterchurnCycleEnabled(enabled) { + localStorage.setItem('butterchurn-cycle-enabled', enabled); + }, + + // Butterchurn randomize preset + isButterchurnRandomizeEnabled() { + try { + return localStorage.getItem('butterchurn-randomize-enabled') !== 'false'; + } catch { + return true; + } + }, + + setButterchurnRandomizeEnabled(enabled) { + localStorage.setItem('butterchurn-randomize-enabled', enabled); + }, }; export const equalizerSettings = { diff --git a/js/visualizer.js b/js/visualizer.js index fcc7e9d..d0a70af 100644 --- a/js/visualizer.js +++ b/js/visualizer.js @@ -146,7 +146,7 @@ export class Visualizer { this.activePreset.lazyInit( this.canvas, this.audioContext, - audioContextManager.source + this.analyser ); } @@ -275,7 +275,7 @@ export class Visualizer { this.presets[key].lazyInit( this.canvas, this.audioContext, - audioContextManager.source + this.analyser ); } } diff --git a/js/visualizers/butterchurn.js b/js/visualizers/butterchurn.js index 7a973f8..70514c8 100644 --- a/js/visualizers/butterchurn.js +++ b/js/visualizers/butterchurn.js @@ -3,7 +3,6 @@ * WebGL-based audio visualization using the Butterchurn library */ import butterchurn from 'butterchurn'; -import butterchurnPresets from 'butterchurn-presets'; import { visualizerSettings } from '../storage.js'; export class ButterchurnPreset { @@ -14,15 +13,65 @@ export class ButterchurnPreset { this.visualizer = null; this.canvas = null; this.audioContext = null; - this.presets = null; - this.presetKeys = []; this.currentPresetIndex = 0; this.lastPresetChange = 0; this.isInitialized = false; + this.presets = {}; + this.presetKeys = []; + this.isLoadingPresets = false; + // Transition settings this.blendProgress = 0; this.blendDuration = 2.7; // seconds for preset transitions + + // Load presets asynchronously + this.loadPresets(); + } + + /** + * Load presets dynamically to avoid blocking main bundle + */ + async loadPresets() { + if (this.isLoadingPresets) return; + this.isLoadingPresets = true; + + try { + const module = await import('butterchurn-presets'); + const presets = module.default.getPresets(); + + this.presets = presets; + this.presetKeys = Object.keys(this.presets); + + // Filter to get a good selection of presets + this.presetKeys = this.presetKeys.filter(key => { + const skipPatterns = ['flexi', 'empty', 'test', '_']; + return !skipPatterns.some(pattern => key.toLowerCase().includes(pattern)); + }); + + if (this.presetKeys.length === 0) { + this.presetKeys = Object.keys(this.presets); + } + + // Shuffle presets for variety + this.shufflePresets(); + + console.log('[Butterchurn] Presets loaded:', this.presetKeys.length); + + // Notify system that presets are ready + window.dispatchEvent(new CustomEvent('butterchurn-presets-loaded')); + + // If initialized (visualizer ready), load a preset immediately + if (this.isInitialized && this.visualizer) { + this.loadNextPreset(); + } + } catch (e) { + console.error('[Butterchurn] Failed to load presets:', e); + this.presets = {}; + this.presetKeys = []; + } finally { + this.isLoadingPresets = false; + } } /** @@ -43,24 +92,6 @@ export class ButterchurnPreset { this.canvas = canvas; this.audioContext = audioContext; - // Load presets - this.presets = butterchurnPresets.getPresets(); - this.presetKeys = Object.keys(this.presets); - - // Filter to get a good selection of presets (some are better than others) - this.presetKeys = this.presetKeys.filter(key => { - // Skip some problematic or less visually appealing presets - const skipPatterns = ['flexi', 'empty', 'test', '_']; - return !skipPatterns.some(pattern => key.toLowerCase().includes(pattern)); - }); - - if (this.presetKeys.length === 0) { - this.presetKeys = Object.keys(this.presets); - } - - // Shuffle presets for variety - this.shufflePresets(); - // Create Butterchurn visualizer this.visualizer = butterchurn.createVisualizer(audioContext, canvas, { width: canvas.width, @@ -75,7 +106,7 @@ export class ButterchurnPreset { } // Load initial preset - this.loadRandomPreset(); + this.loadNextPreset(); this.lastPresetChange = performance.now(); this.isInitialized = true; @@ -97,25 +128,40 @@ export class ButterchurnPreset { } /** - * Load a random preset with smooth transition + * Load next preset based on settings (sequential or random) */ - loadRandomPreset() { + loadNextPreset() { if (!this.visualizer || this.presetKeys.length === 0) return; - this.currentPresetIndex = (this.currentPresetIndex + 1) % this.presetKeys.length; + // If cycle enabled is false, don't change preset automatically unless forced (e.g. init or manual next) + // But here we are just loading 'a' preset. + // The cycling logic is in draw(). + + // Wait, loadNextPreset is general. + // Let's check settings inside loadNextPreset? + // No, loadNextPreset is an action. It should just do it. + // The caller decides when. + + const randomize = visualizerSettings.isButterchurnRandomizeEnabled(); + + if (randomize) { + this.currentPresetIndex = Math.floor(Math.random() * this.presetKeys.length); + } else { + this.currentPresetIndex = (this.currentPresetIndex + 1) % this.presetKeys.length; + } + const presetKey = this.presetKeys[this.currentPresetIndex]; const preset = this.presets[presetKey]; if (preset) { try { this.visualizer.loadPreset(preset, this.blendDuration); - console.log('[Butterchurn] Loaded preset:', presetKey); + // console.log('[Butterchurn] Loaded preset:', presetKey); } catch (error) { console.warn('[Butterchurn] Failed to load preset:', presetKey, error); // Try next preset if (this.presetKeys.length > 1) { - this.presetKeys.splice(this.currentPresetIndex, 1); - this.loadRandomPreset(); + this.loadNextPreset(); } } } @@ -131,6 +177,12 @@ export class ButterchurnPreset { if (preset) { this.visualizer.loadPreset(preset, this.blendDuration); console.log('[Butterchurn] Loaded preset:', presetName); + + // Update current index if found + const index = this.presetKeys.indexOf(presetName); + if (index !== -1) { + this.currentPresetIndex = index; + } } } @@ -149,10 +201,10 @@ export class ButterchurnPreset { } /** - * Skip to next preset + * Skip to next preset (manually triggered) */ nextPreset() { - this.loadRandomPreset(); + this.loadNextPreset(); this.lastPresetChange = performance.now(); } @@ -180,11 +232,14 @@ export class ButterchurnPreset { const { mode } = params; const now = performance.now(); - // Auto-cycle presets (if cycle duration > 0) - const cycleDuration = this.getPresetDuration(); - if (cycleDuration > 0 && now - this.lastPresetChange > cycleDuration) { - this.loadRandomPreset(); - this.lastPresetChange = now; + // Auto-cycle presets + const isCycleEnabled = visualizerSettings.isButterchurnCycleEnabled(); + if (isCycleEnabled) { + const cycleDuration = this.getPresetDuration(); + if (cycleDuration > 0 && now - this.lastPresetChange > cycleDuration) { + this.loadNextPreset(); + this.lastPresetChange = now; + } } // Render the visualization