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#[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#[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}