aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core.Tests/Audio/SynthTests.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Godosa.Core.Tests/Audio/SynthTests.cs')
-rw-r--r--src/Godosa.Core.Tests/Audio/SynthTests.cs122
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]);
+ }
+}