Improve chord/note detection, add tuning persistence/UI

Enhanced AudioAnalyzer with bass chroma, integrated chroma, and chord frame accumulation for more robust and stable chord detection. Added spectral peak note estimation and note continuity logic. Refined chord scoring and introduced chord lock mechanism to prevent rapid switching. Added methods for tuning reset, set, and snapshot retrieval. Updated SensitivityProfile with chord lock parameters.

In Game1, added keyboard shortcuts and JSON persistence for detection tuning profiles. Improved HUD with confidence bars and new indicators. These changes increase detection accuracy, stability, and user customization.
This commit is contained in:
Michal Vrba
2026-02-13 21:53:03 +01:00
parent 79c6267196
commit 069c2a84c5
2 changed files with 417 additions and 21 deletions
+86 -10
View File
@@ -1,4 +1,6 @@
using System;
using System.IO;
using System.Text.Json;
using FractalVisualizer.Audio;
using FractalVisualizer.Models;
using FractalVisualizer.Rendering;
@@ -40,6 +42,7 @@ public class Game1 : Game
(1920, 1080)
];
private int _resolutionIndex = 0;
private const string TuningProfilePath = "detection-tuning.json";
public Game1()
{
@@ -144,6 +147,21 @@ public class Game1 : Game
_showLegend = !_showLegend;
}
if (IsPressed(keyboard, Keys.Back))
{
_audioAnalyzer.ResetTuning(_currentInputMode);
}
if (IsPressed(keyboard, Keys.F5))
{
SaveTuningProfiles();
}
if (IsPressed(keyboard, Keys.F6))
{
LoadTuningProfiles();
}
if (IsPressed(keyboard, Keys.Left))
{
_fractalPresetIndex = Math.Max(0, _fractalPresetIndex - 1);
@@ -243,19 +261,24 @@ public class Game1 : Game
_hudFont.DrawText(_spriteBatch, _pixel, "BPM " + _latestFeatures.Bpm.ToString("0.0"), 12, 150, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "NOTE " + GetDetectedNoteName(_latestFeatures.DetectedNoteIndex), 12, 170, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "CHORD " + GetDetectedChordName(_latestFeatures.DetectedChordRootIndex, _latestFeatures.DetectedChordType, _latestFeatures.DetectedChordConfidence), 12, 190, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "PAUSED " + (_isPaused ? "YES" : "NO"), 12, 210, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "FPS " + _fps, 12, 230, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "QUALITY " + _renderQuality + " 1 2 3", 12, 250, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "F OR F11 FULLSCREEN R RESIZE H HELP", 12, 270, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "[ ] HARM " + tuning.ChordHarmonicityThreshold.ToString("0.000"), 12, 290, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "; ' SEP " + tuning.ChordSeparationThreshold.ToString("0.000"), 12, 310, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, ", . NCONS " + tuning.NoteVoteConsensus.ToString("0.000"), 12, 330, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "- + CCONS " + tuning.ChordVoteConsensus.ToString("0.000"), 12, 350, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "NOCHORD " + (_latestFeatures.DetectedChordRootIndex < 0 ? "YES" : "NO"), 12, 210, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "PAUSED " + (_isPaused ? "YES" : "NO"), 12, 230, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "FPS " + _fps, 12, 250, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "QUALITY " + _renderQuality + " 1 2 3", 12, 270, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "F OR F11 FULLSCREEN R RESIZE H HELP", 12, 290, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "[ ] HARM " + tuning.ChordHarmonicityThreshold.ToString("0.000"), 12, 310, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "; ' SEP " + tuning.ChordSeparationThreshold.ToString("0.000"), 12, 330, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, ", . NCONS " + tuning.NoteVoteConsensus.ToString("0.000"), 12, 350, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "- + CCONS " + tuning.ChordVoteConsensus.ToString("0.000"), 12, 370, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "BACK RESET MODE F5 SAVE F6 LOAD", 12, 390, 2, Color.White);
if (_showLegend)
{
_hudFont.DrawText(_spriteBatch, _pixel, "LEGEND SPACE PAUSE TAB HUD M INPUT <- -> PRESET", 12, 370, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "TUNING [ ] HARM ; ' SEP , . NOTE CONS - + CHORD CONS", 12, 390, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "LEGEND SPACE PAUSE TAB HUD M INPUT <- -> PRESET", 12, 410, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "TUNING [ ] HARM ; ' SEP , . NOTE CONS - + CHORD CONS", 12, 430, 2, Color.White);
}
DrawConfidenceBar(820, 14, 420, 12, _latestFeatures.DetectedNoteStrength, new Color(48, 72, 118), new Color(120, 220, 255), "NOTE CONF", 798, 12);
DrawConfidenceBar(820, 40, 420, 12, _latestFeatures.DetectedChordConfidence, new Color(64, 52, 48), new Color(255, 182, 92), "CHORD CONF", 790, 38);
_spriteBatch.End();
}
@@ -385,4 +408,57 @@ public class Game1 : Game
_graphics.PreferredBackBufferHeight = height;
_graphics.ApplyChanges();
}
private void DrawConfidenceBar(int x, int y, int width, int height, float value, Color bg, Color fg, string label, int labelX, int labelY)
{
if (_spriteBatch is null || _pixel is null || _hudFont is null)
{
return;
}
float clamped = Math.Clamp(value, 0f, 1f);
int fillWidth = (int)MathF.Round(width * clamped);
_spriteBatch.Draw(_pixel, new Rectangle(x, y, width, height), bg);
if (fillWidth > 0)
{
_spriteBatch.Draw(_pixel, new Rectangle(x, y, fillWidth, height), fg);
}
_spriteBatch.Draw(_pixel, new Rectangle(x - 1, y - 1, width + 2, 1), Color.White * 0.45f);
_spriteBatch.Draw(_pixel, new Rectangle(x - 1, y + height, width + 2, 1), Color.White * 0.45f);
_spriteBatch.Draw(_pixel, new Rectangle(x - 1, y - 1, 1, height + 2), Color.White * 0.45f);
_spriteBatch.Draw(_pixel, new Rectangle(x + width, y - 1, 1, height + 2), Color.White * 0.45f);
_hudFont.DrawText(_spriteBatch, _pixel, label + " " + clamped.ToString("0.000"), labelX, labelY, 2, Color.White);
}
private void SaveTuningProfiles()
{
TuningProfiles profile = new(
_audioAnalyzer.GetTuning(AudioInputMode.Microphone),
_audioAnalyzer.GetTuning(AudioInputMode.SystemOutput));
string json = JsonSerializer.Serialize(profile, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(TuningProfilePath, json);
}
private void LoadTuningProfiles()
{
if (!File.Exists(TuningProfilePath))
{
return;
}
string json = File.ReadAllText(TuningProfilePath);
TuningProfiles? profile = JsonSerializer.Deserialize<TuningProfiles>(json);
if (profile is null)
{
return;
}
_audioAnalyzer.SetTuning(AudioInputMode.Microphone, profile.Microphone);
_audioAnalyzer.SetTuning(AudioInputMode.SystemOutput, profile.SystemOutput);
}
private readonly record struct TuningProfiles(
AudioAnalyzer.DetectionTuningSnapshot Microphone,
AudioAnalyzer.DetectionTuningSnapshot SystemOutput);
}