1use crate::modules::common::{flush_denorm, sanitize_audio};
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 spec: PortSpec,
32}
33
34const SVF_K_MIN: f64 = 1e-5;
39
40const SVF_STATE_LIMIT: f64 = 8.0;
45
46#[inline]
50fn svf_soft_clip(x: f64) -> f64 {
51 if Libm::<f64>::fabs(x) <= SVF_STATE_LIMIT {
52 x
53 } else {
54 SVF_STATE_LIMIT * Libm::<f64>::tanh(x / SVF_STATE_LIMIT)
55 }
56}
57
58impl Svf {
59 pub fn new(sample_rate: f64) -> Self {
60 Self {
61 ic1eq: 0.0,
62 ic2eq: 0.0,
63 sample_rate,
64 spec: PortSpec {
65 inputs: vec![
66 PortDef::new(0, "in", SignalKind::Audio),
67 PortDef::new(1, "cutoff", SignalKind::CvUnipolar)
68 .with_default(0.5)
69 .with_attenuverter(),
70 PortDef::new(2, "res", SignalKind::CvUnipolar)
71 .with_default(0.0)
72 .with_attenuverter(),
73 PortDef::new(3, "fm", SignalKind::CvBipolar).with_attenuverter(),
74 PortDef::new(4, "keytrack", SignalKind::VoltPerOctave),
76 PortDef::new(5, "keytrack_amt", SignalKind::CvUnipolar).with_default(0.0),
78 ],
79 outputs: vec![
80 PortDef::new(10, "lp", SignalKind::Audio),
81 PortDef::new(11, "bp", SignalKind::Audio),
82 PortDef::new(12, "hp", SignalKind::Audio),
83 PortDef::new(13, "notch", SignalKind::Audio),
84 ],
85 },
86 }
87 }
88}
89
90impl Default for Svf {
91 fn default() -> Self {
92 Self::new(44100.0)
93 }
94}
95
96impl GraphModule for Svf {
97 fn port_spec(&self) -> &PortSpec {
98 &self.spec
99 }
100
101 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
102 let input = sanitize_audio(inputs.get_or(0, 0.0));
105 let cutoff_cv = inputs.get_or(1, 0.5) + inputs.get_or(3, 0.0);
106 let res = inputs.get_or(2, 0.0).clamp(0.0, 1.0);
107
108 let keytrack_voct = inputs.get_or(4, 0.0);
110 let keytrack_amt = inputs.get_or(5, 0.0).clamp(0.0, 1.0);
111
112 let base_cutoff_hz = 20.0 * Libm::<f64>::pow(1000.0, cutoff_cv.clamp(0.0, 1.0));
114
115 let keytrack_multiplier = Libm::<f64>::pow(2.0, keytrack_voct * keytrack_amt);
117 let cutoff_hz = (base_cutoff_hz * keytrack_multiplier).clamp(20.0, 20000.0);
118
119 let max_fc = 0.49 * self.sample_rate;
123 let fc = Libm::<f64>::fmin(cutoff_hz, max_fc);
124 let g = Libm::<f64>::tan(PI * fc / self.sample_rate);
125
126 let k = Libm::<f64>::fmax(2.0 - 2.0 * res, SVF_K_MIN);
130
131 let a1 = 1.0 / (1.0 + g * (g + k));
133 let a2 = g * a1;
134 let a3 = g * a2;
135
136 let v0 = input;
138 let v3 = v0 - self.ic2eq;
139 let v1 = a1 * self.ic1eq + a2 * v3;
140 let v2 = self.ic2eq + a2 * self.ic1eq + a3 * v3;
141
142 self.ic1eq = flush_denorm(svf_soft_clip(2.0 * v1 - self.ic1eq));
146 self.ic2eq = flush_denorm(svf_soft_clip(2.0 * v2 - self.ic2eq));
147
148 let low = v2;
149 let band = v1;
150 let high = v0 - k * v1 - v2;
151 let notch = low + high; outputs.set(10, low); outputs.set(11, band); outputs.set(12, high); outputs.set(13, notch); }
158
159 fn reset(&mut self) {
160 self.ic1eq = 0.0;
161 self.ic2eq = 0.0;
162 }
163
164 fn set_sample_rate(&mut self, sample_rate: f64) {
165 self.sample_rate = sample_rate;
166 }
167
168 fn type_id(&self) -> &'static str {
169 "svf"
170 }
171}
172
173pub struct DiodeLadderFilter {
184 stages: [f64; 4],
186 feedback: f64,
188 sample_rate: f64,
190 spec: PortSpec,
192}
193
194impl DiodeLadderFilter {
195 pub fn new(sample_rate: f64) -> Self {
196 Self {
197 stages: [0.0; 4],
198 feedback: 0.0,
199 sample_rate,
200 spec: PortSpec {
201 inputs: vec![
202 PortDef::new(0, "in", SignalKind::Audio),
203 PortDef::new(1, "cutoff", SignalKind::CvUnipolar)
204 .with_default(0.5)
205 .with_attenuverter(),
206 PortDef::new(2, "res", SignalKind::CvUnipolar)
207 .with_default(0.0)
208 .with_attenuverter(),
209 PortDef::new(3, "fm", SignalKind::CvBipolar).with_attenuverter(),
210 PortDef::new(4, "keytrack", SignalKind::VoltPerOctave),
211 PortDef::new(5, "keytrack_amt", SignalKind::CvUnipolar).with_default(0.0),
212 PortDef::new(6, "drive", SignalKind::CvUnipolar)
213 .with_default(0.0)
214 .with_attenuverter(),
215 ],
216 outputs: vec![
217 PortDef::new(10, "out", SignalKind::Audio),
218 PortDef::new(11, "pole1", SignalKind::Audio), PortDef::new(12, "pole2", SignalKind::Audio), PortDef::new(13, "pole3", SignalKind::Audio), ],
222 },
223 }
224 }
225
226 #[inline]
228 fn diode_sat(x: f64) -> f64 {
229 if x >= 0.0 {
231 Libm::<f64>::tanh(x * 1.2)
232 } else {
233 Libm::<f64>::tanh(x * 0.8)
234 }
235 }
236
237 #[inline]
246 fn run_cascade(u: f64, s: &[f64; 4], big_g: f64) -> ([f64; 4], [f64; 4]) {
247 let mut y = [0.0f64; 4];
248 let mut new_s = [0.0f64; 4];
249 let mut x = Self::diode_sat(u / 5.0) * 5.0;
251 for i in 0..4 {
252 let v = (x - s[i]) * big_g; let yi = v + s[i]; y[i] = yi;
255 new_s[i] = yi + v; x = Self::diode_sat(yi / 5.0) * 5.0;
258 }
259 (y, new_s)
260 }
261}
262
263impl Default for DiodeLadderFilter {
264 fn default() -> Self {
265 Self::new(44100.0)
266 }
267}
268
269impl GraphModule for DiodeLadderFilter {
270 fn port_spec(&self) -> &PortSpec {
271 &self.spec
272 }
273
274 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
275 let input = sanitize_audio(inputs.get_or(0, 0.0));
278 let cutoff_cv = inputs.get_or(1, 0.5) + inputs.get_or(3, 0.0);
279 let res = inputs.get_or(2, 0.0).clamp(0.0, 1.0);
280 let keytrack_voct = inputs.get_or(4, 0.0);
281 let keytrack_amt = inputs.get_or(5, 0.0).clamp(0.0, 1.0);
282 let drive = inputs.get_or(6, 0.0).clamp(0.0, 1.0);
283
284 let base_cutoff_hz = 20.0 * Libm::<f64>::pow(1000.0, cutoff_cv.clamp(0.0, 1.0));
286
287 let keytrack_multiplier = Libm::<f64>::pow(2.0, keytrack_voct * keytrack_amt);
289 let cutoff_hz = (base_cutoff_hz * keytrack_multiplier).clamp(20.0, 20000.0);
290
291 let max_fc = 0.49 * self.sample_rate;
294 let fc = Libm::<f64>::fmin(cutoff_hz, max_fc);
295 let wc = PI * fc / self.sample_rate;
296 let g = Libm::<f64>::tan(wc);
297 let big_g = g / (1.0 + g);
298
299 let k = res * 4.0;
302
303 let drive_gain = 1.0 + drive * 3.0;
305
306 let input_driven = Self::diode_sat(input / 5.0 * drive_gain) * 5.0;
308
309 let mut fb_norm = self.feedback; for _ in 0..2 {
320 let fb = Self::diode_sat(fb_norm * k);
321 let u = input_driven - fb * 5.0;
322 let (y, _) = Self::run_cascade(u, &self.stages, big_g);
323 fb_norm = y[3] / 5.0;
324 }
325
326 let fb = Self::diode_sat(fb_norm * k);
328 let u = input_driven - fb * 5.0;
329 let (y, new_s) = Self::run_cascade(u, &self.stages, big_g);
330
331 self.stages[0] = flush_denorm(new_s[0]);
333 self.stages[1] = flush_denorm(new_s[1]);
334 self.stages[2] = flush_denorm(new_s[2]);
335 self.stages[3] = flush_denorm(new_s[3]);
336 self.feedback = flush_denorm(y[3] / 5.0);
337
338 outputs.set(10, y[3]); outputs.set(11, y[0]); outputs.set(12, y[1]); outputs.set(13, y[2]); }
344
345 fn reset(&mut self) {
346 self.stages = [0.0; 4];
347 self.feedback = 0.0;
348 }
349
350 fn set_sample_rate(&mut self, sample_rate: f64) {
351 self.sample_rate = sample_rate;
352 }
353
354 fn type_id(&self) -> &'static str {
355 "diode_ladder"
356 }
357}
358
359pub struct ParametricEq {
369 low_state: [f64; 2],
371 mid_state: [f64; 2],
372 high_state: [f64; 2],
373 low_coefs: [f64; 5],
377 mid_coefs: [f64; 5],
378 high_coefs: [f64; 5],
379 cached_low: [f64; 2], cached_mid: [f64; 3], cached_high: [f64; 2], recompute_count: u64,
386 sample_rate: f64,
387 spec: PortSpec,
388}
389
390impl ParametricEq {
391 pub fn new(sample_rate: f64) -> Self {
392 Self {
393 low_state: [0.0; 2],
394 mid_state: [0.0; 2],
395 high_state: [0.0; 2],
396 low_coefs: [0.0; 5],
397 mid_coefs: [0.0; 5],
398 high_coefs: [0.0; 5],
399 cached_low: [f64::NAN; 2],
400 cached_mid: [f64::NAN; 3],
401 cached_high: [f64::NAN; 2],
402 recompute_count: 0,
403 sample_rate,
404 spec: PortSpec {
405 inputs: vec![
406 PortDef::new(0, "in", SignalKind::Audio),
407 PortDef::new(1, "low_gain", SignalKind::CvBipolar)
408 .with_default(0.0)
409 .with_attenuverter(),
410 PortDef::new(2, "low_freq", SignalKind::CvUnipolar)
411 .with_default(0.2)
412 .with_attenuverter(),
413 PortDef::new(3, "mid_gain", SignalKind::CvBipolar)
414 .with_default(0.0)
415 .with_attenuverter(),
416 PortDef::new(4, "mid_freq", SignalKind::CvUnipolar)
417 .with_default(0.5)
418 .with_attenuverter(),
419 PortDef::new(5, "mid_q", SignalKind::CvUnipolar)
420 .with_default(0.5)
421 .with_attenuverter(),
422 PortDef::new(6, "high_gain", SignalKind::CvBipolar)
423 .with_default(0.0)
424 .with_attenuverter(),
425 PortDef::new(7, "high_freq", SignalKind::CvUnipolar)
426 .with_default(0.7)
427 .with_attenuverter(),
428 ],
429 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
430 },
431 }
432 }
433
434 fn calc_low_shelf(freq: f64, gain_db: f64, sample_rate: f64) -> [f64; 5] {
437 let a = Libm::<f64>::pow(10.0, gain_db / 40.0);
438 let w0 = TAU * freq / sample_rate;
439 let cos_w0 = Libm::<f64>::cos(w0);
440 let sin_w0 = Libm::<f64>::sin(w0);
441 let alpha = sin_w0 / 2.0 * Libm::<f64>::sqrt(2.0);
442 let sqrt_a = Libm::<f64>::sqrt(a);
443
444 let a0 = (a + 1.0) + (a - 1.0) * cos_w0 + 2.0 * sqrt_a * alpha;
445 let b0 = a * ((a + 1.0) - (a - 1.0) * cos_w0 + 2.0 * sqrt_a * alpha);
446 let b1 = 2.0 * a * ((a - 1.0) - (a + 1.0) * cos_w0);
447 let b2 = a * ((a + 1.0) - (a - 1.0) * cos_w0 - 2.0 * sqrt_a * alpha);
448 let a1 = -2.0 * ((a - 1.0) + (a + 1.0) * cos_w0);
449 let a2 = (a + 1.0) + (a - 1.0) * cos_w0 - 2.0 * sqrt_a * alpha;
450
451 [b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0]
452 }
453
454 fn calc_high_shelf(freq: f64, gain_db: f64, sample_rate: f64) -> [f64; 5] {
456 let a = Libm::<f64>::pow(10.0, gain_db / 40.0);
457 let w0 = TAU * freq / sample_rate;
458 let cos_w0 = Libm::<f64>::cos(w0);
459 let sin_w0 = Libm::<f64>::sin(w0);
460 let alpha = sin_w0 / 2.0 * Libm::<f64>::sqrt(2.0);
461 let sqrt_a = Libm::<f64>::sqrt(a);
462
463 let a0 = (a + 1.0) - (a - 1.0) * cos_w0 + 2.0 * sqrt_a * alpha;
464 let b0 = a * ((a + 1.0) + (a - 1.0) * cos_w0 + 2.0 * sqrt_a * alpha);
465 let b1 = -2.0 * a * ((a - 1.0) + (a + 1.0) * cos_w0);
466 let b2 = a * ((a + 1.0) + (a - 1.0) * cos_w0 - 2.0 * sqrt_a * alpha);
467 let a1 = 2.0 * ((a - 1.0) - (a + 1.0) * cos_w0);
468 let a2 = (a + 1.0) - (a - 1.0) * cos_w0 - 2.0 * sqrt_a * alpha;
469
470 [b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0]
471 }
472
473 fn calc_peaking(freq: f64, gain_db: f64, q: f64, sample_rate: f64) -> [f64; 5] {
475 let a = Libm::<f64>::pow(10.0, gain_db / 40.0);
476 let w0 = TAU * freq / sample_rate;
477 let cos_w0 = Libm::<f64>::cos(w0);
478 let sin_w0 = Libm::<f64>::sin(w0);
479 let alpha = sin_w0 / (2.0 * q);
480
481 let a0 = 1.0 + alpha / a;
482 let b0 = (1.0 + alpha * a) / a0;
483 let b1 = (-2.0 * cos_w0) / a0;
484 let b2 = (1.0 - alpha * a) / a0;
485 let a1 = (-2.0 * cos_w0) / a0;
486 let a2 = (1.0 - alpha / a) / a0;
487
488 [b0, b1, b2, a1, a2]
489 }
490
491 #[inline]
493 fn process_biquad(input: f64, coefs: &[f64; 5], state: &mut [f64; 2]) -> f64 {
494 let output = coefs[0] * input + state[0];
495 state[0] = coefs[1] * input - coefs[3] * output + state[1];
496 state[1] = coefs[2] * input - coefs[4] * output;
497 output
498 }
499}
500
501impl Default for ParametricEq {
502 fn default() -> Self {
503 Self::new(44100.0)
504 }
505}
506
507impl GraphModule for ParametricEq {
508 fn port_spec(&self) -> &PortSpec {
509 &self.spec
510 }
511
512 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
513 let input = sanitize_audio(inputs.get_or(0, 0.0));
517
518 let low_gain_db = (inputs.get_or(1, 0.0) / 5.0) * 12.0;
521 let mid_gain_db = (inputs.get_or(3, 0.0) / 5.0) * 12.0;
522 let high_gain_db = (inputs.get_or(6, 0.0) / 5.0) * 12.0;
523
524 let low_freq_cv = inputs.get_or(2, 0.2).clamp(0.0, 1.0);
526 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);
529 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);
532 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);
536 let mid_q = 0.5 + mid_q_cv * 9.5;
537
538 let nyquist = self.sample_rate * 0.45;
540 let low_freq = low_freq.clamp(20.0, nyquist);
541 let mid_freq = mid_freq.clamp(20.0, nyquist);
542 let high_freq = high_freq.clamp(20.0, nyquist);
543
544 let low_params = [low_freq, low_gain_db];
550 if self.cached_low != low_params {
551 self.low_coefs = Self::calc_low_shelf(low_freq, low_gain_db, self.sample_rate);
552 self.cached_low = low_params;
553 self.recompute_count += 1;
554 }
555 let mid_params = [mid_freq, mid_gain_db, mid_q];
556 if self.cached_mid != mid_params {
557 self.mid_coefs = Self::calc_peaking(mid_freq, mid_gain_db, mid_q, self.sample_rate);
558 self.cached_mid = mid_params;
559 self.recompute_count += 1;
560 }
561 let high_params = [high_freq, high_gain_db];
562 if self.cached_high != high_params {
563 self.high_coefs = Self::calc_high_shelf(high_freq, high_gain_db, self.sample_rate);
564 self.cached_high = high_params;
565 self.recompute_count += 1;
566 }
567
568 let mut signal = input;
570 signal = Self::process_biquad(signal, &self.low_coefs, &mut self.low_state);
571 signal = Self::process_biquad(signal, &self.mid_coefs, &mut self.mid_state);
572 signal = Self::process_biquad(signal, &self.high_coefs, &mut self.high_state);
573
574 outputs.set(10, signal);
575 }
576
577 fn reset(&mut self) {
578 self.low_state = [0.0; 2];
579 self.mid_state = [0.0; 2];
580 self.high_state = [0.0; 2];
581 }
582
583 fn set_sample_rate(&mut self, sample_rate: f64) {
584 self.sample_rate = sample_rate;
585 self.cached_low = [f64::NAN; 2];
588 self.cached_mid = [f64::NAN; 3];
589 self.cached_high = [f64::NAN; 2];
590 self.reset();
591 }
592
593 fn type_id(&self) -> &'static str {
594 "parametric_eq"
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601 use crate::modules::common::{measure_max_output, SAFE_AUDIO_LIMIT};
602
603 #[test]
604 fn test_svf_filter() {
605 let mut svf = Svf::new(44100.0);
606 let mut inputs = PortValues::new();
607 let mut outputs = PortValues::new();
608
609 inputs.set(0, 5.0); inputs.set(1, 0.1); svf.tick(&inputs, &mut outputs);
614
615 assert!(outputs.get(10).is_some());
617 }
618 #[test]
619 fn test_svf_default_reset_sample_rate() {
620 let mut svf = Svf::default();
621 assert!(svf.sample_rate == 44100.0);
622
623 svf.set_sample_rate(48000.0);
624 assert!(svf.sample_rate == 48000.0);
625
626 let mut inputs = PortValues::new();
627 let mut outputs = PortValues::new();
628 inputs.set(0, 1.0);
629 for _ in 0..100 {
630 svf.tick(&inputs, &mut outputs);
631 }
632
633 svf.reset();
634 assert!(svf.ic1eq == 0.0);
637
638 assert_eq!(svf.type_id(), "svf");
639 }
640 #[test]
641 fn test_diode_ladder_filter_coverage() {
642 use crate::{Crosstalk, DiodeLadderFilter, GroundLoop};
643
644 let mut dlf = DiodeLadderFilter::default();
646 assert!(dlf.sample_rate == 44100.0);
647
648 dlf.set_sample_rate(48000.0);
649 assert!(dlf.sample_rate == 48000.0);
650
651 let mut inputs = PortValues::new();
652 let mut outputs = PortValues::new();
653 inputs.set(0, 1.0);
654 for _ in 0..100 {
655 dlf.tick(&inputs, &mut outputs);
656 }
657
658 dlf.reset();
659 assert!(dlf.stages[0] == 0.0);
660
661 assert_eq!(dlf.type_id(), "diode_ladder");
662
663 let mut crosstalk = Crosstalk::default();
665 crosstalk.set_sample_rate(48000.0);
666 inputs.set(0, 1.0);
667 inputs.set(1, 2.0);
668 crosstalk.tick(&inputs, &mut outputs);
669 crosstalk.reset();
670 assert_eq!(crosstalk.type_id(), "crosstalk");
671
672 let mut gl = GroundLoop::default();
674 gl.set_sample_rate(48000.0);
675 gl.tick(&inputs, &mut outputs);
676 gl.reset();
677 assert_eq!(gl.type_id(), "ground_loop");
678 }
679 #[test]
680 fn test_parametric_eq_passthrough() {
681 let mut eq = ParametricEq::new(44100.0);
682 let mut inputs = PortValues::new();
683 let mut outputs = PortValues::new();
684
685 inputs.set(0, 1.0); inputs.set(1, 0.0); inputs.set(3, 0.0); inputs.set(6, 0.0); for _ in 0..1000 {
693 eq.tick(&inputs, &mut outputs);
694 }
695
696 let out = outputs.get(10).unwrap();
697 assert!((out - 1.0).abs() < 0.01);
699 }
700
701 #[test]
702 fn test_parametric_eq_nan_recovery() {
703 let mut eq = ParametricEq::new(44100.0);
706 let mut inputs = PortValues::new();
707 let mut outputs = PortValues::new();
708 inputs.set(1, 0.0);
709 inputs.set(3, 0.0);
710 inputs.set(6, 0.0);
711
712 for &bad in &[f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
713 inputs.set(0, bad);
714 eq.tick(&inputs, &mut outputs);
715 }
716
717 inputs.set(0, 0.5);
719 let mut last = 0.0;
720 for _ in 0..2000 {
721 eq.tick(&inputs, &mut outputs);
722 last = outputs.get(10).unwrap();
723 }
724 assert!(
725 last.is_finite(),
726 "ParametricEq output stayed non-finite after a NaN input: {last}"
727 );
728 }
729
730 #[test]
731 fn test_parametric_eq_low_boost() {
732 let mut eq = ParametricEq::new(44100.0);
733 let mut inputs = PortValues::new();
734 let mut outputs = PortValues::new();
735
736 inputs.set(0, 1.0);
738 inputs.set(1, 5.0); inputs.set(2, 0.0); for _ in 0..1000 {
742 eq.tick(&inputs, &mut outputs);
743 }
744
745 let out = outputs.get(10).unwrap();
746 assert!(out > 1.0);
748 assert!(out.is_finite());
749 }
750 #[test]
751 fn test_parametric_eq_mid_cut() {
752 let mut eq = ParametricEq::new(44100.0);
753 let mut inputs = PortValues::new();
754 let mut outputs = PortValues::new();
755
756 inputs.set(0, 1.0);
758 inputs.set(3, -5.0); inputs.set(5, 1.0); for _ in 0..1000 {
762 eq.tick(&inputs, &mut outputs);
763 }
764
765 let out = outputs.get(10).unwrap();
766 assert!(out.is_finite());
767 }
768 #[test]
769 fn test_parametric_eq_high_boost() {
770 let mut eq = ParametricEq::new(44100.0);
771 let mut inputs = PortValues::new();
772 let mut outputs = PortValues::new();
773
774 inputs.set(0, 1.0);
775 inputs.set(6, 5.0); for _ in 0..1000 {
778 eq.tick(&inputs, &mut outputs);
779 }
780
781 let out = outputs.get(10).unwrap();
782 assert!(out.is_finite());
783 }
784 #[test]
785 fn test_parametric_eq_default_reset_sample_rate() {
786 let mut eq = ParametricEq::default();
787 assert!(eq.sample_rate == 44100.0);
788
789 let mut inputs = PortValues::new();
791 let mut outputs = PortValues::new();
792 inputs.set(0, 1.0);
793 inputs.set(1, 2.5); for _ in 0..100 {
795 eq.tick(&inputs, &mut outputs);
796 }
797
798 assert!(eq.low_state[0] != 0.0 || eq.low_state[1] != 0.0);
800
801 eq.reset();
803 assert_eq!(eq.low_state, [0.0; 2]);
804 assert_eq!(eq.mid_state, [0.0; 2]);
805 assert_eq!(eq.high_state, [0.0; 2]);
806
807 eq.set_sample_rate(48000.0);
809 assert_eq!(eq.sample_rate, 48000.0);
810
811 assert_eq!(eq.type_id(), "parametric_eq");
812 assert_eq!(eq.port_spec().inputs.len(), 8);
813 assert_eq!(eq.port_spec().outputs.len(), 1);
814 }
815 #[test]
816 fn test_parametric_eq_frequency_ranges() {
817 let mut eq = ParametricEq::new(44100.0);
818 let mut inputs = PortValues::new();
819 let mut outputs = PortValues::new();
820
821 inputs.set(0, 1.0);
823 inputs.set(2, 0.0); inputs.set(4, 0.0); inputs.set(7, 0.0); for _ in 0..100 {
828 eq.tick(&inputs, &mut outputs);
829 }
830 assert!(outputs.get(10).unwrap().is_finite());
831
832 eq.reset();
833 inputs.set(2, 1.0); inputs.set(4, 1.0); inputs.set(7, 1.0); for _ in 0..100 {
838 eq.tick(&inputs, &mut outputs);
839 }
840 assert!(outputs.get(10).unwrap().is_finite());
841 }
842 #[test]
843 fn test_parametric_eq_stability() {
844 let mut eq = ParametricEq::new(44100.0);
845 let mut inputs = PortValues::new();
846 let mut outputs = PortValues::new();
847
848 inputs.set(0, 5.0); inputs.set(1, 5.0); inputs.set(3, 5.0);
852 inputs.set(6, 5.0);
853 inputs.set(5, 1.0); eq.tick(&inputs, &mut outputs);
856
857 inputs.set(0, 0.0);
859 for _ in 0..10000 {
860 eq.tick(&inputs, &mut outputs);
861 }
862
863 let out = outputs.get(10).unwrap();
865 assert!(out.is_finite());
866 assert!(out.abs() < 0.01);
867 }
868 #[test]
869 fn test_svf_high_resonance_bounded() {
870 let test_resonances = [0.8, 0.85, 0.9, 0.92, 0.94, 0.96, 0.98, 1.0];
873
874 for &res in &test_resonances {
875 let mut svf = Svf::new(44100.0);
876 let mut inputs = PortValues::new();
877 let mut outputs = PortValues::new();
878
879 inputs.set(0, 5.0); inputs.set(1, 0.5); inputs.set(2, res); let max = measure_max_output(10000, || {
884 svf.tick(&inputs, &mut outputs);
885 let lp = outputs.get(10).unwrap_or(0.0).abs();
887 let bp = outputs.get(11).unwrap_or(0.0).abs();
888 let hp = outputs.get(12).unwrap_or(0.0).abs();
889 let notch = outputs.get(13).unwrap_or(0.0).abs();
890 lp.max(bp).max(hp).max(notch)
891 });
892
893 assert!(
894 max <= SAFE_AUDIO_LIMIT,
895 "SVF output {} exceeds safe limit {} at resonance {}",
896 max,
897 SAFE_AUDIO_LIMIT,
898 res
899 );
900 }
901 }
902 #[test]
903 fn test_svf_low_cutoff_transient_bounded() {
904 let mut svf = Svf::new(44100.0);
906 let mut inputs = PortValues::new();
907 let mut outputs = PortValues::new();
908
909 inputs.set(1, 0.0); inputs.set(2, 0.9); inputs.set(0, 0.0);
915 for _ in 0..100 {
916 svf.tick(&inputs, &mut outputs);
917 }
918
919 inputs.set(0, 5.0); let max = measure_max_output(5000, || {
921 svf.tick(&inputs, &mut outputs);
922 outputs.get(10).unwrap_or(0.0).abs()
923 });
924
925 assert!(
926 max <= SAFE_AUDIO_LIMIT,
927 "SVF transient response {} exceeds safe limit {} at low cutoff",
928 max,
929 SAFE_AUDIO_LIMIT
930 );
931 }
932 #[test]
933 fn test_svf_self_oscillation_bounded() {
934 let mut svf = Svf::new(44100.0);
936 let mut inputs = PortValues::new();
937 let mut outputs = PortValues::new();
938
939 inputs.set(0, 0.0); inputs.set(1, 0.5); inputs.set(2, 1.0); inputs.set(0, 1.0);
945 svf.tick(&inputs, &mut outputs);
946 inputs.set(0, 0.0);
947
948 let max = measure_max_output(20000, || {
950 svf.tick(&inputs, &mut outputs);
951 outputs.get(10).unwrap_or(0.0).abs()
952 });
953
954 assert!(
955 max <= SAFE_AUDIO_LIMIT,
956 "SVF self-oscillation {} exceeds safe limit {}",
957 max,
958 SAFE_AUDIO_LIMIT
959 );
960 }
961 #[test]
962 fn test_svf_extreme_input_bounded() {
963 let mut svf = Svf::new(44100.0);
965 let mut inputs = PortValues::new();
966 let mut outputs = PortValues::new();
967
968 inputs.set(0, 20.0); inputs.set(1, 0.5);
970 inputs.set(2, 0.9);
971
972 let max = measure_max_output(1000, || {
973 svf.tick(&inputs, &mut outputs);
974 outputs.get(10).unwrap_or(0.0).abs()
975 });
976
977 assert!(
978 max <= SAFE_AUDIO_LIMIT * 2.0, "SVF with extreme input {} exceeds limit {}",
980 max,
981 SAFE_AUDIO_LIMIT * 2.0
982 );
983 }
984 #[test]
985 fn test_diode_ladder_high_resonance_bounded() {
986 let mut filter = DiodeLadderFilter::new(44100.0);
988 let mut inputs = PortValues::new();
989 let mut outputs = PortValues::new();
990
991 inputs.set(0, 5.0); inputs.set(1, 0.5); inputs.set(2, 1.0); let max = measure_max_output(10000, || {
996 filter.tick(&inputs, &mut outputs);
997 outputs.get(10).unwrap_or(0.0).abs()
998 });
999
1000 assert!(
1001 max <= SAFE_AUDIO_LIMIT,
1002 "Diode ladder output {} exceeds safe limit {}",
1003 max,
1004 SAFE_AUDIO_LIMIT
1005 );
1006 }
1007
1008 fn cutoff_cv_for(freq_hz: f64) -> f64 {
1012 (freq_hz / 20.0).ln() / 1000.0_f64.ln()
1013 }
1014
1015 fn rms(samples: &[f64]) -> f64 {
1016 let sum_sq: f64 = samples.iter().map(|x| x * x).sum();
1017 (sum_sq / samples.len() as f64).sqrt()
1018 }
1019
1020 #[test]
1026 fn test_svf_max_resonance_finite_200k() {
1027 let mut svf = Svf::new(44100.0);
1028 let mut inputs = PortValues::new();
1029 let mut outputs = PortValues::new();
1030
1031 inputs.set(0, 1.0); inputs.set(1, 1.0); inputs.set(2, 1.0); let mut max_abs = 0.0f64;
1036 for n in 0..200_000 {
1037 svf.tick(&inputs, &mut outputs);
1038 for &id in &[10u32, 11, 12, 13] {
1039 let v = outputs.get(id).unwrap();
1040 assert!(
1041 v.is_finite(),
1042 "SVF output {id} became non-finite at sample {n}"
1043 );
1044 max_abs = max_abs.max(v.abs());
1045 }
1046 }
1047 assert!(
1048 max_abs < 50.0,
1049 "SVF max resonance output unbounded: {max_abs}"
1050 );
1051 }
1052
1053 #[test]
1059 fn test_svf_cutoff_accuracy() {
1060 let sample_rate = 44100.0;
1061 let res = (2.0 - core::f64::consts::SQRT_2) / 2.0;
1063
1064 for &target_fc in &[1000.0_f64, 10_000.0_f64] {
1065 let cv = cutoff_cv_for(target_fc);
1066
1067 let measure = |freq: f64| -> f64 {
1068 let mut svf = Svf::new(sample_rate);
1069 let mut inputs = PortValues::new();
1070 let mut outputs = PortValues::new();
1071 inputs.set(1, cv);
1072 inputs.set(2, res);
1073 let mut out = alloc::vec::Vec::new();
1074 let dt = freq / sample_rate;
1075 let mut phase = 0.0f64;
1076 for n in 0..40_000 {
1077 let s = Libm::<f64>::sin(TAU * phase);
1078 phase += dt;
1079 if phase >= 1.0 {
1080 phase -= 1.0;
1081 }
1082 inputs.set(0, s);
1083 svf.tick(&inputs, &mut outputs);
1084 if n >= 20_000 {
1085 out.push(outputs.get(10).unwrap());
1086 }
1087 }
1088 rms(&out)
1089 };
1090
1091 let passband = measure(target_fc / 8.0);
1092 let at_fc = measure(target_fc);
1093 let ratio = at_fc / passband;
1094 assert!(
1095 (ratio - core::f64::consts::FRAC_1_SQRT_2).abs() < 0.10,
1096 "SVF -3dB point off at fc={target_fc}: ratio {ratio} (expected ~0.707)"
1097 );
1098 }
1099 }
1100
1101 #[test]
1106 fn test_svf_self_oscillation_sustained() {
1107 let sample_rate = 44100.0;
1108 let mut svf = Svf::new(sample_rate);
1109 let mut inputs = PortValues::new();
1110 let mut outputs = PortValues::new();
1111
1112 inputs.set(1, cutoff_cv_for(2000.0)); inputs.set(2, 1.0); inputs.set(0, 5.0);
1117 svf.tick(&inputs, &mut outputs);
1118 inputs.set(0, 0.0);
1119
1120 let mut window = alloc::vec::Vec::new();
1121 for n in 0..66_150 {
1122 svf.tick(&inputs, &mut outputs);
1124 let v = outputs.get(11).unwrap(); assert!(v.is_finite());
1126 if n >= 44_100 {
1127 window.push(v); }
1129 }
1130 let r = rms(&window);
1131 assert!(
1132 (0.01..20.0).contains(&r),
1133 "SVF self-oscillation not sustained/bounded after 1s: rms {r}"
1134 );
1135 }
1136
1137 #[test]
1143 fn test_diode_ladder_resonance_peak_frequency() {
1144 let sample_rate = 44100.0;
1145 let target_fc = 2000.0;
1146 let cv = cutoff_cv_for(target_fc);
1147
1148 let gain_at = |freq: f64| -> f64 {
1150 let mut filter = DiodeLadderFilter::new(sample_rate);
1151 let mut inputs = PortValues::new();
1152 let mut outputs = PortValues::new();
1153 inputs.set(1, cv);
1154 inputs.set(2, 1.0); let dt = freq / sample_rate;
1156 let mut phase = 0.0f64;
1157 let mut buf = alloc::vec::Vec::new();
1158 for n in 0..40_000 {
1159 let s = 0.1 * Libm::<f64>::sin(TAU * phase);
1160 phase += dt;
1161 if phase >= 1.0 {
1162 phase -= 1.0;
1163 }
1164 inputs.set(0, s);
1165 filter.tick(&inputs, &mut outputs);
1166 if n >= 20_000 {
1167 buf.push(outputs.get(10).unwrap());
1168 }
1169 }
1170 rms(&buf)
1171 };
1172
1173 let spacing = 100.0;
1177 let sweep: alloc::vec::Vec<f64> = (0..13).map(|i| 1400.0 + spacing * i as f64).collect();
1178 let gains: alloc::vec::Vec<f64> = sweep.iter().map(|&f| gain_at(f)).collect();
1179 let mut peak = 1;
1180 for i in 1..gains.len() - 1 {
1181 if gains[i] > gains[peak] {
1182 peak = i;
1183 }
1184 }
1185 assert!(
1186 peak > 0 && peak < gains.len() - 1,
1187 "peak fell on sweep edge"
1188 );
1189 let (a, b, c) = (gains[peak - 1], gains[peak], gains[peak + 1]);
1190 let denom = a - 2.0 * b + c;
1191 let delta = if denom != 0.0 {
1192 0.5 * (a - c) / denom
1193 } else {
1194 0.0
1195 };
1196 let peak_f = sweep[peak] + delta * spacing;
1197 let err = (peak_f - target_fc).abs() / target_fc;
1198 assert!(
1199 err < 0.05,
1200 "Diode ladder resonant peak at {peak_f:.0} Hz, off from {target_fc} Hz by {:.1}%",
1201 err * 100.0
1202 );
1203 }
1204
1205 #[test]
1208 fn test_diode_ladder_max_resonance_stable_100k() {
1209 for &cv in &[0.1_f64, 0.5, 0.9] {
1210 let mut filter = DiodeLadderFilter::new(44100.0);
1211 let mut inputs = PortValues::new();
1212 let mut outputs = PortValues::new();
1213 inputs.set(0, 5.0);
1214 inputs.set(1, cv);
1215 inputs.set(2, 1.0);
1216
1217 let mut max_abs = 0.0f64;
1218 for n in 0..100_000 {
1219 filter.tick(&inputs, &mut outputs);
1220 for &id in &[10u32, 11, 12, 13] {
1221 let v = outputs.get(id).unwrap();
1222 assert!(v.is_finite(), "diode out {id} non-finite at {n} (cv={cv})");
1223 max_abs = max_abs.max(v.abs());
1224 }
1225 }
1226 assert!(
1227 max_abs <= SAFE_AUDIO_LIMIT,
1228 "diode unbounded {max_abs} at cv={cv}"
1229 );
1230 }
1231 }
1232
1233 #[test]
1237 fn test_parametric_eq_caching_bit_identical() {
1238 let sample_rate = 44100.0;
1239 let mut eq = ParametricEq::new(sample_rate);
1240 let mut inputs = PortValues::new();
1241 let mut outputs = PortValues::new();
1242
1243 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;
1254 let mid_gain_db = (-2.0 / 5.0) * 12.0;
1255 let high_gain_db = (4.0 / 5.0) * 12.0;
1256 let low_freq = (50.0 * Libm::<f64>::pow(10.0, 0.4)).clamp(20.0, sample_rate * 0.45);
1257 let mid_freq = (200.0 * Libm::<f64>::pow(40.0, 0.6)).clamp(20.0, sample_rate * 0.45);
1258 let high_freq: f64 = (2000.0 + 0.5 * 10000.0_f64).clamp(20.0, sample_rate * 0.45);
1259 let mid_q = 0.5 + 0.7 * 9.5;
1260 let low_c = ParametricEq::calc_low_shelf(low_freq, low_gain_db, sample_rate);
1261 let mid_c = ParametricEq::calc_peaking(mid_freq, mid_gain_db, mid_q, sample_rate);
1262 let high_c = ParametricEq::calc_high_shelf(high_freq, high_gain_db, sample_rate);
1263 let mut ref_low = [0.0; 2];
1264 let mut ref_mid = [0.0; 2];
1265 let mut ref_high = [0.0; 2];
1266
1267 let mut phase = 0.0f64;
1268 for _ in 0..2000 {
1269 let s = Libm::<f64>::sin(TAU * phase);
1270 phase += 500.0 / sample_rate;
1271 if phase >= 1.0 {
1272 phase -= 1.0;
1273 }
1274 inputs.set(0, s);
1275 eq.tick(&inputs, &mut outputs);
1276 let got = outputs.get(10).unwrap();
1277
1278 let mut r = ParametricEq::process_biquad(s, &low_c, &mut ref_low);
1279 r = ParametricEq::process_biquad(r, &mid_c, &mut ref_mid);
1280 r = ParametricEq::process_biquad(r, &high_c, &mut ref_high);
1281
1282 assert_eq!(
1283 got.to_bits(),
1284 r.to_bits(),
1285 "cached EQ output differs from recompute"
1286 );
1287 }
1288 }
1289
1290 #[test]
1293 fn test_parametric_eq_recompute_count() {
1294 let mut eq = ParametricEq::new(44100.0);
1295 let mut inputs = PortValues::new();
1296 let mut outputs = PortValues::new();
1297 inputs.set(0, 1.0);
1298 inputs.set(1, 2.0);
1299 inputs.set(3, 1.0);
1300 inputs.set(6, -1.0);
1301
1302 for _ in 0..100 {
1303 eq.tick(&inputs, &mut outputs);
1304 }
1305 assert_eq!(
1307 eq.recompute_count, 3,
1308 "static params should not recompute per sample"
1309 );
1310
1311 inputs.set(3, 2.0);
1313 eq.tick(&inputs, &mut outputs);
1314 assert_eq!(
1315 eq.recompute_count, 4,
1316 "changing one band should recompute only that band"
1317 );
1318
1319 for _ in 0..50 {
1321 eq.tick(&inputs, &mut outputs);
1322 }
1323 assert_eq!(
1324 eq.recompute_count, 4,
1325 "returning to static must not recompute"
1326 );
1327 }
1328
1329 #[test]
1332 fn test_parametric_eq_mid_band_response() {
1333 let sample_rate = 44100.0;
1334 let mid_freq = 200.0 * Libm::<f64>::pow(40.0, 0.5);
1337 let out_of_band = mid_freq / 8.0;
1338
1339 let measure = |tone_hz: f64, mid_gain_cv: f64| -> f64 {
1341 let mut eq = ParametricEq::new(sample_rate);
1342 let mut inputs = PortValues::new();
1343 let mut outputs = PortValues::new();
1344 inputs.set(3, mid_gain_cv); inputs.set(5, 1.0); let dt = tone_hz / sample_rate;
1347 let mut phase = 0.0f64;
1348 let mut out = alloc::vec::Vec::new();
1349 for n in 0..40_000 {
1350 let s = Libm::<f64>::sin(TAU * phase);
1351 phase += dt;
1352 if phase >= 1.0 {
1353 phase -= 1.0;
1354 }
1355 inputs.set(0, s);
1356 eq.tick(&inputs, &mut outputs);
1357 if n >= 20_000 {
1358 out.push(outputs.get(10).unwrap());
1359 }
1360 }
1361 rms(&out)
1362 };
1363
1364 let boost_in = measure(mid_freq, 5.0);
1367 let boost_out = measure(out_of_band, 5.0);
1368 let boost_db = 20.0 * Libm::<f64>::log10(boost_in / boost_out);
1369 assert!(
1370 (9.0..=13.0).contains(&boost_db),
1371 "mid +12dB boost: expected ~12dB in-band, got {boost_db:.2}dB"
1372 );
1373
1374 let cut_in = measure(mid_freq, -5.0);
1377 let cut_out = measure(out_of_band, -5.0);
1378 let cut_db = 20.0 * Libm::<f64>::log10(cut_in / cut_out);
1379 assert!(
1380 (-13.0..=-9.0).contains(&cut_db),
1381 "mid -12dB cut: expected ~-12dB in-band, got {cut_db:.2}dB"
1382 );
1383 }
1384}