1use super::common::{
4 db_to_gain, env_coef, flush_denorm, gain_to_db, sanitize_audio, GATE_HIGH_V, GATE_THRESHOLD_V,
5};
6use crate::port::{
7 GraphModule, ModulatedParam, ParamRange, PortDef, PortSpec, PortValues, SignalKind,
8};
9use alloc::vec;
10use libm::Libm;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
14enum AdsrStage {
15 Idle,
16 Attack,
17 Decay,
18 Sustain,
19 Release,
20}
21
22pub struct Adsr {
51 stage: AdsrStage,
52 level: f64,
53 sample_rate: f64,
54 prev_gate: f64,
55 prev_retrig: f64,
56 release_start_level: f64,
60 spec: PortSpec,
61}
62
63impl Adsr {
64 pub fn new(sample_rate: f64) -> Self {
65 Self {
66 stage: AdsrStage::Idle,
67 level: 0.0,
68 sample_rate,
69 prev_gate: 0.0,
70 prev_retrig: 0.0,
71 release_start_level: 0.0,
72 spec: PortSpec {
73 inputs: vec![
74 PortDef::new(0, "gate", SignalKind::Gate),
75 PortDef::new(1, "retrig", SignalKind::Trigger),
76 PortDef::new(2, "attack", SignalKind::CvUnipolar)
77 .with_default(0.1)
78 .with_attenuverter(),
79 PortDef::new(3, "decay", SignalKind::CvUnipolar)
80 .with_default(0.3)
81 .with_attenuverter(),
82 PortDef::new(4, "sustain", SignalKind::CvUnipolar)
83 .with_default(0.7)
84 .with_attenuverter(),
85 PortDef::new(5, "release", SignalKind::CvUnipolar)
86 .with_default(0.4)
87 .with_attenuverter(),
88 PortDef::new(6, "shape", SignalKind::Gate).with_default(0.0),
92 ],
93 outputs: vec![
94 PortDef::new(10, "env", SignalKind::CvUnipolar),
95 PortDef::new(11, "inv", SignalKind::CvUnipolar),
96 PortDef::new(12, "eoc", SignalKind::Trigger),
97 ],
98 },
99 }
100 }
101
102 fn cv_to_time(&self, cv: f64) -> f64 {
103 0.001 * Libm::<f64>::pow(10000.0, cv.clamp(0.0, 1.0))
105 }
106}
107
108impl Default for Adsr {
109 fn default() -> Self {
110 Self::new(44100.0)
111 }
112}
113
114impl GraphModule for Adsr {
115 fn port_spec(&self) -> &PortSpec {
116 &self.spec
117 }
118
119 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
120 let gate = inputs.get_or(0, 0.0);
121 let retrig = inputs.get_or(1, 0.0);
122 let attack_time = self.cv_to_time(inputs.get_or(2, 0.1));
123 let decay_time = self.cv_to_time(inputs.get_or(3, 0.3));
124 let sustain_level = inputs.get_or(4, 0.7).clamp(0.0, 1.0);
125 let release_time = self.cv_to_time(inputs.get_or(5, 0.4));
126 let exp_mode = inputs.get_or(6, 0.0) > GATE_THRESHOLD_V;
127
128 let gate_high = gate > GATE_THRESHOLD_V;
129 let gate_rising = gate_high && self.prev_gate <= GATE_THRESHOLD_V;
130 let gate_falling = !gate_high && self.prev_gate > GATE_THRESHOLD_V;
131 let retrig_rising = retrig > GATE_THRESHOLD_V && self.prev_retrig <= GATE_THRESHOLD_V;
132
133 if gate_rising || (retrig_rising && gate_high) {
136 self.stage = AdsrStage::Attack;
137 } else if gate_falling && self.stage != AdsrStage::Idle {
138 self.release_start_level = self.level;
141 self.stage = AdsrStage::Release;
142 }
143
144 let attack_rate = 1.0 / (attack_time * self.sample_rate);
147 let decay_rate = (1.0 - sustain_level) / (decay_time * self.sample_rate);
148 let release_rate = self.release_start_level / (release_time * self.sample_rate);
149
150 let attack_coef = env_coef(attack_time, self.sample_rate);
152 let decay_coef = env_coef(decay_time, self.sample_rate);
153 let release_coef = env_coef(release_time, self.sample_rate);
154
155 const EXP_DONE: f64 = 1e-3;
157
158 let mut eoc = 0.0;
160 match self.stage {
161 AdsrStage::Idle => {
162 self.level = 0.0;
163 }
164 AdsrStage::Attack => {
165 if exp_mode {
166 self.level += (1.0 - self.level) * (1.0 - attack_coef);
167 if self.level >= 1.0 - EXP_DONE {
168 self.level = 1.0;
169 self.stage = AdsrStage::Decay;
170 }
171 } else {
172 self.level += attack_rate;
173 if self.level >= 1.0 {
174 self.level = 1.0;
175 self.stage = AdsrStage::Decay;
176 }
177 }
178 }
179 AdsrStage::Decay => {
180 if exp_mode {
181 self.level += (sustain_level - self.level) * (1.0 - decay_coef);
182 if self.level - sustain_level <= EXP_DONE {
183 self.level = sustain_level;
184 self.stage = AdsrStage::Sustain;
185 }
186 } else {
187 self.level -= decay_rate;
188 if self.level <= sustain_level {
189 self.level = sustain_level;
190 self.stage = AdsrStage::Sustain;
191 }
192 }
193 }
194 AdsrStage::Sustain => {
195 self.level = sustain_level;
196 }
197 AdsrStage::Release => {
198 if exp_mode {
199 self.level += (0.0 - self.level) * (1.0 - release_coef);
200 if self.level <= EXP_DONE {
201 self.level = 0.0;
202 self.stage = AdsrStage::Idle;
203 eoc = GATE_HIGH_V; }
205 } else {
206 self.level -= release_rate;
207 if self.level <= 0.0 {
208 self.level = 0.0;
209 self.stage = AdsrStage::Idle;
210 eoc = GATE_HIGH_V; }
212 }
213 }
214 }
215
216 self.prev_gate = gate;
217 self.prev_retrig = retrig;
218
219 outputs.set(10, self.level * 10.0); outputs.set(11, (1.0 - self.level) * 10.0); outputs.set(12, eoc);
223 }
224
225 fn reset(&mut self) {
226 self.stage = AdsrStage::Idle;
227 self.level = 0.0;
228 self.prev_gate = 0.0;
229 self.prev_retrig = 0.0;
230 self.release_start_level = 0.0;
231 }
232
233 fn set_sample_rate(&mut self, sample_rate: f64) {
234 self.sample_rate = sample_rate;
235 }
236
237 fn type_id(&self) -> &'static str {
238 "adsr"
239 }
240}
241
242pub struct Vca {
266 spec: PortSpec,
267}
268
269impl Vca {
270 pub fn new() -> Self {
271 Self {
272 spec: PortSpec {
273 inputs: vec![
274 PortDef::new(0, "in", SignalKind::Audio),
275 PortDef::new(1, "cv", SignalKind::CvUnipolar)
276 .with_default(10.0)
277 .with_attenuverter(),
278 PortDef::new(2, "response", SignalKind::Gate).with_default(0.0),
280 PortDef::new(3, "gain", SignalKind::CvUnipolar).with_default(1.0),
282 ],
283 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
284 },
285 }
286 }
287}
288
289impl Default for Vca {
290 fn default() -> Self {
291 Self::new()
292 }
293}
294
295impl GraphModule for Vca {
296 fn port_spec(&self) -> &PortSpec {
297 &self.spec
298 }
299
300 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
301 let input = inputs.get_or(0, 0.0);
302 let cv = inputs.get_or(1, 10.0).clamp(0.0, 10.0) / 10.0;
303 let exp_response = inputs.get_or(2, 0.0) > GATE_THRESHOLD_V;
304 let gain_scale = inputs.get_or(3, 1.0).clamp(0.0, 2.0);
305
306 let base_gain = if exp_response { cv * cv } else { cv };
310
311 outputs.set(10, input * base_gain * gain_scale);
312 }
313
314 fn reset(&mut self) {}
315
316 fn set_sample_rate(&mut self, _: f64) {}
317
318 fn type_id(&self) -> &'static str {
319 "vca"
320 }
321}
322
323pub struct Limiter {
328 sample_rate: f64,
329 envelope: f64,
330 spec: PortSpec,
331}
332
333impl Limiter {
334 pub fn new(sample_rate: f64) -> Self {
335 Self {
336 sample_rate,
337 envelope: 0.0,
338 spec: PortSpec {
339 inputs: vec![
340 PortDef::new(0, "in", SignalKind::Audio),
341 PortDef::new(1, "threshold", SignalKind::CvUnipolar)
342 .with_default(0.8)
343 .with_attenuverter(),
344 PortDef::new(2, "release", SignalKind::CvUnipolar)
345 .with_default(0.3)
346 .with_attenuverter(),
347 PortDef::new(3, "soft", SignalKind::Gate).with_default(5.0),
348 PortDef::new(4, "sidechain", SignalKind::Audio),
352 ],
353 outputs: vec![
354 PortDef::new(10, "out", SignalKind::Audio),
355 PortDef::new(11, "gr", SignalKind::CvUnipolar),
356 ],
357 },
358 }
359 }
360}
361
362impl Default for Limiter {
363 fn default() -> Self {
364 Self::new(44100.0)
365 }
366}
367
368impl GraphModule for Limiter {
369 fn port_spec(&self) -> &PortSpec {
370 &self.spec
371 }
372
373 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
374 let input = sanitize_audio(inputs.get_or(0, 0.0));
377 let threshold = inputs.get_or(1, 0.8).clamp(0.01, 1.0) * 5.0;
378 let release_cv = inputs.get_or(2, 0.3).clamp(0.0, 1.0);
379 let soft_mode = inputs.get_or(3, 5.0) > GATE_THRESHOLD_V;
380 let sidechain = sanitize_audio(inputs.get_or(4, input));
382
383 let release_ms = 10.0 + release_cv * 990.0;
384 let release_coef = env_coef(release_ms / 1000.0, self.sample_rate);
385
386 let abs_input = Libm::<f64>::fabs(sidechain);
387
388 if abs_input > self.envelope {
389 self.envelope = abs_input;
390 } else {
391 self.envelope = release_coef * self.envelope + (1.0 - release_coef) * abs_input;
392 }
393 self.envelope = flush_denorm(self.envelope);
396
397 let gain = if soft_mode {
398 let knee_start = 0.5 * threshold;
408 if self.envelope > knee_start {
409 let span = threshold - knee_start; let target =
411 knee_start + span * Libm::<f64>::tanh((self.envelope - knee_start) / span);
412 target / self.envelope
413 } else {
414 1.0
415 }
416 } else if self.envelope > threshold {
417 threshold / self.envelope
418 } else {
419 1.0
420 };
421
422 let out = (input * gain).clamp(-threshold, threshold);
425 outputs.set(10, out);
426 outputs.set(11, (1.0 - gain) * 10.0);
427 }
428
429 fn reset(&mut self) {
430 self.envelope = 0.0;
431 }
432
433 fn set_sample_rate(&mut self, sample_rate: f64) {
434 self.sample_rate = sample_rate;
435 }
436
437 fn type_id(&self) -> &'static str {
438 "limiter"
439 }
440}
441
442pub struct NoiseGate {
458 sample_rate: f64,
459 envelope: f64,
460 gate_state: f64,
461 gate_open: bool,
463 hold_counter: u32,
466 spec: PortSpec,
467}
468
469impl NoiseGate {
470 const FADE_MS: f64 = 5.0;
472 const HOLD_MS: f64 = 10.0;
475
476 pub fn new(sample_rate: f64) -> Self {
477 Self {
478 sample_rate,
479 envelope: 0.0,
480 gate_state: 0.0,
481 gate_open: false,
482 hold_counter: 0,
483 spec: PortSpec {
484 inputs: vec![
485 PortDef::new(0, "in", SignalKind::Audio),
486 PortDef::new(1, "threshold", SignalKind::CvUnipolar)
487 .with_default(0.1)
488 .with_attenuverter(),
489 PortDef::new(2, "attack", SignalKind::CvUnipolar)
490 .with_default(0.1)
491 .with_attenuverter(),
492 PortDef::new(3, "release", SignalKind::CvUnipolar)
493 .with_default(0.3)
494 .with_attenuverter(),
495 PortDef::new(4, "range", SignalKind::CvUnipolar)
496 .with_default(1.0)
497 .with_attenuverter(),
498 PortDef::new(5, "sidechain", SignalKind::Audio),
502 ],
503 outputs: vec![
504 PortDef::new(10, "out", SignalKind::Audio),
505 PortDef::new(11, "gate", SignalKind::Gate),
506 ],
507 },
508 }
509 }
510}
511
512impl Default for NoiseGate {
513 fn default() -> Self {
514 Self::new(44100.0)
515 }
516}
517
518impl GraphModule for NoiseGate {
519 fn port_spec(&self) -> &PortSpec {
520 &self.spec
521 }
522
523 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
524 let input = sanitize_audio(inputs.get_or(0, 0.0));
527 let threshold = inputs.get_or(1, 0.1).clamp(0.0, 1.0) * 5.0;
528 let attack_cv = inputs.get_or(2, 0.1).clamp(0.0, 1.0);
529 let release_cv = inputs.get_or(3, 0.3).clamp(0.0, 1.0);
530 let range = inputs.get_or(4, 1.0).clamp(0.0, 1.0);
531 let sidechain = sanitize_audio(inputs.get_or(5, input));
533
534 let attack_ms = 0.1 + attack_cv * 49.9;
535 let release_ms = 10.0 + release_cv * 490.0;
536 let attack_coef = env_coef(attack_ms / 1000.0, self.sample_rate);
537 let release_coef = env_coef(release_ms / 1000.0, self.sample_rate);
538
539 let abs_input = Libm::<f64>::fabs(sidechain);
540 if abs_input > self.envelope {
541 self.envelope = attack_coef * self.envelope + (1.0 - attack_coef) * abs_input;
542 } else {
543 self.envelope = release_coef * self.envelope + (1.0 - release_coef) * abs_input;
544 }
545 self.envelope = flush_denorm(self.envelope);
547
548 let open_threshold = threshold;
549 let close_threshold = threshold * 0.7;
550
551 let hold_samples = (Self::HOLD_MS * self.sample_rate / 1000.0) as u32;
556 if self.envelope > open_threshold {
557 self.gate_open = true;
558 self.hold_counter = hold_samples;
559 } else if self.hold_counter > 0 {
560 self.hold_counter -= 1;
561 } else if self.envelope < close_threshold {
562 self.gate_open = false;
563 }
564
565 let fade_coef = env_coef(Self::FADE_MS / 1000.0, self.sample_rate);
568 let target = if self.gate_open { 1.0 } else { 0.0 };
569 self.gate_state = fade_coef * self.gate_state + (1.0 - fade_coef) * target;
570 self.gate_state = flush_denorm(self.gate_state);
572
573 let gain = (1.0 - range) + range * self.gate_state;
574 outputs.set(10, input * gain);
575 outputs.set(
576 11,
577 if self.gate_state > 0.5 {
578 GATE_HIGH_V
579 } else {
580 0.0
581 },
582 );
583 }
584
585 fn reset(&mut self) {
586 self.envelope = 0.0;
587 self.gate_state = 0.0;
588 self.gate_open = false;
589 self.hold_counter = 0;
590 }
591
592 fn set_sample_rate(&mut self, sample_rate: f64) {
593 self.sample_rate = sample_rate;
594 }
595
596 fn type_id(&self) -> &'static str {
597 "noise_gate"
598 }
599}
600
601pub struct Compressor {
605 sample_rate: f64,
606 envelope: f64,
607 spec: PortSpec,
608}
609
610impl Compressor {
611 pub fn new(sample_rate: f64) -> Self {
612 Self {
613 sample_rate,
614 envelope: 0.0,
615 spec: PortSpec {
616 inputs: vec![
617 PortDef::new(0, "in", SignalKind::Audio),
618 PortDef::new(1, "threshold", SignalKind::CvUnipolar)
619 .with_default(0.5)
620 .with_attenuverter(),
621 PortDef::new(2, "ratio", SignalKind::CvUnipolar)
622 .with_default(0.5)
623 .with_attenuverter(),
624 PortDef::new(3, "attack", SignalKind::CvUnipolar)
625 .with_default(0.2)
626 .with_attenuverter(),
627 PortDef::new(4, "release", SignalKind::CvUnipolar)
628 .with_default(0.3)
629 .with_attenuverter(),
630 PortDef::new(5, "makeup", SignalKind::CvUnipolar)
631 .with_default(0.0)
632 .with_attenuverter(),
633 PortDef::new(6, "sidechain", SignalKind::Audio),
634 ],
635 outputs: vec![
636 PortDef::new(10, "out", SignalKind::Audio),
637 PortDef::new(11, "gr", SignalKind::CvUnipolar),
638 ],
639 },
640 }
641 }
642}
643
644impl Default for Compressor {
645 fn default() -> Self {
646 Self::new(44100.0)
647 }
648}
649
650impl GraphModule for Compressor {
651 fn port_spec(&self) -> &PortSpec {
652 &self.spec
653 }
654
655 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
656 let input = sanitize_audio(inputs.get_or(0, 0.0));
659 let threshold_cv = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
660 let ratio_cv = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
661 let attack_cv = inputs.get_or(3, 0.2).clamp(0.0, 1.0);
662 let release_cv = inputs.get_or(4, 0.3).clamp(0.0, 1.0);
663 let makeup_cv = inputs.get_or(5, 0.0).clamp(0.0, 1.0);
664 let sidechain = sanitize_audio(inputs.get_or(6, input));
665
666 let threshold = threshold_cv * 5.0;
667 let ratio = 1.0 + ratio_cv * 19.0;
668 let attack_ms = 0.1 + attack_cv * 99.9;
669 let release_ms = 10.0 + release_cv * 990.0;
670 let makeup_gain = 1.0 + makeup_cv * 3.0;
671
672 let attack_coef = env_coef(attack_ms / 1000.0, self.sample_rate);
673 let release_coef = env_coef(release_ms / 1000.0, self.sample_rate);
674
675 let abs_sidechain = Libm::<f64>::fabs(sidechain);
676 if abs_sidechain > self.envelope {
677 self.envelope = attack_coef * self.envelope + (1.0 - attack_coef) * abs_sidechain;
678 } else {
679 self.envelope = release_coef * self.envelope + (1.0 - release_coef) * abs_sidechain;
680 }
681 self.envelope = flush_denorm(self.envelope);
683
684 let gain = if self.envelope > threshold && threshold > 0.0 {
685 let over_db = gain_to_db(self.envelope / threshold);
686 let compressed_db = over_db / ratio;
687 let gain_reduction_db = over_db - compressed_db;
688 db_to_gain(-gain_reduction_db)
689 } else {
690 1.0
691 };
692
693 outputs.set(10, input * gain * makeup_gain);
694 outputs.set(11, (1.0 - gain) * 10.0);
695 }
696
697 fn reset(&mut self) {
698 self.envelope = 0.0;
699 }
700
701 fn set_sample_rate(&mut self, sample_rate: f64) {
702 self.sample_rate = sample_rate;
703 }
704
705 fn type_id(&self) -> &'static str {
706 "compressor"
707 }
708}
709
710pub struct Ducker {
725 sample_rate: f64,
726 envelope: f64,
728 amount: ModulatedParam,
730 threshold: ModulatedParam,
732 spec: PortSpec,
733}
734
735impl Ducker {
736 pub fn new(sample_rate: f64) -> Self {
737 Self {
738 sample_rate: if sample_rate > 0.0 {
739 sample_rate
740 } else {
741 44100.0
742 },
743 envelope: 0.0,
744 amount: ModulatedParam::new(ParamRange::Linear { min: 0.0, max: 1.0 }).with_base(1.0),
746 threshold: ModulatedParam::new(ParamRange::Linear { min: 0.0, max: 5.0 })
748 .with_base(0.2),
749 spec: PortSpec {
750 inputs: vec![
751 PortDef::new(0, "in", SignalKind::Audio),
752 PortDef::new(1, "key", SignalKind::Audio),
753 PortDef::new(2, "amount", SignalKind::CvBipolar).with_attenuverter(),
754 PortDef::new(3, "threshold", SignalKind::CvBipolar).with_attenuverter(),
755 PortDef::new(4, "attack", SignalKind::CvUnipolar)
756 .with_default(0.1)
757 .with_attenuverter(),
758 PortDef::new(5, "release", SignalKind::CvUnipolar)
759 .with_default(0.3)
760 .with_attenuverter(),
761 ],
762 outputs: vec![
763 PortDef::new(10, "out", SignalKind::Audio),
764 PortDef::new(11, "gr", SignalKind::CvUnipolar),
765 ],
766 },
767 }
768 }
769
770 pub fn set_amount(&mut self, amount: f64) {
772 self.amount.base = amount.clamp(0.0, 1.0);
773 }
774
775 pub fn amount(&self) -> f64 {
777 self.amount.base
778 }
779
780 pub fn set_threshold(&mut self, threshold: f64) {
783 self.threshold.base = threshold.clamp(0.0, 1.0);
784 }
785
786 pub fn threshold(&self) -> f64 {
788 self.threshold.base
789 }
790}
791
792impl Default for Ducker {
793 fn default() -> Self {
794 Self::new(44100.0)
795 }
796}
797
798impl GraphModule for Ducker {
799 fn port_spec(&self) -> &PortSpec {
800 &self.spec
801 }
802
803 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
804 let input = sanitize_audio(inputs.get_or(0, 0.0));
807 let key = sanitize_audio(inputs.get_or(1, 0.0));
808 let amount_cv = inputs.get_or(2, 0.0);
809 let threshold_cv = inputs.get_or(3, 0.0);
810 let attack_cv = inputs.get_or(4, 0.1).clamp(0.0, 1.0);
811 let release_cv = inputs.get_or(5, 0.3).clamp(0.0, 1.0);
812
813 self.amount.set_cv(amount_cv);
815 self.threshold.set_cv(threshold_cv);
816 let amount = self.amount.value().clamp(0.0, 1.0);
817 let threshold = self.threshold.value().max(0.0);
818
819 let attack_ms = 0.1 + attack_cv * 99.9;
820 let release_ms = 10.0 + release_cv * 990.0;
821 let attack_coef = env_coef(attack_ms / 1000.0, self.sample_rate);
822 let release_coef = env_coef(release_ms / 1000.0, self.sample_rate);
823
824 let abs_key = Libm::<f64>::fabs(key);
826 if abs_key > self.envelope {
827 self.envelope = attack_coef * self.envelope + (1.0 - attack_coef) * abs_key;
828 } else {
829 self.envelope = release_coef * self.envelope + (1.0 - release_coef) * abs_key;
830 }
831 self.envelope = flush_denorm(self.envelope);
832
833 let ratio = if threshold > 1e-9 {
836 (self.envelope / threshold).clamp(0.0, 1.0)
837 } else {
838 if self.envelope > 1e-9 {
840 1.0
841 } else {
842 0.0
843 }
844 };
845 let gr = amount * ratio;
846 let gain = 1.0 - gr;
847
848 outputs.set(10, input * gain);
849 outputs.set(11, gr * 10.0);
850 }
851
852 fn reset(&mut self) {
853 self.envelope = 0.0;
854 }
855
856 fn set_sample_rate(&mut self, sample_rate: f64) {
857 if sample_rate > 0.0 {
858 self.sample_rate = sample_rate;
859 }
860 }
861
862 fn type_id(&self) -> &'static str {
863 "ducker"
864 }
865
866 crate::impl_introspect!();
870}
871
872pub struct EnvelopeFollower {
876 sample_rate: f64,
877 envelope: f64,
878 spec: PortSpec,
879}
880
881impl EnvelopeFollower {
882 pub fn new(sample_rate: f64) -> Self {
883 Self {
884 sample_rate,
885 envelope: 0.0,
886 spec: PortSpec {
887 inputs: vec![
888 PortDef::new(0, "in", SignalKind::Audio),
889 PortDef::new(1, "attack", SignalKind::CvUnipolar)
890 .with_default(0.2)
891 .with_attenuverter(),
892 PortDef::new(2, "release", SignalKind::CvUnipolar)
893 .with_default(0.3)
894 .with_attenuverter(),
895 PortDef::new(3, "gain", SignalKind::CvUnipolar)
896 .with_default(0.5)
897 .with_attenuverter(),
898 ],
899 outputs: vec![
900 PortDef::new(10, "out", SignalKind::CvUnipolar),
901 PortDef::new(11, "inv", SignalKind::CvUnipolar),
902 ],
903 },
904 }
905 }
906}
907
908impl Default for EnvelopeFollower {
909 fn default() -> Self {
910 Self::new(44100.0)
911 }
912}
913
914impl GraphModule for EnvelopeFollower {
915 fn port_spec(&self) -> &PortSpec {
916 &self.spec
917 }
918
919 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
920 let input = sanitize_audio(inputs.get_or(0, 0.0));
923 let attack_cv = inputs.get_or(1, 0.2).clamp(0.0, 1.0);
924 let release_cv = inputs.get_or(2, 0.3).clamp(0.0, 1.0);
925 let gain = inputs.get_or(3, 0.5).clamp(0.0, 1.0) * 4.0;
926
927 let attack_ms = 0.1 + attack_cv * 99.9;
928 let release_ms = 1.0 + release_cv * 999.0;
929 let attack_coef = env_coef(attack_ms / 1000.0, self.sample_rate);
930 let release_coef = env_coef(release_ms / 1000.0, self.sample_rate);
931
932 let abs_input = Libm::<f64>::fabs(input);
933 if abs_input > self.envelope {
934 self.envelope = attack_coef * self.envelope + (1.0 - attack_coef) * abs_input;
935 } else {
936 self.envelope = release_coef * self.envelope + (1.0 - release_coef) * abs_input;
937 }
938 self.envelope = flush_denorm(self.envelope);
940
941 let out = (self.envelope * gain).clamp(0.0, 10.0);
942 outputs.set(10, out);
943 outputs.set(11, 10.0 - out);
944 }
945
946 fn reset(&mut self) {
947 self.envelope = 0.0;
948 }
949
950 fn set_sample_rate(&mut self, sample_rate: f64) {
951 self.sample_rate = sample_rate;
952 }
953
954 fn type_id(&self) -> &'static str {
955 "envelope_follower"
956 }
957}
958
959#[cfg(test)]
960mod tests {
961 use super::*;
962 use crate::analog::Saturator;
963 use crate::modules::common::{measure_max_output, SAFE_AUDIO_LIMIT};
964
965 #[test]
966 fn test_adsr_envelope() {
967 let mut adsr = Adsr::new(1000.0); let mut inputs = PortValues::new();
969 let mut outputs = PortValues::new();
970
971 inputs.set(2, 0.1);
973
974 inputs.set(0, 5.0);
976
977 for _ in 0..100 {
979 adsr.tick(&inputs, &mut outputs);
980 }
981
982 let level = outputs.get(10).unwrap();
984 assert!(level > 0.0);
985 }
986 #[test]
987 fn test_vca() {
988 let mut vca = Vca::new();
989 let mut inputs = PortValues::new();
990 let mut outputs = PortValues::new();
991
992 inputs.set(0, 5.0); inputs.set(1, 5.0); vca.tick(&inputs, &mut outputs);
996
997 let out = outputs.get(10).unwrap();
998 assert!((out - 2.5).abs() < 0.01);
999 }
1000 #[test]
1001 fn test_limiter() {
1002 let mut limiter = Limiter::new(44100.0);
1003 let mut inputs = PortValues::new();
1004 let mut outputs = PortValues::new();
1005
1006 inputs.set(0, 10.0); inputs.set(1, 0.5); for _ in 0..100 {
1010 limiter.tick(&inputs, &mut outputs);
1011 }
1012
1013 let out = outputs.get(10).unwrap();
1015 assert!(out.abs() < 10.0);
1016 assert!(out.is_finite());
1017 }
1018 #[test]
1019 fn test_limiter_default() {
1020 let limiter = Limiter::default();
1021 assert_eq!(limiter.type_id(), "limiter");
1022 }
1023 #[test]
1024 fn test_noise_gate() {
1025 let mut gate = NoiseGate::new(44100.0);
1026 let mut inputs = PortValues::new();
1027 let mut outputs = PortValues::new();
1028
1029 inputs.set(0, 0.01); inputs.set(1, 0.5); for _ in 0..1000 {
1033 gate.tick(&inputs, &mut outputs);
1034 }
1035
1036 let out = outputs.get(10).unwrap();
1038 assert!(out.abs() < 0.1);
1039
1040 let gate_out = outputs.get(11).unwrap();
1042 assert!(gate_out < 2.5);
1043 }
1044 #[test]
1045 fn test_noise_gate_default() {
1046 let gate = NoiseGate::default();
1047 assert_eq!(gate.type_id(), "noise_gate");
1048 }
1049 #[test]
1050 fn test_compressor() {
1051 let mut comp = Compressor::new(44100.0);
1052 let mut inputs = PortValues::new();
1053 let mut outputs = PortValues::new();
1054
1055 inputs.set(0, 5.0);
1057 inputs.set(1, 0.2); inputs.set(2, 0.8); for _ in 0..100 {
1060 comp.tick(&inputs, &mut outputs);
1061 }
1062
1063 let out = outputs.get(10).unwrap();
1064 assert!(out.is_finite());
1065
1066 let gr = outputs.get(11).unwrap();
1068 assert!(gr >= 0.0);
1069 }
1070 #[test]
1071 fn test_compressor_default() {
1072 let comp = Compressor::default();
1073 assert_eq!(comp.type_id(), "compressor");
1074 }
1075 #[test]
1076 fn test_envelope_follower() {
1077 let mut ef = EnvelopeFollower::new(44100.0);
1078 let mut inputs = PortValues::new();
1079 let mut outputs = PortValues::new();
1080
1081 inputs.set(0, 5.0);
1083 for _ in 0..1000 {
1084 ef.tick(&inputs, &mut outputs);
1085 }
1086
1087 let out = outputs.get(10).unwrap();
1088 assert!(out > 0.0);
1089 assert!(out.is_finite());
1090
1091 let inv = outputs.get(11).unwrap();
1093 assert!(inv.is_finite());
1094 }
1095 #[test]
1096 fn test_envelope_follower_default() {
1097 let ef = EnvelopeFollower::default();
1098 assert_eq!(ef.type_id(), "envelope_follower");
1099 }
1100 #[test]
1101 fn test_adsr_default_reset_sample_rate() {
1102 let mut adsr = Adsr::default();
1103 assert!(adsr.sample_rate == 44100.0);
1104
1105 adsr.set_sample_rate(48000.0);
1106 assert!(adsr.sample_rate == 48000.0);
1107
1108 let mut inputs = PortValues::new();
1109 let mut outputs = PortValues::new();
1110 inputs.set(0, 5.0); for _ in 0..100 {
1112 adsr.tick(&inputs, &mut outputs);
1113 }
1114
1115 adsr.reset();
1116 assert!(adsr.level == 0.0);
1117 assert!(adsr.stage == AdsrStage::Idle);
1118
1119 assert_eq!(adsr.type_id(), "adsr");
1120 }
1121 #[test]
1122 fn test_vca_default_reset_sample_rate() {
1123 let mut vca = Vca::default();
1124 vca.reset();
1125 vca.set_sample_rate(48000.0);
1126 assert_eq!(vca.type_id(), "vca");
1127 }
1128 #[test]
1129 fn test_adsr_full_cycle() {
1130 let mut adsr = Adsr::new(44100.0);
1131 let mut inputs = PortValues::new();
1132 let mut outputs = PortValues::new();
1133
1134 inputs.set(1, 10.0); inputs.set(2, 10.0); inputs.set(3, 5.0); inputs.set(4, 10.0); inputs.set(0, 5.0);
1142 for _ in 0..1000 {
1143 adsr.tick(&inputs, &mut outputs);
1144 }
1145
1146 let peak = outputs.get(10).unwrap();
1148 assert!(peak > 0.0);
1149
1150 for _ in 0..1000 {
1152 adsr.tick(&inputs, &mut outputs);
1153 }
1154
1155 inputs.set(0, 0.0);
1157 for _ in 0..1000 {
1158 adsr.tick(&inputs, &mut outputs);
1159 }
1160
1161 let after_release = outputs.get(10).unwrap();
1163 assert!(after_release < 0.1);
1164 }
1165 #[test]
1166 fn test_adsr_output_bounded() {
1167 let mut adsr = Adsr::new(44100.0);
1168 let mut inputs = PortValues::new();
1169 let mut outputs = PortValues::new();
1170
1171 inputs.set(2, 0.0); inputs.set(3, 0.0); inputs.set(4, 1.0); inputs.set(5, 0.0); inputs.set(0, 5.0);
1179
1180 let max = measure_max_output(10000, || {
1181 adsr.tick(&inputs, &mut outputs);
1182 outputs.get(10).unwrap_or(0.0).abs()
1183 });
1184
1185 assert!(
1186 max <= 10.5, "ADSR output {} exceeds expected 0-10V range",
1188 max
1189 );
1190 }
1191 #[test]
1192 fn test_limiter_prevents_spikes() {
1193 let mut limiter = Limiter::new(44100.0);
1194 let mut inputs = PortValues::new();
1195 let mut outputs = PortValues::new();
1196
1197 inputs.set(1, 0.3); inputs.set(0, 10.0);
1202
1203 limiter.tick(&inputs, &mut outputs);
1204 let out = outputs.get(10).unwrap_or(0.0);
1205
1206 assert!(
1207 out.abs() <= 5.0,
1208 "Limiter failed to limit 10V input, got {}",
1209 out
1210 );
1211 }
1212 #[test]
1213 fn test_saturator_prevents_spikes() {
1214 let mut sat = Saturator::new(0.8); let mut inputs = PortValues::new();
1216 let mut outputs = PortValues::new();
1217
1218 inputs.set(0, 20.0);
1220
1221 sat.tick(&inputs, &mut outputs);
1222 let out = outputs.get(10).unwrap_or(0.0);
1223
1224 assert!(
1225 out.abs() <= SAFE_AUDIO_LIMIT,
1226 "Saturator failed to limit input, got {}",
1227 out
1228 );
1229 }
1230
1231 #[test]
1234 fn test_limiter_brickwall_never_exceeds_threshold() {
1235 let fs = 44100.0;
1236 for &thr_cv in &[0.2_f64, 0.5, 0.8, 1.0] {
1237 let mut lim = Limiter::new(fs);
1238 let mut inputs = PortValues::new();
1239 let mut outputs = PortValues::new();
1240 inputs.set(1, thr_cv); let threshold = thr_cv.clamp(0.01, 1.0) * 5.0;
1242 let mut max_out = 0.0f64;
1243 for i in 0..4000 {
1244 let x = 25.0 * (i as f64 * 0.05).sin();
1246 inputs.set(0, x);
1247 lim.tick(&inputs, &mut outputs);
1248 max_out = max_out.max(outputs.get(10).unwrap().abs());
1249 }
1250 assert!(
1251 max_out <= threshold + 1e-9,
1252 "soft limiter exceeded threshold {}: peak {}",
1253 threshold,
1254 max_out
1255 );
1256 }
1257 }
1258
1259 #[test]
1260 fn test_limiter_passes_gentle_signals() {
1261 let mut lim = Limiter::new(44100.0);
1262 let mut inputs = PortValues::new();
1263 let mut outputs = PortValues::new();
1264 inputs.set(1, 0.8); for i in 0..1000 {
1266 let x = (i as f64 * 0.05).sin(); inputs.set(0, x);
1268 lim.tick(&inputs, &mut outputs);
1269 let out = outputs.get(10).unwrap();
1270 assert!(
1271 (out - x).abs() < 1e-9,
1272 "gentle signal altered: in={} out={}",
1273 x,
1274 out
1275 );
1276 }
1277 }
1278
1279 #[test]
1287 fn test_limiter_soft_knee_c0_continuous() {
1288 let mut lim = Limiter::new(48000.0);
1289 let threshold = 0.8 * 5.0; let step = 0.01_f64;
1291
1292 let steady_output = |lim: &mut Limiter, a: f64| -> f64 {
1293 lim.reset();
1294 let mut inputs = PortValues::new();
1295 inputs.set(0, a); inputs.set(1, 0.8); inputs.set(2, 0.3); inputs.set(3, 5.0); let mut outputs = PortValues::new();
1300 for _ in 0..8 {
1303 outputs = PortValues::new();
1304 lim.tick(&inputs, &mut outputs);
1305 }
1306 outputs.get(10).unwrap()
1307 };
1308
1309 let mut prev: Option<(f64, f64)> = None;
1310 let mut a = 1.0_f64; while a <= 6.0 {
1312 let out = steady_output(&mut lim, a);
1313
1314 assert!(
1316 out <= threshold + 1e-9,
1317 "soft limiter output {out} exceeds threshold {threshold} at a={a}"
1318 );
1319
1320 if let Some((pa, pout)) = prev {
1321 let jump = (out - pout).abs();
1322 assert!(
1323 jump <= (a - pa) + 1e-6,
1324 "soft-knee discontinuity: output stepped {jump} between \
1325 a={pa} and a={a} (amplitude step {})",
1326 a - pa
1327 );
1328 }
1329 prev = Some((a, out));
1330 a += step;
1331 }
1332 }
1333
1334 #[test]
1337 fn test_adsr_decay_release_durations() {
1338 let fs = 1000.0;
1339 let mut adsr = Adsr::new(fs);
1340 let mut inputs = PortValues::new();
1341 let mut outputs = PortValues::new();
1342 inputs.set(2, 0.0); inputs.set(3, 0.5); inputs.set(4, 0.5); inputs.set(5, 0.5); inputs.set(0, 5.0); loop {
1350 adsr.tick(&inputs, &mut outputs);
1351 if adsr.stage == AdsrStage::Decay {
1352 break;
1353 }
1354 }
1355 let mut decay_samples = 0u32;
1357 while adsr.stage == AdsrStage::Decay {
1358 adsr.tick(&inputs, &mut outputs);
1359 decay_samples += 1;
1360 }
1361 assert!(
1362 (decay_samples as f64 - 100.0).abs() <= 5.0,
1363 "decay lasted {} samples, expected ~100",
1364 decay_samples
1365 );
1366
1367 inputs.set(0, 0.0);
1369 let mut release_samples = 0u32;
1370 loop {
1371 adsr.tick(&inputs, &mut outputs);
1372 match adsr.stage {
1373 AdsrStage::Release => release_samples += 1,
1374 AdsrStage::Idle => {
1375 release_samples += 1;
1376 break;
1377 }
1378 _ => break,
1379 }
1380 }
1381 assert!(
1382 (release_samples as f64 - 100.0).abs() <= 5.0,
1383 "release lasted {} samples, expected ~100",
1384 release_samples
1385 );
1386 }
1387
1388 #[test]
1391 fn test_noise_gate_no_chatter_near_threshold() {
1392 let fs = 44100.0;
1393 let mut gate = NoiseGate::new(fs);
1394 let mut inputs = PortValues::new();
1395 let mut outputs = PortValues::new();
1396 inputs.set(1, 0.2); let mut transitions = 0;
1398 let mut last_hi = false;
1399 for i in 0..(fs as usize) {
1400 let amp = if i % 2 == 0 { 1.3 } else { 0.9 };
1402 inputs.set(0, amp);
1403 gate.tick(&inputs, &mut outputs);
1404 let hi = outputs.get(11).unwrap() > GATE_THRESHOLD_V;
1405 if hi != last_hi {
1406 transitions += 1;
1407 last_hi = hi;
1408 }
1409 }
1410 assert!(
1411 transitions <= 2,
1412 "gate chattered near threshold: {} transitions",
1413 transitions
1414 );
1415 }
1416
1417 #[test]
1418 fn test_noise_gate_fade_rate_independent_of_detector() {
1419 fn measure_open_fade(attack_cv: f64) -> usize {
1423 let fs = 44100.0;
1424 let mut gate = NoiseGate::new(fs);
1425 let mut inputs = PortValues::new();
1426 let mut outputs = PortValues::new();
1427 inputs.set(1, 0.2); inputs.set(2, attack_cv); inputs.set(0, 5.0); let mut started = false;
1431 let mut count = 0usize;
1432 for _ in 0..200_000 {
1433 gate.tick(&inputs, &mut outputs);
1434 if started {
1435 count += 1;
1436 if gate.gate_state >= 0.632 {
1437 return count;
1438 }
1439 } else if gate.gate_state > 0.0 {
1440 started = true;
1441 count = 1;
1442 if gate.gate_state >= 0.632 {
1443 return count;
1444 }
1445 }
1446 }
1447 count
1448 }
1449 let fast = measure_open_fade(0.0); let slow = measure_open_fade(1.0); assert!(
1452 (fast as i64 - slow as i64).abs() <= 2,
1453 "fade rate varied with detector attack: fast={} slow={}",
1454 fast,
1455 slow
1456 );
1457 let expected = (NoiseGate::FADE_MS * 44100.0 / 1000.0) as i64;
1459 assert!(
1460 (fast as i64 - expected).abs() <= 3,
1461 "fade tc {} samples != expected {}",
1462 fast,
1463 expected
1464 );
1465 }
1466
1467 #[test]
1470 fn test_dynamics_detectors_flush_to_zero() {
1471 let fs = 44100.0;
1472 const BUDGET: usize = 500_000;
1473
1474 {
1476 let mut m = EnvelopeFollower::new(fs);
1477 let mut i = PortValues::new();
1478 let mut o = PortValues::new();
1479 i.set(2, 0.0); i.set(0, 5.0);
1481 for _ in 0..2000 {
1482 m.tick(&i, &mut o);
1483 }
1484 i.set(0, 0.0);
1485 let mut n = 0;
1486 while m.envelope != 0.0 && n < BUDGET {
1487 m.tick(&i, &mut o);
1488 n += 1;
1489 }
1490 assert!(
1491 m.envelope == 0.0,
1492 "EnvelopeFollower left tail {}",
1493 m.envelope
1494 );
1495 }
1496
1497 {
1499 let mut m = Limiter::new(fs);
1500 let mut i = PortValues::new();
1501 let mut o = PortValues::new();
1502 i.set(2, 0.0);
1503 i.set(0, 5.0);
1504 for _ in 0..2000 {
1505 m.tick(&i, &mut o);
1506 }
1507 i.set(0, 0.0);
1508 let mut n = 0;
1509 while m.envelope != 0.0 && n < BUDGET {
1510 m.tick(&i, &mut o);
1511 n += 1;
1512 }
1513 assert!(m.envelope == 0.0, "Limiter left tail {}", m.envelope);
1514 }
1515
1516 {
1518 let mut m = Compressor::new(fs);
1519 let mut i = PortValues::new();
1520 let mut o = PortValues::new();
1521 i.set(4, 0.0);
1522 i.set(0, 5.0);
1523 for _ in 0..2000 {
1524 m.tick(&i, &mut o);
1525 }
1526 i.set(0, 0.0);
1527 let mut n = 0;
1528 while m.envelope != 0.0 && n < BUDGET {
1529 m.tick(&i, &mut o);
1530 n += 1;
1531 }
1532 assert!(m.envelope == 0.0, "Compressor left tail {}", m.envelope);
1533 }
1534
1535 {
1537 let mut m = NoiseGate::new(fs);
1538 let mut i = PortValues::new();
1539 let mut o = PortValues::new();
1540 i.set(3, 0.0); i.set(0, 5.0);
1542 for _ in 0..2000 {
1543 m.tick(&i, &mut o);
1544 }
1545 i.set(0, 0.0);
1546 let mut n = 0;
1547 while (m.envelope != 0.0 || m.gate_state != 0.0) && n < BUDGET {
1548 m.tick(&i, &mut o);
1549 n += 1;
1550 }
1551 assert!(
1552 m.envelope == 0.0 && m.gate_state == 0.0,
1553 "NoiseGate left tail: env {} gate {}",
1554 m.envelope,
1555 m.gate_state
1556 );
1557 }
1558 }
1559
1560 #[test]
1563 fn test_adsr_exp_mode_reaches_sustain() {
1564 let fs = 1000.0;
1565 let mut adsr = Adsr::new(fs);
1566 let mut inputs = PortValues::new();
1567 let mut outputs = PortValues::new();
1568 inputs.set(2, 0.3); inputs.set(3, 0.5); inputs.set(4, 0.6); inputs.set(5, 0.5); inputs.set(6, 5.0); inputs.set(0, 5.0); let mut reached = None;
1576 for i in 0..5000 {
1577 adsr.tick(&inputs, &mut outputs);
1578 if adsr.stage == AdsrStage::Sustain {
1579 reached = Some(i);
1580 break;
1581 }
1582 }
1583 let reached = reached.expect("exponential envelope should reach sustain");
1584 assert!(
1585 (adsr.level - 0.6).abs() < 1e-6,
1586 "exp sustain level {} != 0.6",
1587 adsr.level
1588 );
1589 assert!(
1591 reached < 2000,
1592 "exp env took {} samples to reach sustain",
1593 reached
1594 );
1595 }
1596
1597 #[test]
1598 fn test_adsr_linear_mode_is_linear() {
1599 let fs = 1000.0;
1600 let mut adsr = Adsr::new(fs);
1601 let mut inputs = PortValues::new();
1602 let mut outputs = PortValues::new();
1603 inputs.set(2, 0.5); inputs.set(0, 5.0); let mut levels = [0.0f64; 10];
1607 for l in levels.iter_mut() {
1608 adsr.tick(&inputs, &mut outputs);
1609 *l = adsr.level;
1610 }
1611 for (i, &lvl) in levels.iter().enumerate() {
1612 let expected = 0.01 * (i as f64 + 1.0);
1613 assert!(
1614 (lvl - expected).abs() < 1e-9,
1615 "linear attack sample {} = {}, expected {}",
1616 i,
1617 lvl,
1618 expected
1619 );
1620 }
1621 }
1622
1623 #[test]
1626 fn test_vca_default_golden() {
1627 let mut vca = Vca::new();
1628 let mut inputs = PortValues::new();
1629 let mut outputs = PortValues::new();
1630 let cases = [
1632 (1.0_f64, 10.0_f64, 1.0_f64),
1633 (2.0, 5.0, 1.0),
1634 (-4.0, 2.0, -0.8),
1635 (3.0, 7.0, 2.1),
1636 (5.0, 0.0, 0.0),
1637 (0.5, 10.0, 0.5),
1638 ];
1639 for (inp, cv, expected) in cases {
1640 inputs.set(0, inp);
1641 inputs.set(1, cv);
1642 vca.tick(&inputs, &mut outputs);
1643 let out = outputs.get(10).unwrap();
1644 assert!(
1645 (out - expected).abs() < 1e-12,
1646 "in={} cv={} => {} (want {})",
1647 inp,
1648 cv,
1649 out,
1650 expected
1651 );
1652 }
1653 }
1654
1655 #[test]
1656 fn test_vca_exponential_response() {
1657 let mut vca = Vca::new();
1658 let mut inputs = PortValues::new();
1659 let mut outputs = PortValues::new();
1660 inputs.set(2, 5.0); inputs.set(0, 1.0); inputs.set(1, 5.0);
1665 vca.tick(&inputs, &mut outputs);
1666 assert!((outputs.get(10).unwrap() - 0.25).abs() < 1e-9);
1667
1668 let mut prev = -1.0;
1670 for k in 0..=20 {
1671 let cv = k as f64 * 0.5;
1672 inputs.set(1, cv);
1673 vca.tick(&inputs, &mut outputs);
1674 let g = outputs.get(10).unwrap();
1675 assert!(g >= prev - 1e-12, "not monotonic at cv={}", cv);
1676 prev = g;
1677 }
1678
1679 inputs.set(1, 10.0);
1681 vca.tick(&inputs, &mut outputs);
1682 assert!((outputs.get(10).unwrap() - 1.0).abs() < 1e-9);
1683 inputs.set(1, 0.0);
1684 vca.tick(&inputs, &mut outputs);
1685 assert!(outputs.get(10).unwrap().abs() < 1e-12);
1686 }
1687
1688 #[test]
1689 fn test_vca_boost() {
1690 let mut vca = Vca::new();
1691 let mut inputs = PortValues::new();
1692 let mut outputs = PortValues::new();
1693 inputs.set(0, 1.0);
1694 inputs.set(1, 10.0); inputs.set(3, 2.0); vca.tick(&inputs, &mut outputs);
1697 assert!(
1698 (outputs.get(10).unwrap() - 2.0).abs() < 1e-9,
1699 "boost failed: {}",
1700 outputs.get(10).unwrap()
1701 );
1702 inputs.set(3, 5.0);
1704 vca.tick(&inputs, &mut outputs);
1705 assert!((outputs.get(10).unwrap() - 2.0).abs() < 1e-9);
1706 }
1707
1708 #[test]
1713 fn test_noise_gate_opens_from_sidechain() {
1714 let mut gate = NoiseGate::new(44100.0);
1717 let mut inputs = PortValues::new();
1718 let mut outputs = PortValues::new();
1719
1720 inputs.set(0, 0.01); inputs.set(1, 0.3); inputs.set(5, 5.0); for _ in 0..2000 {
1725 gate.tick(&inputs, &mut outputs);
1726 }
1727 assert!(
1729 outputs.get(11).unwrap() > GATE_THRESHOLD_V,
1730 "sidechain key should open the gate"
1731 );
1732 }
1733
1734 #[test]
1735 fn test_noise_gate_sidechain_unpatched_matches_input() {
1736 let mut gate = NoiseGate::new(44100.0);
1739 let mut inputs = PortValues::new();
1740 let mut outputs = PortValues::new();
1741 inputs.set(0, 0.01);
1742 inputs.set(1, 0.5);
1743 for _ in 0..2000 {
1744 gate.tick(&inputs, &mut outputs);
1745 }
1746 assert!(outputs.get(11).unwrap() < GATE_THRESHOLD_V);
1747 }
1748
1749 #[test]
1750 fn test_limiter_sidechain_drives_gain_reduction() {
1751 let mut limiter = Limiter::new(44100.0);
1753 let mut inputs = PortValues::new();
1754 let mut outputs = PortValues::new();
1755 inputs.set(0, 0.5); inputs.set(1, 0.5); inputs.set(4, 10.0); for _ in 0..200 {
1759 limiter.tick(&inputs, &mut outputs);
1760 }
1761 assert!(
1763 outputs.get(11).unwrap() > 0.0,
1764 "sidechain key should drive limiting"
1765 );
1766 }
1767
1768 #[test]
1769 fn test_ducker_attenuates_on_key_and_recovers() {
1770 let sr = 44100.0;
1771 let mut ducker = Ducker::new(sr);
1772 let mut inputs = PortValues::new();
1773 let mut outputs = PortValues::new();
1774
1775 inputs.set(0, 4.0); inputs.set(4, 0.0); inputs.set(5, 0.0); inputs.set(1, 5.0);
1781 for _ in 0..2000 {
1782 ducker.tick(&inputs, &mut outputs);
1783 }
1784 let ducked = outputs.get(10).unwrap();
1785 assert!(
1786 ducked.abs() < 3.5,
1787 "output should be attenuated while key active, got {ducked}"
1788 );
1789 assert!(
1790 outputs.get(11).unwrap() > 0.0,
1791 "gain-reduction CV should be positive while ducking"
1792 );
1793
1794 inputs.set(1, 0.0);
1796 for _ in 0..4000 {
1797 ducker.tick(&inputs, &mut outputs);
1798 }
1799 let recovered = outputs.get(10).unwrap();
1800 assert!(
1801 (recovered - 4.0).abs() < 0.2,
1802 "output should recover after key release, got {recovered}"
1803 );
1804 }
1805
1806 #[test]
1807 fn test_ducker_default_type_id() {
1808 let ducker = Ducker::default();
1809 assert_eq!(ducker.type_id(), "ducker");
1810 }
1811
1812 #[test]
1813 fn test_ducker_no_key_passes_through() {
1814 let mut ducker = Ducker::new(44100.0);
1816 let mut inputs = PortValues::new();
1817 let mut outputs = PortValues::new();
1818 inputs.set(0, 3.0);
1819 inputs.set(1, 0.0);
1820 for _ in 0..500 {
1821 ducker.tick(&inputs, &mut outputs);
1822 }
1823 assert!((outputs.get(10).unwrap() - 3.0).abs() < 1e-9);
1824 assert!(outputs.get(11).unwrap().abs() < 1e-9);
1825 }
1826
1827 fn assert_detector_recovers<M: GraphModule>(
1833 module: &mut M,
1834 poison_ports: &[u32],
1835 clean: &[(u32, f64)],
1836 envelope: impl Fn(&M) -> f64,
1837 ) {
1838 let mut inputs = PortValues::new();
1839 let mut outputs = PortValues::new();
1840 for &bad in &[f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1841 for &p in poison_ports {
1842 inputs.set(p, bad);
1843 }
1844 module.tick(&inputs, &mut outputs);
1845 }
1846 let mut inputs = PortValues::new();
1848 for &(port, value) in clean {
1849 inputs.set(port, value);
1850 }
1851 for _ in 0..2000 {
1852 module.tick(&inputs, &mut outputs);
1853 }
1854 assert!(
1855 envelope(module).is_finite(),
1856 "envelope stayed non-finite after a NaN input"
1857 );
1858 assert!(
1859 outputs.get(10).unwrap().is_finite(),
1860 "output stayed non-finite after a NaN input"
1861 );
1862 }
1863
1864 #[test]
1865 fn test_limiter_nan_recovery() {
1866 let mut m = Limiter::new(44100.0);
1867 assert_detector_recovers(&mut m, &[0, 4], &[(0, 0.5), (4, 0.5)], |m| m.envelope);
1868 }
1869
1870 #[test]
1871 fn test_noise_gate_nan_recovery() {
1872 let mut m = NoiseGate::new(44100.0);
1873 assert_detector_recovers(&mut m, &[0, 5], &[(0, 0.5), (5, 0.5)], |m| m.envelope);
1874 }
1875
1876 #[test]
1877 fn test_compressor_nan_recovery() {
1878 let mut m = Compressor::new(44100.0);
1879 assert_detector_recovers(&mut m, &[0, 6], &[(0, 0.5), (6, 0.5)], |m| m.envelope);
1880 }
1881
1882 #[test]
1883 fn test_ducker_nan_recovery() {
1884 let mut m = Ducker::new(44100.0);
1885 assert_detector_recovers(&mut m, &[0, 1], &[(0, 0.5), (1, 0.5)], |m| m.envelope);
1886 }
1887
1888 #[test]
1889 fn test_envelope_follower_nan_recovery() {
1890 let mut m = EnvelopeFollower::new(44100.0);
1891 assert_detector_recovers(&mut m, &[0], &[(0, 0.5)], |m| m.envelope);
1892 }
1893}