using System; using System.Collections.Generic; using FractalVisualizer.Models; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace FractalVisualizer.Rendering; public sealed class FractalRenderer : IDisposable { private readonly GraphicsDevice _graphicsDevice; private readonly SpriteBatch _spriteBatch; private int _internalWidth; private int _internalHeight; private Texture2D _fractalTexture; private Color[] _pixelBuffer; private Color[] _postBuffer; private Color[] _previousFrame; private readonly List _notes = []; private readonly List _detectedNotes = []; private readonly List _fireworkRockets = []; private readonly List _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 TextGlyphs = 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"], ['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; private int _lastViewportHeight; private bool _disposed; public FractalRenderer(GraphicsDevice graphicsDevice, int internalWidth = 320, int internalHeight = 180) { _graphicsDevice = graphicsDevice ?? throw new ArgumentNullException(nameof(graphicsDevice)); _spriteBatch = new SpriteBatch(_graphicsDevice); _internalWidth = Math.Max(32, internalWidth); _internalHeight = Math.Max(18, internalHeight); _fractalTexture = new Texture2D(_graphicsDevice, _internalWidth, _internalHeight, false, SurfaceFormat.Color); _pixelBuffer = new Color[_internalWidth * _internalHeight]; _postBuffer = new Color[_internalWidth * _internalHeight]; _previousFrame = new Color[_internalWidth * _internalHeight]; _lastViewportWidth = _graphicsDevice.Viewport.Width; _lastViewportHeight = _graphicsDevice.Viewport.Height; } public RenderQuality Quality => _quality; public void SetQuality(RenderQuality quality) { if (_quality == quality) { return; } _quality = quality; ResizeForQuality(); } public void UpdatePerformanceHint(int currentFps, int targetFps) { if (targetFps <= 0) { _runtimePerformanceScale = 1f; return; } float ratio = currentFps / (float)targetFps; _runtimePerformanceScale = Math.Clamp(ratio, 0.55f, 1.15f); } private void EnsureRenderSurface() { Viewport viewport = _graphicsDevice.Viewport; if (viewport.Width == _lastViewportWidth && viewport.Height == _lastViewportHeight) { return; } _lastViewportWidth = viewport.Width; _lastViewportHeight = viewport.Height; ResizeForQuality(); } private void ResizeForQuality() { Viewport viewport = _graphicsDevice.Viewport; float scale = _quality switch { RenderQuality.Low => 0.35f, RenderQuality.Medium => 0.50f, RenderQuality.High => 0.75f, _ => 0.50f }; int width = Math.Max(64, (int)(viewport.Width * scale)); int height = Math.Max(36, (int)(viewport.Height * scale)); ResizeBuffers(width, height); } private void ResizeBuffers(int width, int height) { if (_internalWidth == width && _internalHeight == height) { return; } _fractalTexture.Dispose(); _internalWidth = width; _internalHeight = height; _fractalTexture = new Texture2D(_graphicsDevice, _internalWidth, _internalHeight, false, SurfaceFormat.Color); _pixelBuffer = new Color[_internalWidth * _internalHeight]; _postBuffer = new Color[_internalWidth * _internalHeight]; _previousFrame = new Color[_internalWidth * _internalHeight]; _notes.Clear(); _detectedNotes.Clear(); _fireworkRockets.Clear(); _fireworkSparks.Clear(); _noteSpawnAccumulator = 0f; _detectedNoteAccumulator = 0f; _fireworkSpawnAccumulator = 0f; } private int ScaleCount(int baseCount, int minCount = 1) { float qualityScale = _quality switch { RenderQuality.Low => 0.70f, RenderQuality.Medium => 1.0f, RenderQuality.High => 1.35f, _ => 1.0f }; int scaled = (int)MathF.Round(baseCount * qualityScale * _runtimePerformanceScale); return Math.Max(minCount, scaled); } public void Draw(AudioFeatures features, float timeSeconds, int presetIndex) { ThrowIfDisposed(); EnsureRenderSurface(); switch (presetIndex) { case 0: RenderJulia(features, timeSeconds); break; case 1: RenderMandelbulbStyle(features, timeSeconds); break; case 2: RenderKaleidoscopeTunnel(features, timeSeconds); break; case 3: RenderFlame(features, timeSeconds); break; case 4: RenderPlasma(features, timeSeconds); break; case 5: RenderNebula(features, timeSeconds); break; case 6: RenderFlameGrill(features, timeSeconds); break; case 7: RenderFloatingNotes(features, timeSeconds); break; case 8: RenderFireworks(features, timeSeconds); break; case 9: RenderEqualizerCity(features, timeSeconds); break; case 10: RenderLiquidMetaballs(features, timeSeconds); break; case 11: RenderParticleGalaxy(features, timeSeconds); break; case 12: RenderAuroraCurtains(features, timeSeconds); break; case 13: RenderVortexPortal(features, timeSeconds); break; case 14: RenderLaserGrid(features, timeSeconds); break; case 15: RenderRainRipple(features, timeSeconds); break; case 16: RenderInkSmoke(features, timeSeconds); break; case 17: RenderMirrorMosaic(features, timeSeconds); break; case 18: RenderWaveformRibbon(features, timeSeconds); break; case 19: RenderGrainBars(features, timeSeconds); break; case 20: RenderDetectedNotes(features, timeSeconds); break; case 21: RenderChordCenter(features, timeSeconds); break; default: RenderJulia(features, timeSeconds); break; } ApplyFeedbackAndCrt(features, timeSeconds); _fractalTexture.SetData(_postBuffer); Viewport viewport = _graphicsDevice.Viewport; Rectangle destination = new(0, 0, viewport.Width, viewport.Height); _spriteBatch.Begin(samplerState: SamplerState.LinearClamp, blendState: BlendState.Opaque); _spriteBatch.Draw(_fractalTexture, destination, Color.White); _spriteBatch.End(); } private void ApplyFeedbackAndCrt(AudioFeatures features, float timeSeconds) { float bass = Clamp01(features.Bass * 0.012f); float high = Clamp01(features.High * 0.003f); float beat = Clamp01(features.Beat); float lowSpec = GetSpectrumBand(features, 0.00f, 0.20f); float midSpec = GetSpectrumBand(features, 0.20f, 0.60f); float highSpec = GetSpectrumBand(features, 0.60f, 1.00f); float fullSpec = Clamp01((lowSpec + midSpec + highSpec) / 3f); int shiftPixels = 1 + (int)MathF.Round((high * 2f) + (highSpec * 3f) + (beat * 2f)); float trailStrength = Clamp01(0.14f + (bass * 0.20f) + (lowSpec * 0.20f) + (beat * 0.26f)); float glowBoost = (high * 0.05f) + (highSpec * 0.08f) + (midSpec * 0.05f) + (beat * 0.05f); for (int y = 0; y < _internalHeight; y++) { float ny = (y / (_internalHeight - 1f)) * 2f - 1f; float scanline = 0.90f + (0.10f * MathF.Sin(((y * 0.42f) + (timeSeconds * 22f)) * MathF.PI)); for (int x = 0; x < _internalWidth; x++) { int index = (y * _internalWidth) + x; Vector3 raw = _pixelBuffer[index].ToVector3(); Vector3 previous = _previousFrame[index].ToVector3(); Vector3 trailed = Vector3.Lerp(raw, previous, trailStrength); int xr = Math.Min(_internalWidth - 1, x + shiftPixels); int xb = Math.Max(0, x - shiftPixels); int rightIndex = (y * _internalWidth) + xr; int leftIndex = (y * _internalWidth) + xb; float r = _pixelBuffer[rightIndex].R / 255f; float g = trailed.Y; float b = _pixelBuffer[leftIndex].B / 255f; Vector3 crt = new(r, g, b); float nx = (x / (_internalWidth - 1f)) * 2f - 1f; float vignette = Clamp01(1f - ((nx * nx + ny * ny) * 0.42f)); float spectralIndex = ((x / (float)_internalWidth) * 63f) + (timeSeconds * 6f) + (y * 0.03f); float spectralPulse = GetSpectrumSample(features, spectralIndex); float hueWarpR = 1f + (lowSpec * 0.20f) + spectralPulse * 0.12f; float hueWarpG = 1f + (midSpec * 0.24f) + spectralPulse * 0.08f; float hueWarpB = 1f + (highSpec * 0.28f) + spectralPulse * 0.14f; crt *= scanline; crt *= 0.5f + (vignette * 0.5f); crt += raw * glowBoost; crt.X *= hueWarpR; crt.Y *= hueWarpG; crt.Z *= hueWarpB; crt += new Vector3(spectralPulse * 0.03f, spectralPulse * 0.02f, spectralPulse * 0.035f) * (0.3f + fullSpec); crt.X = Clamp01(crt.X); crt.Y = Clamp01(crt.Y); crt.Z = Clamp01(crt.Z); Color finalColor = new(crt.X, crt.Y, crt.Z, 1f); _postBuffer[index] = finalColor; _previousFrame[index] = finalColor; } } } private float GetSpectrumBand(AudioFeatures features, float normalizedStart, float normalizedEnd) { if (features.Spectrum is null || features.Spectrum.Length == 0) { return 0f; } int length = features.Spectrum.Length; int start = Math.Clamp((int)(normalizedStart * length), 0, length - 1); int end = Math.Clamp((int)(normalizedEnd * length), start + 1, length); float sum = 0f; int count = 0; for (int i = start; i < end; i++) { sum += features.Spectrum[i]; count++; } if (count == 0) { return 0f; } return Clamp01((sum / count) * 0.08f); } private void RenderMandelbulbStyle(AudioFeatures features, float timeSeconds) { float bass = Clamp01(features.Bass * 0.012f); float mid = Clamp01(features.Mid * 0.003f); float high = Clamp01(features.High * 0.0025f); float beat = Clamp01(features.Beat); float power = 6f + (bass * 4f); float detail = 28f + (high * 22f); float rotation = timeSeconds * (0.25f + mid * 0.8f); float aspect = _internalWidth / (float)_internalHeight; float cosR = MathF.Cos(rotation); float sinR = MathF.Sin(rotation); for (int y = 0; y < _internalHeight; y++) { float sy = (y / (float)_internalHeight) * 2f - 1f; for (int x = 0; x < _internalWidth; x++) { float sx = ((x / (float)_internalWidth) * 2f - 1f) * aspect; float rayX = sx; float rayY = sy; float rayZ = 1.65f - beat * 0.25f; float march = 0f; float glow = 0f; float minDist = 10f; int steps = ScaleCount((int)detail, 14); for (int step = 0; step < steps; step++) { float px = rayX * march; float py = rayY * march; float pz = rayZ - march; float rpx = (px * cosR) - (pz * sinR); float rpz = (px * sinR) + (pz * cosR); float d = MandelbulbDistance(rpx, py, rpz, power); minDist = MathF.Min(minDist, d); glow += MathF.Exp(-20f * d) * (0.015f + high * 0.03f); march += d * 0.95f; if (d < 0.002f || march > 3.2f) { break; } } float hit = Clamp01(1f - (minDist * 110f)); float fog = Clamp01(1f - (march / 3.2f)); float pulse = beat * 0.45f; float r = Clamp01(hit * (0.35f + bass * 1.2f + pulse) + glow * 1.3f); float g = Clamp01(hit * (0.22f + mid * 1.1f) + glow * 0.6f); float b = Clamp01(hit * (0.45f + high * 1.5f) + glow * 1.5f + fog * 0.08f); _pixelBuffer[(y * _internalWidth) + x] = new Color(r, g, b, 1f); } } } private void RenderKaleidoscopeTunnel(AudioFeatures features, float timeSeconds) { float bass = Clamp01(features.Bass * 0.01f); float mid = Clamp01(features.Mid * 0.0035f); float high = Clamp01(features.High * 0.0035f); float beat = Clamp01(features.Beat); float level = Clamp01(features.Level * 3f); float aspect = _internalWidth / (float)_internalHeight; float tunnelSpeed = 0.45f + bass * 1.35f; float spin = timeSeconds * (0.25f + mid * 1.1f); float slices = 6f + MathF.Floor(high * 8f); for (int y = 0; y < _internalHeight; y++) { float py = (y / (float)_internalHeight) * 2f - 1f; for (int x = 0; x < _internalWidth; x++) { float px = ((x / (float)_internalWidth) * 2f - 1f) * aspect; float radius = MathF.Sqrt((px * px) + (py * py)) + 1e-5f; float angle = MathF.Atan2(py, px) + spin; float wedge = MathF.PI / slices; angle = MathF.Abs((angle % (2f * wedge)) - wedge); float spectrumDrive = GetSpectrumSample(features, radius * 8f + beat * 10f); float distortion = spectrumDrive * (0.55f + high * 0.7f); float tunnel = MathF.Sin((1f / radius) * 9f - timeSeconds * tunnelSpeed * 7f + distortion * 8f); float rings = MathF.Sin(radius * 35f - timeSeconds * (2f + mid * 4f)); float edge = MathF.Exp(-10f * MathF.Abs(angle - wedge * 0.5f)); float pulse = beat * 0.6f; float brightness = Clamp01((tunnel * 0.5f + 0.5f) * (0.4f + level) + edge * 0.5f + pulse * 0.3f); float hueShift = (timeSeconds * 0.15f) + (beat * 0.25f); float r = Clamp01(brightness * (0.5f + 0.5f * MathF.Sin(hueShift + 0.0f)) + rings * 0.08f + bass * 0.25f); float g = Clamp01(brightness * (0.5f + 0.5f * MathF.Sin(hueShift + 2.09f)) + rings * 0.06f + mid * 0.25f); float b = Clamp01(brightness * (0.5f + 0.5f * MathF.Sin(hueShift + 4.18f)) + rings * 0.10f + high * 0.35f); _pixelBuffer[(y * _internalWidth) + x] = new Color(r, g, b, 1f); } } } private float GetSpectrumSample(AudioFeatures features, float virtualIndex) { if (features.Spectrum is null || features.Spectrum.Length == 0) { return 0f; } int i0 = ((int)virtualIndex) % features.Spectrum.Length; if (i0 < 0) { i0 += features.Spectrum.Length; } int i1 = (i0 + 1) % features.Spectrum.Length; float t = virtualIndex - MathF.Floor(virtualIndex); float sample = features.Spectrum[i0] + ((features.Spectrum[i1] - features.Spectrum[i0]) * t); return Clamp01(sample * 0.06f); } private static float MandelbulbDistance(float x, float y, float z, float power) { float zx = x; float zy = y; float zz = z; float dr = 1f; float r = 0f; for (int i = 0; i < 7; i++) { r = MathF.Sqrt((zx * zx) + (zy * zy) + (zz * zz)); if (r > 2f) { break; } float theta = MathF.Acos(zz / (r + 1e-6f)); float phi = MathF.Atan2(zy, zx); float zr = MathF.Pow(r, power); dr = MathF.Pow(r, power - 1f) * power * dr + 1f; float sinTheta = MathF.Sin(theta * power); zx = zr * sinTheta * MathF.Cos(phi * power) + x; zy = zr * sinTheta * MathF.Sin(phi * power) + y; zz = zr * MathF.Cos(theta * power) + z; } return 0.5f * MathF.Log(r + 1e-6f) * r / dr; } private static float Frac(float v) { return v - MathF.Floor(v); } private static float Hash01(int x, int y, int seed) { unchecked { int h = x * 374761393 + y * 668265263 + seed * 2147483647; h = (h ^ (h >> 13)) * 1274126177; h ^= h >> 16; return (h & 0x7fffffff) / (float)int.MaxValue; } } private void RenderFlame(AudioFeatures features, float timeSeconds) { float bass = Clamp01(features.Bass * 0.012f); float high = Clamp01(features.High * 0.003f); float beat = Clamp01(features.Beat); float level = Clamp01(features.Level * 3f); float aspect = _internalWidth / (float)_internalHeight; float flowSpeed = 0.65f + (bass * 1.8f); float turbulence = 1.2f + (high * 3.0f); float pulse = 0.2f + (beat * 0.8f); for (int y = 0; y < _internalHeight; y++) { float y01 = y / (float)(_internalHeight - 1); float fy = 1f - y01; for (int x = 0; x < _internalWidth; x++) { float x01 = x / (float)(_internalWidth - 1); float fx = (x01 * 2f - 1f) * aspect; float t = timeSeconds * flowSpeed; float n1 = MathF.Sin((fx * 6.0f) + (fy * 7.5f) - t * 5.0f); float n2 = MathF.Sin((fx * 12.0f - t * 2.1f) + MathF.Cos(fy * 10.0f + t * 3.3f)); float n3 = MathF.Sin((fx * 22.0f + t * 1.4f) + MathF.Sin(fy * 18.0f - t * 2.7f)); float noise = ((n1 * 0.55f) + (n2 * 0.30f) + (n3 * 0.15f)) * turbulence; noise = noise * 0.5f + 0.5f; float cone = 1f - MathF.Abs(fx) * (0.55f + fy * 0.65f); cone = Clamp01(cone); float baseIntensity = MathF.Pow(fy, 1.8f); float flame = Clamp01(baseIntensity * cone * (0.6f + noise * 0.9f + pulse * 0.25f)); flame *= 0.65f + level * 0.75f; float ember = MathF.Sin((fx * 45f) + (fy * 14f) - t * 18f); float sparks = ember > 0.985f ? 0.35f + beat * 0.45f : 0f; float r = Clamp01(flame * 2.1f + sparks); float g = Clamp01(MathF.Pow(flame, 1.35f) * (0.95f + high * 0.45f) + sparks * 0.7f); float b = Clamp01(MathF.Pow(flame, 3.2f) * (0.25f + beat * 0.25f)); _pixelBuffer[(y * _internalWidth) + x] = new Color(r, g, b, 1f); } } } private void RenderPlasma(AudioFeatures features, float timeSeconds) { float bass = Clamp01(features.Bass * 0.01f); float mid = Clamp01(features.Mid * 0.003f); float high = Clamp01(features.High * 0.003f); float beat = Clamp01(features.Beat); float level = Clamp01(features.Level * 3f); float aspect = _internalWidth / (float)_internalHeight; float speed = 0.8f + bass * 1.6f; for (int y = 0; y < _internalHeight; y++) { float py = (y / (float)_internalHeight) * 2f - 1f; for (int x = 0; x < _internalWidth; x++) { float px = ((x / (float)_internalWidth) * 2f - 1f) * aspect; float t = timeSeconds * speed; float v1 = MathF.Sin((px * 4.5f) + t * 1.9f); float v2 = MathF.Sin((py * 5.0f) - t * 1.3f); float v3 = MathF.Sin((px + py) * 4.0f + t * (2.2f + mid * 2f)); float v4 = MathF.Sin(MathF.Sqrt(px * px + py * py) * 10f - t * (3f + high * 3f)); float plasma = (v1 + v2 + v3 + v4) * 0.25f; float p = plasma * 0.5f + 0.5f; float pulse = beat * 0.35f; float spec = GetSpectrumSample(features, (x * 0.08f) + (y * 0.03f)); float r = Clamp01((0.35f + 0.65f * MathF.Sin((p + pulse) * 6.28f)) * (0.6f + level * 0.6f)); float g = Clamp01((0.35f + 0.65f * MathF.Sin((p + 0.33f) * 6.28f + spec * 2f)) * (0.55f + mid * 0.8f)); float b = Clamp01((0.35f + 0.65f * MathF.Sin((p + 0.66f) * 6.28f + spec * 3f)) * (0.55f + high * 0.9f + pulse)); _pixelBuffer[(y * _internalWidth) + x] = new Color(r, g, b, 1f); } } } private void RenderNebula(AudioFeatures features, float timeSeconds) { float bass = Clamp01(features.Bass * 0.01f); float mid = Clamp01(features.Mid * 0.003f); float high = Clamp01(features.High * 0.003f); float beat = Clamp01(features.Beat); float level = Clamp01(features.Level * 3f); float aspect = _internalWidth / (float)_internalHeight; float zoom = 1.6f - bass * 0.5f; float driftX = MathF.Sin(timeSeconds * (0.16f + mid * 0.4f)) * 0.45f; float driftY = MathF.Cos(timeSeconds * (0.12f + high * 0.5f)) * 0.45f; for (int y = 0; y < _internalHeight; y++) { float py = ((y / (float)_internalHeight) * 2f - 1f) * zoom; for (int x = 0; x < _internalWidth; x++) { float px = (((x / (float)_internalWidth) * 2f - 1f) * aspect) * zoom; float fx = px + driftX; float fy = py + driftY; float density = 0f; float amp = 0.55f; float freq = 1.1f; for (int o = 0; o < 5; o++) { float n = MathF.Sin((fx * freq + timeSeconds * 0.15f) * 2.1f) * MathF.Cos((fy * freq - timeSeconds * 0.19f) * 2.3f); density += (n * 0.5f + 0.5f) * amp; freq *= 1.9f; amp *= 0.55f; } float rad = MathF.Sqrt(fx * fx + fy * fy); float core = MathF.Exp(-rad * (1.6f + bass * 1.2f)); float stars = GetSpectrumSample(features, rad * 24f + timeSeconds * 8f); float pulse = beat * 0.45f; float cloud = Clamp01(density * (0.6f + level * 0.5f)); float r = Clamp01(cloud * (0.25f + mid * 0.8f) + core * (0.8f + pulse)); float g = Clamp01(cloud * (0.35f + high * 0.9f) + core * 0.55f + stars * 0.35f); float b = Clamp01(cloud * (0.55f + bass * 0.8f) + core * 0.75f + stars * 0.5f + pulse * 0.2f); _pixelBuffer[(y * _internalWidth) + x] = new Color(r, g, b, 1f); } } } private void RenderFlameGrill(AudioFeatures features, float timeSeconds) { float level = Clamp01(features.Level * 3f); float bass = Clamp01(features.Bass * 0.012f); float high = Clamp01(features.High * 0.003f); float beat = Clamp01(features.Beat); float intensity = Clamp01(0.25f + level * 0.45f + bass * 0.35f + beat * 0.35f); float flameHeight = 0.22f + intensity * 0.68f; float flickerSpeed = 2.2f + bass * 4.0f; int grillTop = (int)(_internalHeight * 0.86f); float barSpacing = 14f; float barThickness = 3.5f; for (int y = 0; y < _internalHeight; y++) { float y01 = y / (float)(_internalHeight - 1); float fromBottom = 1f - y01; for (int x = 0; x < _internalWidth; x++) { int idx = (y * _internalWidth) + x; float x01 = x / (float)(_internalWidth - 1); float wave1 = MathF.Sin((x01 * 18f) + timeSeconds * flickerSpeed); float wave2 = MathF.Sin((x01 * 36f) - timeSeconds * (flickerSpeed * 0.65f)); float wave3 = MathF.Sin((x01 * 72f) + timeSeconds * (flickerSpeed * 1.35f)); float turbulence = (wave1 * 0.45f + wave2 * 0.35f + wave3 * 0.20f) * (0.22f + high * 0.55f); float localHeight = flameHeight + turbulence; float flameMask = Clamp01((fromBottom - 0.02f) / MathF.Max(0.05f, localHeight)); float taper = Clamp01(1f - MathF.Pow(fromBottom / MathF.Max(0.05f, localHeight), 1.35f)); float flameCore = flameMask * taper; float tongues = MathF.Sin((x01 * 110f) + timeSeconds * (6f + high * 8f) - fromBottom * 22f); float tongueMask = Clamp01(tongues * 0.5f + 0.5f); float flame = flameCore * (0.72f + tongueMask * 0.45f); flame *= 0.65f + intensity * 0.7f; float emberNoise = MathF.Sin((x01 * 180f) + (fromBottom * 90f) - timeSeconds * 18f); float sparks = emberNoise > 0.992f && fromBottom < localHeight ? 0.55f + beat * 0.35f : 0f; float r = Clamp01(flame * 2.4f + sparks); float g = Clamp01(MathF.Pow(flame, 1.4f) * (1.05f + high * 0.35f) + sparks * 0.55f); float b = Clamp01(MathF.Pow(flame, 3.1f) * (0.22f + beat * 0.18f)); if (y >= grillTop) { float line = MathF.Abs(((x % barSpacing) - barSpacing * 0.5f)); float barMask = Clamp01(1f - (line / barThickness)); float hot = 0.25f + intensity * 0.55f; float metal = 0.08f; float grillR = Clamp01(metal + barMask * hot); float grillG = Clamp01(metal * 0.7f + barMask * hot * 0.45f); float grillB = Clamp01(metal * 0.6f + barMask * hot * 0.18f); r = MathF.Max(r * 0.45f, grillR); g = MathF.Max(g * 0.45f, grillG); b = MathF.Max(b * 0.45f, grillB); } _pixelBuffer[idx] = new Color(r, g, b, 1f); } } } private void RenderFloatingNotes(AudioFeatures features, float timeSeconds) { float level = Clamp01(features.Level * 3f); float bass = Clamp01(features.Bass * 0.012f); float high = Clamp01(features.High * 0.003f); float beat = Clamp01(features.Beat); float intensity = Clamp01(level * 0.6f + bass * 0.25f + beat * 0.35f); for (int y = 0; y < _internalHeight; y++) { float t = y / (float)(_internalHeight - 1); float r = 0.03f + 0.08f * (1f - t); float g = 0.02f + 0.05f * (1f - t); float b = 0.08f + 0.20f * (1f - t); Color bg = new(r, g, b, 1f); int row = y * _internalWidth; for (int x = 0; x < _internalWidth; x++) { _pixelBuffer[row + x] = bg; } } float spawnRate = (0.08f + intensity * 1.8f + high * 0.7f) * _runtimePerformanceScale; _noteSpawnAccumulator += spawnRate; while (_noteSpawnAccumulator >= 1f) { SpawnNote(intensity); _noteSpawnAccumulator -= 1f; } for (int i = _notes.Count - 1; i >= 0; i--) { NoteParticle note = _notes[i]; note.Y += note.Vy; note.X += note.Vx + MathF.Sin(timeSeconds * 2.8f + note.Phase) * 0.0012f; note.Life -= 0.012f + (1f - intensity) * 0.004f; if (note.Life <= 0f || note.Y < -0.12f || note.X < -0.2f || note.X > 1.2f) { _notes.RemoveAt(i); continue; } _notes[i] = note; DrawNote(note); } } private void RenderFireworks(AudioFeatures features, float timeSeconds) { float level = Clamp01(features.Level * 3f); float bass = Clamp01(features.Bass * 0.012f); float high = Clamp01(features.High * 0.003f); float beat = Clamp01(features.Beat); float intensity = Clamp01(level * 0.55f + bass * 0.30f + beat * 0.45f); for (int y = 0; y < _internalHeight; y++) { float t = y / (float)(_internalHeight - 1); float r = 0.01f + 0.05f * (1f - t); float g = 0.01f + 0.04f * (1f - t); float b = 0.04f + 0.16f * (1f - t); Color bg = new(r, g, b, 1f); int row = y * _internalWidth; for (int x = 0; x < _internalWidth; x++) { _pixelBuffer[row + x] = bg; } } float spawnRate = (0.02f + intensity * 0.45f + high * 0.15f) * _runtimePerformanceScale; _fireworkSpawnAccumulator += spawnRate; while (_fireworkSpawnAccumulator >= 1f) { SpawnFireworkRocket(intensity); _fireworkSpawnAccumulator -= 1f; } for (int i = _fireworkRockets.Count - 1; i >= 0; i--) { FireworkRocket rocket = _fireworkRockets[i]; rocket.Y += rocket.Vy; rocket.X += rocket.Vx; rocket.Age += 0.016f; DrawFilledCircle( (int)(rocket.X * _internalWidth), (int)(rocket.Y * _internalHeight), 1 + (int)(rocket.Size * 0.6f), rocket.Color, 0.95f); bool explode = rocket.Y <= rocket.TargetY || rocket.Age >= rocket.MaxAge; if (explode) { ExplodeFirework(rocket, intensity); _fireworkRockets.RemoveAt(i); } else { _fireworkRockets[i] = rocket; } } for (int i = _fireworkSparks.Count - 1; i >= 0; i--) { FireworkSpark spark = _fireworkSparks[i]; spark.X += spark.Vx; spark.Y += spark.Vy; spark.Vy += 0.00022f; spark.Vx *= 0.995f; spark.Life -= 0.012f; if (spark.Life <= 0f || spark.Y > 1.1f || spark.X < -0.2f || spark.X > 1.2f) { _fireworkSparks.RemoveAt(i); continue; } _fireworkSparks[i] = spark; int sx = (int)(spark.X * _internalWidth); int sy = (int)(spark.Y * _internalHeight); DrawFilledCircle(sx, sy, spark.Size, spark.Color, spark.Life); } } private void RenderEqualizerCity(AudioFeatures features, float timeSeconds) { for (int y = 0; y < _internalHeight; y++) { float t = y / (float)(_internalHeight - 1); Color bg = new(0.01f, 0.02f + 0.08f * (1f - t), 0.08f + 0.18f * (1f - t), 1f); int row = y * _internalWidth; for (int x = 0; x < _internalWidth; x++) { _pixelBuffer[row + x] = bg; } } int buildings = 48; float beat = Clamp01(features.Beat); for (int i = 0; i < buildings; i++) { float bin = GetSpectrumSample(features, i * (64f / buildings)); int bw = Math.Max(2, _internalWidth / buildings); int x0 = i * bw; int h = (int)((_internalHeight * 0.15f) + bin * _internalHeight * 0.75f); int y0 = _internalHeight - h; Vector3 c = new(0.08f + bin * 0.9f, 0.2f + bin * 0.6f + beat * 0.2f, 0.4f + bin * 0.5f); DrawRect(x0, y0, bw - 1, h, c, 0.95f); for (int wy = y0 + 4; wy < _internalHeight - 2; wy += 6) { for (int wx = x0 + 1; wx < x0 + bw - 1; wx += 4) { if (Hash01(wx, wy, i) > 0.55f - beat * 0.25f) { DrawRect(wx, wy, 2, 2, new Vector3(1f, 0.85f, 0.4f), 0.75f); } } } } } private void RenderLiquidMetaballs(AudioFeatures features, float timeSeconds) { float bass = Clamp01(features.Bass * 0.012f); float high = Clamp01(features.High * 0.003f); for (int y = 0; y < _internalHeight; y++) { float py = y / (float)_internalHeight; for (int x = 0; x < _internalWidth; x++) { float px = x / (float)_internalWidth; float field = 0f; for (int i = 0; i < 6; i++) { float ax = 0.5f + MathF.Sin(timeSeconds * (0.4f + i * 0.13f) + i) * (0.2f + bass * 0.1f); float ay = 0.5f + MathF.Cos(timeSeconds * (0.35f + i * 0.11f) + i * 1.7f) * (0.25f + bass * 0.12f); float dx = px - ax; float dy = py - ay; field += 0.02f / (dx * dx + dy * dy + 0.002f); } float m = Clamp01((field - 0.35f) * 0.8f); float edge = Clamp01((field - 0.5f) * 2.5f); float r = Clamp01(m * 0.2f + edge * (0.5f + high)); float g = Clamp01(m * 0.45f + edge * 0.3f); float b = Clamp01(m * 0.75f + edge * (0.2f + high)); _pixelBuffer[y * _internalWidth + x] = new Color(r, g, b, 1f); } } } private void RenderParticleGalaxy(AudioFeatures features, float timeSeconds) { for (int i = 0; i < _pixelBuffer.Length; i++) { _pixelBuffer[i] = new Color(0.01f, 0.01f, 0.04f, 1f); } float beat = Clamp01(features.Beat); int particles = ScaleCount(2200, 600); float arms = 4f + Clamp01(features.Mid * 0.002f) * 3f; for (int i = 0; i < particles; i++) { float fi = i / (float)particles; float radius = MathF.Pow(fi, 0.65f) * 0.48f * (1f + beat * 0.2f); float arm = (i % (int)arms) / arms; float angle = arm * MathF.Tau + radius * 14f + timeSeconds * (0.15f + beat * 0.8f); float x = 0.5f + MathF.Cos(angle) * radius; float y = 0.5f + MathF.Sin(angle) * radius; int px = (int)(x * _internalWidth); int py = (int)(y * _internalHeight); if (px < 0 || py < 0 || px >= _internalWidth || py >= _internalHeight) { continue; } float hue = fi * 0.7f + beat * 0.2f; Vector3 c = new( 0.5f + 0.5f * MathF.Sin(hue * 6.28f), 0.5f + 0.5f * MathF.Sin(hue * 6.28f + 2.1f), 0.5f + 0.5f * MathF.Sin(hue * 6.28f + 4.2f)); DrawFilledCircle(px, py, 1, c, 0.85f); } DrawFilledCircle(_internalWidth / 2, _internalHeight / 2, 6, new Vector3(1f, 0.9f, 0.7f), 0.9f); } private void RenderAuroraCurtains(AudioFeatures features, float timeSeconds) { float mid = Clamp01(features.Mid * 0.003f); float high = Clamp01(features.High * 0.003f); for (int y = 0; y < _internalHeight; y++) { float ny = y / (float)(_internalHeight - 1); for (int x = 0; x < _internalWidth; x++) { float nx = x / (float)(_internalWidth - 1); float wave = MathF.Sin(nx * 8f + timeSeconds * (0.3f + mid * 1.2f) + MathF.Sin(nx * 3f + timeSeconds)); float layer = MathF.Exp(-MathF.Abs(ny - (0.35f + wave * 0.18f)) * (6f + high * 12f)); float layer2 = MathF.Exp(-MathF.Abs(ny - (0.55f + MathF.Sin(nx * 5f - timeSeconds * 0.4f) * 0.12f)) * 9f); float a = Clamp01(layer + layer2 * 0.7f); float r = a * 0.15f; float g = a * (0.6f + high * 0.5f); float b = a * (0.45f + mid * 0.5f); _pixelBuffer[y * _internalWidth + x] = new Color(r, g, b, 1f); } } } private void RenderVortexPortal(AudioFeatures features, float timeSeconds) { float bass = Clamp01(features.Bass * 0.012f); float aspect = _internalWidth / (float)_internalHeight; for (int y = 0; y < _internalHeight; y++) { float py = (y / (float)_internalHeight) * 2f - 1f; for (int x = 0; x < _internalWidth; x++) { float px = ((x / (float)_internalWidth) * 2f - 1f) * aspect; float r = MathF.Sqrt(px * px + py * py) + 1e-5f; float a = MathF.Atan2(py, px) + timeSeconds * (0.5f + bass * 2f); float spiral = MathF.Sin(14f * r - a * 5f - timeSeconds * 3f); float glow = MathF.Exp(-r * (2.5f - bass)); float v = Clamp01((spiral * 0.5f + 0.5f) * glow * 2f); _pixelBuffer[y * _internalWidth + x] = new Color(v * 0.7f, v * 0.4f, v, 1f); } } } private void RenderLaserGrid(AudioFeatures features, float timeSeconds) { float beat = Clamp01(features.Beat); float bass = Clamp01(features.Bass * 0.012f); for (int y = 0; y < _internalHeight; y++) { float fy = y / (float)(_internalHeight - 1); for (int x = 0; x < _internalWidth; x++) { float fx = x / (float)(_internalWidth - 1); float perspective = 1f / (0.2f + fy * (1.8f - bass * 0.6f)); float gx = MathF.Abs(((fx * perspective * 20f + timeSeconds) % 1f) - 0.5f); float gy = MathF.Abs(((fy * perspective * 20f) % 1f) - 0.5f); float line = Clamp01(0.08f / (gx + 0.02f) + 0.08f / (gy + 0.02f)); float sweep = MathF.Exp(-MathF.Abs(fy - ((timeSeconds * 0.25f + beat * 0.2f) % 1f)) * 60f); float v = Clamp01(line * 0.6f + sweep); _pixelBuffer[y * _internalWidth + x] = new Color(v * 0.1f, v * 0.9f, v, 1f); } } } private void RenderRainRipple(AudioFeatures features, float timeSeconds) { float level = Clamp01(features.Level * 3f); float density = 70f + level * 220f; for (int y = 0; y < _internalHeight; y++) { for (int x = 0; x < _internalWidth; x++) { _pixelBuffer[y * _internalWidth + x] = new Color(0.02f, 0.04f, 0.08f, 1f); } } int drops = ScaleCount((int)density, 20); for (int i = 0; i < drops; i++) { float seed = i * 13.37f; float x = Frac(seed * 0.731f); float phase = Frac(seed * 0.379f + timeSeconds * (0.4f + level * 1.2f)); float y = phase; int px = (int)(x * _internalWidth); int py = (int)(y * _internalHeight); DrawRect(px, py, 1, 6, new Vector3(0.6f, 0.8f, 1f), 0.7f); if (py > _internalHeight * 0.8f) { float rr = (phase - 0.8f) * 80f; DrawRing(px, (int)(_internalHeight * 0.82f), rr, new Vector3(0.7f, 0.9f, 1f), 0.35f); } } } private void RenderInkSmoke(AudioFeatures features, float timeSeconds) { float bass = Clamp01(features.Bass * 0.012f); float high = Clamp01(features.High * 0.003f); for (int y = 0; y < _internalHeight; y++) { float ny = y / (float)(_internalHeight - 1); for (int x = 0; x < _internalWidth; x++) { float nx = (x / (float)(_internalWidth - 1)) * 2f - 1f; float basePlume = MathF.Exp(-MathF.Abs(nx) * (2.5f - bass)); float turbulence = MathF.Sin(nx * 12f + ny * 9f - timeSeconds * (1.5f + high * 5f)) + MathF.Sin(nx * 21f - ny * 14f + timeSeconds * 2.2f); turbulence = turbulence * 0.25f + 0.5f; float rise = Clamp01((1f - ny) * (0.7f + bass * 0.7f)); float d = Clamp01(basePlume * rise * turbulence); _pixelBuffer[y * _internalWidth + x] = new Color(d * 0.18f, d * 0.2f, d * 0.24f, 1f); } } } private void RenderMirrorMosaic(AudioFeatures features, float timeSeconds) { int tiles = 3 + (int)(Clamp01(features.Beat + features.Level * 2f) * 5f); float t = timeSeconds * 0.6f; for (int y = 0; y < _internalHeight; y++) { float v = y / (float)_internalHeight; for (int x = 0; x < _internalWidth; x++) { float u = x / (float)_internalWidth; float mu = Frac(u * tiles); float mv = Frac(v * tiles); if (((int)(u * tiles) & 1) == 1) mu = 1f - mu; if (((int)(v * tiles) & 1) == 1) mv = 1f - mv; float pattern = MathF.Sin((mu * 10f + t) * MathF.Tau) * MathF.Cos((mv * 10f - t) * MathF.Tau); float p = pattern * 0.5f + 0.5f; _pixelBuffer[y * _internalWidth + x] = new Color( Clamp01(0.2f + p * 0.9f), Clamp01(0.2f + (1f - p) * 0.8f), Clamp01(0.35f + p * 0.7f), 1f); } } } private void RenderWaveformRibbon(AudioFeatures features, float timeSeconds) { for (int i = 0; i < _pixelBuffer.Length; i++) { _pixelBuffer[i] = new Color(0.01f, 0.01f, 0.03f, 1f); } if (features.Spectrum is null || features.Spectrum.Length == 0) { return; } int points = _internalWidth; float beat = Clamp01(features.Beat); for (int x = 0; x < points; x++) { float tx = x / (float)(points - 1); float idx = tx * (features.Spectrum.Length - 1); int i0 = (int)idx; int i1 = Math.Min(features.Spectrum.Length - 1, i0 + 1); float f = idx - i0; float amp = (features.Spectrum[i0] + (features.Spectrum[i1] - features.Spectrum[i0]) * f) * 0.18f; amp = Clamp01(amp); float center = 0.5f + MathF.Sin(tx * 8f + timeSeconds * 0.9f) * 0.08f; float yNorm = center - amp * (0.35f + beat * 0.2f); int y = (int)(yNorm * _internalHeight); int thickness = 2 + (int)(amp * 9f); for (int k = -thickness; k <= thickness; k++) { int yy = y + k; if (yy < 0 || yy >= _internalHeight) { continue; } float w = 1f - MathF.Abs(k) / (float)(thickness + 1); Vector3 c = new(0.2f + amp * 1.0f, 0.3f + w * 0.7f, 0.7f + beat * 0.3f); BlendPixel(x, yy, c, w * 0.8f); } } } private void RenderGrainBars(AudioFeatures features, float timeSeconds) { float bass = Clamp01(features.Bass * 0.012f); float mid = Clamp01(features.Mid * 0.0035f); float high = Clamp01(features.High * 0.0035f); float beat = Clamp01(features.Beat); float level = Clamp01(features.Level * 3f); for (int y = 0; y < _internalHeight; y++) { float t = y / (float)(_internalHeight - 1); Color bg = new(0.015f, 0.02f + 0.04f * (1f - t), 0.03f + 0.07f * (1f - t), 1f); int row = y * _internalWidth; for (int x = 0; x < _internalWidth; x++) { _pixelBuffer[row + x] = bg; } } int totalBars = ScaleCount(Math.Max(120, _internalWidth / 2), 60); float bandWidth = _internalWidth / 3f; float maxHeight = _internalHeight * (0.22f + 0.62f * (0.35f + level * 0.65f)); for (int i = 0; i < totalBars; i++) { float xNorm = i / (float)(totalBars - 1); int x = (int)(xNorm * (_internalWidth - 1)); int region = x < bandWidth ? 0 : (x < bandWidth * 2f ? 1 : 2); float band = region switch { 0 => bass, 1 => mid, _ => high }; float grain = Hash01(i, (int)(timeSeconds * 60f), region) * 2f - 1f; float wobble = MathF.Sin(timeSeconds * (6.0f + region * 2.0f) + i * 0.21f) * 0.22f; float spectrum = GetSpectrumSample(features, i * 0.43f + region * 7f) * 1.8f; float amp = Clamp01(band * 0.8f + spectrum * 0.45f + beat * 0.3f + grain * 0.13f + wobble * 0.1f); int h = Math.Max(2, (int)(amp * maxHeight)); Vector3 color = region switch { 0 => new Vector3(1f, 0.35f + amp * 0.4f, 0.2f), 1 => new Vector3(0.2f, 0.9f, 0.35f + amp * 0.5f), _ => new Vector3(0.25f + amp * 0.4f, 0.5f, 1f) }; int barW = 1; int y0 = _internalHeight - h; DrawRect(x, y0, barW, h, color, 0.95f); if (Hash01(i, region, (int)(timeSeconds * 100f)) > 0.68f) { int gritCount = 2 + (int)(amp * 8f); for (int g = 0; g < gritCount; g++) { int gy = _internalHeight - 1 - (int)(Hash01(g, i, region) * h); DrawRect(x, gy, 1, 1, Vector3.One, 0.3f + amp * 0.5f); } } } } 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 whistleBoost = GetWhistleSpawnBoost(features.DominantFrequencyHz); float spawnRate = features.DetectedNoteStrength * (0.9f + whistleBoost * 1.7f + Clamp01(features.Beat) * 0.35f); _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 static float GetWhistleSpawnBoost(float frequencyHz) { if (frequencyHz < 800f || frequencyHz > 5000f) { return 0f; } float norm = (frequencyHz - 800f) / (5000f - 800f); return Clamp01(norm); } 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) { 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 (!TextGlyphs.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 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)) { return; } float x = 0.1f + (float)_rng.NextDouble() * 0.8f; float y = 1.02f; float vy = -(0.0065f + (float)_rng.NextDouble() * (0.0025f + intensity * 0.0025f)); float vx = ((float)_rng.NextDouble() - 0.5f) * 0.0018f; float targetY = 0.2f + (float)_rng.NextDouble() * 0.42f; float maxAge = 0.7f + (float)_rng.NextDouble() * 0.45f; Vector3 color = new( 0.55f + (float)_rng.NextDouble() * 0.45f, 0.45f + (float)_rng.NextDouble() * 0.55f, 0.45f + (float)_rng.NextDouble() * 0.55f); _fireworkRockets.Add(new FireworkRocket { X = x, Y = y, Vx = vx, Vy = vy, TargetY = targetY, Age = 0f, MaxAge = maxAge, Size = 1.4f + (float)_rng.NextDouble() * 1.4f, Color = color }); } private void ExplodeFirework(FireworkRocket rocket, float intensity) { int count = ScaleCount(22 + (int)(intensity * 38f), 10); float speedBase = 0.0018f + intensity * 0.0034f; for (int i = 0; i < count; i++) { if (_fireworkSparks.Count >= ScaleCount(4500, 800)) { break; } float a = (float)(i * (Math.PI * 2.0 / count)); float speed = speedBase + (float)_rng.NextDouble() * speedBase; float vx = MathF.Cos(a) * speed; float vy = MathF.Sin(a) * speed; _fireworkSparks.Add(new FireworkSpark { X = rocket.X, Y = rocket.Y, Vx = vx, Vy = vy, Life = 0.65f + (float)_rng.NextDouble() * 0.35f, Size = 1 + _rng.Next(0, 2), Color = rocket.Color }); } } private void SpawnNote(float intensity) { if (_notes.Count >= ScaleCount(320, 80)) { return; } float x = 0.08f + (float)_rng.NextDouble() * 0.84f; float y = 0.94f + (float)_rng.NextDouble() * 0.08f; float vy = -(0.0025f + (float)_rng.NextDouble() * (0.0035f + intensity * 0.004f)); float vx = ((float)_rng.NextDouble() - 0.5f) * (0.0018f + intensity * 0.0015f); float size = 0.75f + (float)_rng.NextDouble() * (1.6f + intensity * 1.2f); bool flagged = _rng.NextDouble() > 0.42; Vector3 color = new( 0.7f + (float)_rng.NextDouble() * 0.3f, 0.55f + (float)_rng.NextDouble() * 0.45f, 0.4f + (float)_rng.NextDouble() * 0.6f); _notes.Add(new NoteParticle { X = x, Y = y, Vx = vx, Vy = vy, Life = 1f, Size = size, Phase = (float)_rng.NextDouble() * MathF.PI * 2f, Color = color, HasFlag = flagged }); } private void DrawNote(NoteParticle note) { int px = (int)(note.X * _internalWidth); int py = (int)(note.Y * _internalHeight); int s = Math.Max(1, (int)MathF.Round(note.Size)); float alpha = Clamp01(note.Life); Vector3 color = note.Color; int headX = px; int headY = py; DrawFilledCircle(headX, headY, s + 1, color, alpha * 0.75f); DrawFilledCircle(headX, headY, s, color, alpha); int stemHeight = 6 + s * 4; DrawRect(headX + s, headY - stemHeight, 2, stemHeight, color, alpha); if (note.HasFlag) { for (int i = 0; i < 6 + s; i++) { int fx = headX + s + 1 + i; int fy = headY - stemHeight + (int)(MathF.Sin(i * 0.55f) * (2f + s * 0.25f)); DrawRect(fx, fy, 2, 2, color, alpha * 0.95f); } } } private void DrawFilledCircle(int cx, int cy, int radius, Vector3 color, float alpha) { int r2 = radius * radius; for (int oy = -radius; oy <= radius; oy++) { int yy = cy + oy; if (yy < 0 || yy >= _internalHeight) { continue; } int oy2 = oy * oy; for (int ox = -radius; ox <= radius; ox++) { if (ox * ox + oy2 > r2) { continue; } int xx = cx + ox; if (xx < 0 || xx >= _internalWidth) { continue; } BlendPixel(xx, yy, color, alpha); } } } private void DrawRect(int x, int y, int width, int height, Vector3 color, float alpha) { for (int yy = y; yy < y + height; yy++) { if (yy < 0 || yy >= _internalHeight) { continue; } for (int xx = x; xx < x + width; xx++) { if (xx < 0 || xx >= _internalWidth) { continue; } BlendPixel(xx, yy, color, alpha); } } } private void DrawRing(int cx, int cy, float radius, Vector3 color, float alpha) { int r = Math.Max(1, (int)radius); int minX = Math.Max(0, cx - r - 1); int maxX = Math.Min(_internalWidth - 1, cx + r + 1); int minY = Math.Max(0, cy - r - 1); int maxY = Math.Min(_internalHeight - 1, cy + r + 1); float r0 = MathF.Max(0f, radius - 1.25f); float r1 = radius + 1.25f; float r0s = r0 * r0; float r1s = r1 * r1; for (int y = minY; y <= maxY; y++) { float dy = y - cy; for (int x = minX; x <= maxX; x++) { float dx = x - cx; float d2 = dx * dx + dy * dy; if (d2 < r0s || d2 > r1s) { continue; } BlendPixel(x, y, color, alpha); } } } private void BlendPixel(int x, int y, Vector3 color, float alpha) { int idx = y * _internalWidth + x; Vector3 baseColor = _pixelBuffer[idx].ToVector3(); Vector3 mixed = Vector3.Lerp(baseColor, color, Clamp01(alpha)); _pixelBuffer[idx] = new Color(Clamp01(mixed.X), Clamp01(mixed.Y), Clamp01(mixed.Z), 1f); } private void RenderJulia(AudioFeatures features, float timeSeconds) { float level = Clamp01(features.Level * 3f); float bass = Clamp01(features.Bass * 0.015f); float mid = Clamp01(features.Mid * 0.004f); float high = Clamp01(features.High * 0.003f); float beat = Clamp01(features.Beat); float zoom = 1.1f - (bass * 0.45f) - (beat * 0.15f); if (zoom < 0.45f) { zoom = 0.45f; } float cRe = -0.8f + (mid * 1.6f) + (MathF.Sin(timeSeconds * 0.31f) * 0.06f); float cIm = 0.156f + ((high - 0.5f) * 1.0f) + (MathF.Cos(timeSeconds * 0.23f) * 0.04f); int maxIterations = 42 + (int)(level * 54f); float aspect = _internalWidth / (float)_internalHeight; for (int y = 0; y < _internalHeight; y++) { float ny = ((y / (float)_internalHeight) * 2f - 1f) * zoom; for (int x = 0; x < _internalWidth; x++) { float nx = (((x / (float)_internalWidth) * 2f - 1f) * aspect) * zoom; float zr = nx; float zi = ny; int iter = 0; while (iter < maxIterations) { float zr2 = zr * zr; float zi2 = zi * zi; if (zr2 + zi2 > 4f) { break; } zi = (2f * zr * zi) + cIm; zr = zr2 - zi2 + cRe; iter++; } float t = iter / (float)maxIterations; float pulse = beat * 0.35f; float r = MathF.Pow(t, 0.65f) * (0.35f + high * 1.2f + pulse); float g = MathF.Pow(t, 1.2f) * (0.28f + mid * 1.1f); float b = MathF.Pow(t, 0.85f) * (0.45f + bass * 1.5f + pulse); _pixelBuffer[(y * _internalWidth) + x] = new Color( Clamp01(r), Clamp01(g), Clamp01(b), 1f); } } } private static float Clamp01(float value) { if (value < 0f) { return 0f; } if (value > 1f) { return 1f; } return value; } private void ThrowIfDisposed() { if (_disposed) { throw new ObjectDisposedException(nameof(FractalRenderer)); } } public void Dispose() { if (_disposed) { return; } _fractalTexture.Dispose(); _spriteBatch.Dispose(); _disposed = true; } private struct NoteParticle { public float X; public float Y; public float Vx; public float Vy; public float Life; public float Size; public float Phase; public Vector3 Color; 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; public float Y; public float Vx; public float Vy; public float TargetY; public float Age; public float MaxAge; public float Size; public Vector3 Color; } private struct FireworkSpark { public float X; public float Y; public float Vx; public float Vy; public float Life; public int Size; public Vector3 Color; } }