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:
Michal Vrba
2026-02-13 20:38:50 +01:00
parent 2926f739ee
commit 8bf3978354
3 changed files with 63 additions and 6 deletions
+56 -1
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using System.Threading;
@@ -32,6 +33,11 @@ public sealed class AudioAnalyzer : IDisposable
private float _smoothedHigh;
private float _smoothedBeat;
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 bool _disposed;
@@ -217,6 +223,31 @@ public sealed class AudioAnalyzer : IDisposable
_smoothedHigh = Smooth(_smoothedHigh, highEnergy, 0.30f, 0.08f);
_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()
{
Level = _smoothedLevel,
@@ -224,8 +255,9 @@ public sealed class AudioAnalyzer : IDisposable
Mid = _smoothedMid,
High = _smoothedHigh,
Beat = _smoothedBeat,
Bpm = _smoothedBpm,
Spectrum = spectrum,
TimeSeconds = (float)_clock.Elapsed.TotalSeconds
TimeSeconds = now
};
Volatile.Write(ref _latestSnapshot, new Snapshot(features));
@@ -264,6 +296,29 @@ public sealed class AudioAnalyzer : IDisposable
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()
{
if (_disposed)
+6 -5
View File
@@ -166,7 +166,7 @@ public class Game1 : Game
_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;
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, "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, "PAUSED " + (_isPaused ? "YES" : "NO"), 12, 150, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "FPS " + _fps, 12, 170, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "QUALITY " + _renderQuality + " 1 2 3", 12, 190, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "F OR F11 FULLSCREEN R RESIZE", 12, 210, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "BPM " + _latestFeatures.Bpm.ToString("0.0"), 12, 150, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "PAUSED " + (_isPaused ? "YES" : "NO"), 12, 170, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "FPS " + _fps, 12, 190, 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();
}
@@ -7,6 +7,7 @@ public struct AudioFeatures
public float Mid;
public float High;
public float Beat;
public float Bpm;
public float[] Spectrum;
public float TimeSeconds;