Feature: complex BPM detection via multiple approaches
This commit is contained in:
parent
48fa197b69
commit
89dc5db53c
@ -38,6 +38,17 @@ export class MusicVisualizer extends SceneFeature {
|
||||
this.beatPhase = 0;
|
||||
this.measurePhase = 0;
|
||||
this.averageLoudness = 0;
|
||||
this.prevLoudness = 0;
|
||||
this.prevLoudnessLows = 0;
|
||||
this.prevLoudnessHighs = 0;
|
||||
this.beatSignalAvg = 0;
|
||||
|
||||
// 3 Detectors: Lows (Bass), Highs (Snare/Hats), Flux (All)
|
||||
this.detectors = [
|
||||
{ name: 'low', lastTime: 0, threshold: 0.3, intervals: [], bpm: 120, weight: 1.0 },
|
||||
{ name: 'high', lastTime: 0, threshold: 0.3, intervals: [], bpm: 120, weight: 0.5 },
|
||||
{ name: 'flux', lastTime: 0, threshold: 0.3, intervals: [], bpm: 120, weight: 0.8 }
|
||||
];
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
@ -95,40 +106,77 @@ export class MusicVisualizer extends SceneFeature {
|
||||
state.music.averageLoudness = this.averageLoudness;
|
||||
|
||||
// --- Beat Detection & Auto-BPM ---
|
||||
// Use Lows (Bass) for clearer beat detection
|
||||
const beatSignal = state.music.loudnessLows || 0;
|
||||
const beatAvg = state.music.loudnessLowsAverage || 0;
|
||||
|
||||
// Decay threshold based on time
|
||||
this.beatThreshold -= deltaTime * 0.5;
|
||||
const thresholdFloor = Math.max(0.15, beatAvg * 1.1);
|
||||
if (this.beatThreshold < thresholdFloor) this.beatThreshold = thresholdFloor;
|
||||
|
||||
state.music.beatThreshold = this.beatThreshold;
|
||||
// 1. Calculate derivatives for 3 bands
|
||||
const dLows = Math.max(0, state.music.loudnessLows - this.prevLoudnessLows);
|
||||
const dHighs = Math.max(0, state.music.loudnessHighs - this.prevLoudnessHighs);
|
||||
const dAll = Math.max(0, state.music.loudness - this.prevLoudness);
|
||||
|
||||
this.prevLoudnessLows = state.music.loudnessLows;
|
||||
this.prevLoudnessHighs = state.music.loudnessHighs;
|
||||
this.prevLoudness = state.music.loudness;
|
||||
|
||||
const signals = [dLows * 10.0, dHighs * 10.0, dAll * 10.0];
|
||||
const now = time;
|
||||
const lastBeatInterval = now - this.lastBeatTime;
|
||||
if (beatSignal > this.beatThreshold) {
|
||||
if (lastBeatInterval > 0.25) { // Min interval (~240 BPM)
|
||||
this.lastBeatTime = now;
|
||||
this.beatThreshold = beatSignal * 1.2; // Bump threshold
|
||||
let mainTrigger = false;
|
||||
|
||||
// Add only valid BPM range into history of beats
|
||||
if (lastBeatInterval >= 0.2 && lastBeatInterval <= 1.5) {
|
||||
this.beatIntervals.push(lastBeatInterval);
|
||||
if (this.beatIntervals.length > 8) this.beatIntervals.shift();
|
||||
// 2. Update Detectors
|
||||
this.detectors.forEach((d, i) => {
|
||||
const signal = signals[i];
|
||||
|
||||
// Decay threshold
|
||||
d.threshold -= deltaTime * 1.5;
|
||||
const floor = 0.15;
|
||||
if (d.threshold < floor) d.threshold = floor;
|
||||
|
||||
const avgInterval = this.beatIntervals.reduce((a, b) => a + b, 0) / this.beatIntervals.length;
|
||||
if (signal > d.threshold) {
|
||||
const interval = now - d.lastTime;
|
||||
// Debounce (Max 240 BPM)
|
||||
if (interval > 0.25) {
|
||||
d.lastTime = now;
|
||||
d.threshold = signal * 1.2;
|
||||
|
||||
// Valid interval (30 - 300 BPM)
|
||||
if (interval < 2.0) {
|
||||
d.intervals.push(interval);
|
||||
if (d.intervals.length > 8) d.intervals.shift();
|
||||
|
||||
const avgInterval = d.intervals.reduce((a, b) => a + b, 0) / d.intervals.length;
|
||||
d.bpm = 60 / avgInterval;
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (i === 0) mainTrigger = true; // Bass triggers visual beat
|
||||
}
|
||||
|
||||
// Sync phase on beat
|
||||
this.beatPhase = 0;
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Combine Results
|
||||
let bpmSum = 0;
|
||||
let weightSum = 0;
|
||||
this.detectors.forEach(d => {
|
||||
let bpm = d.bpm;
|
||||
// Normalize to 90-180 range for consensus
|
||||
if (bpm > 0) {
|
||||
while (bpm < 90) bpm *= 2;
|
||||
while (bpm > 180) bpm /= 2;
|
||||
bpmSum += bpm * d.weight;
|
||||
weightSum += d.weight;
|
||||
}
|
||||
});
|
||||
|
||||
if (weightSum > 0) {
|
||||
const targetBPM = bpmSum / weightSum;
|
||||
const targetDuration = 60 / targetBPM;
|
||||
state.music.beatDuration = THREE.MathUtils.lerp(state.music.beatDuration, targetDuration, deltaTime * 0.5);
|
||||
state.music.measureDuration = state.music.beatDuration * 4;
|
||||
state.music.bpm = Math.round(60 / state.music.beatDuration);
|
||||
}
|
||||
|
||||
// Visuals use the Bass detector for immediate feedback
|
||||
state.music.beatThreshold = this.detectors[0].threshold;
|
||||
const lastBeatInterval = now - this.detectors[0].lastTime;
|
||||
|
||||
if (mainTrigger) {
|
||||
this.beatPhase = 0;
|
||||
}
|
||||
|
||||
// --- Phase Accumulation ---
|
||||
|
||||
Loading…
Reference in New Issue
Block a user