Added FodyWeavers.xsd, .dotnet-home/.dotnet, .dotnet-home/.dotnet/TelemetryStorageService, and .nuget to .gitignore to prevent tracking of generated and tool-related files.
2034 lines
71 KiB
C#
2034 lines
71 KiB
C#
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<NoteParticle> _notes = [];
|
|
private readonly List<DetectedNoteParticle> _detectedNotes = [];
|
|
private readonly List<StaffNoteParticle> _staffNotes = [];
|
|
private readonly List<ScrollingStaffNoteParticle> _scrollStaffNotes = [];
|
|
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[]> 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 _fireworkSpawnAccumulator;
|
|
private float _sheetOffsetX;
|
|
private float _lastRenderTimeSeconds = -1f;
|
|
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();
|
|
_staffNotes.Clear();
|
|
_scrollStaffNotes.Clear();
|
|
_fireworkRockets.Clear();
|
|
_fireworkSparks.Clear();
|
|
_noteSpawnAccumulator = 0f;
|
|
_fireworkSpawnAccumulator = 0f;
|
|
_sheetOffsetX = 0f;
|
|
_lastRenderTimeSeconds = -1f;
|
|
}
|
|
|
|
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;
|
|
case 22:
|
|
RenderMusicStaffNotes(features, timeSeconds);
|
|
break;
|
|
case 23:
|
|
RenderScrollingMusicStaff(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;
|
|
}
|
|
}
|
|
|
|
if (features.NoteOnset)
|
|
{
|
|
SpawnDetectedNote(features);
|
|
}
|
|
|
|
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 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 RenderMusicStaffNotes(AudioFeatures features, float timeSeconds)
|
|
{
|
|
for (int y = 0; y < _internalHeight; y++)
|
|
{
|
|
float t = y / (float)(_internalHeight - 1);
|
|
Color bg = new(0.98f - t * 0.05f, 0.97f - t * 0.05f, 0.93f - t * 0.04f, 1f);
|
|
int row = y * _internalWidth;
|
|
for (int x = 0; x < _internalWidth; x++)
|
|
{
|
|
_pixelBuffer[row + x] = bg;
|
|
}
|
|
}
|
|
|
|
int left = (int)(_internalWidth * 0.08f);
|
|
int right = (int)(_internalWidth * 0.92f);
|
|
int top = (int)(_internalHeight * 0.32f);
|
|
int spacing = Math.Max(5, _internalHeight / 24);
|
|
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
int y = top + i * spacing;
|
|
DrawRect(left, y, right - left, 1, new Vector3(0.15f, 0.15f, 0.18f), 0.85f);
|
|
}
|
|
|
|
if (features.NoteOnset)
|
|
{
|
|
SpawnStaffNote(features, left, right, top, spacing);
|
|
}
|
|
|
|
for (int i = _staffNotes.Count - 1; i >= 0; i--)
|
|
{
|
|
StaffNoteParticle n = _staffNotes[i];
|
|
n.Life -= 0.0065f;
|
|
n.X += n.Vx;
|
|
if (n.Life <= 0f || n.X > right + 20)
|
|
{
|
|
_staffNotes.RemoveAt(i);
|
|
continue;
|
|
}
|
|
|
|
_staffNotes[i] = n;
|
|
DrawStaffNote(n, top, spacing);
|
|
}
|
|
}
|
|
|
|
private void RenderScrollingMusicStaff(AudioFeatures features, float timeSeconds)
|
|
{
|
|
float dt;
|
|
if (_lastRenderTimeSeconds < 0f)
|
|
{
|
|
dt = 1f / 120f;
|
|
}
|
|
else
|
|
{
|
|
dt = Math.Clamp(timeSeconds - _lastRenderTimeSeconds, 1f / 240f, 1f / 20f);
|
|
}
|
|
|
|
_lastRenderTimeSeconds = timeSeconds;
|
|
|
|
for (int y = 0; y < _internalHeight; y++)
|
|
{
|
|
float t = y / (float)(_internalHeight - 1);
|
|
Color bg = new(0.985f - t * 0.04f, 0.98f - t * 0.04f, 0.95f - t * 0.03f, 1f);
|
|
int row = y * _internalWidth;
|
|
for (int x = 0; x < _internalWidth; x++)
|
|
{
|
|
_pixelBuffer[row + x] = bg;
|
|
}
|
|
}
|
|
|
|
int left = (int)(_internalWidth * 0.06f);
|
|
int right = (int)(_internalWidth * 0.94f);
|
|
int top = (int)(_internalHeight * 0.34f);
|
|
int spacing = Math.Max(5, _internalHeight / 24);
|
|
|
|
float bpm = features.Bpm > 0f ? features.Bpm : 90f;
|
|
float pxPerSecond = 14f + bpm * 0.24f;
|
|
float speed = pxPerSecond * dt;
|
|
_sheetOffsetX += speed;
|
|
int measureSpacing = Math.Max(30, _internalWidth / 6);
|
|
if (_sheetOffsetX >= measureSpacing)
|
|
{
|
|
_sheetOffsetX -= measureSpacing;
|
|
}
|
|
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
int y = top + i * spacing;
|
|
DrawRect(left, y, right - left, 1, new Vector3(0.14f, 0.14f, 0.16f), 0.85f);
|
|
}
|
|
|
|
for (int x = right - (int)_sheetOffsetX; x > left; x -= measureSpacing)
|
|
{
|
|
DrawRect(x, top - spacing / 2, 1, spacing * 5, new Vector3(0.2f, 0.2f, 0.24f), 0.55f);
|
|
}
|
|
|
|
if (features.NoteOnset)
|
|
{
|
|
SpawnScrollingStaffNote(features, right, top, spacing);
|
|
}
|
|
|
|
for (int i = _scrollStaffNotes.Count - 1; i >= 0; i--)
|
|
{
|
|
ScrollingStaffNoteParticle n = _scrollStaffNotes[i];
|
|
n.X -= speed * (0.95f + n.SpeedJitter);
|
|
n.Life -= 0.0045f;
|
|
if (n.Life <= 0f || n.X < left - 30)
|
|
{
|
|
_scrollStaffNotes.RemoveAt(i);
|
|
continue;
|
|
}
|
|
|
|
_scrollStaffNotes[i] = n;
|
|
DrawScrollingStaffNote(n, top, spacing);
|
|
}
|
|
}
|
|
|
|
private void SpawnScrollingStaffNote(AudioFeatures features, int right, int top, int spacing)
|
|
{
|
|
if (features.DetectedNoteIndex < 0 || _scrollStaffNotes.Count >= ScaleCount(180, 30))
|
|
{
|
|
return;
|
|
}
|
|
|
|
int staffStep = NoteToStaffStep(features.DetectedNoteIndex);
|
|
int staffCenterY = top + spacing * 4;
|
|
int y = staffCenterY - (staffStep * (spacing / 2));
|
|
int x = right - 8 + _rng.Next(0, 8);
|
|
|
|
_scrollStaffNotes.Add(new ScrollingStaffNoteParticle
|
|
{
|
|
X = x,
|
|
Y = y,
|
|
Life = 1f,
|
|
UpStem = _rng.NextDouble() > 0.5,
|
|
Filled = features.DetectedNoteStrength > 0.20f,
|
|
SpeedJitter = (float)_rng.NextDouble() * 0.35f
|
|
});
|
|
}
|
|
|
|
private void DrawScrollingStaffNote(ScrollingStaffNoteParticle n, int top, int spacing)
|
|
{
|
|
int x = (int)n.X;
|
|
int y = n.Y;
|
|
float a = Clamp01(n.Life);
|
|
Vector3 ink = new(0.07f, 0.07f, 0.1f);
|
|
|
|
int rx = Math.Max(2, spacing / 3);
|
|
int ry = Math.Max(2, spacing / 4 + 1);
|
|
DrawEllipse(x, y, rx, ry, ink, a, n.Filled);
|
|
|
|
int stemH = spacing * 3;
|
|
if (n.UpStem)
|
|
{
|
|
DrawRect(x + rx, y - stemH, 1, stemH, ink, a);
|
|
}
|
|
else
|
|
{
|
|
DrawRect(x - rx, y, 1, stemH, ink, a);
|
|
}
|
|
|
|
int staffTop = top;
|
|
int staffBottom = top + spacing * 4;
|
|
if (y < staffTop - spacing / 2 || y > staffBottom + spacing / 2)
|
|
{
|
|
DrawRect(x - spacing / 2, y, spacing, 1, ink, a * 0.9f);
|
|
}
|
|
}
|
|
|
|
private void SpawnStaffNote(AudioFeatures features, int left, int right, int top, int spacing)
|
|
{
|
|
if (features.DetectedNoteIndex < 0 || _staffNotes.Count >= ScaleCount(120, 20))
|
|
{
|
|
return;
|
|
}
|
|
|
|
int noteIndex = features.DetectedNoteIndex;
|
|
int staffStep = NoteToStaffStep(noteIndex);
|
|
int staffCenterY = top + spacing * 4;
|
|
int y = staffCenterY - (staffStep * (spacing / 2));
|
|
int x = left + _rng.Next(0, Math.Max(10, right - left - 20));
|
|
|
|
_staffNotes.Add(new StaffNoteParticle
|
|
{
|
|
X = x,
|
|
Y = y,
|
|
Vx = 0.20f + (float)_rng.NextDouble() * 0.35f,
|
|
Life = 1f,
|
|
UpStem = _rng.NextDouble() > 0.5,
|
|
Filled = features.DetectedNoteStrength > 0.22f
|
|
});
|
|
}
|
|
|
|
private static int NoteToStaffStep(int noteIndex)
|
|
{
|
|
return noteIndex switch
|
|
{
|
|
0 => -1,
|
|
1 => -1,
|
|
2 => 0,
|
|
3 => 0,
|
|
4 => 1,
|
|
5 => 2,
|
|
6 => 2,
|
|
7 => 3,
|
|
8 => 3,
|
|
9 => 4,
|
|
10 => 4,
|
|
11 => 5,
|
|
_ => 0
|
|
};
|
|
}
|
|
|
|
private void DrawStaffNote(StaffNoteParticle n, int top, int spacing)
|
|
{
|
|
int x = (int)n.X;
|
|
int y = n.Y;
|
|
float a = Clamp01(n.Life);
|
|
Vector3 ink = new(0.08f, 0.08f, 0.1f);
|
|
|
|
int rx = Math.Max(2, spacing / 3);
|
|
int ry = Math.Max(2, spacing / 4 + 1);
|
|
DrawEllipse(x, y, rx, ry, ink, a, n.Filled);
|
|
|
|
int stemH = spacing * 3;
|
|
if (n.UpStem)
|
|
{
|
|
DrawRect(x + rx, y - stemH, 1, stemH, ink, a);
|
|
}
|
|
else
|
|
{
|
|
DrawRect(x - rx, y, 1, stemH, ink, a);
|
|
}
|
|
|
|
int staffTop = top;
|
|
int staffBottom = top + spacing * 4;
|
|
if (y < staffTop - spacing / 2 || y > staffBottom + spacing / 2)
|
|
{
|
|
DrawRect(x - spacing / 2, y, spacing, 1, ink, a * 0.9f);
|
|
}
|
|
}
|
|
|
|
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 DrawEllipse(int cx, int cy, int rx, int ry, Vector3 color, float alpha, bool filled)
|
|
{
|
|
int minX = cx - rx - 1;
|
|
int maxX = cx + rx + 1;
|
|
int minY = cy - ry - 1;
|
|
int maxY = cy + ry + 1;
|
|
float irx = 1f / (rx * rx + 1e-6f);
|
|
float iry = 1f / (ry * ry + 1e-6f);
|
|
for (int y = minY; y <= maxY; y++)
|
|
{
|
|
if (y < 0 || y >= _internalHeight)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (int x = minX; x <= maxX; x++)
|
|
{
|
|
if (x < 0 || x >= _internalWidth)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
float dx = x - cx;
|
|
float dy = y - cy;
|
|
float d = dx * dx * irx + dy * dy * iry;
|
|
if (filled)
|
|
{
|
|
if (d <= 1f)
|
|
{
|
|
BlendPixel(x, y, color, alpha);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (d >= 0.7f && d <= 1.2f)
|
|
{
|
|
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 StaffNoteParticle
|
|
{
|
|
public float X;
|
|
public int Y;
|
|
public float Vx;
|
|
public float Life;
|
|
public bool UpStem;
|
|
public bool Filled;
|
|
}
|
|
|
|
private struct ScrollingStaffNoteParticle
|
|
{
|
|
public float X;
|
|
public int Y;
|
|
public float Life;
|
|
public bool UpStem;
|
|
public bool Filled;
|
|
public float SpeedJitter;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|