Files
SoundFlix/FractalVisualizer/Game1.cs
T
Michal Vrba 8bf3978354 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.
2026-02-13 20:38:50 +01:00

279 lines
9.0 KiB
C#

using System;
using FractalVisualizer.Audio;
using FractalVisualizer.Models;
using FractalVisualizer.Rendering;
using FractalVisualizer.UI;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace FractalVisualizer;
public class Game1 : Game
{
private readonly GraphicsDeviceManager _graphics;
private SpriteBatch? _spriteBatch;
private Texture2D? _pixel;
private HudBitmapFont? _hudFont;
private readonly AudioCapture _audioCapture;
private readonly AudioAnalyzer _audioAnalyzer;
private FractalRenderer? _fractalRenderer;
private KeyboardState _previousKeyboardState;
private AudioInputMode _currentInputMode;
private AudioFeatures _latestFeatures;
private bool _isPaused;
private bool _showHud = true;
private int _fractalPresetIndex;
private int _fps;
private int _frameCounter;
private TimeSpan _fpsElapsed;
private const int TargetFps = 120;
private RenderQuality _renderQuality = RenderQuality.Medium;
private readonly (int Width, int Height)[] _windowedResolutions =
[
(1280, 720),
(1600, 900),
(1920, 1080)
];
private int _resolutionIndex = 0;
public Game1()
{
_graphics = new GraphicsDeviceManager(this);
_graphics.SynchronizeWithVerticalRetrace = false;
_graphics.PreferredBackBufferWidth = 1280;
_graphics.PreferredBackBufferHeight = 720;
Content.RootDirectory = "Content";
IsMouseVisible = true;
Window.AllowUserResizing = true;
IsFixedTimeStep = true;
TargetElapsedTime = TimeSpan.FromSeconds(1.0 / 120.0);
_audioCapture = new AudioCapture();
_audioAnalyzer = new AudioAnalyzer(_audioCapture, fftSize: 2048, spectrumBins: 64);
_currentInputMode = AudioInputMode.Microphone;
_latestFeatures = AudioFeatures.CreateEmpty(64);
}
protected override void Initialize()
{
_audioCapture.Start();
_audioAnalyzer.Start();
Window.Title = "Fractal Visualizer";
base.Initialize();
}
protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);
_fractalRenderer = new FractalRenderer(GraphicsDevice);
_fractalRenderer.SetQuality(_renderQuality);
_pixel = new Texture2D(GraphicsDevice, 1, 1, false, SurfaceFormat.Color);
_pixel.SetData([Color.White]);
_hudFont = new HudBitmapFont();
}
protected override void Update(GameTime gameTime)
{
KeyboardState keyboard = Keyboard.GetState();
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
{
Exit();
return;
}
if (IsPressed(keyboard, Keys.Escape))
{
if (_graphics.IsFullScreen)
{
_graphics.ToggleFullScreen();
}
else
{
Exit();
return;
}
}
if (IsPressed(keyboard, Keys.F11))
{
ToggleFullScreen();
}
if (IsPressed(keyboard, Keys.F))
{
ToggleFullScreen();
}
if (IsPressed(keyboard, Keys.R))
{
CycleWindowResolution();
}
if (IsPressed(keyboard, Keys.D1))
{
SetRenderQuality(RenderQuality.Low);
}
if (IsPressed(keyboard, Keys.D2))
{
SetRenderQuality(RenderQuality.Medium);
}
if (IsPressed(keyboard, Keys.D3))
{
SetRenderQuality(RenderQuality.High);
}
if (IsPressed(keyboard, Keys.Space))
{
_isPaused = !_isPaused;
}
if (IsPressed(keyboard, Keys.Tab))
{
_showHud = !_showHud;
}
if (IsPressed(keyboard, Keys.Left))
{
_fractalPresetIndex = Math.Max(0, _fractalPresetIndex - 1);
}
if (IsPressed(keyboard, Keys.Right))
{
_fractalPresetIndex = Math.Min(19, _fractalPresetIndex + 1);
}
if (IsPressed(keyboard, Keys.M))
{
_currentInputMode = _currentInputMode == AudioInputMode.Microphone
? AudioInputMode.SystemOutput
: AudioInputMode.Microphone;
_audioCapture.SwitchMode(_currentInputMode);
}
_latestFeatures = _audioAnalyzer.GetLatestFeatures();
_fpsElapsed += gameTime.ElapsedGameTime;
_frameCounter++;
if (_fpsElapsed >= TimeSpan.FromSeconds(1))
{
_fps = _frameCounter;
_frameCounter = 0;
_fpsElapsed -= TimeSpan.FromSeconds(1);
}
Window.Title = $"Fractal Visualizer | {GetPresetName(_fractalPresetIndex)} | {_renderQuality} | Mode: {_currentInputMode} | FPS:{_fps} | BPM:{_latestFeatures.Bpm:F1}";
_previousKeyboardState = keyboard;
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
if (_fractalRenderer is not null)
{
_fractalRenderer.UpdatePerformanceHint(_fps, TargetFps);
float renderTime = _isPaused ? _latestFeatures.TimeSeconds : (float)gameTime.TotalGameTime.TotalSeconds;
_fractalRenderer.Draw(_latestFeatures, renderTime, _fractalPresetIndex);
}
if (_showHud && _spriteBatch is not null && _hudFont is not null && _pixel is not null)
{
_spriteBatch.Begin(samplerState: SamplerState.PointClamp, blendState: BlendState.AlphaBlend);
string modeText = _currentInputMode == AudioInputMode.Microphone ? "MIC" : "SYS";
_hudFont.DrawText(_spriteBatch, _pixel, "MODE " + modeText + " M SWITCH", 12, 10, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "PRESET " + GetPresetName(_fractalPresetIndex) + " LEFT RIGHT", 12, 30, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "LEVEL " + _latestFeatures.Level.ToString("0.000"), 12, 50, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "BASS " + _latestFeatures.Bass.ToString("0.000"), 12, 70, 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, "BEAT " + _latestFeatures.Beat.ToString("0.000"), 12, 130, 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();
}
base.Draw(gameTime);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_audioAnalyzer.Dispose();
_audioCapture.Dispose();
_fractalRenderer?.Dispose();
_pixel?.Dispose();
_spriteBatch?.Dispose();
}
base.Dispose(disposing);
}
private bool IsPressed(KeyboardState keyboardState, Keys key)
{
return keyboardState.IsKeyDown(key) && _previousKeyboardState.IsKeyUp(key);
}
private static string GetPresetName(int presetIndex)
{
return presetIndex switch
{
0 => "JULIA",
1 => "MANDELBULB",
2 => "KALEIDO",
3 => "FLAME",
4 => "PLASMA",
5 => "NEBULA",
6 => "FLAMEGRILL",
7 => "NOTES",
8 => "FIREWORKS",
9 => "EQCITY",
10 => "METABALL",
11 => "GALAXY",
12 => "AURORA",
13 => "VORTEX",
14 => "LASERGRID",
15 => "RAINRIPPLE",
16 => "INKSMOKE",
17 => "MOSAIC",
18 => "RIBBON",
19 => "GRAINBARS",
_ => "JULIA"
};
}
private void ToggleFullScreen()
{
_graphics.ToggleFullScreen();
}
private void SetRenderQuality(RenderQuality quality)
{
_renderQuality = quality;
_fractalRenderer?.SetQuality(quality);
}
private void CycleWindowResolution()
{
if (_graphics.IsFullScreen)
{
return;
}
_resolutionIndex = (_resolutionIndex + 1) % _windowedResolutions.Length;
(int width, int height) = _windowedResolutions[_resolutionIndex];
_graphics.PreferredBackBufferWidth = width;
_graphics.PreferredBackBufferHeight = height;
_graphics.ApplyChanges();
}
}