using System; using System.Linq; using Godosa.Core.Audio; using Xunit; namespace Godosa.Core.Tests.Audio; public class SynthTests { [Fact] public void Random_IsDeterministicAndInUnitRange() { var a = new SynthRandom(42); var b = new SynthRandom(42); var values = Enumerable.Range(0, 1000).Select(_ => a.Next01()).ToArray(); Assert.Equal(values, Enumerable.Range(0, 1000).Select(_ => b.Next01()).ToArray()); Assert.All(values, v => Assert.InRange(v, 0f, 0.99999f)); Assert.InRange(values.Average(), 0.45f, 0.55f); } [Fact] public void Random_IsXorshift32OfTheMixedSeed() // pinned: seeded songs must sound the same in every run { // oracle: independent Python murmur3 fmix32 + xorshift32 — fmix32(1) = 0x514E28B7, first state 0x1F48D1FB Assert.Equal(0x514E28B7u, SynthRandom.Mix(1)); Assert.Equal(2050257f / 16777216f, new SynthRandom(1).Next01()); } [Fact] public void Random_NearbySeeds_StartFarApart() { var firsts = Enumerable.Range(0, 20).Select(s => new SynthRandom((uint)s).Next01()).ToList(); Assert.True(firsts.Max() - firsts.Min() > 0.5f); } [Fact] public void Loop_CrossfadesTheTailIntoTheHead() { var x = new float[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; var y = Synth.Loop(x, overlapSamples: 2); Assert.Equal(new float[] { 8, 5, 2, 3, 4, 5, 6, 7 }, y); // y[^1] = 7 wraps to y[0] = 8, like x[7] → x[8] } [Fact] public void Hash_IsFnv1a() // stable across runs, unlike string.GetHashCode { Assert.Equal(0xE40C292Cu, SynthRandom.Hash("a")); Assert.Equal(0x1A47E90Bu, SynthRandom.Hash("abc")); } [Fact] public void Sine_SteadyPitch() { var x = Synth.Sine(1f, 100f); Assert.Equal(44100, x.Length); Assert.InRange(SignalStats.ZeroCrossings(x), 198, 201); } [Fact] public void Sine_SweepIsExponential() { var x = Synth.Sine(1f, 100f, 400f); // f = 100·4^t: 144 crossings in the first half, 289 in the second Assert.InRange(SignalStats.ZeroCrossings(x, 0, 22050), 141, 148); Assert.InRange(SignalStats.ZeroCrossings(x, 22050), 285, 292); } [Fact] public void LowPass_PassesLowCutsHigh() { float low = SignalStats.Rms(Synth.Sine(0.5f, 200f).LowPass(1000f), 2000) / SignalStats.Rms(Synth.Sine(0.5f, 200f), 2000); float high = SignalStats.Rms(Synth.Sine(0.5f, 8000f).LowPass(1000f), 2000) / SignalStats.Rms(Synth.Sine(0.5f, 8000f), 2000); Assert.InRange(low, 0.89f, 1.05f); // within 1 dB Assert.InRange(high, 0f, 0.1f); // −20 dB or more (2nd order: ≈ −36 dB at 3 octaves) } [Fact] public void HighPass_PassesHighCutsLow() { float low = SignalStats.Rms(Synth.Sine(0.5f, 100f).HighPass(1000f), 4000) / SignalStats.Rms(Synth.Sine(0.5f, 100f), 4000); float high = SignalStats.Rms(Synth.Sine(0.5f, 6000f).HighPass(1000f), 4000) / SignalStats.Rms(Synth.Sine(0.5f, 6000f), 4000); Assert.InRange(low, 0f, 0.05f); Assert.InRange(high, 0.89f, 1.05f); } [Fact] public void BandPass_PassesCentreOnly() { float Ratio(float hz) => SignalStats.Rms(Synth.Sine(0.5f, hz).BandPass(1000f, 2f), 4000) / SignalStats.Rms(Synth.Sine(0.5f, hz), 4000); Assert.InRange(Ratio(1000f), 0.85f, 1.05f); Assert.InRange(Ratio(100f), 0f, 0.1f); Assert.InRange(Ratio(8000f), 0f, 0.1f); } [Fact] public void Perc_RisesThenDecaysByTimeConstant() { var x = Enumerable.Repeat(1f, 44100).ToArray().Perc(attack: 0.01f, decay: 0.1f); Assert.Equal(0f, x[0]); Assert.InRange(x[441], 0.99f, 1f); // top at the end of the attack Assert.InRange(x[441 + 4410], 0.36f, 0.375f); // e^-1 one time constant later } [Fact] public void ToPcm_NormalisesToLevel_AndFadesEnds() { var pcm = Synth.ToPcm(Enumerable.Repeat(0.3f, 4410).ToArray(), level: 0.5f); Assert.Equal((short)16384, pcm.Max()); Assert.Equal(0, pcm[0]); Assert.Equal(0, pcm[^1]); } }