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;
}
}