1use crate::modules::common::{flush_denorm, sanitize_audio, Memo};
4use crate::port::{GraphModule, PortDef, PortSpec, PortValues, SignalKind};
5use alloc::vec;
6use core::f64::consts::{PI, TAU};
7use libm::Libm;
8
9pub struct Svf {
26 ic1eq: f64,
28 ic2eq: f64,
30 sample_rate: f64,
31 g_memo: Memo<4, f64>,
34 spec: PortSpec,
35}
36
37const SVF_K_MIN: f64 = 1e-5;
42
43const SVF_STATE_LIMIT: f64 = 8.0;
48
49#[inline]
53fn svf_soft_clip(x: f64) -> f64 {
54 if Libm::<f64>::fabs(x) <= SVF_STATE_LIMIT {
55 x
56 } else {
57 SVF_STATE_LIMIT * Libm::<f64>::tanh(x / SVF_STATE_LIMIT)
58 }
59}
60
61impl Svf {
62 pub fn new(sample_rate: f64) -> Self {
63 Self {
64 ic1eq: 0.0,
65 ic2eq: 0.0,
66 sample_rate,
67 g_memo: Memo::new(0.0),
68 spec: PortSpec {
69 inputs: vec![
70 PortDef::new(0, "in", SignalKind::Audio),
71 PortDef::new(1, "cutoff", SignalKind::CvUnipolar)
72 .with_default(0.5)
73 .with_attenuverter(),
74 PortDef::new(2, "res", SignalKind::CvUnipolar)
75 .with_default(0.0)
76 .with_attenuverter(),
77 PortDef::new(3, "fm", SignalKind::CvBipolar).with_attenuverter(),
78 PortDef::new(4, "keytrack", SignalKind::VoltPerOctave),
80 PortDef::new(5, "keytrack_amt", SignalKind::CvUnipolar).with_default(0.0),
82 ],
83 outputs: vec![
84 PortDef::new(10, "lp", SignalKind::Audio),
85 PortDef::new(11, "bp", SignalKind::Audio),
86 PortDef::new(12, "hp", SignalKind::Audio),
87 PortDef::new(13, "notch", SignalKind::Audio),
88 ],
89 },
90 }
91 }
92}
93
94impl Default for Svf {
95 fn default() -> Self {
96 Self::new(44100.0)
97 }
98}
99
100impl GraphModule for Svf {
101 fn port_spec(&self) -> &PortSpec {
102 &self.spec
103 }
104
105 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
106 let input = sanitize_audio(inputs.get_or(0, 0.0));
109 let cutoff_cv = inputs.get_or(1, 0.5) + inputs.get_or(3, 0.0);
110 let res = inputs.get_or(2, 0.0).clamp(0.0, 1.0);
111
112 let keytrack_voct = inputs.get_or(4, 0.0);
114 let keytrack_amt = inputs.get_or(5, 0.0).clamp(0.0, 1.0);
115
116 let sample_rate = self.sample_rate;
119 let g = self.g_memo.get_or_compute(
120 [cutoff_cv, keytrack_voct, keytrack_amt, sample_rate],
121 || {
122 let base_cutoff_hz = 20.0 * Libm::<f64>::pow(1000.0, cutoff_cv.clamp(0.0, 1.0));
124
125 let keytrack_multiplier = Libm::<f64>::pow(2.0, keytrack_voct * keytrack_amt);
127 let cutoff_hz = (base_cutoff_hz * keytrack_multiplier).clamp(20.0, 20000.0);
128
129 let max_fc = 0.49 * sample_rate;
133 let fc = Libm::<f64>::fmin(cutoff_hz, max_fc);
134 Libm::<f64>::tan(PI * fc / sample_rate)
135 },
136 );
137
138 let k = Libm::<f64>::fmax(2.0 - 2.0 * res, SVF_K_MIN);
142
143 let a1 = 1.0 / (1.0 + g * (g + k));
145 let a2 = g * a1;
146 let a3 = g * a2;
147
148 let v0 = input;
150 let v3 = v0 - self.ic2eq;
151 let v1 = a1 * self.ic1eq + a2 * v3;
152 let v2 = self.ic2eq + a2 * self.ic1eq + a3 * v3;
153
154 self.ic1eq = flush_denorm(svf_soft_clip(2.0 * v1 - self.ic1eq));
158 self.ic2eq = flush_denorm(svf_soft_clip(2.0 * v2 - self.ic2eq));
159
160 let low = v2;
161 let band = v1;
162 let high = v0 - k * v1 - v2;
163 let notch = low + high; outputs.set(10, low); outputs.set(11, band); outputs.set(12, high); outputs.set(13, notch); }
170
171 fn reset(&mut self) {
172 self.ic1eq = 0.0;
173 self.ic2eq = 0.0;
174 }
175
176 fn set_sample_rate(&mut self, sample_rate: f64) {
177 self.sample_rate = sample_rate;
178 }
179
180 fn type_id(&self) -> &'static str {
181 "svf"
182 }
183}
184
185pub struct DiodeLadderFilter {
196 stages: [f64; 4],
198 feedback: f64,
200 sample_rate: f64,
202 big_g_memo: Memo<4, f64>,
205 spec: PortSpec,
207}
208
209impl DiodeLadderFilter {
210 pub fn new(sample_rate: f64) -> Self {
211 Self {
212 stages: [0.0; 4],
213 feedback: 0.0,
214 sample_rate,
215 big_g_memo: Memo::new(0.0),
216 spec: PortSpec {
217 inputs: vec![
218 PortDef::new(0, "in", SignalKind::Audio),
219 PortDef::new(1, "cutoff", SignalKind::CvUnipolar)
220 .with_default(0.5)
221 .with_attenuverter(),
222 PortDef::new(2, "res", SignalKind::CvUnipolar)
223 .with_default(0.0)
224 .with_attenuverter(),
225 PortDef::new(3, "fm", SignalKind::CvBipolar).with_attenuverter(),
226 PortDef::new(4, "keytrack", SignalKind::VoltPerOctave),
227 PortDef::new(5, "keytrack_amt", SignalKind::CvUnipolar).with_default(0.0),
228 PortDef::new(6, "drive", SignalKind::CvUnipolar)
229 .with_default(0.0)
230 .with_attenuverter(),
231 ],
232 outputs: vec![
233 PortDef::new(10, "out", SignalKind::Audio),
234 PortDef::new(11, "pole1", SignalKind::Audio), PortDef::new(12, "pole2", SignalKind::Audio), PortDef::new(13, "pole3", SignalKind::Audio), ],
238 },
239 }
240 }
241
242 #[inline]
244 fn diode_sat(x: f64) -> f64 {
245 if x >= 0.0 {
247 Libm::<f64>::tanh(x * 1.2)
248 } else {
249 Libm::<f64>::tanh(x * 0.8)
250 }
251 }
252
253 #[inline]
262 fn run_cascade(u: f64, s: &[f64; 4], big_g: f64) -> ([f64; 4], [f64; 4]) {
263 let mut y = [0.0f64; 4];
264 let mut new_s = [0.0f64; 4];
265 let mut x = Self::diode_sat(u / 5.0) * 5.0;
267 for i in 0..4 {
268 let v = (x - s[i]) * big_g; let yi = v + s[i]; y[i] = yi;
271 new_s[i] = yi + v; x = Self::diode_sat(yi / 5.0) * 5.0;
274 }
275 (y, new_s)
276 }
277}
278
279impl Default for DiodeLadderFilter {
280 fn default() -> Self {
281 Self::new(44100.0)
282 }
283}
284
285impl GraphModule for DiodeLadderFilter {
286 fn port_spec(&self) -> &PortSpec {
287 &self.spec
288 }
289
290 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
291 let input = sanitize_audio(inputs.get_or(0, 0.0));
294 let cutoff_cv = inputs.get_or(1, 0.5) + inputs.get_or(3, 0.0);
295 let res = inputs.get_or(2, 0.0).clamp(0.0, 1.0);
296 let keytrack_voct = inputs.get_or(4, 0.0);
297 let keytrack_amt = inputs.get_or(5, 0.0).clamp(0.0, 1.0);
298 let drive = inputs.get_or(6, 0.0).clamp(0.0, 1.0);
299
300 let sample_rate = self.sample_rate;
303 let big_g = self.big_g_memo.get_or_compute(
304 [cutoff_cv, keytrack_voct, keytrack_amt, sample_rate],
305 || {
306 let base_cutoff_hz = 20.0 * Libm::<f64>::pow(1000.0, cutoff_cv.clamp(0.0, 1.0));
308
309 let keytrack_multiplier = Libm::<f64>::pow(2.0, keytrack_voct * keytrack_amt);
311 let cutoff_hz = (base_cutoff_hz * keytrack_multiplier).clamp(20.0, 20000.0);
312
313 let max_fc = 0.49 * sample_rate;
316 let fc = Libm::<f64>::fmin(cutoff_hz, max_fc);
317 let wc = PI * fc / sample_rate;
318 let g = Libm::<f64>::tan(wc);
319 g / (1.0 + g)
320 },
321 );
322
323 let k = res * 4.0;
326
327 let drive_gain = 1.0 + drive * 3.0;
329
330 let input_driven = Self::diode_sat(input / 5.0 * drive_gain) * 5.0;
332
333 let mut fb_norm = self.feedback; for _ in 0..2 {
344 let fb = Self::diode_sat(fb_norm * k);
345 let u = input_driven - fb * 5.0;
346 let (y, _) = Self::run_cascade(u, &self.stages, big_g);
347 fb_norm = y[3] / 5.0;
348 }
349
350 let fb = Self::diode_sat(fb_norm * k);
352 let u = input_driven - fb * 5.0;
353 let (y, new_s) = Self::run_cascade(u, &self.stages, big_g);
354
355 self.stages[0] = flush_denorm(new_s[0]);
357 self.stages[1] = flush_denorm(new_s[1]);
358 self.stages[2] = flush_denorm(new_s[2]);
359 self.stages[3] = flush_denorm(new_s[3]);
360 self.feedback = flush_denorm(y[3] / 5.0);
361
362 outputs.set(10, y[3]); outputs.set(11, y[0]); outputs.set(12, y[1]); outputs.set(13, y[2]); }
368
369 fn reset(&mut self) {
370 self.stages = [0.0; 4];
371 self.feedback = 0.0;
372 }
373
374 fn set_sample_rate(&mut self, sample_rate: f64) {
375 self.sample_rate = sample_rate;
376 }
377
378 fn type_id(&self) -> &'static str {
379 "diode_ladder"
380 }
381}
382
383pub struct ParametricEq {
393 low_state: [f64; 2],
395 mid_state: [f64; 2],
396 high_state: [f64; 2],
397 low_coefs: [f64; 5],
401 mid_coefs: [f64; 5],
402 high_coefs: [f64; 5],
403 cached_low: [f64; 2], cached_mid: [f64; 3], cached_high: [f64; 2], recompute_count: u64,
410 sample_rate: f64,
411 spec: PortSpec,
412}
413
414impl ParametricEq {
415 pub fn new(sample_rate: f64) -> Self {
416 Self {
417 low_state: [0.0; 2],
418 mid_state: [0.0; 2],
419 high_state: [0.0; 2],
420 low_coefs: [0.0; 5],
421 mid_coefs: [0.0; 5],
422 high_coefs: [0.0; 5],
423 cached_low: [f64::NAN; 2],
424 cached_mid: [f64::NAN; 3],
425 cached_high: [f64::NAN; 2],
426 recompute_count: 0,
427 sample_rate,
428 spec: PortSpec {
429 inputs: vec![
430 PortDef::new(0, "in", SignalKind::Audio),
431 PortDef::new(1, "low_gain", SignalKind::CvBipolar)
432 .with_default(0.0)
433 .with_attenuverter(),
434 PortDef::new(2, "low_freq", SignalKind::CvUnipolar)
435 .with_default(0.2)
436 .with_attenuverter(),
437 PortDef::new(3, "mid_gain", SignalKind::CvBipolar)
438 .with_default(0.0)
439 .with_attenuverter(),
440 PortDef::new(4, "mid_freq", SignalKind::CvUnipolar)
441 .with_default(0.5)
442 .with_attenuverter(),
443 PortDef::new(5, "mid_q", SignalKind::CvUnipolar)
444 .with_default(0.5)
445 .with_attenuverter(),
446 PortDef::new(6, "high_gain", SignalKind::CvBipolar)
447 .with_default(0.0)
448 .with_attenuverter(),
449 PortDef::new(7, "high_freq", SignalKind::CvUnipolar)
450 .with_default(0.7)
451 .with_attenuverter(),
452 ],
453 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
454 },
455 }
456 }
457
458 fn calc_low_shelf(freq: f64, gain_db: f64, sample_rate: f64) -> [f64; 5] {
461 let a = Libm::<f64>::pow(10.0, gain_db / 40.0);
462 let w0 = TAU * freq / sample_rate;
463 let cos_w0 = Libm::<f64>::cos(w0);
464 let sin_w0 = Libm::<f64>::sin(w0);
465 let alpha = sin_w0 / 2.0 * Libm::<f64>::sqrt(2.0);
466 let sqrt_a = Libm::<f64>::sqrt(a);
467
468 let a0 = (a + 1.0) + (a - 1.0) * cos_w0 + 2.0 * sqrt_a * alpha;
469 let b0 = a * ((a + 1.0) - (a - 1.0) * cos_w0 + 2.0 * sqrt_a * alpha);
470 let b1 = 2.0 * a * ((a - 1.0) - (a + 1.0) * cos_w0);
471 let b2 = a * ((a + 1.0) - (a - 1.0) * cos_w0 - 2.0 * sqrt_a * alpha);
472 let a1 = -2.0 * ((a - 1.0) + (a + 1.0) * cos_w0);
473 let a2 = (a + 1.0) + (a - 1.0) * cos_w0 - 2.0 * sqrt_a * alpha;
474
475 [b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0]
476 }
477
478 fn calc_high_shelf(freq: f64, gain_db: f64, sample_rate: f64) -> [f64; 5] {
480 let a = Libm::<f64>::pow(10.0, gain_db / 40.0);
481 let w0 = TAU * freq / sample_rate;
482 let cos_w0 = Libm::<f64>::cos(w0);
483 let sin_w0 = Libm::<f64>::sin(w0);
484 let alpha = sin_w0 / 2.0 * Libm::<f64>::sqrt(2.0);
485 let sqrt_a = Libm::<f64>::sqrt(a);
486
487 let a0 = (a + 1.0) - (a - 1.0) * cos_w0 + 2.0 * sqrt_a * alpha;
488 let b0 = a * ((a + 1.0) + (a - 1.0) * cos_w0 + 2.0 * sqrt_a * alpha);
489 let b1 = -2.0 * a * ((a - 1.0) + (a + 1.0) * cos_w0);
490 let b2 = a * ((a + 1.0) + (a - 1.0) * cos_w0 - 2.0 * sqrt_a * alpha);
491 let a1 = 2.0 * ((a - 1.0) - (a + 1.0) * cos_w0);
492 let a2 = (a + 1.0) - (a - 1.0) * cos_w0 - 2.0 * sqrt_a * alpha;
493
494 [b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0]
495 }
496
497 fn calc_peaking(freq: f64, gain_db: f64, q: f64, sample_rate: f64) -> [f64; 5] {
499 let a = Libm::<f64>::pow(10.0, gain_db / 40.0);
500 let w0 = TAU * freq / sample_rate;
501 let cos_w0 = Libm::<f64>::cos(w0);
502 let sin_w0 = Libm::<f64>::sin(w0);
503 let alpha = sin_w0 / (2.0 * q);
504
505 let a0 = 1.0 + alpha / a;
506 let b0 = (1.0 + alpha * a) / a0;
507 let b1 = (-2.0 * cos_w0) / a0;
508 let b2 = (1.0 - alpha * a) / a0;
509 let a1 = (-2.0 * cos_w0) / a0;
510 let a2 = (1.0 - alpha / a) / a0;
511
512 [b0, b1, b2, a1, a2]
513 }
514
515 #[inline]
517 fn process_biquad(input: f64, coefs: &[f64; 5], state: &mut [f64; 2]) -> f64 {
518 let output = coefs[0] * input + state[0];
519 state[0] = coefs[1] * input - coefs[3] * output + state[1];
520 state[1] = coefs[2] * input - coefs[4] * output;
521 output
522 }
523}
524
525impl Default for ParametricEq {
526 fn default() -> Self {
527 Self::new(44100.0)
528 }
529}
530
531impl GraphModule for ParametricEq {
532 fn port_spec(&self) -> &PortSpec {
533 &self.spec
534 }
535
536 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
537 let input = sanitize_audio(inputs.get_or(0, 0.0));
541
542 let low_gain_db = (inputs.get_or(1, 0.0) / 5.0) * 12.0;
545 let mid_gain_db = (inputs.get_or(3, 0.0) / 5.0) * 12.0;
546 let high_gain_db = (inputs.get_or(6, 0.0) / 5.0) * 12.0;
547
548 let low_freq_cv = inputs.get_or(2, 0.2).clamp(0.0, 1.0);
550 let low_freq = 50.0 * Libm::<f64>::pow(10.0, low_freq_cv); let mid_freq_cv = inputs.get_or(4, 0.5).clamp(0.0, 1.0);
553 let mid_freq = 200.0 * Libm::<f64>::pow(40.0, mid_freq_cv); let high_freq_cv = inputs.get_or(7, 0.7).clamp(0.0, 1.0);
556 let high_freq = 2000.0 + high_freq_cv * 10000.0; let mid_q_cv = inputs.get_or(5, 0.5).clamp(0.0, 1.0);
560 let mid_q = 0.5 + mid_q_cv * 9.5;
561
562 let nyquist = self.sample_rate * 0.45;
564 let low_freq = low_freq.clamp(20.0, nyquist);
565 let mid_freq = mid_freq.clamp(20.0, nyquist);
566 let high_freq = high_freq.clamp(20.0, nyquist);
567
568 let low_params = [low_freq, low_gain_db];
574 if self.cached_low != low_params {
575 self.low_coefs = Self::calc_low_shelf(low_freq, low_gain_db, self.sample_rate);
576 self.cached_low = low_params;
577 self.recompute_count += 1;
578 }
579 let mid_params = [mid_freq, mid_gain_db, mid_q];
580 if self.cached_mid != mid_params {
581 self.mid_coefs = Self::calc_peaking(mid_freq, mid_gain_db, mid_q, self.sample_rate);
582 self.cached_mid = mid_params;
583 self.recompute_count += 1;
584 }
585 let high_params = [high_freq, high_gain_db];
586 if self.cached_high != high_params {
587 self.high_coefs = Self::calc_high_shelf(high_freq, high_gain_db, self.sample_rate);
588 self.cached_high = high_params;
589 self.recompute_count += 1;
590 }
591
592 let mut signal = input;
594 signal = Self::process_biquad(signal, &self.low_coefs, &mut self.low_state);
595 signal = Self::process_biquad(signal, &self.mid_coefs, &mut self.mid_state);
596 signal = Self::process_biquad(signal, &self.high_coefs, &mut self.high_state);
597
598 outputs.set(10, signal);
599 }
600
601 fn reset(&mut self) {
602 self.low_state = [0.0; 2];
603 self.mid_state = [0.0; 2];
604 self.high_state = [0.0; 2];
605 }
606
607 fn set_sample_rate(&mut self, sample_rate: f64) {
608 self.sample_rate = sample_rate;
609 self.cached_low = [f64::NAN; 2];
612 self.cached_mid = [f64::NAN; 3];
613 self.cached_high = [f64::NAN; 2];
614 self.reset();
615 }
616
617 fn type_id(&self) -> &'static str {
618 "parametric_eq"
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use super::*;
625 use crate::modules::common::{measure_max_output, SAFE_AUDIO_LIMIT};
626
627 #[test]
628 fn test_svf_filter() {
629 let mut svf = Svf::new(44100.0);
630 let mut inputs = PortValues::new();
631 let mut outputs = PortValues::new();
632
633 inputs.set(0, 5.0); inputs.set(1, 0.1); svf.tick(&inputs, &mut outputs);
638
639 assert!(outputs.get(10).is_some());
641 }
642 #[test]
643 fn test_svf_default_reset_sample_rate() {
644 let mut svf = Svf::default();
645 assert!(svf.sample_rate == 44100.0);
646
647 svf.set_sample_rate(48000.0);
648 assert!(svf.sample_rate == 48000.0);
649
650 let mut inputs = PortValues::new();
651 let mut outputs = PortValues::new();
652 inputs.set(0, 1.0);
653 for _ in 0..100 {
654 svf.tick(&inputs, &mut outputs);
655 }
656
657 svf.reset();
658 assert!(svf.ic1eq == 0.0);
661
662 assert_eq!(svf.type_id(), "svf");
663 }
664 #[test]
665 fn test_diode_ladder_filter_coverage() {
666 use crate::{Crosstalk, DiodeLadderFilter, GroundLoop};
667
668 let mut dlf = DiodeLadderFilter::default();
670 assert!(dlf.sample_rate == 44100.0);
671
672 dlf.set_sample_rate(48000.0);
673 assert!(dlf.sample_rate == 48000.0);
674
675 let mut inputs = PortValues::new();
676 let mut outputs = PortValues::new();
677 inputs.set(0, 1.0);
678 for _ in 0..100 {
679 dlf.tick(&inputs, &mut outputs);
680 }
681
682 dlf.reset();
683 assert!(dlf.stages[0] == 0.0);
684
685 assert_eq!(dlf.type_id(), "diode_ladder");
686
687 let mut crosstalk = Crosstalk::default();
689 crosstalk.set_sample_rate(48000.0);
690 inputs.set(0, 1.0);
691 inputs.set(1, 2.0);
692 crosstalk.tick(&inputs, &mut outputs);
693 crosstalk.reset();
694 assert_eq!(crosstalk.type_id(), "crosstalk");
695
696 let mut gl = GroundLoop::default();
698 gl.set_sample_rate(48000.0);
699 gl.tick(&inputs, &mut outputs);
700 gl.reset();
701 assert_eq!(gl.type_id(), "ground_loop");
702 }
703 #[test]
704 fn test_parametric_eq_passthrough() {
705 let mut eq = ParametricEq::new(44100.0);
706 let mut inputs = PortValues::new();
707 let mut outputs = PortValues::new();
708
709 inputs.set(0, 1.0); inputs.set(1, 0.0); inputs.set(3, 0.0); inputs.set(6, 0.0); for _ in 0..1000 {
717 eq.tick(&inputs, &mut outputs);
718 }
719
720 let out = outputs.get(10).unwrap();
721 assert!((out - 1.0).abs() < 0.01);
723 }
724
725 #[test]
726 fn test_parametric_eq_nan_recovery() {
727 let mut eq = ParametricEq::new(44100.0);
730 let mut inputs = PortValues::new();
731 let mut outputs = PortValues::new();
732 inputs.set(1, 0.0);
733 inputs.set(3, 0.0);
734 inputs.set(6, 0.0);
735
736 for &bad in &[f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
737 inputs.set(0, bad);
738 eq.tick(&inputs, &mut outputs);
739 }
740
741 inputs.set(0, 0.5);
743 let mut last = 0.0;
744 for _ in 0..2000 {
745 eq.tick(&inputs, &mut outputs);
746 last = outputs.get(10).unwrap();
747 }
748 assert!(
749 last.is_finite(),
750 "ParametricEq output stayed non-finite after a NaN input: {last}"
751 );
752 }
753
754 #[test]
755 fn test_parametric_eq_low_boost() {
756 let mut eq = ParametricEq::new(44100.0);
757 let mut inputs = PortValues::new();
758 let mut outputs = PortValues::new();
759
760 inputs.set(0, 1.0);
762 inputs.set(1, 5.0); inputs.set(2, 0.0); for _ in 0..1000 {
766 eq.tick(&inputs, &mut outputs);
767 }
768
769 let out = outputs.get(10).unwrap();
770 assert!(out > 1.0);
772 assert!(out.is_finite());
773 }
774 #[test]
775 fn test_parametric_eq_mid_cut() {
776 let mut eq = ParametricEq::new(44100.0);
777 let mut inputs = PortValues::new();
778 let mut outputs = PortValues::new();
779
780 inputs.set(0, 1.0);
782 inputs.set(3, -5.0); inputs.set(5, 1.0); for _ in 0..1000 {
786 eq.tick(&inputs, &mut outputs);
787 }
788
789 let out = outputs.get(10).unwrap();
790 assert!(out.is_finite());
791 }
792 #[test]
793 fn test_parametric_eq_high_boost() {
794 let mut eq = ParametricEq::new(44100.0);
795 let mut inputs = PortValues::new();
796 let mut outputs = PortValues::new();
797
798 inputs.set(0, 1.0);
799 inputs.set(6, 5.0); for _ in 0..1000 {
802 eq.tick(&inputs, &mut outputs);
803 }
804
805 let out = outputs.get(10).unwrap();
806 assert!(out.is_finite());
807 }
808 #[test]
809 fn test_parametric_eq_default_reset_sample_rate() {
810 let mut eq = ParametricEq::default();
811 assert!(eq.sample_rate == 44100.0);
812
813 let mut inputs = PortValues::new();
815 let mut outputs = PortValues::new();
816 inputs.set(0, 1.0);
817 inputs.set(1, 2.5); for _ in 0..100 {
819 eq.tick(&inputs, &mut outputs);
820 }
821
822 assert!(eq.low_state[0] != 0.0 || eq.low_state[1] != 0.0);
824
825 eq.reset();
827 assert_eq!(eq.low_state, [0.0; 2]);
828 assert_eq!(eq.mid_state, [0.0; 2]);
829 assert_eq!(eq.high_state, [0.0; 2]);
830
831 eq.set_sample_rate(48000.0);
833 assert_eq!(eq.sample_rate, 48000.0);
834
835 assert_eq!(eq.type_id(), "parametric_eq");
836 assert_eq!(eq.port_spec().inputs.len(), 8);
837 assert_eq!(eq.port_spec().outputs.len(), 1);
838 }
839 #[test]
840 fn test_parametric_eq_frequency_ranges() {
841 let mut eq = ParametricEq::new(44100.0);
842 let mut inputs = PortValues::new();
843 let mut outputs = PortValues::new();
844
845 inputs.set(0, 1.0);
847 inputs.set(2, 0.0); inputs.set(4, 0.0); inputs.set(7, 0.0); for _ in 0..100 {
852 eq.tick(&inputs, &mut outputs);
853 }
854 assert!(outputs.get(10).unwrap().is_finite());
855
856 eq.reset();
857 inputs.set(2, 1.0); inputs.set(4, 1.0); inputs.set(7, 1.0); for _ in 0..100 {
862 eq.tick(&inputs, &mut outputs);
863 }
864 assert!(outputs.get(10).unwrap().is_finite());
865 }
866 #[test]
867 fn test_parametric_eq_stability() {
868 let mut eq = ParametricEq::new(44100.0);
869 let mut inputs = PortValues::new();
870 let mut outputs = PortValues::new();
871
872 inputs.set(0, 5.0); inputs.set(1, 5.0); inputs.set(3, 5.0);
876 inputs.set(6, 5.0);
877 inputs.set(5, 1.0); eq.tick(&inputs, &mut outputs);
880
881 inputs.set(0, 0.0);
883 for _ in 0..10000 {
884 eq.tick(&inputs, &mut outputs);
885 }
886
887 let out = outputs.get(10).unwrap();
889 assert!(out.is_finite());
890 assert!(out.abs() < 0.01);
891 }
892 #[test]
893 fn test_svf_high_resonance_bounded() {
894 let test_resonances = [0.8, 0.85, 0.9, 0.92, 0.94, 0.96, 0.98, 1.0];
897
898 for &res in &test_resonances {
899 let mut svf = Svf::new(44100.0);
900 let mut inputs = PortValues::new();
901 let mut outputs = PortValues::new();
902
903 inputs.set(0, 5.0); inputs.set(1, 0.5); inputs.set(2, res); let max = measure_max_output(10000, || {
908 svf.tick(&inputs, &mut outputs);
909 let lp = outputs.get(10).unwrap_or(0.0).abs();
911 let bp = outputs.get(11).unwrap_or(0.0).abs();
912 let hp = outputs.get(12).unwrap_or(0.0).abs();
913 let notch = outputs.get(13).unwrap_or(0.0).abs();
914 lp.max(bp).max(hp).max(notch)
915 });
916
917 assert!(
918 max <= SAFE_AUDIO_LIMIT,
919 "SVF output {} exceeds safe limit {} at resonance {}",
920 max,
921 SAFE_AUDIO_LIMIT,
922 res
923 );
924 }
925 }
926 #[test]
927 fn test_svf_low_cutoff_transient_bounded() {
928 let mut svf = Svf::new(44100.0);
930 let mut inputs = PortValues::new();
931 let mut outputs = PortValues::new();
932
933 inputs.set(1, 0.0); inputs.set(2, 0.9); inputs.set(0, 0.0);
939 for _ in 0..100 {
940 svf.tick(&inputs, &mut outputs);
941 }
942
943 inputs.set(0, 5.0); let max = measure_max_output(5000, || {
945 svf.tick(&inputs, &mut outputs);
946 outputs.get(10).unwrap_or(0.0).abs()
947 });
948
949 assert!(
950 max <= SAFE_AUDIO_LIMIT,
951 "SVF transient response {} exceeds safe limit {} at low cutoff",
952 max,
953 SAFE_AUDIO_LIMIT
954 );
955 }
956 #[test]
957 fn test_svf_self_oscillation_bounded() {
958 let mut svf = Svf::new(44100.0);
960 let mut inputs = PortValues::new();
961 let mut outputs = PortValues::new();
962
963 inputs.set(0, 0.0); inputs.set(1, 0.5); inputs.set(2, 1.0); inputs.set(0, 1.0);
969 svf.tick(&inputs, &mut outputs);
970 inputs.set(0, 0.0);
971
972 let max = measure_max_output(20000, || {
974 svf.tick(&inputs, &mut outputs);
975 outputs.get(10).unwrap_or(0.0).abs()
976 });
977
978 assert!(
979 max <= SAFE_AUDIO_LIMIT,
980 "SVF self-oscillation {} exceeds safe limit {}",
981 max,
982 SAFE_AUDIO_LIMIT
983 );
984 }
985 #[test]
986 fn test_svf_extreme_input_bounded() {
987 let mut svf = Svf::new(44100.0);
989 let mut inputs = PortValues::new();
990 let mut outputs = PortValues::new();
991
992 inputs.set(0, 20.0); inputs.set(1, 0.5);
994 inputs.set(2, 0.9);
995
996 let max = measure_max_output(1000, || {
997 svf.tick(&inputs, &mut outputs);
998 outputs.get(10).unwrap_or(0.0).abs()
999 });
1000
1001 assert!(
1002 max <= SAFE_AUDIO_LIMIT * 2.0, "SVF with extreme input {} exceeds limit {}",
1004 max,
1005 SAFE_AUDIO_LIMIT * 2.0
1006 );
1007 }
1008 #[test]
1009 fn test_diode_ladder_high_resonance_bounded() {
1010 let mut filter = DiodeLadderFilter::new(44100.0);
1012 let mut inputs = PortValues::new();
1013 let mut outputs = PortValues::new();
1014
1015 inputs.set(0, 5.0); inputs.set(1, 0.5); inputs.set(2, 1.0); let max = measure_max_output(10000, || {
1020 filter.tick(&inputs, &mut outputs);
1021 outputs.get(10).unwrap_or(0.0).abs()
1022 });
1023
1024 assert!(
1025 max <= SAFE_AUDIO_LIMIT,
1026 "Diode ladder output {} exceeds safe limit {}",
1027 max,
1028 SAFE_AUDIO_LIMIT
1029 );
1030 }
1031
1032 fn cutoff_cv_for(freq_hz: f64) -> f64 {
1036 (freq_hz / 20.0).ln() / 1000.0_f64.ln()
1037 }
1038
1039 fn rms(samples: &[f64]) -> f64 {
1040 let sum_sq: f64 = samples.iter().map(|x| x * x).sum();
1041 (sum_sq / samples.len() as f64).sqrt()
1042 }
1043
1044 #[test]
1050 fn test_svf_max_resonance_finite_200k() {
1051 let mut svf = Svf::new(44100.0);
1052 let mut inputs = PortValues::new();
1053 let mut outputs = PortValues::new();
1054
1055 inputs.set(0, 1.0); inputs.set(1, 1.0); inputs.set(2, 1.0); let mut max_abs = 0.0f64;
1060 for n in 0..200_000 {
1061 svf.tick(&inputs, &mut outputs);
1062 for &id in &[10u32, 11, 12, 13] {
1063 let v = outputs.get(id).unwrap();
1064 assert!(
1065 v.is_finite(),
1066 "SVF output {id} became non-finite at sample {n}"
1067 );
1068 max_abs = max_abs.max(v.abs());
1069 }
1070 }
1071 assert!(
1072 max_abs < 50.0,
1073 "SVF max resonance output unbounded: {max_abs}"
1074 );
1075 }
1076
1077 #[test]
1083 fn test_svf_cutoff_accuracy() {
1084 let sample_rate = 44100.0;
1085 let res = (2.0 - core::f64::consts::SQRT_2) / 2.0;
1087
1088 for &target_fc in &[1000.0_f64, 10_000.0_f64] {
1089 let cv = cutoff_cv_for(target_fc);
1090
1091 let measure = |freq: f64| -> f64 {
1092 let mut svf = Svf::new(sample_rate);
1093 let mut inputs = PortValues::new();
1094 let mut outputs = PortValues::new();
1095 inputs.set(1, cv);
1096 inputs.set(2, res);
1097 let mut out = alloc::vec::Vec::new();
1098 let dt = freq / sample_rate;
1099 let mut phase = 0.0f64;
1100 for n in 0..40_000 {
1101 let s = Libm::<f64>::sin(TAU * phase);
1102 phase += dt;
1103 if phase >= 1.0 {
1104 phase -= 1.0;
1105 }
1106 inputs.set(0, s);
1107 svf.tick(&inputs, &mut outputs);
1108 if n >= 20_000 {
1109 out.push(outputs.get(10).unwrap());
1110 }
1111 }
1112 rms(&out)
1113 };
1114
1115 let passband = measure(target_fc / 8.0);
1116 let at_fc = measure(target_fc);
1117 let ratio = at_fc / passband;
1118 assert!(
1119 (ratio - core::f64::consts::FRAC_1_SQRT_2).abs() < 0.10,
1120 "SVF -3dB point off at fc={target_fc}: ratio {ratio} (expected ~0.707)"
1121 );
1122 }
1123 }
1124
1125 #[test]
1130 fn test_svf_self_oscillation_sustained() {
1131 let sample_rate = 44100.0;
1132 let mut svf = Svf::new(sample_rate);
1133 let mut inputs = PortValues::new();
1134 let mut outputs = PortValues::new();
1135
1136 inputs.set(1, cutoff_cv_for(2000.0)); inputs.set(2, 1.0); inputs.set(0, 5.0);
1141 svf.tick(&inputs, &mut outputs);
1142 inputs.set(0, 0.0);
1143
1144 let mut window = alloc::vec::Vec::new();
1145 for n in 0..66_150 {
1146 svf.tick(&inputs, &mut outputs);
1148 let v = outputs.get(11).unwrap(); assert!(v.is_finite());
1150 if n >= 44_100 {
1151 window.push(v); }
1153 }
1154 let r = rms(&window);
1155 assert!(
1156 (0.01..20.0).contains(&r),
1157 "SVF self-oscillation not sustained/bounded after 1s: rms {r}"
1158 );
1159 }
1160
1161 #[test]
1167 fn test_diode_ladder_resonance_peak_frequency() {
1168 let sample_rate = 44100.0;
1169 let target_fc = 2000.0;
1170 let cv = cutoff_cv_for(target_fc);
1171
1172 let gain_at = |freq: f64| -> f64 {
1174 let mut filter = DiodeLadderFilter::new(sample_rate);
1175 let mut inputs = PortValues::new();
1176 let mut outputs = PortValues::new();
1177 inputs.set(1, cv);
1178 inputs.set(2, 1.0); let dt = freq / sample_rate;
1180 let mut phase = 0.0f64;
1181 let mut buf = alloc::vec::Vec::new();
1182 for n in 0..40_000 {
1183 let s = 0.1 * Libm::<f64>::sin(TAU * phase);
1184 phase += dt;
1185 if phase >= 1.0 {
1186 phase -= 1.0;
1187 }
1188 inputs.set(0, s);
1189 filter.tick(&inputs, &mut outputs);
1190 if n >= 20_000 {
1191 buf.push(outputs.get(10).unwrap());
1192 }
1193 }
1194 rms(&buf)
1195 };
1196
1197 let spacing = 100.0;
1201 let sweep: alloc::vec::Vec<f64> = (0..13).map(|i| 1400.0 + spacing * i as f64).collect();
1202 let gains: alloc::vec::Vec<f64> = sweep.iter().map(|&f| gain_at(f)).collect();
1203 let mut peak = 1;
1204 for i in 1..gains.len() - 1 {
1205 if gains[i] > gains[peak] {
1206 peak = i;
1207 }
1208 }
1209 assert!(
1210 peak > 0 && peak < gains.len() - 1,
1211 "peak fell on sweep edge"
1212 );
1213 let (a, b, c) = (gains[peak - 1], gains[peak], gains[peak + 1]);
1214 let denom = a - 2.0 * b + c;
1215 let delta = if denom != 0.0 {
1216 0.5 * (a - c) / denom
1217 } else {
1218 0.0
1219 };
1220 let peak_f = sweep[peak] + delta * spacing;
1221 let err = (peak_f - target_fc).abs() / target_fc;
1222 assert!(
1223 err < 0.05,
1224 "Diode ladder resonant peak at {peak_f:.0} Hz, off from {target_fc} Hz by {:.1}%",
1225 err * 100.0
1226 );
1227 }
1228
1229 #[test]
1232 fn test_diode_ladder_max_resonance_stable_100k() {
1233 for &cv in &[0.1_f64, 0.5, 0.9] {
1234 let mut filter = DiodeLadderFilter::new(44100.0);
1235 let mut inputs = PortValues::new();
1236 let mut outputs = PortValues::new();
1237 inputs.set(0, 5.0);
1238 inputs.set(1, cv);
1239 inputs.set(2, 1.0);
1240
1241 let mut max_abs = 0.0f64;
1242 for n in 0..100_000 {
1243 filter.tick(&inputs, &mut outputs);
1244 for &id in &[10u32, 11, 12, 13] {
1245 let v = outputs.get(id).unwrap();
1246 assert!(v.is_finite(), "diode out {id} non-finite at {n} (cv={cv})");
1247 max_abs = max_abs.max(v.abs());
1248 }
1249 }
1250 assert!(
1251 max_abs <= SAFE_AUDIO_LIMIT,
1252 "diode unbounded {max_abs} at cv={cv}"
1253 );
1254 }
1255 }
1256
1257 #[test]
1261 fn test_parametric_eq_caching_bit_identical() {
1262 let sample_rate = 44100.0;
1263 let mut eq = ParametricEq::new(sample_rate);
1264 let mut inputs = PortValues::new();
1265 let mut outputs = PortValues::new();
1266
1267 inputs.set(1, 3.0); inputs.set(2, 0.4); inputs.set(3, -2.0); inputs.set(4, 0.6); inputs.set(5, 0.7); inputs.set(6, 4.0); inputs.set(7, 0.5); let low_gain_db = (3.0 / 5.0) * 12.0;
1278 let mid_gain_db = (-2.0 / 5.0) * 12.0;
1279 let high_gain_db = (4.0 / 5.0) * 12.0;
1280 let low_freq = (50.0 * Libm::<f64>::pow(10.0, 0.4)).clamp(20.0, sample_rate * 0.45);
1281 let mid_freq = (200.0 * Libm::<f64>::pow(40.0, 0.6)).clamp(20.0, sample_rate * 0.45);
1282 let high_freq: f64 = (2000.0 + 0.5 * 10000.0_f64).clamp(20.0, sample_rate * 0.45);
1283 let mid_q = 0.5 + 0.7 * 9.5;
1284 let low_c = ParametricEq::calc_low_shelf(low_freq, low_gain_db, sample_rate);
1285 let mid_c = ParametricEq::calc_peaking(mid_freq, mid_gain_db, mid_q, sample_rate);
1286 let high_c = ParametricEq::calc_high_shelf(high_freq, high_gain_db, sample_rate);
1287 let mut ref_low = [0.0; 2];
1288 let mut ref_mid = [0.0; 2];
1289 let mut ref_high = [0.0; 2];
1290
1291 let mut phase = 0.0f64;
1292 for _ in 0..2000 {
1293 let s = Libm::<f64>::sin(TAU * phase);
1294 phase += 500.0 / sample_rate;
1295 if phase >= 1.0 {
1296 phase -= 1.0;
1297 }
1298 inputs.set(0, s);
1299 eq.tick(&inputs, &mut outputs);
1300 let got = outputs.get(10).unwrap();
1301
1302 let mut r = ParametricEq::process_biquad(s, &low_c, &mut ref_low);
1303 r = ParametricEq::process_biquad(r, &mid_c, &mut ref_mid);
1304 r = ParametricEq::process_biquad(r, &high_c, &mut ref_high);
1305
1306 assert_eq!(
1307 got.to_bits(),
1308 r.to_bits(),
1309 "cached EQ output differs from recompute"
1310 );
1311 }
1312 }
1313
1314 #[test]
1317 fn test_parametric_eq_recompute_count() {
1318 let mut eq = ParametricEq::new(44100.0);
1319 let mut inputs = PortValues::new();
1320 let mut outputs = PortValues::new();
1321 inputs.set(0, 1.0);
1322 inputs.set(1, 2.0);
1323 inputs.set(3, 1.0);
1324 inputs.set(6, -1.0);
1325
1326 for _ in 0..100 {
1327 eq.tick(&inputs, &mut outputs);
1328 }
1329 assert_eq!(
1331 eq.recompute_count, 3,
1332 "static params should not recompute per sample"
1333 );
1334
1335 inputs.set(3, 2.0);
1337 eq.tick(&inputs, &mut outputs);
1338 assert_eq!(
1339 eq.recompute_count, 4,
1340 "changing one band should recompute only that band"
1341 );
1342
1343 for _ in 0..50 {
1345 eq.tick(&inputs, &mut outputs);
1346 }
1347 assert_eq!(
1348 eq.recompute_count, 4,
1349 "returning to static must not recompute"
1350 );
1351 }
1352
1353 #[test]
1356 fn test_parametric_eq_mid_band_response() {
1357 let sample_rate = 44100.0;
1358 let mid_freq = 200.0 * Libm::<f64>::pow(40.0, 0.5);
1361 let out_of_band = mid_freq / 8.0;
1362
1363 let measure = |tone_hz: f64, mid_gain_cv: f64| -> f64 {
1365 let mut eq = ParametricEq::new(sample_rate);
1366 let mut inputs = PortValues::new();
1367 let mut outputs = PortValues::new();
1368 inputs.set(3, mid_gain_cv); inputs.set(5, 1.0); let dt = tone_hz / sample_rate;
1371 let mut phase = 0.0f64;
1372 let mut out = alloc::vec::Vec::new();
1373 for n in 0..40_000 {
1374 let s = Libm::<f64>::sin(TAU * phase);
1375 phase += dt;
1376 if phase >= 1.0 {
1377 phase -= 1.0;
1378 }
1379 inputs.set(0, s);
1380 eq.tick(&inputs, &mut outputs);
1381 if n >= 20_000 {
1382 out.push(outputs.get(10).unwrap());
1383 }
1384 }
1385 rms(&out)
1386 };
1387
1388 let boost_in = measure(mid_freq, 5.0);
1391 let boost_out = measure(out_of_band, 5.0);
1392 let boost_db = 20.0 * Libm::<f64>::log10(boost_in / boost_out);
1393 assert!(
1394 (9.0..=13.0).contains(&boost_db),
1395 "mid +12dB boost: expected ~12dB in-band, got {boost_db:.2}dB"
1396 );
1397
1398 let cut_in = measure(mid_freq, -5.0);
1401 let cut_out = measure(out_of_band, -5.0);
1402 let cut_db = 20.0 * Libm::<f64>::log10(cut_in / cut_out);
1403 assert!(
1404 (-13.0..=-9.0).contains(&cut_db),
1405 "mid -12dB cut: expected ~-12dB in-band, got {cut_db:.2}dB"
1406 );
1407 }
1408
1409 #[test]
1416 fn test_svf_memo_bit_identical() {
1417 let mut memoized = Svf::new(44100.0);
1418 let mut forced = Svf::new(44100.0);
1419 let mut inputs = PortValues::new();
1420 let mut out_m = PortValues::new();
1421 let mut out_f = PortValues::new();
1422
1423 for n in 0..20_000u32 {
1424 let t = n as f64;
1425 inputs.set(0, Libm::<f64>::sin(t * 0.037) * 4.0);
1426 if n < 10_000 {
1427 inputs.set(1, 0.6);
1429 } else {
1430 inputs.set(1, 0.3 + 0.3 * Libm::<f64>::sin(t * 0.001));
1432 }
1433 inputs.set(2, 0.4);
1434 inputs.set(4, 0.25);
1435 inputs.set(5, 0.5);
1436
1437 memoized.tick(&inputs, &mut out_m);
1438 forced.g_memo.invalidate();
1439 forced.tick(&inputs, &mut out_f);
1440
1441 for &id in &[10u32, 11, 12, 13] {
1442 assert_eq!(
1443 out_m.get(id).unwrap().to_bits(),
1444 out_f.get(id).unwrap().to_bits(),
1445 "SVF output {id} diverged at sample {n}"
1446 );
1447 }
1448 }
1449 assert!(memoized.g_memo.recompute_count() <= 10_001);
1451 assert_eq!(forced.g_memo.recompute_count(), 20_000);
1452 }
1453
1454 #[test]
1456 fn test_diode_ladder_memo_bit_identical() {
1457 let mut memoized = DiodeLadderFilter::new(44100.0);
1458 let mut forced = DiodeLadderFilter::new(44100.0);
1459 let mut inputs = PortValues::new();
1460 let mut out_m = PortValues::new();
1461 let mut out_f = PortValues::new();
1462
1463 for n in 0..10_000u32 {
1464 let t = n as f64;
1465 inputs.set(0, Libm::<f64>::sin(t * 0.041) * 4.0);
1466 if n < 5_000 {
1467 inputs.set(1, 0.5);
1468 } else {
1469 inputs.set(1, 0.4 + 0.2 * Libm::<f64>::sin(t * 0.002));
1470 }
1471 inputs.set(2, 0.8);
1472 inputs.set(6, 0.5);
1473
1474 memoized.tick(&inputs, &mut out_m);
1475 forced.big_g_memo.invalidate();
1476 forced.tick(&inputs, &mut out_f);
1477
1478 for &id in &[10u32, 11, 12, 13] {
1479 assert_eq!(
1480 out_m.get(id).unwrap().to_bits(),
1481 out_f.get(id).unwrap().to_bits(),
1482 "diode ladder output {id} diverged at sample {n}"
1483 );
1484 }
1485 }
1486 assert!(memoized.big_g_memo.recompute_count() <= 5_001);
1487 }
1488
1489 #[test]
1492 fn test_svf_memo_recompute_count() {
1493 let mut svf = Svf::new(44100.0);
1494 let mut inputs = PortValues::new();
1495 let mut outputs = PortValues::new();
1496 inputs.set(0, 1.0);
1497 inputs.set(1, 0.5);
1498 for _ in 0..1000 {
1499 svf.tick(&inputs, &mut outputs);
1500 }
1501 assert_eq!(svf.g_memo.recompute_count(), 1);
1502
1503 svf.set_sample_rate(48000.0);
1504 svf.tick(&inputs, &mut outputs);
1505 assert_eq!(svf.g_memo.recompute_count(), 2);
1506 }
1507}