diff --git a/FractalVisualizer/Audio/AudioAnalyzer.cs b/FractalVisualizer/Audio/AudioAnalyzer.cs index a8bf528..e449038 100644 --- a/FractalVisualizer/Audio/AudioAnalyzer.cs +++ b/FractalVisualizer/Audio/AudioAnalyzer.cs @@ -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 _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) diff --git a/FractalVisualizer/Game1.cs b/FractalVisualizer/Game1.cs index 61a11ae..89026f3 100644 --- a/FractalVisualizer/Game1.cs +++ b/FractalVisualizer/Game1.cs @@ -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(); } diff --git a/FractalVisualizer/Models/AudioFeatures.cs b/FractalVisualizer/Models/AudioFeatures.cs index 7476e30..fbd3475 100644 --- a/FractalVisualizer/Models/AudioFeatures.cs +++ b/FractalVisualizer/Models/AudioFeatures.cs @@ -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;