aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core.Tests/Audio/SynthTests.cs
blob: 7c2c29b1a026aa6c4b95469eae9463c04173507f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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]);
    }
}