aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core/Audio/Synth.cs
blob: cb663a94b5a03c637be9662c08d10660f7d548e0 (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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
using System;

namespace Godosa.Core.Audio;

/// <summary>Deterministic xorshift32 for sound variants and seeded songs. The seed is mixed first
/// (murmur3 fmix32): raw xorshift's first draws barely change between nearby seeds.</summary>
public sealed class SynthRandom(uint seed)
{
    private uint _state = Mix(seed) is var s && s != 0 ? s : 0x9E3779B9u;

    public static uint Mix(uint h)
    {
        h ^= h >> 16;
        h *= 0x85EBCA6Bu;
        h ^= h >> 13;
        h *= 0xC2B2AE35u;
        h ^= h >> 16;
        return h;
    }

    public float Next01()
    {
        _state ^= _state << 13;
        _state ^= _state >> 17;
        _state ^= _state << 5;
        return (_state >> 8) / 16777216f;
    }

    public float Range(float min, float max) => min + (max - min) * Next01();

    /// <summary>Uniform in [−1, 1).</summary>
    public float Signed() => Next01() * 2f - 1f;

    /// <summary>FNV-1a: stable across runs (string.GetHashCode isn't).</summary>
    public static uint Hash(string text)
    {
        uint h = 2166136261;
        foreach (char c in text) h = (h ^ c) * 16777619;
        return h;
    }
}

public enum WaveShape { Sine, Saw, Square, Triangle }

/// <summary>Placeholder sound synthesis (audio spec §3): float buffers at 44.1 kHz,
/// oscillators with exponential sweeps, noise, envelopes, swept RBJ biquads.</summary>
public static class Synth
{
    public const int Rate = Pcm.SampleRate;
    private const int FilterBlock = 32;

    public static int Samples(float seconds) => Math.Max(1, (int)(seconds * Rate));

    public static float[] Sine(float seconds, float f0, float? f1 = null) => Wave(WaveShape.Sine, seconds, f0, f1 ?? f0);

    /// <summary>Oscillator whose pitch sweeps exponentially from f0 to f1 over the buffer.</summary>
    public static float[] Wave(WaveShape shape, float seconds, float f0, float f1)
    {
        var x = new float[Samples(seconds)];
        double phase = 0, ratio = f1 / f0;
        for (int i = 0; i < x.Length; i++)
        {
            double f = f0 * Math.Pow(ratio, i / (double)x.Length);
            float p = (float)(phase - Math.Floor(phase));
            x[i] = shape switch
            {
                WaveShape.Saw => 2f * p - 1f,
                WaveShape.Square => p < 0.5f ? 1f : -1f,
                WaveShape.Triangle => 1f - 4f * MathF.Abs(p - 0.5f),
                _ => MathF.Sin(2f * MathF.PI * p),
            };
            phase += f / Rate;
        }
        return x;
    }

    /// <summary>FM: carrier f, modulator at f·ratio, index sweeping index0 → index1 (bells, chirps).</summary>
    public static float[] Fm(float seconds, float carrier, float ratio, float index0, float index1, float? carrierEnd = null)
    {
        var x = new float[Samples(seconds)];
        double pc = 0, pm = 0, cRatio = (carrierEnd ?? carrier) / carrier;
        for (int i = 0; i < x.Length; i++)
        {
            float t = i / (float)x.Length;
            double c = carrier * Math.Pow(cRatio, t);
            float index = index0 + (index1 - index0) * t;
            x[i] = (float)Math.Sin(2 * Math.PI * pc + index * Math.Sin(2 * Math.PI * pm));
            pc += c / Rate;
            pm += c * ratio / Rate;
        }
        return x;
    }

    public static float[] White(float seconds, SynthRandom rng)
    {
        var x = new float[Samples(seconds)];
        for (int i = 0; i < x.Length; i++) x[i] = rng.Signed();
        return x;
    }

    /// <summary>Pink-ish noise (Paul Kellet's economy filter): softer, like wind and leaves.</summary>
    public static float[] Pink(float seconds, SynthRandom rng)
    {
        var x = new float[Samples(seconds)];
        float b0 = 0, b1 = 0, b2 = 0;
        for (int i = 0; i < x.Length; i++)
        {
            float w = rng.Signed();
            b0 = 0.99765f * b0 + w * 0.0990460f;
            b1 = 0.96300f * b1 + w * 0.2965164f;
            b2 = 0.57000f * b2 + w * 1.0526913f;
            x[i] = (b0 + b1 + b2 + w * 0.1848f) * 0.25f;
        }
        return x;
    }

    /// <summary>Linear attack to 1, then exponential decay with time constant <paramref name="decay"/>.</summary>
    public static float[] Perc(this float[] x, float attack, float decay)
    {
        for (int i = 0; i < x.Length; i++)
        {
            float t = i / (float)Rate;
            x[i] *= t < attack ? t / attack : MathF.Exp(-(t - attack) / decay);
        }
        return x;
    }

    public static float[] Gain(this float[] x, float gain)
    {
        for (int i = 0; i < x.Length; i++) x[i] *= gain;
        return x;
    }

    /// <summary>Adds <paramref name="y"/> into <paramref name="into"/> at a time offset (clipped to fit).</summary>
    public static float[] Add(this float[] into, float[] y, float atSeconds = 0f, float gain = 1f)
    {
        int offset = (int)(atSeconds * Rate);
        for (int i = 0; i < y.Length && offset + i < into.Length; i++) into[offset + i] += y[i] * gain;
        return into;
    }

    public static float[] LowPass(this float[] x, float f0, float? f1 = null, float q = 0.7071f) => Filter(x, FilterKind.LowPass, f0, f1 ?? f0, q);
    public static float[] HighPass(this float[] x, float f0, float? f1 = null, float q = 0.7071f) => Filter(x, FilterKind.HighPass, f0, f1 ?? f0, q);
    public static float[] BandPass(this float[] x, float f0, float q, float? f1 = null) => Filter(x, FilterKind.BandPass, f0, f1 ?? f0, q);

    /// <summary>Biquad with its cutoff swept exponentially f0 → f1, retuned every 32 samples. In place.</summary>
    private static float[] Filter(float[] x, FilterKind kind, float f0, float f1, float q)
    {
        var filter = new BiquadFilter();
        for (int i = 0; i < x.Length; i++)
        {
            if (i % FilterBlock == 0) filter.Set(kind, f0 * MathF.Pow(f1 / f0, i / (float)x.Length), q);
            x[i] = filter.Process(x[i]);
        }
        return x;
    }

    /// <summary>Makes a loop seamless: the extra <paramref name="overlapSamples"/> at the end fade
    /// into the start, so the result wraps like any adjacent pair of samples.</summary>
    public static float[] Loop(float[] x, int overlapSamples)
    {
        int overlap = overlapSamples, n = x.Length - overlap;
        var y = new float[n];
        Array.Copy(x, y, n);
        for (int i = 0; i < overlap; i++)
        {
            float t = i / (float)overlap;
            y[i] = x[i] * t + x[n + i] * (1f - t);
        }
        return y;
    }

    /// <summary>Normalises the peak to <paramref name="level"/> (0–1 FS) and, unless it's a loop,
    /// fades 2 ms at both ends so starts and stops never click.</summary>
    public static short[] ToPcm(float[] x, float level, bool fadeEnds = true)
    {
        float peak = 0f;
        foreach (float v in x) peak = MathF.Max(peak, MathF.Abs(v));
        float scale = peak > 0f ? level / peak : 0f;
        int fade = fadeEnds ? Math.Min(x.Length / 2, Samples(0.002f)) : 0;
        var pcm = new short[x.Length];
        for (int i = 0; i < x.Length; i++)
        {
            float env = fade == 0 ? 1f : Math.Min(1f, Math.Min(i, x.Length - 1 - i) / (float)fade);
            pcm[i] = Pcm.ToShort(x[i] * scale * env);
        }
        return pcm;
    }
}