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:
@@ -26,7 +26,7 @@ public sealed class FractalRenderer : IDisposable
|
||||
[
|
||||
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
|
||||
];
|
||||
private static readonly Dictionary<char, string[]> NoteGlyphs = new()
|
||||
private static readonly Dictionary<char, string[]> TextGlyphs = new()
|
||||
{
|
||||
['A'] = ["01110", "10001", "10001", "11111", "10001", "10001", "10001"],
|
||||
['B'] = ["11110", "10001", "10001", "11110", "10001", "10001", "11110"],
|
||||
@@ -35,12 +35,20 @@ public sealed class FractalRenderer : IDisposable
|
||||
['E'] = ["11111", "10000", "10000", "11110", "10000", "10000", "11111"],
|
||||
['F'] = ["11111", "10000", "10000", "11110", "10000", "10000", "10000"],
|
||||
['G'] = ["01111", "10000", "10000", "10111", "10001", "10001", "01111"],
|
||||
['I'] = ["01110", "00100", "00100", "00100", "00100", "00100", "01110"],
|
||||
['J'] = ["00001", "00001", "00001", "00001", "10001", "10001", "01110"],
|
||||
['M'] = ["10001", "11011", "10101", "10001", "10001", "10001", "10001"],
|
||||
['N'] = ["10001", "11001", "10101", "10011", "10001", "10001", "10001"],
|
||||
['#'] = ["01010", "11111", "01010", "01010", "11111", "01010", "00000"]
|
||||
,['-'] = ["00000", "00000", "00000", "01110", "00000", "00000", "00000"]
|
||||
,[' '] = ["00000", "00000", "00000", "00000", "00000", "00000", "00000"]
|
||||
};
|
||||
|
||||
private float _noteSpawnAccumulator;
|
||||
private float _detectedNoteAccumulator;
|
||||
private float _fireworkSpawnAccumulator;
|
||||
private int _lastChordCode = -1;
|
||||
private float _chordFlash;
|
||||
private float _runtimePerformanceScale = 1f;
|
||||
private RenderQuality _quality = RenderQuality.Medium;
|
||||
private int _lastViewportWidth;
|
||||
@@ -223,6 +231,9 @@ public sealed class FractalRenderer : IDisposable
|
||||
case 20:
|
||||
RenderDetectedNotes(features, timeSeconds);
|
||||
break;
|
||||
case 21:
|
||||
RenderChordCenter(features, timeSeconds);
|
||||
break;
|
||||
default:
|
||||
RenderJulia(features, timeSeconds);
|
||||
break;
|
||||
@@ -1220,6 +1231,47 @@ public sealed class FractalRenderer : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderChordCenter(AudioFeatures features, float timeSeconds)
|
||||
{
|
||||
for (int y = 0; y < _internalHeight; y++)
|
||||
{
|
||||
float t = y / (float)(_internalHeight - 1);
|
||||
Color bg = new(0.02f + 0.03f * t, 0.02f + 0.02f * t, 0.05f + 0.08f * (1f - t), 1f);
|
||||
int row = y * _internalWidth;
|
||||
for (int x = 0; x < _internalWidth; x++)
|
||||
{
|
||||
_pixelBuffer[row + x] = bg;
|
||||
}
|
||||
}
|
||||
|
||||
int chordCode = features.DetectedChordRootIndex < 0 ? -1 : (features.DetectedChordRootIndex * 2 + (features.DetectedChordType == 1 ? 1 : 0));
|
||||
if (chordCode != _lastChordCode)
|
||||
{
|
||||
_lastChordCode = chordCode;
|
||||
_chordFlash = 1f;
|
||||
}
|
||||
else
|
||||
{
|
||||
_chordFlash *= 0.94f;
|
||||
}
|
||||
|
||||
string chordText = GetChordText(features);
|
||||
float strength = Clamp01(features.DetectedChordConfidence * 1.8f);
|
||||
int scale = 4 + (int)MathF.Round(strength * 5f + _chordFlash * 2f);
|
||||
int textWidth = chordText.Length * 6 * scale;
|
||||
int x0 = (_internalWidth - textWidth) / 2;
|
||||
int y0 = (_internalHeight - 7 * scale) / 2;
|
||||
|
||||
float pulse = 0.25f + _chordFlash * 0.45f + Clamp01(features.Beat) * 0.2f;
|
||||
Vector3 textColor = new(
|
||||
Clamp01(0.45f + strength * 0.55f + pulse * 0.2f),
|
||||
Clamp01(0.5f + strength * 0.45f),
|
||||
Clamp01(0.8f + pulse * 0.3f));
|
||||
|
||||
DrawRect(x0 - 8, y0 - 8, textWidth + 16, 7 * scale + 16, new Vector3(0.08f, 0.1f, 0.16f), 0.8f);
|
||||
DrawTextPixels(chordText, x0, y0, scale, textColor, 0.95f);
|
||||
}
|
||||
|
||||
private void SpawnDetectedNote(AudioFeatures features)
|
||||
{
|
||||
if (features.DetectedNoteIndex < 0 || features.DetectedNoteIndex > 11)
|
||||
@@ -1270,7 +1322,7 @@ public sealed class FractalRenderer : IDisposable
|
||||
|
||||
private void DrawGlyph(char c, int x, int y, int scale, Vector3 color, float alpha)
|
||||
{
|
||||
if (!NoteGlyphs.TryGetValue(c, out string[]? glyph))
|
||||
if (!TextGlyphs.TryGetValue(c, out string[]? glyph))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1290,6 +1342,25 @@ public sealed class FractalRenderer : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTextPixels(string text, int x, int y, int scale, Vector3 color, float alpha)
|
||||
{
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
{
|
||||
DrawGlyph(text[i], x + i * (6 * scale), y, scale, color, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetChordText(AudioFeatures features)
|
||||
{
|
||||
if (features.DetectedChordRootIndex < 0 || features.DetectedChordRootIndex > 11)
|
||||
{
|
||||
return "--";
|
||||
}
|
||||
|
||||
string root = NoteNames[features.DetectedChordRootIndex];
|
||||
return features.DetectedChordType == 1 ? root + " MIN" : root + " MAJ";
|
||||
}
|
||||
|
||||
private void SpawnFireworkRocket(float intensity)
|
||||
{
|
||||
if (_fireworkRockets.Count >= ScaleCount(42, 12))
|
||||
|
||||
Reference in New Issue
Block a user