Add real-time BPM detection and display to HUD and title
Implement real-time BPM (beats per minute) estimation in AudioAnalyzer by tracking beat intervals and smoothing the result. Display the current BPM in both the window title and the on-screen HUD. Update AudioFeatures to include a Bpm field. Adjust HUD layout to accommodate the new BPM information.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -32,6 +33,11 @@ public sealed class AudioAnalyzer : IDisposable
|
|||||||
private float _smoothedHigh;
|
private float _smoothedHigh;
|
||||||
private float _smoothedBeat;
|
private float _smoothedBeat;
|
||||||
private float _energyBaseline;
|
private float _energyBaseline;
|
||||||
|
private float _smoothedBpm;
|
||||||
|
private float _lastBeatTimeSeconds = -10f;
|
||||||
|
private bool _beatGateOpen = true;
|
||||||
|
private readonly Queue<float> _beatIntervals = new();
|
||||||
|
private const int MaxBeatIntervals = 8;
|
||||||
|
|
||||||
private Snapshot _latestSnapshot;
|
private Snapshot _latestSnapshot;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
@@ -217,6 +223,31 @@ public sealed class AudioAnalyzer : IDisposable
|
|||||||
_smoothedHigh = Smooth(_smoothedHigh, highEnergy, 0.30f, 0.08f);
|
_smoothedHigh = Smooth(_smoothedHigh, highEnergy, 0.30f, 0.08f);
|
||||||
_smoothedBeat = MathF.Max(beat, _smoothedBeat * 0.85f);
|
_smoothedBeat = MathF.Max(beat, _smoothedBeat * 0.85f);
|
||||||
|
|
||||||
|
float now = (float)_clock.Elapsed.TotalSeconds;
|
||||||
|
bool beatHit = beat > 0.62f && _beatGateOpen;
|
||||||
|
if (beatHit)
|
||||||
|
{
|
||||||
|
float delta = now - _lastBeatTimeSeconds;
|
||||||
|
if (delta > 0.22f && delta < 2.0f)
|
||||||
|
{
|
||||||
|
_beatIntervals.Enqueue(delta);
|
||||||
|
while (_beatIntervals.Count > MaxBeatIntervals)
|
||||||
|
{
|
||||||
|
_beatIntervals.Dequeue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastBeatTimeSeconds = now;
|
||||||
|
_beatGateOpen = false;
|
||||||
|
}
|
||||||
|
else if (beat < 0.20f)
|
||||||
|
{
|
||||||
|
_beatGateOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
float bpmInstant = CalculateBpm();
|
||||||
|
_smoothedBpm = Smooth(_smoothedBpm, bpmInstant, 0.18f, 0.04f);
|
||||||
|
|
||||||
AudioFeatures features = new()
|
AudioFeatures features = new()
|
||||||
{
|
{
|
||||||
Level = _smoothedLevel,
|
Level = _smoothedLevel,
|
||||||
@@ -224,8 +255,9 @@ public sealed class AudioAnalyzer : IDisposable
|
|||||||
Mid = _smoothedMid,
|
Mid = _smoothedMid,
|
||||||
High = _smoothedHigh,
|
High = _smoothedHigh,
|
||||||
Beat = _smoothedBeat,
|
Beat = _smoothedBeat,
|
||||||
|
Bpm = _smoothedBpm,
|
||||||
Spectrum = spectrum,
|
Spectrum = spectrum,
|
||||||
TimeSeconds = (float)_clock.Elapsed.TotalSeconds
|
TimeSeconds = now
|
||||||
};
|
};
|
||||||
|
|
||||||
Volatile.Write(ref _latestSnapshot, new Snapshot(features));
|
Volatile.Write(ref _latestSnapshot, new Snapshot(features));
|
||||||
@@ -264,6 +296,29 @@ public sealed class AudioAnalyzer : IDisposable
|
|||||||
return a + ((b - a) * t);
|
return a + ((b - a) * t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private float CalculateBpm()
|
||||||
|
{
|
||||||
|
if (_beatIntervals.Count == 0)
|
||||||
|
{
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float sum = 0f;
|
||||||
|
foreach (float interval in _beatIntervals)
|
||||||
|
{
|
||||||
|
sum += interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
float avg = sum / _beatIntervals.Count;
|
||||||
|
if (avg <= 1e-5f)
|
||||||
|
{
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float bpm = 60f / avg;
|
||||||
|
return Math.Clamp(bpm, 40f, 220f);
|
||||||
|
}
|
||||||
|
|
||||||
private void ThrowIfDisposed()
|
private void ThrowIfDisposed()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ public class Game1 : Game
|
|||||||
_fpsElapsed -= TimeSpan.FromSeconds(1);
|
_fpsElapsed -= TimeSpan.FromSeconds(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
Window.Title = $"Fractal Visualizer | {GetPresetName(_fractalPresetIndex)} | {_renderQuality} | Mode: {_currentInputMode} | FPS:{_fps}";
|
Window.Title = $"Fractal Visualizer | {GetPresetName(_fractalPresetIndex)} | {_renderQuality} | Mode: {_currentInputMode} | FPS:{_fps} | BPM:{_latestFeatures.Bpm:F1}";
|
||||||
_previousKeyboardState = keyboard;
|
_previousKeyboardState = keyboard;
|
||||||
base.Update(gameTime);
|
base.Update(gameTime);
|
||||||
}
|
}
|
||||||
@@ -193,10 +193,11 @@ public class Game1 : Game
|
|||||||
_hudFont.DrawText(_spriteBatch, _pixel, "MID " + _latestFeatures.Mid.ToString("0.000"), 12, 90, 2, Color.White);
|
_hudFont.DrawText(_spriteBatch, _pixel, "MID " + _latestFeatures.Mid.ToString("0.000"), 12, 90, 2, Color.White);
|
||||||
_hudFont.DrawText(_spriteBatch, _pixel, "HIGH " + _latestFeatures.High.ToString("0.000"), 12, 110, 2, Color.White);
|
_hudFont.DrawText(_spriteBatch, _pixel, "HIGH " + _latestFeatures.High.ToString("0.000"), 12, 110, 2, Color.White);
|
||||||
_hudFont.DrawText(_spriteBatch, _pixel, "BEAT " + _latestFeatures.Beat.ToString("0.000"), 12, 130, 2, Color.White);
|
_hudFont.DrawText(_spriteBatch, _pixel, "BEAT " + _latestFeatures.Beat.ToString("0.000"), 12, 130, 2, Color.White);
|
||||||
_hudFont.DrawText(_spriteBatch, _pixel, "PAUSED " + (_isPaused ? "YES" : "NO"), 12, 150, 2, Color.White);
|
_hudFont.DrawText(_spriteBatch, _pixel, "BPM " + _latestFeatures.Bpm.ToString("0.0"), 12, 150, 2, Color.White);
|
||||||
_hudFont.DrawText(_spriteBatch, _pixel, "FPS " + _fps, 12, 170, 2, Color.White);
|
_hudFont.DrawText(_spriteBatch, _pixel, "PAUSED " + (_isPaused ? "YES" : "NO"), 12, 170, 2, Color.White);
|
||||||
_hudFont.DrawText(_spriteBatch, _pixel, "QUALITY " + _renderQuality + " 1 2 3", 12, 190, 2, Color.White);
|
_hudFont.DrawText(_spriteBatch, _pixel, "FPS " + _fps, 12, 190, 2, Color.White);
|
||||||
_hudFont.DrawText(_spriteBatch, _pixel, "F OR F11 FULLSCREEN R RESIZE", 12, 210, 2, Color.White);
|
_hudFont.DrawText(_spriteBatch, _pixel, "QUALITY " + _renderQuality + " 1 2 3", 12, 210, 2, Color.White);
|
||||||
|
_hudFont.DrawText(_spriteBatch, _pixel, "F OR F11 FULLSCREEN R RESIZE", 12, 230, 2, Color.White);
|
||||||
_spriteBatch.End();
|
_spriteBatch.End();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ public struct AudioFeatures
|
|||||||
public float Mid;
|
public float Mid;
|
||||||
public float High;
|
public float High;
|
||||||
public float Beat;
|
public float Beat;
|
||||||
|
public float Bpm;
|
||||||
public float[] Spectrum;
|
public float[] Spectrum;
|
||||||
public float TimeSeconds;
|
public float TimeSeconds;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user