diff options
| author | godosa <godosa@godosa.eu> | 2026-10-06 23:39:36 +0200 |
|---|---|---|
| committer | godosa <godosa@godosa.eu> | 2026-10-06 23:39:36 +0200 |
| commit | 39066900773e7857faf2d02e7ed51b71d219d97d (patch) | |
| tree | ffab8e4ddd971626776c3b1f4ce1da2c186c2187 /src/Godosa.Core.Tests/Audio/SynthTests.cs | |
| download | godosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.tar.gz godosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.zip | |
godosa-engine: initial public history
Diffstat (limited to 'src/Godosa.Core.Tests/Audio/SynthTests.cs')
| -rw-r--r-- | src/Godosa.Core.Tests/Audio/SynthTests.cs | 122 |
1 files changed, 122 insertions, 0 deletions
diff --git a/src/Godosa.Core.Tests/Audio/SynthTests.cs b/src/Godosa.Core.Tests/Audio/SynthTests.cs new file mode 100644 index 0000000..7c2c29b --- /dev/null +++ b/src/Godosa.Core.Tests/Audio/SynthTests.cs @@ -0,0 +1,122 @@ +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]); + } +} |
