Skip to main content

rustmotion_core/audio/
dsp.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4pub fn sine(phase: f32) -> f32 {
5    (phase * std::f32::consts::TAU).sin()
6}
7
8pub fn square(phase: f32) -> f32 {
9    if phase < 0.5 {
10        1.0
11    } else {
12        -1.0
13    }
14}
15
16pub fn saw(phase: f32) -> f32 {
17    2.0 * phase - 1.0
18}
19
20pub fn triangle(phase: f32) -> f32 {
21    if phase < 0.5 {
22        4.0 * phase - 1.0
23    } else {
24        3.0 - 4.0 * phase
25    }
26}
27
28#[derive(Debug, Clone, Copy)]
29pub struct Xorshift32 {
30    state: u32,
31}
32
33impl Xorshift32 {
34    pub fn new(seed: u32) -> Self {
35        Xorshift32 {
36            state: if seed == 0 { 0x9E37_79B9 } else { seed },
37        }
38    }
39
40    pub fn next_u32(&mut self) -> u32 {
41        let mut x = self.state;
42        x ^= x << 13;
43        x ^= x >> 17;
44        x ^= x << 5;
45        self.state = x;
46        x
47    }
48
49    pub fn next_f32(&mut self) -> f32 {
50        (self.next_u32() as f32 / u32::MAX as f32) * 2.0 - 1.0
51    }
52}
53
54/// Oscillator waveform a [`super::voices::Voice`] selects — the sine/
55/// square/saw/triangle quartet, plus `noise` for a non-tonal voice (hats,
56/// snares). Lives here rather than in `voices.rs` because it is exactly
57/// the vocabulary [`sine`]/[`square`]/[`saw`]/[`triangle`]/[`Xorshift32`]
58/// above already speak.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
60#[serde(rename_all = "snake_case")]
61pub enum OscKind {
62    Sine,
63    Square,
64    Saw,
65    Triangle,
66    Noise,
67}
68
69/// The four biquad topologies this synth implements — deliberately just
70/// these four (deliverable #2's list), no shelving/peaking EQ.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
72#[serde(rename_all = "snake_case")]
73pub enum FilterKind {
74    Lowpass,
75    Highpass,
76    Bandpass,
77    Notch,
78}
79
80#[derive(Debug, Clone, Copy, Default)]
81pub struct BiquadState {
82    x1: f32,
83    x2: f32,
84    y1: f32,
85    y2: f32,
86}
87
88#[derive(Debug, Clone, Copy)]
89pub struct BiquadCoeffs {
90    b0: f32,
91    b1: f32,
92    b2: f32,
93    a1: f32,
94    a2: f32,
95}
96
97impl BiquadCoeffs {
98    pub fn design(kind: FilterKind, freq: f32, q: f32, sample_rate: f32) -> Self {
99        let freq = freq.clamp(1.0, sample_rate * 0.499);
100        let q = q.max(0.01);
101        let w0 = std::f32::consts::TAU * freq / sample_rate;
102        let cos_w0 = w0.cos();
103        let sin_w0 = w0.sin();
104        let alpha = sin_w0 / (2.0 * q);
105
106        let (b0, b1, b2, a0, a1, a2) = match kind {
107            FilterKind::Lowpass => {
108                let b1 = 1.0 - cos_w0;
109                let b0 = b1 / 2.0;
110                (b0, b1, b0, 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha)
111            }
112            FilterKind::Highpass => {
113                let b1 = -(1.0 + cos_w0);
114                let b0 = (1.0 + cos_w0) / 2.0;
115                (b0, b1, b0, 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha)
116            }
117            FilterKind::Bandpass => (alpha, 0.0, -alpha, 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha),
118            FilterKind::Notch => (
119                1.0,
120                -2.0 * cos_w0,
121                1.0,
122                1.0 + alpha,
123                -2.0 * cos_w0,
124                1.0 - alpha,
125            ),
126        };
127
128        BiquadCoeffs {
129            b0: b0 / a0,
130            b1: b1 / a0,
131            b2: b2 / a0,
132            a1: a1 / a0,
133            a2: a2 / a0,
134        }
135    }
136
137    pub fn process(&self, state: &mut BiquadState, x: f32) -> f32 {
138        let y = self.b0 * x + self.b1 * state.x1 + self.b2 * state.x2
139            - self.a1 * state.y1
140            - self.a2 * state.y2;
141        state.x2 = state.x1;
142        state.x1 = x;
143        state.y2 = state.y1;
144        state.y1 = y;
145        y
146    }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq)]
150pub struct Adsr {
151    pub attack: f32,
152    pub decay: f32,
153    pub sustain: f32,
154    pub hold: f32,
155    pub release: f32,
156}
157
158impl Adsr {
159    pub fn total_duration(&self) -> f32 {
160        self.attack.max(0.0) + self.decay.max(0.0) + self.hold.max(0.0) + self.release.max(0.0)
161    }
162
163    pub fn level_at(&self, t: f32) -> f32 {
164        if t < 0.0 {
165            return 0.0;
166        }
167        let a = self.attack.max(0.0);
168        let d = self.decay.max(0.0);
169        let h = self.hold.max(0.0);
170        let r = self.release.max(0.0);
171        let sustain = self.sustain.clamp(0.0, 1.0);
172
173        if t < a {
174            if a <= 0.0 {
175                1.0
176            } else {
177                t / a
178            }
179        } else if t < a + d {
180            let dt = (t - a) / d.max(1e-9);
181            1.0 + (sustain - 1.0) * dt
182        } else if t < a + d + h {
183            sustain
184        } else if t < a + d + h + r {
185            let dt = (t - (a + d + h)) / r.max(1e-9);
186            sustain * (1.0 - dt)
187        } else {
188            0.0
189        }
190    }
191}
192
193#[derive(Debug, Clone, Copy)]
194pub struct CompressorParams {
195    pub threshold_db: f32,
196    pub ratio: f32,
197    pub attack_ms: f32,
198    pub release_ms: f32,
199}
200
201fn db_to_lin(db: f32) -> f32 {
202    10f32.powf(db / 20.0)
203}
204
205fn lin_to_db(lin: f32) -> f32 {
206    20.0 * lin.max(1e-9).log10()
207}
208
209fn time_const_coeff(ms: f32, sample_rate: f32) -> f32 {
210    if ms <= 0.0 {
211        0.0
212    } else {
213        (-1.0 / (ms / 1000.0 * sample_rate)).exp()
214    }
215}
216
217pub fn apply_compressor(buffer: &mut [f32], params: CompressorParams, sample_rate: u32) {
218    let sr = sample_rate as f32;
219    let attack_coeff = time_const_coeff(params.attack_ms, sr);
220    let release_coeff = time_const_coeff(params.release_ms, sr);
221    let ratio = params.ratio.max(1.0);
222    let mut envelope = 0.0f32;
223
224    for sample in buffer.iter_mut() {
225        let input_level = sample.abs();
226        let coeff = if input_level > envelope {
227            attack_coeff
228        } else {
229            release_coeff
230        };
231        envelope = coeff * envelope + (1.0 - coeff) * input_level;
232
233        let gain = if envelope > 1e-9 {
234            let envelope_db = lin_to_db(envelope);
235            let over_db = envelope_db - params.threshold_db;
236            if over_db > 0.0 {
237                let gain_reduction_db = over_db * (1.0 - 1.0 / ratio);
238                db_to_lin(-gain_reduction_db)
239            } else {
240                1.0
241            }
242        } else {
243            1.0
244        };
245        *sample *= gain;
246    }
247}
248
249pub const LIMITER_CEILING_DB: f32 = -0.3;
250
251pub fn apply_limiter(buffer: &mut [f32], sample_rate: u32) {
252    let ceiling = db_to_lin(LIMITER_CEILING_DB);
253    let sr = sample_rate as f32;
254    let attack_coeff = time_const_coeff(1.0, sr);
255    let release_coeff = time_const_coeff(50.0, sr);
256    let mut envelope = 0.0f32;
257
258    for sample in buffer.iter_mut() {
259        let input_level = sample.abs();
260        let coeff = if input_level > envelope {
261            attack_coeff
262        } else {
263            release_coeff
264        };
265        envelope = coeff * envelope + (1.0 - coeff) * input_level;
266
267        let gain = if envelope > ceiling && envelope > 1e-9 {
268            ceiling / envelope
269        } else {
270            1.0
271        };
272        *sample = (*sample * gain).clamp(-ceiling, ceiling);
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn oscillators_are_bounded_and_hit_their_landmarks() {
282        assert!((sine(0.0)).abs() < 1e-6);
283        assert!((sine(0.25) - 1.0).abs() < 1e-6);
284        assert_eq!(square(0.0), 1.0);
285        assert_eq!(square(0.75), -1.0);
286        assert!((saw(0.0) - -1.0).abs() < 1e-6);
287        assert!((saw(1.0) - 1.0).abs() < 1e-6);
288        assert!((triangle(0.0) - -1.0).abs() < 1e-6);
289        assert!((triangle(0.5) - 1.0).abs() < 1e-6);
290    }
291
292    #[test]
293    fn xorshift32_is_deterministic_for_a_given_seed() {
294        let mut a = Xorshift32::new(42);
295        let mut b = Xorshift32::new(42);
296        let seq_a: Vec<f32> = (0..64).map(|_| a.next_f32()).collect();
297        let seq_b: Vec<f32> = (0..64).map(|_| b.next_f32()).collect();
298        assert_eq!(seq_a, seq_b);
299        assert!(seq_a.iter().all(|&s| (-1.0..=1.0).contains(&s)));
300    }
301
302    #[test]
303    fn xorshift32_seed_zero_does_not_lock_up() {
304        let mut z = Xorshift32::new(0);
305        let vals: Vec<u32> = (0..8).map(|_| z.next_u32()).collect();
306        assert!(vals.iter().any(|&v| v != 0));
307    }
308
309    #[test]
310    fn lowpass_attenuates_a_tone_well_above_cutoff_more_than_one_well_below() {
311        let sr = 48_000.0f32;
312        let coeffs = BiquadCoeffs::design(FilterKind::Lowpass, 500.0, 0.707, sr);
313
314        let rms_after = |freq: f32| -> f32 {
315            let mut state = BiquadState::default();
316            let n = 4800usize;
317            let mut acc = 0.0f64;
318            for i in 0..n {
319                let phase = (i as f32 * freq / sr).fract();
320                let x = sine(phase);
321                let y = coeffs.process(&mut state, x);
322                if i > n / 2 {
323                    acc += (y as f64) * (y as f64);
324                }
325            }
326            ((acc / (n / 2) as f64).sqrt()) as f32
327        };
328
329        let low = rms_after(100.0);
330        let high = rms_after(8000.0);
331        assert!(
332            high < low * 0.5,
333            "8kHz through a 500Hz lowpass ({high}) should be much quieter than 100Hz ({low})"
334        );
335    }
336
337    #[test]
338    fn adsr_percussive_default_reaches_silence_after_attack_plus_decay() {
339        let env = Adsr {
340            attack: 0.01,
341            decay: 0.1,
342            sustain: 0.0,
343            hold: 0.0,
344            release: 0.0,
345        };
346        assert_eq!(env.level_at(-1.0), 0.0);
347        assert!(env.level_at(0.0) < env.level_at(0.01));
348        assert!((env.level_at(0.01) - 1.0).abs() < 1e-4);
349        assert!(env.level_at(0.05) < 1.0);
350        assert_eq!(env.level_at(0.11), 0.0);
351        assert!((env.total_duration() - 0.11).abs() < 1e-6);
352    }
353
354    #[test]
355    fn compressor_reduces_gain_above_threshold_and_leaves_quiet_signal_alone() {
356        let sr = 48_000;
357        let mut loud = vec![0.9f32; 4800];
358        let mut quiet = vec![0.05f32; 4800];
359        let params = CompressorParams {
360            threshold_db: -12.0,
361            ratio: 4.0,
362            attack_ms: 1.0,
363            release_ms: 20.0,
364        };
365        apply_compressor(&mut loud, params, sr);
366        apply_compressor(&mut quiet, params, sr);
367        assert!(loud.last().unwrap() < &0.9);
368        assert!((quiet.last().unwrap() - 0.05).abs() < 1e-4);
369    }
370
371    #[test]
372    fn limiter_never_exceeds_its_ceiling_even_on_a_full_scale_step() {
373        let sr = 48_000;
374        let mut buf = vec![1.0f32; 4800];
375        buf[0] = 1.0;
376        apply_limiter(&mut buf, sr);
377        let ceiling = db_to_lin(LIMITER_CEILING_DB);
378        assert!(buf.iter().all(|&s| s.abs() <= ceiling + 1e-6));
379    }
380}