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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user