Add real-time chord detection and visualization mode

Implemented chroma-based major/minor chord detection in AudioAnalyzer and exposed results in AudioFeatures. Updated HUD to display detected chord names. Added a new "CHORDS" fractal preset that visualizes the current chord with large pixel text, modulated by confidence and beat. Extended pixel font for chord display. Ensured graceful handling when no chord is detected. These changes enhance both analysis and visual feedback for harmonic content.
This commit is contained in:
Michal Vrba
2026-02-13 20:52:02 +01:00
parent 6a1bdfb935
commit 2d535addd0
4 changed files with 188 additions and 7 deletions
+94
View File
@@ -19,6 +19,7 @@ public sealed class AudioAnalyzer : IDisposable
private readonly Complex[] _fftBuffer;
private readonly float[] _spectrumAccumulator;
private readonly int[] _spectrumAccumulatorCounts;
private readonly float[] _chroma = new float[12];
private readonly Stopwatch _clock = new();
private readonly object _lifecycleSync = new();
@@ -38,6 +39,8 @@ public sealed class AudioAnalyzer : IDisposable
private bool _beatGateOpen = true;
private readonly Queue<float> _beatIntervals = new();
private const int MaxBeatIntervals = 8;
private int _lastChordRoot = -1;
private int _lastChordType = -1;
private Snapshot _latestSnapshot;
private bool _disposed;
@@ -170,6 +173,7 @@ public sealed class AudioAnalyzer : IDisposable
float highEnergy = 0f;
float dominantMagnitude = 0f;
float dominantFrequency = 0f;
Array.Clear(_chroma);
for (int bin = 1; bin < half; bin++)
{
@@ -203,6 +207,15 @@ public sealed class AudioAnalyzer : IDisposable
dominantMagnitude = magnitude;
dominantFrequency = frequency;
}
if (frequency >= 60f && frequency <= 5000f)
{
int pitchClass = GetNoteIndexFromFrequency(frequency);
if (pitchClass >= 0)
{
_chroma[pitchClass] += magnitude;
}
}
}
float[] spectrum = new float[_spectrumBins];
@@ -258,6 +271,7 @@ public sealed class AudioAnalyzer : IDisposable
int noteIndex = GetNoteIndexFromFrequency(dominantFrequency);
float dominantStrength = Clamp01(dominantMagnitude * 0.02f);
DetectChord(_chroma, out int chordRoot, out int chordType, out float chordConfidence);
AudioFeatures features = new()
{
@@ -270,6 +284,9 @@ public sealed class AudioAnalyzer : IDisposable
DominantFrequencyHz = dominantFrequency,
DetectedNoteIndex = noteIndex,
DetectedNoteStrength = dominantStrength,
DetectedChordRootIndex = chordRoot,
DetectedChordType = chordType,
DetectedChordConfidence = chordConfidence,
Spectrum = spectrum,
TimeSeconds = now
};
@@ -350,6 +367,83 @@ public sealed class AudioAnalyzer : IDisposable
return note;
}
private void DetectChord(float[] chroma, out int root, out int type, out float confidence)
{
float total = 0f;
for (int i = 0; i < chroma.Length; i++)
{
total += chroma[i];
}
if (total <= 1e-6f)
{
root = -1;
type = -1;
confidence = 0f;
return;
}
float bestScore = 0f;
int bestRoot = -1;
int bestType = -1;
for (int r = 0; r < 12; r++)
{
float major = ScoreChord(chroma, r, 0);
if (major > bestScore)
{
bestScore = major;
bestRoot = r;
bestType = 0;
}
float minor = ScoreChord(chroma, r, 1);
if (minor > bestScore)
{
bestScore = minor;
bestRoot = r;
bestType = 1;
}
}
float conf = Clamp01(bestScore / (total + 1e-6f));
if (conf < 0.17f)
{
root = -1;
type = -1;
confidence = conf;
return;
}
if (_lastChordRoot >= 0 && _lastChordType >= 0)
{
float held = ScoreChord(chroma, _lastChordRoot, _lastChordType);
if ((bestRoot != _lastChordRoot || bestType != _lastChordType) && held * 1.08f > bestScore)
{
bestRoot = _lastChordRoot;
bestType = _lastChordType;
bestScore = held;
conf = Clamp01(bestScore / (total + 1e-6f));
}
}
_lastChordRoot = bestRoot;
_lastChordType = bestType;
root = bestRoot;
type = bestType;
confidence = conf;
}
private static float ScoreChord(float[] chroma, int root, int type)
{
int third = type == 0 ? (root + 4) % 12 : (root + 3) % 12;
int fifth = (root + 7) % 12;
int second = (root + 2) % 12;
int fourth = (root + 5) % 12;
float score = chroma[root] * 1.0f + chroma[third] * 0.9f + chroma[fifth] * 0.8f;
score -= (chroma[second] + chroma[fourth]) * 0.15f;
return Math.Max(0f, score);
}
private static float Clamp01(float value)
{
if (value < 0f)