Add real-time note detection and NOTEDROP visualization

Implemented musical note detection in AudioAnalyzer, exposing dominant note and strength in AudioFeatures. Updated HUD to display detected note. Added "NOTEDROP" fractal preset that animates detected notes as colored glyphs. Extended bitmap font to support sharps. Increased fractal preset count and adjusted HUD layout.
This commit is contained in:
Michal Vrba
2026-02-13 20:43:29 +01:00
parent 8bf3978354
commit 6a1bdfb935
5 changed files with 221 additions and 5 deletions
@@ -18,11 +18,28 @@ public sealed class FractalRenderer : IDisposable
private Color[] _postBuffer;
private Color[] _previousFrame;
private readonly List<NoteParticle> _notes = [];
private readonly List<DetectedNoteParticle> _detectedNotes = [];
private readonly List<FireworkRocket> _fireworkRockets = [];
private readonly List<FireworkSpark> _fireworkSparks = [];
private readonly Random _rng = new();
private static readonly string[] NoteNames =
[
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
];
private static readonly Dictionary<char, string[]> NoteGlyphs = new()
{
['A'] = ["01110", "10001", "10001", "11111", "10001", "10001", "10001"],
['B'] = ["11110", "10001", "10001", "11110", "10001", "10001", "11110"],
['C'] = ["01111", "10000", "10000", "10000", "10000", "10000", "01111"],
['D'] = ["11110", "10001", "10001", "10001", "10001", "10001", "11110"],
['E'] = ["11111", "10000", "10000", "11110", "10000", "10000", "11111"],
['F'] = ["11111", "10000", "10000", "11110", "10000", "10000", "10000"],
['G'] = ["01111", "10000", "10000", "10111", "10001", "10001", "01111"],
['#'] = ["01010", "11111", "01010", "01010", "11111", "01010", "00000"]
};
private float _noteSpawnAccumulator;
private float _detectedNoteAccumulator;
private float _fireworkSpawnAccumulator;
private float _runtimePerformanceScale = 1f;
private RenderQuality _quality = RenderQuality.Medium;
@@ -114,9 +131,11 @@ public sealed class FractalRenderer : IDisposable
_postBuffer = new Color[_internalWidth * _internalHeight];
_previousFrame = new Color[_internalWidth * _internalHeight];
_notes.Clear();
_detectedNotes.Clear();
_fireworkRockets.Clear();
_fireworkSparks.Clear();
_noteSpawnAccumulator = 0f;
_detectedNoteAccumulator = 0f;
_fireworkSpawnAccumulator = 0f;
}
@@ -201,6 +220,9 @@ public sealed class FractalRenderer : IDisposable
case 19:
RenderGrainBars(features, timeSeconds);
break;
case 20:
RenderDetectedNotes(features, timeSeconds);
break;
default:
RenderJulia(features, timeSeconds);
break;
@@ -1158,6 +1180,116 @@ public sealed class FractalRenderer : IDisposable
}
}
private void RenderDetectedNotes(AudioFeatures features, float timeSeconds)
{
for (int y = 0; y < _internalHeight; y++)
{
float t = y / (float)(_internalHeight - 1);
Color bg = new(0.02f, 0.02f + 0.04f * (1f - t), 0.05f + 0.10f * (1f - t), 1f);
int row = y * _internalWidth;
for (int x = 0; x < _internalWidth; x++)
{
_pixelBuffer[row + x] = bg;
}
}
float spawnRate = features.DetectedNoteStrength * (0.3f + Clamp01(features.Beat) * 1.3f);
_detectedNoteAccumulator += spawnRate * _runtimePerformanceScale;
while (_detectedNoteAccumulator >= 1f)
{
SpawnDetectedNote(features);
_detectedNoteAccumulator -= 1f;
}
for (int i = _detectedNotes.Count - 1; i >= 0; i--)
{
DetectedNoteParticle p = _detectedNotes[i];
p.X += p.Vx;
p.Y += p.Vy;
p.Vy -= 0.00002f;
p.Life -= 0.008f + (1f - features.DetectedNoteStrength) * 0.006f;
if (p.Life <= 0f || p.X < -0.2f || p.X > 1.2f || p.Y < -0.2f)
{
_detectedNotes.RemoveAt(i);
continue;
}
_detectedNotes[i] = p;
DrawDetectedNoteText(p);
}
}
private void SpawnDetectedNote(AudioFeatures features)
{
if (features.DetectedNoteIndex < 0 || features.DetectedNoteIndex > 11)
{
return;
}
if (_detectedNotes.Count >= ScaleCount(96, 20))
{
return;
}
string text = NoteNames[features.DetectedNoteIndex];
float x = 0.1f + (float)_rng.NextDouble() * 0.8f;
float y = 0.25f + (float)_rng.NextDouble() * 0.65f;
float vx = ((float)_rng.NextDouble() - 0.5f) * 0.0022f;
float vy = -(0.0014f + (float)_rng.NextDouble() * 0.0025f);
float scale = 1.2f + (float)_rng.NextDouble() * (1.8f + features.DetectedNoteStrength * 1.6f);
float hue = features.DetectedNoteIndex / 12f;
Vector3 color = new(
0.5f + 0.5f * MathF.Sin(hue * MathF.Tau),
0.5f + 0.5f * MathF.Sin(hue * MathF.Tau + 2.09f),
0.5f + 0.5f * MathF.Sin(hue * MathF.Tau + 4.18f));
_detectedNotes.Add(new DetectedNoteParticle
{
X = x,
Y = y,
Vx = vx,
Vy = vy,
Life = 1f,
Scale = scale,
Text = text,
Color = color
});
}
private void DrawDetectedNoteText(DetectedNoteParticle p)
{
int px = (int)(p.X * _internalWidth);
int py = (int)(p.Y * _internalHeight);
int scale = Math.Max(1, (int)MathF.Round(p.Scale));
for (int i = 0; i < p.Text.Length; i++)
{
DrawGlyph(p.Text[i], px + i * (6 * scale), py, scale, p.Color, p.Life);
}
}
private void DrawGlyph(char c, int x, int y, int scale, Vector3 color, float alpha)
{
if (!NoteGlyphs.TryGetValue(c, out string[]? glyph))
{
return;
}
for (int row = 0; row < glyph.Length; row++)
{
string line = glyph[row];
for (int col = 0; col < line.Length; col++)
{
if (line[col] != '1')
{
continue;
}
DrawRect(x + col * scale, y + row * scale, scale, scale, color, alpha);
}
}
}
private void SpawnFireworkRocket(float intensity)
{
if (_fireworkRockets.Count >= ScaleCount(42, 12))
@@ -1478,6 +1610,18 @@ public sealed class FractalRenderer : IDisposable
public bool HasFlag;
}
private struct DetectedNoteParticle
{
public float X;
public float Y;
public float Vx;
public float Vy;
public float Life;
public float Scale;
public string Text;
public Vector3 Color;
}
private struct FireworkRocket
{
public float X;