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)