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);
|
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
|
// Blackout Toggle
|
||||||
createToggle('BLACKOUT', 'blackout', (enabled) => {
|
createToggle('BLACKOUT', 'blackout', (enabled) => {
|
||||||
state.blackoutMode = enabled;
|
state.blackoutMode = enabled;
|
||||||
@ -481,7 +487,8 @@ export class ConfigUI extends SceneFeature {
|
|||||||
laserColorMode: 'RUNNING',
|
laserColorMode: 'RUNNING',
|
||||||
guestCount: 150,
|
guestCount: 150,
|
||||||
blackout: false,
|
blackout: false,
|
||||||
djHat: 'None'
|
djHat: 'None',
|
||||||
|
debugPanelEnabled: false
|
||||||
};
|
};
|
||||||
for (const key in defaults) {
|
for (const key in defaults) {
|
||||||
state.config[key] = defaults[key];
|
state.config[key] = defaults[key];
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { SceneFeature } from './SceneFeature.js';
|
import { SceneFeature } from './SceneFeature.js';
|
||||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||||
|
import { state } from '../state.js';
|
||||||
|
|
||||||
export class FPSCounter extends SceneFeature {
|
export class DebugPanel extends SceneFeature {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.frames = 0;
|
this.frames = 0;
|
||||||
@ -15,10 +16,17 @@ export class FPSCounter extends SceneFeature {
|
|||||||
this.memCanvas = null;
|
this.memCanvas = null;
|
||||||
this.memCtx = null;
|
this.memCtx = null;
|
||||||
this.memHistory = [];
|
this.memHistory = [];
|
||||||
|
this.musicTextElement = null;
|
||||||
|
this.musicCanvas = null;
|
||||||
|
this.musicCtx = null;
|
||||||
|
this.musicHistory = [];
|
||||||
sceneFeatureManager.register(this);
|
sceneFeatureManager.register(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
const graphWidth = 150;
|
||||||
|
const graphHeight = 80;
|
||||||
|
|
||||||
// Create the FPS display container
|
// Create the FPS display container
|
||||||
this.fpsElement = document.createElement('div');
|
this.fpsElement = document.createElement('div');
|
||||||
Object.assign(this.fpsElement.style, {
|
Object.assign(this.fpsElement.style, {
|
||||||
@ -29,6 +37,7 @@ export class FPSCounter extends SceneFeature {
|
|||||||
padding: '5px',
|
padding: '5px',
|
||||||
background: 'rgba(0, 0, 0, 0.5)',
|
background: 'rgba(0, 0, 0, 0.5)',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
|
// Allow pointer events on the panel itself if we want to interact, but usually debug panels are pass-through
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@ -49,10 +58,10 @@ export class FPSCounter extends SceneFeature {
|
|||||||
|
|
||||||
// Graph Canvas
|
// Graph Canvas
|
||||||
this.canvas = document.createElement('canvas');
|
this.canvas = document.createElement('canvas');
|
||||||
this.canvas.width = 100;
|
this.canvas.width = graphWidth;
|
||||||
this.canvas.height = 40;
|
this.canvas.height = 40;
|
||||||
Object.assign(this.canvas.style, {
|
Object.assign(this.canvas.style, {
|
||||||
width: '100px',
|
width: graphWidth + 'px',
|
||||||
height: '40px',
|
height: '40px',
|
||||||
background: '#222',
|
background: '#222',
|
||||||
border: '1px solid #444'
|
border: '1px solid #444'
|
||||||
@ -75,10 +84,10 @@ export class FPSCounter extends SceneFeature {
|
|||||||
|
|
||||||
// Memory Graph Canvas
|
// Memory Graph Canvas
|
||||||
this.memCanvas = document.createElement('canvas');
|
this.memCanvas = document.createElement('canvas');
|
||||||
this.memCanvas.width = 100;
|
this.memCanvas.width = graphWidth;
|
||||||
this.memCanvas.height = 40;
|
this.memCanvas.height = 40;
|
||||||
Object.assign(this.memCanvas.style, {
|
Object.assign(this.memCanvas.style, {
|
||||||
width: '100px',
|
width: graphWidth + 'px',
|
||||||
height: '40px',
|
height: '40px',
|
||||||
background: '#222',
|
background: '#222',
|
||||||
border: '1px solid #444'
|
border: '1px solid #444'
|
||||||
@ -86,10 +95,42 @@ export class FPSCounter extends SceneFeature {
|
|||||||
this.memCtx = this.memCanvas.getContext('2d');
|
this.memCtx = this.memCanvas.getContext('2d');
|
||||||
this.fpsElement.appendChild(this.memCanvas);
|
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);
|
document.body.appendChild(this.fpsElement);
|
||||||
|
|
||||||
this.history = new Array(this.canvas.width).fill(0);
|
this.history = new Array(this.canvas.width).fill(0);
|
||||||
this.memHistory = new Array(this.memCanvas.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) {
|
update(deltaTime) {
|
||||||
@ -99,16 +140,7 @@ export class FPSCounter extends SceneFeature {
|
|||||||
const segment = 0.25;
|
const segment = 0.25;
|
||||||
if (this.timeAccumulator >= segment) {
|
if (this.timeAccumulator >= segment) {
|
||||||
const fps = Math.round(this.frames / this.timeAccumulator);
|
const fps = Math.round(this.frames / this.timeAccumulator);
|
||||||
if (this.textElement) {
|
if (this.textElement) this.textElement.innerText = `FPS: ${fps}`;
|
||||||
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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update Graph
|
// Update Graph
|
||||||
this.history.push(fps);
|
this.history.push(fps);
|
||||||
@ -127,14 +159,52 @@ export class FPSCounter extends SceneFeature {
|
|||||||
this.memHistory.shift();
|
this.memHistory.shift();
|
||||||
}
|
}
|
||||||
this.drawMemGraph();
|
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 {
|
} else {
|
||||||
if (this.memTextElement) this.memTextElement.style.display = 'none';
|
if (this.memTextElement) this.memTextElement.innerText = `MEM: N/A`;
|
||||||
if (this.memCanvas) this.memCanvas.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.frames = 0;
|
this.frames = 0;
|
||||||
this.timeAccumulator = 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() {
|
drawGraph() {
|
||||||
@ -155,25 +225,18 @@ export class FPSCounter extends SceneFeature {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Draw Graph
|
// Draw Graph
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.strokeStyle = '#00ff00';
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = 1.5;
|
||||||
|
|
||||||
for (let i = 0; i < this.history.length - 1; i++) {
|
for (let i = 0; i < this.history.length; i++) {
|
||||||
const val = this.history[i + 1];
|
const val = this.history[i];
|
||||||
const x1 = i;
|
const x = i;
|
||||||
const y1 = h - (this.history[i] / maxFps) * h;
|
const y = h - (val / maxFps) * h;
|
||||||
const x2 = i + 1;
|
if (i === 0) ctx.moveTo(x, y);
|
||||||
const y2 = h - (val / maxFps) * h;
|
else ctx.lineTo(x, y);
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
drawMemGraph() {
|
drawMemGraph() {
|
||||||
@ -200,6 +263,118 @@ export class FPSCounter extends SceneFeature {
|
|||||||
}
|
}
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
new FPSCounter();
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.analyser = null;
|
||||||
this.source = null;
|
this.source = null;
|
||||||
this.dataArray = null;
|
this.dataArray = null;
|
||||||
this.loudnessHistory = [];
|
|
||||||
sceneFeatureManager.register(this);
|
sceneFeatureManager.register(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
state.music.player = document.getElementById('audioPlayer');
|
state.music.player = document.getElementById('audioPlayer');
|
||||||
state.music.loudness = 0;
|
state.music.loudness = 0;
|
||||||
state.music.isLoudEnough = false;
|
state.music.loudnessAverage = 0;
|
||||||
|
|
||||||
const loadButton = document.getElementById('loadMusicButton');
|
const loadButton = document.getElementById('loadMusicButton');
|
||||||
const fileInput = document.getElementById('musicFileInput');
|
const fileInput = document.getElementById('musicFileInput');
|
||||||
|
|
||||||
@ -58,6 +56,7 @@ export class MusicPlayer extends SceneFeature {
|
|||||||
this.source.connect(this.analyser);
|
this.source.connect(this.analyser);
|
||||||
this.analyser.connect(this.audioContext.destination);
|
this.analyser.connect(this.audioContext.destination);
|
||||||
this.dataArray = new Uint8Array(this.analyser.frequencyBinCount);
|
this.dataArray = new Uint8Array(this.analyser.frequencyBinCount);
|
||||||
|
state.music.frequencyData = this.dataArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update State
|
// Update State
|
||||||
@ -99,9 +98,6 @@ export class MusicPlayer extends SceneFeature {
|
|||||||
document.getElementById('ui-container').style.display = 'none';
|
document.getElementById('ui-container').style.display = 'none';
|
||||||
state.partyStarted = true;
|
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
|
// Trigger 'start' event for other features
|
||||||
this.notifyFeatures('onPartyStart');
|
this.notifyFeatures('onPartyStart');
|
||||||
}
|
}
|
||||||
@ -125,26 +121,6 @@ export class MusicPlayer extends SceneFeature {
|
|||||||
if (!state.partyStarted || !this.analyser) return;
|
if (!state.partyStarted || !this.analyser) return;
|
||||||
|
|
||||||
this.analyser.getByteFrequencyData(this.dataArray);
|
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 { state } from '../state.js';
|
||||||
import { SceneFeature } from './SceneFeature.js';
|
import { SceneFeature } from './SceneFeature.js';
|
||||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||||
@ -7,53 +8,158 @@ export class MusicVisualizer extends SceneFeature {
|
|||||||
super();
|
super();
|
||||||
this.lastStateChangeTime = 0;
|
this.lastStateChangeTime = 0;
|
||||||
this.lastBlackoutMode = false;
|
this.lastBlackoutMode = false;
|
||||||
|
this.lastBeatTime = 0;
|
||||||
|
this.beatIntervals = [];
|
||||||
|
this.beatThreshold = 0.3;
|
||||||
|
this.loudnessHistory = [];
|
||||||
|
this.loudnessLowsHistory = [];
|
||||||
sceneFeatureManager.register(this);
|
sceneFeatureManager.register(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// Initialize music state
|
// Initialize music state
|
||||||
state.music = {
|
state.music = {
|
||||||
bpm: 120,
|
|
||||||
beatDuration: 60 / 120,
|
beatDuration: 60 / 120,
|
||||||
measureDuration: (60 / 120) * 4,
|
measureDuration: (60 / 120) * 4,
|
||||||
beatIntensity: 0,
|
beatIntensity: 0,
|
||||||
measurePulse: 0,
|
measurePulse: 0,
|
||||||
isLoudEnough: false,
|
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) {
|
update(deltaTime) {
|
||||||
if (!state.music || !state.partyStarted) return;
|
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 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) ---
|
// --- Beat Detection & Auto-BPM ---
|
||||||
// This creates a sharp attack and slower decay (0 -> 1 -> 0)
|
this.beatThreshold = Math.max(loudnessAvg * 1.01, this.beatThreshold * 0.96);
|
||||||
const beatProgress = (time % state.music.beatDuration) / state.music.beatDuration;
|
|
||||||
state.music.beatIntensity = Math.pow(1.0 - beatProgress, 2);
|
|
||||||
|
|
||||||
// --- Calculate Measure Pulse (spikes every 4 beats) ---
|
if (loudness > this.beatThreshold) {
|
||||||
// This creates a very sharp spike for the torch flame effect
|
const now = time;
|
||||||
const measureProgress = (time % state.music.measureDuration) / state.music.measureDuration;
|
if (now - this.lastBeatTime > 0.3) { // Min interval (~200 BPM)
|
||||||
state.music.measurePulse = measureProgress < 0.2 ? Math.sin(measureProgress * Math.PI * 5) : 0;
|
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;
|
||||||
|
|
||||||
|
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
|
||||||
|
// 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));
|
||||||
|
|
||||||
|
// 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 ---
|
// --- Auto-Blackout Logic ---
|
||||||
// If blackout is active, monitor for loud events (The Drop) to disable it.
|
// If blackout is active, monitor for loud events (The Drop) to disable it.
|
||||||
if (state.config.blackout) {
|
if (state.config.blackout) {
|
||||||
if (state.blackoutMode !== this.lastBlackoutMode) {
|
|
||||||
this.lastStateChangeTime = time;
|
|
||||||
this.lastBlackoutMode = state.blackoutMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeInState = time - this.lastStateChangeTime;
|
|
||||||
|
|
||||||
// Dynamic Thresholds
|
|
||||||
// Exit blackout: Start high 0.8 and decrease over time
|
|
||||||
let loudThreshold = Math.max(0.4, 0.8 - (timeInState * 0.05));
|
|
||||||
|
|
||||||
// Enter blackout: Start low and increase over time
|
|
||||||
let quietThreshold = Math.min(0.2, 0.05 + (timeInState * 0.01));
|
|
||||||
|
|
||||||
const beatThreshold = 0.8;
|
const beatThreshold = 0.8;
|
||||||
|
|
||||||
if (state.blackoutMode) {
|
if (state.blackoutMode) {
|
||||||
@ -67,7 +173,16 @@ export class MusicVisualizer extends SceneFeature {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state.blackoutMode = false;
|
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 { StageLasers } from './stage-lasers.js';
|
||||||
import { ConfigUI } from './config-ui.js';
|
import { ConfigUI } from './config-ui.js';
|
||||||
import { StageLightBars } from './stage-light-bars.js';
|
import { StageLightBars } from './stage-light-bars.js';
|
||||||
import { FPSCounter } from './fps-counter.js';
|
import { DebugPanel } from './fps-counter.js';
|
||||||
// Scene Features ^^^
|
// Scene Features ^^^
|
||||||
|
|
||||||
// --- Scene Modeling Function ---
|
// --- Scene Modeling Function ---
|
||||||
|
|||||||
@ -132,7 +132,7 @@ export class StageLasers extends SceneFeature {
|
|||||||
|
|
||||||
if (this.activationState === 'IDLE') {
|
if (this.activationState === 'IDLE') {
|
||||||
// Wait for song to pick up before first activation
|
// 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.activationState = 'WARMUP';
|
||||||
this.stateTimer = 1.0; // Warmup duration
|
this.stateTimer = 1.0; // Warmup duration
|
||||||
|
|
||||||
|
|||||||
@ -111,7 +111,7 @@ export class StageTorches extends SceneFeature {
|
|||||||
|
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
|
|
||||||
if (!state.partyStarted) {
|
if (!state.partyStarted && state.music.isLoudEnough) {
|
||||||
this.torches.forEach(torch => {
|
this.torches.forEach(torch => {
|
||||||
if (torch.light.visible) torch.light.visible = false;
|
if (torch.light.visible) torch.light.visible = false;
|
||||||
if (torch.particles.visible) torch.particles.visible = false;
|
if (torch.particles.visible) torch.particles.visible = false;
|
||||||
|
|||||||
@ -15,7 +15,8 @@ export function initState() {
|
|||||||
lightBarColors: ['#ff00ff', '#00ffff', '#ffff00'], // Default neon colors
|
lightBarColors: ['#ff00ff', '#00ffff', '#ffff00'], // Default neon colors
|
||||||
guestCount: 150,
|
guestCount: 150,
|
||||||
blackout: false,
|
blackout: false,
|
||||||
djHat: 'None' // 'None', 'Santa', 'Top Hat'
|
djHat: 'None', // 'None', 'Santa', 'Top Hat'
|
||||||
|
debugPanelEnabled: false
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const saved = localStorage.getItem('partyConfig');
|
const saved = localStorage.getItem('partyConfig');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user