Tweak: Beat detection and blackout detection algos
This commit is contained in:
parent
7c18a3db11
commit
cae867fd9f
@ -103,6 +103,12 @@ export class ConfigUI extends SceneFeature {
|
||||
rightContainer.appendChild(row);
|
||||
};
|
||||
|
||||
// Debug Panel Toggle
|
||||
createToggle('Debug Panel', 'debugPanelEnabled', (enabled) => {
|
||||
const debugPanel = sceneFeatureManager.features.find(f => f.constructor.name === 'DebugPanel');
|
||||
if (debugPanel) debugPanel.setVisibility(enabled);
|
||||
});
|
||||
|
||||
// Blackout Toggle
|
||||
createToggle('BLACKOUT', 'blackout', (enabled) => {
|
||||
state.blackoutMode = enabled;
|
||||
@ -481,7 +487,8 @@ export class ConfigUI extends SceneFeature {
|
||||
laserColorMode: 'RUNNING',
|
||||
guestCount: 150,
|
||||
blackout: false,
|
||||
djHat: 'None'
|
||||
djHat: 'None',
|
||||
debugPanelEnabled: false
|
||||
};
|
||||
for (const key in defaults) {
|
||||
state.config[key] = defaults[key];
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { SceneFeature } from './SceneFeature.js';
|
||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export class FPSCounter extends SceneFeature {
|
||||
export class DebugPanel extends SceneFeature {
|
||||
constructor() {
|
||||
super();
|
||||
this.frames = 0;
|
||||
@ -15,10 +16,17 @@ export class FPSCounter extends SceneFeature {
|
||||
this.memCanvas = null;
|
||||
this.memCtx = null;
|
||||
this.memHistory = [];
|
||||
this.musicTextElement = null;
|
||||
this.musicCanvas = null;
|
||||
this.musicCtx = null;
|
||||
this.musicHistory = [];
|
||||
sceneFeatureManager.register(this);
|
||||
}
|
||||
|
||||
init() {
|
||||
const graphWidth = 150;
|
||||
const graphHeight = 80;
|
||||
|
||||
// Create the FPS display container
|
||||
this.fpsElement = document.createElement('div');
|
||||
Object.assign(this.fpsElement.style, {
|
||||
@ -29,6 +37,7 @@ export class FPSCounter extends SceneFeature {
|
||||
padding: '5px',
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
pointerEvents: 'none',
|
||||
// Allow pointer events on the panel itself if we want to interact, but usually debug panels are pass-through
|
||||
userSelect: 'none',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@ -49,10 +58,10 @@ export class FPSCounter extends SceneFeature {
|
||||
|
||||
// Graph Canvas
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.width = 100;
|
||||
this.canvas.width = graphWidth;
|
||||
this.canvas.height = 40;
|
||||
Object.assign(this.canvas.style, {
|
||||
width: '100px',
|
||||
width: graphWidth + 'px',
|
||||
height: '40px',
|
||||
background: '#222',
|
||||
border: '1px solid #444'
|
||||
@ -75,10 +84,10 @@ export class FPSCounter extends SceneFeature {
|
||||
|
||||
// Memory Graph Canvas
|
||||
this.memCanvas = document.createElement('canvas');
|
||||
this.memCanvas.width = 100;
|
||||
this.memCanvas.width = graphWidth;
|
||||
this.memCanvas.height = 40;
|
||||
Object.assign(this.memCanvas.style, {
|
||||
width: '100px',
|
||||
width: graphWidth + 'px',
|
||||
height: '40px',
|
||||
background: '#222',
|
||||
border: '1px solid #444'
|
||||
@ -86,10 +95,42 @@ export class FPSCounter extends SceneFeature {
|
||||
this.memCtx = this.memCanvas.getContext('2d');
|
||||
this.fpsElement.appendChild(this.memCanvas);
|
||||
|
||||
// Music Text Display
|
||||
this.musicTextElement = document.createElement('div');
|
||||
Object.assign(this.musicTextElement.style, {
|
||||
color: '#ff00ff',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '2px',
|
||||
marginTop: '5px'
|
||||
});
|
||||
this.musicTextElement.innerText = 'MUSIC';
|
||||
this.fpsElement.appendChild(this.musicTextElement);
|
||||
|
||||
// Music Graph Canvas
|
||||
this.musicCanvas = document.createElement('canvas');
|
||||
this.musicCanvas.width = graphWidth;
|
||||
this.musicCanvas.height = graphHeight;
|
||||
Object.assign(this.musicCanvas.style, {
|
||||
width: graphWidth + 'px',
|
||||
height: graphHeight + 'px',
|
||||
background: '#222',
|
||||
border: '1px solid #444'
|
||||
});
|
||||
this.musicCtx = this.musicCanvas.getContext('2d');
|
||||
this.fpsElement.appendChild(this.musicCanvas);
|
||||
|
||||
document.body.appendChild(this.fpsElement);
|
||||
|
||||
this.history = new Array(this.canvas.width).fill(0);
|
||||
this.memHistory = new Array(this.memCanvas.width).fill(0);
|
||||
this.musicHistory = new Array(this.musicCanvas.width).fill({l:0, low:0, high:0, b:0, lt:0, qt:0, m:0.5, bo: false});
|
||||
|
||||
// Initialize visibility based on config
|
||||
if (state.config) {
|
||||
this.setVisibility(state.config.debugPanelEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
@ -99,16 +140,7 @@ export class FPSCounter extends SceneFeature {
|
||||
const segment = 0.25;
|
||||
if (this.timeAccumulator >= segment) {
|
||||
const fps = Math.round(this.frames / this.timeAccumulator);
|
||||
if (this.textElement) {
|
||||
this.textElement.innerText = `FPS: ${fps}`;
|
||||
if (fps >= 58) {
|
||||
this.textElement.style.color = '#00ff00';
|
||||
} else if (fps >= 55) {
|
||||
this.textElement.style.color = '#ffff00';
|
||||
} else {
|
||||
this.textElement.style.color = '#ff0000';
|
||||
}
|
||||
}
|
||||
if (this.textElement) this.textElement.innerText = `FPS: ${fps}`;
|
||||
|
||||
// Update Graph
|
||||
this.history.push(fps);
|
||||
@ -127,14 +159,52 @@ export class FPSCounter extends SceneFeature {
|
||||
this.memHistory.shift();
|
||||
}
|
||||
this.drawMemGraph();
|
||||
} else if (state.renderer) {
|
||||
// Fallback for Firefox: Visualize Scene Complexity (Triangles)
|
||||
const tris = state.renderer.info.render.triangles;
|
||||
const kTris = Math.round(tris / 1000);
|
||||
if (this.memTextElement) this.memTextElement.innerText = `TRIS: ${kTris}k`;
|
||||
|
||||
this.memHistory.push(kTris);
|
||||
if (this.memHistory.length > this.memCanvas.width) {
|
||||
this.memHistory.shift();
|
||||
}
|
||||
this.drawMemGraph();
|
||||
} else {
|
||||
if (this.memTextElement) this.memTextElement.style.display = 'none';
|
||||
if (this.memCanvas) this.memCanvas.style.display = 'none';
|
||||
if (this.memTextElement) this.memTextElement.innerText = `MEM: N/A`;
|
||||
}
|
||||
|
||||
this.frames = 0;
|
||||
this.timeAccumulator = 0;
|
||||
}
|
||||
|
||||
// Update Music Graph (Every frame for smoothness)
|
||||
if (state.music) {
|
||||
const loudness = state.music.loudness || 0;
|
||||
const loudnessAvg = state.music.loudnessAverage || 0;
|
||||
const lows = state.music.loudnessLows || 0;
|
||||
const highs = state.music.loudnessHighs || 0;
|
||||
const beat = state.music.beatIntensity || 0;
|
||||
const thresholds = state.music.thresholds || { loud: 0, quiet: 0 };
|
||||
let modeVal = 0.5;
|
||||
if (state.music.mode === 'Loud') modeVal = 1.0;
|
||||
else if (state.music.mode === 'Quiet') modeVal = 0.0;
|
||||
|
||||
this.musicHistory.push({ l: loudness, la: loudnessAvg, low: lows, high: highs, b: beat, lt: thresholds.loud, qt: thresholds.quiet, m: modeVal, bo: state.blackoutMode });
|
||||
if (this.musicHistory.length > this.musicCanvas.width) {
|
||||
this.musicHistory.shift();
|
||||
}
|
||||
this.drawMusicGraph();
|
||||
|
||||
if (this.musicTextElement) {
|
||||
this.musicTextElement.innerText = `Volume:${loudness.toFixed(2)}\nMode:${state.music.mode}\nBPM:${state.music.bpm}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setVisibility(visible) {
|
||||
if (!this.fpsElement) return;
|
||||
this.fpsElement.style.display = visible ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
drawGraph() {
|
||||
@ -155,25 +225,18 @@ export class FPSCounter extends SceneFeature {
|
||||
ctx.stroke();
|
||||
|
||||
// Draw Graph
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#00ff00';
|
||||
ctx.lineWidth = 1.5;
|
||||
|
||||
for (let i = 0; i < this.history.length - 1; i++) {
|
||||
const val = this.history[i + 1];
|
||||
const x1 = i;
|
||||
const y1 = h - (this.history[i] / maxFps) * h;
|
||||
const x2 = i + 1;
|
||||
const y2 = h - (val / maxFps) * h;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
|
||||
if (val >= 58) ctx.strokeStyle = '#00ff00';
|
||||
else if (val >= 55) ctx.strokeStyle = '#ffff00';
|
||||
else ctx.strokeStyle = '#ff0000';
|
||||
|
||||
ctx.stroke();
|
||||
for (let i = 0; i < this.history.length; i++) {
|
||||
const val = this.history[i];
|
||||
const x = i;
|
||||
const y = h - (val / maxFps) * h;
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
drawMemGraph() {
|
||||
@ -200,6 +263,118 @@ export class FPSCounter extends SceneFeature {
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
drawMusicGraph() {
|
||||
if (!this.musicCtx) return;
|
||||
const ctx = this.musicCtx;
|
||||
const w = this.musicCanvas.width;
|
||||
const h = this.musicCanvas.height;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// Draw Lows (Brown)
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#D2691E'; // Chocolate Brown
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < this.musicHistory.length; i++) {
|
||||
const val = this.musicHistory[i].low !== undefined ? this.musicHistory[i].low : 0;
|
||||
const x = i;
|
||||
const y = h - (val * h);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
// Draw All (Blue)
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#66aaff'; // Visible Blue
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < this.musicHistory.length; i++) {
|
||||
const val = this.musicHistory[i].l;
|
||||
const x = i;
|
||||
const y = h - (val * h);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
// Draw Highs (White)
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#ffffff';
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < this.musicHistory.length; i++) {
|
||||
const val = this.musicHistory[i].high !== undefined ? this.musicHistory[i].high : 0;
|
||||
const x = i;
|
||||
const y = h - (val * h);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
// Draw All Avg (Blue)
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#8866ff';
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < this.musicHistory.length; i++) {
|
||||
const val = this.musicHistory[i].la;
|
||||
const x = i;
|
||||
const y = h - (val * h);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
// Draw Beat Intensity (Magenta)
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#ff00ff';
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < this.musicHistory.length; i++) {
|
||||
const val = this.musicHistory[i].b;
|
||||
const x = i;
|
||||
const y = h - (val * h);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
// Draw Active Threshold (Green for Loud/Blackout, Red for Quiet/Normal)
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
for (let i = 0; i < this.musicHistory.length - 1; i++) {
|
||||
const curr = this.musicHistory[i];
|
||||
const next = this.musicHistory[i+1];
|
||||
|
||||
ctx.beginPath();
|
||||
|
||||
if (curr.bo) {
|
||||
// Blackout Mode: Draw Loud Threshold (Green Dashed)
|
||||
ctx.strokeStyle = '#00ff00';
|
||||
ctx.setLineDash([2, 2]);
|
||||
ctx.moveTo(i, h - (curr.lt * h));
|
||||
ctx.lineTo(i + 1, h - (next.lt * h));
|
||||
} else {
|
||||
// Normal Mode: Draw Quiet Threshold (Red Solid)
|
||||
ctx.strokeStyle = '#ff0000';
|
||||
ctx.setLineDash([]);
|
||||
ctx.moveTo(i, h - (curr.qt * h));
|
||||
ctx.lineTo(i + 1, h - (next.qt * h));
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
new FPSCounter();
|
||||
// Draw Mode (Yellow)
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#ffff00';
|
||||
ctx.setLineDash([1, 2]);
|
||||
for (let i = 0; i < this.musicHistory.length; i++) {
|
||||
const val = this.musicHistory[i].m !== undefined ? this.musicHistory[i].m : 0.5;
|
||||
const x = i;
|
||||
const y = h - (val * h);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]); // Reset dash
|
||||
}
|
||||
}
|
||||
new DebugPanel();
|
||||
@ -11,15 +11,13 @@ export class MusicPlayer extends SceneFeature {
|
||||
this.analyser = null;
|
||||
this.source = null;
|
||||
this.dataArray = null;
|
||||
this.loudnessHistory = [];
|
||||
sceneFeatureManager.register(this);
|
||||
}
|
||||
|
||||
init() {
|
||||
state.music.player = document.getElementById('audioPlayer');
|
||||
state.music.loudness = 0;
|
||||
state.music.isLoudEnough = false;
|
||||
|
||||
state.music.loudnessAverage = 0;
|
||||
const loadButton = document.getElementById('loadMusicButton');
|
||||
const fileInput = document.getElementById('musicFileInput');
|
||||
|
||||
@ -58,6 +56,7 @@ export class MusicPlayer extends SceneFeature {
|
||||
this.source.connect(this.analyser);
|
||||
this.analyser.connect(this.audioContext.destination);
|
||||
this.dataArray = new Uint8Array(this.analyser.frequencyBinCount);
|
||||
state.music.frequencyData = this.dataArray;
|
||||
}
|
||||
|
||||
// Update State
|
||||
@ -99,9 +98,6 @@ export class MusicPlayer extends SceneFeature {
|
||||
document.getElementById('ui-container').style.display = 'none';
|
||||
state.partyStarted = true;
|
||||
|
||||
// You could add BPM detection here in the future
|
||||
// For now, we use the fixed BPM
|
||||
|
||||
// Trigger 'start' event for other features
|
||||
this.notifyFeatures('onPartyStart');
|
||||
}
|
||||
@ -125,26 +121,6 @@ export class MusicPlayer extends SceneFeature {
|
||||
if (!state.partyStarted || !this.analyser) return;
|
||||
|
||||
this.analyser.getByteFrequencyData(this.dataArray);
|
||||
|
||||
// --- Calculate current loudness ---
|
||||
let sum = 0;
|
||||
for (let i = 0; i < this.dataArray.length; i++) {
|
||||
sum += this.dataArray[i];
|
||||
}
|
||||
const average = sum / this.dataArray.length;
|
||||
state.music.loudness = average / 255; // Normalize to 0-1 range
|
||||
|
||||
// --- Track loudness over the last 2 seconds ---
|
||||
this.loudnessHistory.push(state.music.loudness);
|
||||
if (this.loudnessHistory.length > 120) { // Assuming ~60fps, 2 seconds of history
|
||||
this.loudnessHistory.shift();
|
||||
}
|
||||
|
||||
// --- Determine if it's loud enough to jump ---
|
||||
const avgLoudness = this.loudnessHistory.reduce((a, b) => a + b, 0) / this.loudnessHistory.length;
|
||||
const quietThreshold = 0.1; // Adjust this value based on testing
|
||||
|
||||
state.music.isLoudEnough = avgLoudness > quietThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
import { SceneFeature } from './SceneFeature.js';
|
||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||
@ -7,53 +8,158 @@ export class MusicVisualizer extends SceneFeature {
|
||||
super();
|
||||
this.lastStateChangeTime = 0;
|
||||
this.lastBlackoutMode = false;
|
||||
this.lastBeatTime = 0;
|
||||
this.beatIntervals = [];
|
||||
this.beatThreshold = 0.3;
|
||||
this.loudnessHistory = [];
|
||||
this.loudnessLowsHistory = [];
|
||||
sceneFeatureManager.register(this);
|
||||
}
|
||||
|
||||
init() {
|
||||
// Initialize music state
|
||||
state.music = {
|
||||
bpm: 120,
|
||||
beatDuration: 60 / 120,
|
||||
measureDuration: (60 / 120) * 4,
|
||||
beatIntensity: 0,
|
||||
measurePulse: 0,
|
||||
isLoudEnough: false,
|
||||
thresholds: { loud: 0, quiet: 0 },
|
||||
mode: 'Normal',
|
||||
bpm: 50,
|
||||
loudness: 0,
|
||||
loudnessLows: 0,
|
||||
loudnessHighs: 0,
|
||||
loudnessAverage: 0,
|
||||
loudnessLowsAverage: 0,
|
||||
frequencyData: null
|
||||
};
|
||||
this.beatPhase = 0;
|
||||
this.measurePhase = 0;
|
||||
this.averageLoudness = 0;
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
if (!state.music || !state.partyStarted) return;
|
||||
|
||||
// --- Calculate Loudness from Frequency Data ---
|
||||
if (state.music.frequencyData) {
|
||||
const dataArray = state.music.frequencyData;
|
||||
let sumLows = 0;
|
||||
let sumHighs = 0;
|
||||
let sumAll = 0;
|
||||
const countLows = Math.min(dataArray.length * 0.2, dataArray.length);
|
||||
const countHighs = Math.min(dataArray.length * 0.6, dataArray.length);
|
||||
|
||||
for (let i = 0; i < dataArray.length; i++) {
|
||||
sumAll += dataArray[i];
|
||||
if (i < countLows) {
|
||||
sumLows += dataArray[i];
|
||||
}
|
||||
if (i > dataArray.length - countHighs) {
|
||||
sumHighs += dataArray[i];
|
||||
}
|
||||
}
|
||||
|
||||
const averageLows = sumLows / countLows;
|
||||
const averageHighs = sumHighs / countHighs;
|
||||
const averageAll = sumAll / dataArray.length;
|
||||
|
||||
// Normalize to 0-1 range
|
||||
state.music.loudness = averageAll / 255;
|
||||
state.music.loudnessLows = averageLows / 255;
|
||||
state.music.loudnessHighs = averageHighs / 255;
|
||||
|
||||
// --- Track loudness over the last 2 seconds ---
|
||||
this.loudnessHistory.push(state.music.loudness);
|
||||
if (this.loudnessHistory.length > 120) this.loudnessHistory.shift();
|
||||
|
||||
this.loudnessLowsHistory.push(state.music.loudnessLows);
|
||||
if (this.loudnessLowsHistory.length > 120) this.loudnessLowsHistory.shift();
|
||||
|
||||
const avgLoudness = this.loudnessHistory.reduce((a, b) => a + b, 0) / this.loudnessHistory.length;
|
||||
const avgLowsLoudness = this.loudnessLowsHistory.reduce((a, b) => a + b, 0) / this.loudnessLowsHistory.length;
|
||||
|
||||
state.music.loudnessAverage = avgLoudness;
|
||||
state.music.loudnessLowsAverage = avgLowsLoudness;
|
||||
state.music.isLoudEnough = avgLoudness > 0.1;
|
||||
}
|
||||
|
||||
const time = state.clock.getElapsedTime();
|
||||
const loudness = state.music.loudness || 0;
|
||||
const loudnessAvg = state.music.loudnessAverage || 0;
|
||||
this.averageLoudness = loudnessAvg;
|
||||
|
||||
// --- Calculate Beat Intensity (pulses every beat) ---
|
||||
// This creates a sharp attack and slower decay (0 -> 1 -> 0)
|
||||
const beatProgress = (time % state.music.beatDuration) / state.music.beatDuration;
|
||||
state.music.beatIntensity = Math.pow(1.0 - beatProgress, 2);
|
||||
// --- Beat Detection & Auto-BPM ---
|
||||
this.beatThreshold = Math.max(loudnessAvg * 1.01, this.beatThreshold * 0.96);
|
||||
|
||||
// --- Calculate Measure Pulse (spikes every 4 beats) ---
|
||||
// This creates a very sharp spike for the torch flame effect
|
||||
const measureProgress = (time % state.music.measureDuration) / state.music.measureDuration;
|
||||
state.music.measurePulse = measureProgress < 0.2 ? Math.sin(measureProgress * Math.PI * 5) : 0;
|
||||
if (loudness > this.beatThreshold) {
|
||||
const now = time;
|
||||
if (now - this.lastBeatTime > 0.3) { // Min interval (~200 BPM)
|
||||
const interval = now - this.lastBeatTime;
|
||||
this.lastBeatTime = now;
|
||||
this.beatThreshold = loudness * 1.2; // Bump threshold
|
||||
|
||||
// Valid BPM range: 60-180 (interval 1.0s - 0.33s)
|
||||
if (interval >= 0.33 && interval <= 1.0) {
|
||||
this.beatIntervals.push(interval);
|
||||
if (this.beatIntervals.length > 8) this.beatIntervals.shift();
|
||||
|
||||
const avgInterval = this.beatIntervals.reduce((a, b) => a + b, 0) / this.beatIntervals.length;
|
||||
|
||||
// Smoothly adjust beat duration
|
||||
state.music.beatDuration = THREE.MathUtils.lerp(state.music.beatDuration, avgInterval, 0.1);
|
||||
state.music.measureDuration = state.music.beatDuration * 4;
|
||||
state.music.bpm = Math.round(60 / state.music.beatDuration);
|
||||
}
|
||||
|
||||
// Sync phase on beat
|
||||
this.beatPhase = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Phase Accumulation ---
|
||||
this.beatPhase += deltaTime / state.music.beatDuration;
|
||||
if (this.beatPhase >= 1) this.beatPhase -= 1;
|
||||
|
||||
this.measurePhase += deltaTime / state.music.measureDuration;
|
||||
if (this.measurePhase >= 1) this.measurePhase -= 1;
|
||||
|
||||
// --- Calculate Beat Intensity ---
|
||||
state.music.beatIntensity = Math.pow(1.0 - this.beatPhase, 2);
|
||||
|
||||
// --- Calculate Measure Pulse ---
|
||||
state.music.measurePulse = this.measurePhase < 0.2 ? Math.sin(this.measurePhase * Math.PI * 5) : 0;
|
||||
|
||||
// --- Auto-Blackout Logic ---
|
||||
// If blackout is active, monitor for loud events (The Drop) to disable it.
|
||||
if (state.config.blackout) {
|
||||
if (state.blackoutMode !== this.lastBlackoutMode) {
|
||||
this.lastStateChangeTime = time;
|
||||
this.lastBlackoutMode = state.blackoutMode;
|
||||
}
|
||||
|
||||
const timeInState = time - this.lastStateChangeTime;
|
||||
let timeInStateLoud = 0;
|
||||
let timeInStateQuiet = 0;
|
||||
|
||||
if (state.blackoutMode) {
|
||||
timeInStateQuiet = timeInState;
|
||||
} else {
|
||||
timeInStateLoud = timeInState;
|
||||
}
|
||||
|
||||
// Dynamic Thresholds
|
||||
// Exit blackout: Start high 0.8 and decrease over time
|
||||
let loudThreshold = Math.max(0.4, 0.8 - (timeInState * 0.05));
|
||||
// The threshold to EXIT blackout. It should be a significant spike above the average loudness.
|
||||
// It starts high and decays, making it easier to exit the longer we're in blackout.
|
||||
const loudSpikeModif = 1.2; // How much louder than average a "drop" needs to be.
|
||||
let loudThreshold = this.averageLoudness * loudSpikeModif;
|
||||
loudThreshold = Math.max(this.averageLoudness + 0.1, loudThreshold - (timeInStateQuiet * 0.05));
|
||||
|
||||
// Enter blackout: Start low and increase over time
|
||||
let quietThreshold = Math.min(0.2, 0.05 + (timeInState * 0.01));
|
||||
// The threshold to ENTER blackout, based on a percentage of the song's average loudness.
|
||||
let quietThreshold = this.averageLoudness * 0.75;
|
||||
quietThreshold = THREE.MathUtils.clamp(quietThreshold, 0.02, 0.3); // Clamp to a reasonable range.
|
||||
|
||||
// --- Auto-Blackout Logic ---
|
||||
// If blackout is active, monitor for loud events (The Drop) to disable it.
|
||||
if (state.config.blackout) {
|
||||
const beatThreshold = 0.8;
|
||||
|
||||
if (state.blackoutMode) {
|
||||
@ -67,7 +173,16 @@ export class MusicVisualizer extends SceneFeature {
|
||||
}
|
||||
} else {
|
||||
state.blackoutMode = false;
|
||||
this.lastBlackoutMode = false;
|
||||
}
|
||||
|
||||
state.music.thresholds = { loud: loudThreshold, quiet: quietThreshold };
|
||||
|
||||
if (loudness > loudThreshold) {
|
||||
state.music.mode = 'Loud';
|
||||
} else if (loudness < quietThreshold) {
|
||||
state.music.mode = 'Quiet';
|
||||
} else {
|
||||
state.music.mode = 'Normal';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ import { ProjectionScreen } from './projection-screen.js';
|
||||
import { StageLasers } from './stage-lasers.js';
|
||||
import { ConfigUI } from './config-ui.js';
|
||||
import { StageLightBars } from './stage-light-bars.js';
|
||||
import { FPSCounter } from './fps-counter.js';
|
||||
import { DebugPanel } from './fps-counter.js';
|
||||
// Scene Features ^^^
|
||||
|
||||
// --- Scene Modeling Function ---
|
||||
|
||||
@ -132,7 +132,7 @@ export class StageLasers extends SceneFeature {
|
||||
|
||||
if (this.activationState === 'IDLE') {
|
||||
// Wait for song to pick up before first activation
|
||||
if (time > this.initialSilenceSeconds && loudness > this.averageLoudness + 0.1) {
|
||||
if (time > this.initialSilenceSeconds && state.music.isLoudEnough && loudness > this.averageLoudness + 0.1) {
|
||||
this.activationState = 'WARMUP';
|
||||
this.stateTimer = 1.0; // Warmup duration
|
||||
|
||||
|
||||
@ -111,7 +111,7 @@ export class StageTorches extends SceneFeature {
|
||||
|
||||
if (!enabled) return;
|
||||
|
||||
if (!state.partyStarted) {
|
||||
if (!state.partyStarted && state.music.isLoudEnough) {
|
||||
this.torches.forEach(torch => {
|
||||
if (torch.light.visible) torch.light.visible = false;
|
||||
if (torch.particles.visible) torch.particles.visible = false;
|
||||
|
||||
@ -15,7 +15,8 @@ export function initState() {
|
||||
lightBarColors: ['#ff00ff', '#00ffff', '#ffff00'], // Default neon colors
|
||||
guestCount: 150,
|
||||
blackout: false,
|
||||
djHat: 'None' // 'None', 'Santa', 'Top Hat'
|
||||
djHat: 'None', // 'None', 'Santa', 'Top Hat'
|
||||
debugPanelEnabled: false
|
||||
};
|
||||
try {
|
||||
const saved = localStorage.getItem('partyConfig');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user