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