(final) implement butterchurn
This commit is contained in:
parent
cc6c600817
commit
4636ae3151
7 changed files with 511 additions and 44 deletions
241
bc-demo.html
Normal file
241
bc-demo.html
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>Butterchurn Demo Fixed</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/normalize.css/normalize.css" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: black;
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#canvas {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#audioSelectWrapper,
|
||||||
|
#presetControls {
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
#presetSelect {
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#presetCycleLength {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="mainWrapper">
|
||||||
|
<div id="audioSelectWrapper">
|
||||||
|
<button id="localFileBut">Load local files</button>
|
||||||
|
<button id="micSelect">Use Mic</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="presetControls">
|
||||||
|
<label>Preset: <select id="presetSelect"></select></label>
|
||||||
|
<label>Cycle: <input type="checkbox" id="presetCycle" checked></label>
|
||||||
|
<label>Seconds: <input type="number" id="presetCycleLength" value="15" min="1"></label>
|
||||||
|
<label>Random: <input type="checkbox" id="presetRandom" checked></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<canvas id="canvas" width="800" height="600"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Load Butterchurn and presets -->
|
||||||
|
<script type="module">
|
||||||
|
import butterchurn from 'https://unpkg.com/butterchurn@3.0.0-beta.5/dist/butterchurn.js';
|
||||||
|
|
||||||
|
let audioContext = null;
|
||||||
|
let visualizer = null;
|
||||||
|
let sourceNode = null;
|
||||||
|
let delayedAudible = null;
|
||||||
|
let cycleInterval = null;
|
||||||
|
|
||||||
|
let presets = {};
|
||||||
|
let presetKeys = [];
|
||||||
|
let presetIndexHist = [];
|
||||||
|
let presetIndex = 0;
|
||||||
|
let presetCycle = true;
|
||||||
|
let presetCycleLength = 15000;
|
||||||
|
let presetRandom = true;
|
||||||
|
|
||||||
|
const canvas = document.getElementById('canvas');
|
||||||
|
|
||||||
|
function startRenderer() {
|
||||||
|
requestAnimationFrame(startRenderer);
|
||||||
|
visualizer.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectToAudioAnalyzer(node) {
|
||||||
|
if (delayedAudible) delayedAudible.disconnect();
|
||||||
|
delayedAudible = audioContext.createDelay();
|
||||||
|
delayedAudible.delayTime.value = 0.26;
|
||||||
|
node.connect(delayedAudible);
|
||||||
|
delayedAudible.connect(audioContext.destination);
|
||||||
|
visualizer.connectAudio(delayedAudible);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playBufferSource(buffer) {
|
||||||
|
if (sourceNode) sourceNode.disconnect();
|
||||||
|
sourceNode = audioContext.createBufferSource();
|
||||||
|
sourceNode.buffer = buffer;
|
||||||
|
connectToAudioAnalyzer(sourceNode);
|
||||||
|
sourceNode.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLocalFiles(files, index = 0) {
|
||||||
|
audioContext.resume();
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
audioContext.decodeAudioData(e.target.result, buffer => {
|
||||||
|
playBufferSource(buffer);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (files.length > index + 1) loadLocalFiles(files, index + 1);
|
||||||
|
}, buffer.duration * 1000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(files[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectMicAudio(stream) {
|
||||||
|
audioContext.resume();
|
||||||
|
const micSource = audioContext.createMediaStreamSource(stream);
|
||||||
|
const gainNode = audioContext.createGain();
|
||||||
|
gainNode.gain.value = 1.25;
|
||||||
|
micSource.connect(gainNode);
|
||||||
|
visualizer.connectAudio(gainNode);
|
||||||
|
startRenderer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextPreset(blendTime = 5.7) {
|
||||||
|
presetIndexHist.push(presetIndex);
|
||||||
|
if (presetRandom) presetIndex = Math.floor(Math.random() * presetKeys.length);
|
||||||
|
else presetIndex = (presetIndex + 1) % presetKeys.length;
|
||||||
|
visualizer.loadPreset(presets[presetKeys[presetIndex]], blendTime);
|
||||||
|
document.getElementById('presetSelect').value = presetIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevPreset(blendTime = 5.7) {
|
||||||
|
if (presetIndexHist.length > 0) presetIndex = presetIndexHist.pop();
|
||||||
|
else presetIndex = ((presetIndex - 1) + presetKeys.length) % presetKeys.length;
|
||||||
|
visualizer.loadPreset(presets[presetKeys[presetIndex]], blendTime);
|
||||||
|
document.getElementById('presetSelect').value = presetIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function restartCycleInterval() {
|
||||||
|
if (cycleInterval) clearInterval(cycleInterval);
|
||||||
|
if (presetCycle) cycleInterval = setInterval(() => nextPreset(2.7), presetCycleLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initPlayer() {
|
||||||
|
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
|
||||||
|
// Load presets from window globals
|
||||||
|
presets = { ...(window.base?.default || {}), ...(window.extra?.default || {}) };
|
||||||
|
presets = Object.fromEntries(
|
||||||
|
Object.entries(presets).sort(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase()))
|
||||||
|
);
|
||||||
|
presetKeys = Object.keys(presets);
|
||||||
|
presetIndex = Math.floor(Math.random() * presetKeys.length);
|
||||||
|
|
||||||
|
// Populate preset select
|
||||||
|
const presetSelect = document.getElementById('presetSelect');
|
||||||
|
presetKeys.forEach((k, i) => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = i;
|
||||||
|
opt.textContent = k.length > 60 ? k.substring(0, 60) + '…' : k;
|
||||||
|
presetSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
|
||||||
|
visualizer = butterchurn.createVisualizer(audioContext, canvas, {
|
||||||
|
width: canvas.width,
|
||||||
|
height: canvas.height,
|
||||||
|
pixelRatio: window.devicePixelRatio || 1,
|
||||||
|
textureRatio: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
nextPreset(0);
|
||||||
|
startRenderer();
|
||||||
|
restartCycleInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
document.getElementById('localFileBut').onclick = () => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = 'audio/*';
|
||||||
|
input.multiple = true;
|
||||||
|
input.onchange = e => loadLocalFiles(e.target.files);
|
||||||
|
input.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('micSelect').onclick = async () => {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
connectMicAudio(stream);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Microphone error', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('presetSelect').onchange = () => {
|
||||||
|
presetIndexHist.push(presetIndex);
|
||||||
|
presetIndex = parseInt(document.getElementById('presetSelect').value);
|
||||||
|
visualizer.loadPreset(presets[presetKeys[presetIndex]], 5.7);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('presetCycle').onchange = e => {
|
||||||
|
presetCycle = e.target.checked;
|
||||||
|
restartCycleInterval();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('presetCycleLength').onchange = e => {
|
||||||
|
presetCycleLength = parseInt(e.target.value) * 1000;
|
||||||
|
restartCycleInterval();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('presetRandom').onchange = e => {
|
||||||
|
presetRandom = e.target.checked;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start on first user click (required for autoplay policies)
|
||||||
|
document.body.addEventListener('click', () => {
|
||||||
|
if (!audioContext) initPlayer();
|
||||||
|
}, { once: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Load Butterchurn presets as global scripts -->
|
||||||
|
<script src="https://unpkg.com/butterchurn-presets@3.0.0-beta.4/dist/base.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/butterchurn-presets@3.0.0-beta.4/dist/extra.min.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
38
index.html
38
index.html
|
|
@ -1600,6 +1600,44 @@
|
||||||
style="font-size: 0.9rem; min-width: 3em; text-align: right">60%</span>
|
style="font-size: 0.9rem; min-width: 3em; text-align: right">60%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Butterchurn Settings -->
|
||||||
|
<div class="setting-item" id="butterchurn-cycle-setting" style="display: none">
|
||||||
|
<div class="info">
|
||||||
|
<span class="label">Cycle Presets</span>
|
||||||
|
<span class="description">Automatically change visualizer presets</span>
|
||||||
|
</div>
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="butterchurn-cycle-toggle" />
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item" id="butterchurn-specific-preset-setting" style="display: none">
|
||||||
|
<div class="info">
|
||||||
|
<span class="label">Current Preset</span>
|
||||||
|
<span class="description">Select a specific Butterchurn preset</span>
|
||||||
|
</div>
|
||||||
|
<select id="butterchurn-specific-preset-select" style="width: 200px">
|
||||||
|
<option value="">Loading...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item" id="butterchurn-duration-setting" style="display: none">
|
||||||
|
<div class="info">
|
||||||
|
<span class="label">Cycle Duration</span>
|
||||||
|
<span class="description">Seconds between preset changes</span>
|
||||||
|
</div>
|
||||||
|
<input type="number" id="butterchurn-duration-input" min="5" max="300" value="30"
|
||||||
|
class="template-input" style="width: 80px" />
|
||||||
|
</div>
|
||||||
|
<div class="setting-item" id="butterchurn-randomize-setting" style="display: none">
|
||||||
|
<div class="info">
|
||||||
|
<span class="label">Randomize Presets</span>
|
||||||
|
<span class="description">Select next preset randomly instead of sequentially</span>
|
||||||
|
</div>
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="butterchurn-randomize-toggle" />
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,13 @@ class AudioContextManager {
|
||||||
// Disconnect everything first
|
// Disconnect everything first
|
||||||
this.source.disconnect();
|
this.source.disconnect();
|
||||||
this.outputNode.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) {
|
if (this.isEQEnabled && this.filters.length > 0) {
|
||||||
// EQ enabled: source -> EQ filters -> output -> analyser -> destination
|
// EQ enabled: source -> EQ filters -> output -> analyser -> destination
|
||||||
|
|
|
||||||
111
js/settings.js
111
js/settings.js
|
|
@ -857,6 +857,62 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
const visualizerSmartIntensitySetting = document.getElementById('visualizer-smart-intensity-setting');
|
const visualizerSmartIntensitySetting = document.getElementById('visualizer-smart-intensity-setting');
|
||||||
const visualizerSensitivitySetting = document.getElementById('visualizer-sensitivity-setting');
|
const visualizerSensitivitySetting = document.getElementById('visualizer-sensitivity-setting');
|
||||||
const visualizerPresetSetting = document.getElementById('visualizer-preset-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 updateVisualizerSettingsVisibility = (enabled) => {
|
||||||
const display = enabled ? 'flex' : 'none';
|
const display = enabled ? 'flex' : 'none';
|
||||||
|
|
@ -864,10 +920,19 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
if (visualizerSmartIntensitySetting) visualizerSmartIntensitySetting.style.display = display;
|
if (visualizerSmartIntensitySetting) visualizerSmartIntensitySetting.style.display = display;
|
||||||
if (visualizerSensitivitySetting) visualizerSensitivitySetting.style.display = display;
|
if (visualizerSensitivitySetting) visualizerSensitivitySetting.style.display = display;
|
||||||
if (visualizerPresetSetting) visualizerPresetSetting.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) {
|
if (visualizerEnabledToggle) {
|
||||||
visualizerEnabledToggle.checked = visualizerSettings.isEnabled();
|
visualizerEnabledToggle.checked = visualizerSettings.isEnabled();
|
||||||
|
|
||||||
updateVisualizerSettingsVisibility(visualizerEnabledToggle.checked);
|
updateVisualizerSettingsVisibility(visualizerEnabledToggle.checked);
|
||||||
|
|
||||||
visualizerEnabledToggle.addEventListener('change', (e) => {
|
visualizerEnabledToggle.addEventListener('change', (e) => {
|
||||||
|
|
@ -877,21 +942,57 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visualizer Preset Select
|
// Visualizer Preset Select
|
||||||
const visualizerPresetSelect = document.getElementById('visualizer-preset-select');
|
|
||||||
if (visualizerPresetSelect) {
|
if (visualizerPresetSelect) {
|
||||||
visualizerPresetSelect.value = visualizerSettings.getPreset();
|
// value set above
|
||||||
visualizerPresetSelect.addEventListener('change', (e) => {
|
visualizerPresetSelect.addEventListener('change', (e) => {
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
visualizerSettings.setPreset(val);
|
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) {
|
if (ui && ui.visualizer) {
|
||||||
ui.visualizer.setPreset(val);
|
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
|
// Visualizer Mode Select
|
||||||
const visualizerModeSelect = document.getElementById('visualizer-mode-select');
|
const visualizerModeSelect = document.getElementById('visualizer-mode-select');
|
||||||
if (visualizerModeSelect) {
|
if (visualizerModeSelect) {
|
||||||
|
|
|
||||||
|
|
@ -701,7 +701,7 @@ export const visualizerSettings = {
|
||||||
localStorage.setItem(this.SMART_INTENSITY_KEY, enabled);
|
localStorage.setItem(this.SMART_INTENSITY_KEY, enabled);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Butterchurn preset cycle duration in seconds (0 = disabled)
|
// Butterchurn preset cycle duration in seconds
|
||||||
getButterchurnCycleDuration() {
|
getButterchurnCycleDuration() {
|
||||||
try {
|
try {
|
||||||
const val = localStorage.getItem(this.BUTTERCHURN_CYCLE_KEY);
|
const val = localStorage.getItem(this.BUTTERCHURN_CYCLE_KEY);
|
||||||
|
|
@ -714,6 +714,32 @@ export const visualizerSettings = {
|
||||||
setButterchurnCycleDuration(seconds) {
|
setButterchurnCycleDuration(seconds) {
|
||||||
localStorage.setItem(this.BUTTERCHURN_CYCLE_KEY, seconds.toString());
|
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 = {
|
export const equalizerSettings = {
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ export class Visualizer {
|
||||||
this.activePreset.lazyInit(
|
this.activePreset.lazyInit(
|
||||||
this.canvas,
|
this.canvas,
|
||||||
this.audioContext,
|
this.audioContext,
|
||||||
audioContextManager.source
|
this.analyser
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -275,7 +275,7 @@ export class Visualizer {
|
||||||
this.presets[key].lazyInit(
|
this.presets[key].lazyInit(
|
||||||
this.canvas,
|
this.canvas,
|
||||||
this.audioContext,
|
this.audioContext,
|
||||||
audioContextManager.source
|
this.analyser
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
* WebGL-based audio visualization using the Butterchurn library
|
* WebGL-based audio visualization using the Butterchurn library
|
||||||
*/
|
*/
|
||||||
import butterchurn from 'butterchurn';
|
import butterchurn from 'butterchurn';
|
||||||
import butterchurnPresets from 'butterchurn-presets';
|
|
||||||
import { visualizerSettings } from '../storage.js';
|
import { visualizerSettings } from '../storage.js';
|
||||||
|
|
||||||
export class ButterchurnPreset {
|
export class ButterchurnPreset {
|
||||||
|
|
@ -14,15 +13,65 @@ export class ButterchurnPreset {
|
||||||
this.visualizer = null;
|
this.visualizer = null;
|
||||||
this.canvas = null;
|
this.canvas = null;
|
||||||
this.audioContext = null;
|
this.audioContext = null;
|
||||||
this.presets = null;
|
|
||||||
this.presetKeys = [];
|
|
||||||
this.currentPresetIndex = 0;
|
this.currentPresetIndex = 0;
|
||||||
this.lastPresetChange = 0;
|
this.lastPresetChange = 0;
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
|
|
||||||
|
this.presets = {};
|
||||||
|
this.presetKeys = [];
|
||||||
|
this.isLoadingPresets = false;
|
||||||
|
|
||||||
// Transition settings
|
// Transition settings
|
||||||
this.blendProgress = 0;
|
this.blendProgress = 0;
|
||||||
this.blendDuration = 2.7; // seconds for preset transitions
|
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.canvas = canvas;
|
||||||
this.audioContext = audioContext;
|
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
|
// Create Butterchurn visualizer
|
||||||
this.visualizer = butterchurn.createVisualizer(audioContext, canvas, {
|
this.visualizer = butterchurn.createVisualizer(audioContext, canvas, {
|
||||||
width: canvas.width,
|
width: canvas.width,
|
||||||
|
|
@ -75,7 +106,7 @@ export class ButterchurnPreset {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load initial preset
|
// Load initial preset
|
||||||
this.loadRandomPreset();
|
this.loadNextPreset();
|
||||||
|
|
||||||
this.lastPresetChange = performance.now();
|
this.lastPresetChange = performance.now();
|
||||||
this.isInitialized = true;
|
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;
|
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 presetKey = this.presetKeys[this.currentPresetIndex];
|
||||||
const preset = this.presets[presetKey];
|
const preset = this.presets[presetKey];
|
||||||
|
|
||||||
if (preset) {
|
if (preset) {
|
||||||
try {
|
try {
|
||||||
this.visualizer.loadPreset(preset, this.blendDuration);
|
this.visualizer.loadPreset(preset, this.blendDuration);
|
||||||
console.log('[Butterchurn] Loaded preset:', presetKey);
|
// console.log('[Butterchurn] Loaded preset:', presetKey);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[Butterchurn] Failed to load preset:', presetKey, error);
|
console.warn('[Butterchurn] Failed to load preset:', presetKey, error);
|
||||||
// Try next preset
|
// Try next preset
|
||||||
if (this.presetKeys.length > 1) {
|
if (this.presetKeys.length > 1) {
|
||||||
this.presetKeys.splice(this.currentPresetIndex, 1);
|
this.loadNextPreset();
|
||||||
this.loadRandomPreset();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -131,6 +177,12 @@ export class ButterchurnPreset {
|
||||||
if (preset) {
|
if (preset) {
|
||||||
this.visualizer.loadPreset(preset, this.blendDuration);
|
this.visualizer.loadPreset(preset, this.blendDuration);
|
||||||
console.log('[Butterchurn] Loaded preset:', presetName);
|
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() {
|
nextPreset() {
|
||||||
this.loadRandomPreset();
|
this.loadNextPreset();
|
||||||
this.lastPresetChange = performance.now();
|
this.lastPresetChange = performance.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,11 +232,14 @@ export class ButterchurnPreset {
|
||||||
const { mode } = params;
|
const { mode } = params;
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
|
|
||||||
// Auto-cycle presets (if cycle duration > 0)
|
// Auto-cycle presets
|
||||||
const cycleDuration = this.getPresetDuration();
|
const isCycleEnabled = visualizerSettings.isButterchurnCycleEnabled();
|
||||||
if (cycleDuration > 0 && now - this.lastPresetChange > cycleDuration) {
|
if (isCycleEnabled) {
|
||||||
this.loadRandomPreset();
|
const cycleDuration = this.getPresetDuration();
|
||||||
this.lastPresetChange = now;
|
if (cycleDuration > 0 && now - this.lastPresetChange > cycleDuration) {
|
||||||
|
this.loadNextPreset();
|
||||||
|
this.lastPresetChange = now;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the visualization
|
// Render the visualization
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue