using System; namespace Godosa.Core.Audio; /// Deterministic xorshift32 for sound variants and seeded songs. The seed is mixed first /// (murmur3 fmix32): raw xorshift's first draws barely change between nearby seeds. public sealed class SynthRandom(uint seed) { private uint _state = Mix(seed) is var s && s != 0 ? s : 0x9E3779B9u; public static uint Mix(uint h) { h ^= h >> 16; h *= 0x85EBCA6Bu; h ^= h >> 13; h *= 0xC2B2AE35u; h ^= h >> 16; return h; } public float Next01() { _state ^= _state << 13; _state ^= _state >> 17; _state ^= _state << 5; return (_state >> 8) / 16777216f; } public float Range(float min, float max) => min + (max - min) * Next01(); /// Uniform in [−1, 1). public float Signed() => Next01() * 2f - 1f; /// FNV-1a: stable across runs (string.GetHashCode isn't). public static uint Hash(string text) { uint h = 2166136261; foreach (char c in text) h = (h ^ c) * 16777619; return h; } } public enum WaveShape { Sine, Saw, Square, Triangle } /// Placeholder sound synthesis (audio spec §3): float buffers at 44.1 kHz, /// oscillators with exponential sweeps, noise, envelopes, swept RBJ biquads. public static class Synth { public const int Rate = Pcm.SampleRate; private const int FilterBlock = 32; public static int Samples(float seconds) => Math.Max(1, (int)(seconds * Rate)); public static float[] Sine(float seconds, float f0, float? f1 = null) => Wave(WaveShape.Sine, seconds, f0, f1 ?? f0); /// Oscillator whose pitch sweeps exponentially from f0 to f1 over the buffer. public static float[] Wave(WaveShape shape, float seconds, float f0, float f1) { var x = new float[Samples(seconds)]; double phase = 0, ratio = f1 / f0; for (int i = 0; i < x.Length; i++) { double f = f0 * Math.Pow(ratio, i / (double)x.Length); float p = (float)(phase - Math.Floor(phase)); x[i] = shape switch { WaveShape.Saw => 2f * p - 1f, WaveShape.Square => p < 0.5f ? 1f : -1f, WaveShape.Triangle => 1f - 4f * MathF.Abs(p - 0.5f), _ => MathF.Sin(2f * MathF.PI * p), }; phase += f / Rate; } return x; } /// FM: carrier f, modulator at f·ratio, index sweeping index0 → index1 (bells, chirps). public static float[] Fm(float seconds, float carrier, float ratio, float index0, float index1, float? carrierEnd = null) { var x = new float[Samples(seconds)]; double pc = 0, pm = 0, cRatio = (carrierEnd ?? carrier) / carrier; for (int i = 0; i < x.Length; i++) { float t = i / (float)x.Length; double c = carrier * Math.Pow(cRatio, t); float index = index0 + (index1 - index0) * t; x[i] = (float)Math.Sin(2 * Math.PI * pc + index * Math.Sin(2 * Math.PI * pm)); pc += c / Rate; pm += c * ratio / Rate; } return x; } public static float[] White(float seconds, SynthRandom rng) { var x = new float[Samples(seconds)]; for (int i = 0; i < x.Length; i++) x[i] = rng.Signed(); return x; } /// Pink-ish noise (Paul Kellet's economy filter): softer, like wind and leaves. public static float[] Pink(float seconds, SynthRandom rng) { var x = new float[Samples(seconds)]; float b0 = 0, b1 = 0, b2 = 0; for (int i = 0; i < x.Length; i++) { float w = rng.Signed(); b0 = 0.99765f * b0 + w * 0.0990460f; b1 = 0.96300f * b1 + w * 0.2965164f; b2 = 0.57000f * b2 + w * 1.0526913f; x[i] = (b0 + b1 + b2 + w * 0.1848f) * 0.25f; } return x; } /// Linear attack to 1, then exponential decay with time constant . public static float[] Perc(this float[] x, float attack, float decay) { for (int i = 0; i < x.Length; i++) { float t = i / (float)Rate; x[i] *= t < attack ? t / attack : MathF.Exp(-(t - attack) / decay); } return x; } public static float[] Gain(this float[] x, float gain) { for (int i = 0; i < x.Length; i++) x[i] *= gain; return x; } /// Adds into at a time offset (clipped to fit). public static float[] Add(this float[] into, float[] y, float atSeconds = 0f, float gain = 1f) { int offset = (int)(atSeconds * Rate); for (int i = 0; i < y.Length && offset + i < into.Length; i++) into[offset + i] += y[i] * gain; return into; } public static float[] LowPass(this float[] x, float f0, float? f1 = null, float q = 0.7071f) => Filter(x, FilterKind.LowPass, f0, f1 ?? f0, q); public static float[] HighPass(this float[] x, float f0, float? f1 = null, float q = 0.7071f) => Filter(x, FilterKind.HighPass, f0, f1 ?? f0, q); public static float[] BandPass(this float[] x, float f0, float q, float? f1 = null) => Filter(x, FilterKind.BandPass, f0, f1 ?? f0, q); /// Biquad with its cutoff swept exponentially f0 → f1, retuned every 32 samples. In place. private static float[] Filter(float[] x, FilterKind kind, float f0, float f1, float q) { var filter = new BiquadFilter(); for (int i = 0; i < x.Length; i++) { if (i % FilterBlock == 0) filter.Set(kind, f0 * MathF.Pow(f1 / f0, i / (float)x.Length), q); x[i] = filter.Process(x[i]); } return x; } /// Makes a loop seamless: the extra at the end fade /// into the start, so the result wraps like any adjacent pair of samples. public static float[] Loop(float[] x, int overlapSamples) { int overlap = overlapSamples, n = x.Length - overlap; var y = new float[n]; Array.Copy(x, y, n); for (int i = 0; i < overlap; i++) { float t = i / (float)overlap; y[i] = x[i] * t + x[n + i] * (1f - t); } return y; } /// Normalises the peak to (0–1 FS) and, unless it's a loop, /// fades 2 ms at both ends so starts and stops never click. public static short[] ToPcm(float[] x, float level, bool fadeEnds = true) { float peak = 0f; foreach (float v in x) peak = MathF.Max(peak, MathF.Abs(v)); float scale = peak > 0f ? level / peak : 0f; int fade = fadeEnds ? Math.Min(x.Length / 2, Samples(0.002f)) : 0; var pcm = new short[x.Length]; for (int i = 0; i < x.Length; i++) { float env = fade == 0 ? 1f : Math.Min(1f, Math.Min(i, x.Length - 1 - i) / (float)fade); pcm[i] = Pcm.ToShort(x[i] * scale * env); } return pcm; } }