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:
@@ -168,6 +168,8 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
float bassEnergy = 0f;
|
||||
float midEnergy = 0f;
|
||||
float highEnergy = 0f;
|
||||
float dominantMagnitude = 0f;
|
||||
float dominantFrequency = 0f;
|
||||
|
||||
for (int bin = 1; bin < half; bin++)
|
||||
{
|
||||
@@ -195,6 +197,12 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
{
|
||||
highEnergy += magnitude;
|
||||
}
|
||||
|
||||
if (frequency >= 80f && frequency <= 2000f && magnitude > dominantMagnitude)
|
||||
{
|
||||
dominantMagnitude = magnitude;
|
||||
dominantFrequency = frequency;
|
||||
}
|
||||
}
|
||||
|
||||
float[] spectrum = new float[_spectrumBins];
|
||||
@@ -248,6 +256,9 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
float bpmInstant = CalculateBpm();
|
||||
_smoothedBpm = Smooth(_smoothedBpm, bpmInstant, 0.18f, 0.04f);
|
||||
|
||||
int noteIndex = GetNoteIndexFromFrequency(dominantFrequency);
|
||||
float dominantStrength = Clamp01(dominantMagnitude * 0.02f);
|
||||
|
||||
AudioFeatures features = new()
|
||||
{
|
||||
Level = _smoothedLevel,
|
||||
@@ -256,6 +267,9 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
High = _smoothedHigh,
|
||||
Beat = _smoothedBeat,
|
||||
Bpm = _smoothedBpm,
|
||||
DominantFrequencyHz = dominantFrequency,
|
||||
DetectedNoteIndex = noteIndex,
|
||||
DetectedNoteStrength = dominantStrength,
|
||||
Spectrum = spectrum,
|
||||
TimeSeconds = now
|
||||
};
|
||||
@@ -319,6 +333,38 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
return Math.Clamp(bpm, 40f, 220f);
|
||||
}
|
||||
|
||||
private static int GetNoteIndexFromFrequency(float frequencyHz)
|
||||
{
|
||||
if (frequencyHz <= 0f)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
float midi = 69f + (12f * MathF.Log2(frequencyHz / 440f));
|
||||
int note = ((int)MathF.Round(midi)) % 12;
|
||||
if (note < 0)
|
||||
{
|
||||
note += 12;
|
||||
}
|
||||
|
||||
return note;
|
||||
}
|
||||
|
||||
private static float Clamp01(float value)
|
||||
{
|
||||
if (value < 0f)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
if (value > 1f)
|
||||
{
|
||||
return 1f;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
|
||||
@@ -145,7 +145,7 @@ public class Game1 : Game
|
||||
|
||||
if (IsPressed(keyboard, Keys.Right))
|
||||
{
|
||||
_fractalPresetIndex = Math.Min(19, _fractalPresetIndex + 1);
|
||||
_fractalPresetIndex = Math.Min(20, _fractalPresetIndex + 1);
|
||||
}
|
||||
|
||||
if (IsPressed(keyboard, Keys.M))
|
||||
@@ -194,10 +194,11 @@ public class Game1 : Game
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "HIGH " + _latestFeatures.High.ToString("0.000"), 12, 110, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "BEAT " + _latestFeatures.Beat.ToString("0.000"), 12, 130, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "BPM " + _latestFeatures.Bpm.ToString("0.0"), 12, 150, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "PAUSED " + (_isPaused ? "YES" : "NO"), 12, 170, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "FPS " + _fps, 12, 190, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "QUALITY " + _renderQuality + " 1 2 3", 12, 210, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "F OR F11 FULLSCREEN R RESIZE", 12, 230, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "NOTE " + GetDetectedNoteName(_latestFeatures.DetectedNoteIndex), 12, 170, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "PAUSED " + (_isPaused ? "YES" : "NO"), 12, 190, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "FPS " + _fps, 12, 210, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "QUALITY " + _renderQuality + " 1 2 3", 12, 230, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "F OR F11 FULLSCREEN R RESIZE", 12, 250, 2, Color.White);
|
||||
_spriteBatch.End();
|
||||
}
|
||||
|
||||
@@ -247,10 +248,31 @@ public class Game1 : Game
|
||||
17 => "MOSAIC",
|
||||
18 => "RIBBON",
|
||||
19 => "GRAINBARS",
|
||||
20 => "NOTEDROP",
|
||||
_ => "JULIA"
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetDetectedNoteName(int noteIndex)
|
||||
{
|
||||
return noteIndex switch
|
||||
{
|
||||
0 => "C",
|
||||
1 => "C#",
|
||||
2 => "D",
|
||||
3 => "D#",
|
||||
4 => "E",
|
||||
5 => "F",
|
||||
6 => "F#",
|
||||
7 => "G",
|
||||
8 => "G#",
|
||||
9 => "A",
|
||||
10 => "A#",
|
||||
11 => "B",
|
||||
_ => "-"
|
||||
};
|
||||
}
|
||||
|
||||
private void ToggleFullScreen()
|
||||
{
|
||||
_graphics.ToggleFullScreen();
|
||||
|
||||
@@ -8,6 +8,9 @@ public struct AudioFeatures
|
||||
public float High;
|
||||
public float Beat;
|
||||
public float Bpm;
|
||||
public float DominantFrequencyHz;
|
||||
public int DetectedNoteIndex;
|
||||
public float DetectedNoteStrength;
|
||||
public float[] Spectrum;
|
||||
public float TimeSeconds;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -44,6 +44,7 @@ public sealed class HudBitmapFont
|
||||
['7'] = ["11111", "00001", "00010", "00100", "01000", "01000", "01000"],
|
||||
['8'] = ["01110", "10001", "10001", "01110", "10001", "10001", "01110"],
|
||||
['9'] = ["01110", "10001", "10001", "01111", "00001", "00001", "01110"],
|
||||
['#'] = ["01010", "11111", "01010", "01010", "11111", "01010", "00000"],
|
||||
['.'] = ["00000", "00000", "00000", "00000", "00000", "00110", "00110"],
|
||||
['-'] = ["00000", "00000", "00000", "01110", "00000", "00000", "00000"],
|
||||
[':'] = ["00000", "00110", "00110", "00000", "00110", "00110", "00000"],
|
||||
|
||||
Reference in New Issue
Block a user