1use super::common::{
4 db_to_gain, env_coef, flush_denorm, gain_to_db, sanitize_audio, Memo, GATE_HIGH_V,
5 GATE_THRESHOLD_V,
6};
7use crate::port::{
8 GraphModule, ModulatedParam, ParamRange, PortDef, PortSpec, PortValues, SignalKind,
9};
10use alloc::vec;
11use libm::Libm;
12
13#[derive(Debug, Clone, Copy, PartialEq)]
15enum AdsrStage {
16 Idle,
17 Attack,
18 Decay,
19 Sustain,
20 Release,
21}
22
23pub struct Adsr {
52 stage: AdsrStage,
53 level: f64,
54 sample_rate: f64,
55 prev_gate: f64,
56 prev_retrig: f64,
57 release_start_level: f64,
61 time_memo: Memo<4, [f64; 6]>,
67 spec: PortSpec,
68}
69
70impl Adsr {
71 pub fn new(sample_rate: f64) -> Self {
72 Self {
73 stage: AdsrStage::Idle,
74 level: 0.0,
75 sample_rate,
76 prev_gate: 0.0,
77 prev_retrig: 0.0,
78 release_start_level: 0.0,
79 time_memo: Memo::new([0.0; 6]),
80 spec: PortSpec {
81 inputs: vec![
82 PortDef::new(0, "gate", SignalKind::Gate),
83 PortDef::new(1, "retrig", SignalKind::Trigger),
84 PortDef::new(2, "attack", SignalKind::CvUnipolar)
85 .with_default(0.1)
86 .with_attenuverter(),
87 PortDef::new(3, "decay", SignalKind::CvUnipolar)
88 .with_default(0.3)
89 .with_attenuverter(),
90 PortDef::new(4, "sustain", SignalKind::CvUnipolar)
91 .with_default(0.7)
92 .with_attenuverter(),
93 PortDef::new(5, "release", SignalKind::CvUnipolar)
94 .with_default(0.4)
95 .with_attenuverter(),
96 PortDef::new(6, "shape", SignalKind::Gate).with_default(0.0),
100 ],
101 outputs: vec![
102 PortDef::new(10, "env", SignalKind::CvUnipolar),
103 PortDef::new(11, "inv", SignalKind::CvUnipolar),
104 PortDef::new(12, "eoc", SignalKind::Trigger),
105 ],
106 },
107 }
108 }
109
110 fn cv_to_time(cv: f64) -> f64 {
111 0.001 * Libm::<f64>::pow(10000.0, cv.clamp(0.0, 1.0))
113 }
114}
115
116impl Default for Adsr {
117 fn default() -> Self {
118 Self::new(44100.0)
119 }
120}
121
122impl GraphModule for Adsr {
123 fn port_spec(&self) -> &PortSpec {
124 &self.spec
125 }
126
127 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
128 let gate = inputs.get_or(0, 0.0);
129 let retrig = inputs.get_or(1, 0.0);
130 let attack_cv = inputs.get_or(2, 0.1);
131 let decay_cv = inputs.get_or(3, 0.3);
132 let sustain_level = inputs.get_or(4, 0.7).clamp(0.0, 1.0);
133 let release_cv = inputs.get_or(5, 0.4);
134 let exp_mode = inputs.get_or(6, 0.0) > GATE_THRESHOLD_V;
135
136 let sample_rate = self.sample_rate;
140 let [attack_time, decay_time, release_time, attack_coef, decay_coef, release_coef] = self
141 .time_memo
142 .get_or_compute([attack_cv, decay_cv, release_cv, sample_rate], || {
143 let attack_time = Self::cv_to_time(attack_cv);
144 let decay_time = Self::cv_to_time(decay_cv);
145 let release_time = Self::cv_to_time(release_cv);
146 [
147 attack_time,
148 decay_time,
149 release_time,
150 env_coef(attack_time, sample_rate),
151 env_coef(decay_time, sample_rate),
152 env_coef(release_time, sample_rate),
153 ]
154 });
155
156 let gate_high = gate > GATE_THRESHOLD_V;
157 let gate_rising = gate_high && self.prev_gate <= GATE_THRESHOLD_V;
158 let gate_falling = !gate_high && self.prev_gate > GATE_THRESHOLD_V;
159 let retrig_rising = retrig > GATE_THRESHOLD_V && self.prev_retrig <= GATE_THRESHOLD_V;
160
161 if gate_rising || (retrig_rising && gate_high) {
164 self.stage = AdsrStage::Attack;
165 } else if gate_falling && self.stage != AdsrStage::Idle {
166 self.release_start_level = self.level;
169 self.stage = AdsrStage::Release;
170 }
171
172 let attack_rate = 1.0 / (attack_time * self.sample_rate);
175 let decay_rate = (1.0 - sustain_level) / (decay_time * self.sample_rate);
176 let release_rate = self.release_start_level / (release_time * self.sample_rate);
177
178 const EXP_DONE: f64 = 1e-3;
180
181 let mut eoc = 0.0;
183 match self.stage {
184 AdsrStage::Idle => {
185 self.level = 0.0;
186 }
187 AdsrStage::Attack => {
188 if exp_mode {
189 self.level += (1.0 - self.level) * (1.0 - attack_coef);
190 if self.level >= 1.0 - EXP_DONE {
191 self.level = 1.0;
192 self.stage = AdsrStage::Decay;
193 }
194 } else {
195 self.level += attack_rate;
196 if self.level >= 1.0 {
197 self.level = 1.0;
198 self.stage = AdsrStage::Decay;
199 }
200 }
201 }
202 AdsrStage::Decay => {
203 if exp_mode {
204 self.level += (sustain_level - self.level) * (1.0 - decay_coef);
205 if self.level - sustain_level <= EXP_DONE {
206 self.level = sustain_level;
207 self.stage = AdsrStage::Sustain;
208 }
209 } else {
210 self.level -= decay_rate;
211 if self.level <= sustain_level {
212 self.level = sustain_level;
213 self.stage = AdsrStage::Sustain;
214 }
215 }
216 }
217 AdsrStage::Sustain => {
218 self.level = sustain_level;
219 }
220 AdsrStage::Release => {
221 if exp_mode {
222 self.level += (0.0 - self.level) * (1.0 - release_coef);
223 if self.level <= EXP_DONE {
224 self.level = 0.0;
225 self.stage = AdsrStage::Idle;
226 eoc = GATE_HIGH_V; }
228 } else {
229 self.level -= release_rate;
230 if self.level <= 0.0 {
231 self.level = 0.0;
232 self.stage = AdsrStage::Idle;
233 eoc = GATE_HIGH_V; }
235 }
236 }
237 }
238
239 self.prev_gate = gate;
240 self.prev_retrig = retrig;
241
242 outputs.set(10, self.level * 10.0); outputs.set(11, (1.0 - self.level) * 10.0); outputs.set(12, eoc);
246 }
247
248 fn reset(&mut self) {
249 self.stage = AdsrStage::Idle;
250 self.level = 0.0;
251 self.prev_gate = 0.0;
252 self.prev_retrig = 0.0;
253 self.release_start_level = 0.0;
254 }
255
256 fn set_sample_rate(&mut self, sample_rate: f64) {
257 self.sample_rate = sample_rate;
258 }
259
260 fn type_id(&self) -> &'static str {
261 "adsr"
262 }
263}
264
265pub struct Vca {
289 spec: PortSpec,
290}
291
292impl Vca {
293 pub fn new() -> Self {
294 Self {
295 spec: PortSpec {
296 inputs: vec![
297 PortDef::new(0, "in", SignalKind::Audio),
298 PortDef::new(1, "cv", SignalKind::CvUnipolar)
299 .with_default(10.0)
300 .with_attenuverter(),
301 PortDef::new(2, "response", SignalKind::Gate).with_default(0.0),
303 PortDef::new(3, "gain", SignalKind::CvUnipolar).with_default(1.0),
305 ],
306 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
307 },
308 }
309 }
310}
311
312impl Default for Vca {
313 fn default() -> Self {
314 Self::new()
315 }
316}
317
318impl GraphModule for Vca {
319 fn port_spec(&self) -> &PortSpec {
320 &self.spec
321 }
322
323 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
324 let input = inputs.get_or(0, 0.0);
325 let cv = inputs.get_or(1, 10.0).clamp(0.0, 10.0) / 10.0;
326 let exp_response = inputs.get_or(2, 0.0) > GATE_THRESHOLD_V;
327 let gain_scale = inputs.get_or(3, 1.0).clamp(0.0, 2.0);
328
329 let base_gain = if exp_response { cv * cv } else { cv };
333
334 outputs.set(10, input * base_gain * gain_scale);
335 }
336
337 fn reset(&mut self) {}
338
339 fn set_sample_rate(&mut self, _: f64) {}
340
341 fn type_id(&self) -> &'static str {
342 "vca"
343 }
344}
345
346pub struct Limiter {
351 sample_rate: f64,
352 envelope: f64,
353 release_memo: Memo<2, f64>,
355 spec: PortSpec,
356}
357
358impl Limiter {
359 pub fn new(sample_rate: f64) -> Self {
360 Self {
361 sample_rate,
362 envelope: 0.0,
363 release_memo: Memo::new(0.0),
364 spec: PortSpec {
365 inputs: vec![
366 PortDef::new(0, "in", SignalKind::Audio),
367 PortDef::new(1, "threshold", SignalKind::CvUnipolar)
368 .with_default(0.8)
369 .with_attenuverter(),
370 PortDef::new(2, "release", SignalKind::CvUnipolar)
371 .with_default(0.3)
372 .with_attenuverter(),
373 PortDef::new(3, "soft", SignalKind::Gate).with_default(5.0),
374 PortDef::new(4, "sidechain", SignalKind::Audio),
378 ],
379 outputs: vec![
380 PortDef::new(10, "out", SignalKind::Audio),
381 PortDef::new(11, "gr", SignalKind::CvUnipolar),
382 ],
383 },
384 }
385 }
386}
387
388impl Default for Limiter {
389 fn default() -> Self {
390 Self::new(44100.0)
391 }
392}
393
394impl GraphModule for Limiter {
395 fn port_spec(&self) -> &PortSpec {
396 &self.spec
397 }
398
399 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
400 let input = sanitize_audio(inputs.get_or(0, 0.0));
403 let threshold = inputs.get_or(1, 0.8).clamp(0.01, 1.0) * 5.0;
404 let release_cv = inputs.get_or(2, 0.3).clamp(0.0, 1.0);
405 let soft_mode = inputs.get_or(3, 5.0) > GATE_THRESHOLD_V;
406 let sidechain = sanitize_audio(inputs.get_or(4, input));
408
409 let sample_rate = self.sample_rate;
411 let release_coef = self
412 .release_memo
413 .get_or_compute([release_cv, sample_rate], || {
414 let release_ms = 10.0 + release_cv * 990.0;
415 env_coef(release_ms / 1000.0, sample_rate)
416 });
417
418 let abs_input = Libm::<f64>::fabs(sidechain);
419
420 if abs_input > self.envelope {
421 self.envelope = abs_input;
422 } else {
423 self.envelope = release_coef * self.envelope + (1.0 - release_coef) * abs_input;
424 }
425 self.envelope = flush_denorm(self.envelope);
428
429 let gain = if soft_mode {
430 let knee_start = 0.5 * threshold;
440 if self.envelope > knee_start {
441 let span = threshold - knee_start; let target =
443 knee_start + span * Libm::<f64>::tanh((self.envelope - knee_start) / span);
444 target / self.envelope
445 } else {
446 1.0
447 }
448 } else if self.envelope > threshold {
449 threshold / self.envelope
450 } else {
451 1.0
452 };
453
454 let out = (input * gain).clamp(-threshold, threshold);
457 outputs.set(10, out);
458 outputs.set(11, (1.0 - gain) * 10.0);
459 }
460
461 fn reset(&mut self) {
462 self.envelope = 0.0;
463 }
464
465 fn set_sample_rate(&mut self, sample_rate: f64) {
466 self.sample_rate = sample_rate;
467 }
468
469 fn type_id(&self) -> &'static str {
470 "limiter"
471 }
472}
473
474pub struct NoiseGate {
490 sample_rate: f64,
491 envelope: f64,
492 gate_state: f64,
493 gate_open: bool,
495 hold_counter: u32,
498 coef_memo: Memo<3, [f64; 2]>,
501 fade_coef: f64,
505 spec: PortSpec,
506}
507
508impl NoiseGate {
509 const FADE_MS: f64 = 5.0;
511 const HOLD_MS: f64 = 10.0;
514
515 pub fn new(sample_rate: f64) -> Self {
516 Self {
517 sample_rate,
518 envelope: 0.0,
519 gate_state: 0.0,
520 gate_open: false,
521 hold_counter: 0,
522 coef_memo: Memo::new([0.0; 2]),
523 fade_coef: env_coef(Self::FADE_MS / 1000.0, sample_rate),
524 spec: PortSpec {
525 inputs: vec![
526 PortDef::new(0, "in", SignalKind::Audio),
527 PortDef::new(1, "threshold", SignalKind::CvUnipolar)
528 .with_default(0.1)
529 .with_attenuverter(),
530 PortDef::new(2, "attack", SignalKind::CvUnipolar)
531 .with_default(0.1)
532 .with_attenuverter(),
533 PortDef::new(3, "release", SignalKind::CvUnipolar)
534 .with_default(0.3)
535 .with_attenuverter(),
536 PortDef::new(4, "range", SignalKind::CvUnipolar)
537 .with_default(1.0)
538 .with_attenuverter(),
539 PortDef::new(5, "sidechain", SignalKind::Audio),
543 ],
544 outputs: vec![
545 PortDef::new(10, "out", SignalKind::Audio),
546 PortDef::new(11, "gate", SignalKind::Gate),
547 ],
548 },
549 }
550 }
551}
552
553impl Default for NoiseGate {
554 fn default() -> Self {
555 Self::new(44100.0)
556 }
557}
558
559impl GraphModule for NoiseGate {
560 fn port_spec(&self) -> &PortSpec {
561 &self.spec
562 }
563
564 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
565 let input = sanitize_audio(inputs.get_or(0, 0.0));
568 let threshold = inputs.get_or(1, 0.1).clamp(0.0, 1.0) * 5.0;
569 let attack_cv = inputs.get_or(2, 0.1).clamp(0.0, 1.0);
570 let release_cv = inputs.get_or(3, 0.3).clamp(0.0, 1.0);
571 let range = inputs.get_or(4, 1.0).clamp(0.0, 1.0);
572 let sidechain = sanitize_audio(inputs.get_or(5, input));
574
575 let sample_rate = self.sample_rate;
577 let [attack_coef, release_coef] =
578 self.coef_memo
579 .get_or_compute([attack_cv, release_cv, sample_rate], || {
580 let attack_ms = 0.1 + attack_cv * 49.9;
581 let release_ms = 10.0 + release_cv * 490.0;
582 [
583 env_coef(attack_ms / 1000.0, sample_rate),
584 env_coef(release_ms / 1000.0, sample_rate),
585 ]
586 });
587
588 let abs_input = Libm::<f64>::fabs(sidechain);
589 if abs_input > self.envelope {
590 self.envelope = attack_coef * self.envelope + (1.0 - attack_coef) * abs_input;
591 } else {
592 self.envelope = release_coef * self.envelope + (1.0 - release_coef) * abs_input;
593 }
594 self.envelope = flush_denorm(self.envelope);
596
597 let open_threshold = threshold;
598 let close_threshold = threshold * 0.7;
599
600 let hold_samples = (Self::HOLD_MS * self.sample_rate / 1000.0) as u32;
605 if self.envelope > open_threshold {
606 self.gate_open = true;
607 self.hold_counter = hold_samples;
608 } else if self.hold_counter > 0 {
609 self.hold_counter -= 1;
610 } else if self.envelope < close_threshold {
611 self.gate_open = false;
612 }
613
614 let fade_coef = self.fade_coef;
617 let target = if self.gate_open { 1.0 } else { 0.0 };
618 self.gate_state = fade_coef * self.gate_state + (1.0 - fade_coef) * target;
619 self.gate_state = flush_denorm(self.gate_state);
621
622 let gain = (1.0 - range) + range * self.gate_state;
623 outputs.set(10, input * gain);
624 outputs.set(
625 11,
626 if self.gate_state > 0.5 {
627 GATE_HIGH_V
628 } else {
629 0.0
630 },
631 );
632 }
633
634 fn reset(&mut self) {
635 self.envelope = 0.0;
636 self.gate_state = 0.0;
637 self.gate_open = false;
638 self.hold_counter = 0;
639 }
640
641 fn set_sample_rate(&mut self, sample_rate: f64) {
642 self.sample_rate = sample_rate;
643 self.fade_coef = env_coef(Self::FADE_MS / 1000.0, sample_rate);
645 }
646
647 fn type_id(&self) -> &'static str {
648 "noise_gate"
649 }
650}
651
652pub struct Compressor {
656 sample_rate: f64,
657 envelope: f64,
658 coef_memo: Memo<3, [f64; 2]>,
661 spec: PortSpec,
662}
663
664impl Compressor {
665 pub fn new(sample_rate: f64) -> Self {
666 Self {
667 sample_rate,
668 envelope: 0.0,
669 coef_memo: Memo::new([0.0; 2]),
670 spec: PortSpec {
671 inputs: vec![
672 PortDef::new(0, "in", SignalKind::Audio),
673 PortDef::new(1, "threshold", SignalKind::CvUnipolar)
674 .with_default(0.5)
675 .with_attenuverter(),
676 PortDef::new(2, "ratio", SignalKind::CvUnipolar)
677 .with_default(0.5)
678 .with_attenuverter(),
679 PortDef::new(3, "attack", SignalKind::CvUnipolar)
680 .with_default(0.2)
681 .with_attenuverter(),
682 PortDef::new(4, "release", SignalKind::CvUnipolar)
683 .with_default(0.3)
684 .with_attenuverter(),
685 PortDef::new(5, "makeup", SignalKind::CvUnipolar)
686 .with_default(0.0)
687 .with_attenuverter(),
688 PortDef::new(6, "sidechain", SignalKind::Audio),
689 ],
690 outputs: vec![
691 PortDef::new(10, "out", SignalKind::Audio),
692 PortDef::new(11, "gr", SignalKind::CvUnipolar),
693 ],
694 },
695 }
696 }
697}
698
699impl Default for Compressor {
700 fn default() -> Self {
701 Self::new(44100.0)
702 }
703}
704
705impl GraphModule for Compressor {
706 fn port_spec(&self) -> &PortSpec {
707 &self.spec
708 }
709
710 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
711 let input = sanitize_audio(inputs.get_or(0, 0.0));
714 let threshold_cv = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
715 let ratio_cv = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
716 let attack_cv = inputs.get_or(3, 0.2).clamp(0.0, 1.0);
717 let release_cv = inputs.get_or(4, 0.3).clamp(0.0, 1.0);
718 let makeup_cv = inputs.get_or(5, 0.0).clamp(0.0, 1.0);
719 let sidechain = sanitize_audio(inputs.get_or(6, input));
720
721 let threshold = threshold_cv * 5.0;
722 let ratio = 1.0 + ratio_cv * 19.0;
723 let makeup_gain = 1.0 + makeup_cv * 3.0;
724
725 let sample_rate = self.sample_rate;
727 let [attack_coef, release_coef] =
728 self.coef_memo
729 .get_or_compute([attack_cv, release_cv, sample_rate], || {
730 let attack_ms = 0.1 + attack_cv * 99.9;
731 let release_ms = 10.0 + release_cv * 990.0;
732 [
733 env_coef(attack_ms / 1000.0, sample_rate),
734 env_coef(release_ms / 1000.0, sample_rate),
735 ]
736 });
737
738 let abs_sidechain = Libm::<f64>::fabs(sidechain);
739 if abs_sidechain > self.envelope {
740 self.envelope = attack_coef * self.envelope + (1.0 - attack_coef) * abs_sidechain;
741 } else {
742 self.envelope = release_coef * self.envelope + (1.0 - release_coef) * abs_sidechain;
743 }
744 self.envelope = flush_denorm(self.envelope);
746
747 let gain = if self.envelope > threshold && threshold > 0.0 {
748 let over_db = gain_to_db(self.envelope / threshold);
749 let compressed_db = over_db / ratio;
750 let gain_reduction_db = over_db - compressed_db;
751 db_to_gain(-gain_reduction_db)
752 } else {
753 1.0
754 };
755
756 outputs.set(10, input * gain * makeup_gain);
757 outputs.set(11, (1.0 - gain) * 10.0);
758 }
759
760 fn reset(&mut self) {
761 self.envelope = 0.0;
762 }
763
764 fn set_sample_rate(&mut self, sample_rate: f64) {
765 self.sample_rate = sample_rate;
766 }
767
768 fn type_id(&self) -> &'static str {
769 "compressor"
770 }
771}
772
773pub struct Ducker {
788 sample_rate: f64,
789 envelope: f64,
791 amount: ModulatedParam,
793 threshold: ModulatedParam,
795 coef_memo: Memo<3, [f64; 2]>,
798 spec: PortSpec,
799}
800
801impl Ducker {
802 pub fn new(sample_rate: f64) -> Self {
803 Self {
804 sample_rate: if sample_rate > 0.0 {
805 sample_rate
806 } else {
807 44100.0
808 },
809 envelope: 0.0,
810 amount: ModulatedParam::new(ParamRange::Linear { min: 0.0, max: 1.0 }).with_base(1.0),
812 threshold: ModulatedParam::new(ParamRange::Linear { min: 0.0, max: 5.0 })
814 .with_base(0.2),
815 coef_memo: Memo::new([0.0; 2]),
816 spec: PortSpec {
817 inputs: vec![
818 PortDef::new(0, "in", SignalKind::Audio),
819 PortDef::new(1, "key", SignalKind::Audio),
820 PortDef::new(2, "amount", SignalKind::CvBipolar).with_attenuverter(),
821 PortDef::new(3, "threshold", SignalKind::CvBipolar).with_attenuverter(),
822 PortDef::new(4, "attack", SignalKind::CvUnipolar)
823 .with_default(0.1)
824 .with_attenuverter(),
825 PortDef::new(5, "release", SignalKind::CvUnipolar)
826 .with_default(0.3)
827 .with_attenuverter(),
828 ],
829 outputs: vec![
830 PortDef::new(10, "out", SignalKind::Audio),
831 PortDef::new(11, "gr", SignalKind::CvUnipolar),
832 ],
833 },
834 }
835 }
836
837 pub fn set_amount(&mut self, amount: f64) {
839 self.amount.base = amount.clamp(0.0, 1.0);
840 }
841
842 pub fn amount(&self) -> f64 {
844 self.amount.base
845 }
846
847 pub fn set_threshold(&mut self, threshold: f64) {
850 self.threshold.base = threshold.clamp(0.0, 1.0);
851 }
852
853 pub fn threshold(&self) -> f64 {
855 self.threshold.base
856 }
857}
858
859impl Default for Ducker {
860 fn default() -> Self {
861 Self::new(44100.0)
862 }
863}
864
865impl GraphModule for Ducker {
866 fn port_spec(&self) -> &PortSpec {
867 &self.spec
868 }
869
870 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
871 let input = sanitize_audio(inputs.get_or(0, 0.0));
874 let key = sanitize_audio(inputs.get_or(1, 0.0));
875 let amount_cv = inputs.get_or(2, 0.0);
876 let threshold_cv = inputs.get_or(3, 0.0);
877 let attack_cv = inputs.get_or(4, 0.1).clamp(0.0, 1.0);
878 let release_cv = inputs.get_or(5, 0.3).clamp(0.0, 1.0);
879
880 self.amount.set_cv(amount_cv);
882 self.threshold.set_cv(threshold_cv);
883 let amount = self.amount.value().clamp(0.0, 1.0);
884 let threshold = self.threshold.value().max(0.0);
885
886 let sample_rate = self.sample_rate;
888 let [attack_coef, release_coef] =
889 self.coef_memo
890 .get_or_compute([attack_cv, release_cv, sample_rate], || {
891 let attack_ms = 0.1 + attack_cv * 99.9;
892 let release_ms = 10.0 + release_cv * 990.0;
893 [
894 env_coef(attack_ms / 1000.0, sample_rate),
895 env_coef(release_ms / 1000.0, sample_rate),
896 ]
897 });
898
899 let abs_key = Libm::<f64>::fabs(key);
901 if abs_key > self.envelope {
902 self.envelope = attack_coef * self.envelope + (1.0 - attack_coef) * abs_key;
903 } else {
904 self.envelope = release_coef * self.envelope + (1.0 - release_coef) * abs_key;
905 }
906 self.envelope = flush_denorm(self.envelope);
907
908 let ratio = if threshold > 1e-9 {
911 (self.envelope / threshold).clamp(0.0, 1.0)
912 } else {
913 if self.envelope > 1e-9 {
915 1.0
916 } else {
917 0.0
918 }
919 };
920 let gr = amount * ratio;
921 let gain = 1.0 - gr;
922
923 outputs.set(10, input * gain);
924 outputs.set(11, gr * 10.0);
925 }
926
927 fn reset(&mut self) {
928 self.envelope = 0.0;
929 }
930
931 fn set_sample_rate(&mut self, sample_rate: f64) {
932 if sample_rate > 0.0 {
933 self.sample_rate = sample_rate;
934 }
935 }
936
937 fn type_id(&self) -> &'static str {
938 "ducker"
939 }
940
941 crate::impl_introspect!();
945}
946
947pub struct EnvelopeFollower {
951 sample_rate: f64,
952 envelope: f64,
953 coef_memo: Memo<3, [f64; 2]>,
956 spec: PortSpec,
957}
958
959impl EnvelopeFollower {
960 pub fn new(sample_rate: f64) -> Self {
961 Self {
962 sample_rate,
963 envelope: 0.0,
964 coef_memo: Memo::new([0.0; 2]),
965 spec: PortSpec {
966 inputs: vec![
967 PortDef::new(0, "in", SignalKind::Audio),
968 PortDef::new(1, "attack", SignalKind::CvUnipolar)
969 .with_default(0.2)
970 .with_attenuverter(),
971 PortDef::new(2, "release", SignalKind::CvUnipolar)
972 .with_default(0.3)
973 .with_attenuverter(),
974 PortDef::new(3, "gain", SignalKind::CvUnipolar)
975 .with_default(0.5)
976 .with_attenuverter(),
977 ],
978 outputs: vec![
979 PortDef::new(10, "out", SignalKind::CvUnipolar),
980 PortDef::new(11, "inv", SignalKind::CvUnipolar),
981 ],
982 },
983 }
984 }
985}
986
987impl Default for EnvelopeFollower {
988 fn default() -> Self {
989 Self::new(44100.0)
990 }
991}
992
993impl GraphModule for EnvelopeFollower {
994 fn port_spec(&self) -> &PortSpec {
995 &self.spec
996 }
997
998 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
999 let input = sanitize_audio(inputs.get_or(0, 0.0));
1002 let attack_cv = inputs.get_or(1, 0.2).clamp(0.0, 1.0);
1003 let release_cv = inputs.get_or(2, 0.3).clamp(0.0, 1.0);
1004 let gain = inputs.get_or(3, 0.5).clamp(0.0, 1.0) * 4.0;
1005
1006 let sample_rate = self.sample_rate;
1008 let [attack_coef, release_coef] =
1009 self.coef_memo
1010 .get_or_compute([attack_cv, release_cv, sample_rate], || {
1011 let attack_ms = 0.1 + attack_cv * 99.9;
1012 let release_ms = 1.0 + release_cv * 999.0;
1013 [
1014 env_coef(attack_ms / 1000.0, sample_rate),
1015 env_coef(release_ms / 1000.0, sample_rate),
1016 ]
1017 });
1018
1019 let abs_input = Libm::<f64>::fabs(input);
1020 if abs_input > self.envelope {
1021 self.envelope = attack_coef * self.envelope + (1.0 - attack_coef) * abs_input;
1022 } else {
1023 self.envelope = release_coef * self.envelope + (1.0 - release_coef) * abs_input;
1024 }
1025 self.envelope = flush_denorm(self.envelope);
1027
1028 let out = (self.envelope * gain).clamp(0.0, 10.0);
1029 outputs.set(10, out);
1030 outputs.set(11, 10.0 - out);
1031 }
1032
1033 fn reset(&mut self) {
1034 self.envelope = 0.0;
1035 }
1036
1037 fn set_sample_rate(&mut self, sample_rate: f64) {
1038 self.sample_rate = sample_rate;
1039 }
1040
1041 fn type_id(&self) -> &'static str {
1042 "envelope_follower"
1043 }
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048 use super::*;
1049 use crate::analog::Saturator;
1050 use crate::modules::common::{measure_max_output, SAFE_AUDIO_LIMIT};
1051
1052 #[test]
1053 fn test_adsr_envelope() {
1054 let mut adsr = Adsr::new(1000.0); let mut inputs = PortValues::new();
1056 let mut outputs = PortValues::new();
1057
1058 inputs.set(2, 0.1);
1060
1061 inputs.set(0, 5.0);
1063
1064 for _ in 0..100 {
1066 adsr.tick(&inputs, &mut outputs);
1067 }
1068
1069 let level = outputs.get(10).unwrap();
1071 assert!(level > 0.0);
1072 }
1073 #[test]
1074 fn test_vca() {
1075 let mut vca = Vca::new();
1076 let mut inputs = PortValues::new();
1077 let mut outputs = PortValues::new();
1078
1079 inputs.set(0, 5.0); inputs.set(1, 5.0); vca.tick(&inputs, &mut outputs);
1083
1084 let out = outputs.get(10).unwrap();
1085 assert!((out - 2.5).abs() < 0.01);
1086 }
1087 #[test]
1088 fn test_limiter() {
1089 let mut limiter = Limiter::new(44100.0);
1090 let mut inputs = PortValues::new();
1091 let mut outputs = PortValues::new();
1092
1093 inputs.set(0, 10.0); inputs.set(1, 0.5); for _ in 0..100 {
1097 limiter.tick(&inputs, &mut outputs);
1098 }
1099
1100 let out = outputs.get(10).unwrap();
1102 assert!(out.abs() < 10.0);
1103 assert!(out.is_finite());
1104 }
1105 #[test]
1106 fn test_limiter_default() {
1107 let limiter = Limiter::default();
1108 assert_eq!(limiter.type_id(), "limiter");
1109 }
1110 #[test]
1111 fn test_noise_gate() {
1112 let mut gate = NoiseGate::new(44100.0);
1113 let mut inputs = PortValues::new();
1114 let mut outputs = PortValues::new();
1115
1116 inputs.set(0, 0.01); inputs.set(1, 0.5); for _ in 0..1000 {
1120 gate.tick(&inputs, &mut outputs);
1121 }
1122
1123 let out = outputs.get(10).unwrap();
1125 assert!(out.abs() < 0.1);
1126
1127 let gate_out = outputs.get(11).unwrap();
1129 assert!(gate_out < 2.5);
1130 }
1131 #[test]
1132 fn test_noise_gate_default() {
1133 let gate = NoiseGate::default();
1134 assert_eq!(gate.type_id(), "noise_gate");
1135 }
1136 #[test]
1137 fn test_compressor() {
1138 let mut comp = Compressor::new(44100.0);
1139 let mut inputs = PortValues::new();
1140 let mut outputs = PortValues::new();
1141
1142 inputs.set(0, 5.0);
1144 inputs.set(1, 0.2); inputs.set(2, 0.8); for _ in 0..100 {
1147 comp.tick(&inputs, &mut outputs);
1148 }
1149
1150 let out = outputs.get(10).unwrap();
1151 assert!(out.is_finite());
1152
1153 let gr = outputs.get(11).unwrap();
1155 assert!(gr >= 0.0);
1156 }
1157 #[test]
1158 fn test_compressor_default() {
1159 let comp = Compressor::default();
1160 assert_eq!(comp.type_id(), "compressor");
1161 }
1162 #[test]
1163 fn test_envelope_follower() {
1164 let mut ef = EnvelopeFollower::new(44100.0);
1165 let mut inputs = PortValues::new();
1166 let mut outputs = PortValues::new();
1167
1168 inputs.set(0, 5.0);
1170 for _ in 0..1000 {
1171 ef.tick(&inputs, &mut outputs);
1172 }
1173
1174 let out = outputs.get(10).unwrap();
1175 assert!(out > 0.0);
1176 assert!(out.is_finite());
1177
1178 let inv = outputs.get(11).unwrap();
1180 assert!(inv.is_finite());
1181 }
1182 #[test]
1183 fn test_envelope_follower_default() {
1184 let ef = EnvelopeFollower::default();
1185 assert_eq!(ef.type_id(), "envelope_follower");
1186 }
1187 #[test]
1188 fn test_adsr_default_reset_sample_rate() {
1189 let mut adsr = Adsr::default();
1190 assert!(adsr.sample_rate == 44100.0);
1191
1192 adsr.set_sample_rate(48000.0);
1193 assert!(adsr.sample_rate == 48000.0);
1194
1195 let mut inputs = PortValues::new();
1196 let mut outputs = PortValues::new();
1197 inputs.set(0, 5.0); for _ in 0..100 {
1199 adsr.tick(&inputs, &mut outputs);
1200 }
1201
1202 adsr.reset();
1203 assert!(adsr.level == 0.0);
1204 assert!(adsr.stage == AdsrStage::Idle);
1205
1206 assert_eq!(adsr.type_id(), "adsr");
1207 }
1208 #[test]
1209 fn test_vca_default_reset_sample_rate() {
1210 let mut vca = Vca::default();
1211 vca.reset();
1212 vca.set_sample_rate(48000.0);
1213 assert_eq!(vca.type_id(), "vca");
1214 }
1215 #[test]
1216 fn test_adsr_full_cycle() {
1217 let mut adsr = Adsr::new(44100.0);
1218 let mut inputs = PortValues::new();
1219 let mut outputs = PortValues::new();
1220
1221 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);
1229 for _ in 0..1000 {
1230 adsr.tick(&inputs, &mut outputs);
1231 }
1232
1233 let peak = outputs.get(10).unwrap();
1235 assert!(peak > 0.0);
1236
1237 for _ in 0..1000 {
1239 adsr.tick(&inputs, &mut outputs);
1240 }
1241
1242 inputs.set(0, 0.0);
1244 for _ in 0..1000 {
1245 adsr.tick(&inputs, &mut outputs);
1246 }
1247
1248 let after_release = outputs.get(10).unwrap();
1250 assert!(after_release < 0.1);
1251 }
1252 #[test]
1253 fn test_adsr_output_bounded() {
1254 let mut adsr = Adsr::new(44100.0);
1255 let mut inputs = PortValues::new();
1256 let mut outputs = PortValues::new();
1257
1258 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);
1266
1267 let max = measure_max_output(10000, || {
1268 adsr.tick(&inputs, &mut outputs);
1269 outputs.get(10).unwrap_or(0.0).abs()
1270 });
1271
1272 assert!(
1273 max <= 10.5, "ADSR output {} exceeds expected 0-10V range",
1275 max
1276 );
1277 }
1278 #[test]
1279 fn test_limiter_prevents_spikes() {
1280 let mut limiter = Limiter::new(44100.0);
1281 let mut inputs = PortValues::new();
1282 let mut outputs = PortValues::new();
1283
1284 inputs.set(1, 0.3); inputs.set(0, 10.0);
1289
1290 limiter.tick(&inputs, &mut outputs);
1291 let out = outputs.get(10).unwrap_or(0.0);
1292
1293 assert!(
1294 out.abs() <= 5.0,
1295 "Limiter failed to limit 10V input, got {}",
1296 out
1297 );
1298 }
1299 #[test]
1300 fn test_saturator_prevents_spikes() {
1301 let mut sat = Saturator::new(0.8); let mut inputs = PortValues::new();
1303 let mut outputs = PortValues::new();
1304
1305 inputs.set(0, 20.0);
1307
1308 sat.tick(&inputs, &mut outputs);
1309 let out = outputs.get(10).unwrap_or(0.0);
1310
1311 assert!(
1312 out.abs() <= SAFE_AUDIO_LIMIT,
1313 "Saturator failed to limit input, got {}",
1314 out
1315 );
1316 }
1317
1318 #[test]
1321 fn test_limiter_brickwall_never_exceeds_threshold() {
1322 let fs = 44100.0;
1323 for &thr_cv in &[0.2_f64, 0.5, 0.8, 1.0] {
1324 let mut lim = Limiter::new(fs);
1325 let mut inputs = PortValues::new();
1326 let mut outputs = PortValues::new();
1327 inputs.set(1, thr_cv); let threshold = thr_cv.clamp(0.01, 1.0) * 5.0;
1329 let mut max_out = 0.0f64;
1330 for i in 0..4000 {
1331 let x = 25.0 * (i as f64 * 0.05).sin();
1333 inputs.set(0, x);
1334 lim.tick(&inputs, &mut outputs);
1335 max_out = max_out.max(outputs.get(10).unwrap().abs());
1336 }
1337 assert!(
1338 max_out <= threshold + 1e-9,
1339 "soft limiter exceeded threshold {}: peak {}",
1340 threshold,
1341 max_out
1342 );
1343 }
1344 }
1345
1346 #[test]
1347 fn test_limiter_passes_gentle_signals() {
1348 let mut lim = Limiter::new(44100.0);
1349 let mut inputs = PortValues::new();
1350 let mut outputs = PortValues::new();
1351 inputs.set(1, 0.8); for i in 0..1000 {
1353 let x = (i as f64 * 0.05).sin(); inputs.set(0, x);
1355 lim.tick(&inputs, &mut outputs);
1356 let out = outputs.get(10).unwrap();
1357 assert!(
1358 (out - x).abs() < 1e-9,
1359 "gentle signal altered: in={} out={}",
1360 x,
1361 out
1362 );
1363 }
1364 }
1365
1366 #[test]
1374 fn test_limiter_soft_knee_c0_continuous() {
1375 let mut lim = Limiter::new(48000.0);
1376 let threshold = 0.8 * 5.0; let step = 0.01_f64;
1378
1379 let steady_output = |lim: &mut Limiter, a: f64| -> f64 {
1380 lim.reset();
1381 let mut inputs = PortValues::new();
1382 inputs.set(0, a); inputs.set(1, 0.8); inputs.set(2, 0.3); inputs.set(3, 5.0); let mut outputs = PortValues::new();
1387 for _ in 0..8 {
1390 outputs = PortValues::new();
1391 lim.tick(&inputs, &mut outputs);
1392 }
1393 outputs.get(10).unwrap()
1394 };
1395
1396 let mut prev: Option<(f64, f64)> = None;
1397 let mut a = 1.0_f64; while a <= 6.0 {
1399 let out = steady_output(&mut lim, a);
1400
1401 assert!(
1403 out <= threshold + 1e-9,
1404 "soft limiter output {out} exceeds threshold {threshold} at a={a}"
1405 );
1406
1407 if let Some((pa, pout)) = prev {
1408 let jump = (out - pout).abs();
1409 assert!(
1410 jump <= (a - pa) + 1e-6,
1411 "soft-knee discontinuity: output stepped {jump} between \
1412 a={pa} and a={a} (amplitude step {})",
1413 a - pa
1414 );
1415 }
1416 prev = Some((a, out));
1417 a += step;
1418 }
1419 }
1420
1421 #[test]
1424 fn test_adsr_decay_release_durations() {
1425 let fs = 1000.0;
1426 let mut adsr = Adsr::new(fs);
1427 let mut inputs = PortValues::new();
1428 let mut outputs = PortValues::new();
1429 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 {
1437 adsr.tick(&inputs, &mut outputs);
1438 if adsr.stage == AdsrStage::Decay {
1439 break;
1440 }
1441 }
1442 let mut decay_samples = 0u32;
1444 while adsr.stage == AdsrStage::Decay {
1445 adsr.tick(&inputs, &mut outputs);
1446 decay_samples += 1;
1447 }
1448 assert!(
1449 (decay_samples as f64 - 100.0).abs() <= 5.0,
1450 "decay lasted {} samples, expected ~100",
1451 decay_samples
1452 );
1453
1454 inputs.set(0, 0.0);
1456 let mut release_samples = 0u32;
1457 loop {
1458 adsr.tick(&inputs, &mut outputs);
1459 match adsr.stage {
1460 AdsrStage::Release => release_samples += 1,
1461 AdsrStage::Idle => {
1462 release_samples += 1;
1463 break;
1464 }
1465 _ => break,
1466 }
1467 }
1468 assert!(
1469 (release_samples as f64 - 100.0).abs() <= 5.0,
1470 "release lasted {} samples, expected ~100",
1471 release_samples
1472 );
1473 }
1474
1475 #[test]
1478 fn test_noise_gate_no_chatter_near_threshold() {
1479 let fs = 44100.0;
1480 let mut gate = NoiseGate::new(fs);
1481 let mut inputs = PortValues::new();
1482 let mut outputs = PortValues::new();
1483 inputs.set(1, 0.2); let mut transitions = 0;
1485 let mut last_hi = false;
1486 for i in 0..(fs as usize) {
1487 let amp = if i % 2 == 0 { 1.3 } else { 0.9 };
1489 inputs.set(0, amp);
1490 gate.tick(&inputs, &mut outputs);
1491 let hi = outputs.get(11).unwrap() > GATE_THRESHOLD_V;
1492 if hi != last_hi {
1493 transitions += 1;
1494 last_hi = hi;
1495 }
1496 }
1497 assert!(
1498 transitions <= 2,
1499 "gate chattered near threshold: {} transitions",
1500 transitions
1501 );
1502 }
1503
1504 #[test]
1505 fn test_noise_gate_fade_rate_independent_of_detector() {
1506 fn measure_open_fade(attack_cv: f64) -> usize {
1510 let fs = 44100.0;
1511 let mut gate = NoiseGate::new(fs);
1512 let mut inputs = PortValues::new();
1513 let mut outputs = PortValues::new();
1514 inputs.set(1, 0.2); inputs.set(2, attack_cv); inputs.set(0, 5.0); let mut started = false;
1518 let mut count = 0usize;
1519 for _ in 0..200_000 {
1520 gate.tick(&inputs, &mut outputs);
1521 if started {
1522 count += 1;
1523 if gate.gate_state >= 0.632 {
1524 return count;
1525 }
1526 } else if gate.gate_state > 0.0 {
1527 started = true;
1528 count = 1;
1529 if gate.gate_state >= 0.632 {
1530 return count;
1531 }
1532 }
1533 }
1534 count
1535 }
1536 let fast = measure_open_fade(0.0); let slow = measure_open_fade(1.0); assert!(
1539 (fast as i64 - slow as i64).abs() <= 2,
1540 "fade rate varied with detector attack: fast={} slow={}",
1541 fast,
1542 slow
1543 );
1544 let expected = (NoiseGate::FADE_MS * 44100.0 / 1000.0) as i64;
1546 assert!(
1547 (fast as i64 - expected).abs() <= 3,
1548 "fade tc {} samples != expected {}",
1549 fast,
1550 expected
1551 );
1552 }
1553
1554 #[test]
1557 fn test_dynamics_detectors_flush_to_zero() {
1558 let fs = 44100.0;
1559 const BUDGET: usize = 500_000;
1560
1561 {
1563 let mut m = EnvelopeFollower::new(fs);
1564 let mut i = PortValues::new();
1565 let mut o = PortValues::new();
1566 i.set(2, 0.0); i.set(0, 5.0);
1568 for _ in 0..2000 {
1569 m.tick(&i, &mut o);
1570 }
1571 i.set(0, 0.0);
1572 let mut n = 0;
1573 while m.envelope != 0.0 && n < BUDGET {
1574 m.tick(&i, &mut o);
1575 n += 1;
1576 }
1577 assert!(
1578 m.envelope == 0.0,
1579 "EnvelopeFollower left tail {}",
1580 m.envelope
1581 );
1582 }
1583
1584 {
1586 let mut m = Limiter::new(fs);
1587 let mut i = PortValues::new();
1588 let mut o = PortValues::new();
1589 i.set(2, 0.0);
1590 i.set(0, 5.0);
1591 for _ in 0..2000 {
1592 m.tick(&i, &mut o);
1593 }
1594 i.set(0, 0.0);
1595 let mut n = 0;
1596 while m.envelope != 0.0 && n < BUDGET {
1597 m.tick(&i, &mut o);
1598 n += 1;
1599 }
1600 assert!(m.envelope == 0.0, "Limiter left tail {}", m.envelope);
1601 }
1602
1603 {
1605 let mut m = Compressor::new(fs);
1606 let mut i = PortValues::new();
1607 let mut o = PortValues::new();
1608 i.set(4, 0.0);
1609 i.set(0, 5.0);
1610 for _ in 0..2000 {
1611 m.tick(&i, &mut o);
1612 }
1613 i.set(0, 0.0);
1614 let mut n = 0;
1615 while m.envelope != 0.0 && n < BUDGET {
1616 m.tick(&i, &mut o);
1617 n += 1;
1618 }
1619 assert!(m.envelope == 0.0, "Compressor left tail {}", m.envelope);
1620 }
1621
1622 {
1624 let mut m = NoiseGate::new(fs);
1625 let mut i = PortValues::new();
1626 let mut o = PortValues::new();
1627 i.set(3, 0.0); i.set(0, 5.0);
1629 for _ in 0..2000 {
1630 m.tick(&i, &mut o);
1631 }
1632 i.set(0, 0.0);
1633 let mut n = 0;
1634 while (m.envelope != 0.0 || m.gate_state != 0.0) && n < BUDGET {
1635 m.tick(&i, &mut o);
1636 n += 1;
1637 }
1638 assert!(
1639 m.envelope == 0.0 && m.gate_state == 0.0,
1640 "NoiseGate left tail: env {} gate {}",
1641 m.envelope,
1642 m.gate_state
1643 );
1644 }
1645 }
1646
1647 #[test]
1650 fn test_adsr_exp_mode_reaches_sustain() {
1651 let fs = 1000.0;
1652 let mut adsr = Adsr::new(fs);
1653 let mut inputs = PortValues::new();
1654 let mut outputs = PortValues::new();
1655 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;
1663 for i in 0..5000 {
1664 adsr.tick(&inputs, &mut outputs);
1665 if adsr.stage == AdsrStage::Sustain {
1666 reached = Some(i);
1667 break;
1668 }
1669 }
1670 let reached = reached.expect("exponential envelope should reach sustain");
1671 assert!(
1672 (adsr.level - 0.6).abs() < 1e-6,
1673 "exp sustain level {} != 0.6",
1674 adsr.level
1675 );
1676 assert!(
1678 reached < 2000,
1679 "exp env took {} samples to reach sustain",
1680 reached
1681 );
1682 }
1683
1684 #[test]
1685 fn test_adsr_linear_mode_is_linear() {
1686 let fs = 1000.0;
1687 let mut adsr = Adsr::new(fs);
1688 let mut inputs = PortValues::new();
1689 let mut outputs = PortValues::new();
1690 inputs.set(2, 0.5); inputs.set(0, 5.0); let mut levels = [0.0f64; 10];
1694 for l in levels.iter_mut() {
1695 adsr.tick(&inputs, &mut outputs);
1696 *l = adsr.level;
1697 }
1698 for (i, &lvl) in levels.iter().enumerate() {
1699 let expected = 0.01 * (i as f64 + 1.0);
1700 assert!(
1701 (lvl - expected).abs() < 1e-9,
1702 "linear attack sample {} = {}, expected {}",
1703 i,
1704 lvl,
1705 expected
1706 );
1707 }
1708 }
1709
1710 #[test]
1713 fn test_vca_default_golden() {
1714 let mut vca = Vca::new();
1715 let mut inputs = PortValues::new();
1716 let mut outputs = PortValues::new();
1717 let cases = [
1719 (1.0_f64, 10.0_f64, 1.0_f64),
1720 (2.0, 5.0, 1.0),
1721 (-4.0, 2.0, -0.8),
1722 (3.0, 7.0, 2.1),
1723 (5.0, 0.0, 0.0),
1724 (0.5, 10.0, 0.5),
1725 ];
1726 for (inp, cv, expected) in cases {
1727 inputs.set(0, inp);
1728 inputs.set(1, cv);
1729 vca.tick(&inputs, &mut outputs);
1730 let out = outputs.get(10).unwrap();
1731 assert!(
1732 (out - expected).abs() < 1e-12,
1733 "in={} cv={} => {} (want {})",
1734 inp,
1735 cv,
1736 out,
1737 expected
1738 );
1739 }
1740 }
1741
1742 #[test]
1743 fn test_vca_exponential_response() {
1744 let mut vca = Vca::new();
1745 let mut inputs = PortValues::new();
1746 let mut outputs = PortValues::new();
1747 inputs.set(2, 5.0); inputs.set(0, 1.0); inputs.set(1, 5.0);
1752 vca.tick(&inputs, &mut outputs);
1753 assert!((outputs.get(10).unwrap() - 0.25).abs() < 1e-9);
1754
1755 let mut prev = -1.0;
1757 for k in 0..=20 {
1758 let cv = k as f64 * 0.5;
1759 inputs.set(1, cv);
1760 vca.tick(&inputs, &mut outputs);
1761 let g = outputs.get(10).unwrap();
1762 assert!(g >= prev - 1e-12, "not monotonic at cv={}", cv);
1763 prev = g;
1764 }
1765
1766 inputs.set(1, 10.0);
1768 vca.tick(&inputs, &mut outputs);
1769 assert!((outputs.get(10).unwrap() - 1.0).abs() < 1e-9);
1770 inputs.set(1, 0.0);
1771 vca.tick(&inputs, &mut outputs);
1772 assert!(outputs.get(10).unwrap().abs() < 1e-12);
1773 }
1774
1775 #[test]
1776 fn test_vca_boost() {
1777 let mut vca = Vca::new();
1778 let mut inputs = PortValues::new();
1779 let mut outputs = PortValues::new();
1780 inputs.set(0, 1.0);
1781 inputs.set(1, 10.0); inputs.set(3, 2.0); vca.tick(&inputs, &mut outputs);
1784 assert!(
1785 (outputs.get(10).unwrap() - 2.0).abs() < 1e-9,
1786 "boost failed: {}",
1787 outputs.get(10).unwrap()
1788 );
1789 inputs.set(3, 5.0);
1791 vca.tick(&inputs, &mut outputs);
1792 assert!((outputs.get(10).unwrap() - 2.0).abs() < 1e-9);
1793 }
1794
1795 #[test]
1800 fn test_noise_gate_opens_from_sidechain() {
1801 let mut gate = NoiseGate::new(44100.0);
1804 let mut inputs = PortValues::new();
1805 let mut outputs = PortValues::new();
1806
1807 inputs.set(0, 0.01); inputs.set(1, 0.3); inputs.set(5, 5.0); for _ in 0..2000 {
1812 gate.tick(&inputs, &mut outputs);
1813 }
1814 assert!(
1816 outputs.get(11).unwrap() > GATE_THRESHOLD_V,
1817 "sidechain key should open the gate"
1818 );
1819 }
1820
1821 #[test]
1822 fn test_noise_gate_sidechain_unpatched_matches_input() {
1823 let mut gate = NoiseGate::new(44100.0);
1826 let mut inputs = PortValues::new();
1827 let mut outputs = PortValues::new();
1828 inputs.set(0, 0.01);
1829 inputs.set(1, 0.5);
1830 for _ in 0..2000 {
1831 gate.tick(&inputs, &mut outputs);
1832 }
1833 assert!(outputs.get(11).unwrap() < GATE_THRESHOLD_V);
1834 }
1835
1836 #[test]
1837 fn test_limiter_sidechain_drives_gain_reduction() {
1838 let mut limiter = Limiter::new(44100.0);
1840 let mut inputs = PortValues::new();
1841 let mut outputs = PortValues::new();
1842 inputs.set(0, 0.5); inputs.set(1, 0.5); inputs.set(4, 10.0); for _ in 0..200 {
1846 limiter.tick(&inputs, &mut outputs);
1847 }
1848 assert!(
1850 outputs.get(11).unwrap() > 0.0,
1851 "sidechain key should drive limiting"
1852 );
1853 }
1854
1855 #[test]
1856 fn test_ducker_attenuates_on_key_and_recovers() {
1857 let sr = 44100.0;
1858 let mut ducker = Ducker::new(sr);
1859 let mut inputs = PortValues::new();
1860 let mut outputs = PortValues::new();
1861
1862 inputs.set(0, 4.0); inputs.set(4, 0.0); inputs.set(5, 0.0); inputs.set(1, 5.0);
1868 for _ in 0..2000 {
1869 ducker.tick(&inputs, &mut outputs);
1870 }
1871 let ducked = outputs.get(10).unwrap();
1872 assert!(
1873 ducked.abs() < 3.5,
1874 "output should be attenuated while key active, got {ducked}"
1875 );
1876 assert!(
1877 outputs.get(11).unwrap() > 0.0,
1878 "gain-reduction CV should be positive while ducking"
1879 );
1880
1881 inputs.set(1, 0.0);
1883 for _ in 0..4000 {
1884 ducker.tick(&inputs, &mut outputs);
1885 }
1886 let recovered = outputs.get(10).unwrap();
1887 assert!(
1888 (recovered - 4.0).abs() < 0.2,
1889 "output should recover after key release, got {recovered}"
1890 );
1891 }
1892
1893 #[test]
1894 fn test_ducker_default_type_id() {
1895 let ducker = Ducker::default();
1896 assert_eq!(ducker.type_id(), "ducker");
1897 }
1898
1899 #[test]
1900 fn test_ducker_no_key_passes_through() {
1901 let mut ducker = Ducker::new(44100.0);
1903 let mut inputs = PortValues::new();
1904 let mut outputs = PortValues::new();
1905 inputs.set(0, 3.0);
1906 inputs.set(1, 0.0);
1907 for _ in 0..500 {
1908 ducker.tick(&inputs, &mut outputs);
1909 }
1910 assert!((outputs.get(10).unwrap() - 3.0).abs() < 1e-9);
1911 assert!(outputs.get(11).unwrap().abs() < 1e-9);
1912 }
1913
1914 fn assert_detector_recovers<M: GraphModule>(
1920 module: &mut M,
1921 poison_ports: &[u32],
1922 clean: &[(u32, f64)],
1923 envelope: impl Fn(&M) -> f64,
1924 ) {
1925 let mut inputs = PortValues::new();
1926 let mut outputs = PortValues::new();
1927 for &bad in &[f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1928 for &p in poison_ports {
1929 inputs.set(p, bad);
1930 }
1931 module.tick(&inputs, &mut outputs);
1932 }
1933 let mut inputs = PortValues::new();
1935 for &(port, value) in clean {
1936 inputs.set(port, value);
1937 }
1938 for _ in 0..2000 {
1939 module.tick(&inputs, &mut outputs);
1940 }
1941 assert!(
1942 envelope(module).is_finite(),
1943 "envelope stayed non-finite after a NaN input"
1944 );
1945 assert!(
1946 outputs.get(10).unwrap().is_finite(),
1947 "output stayed non-finite after a NaN input"
1948 );
1949 }
1950
1951 #[test]
1952 fn test_limiter_nan_recovery() {
1953 let mut m = Limiter::new(44100.0);
1954 assert_detector_recovers(&mut m, &[0, 4], &[(0, 0.5), (4, 0.5)], |m| m.envelope);
1955 }
1956
1957 #[test]
1958 fn test_noise_gate_nan_recovery() {
1959 let mut m = NoiseGate::new(44100.0);
1960 assert_detector_recovers(&mut m, &[0, 5], &[(0, 0.5), (5, 0.5)], |m| m.envelope);
1961 }
1962
1963 #[test]
1964 fn test_compressor_nan_recovery() {
1965 let mut m = Compressor::new(44100.0);
1966 assert_detector_recovers(&mut m, &[0, 6], &[(0, 0.5), (6, 0.5)], |m| m.envelope);
1967 }
1968
1969 #[test]
1970 fn test_ducker_nan_recovery() {
1971 let mut m = Ducker::new(44100.0);
1972 assert_detector_recovers(&mut m, &[0, 1], &[(0, 0.5), (1, 0.5)], |m| m.envelope);
1973 }
1974
1975 #[test]
1976 fn test_envelope_follower_nan_recovery() {
1977 let mut m = EnvelopeFollower::new(44100.0);
1978 assert_detector_recovers(&mut m, &[0], &[(0, 0.5)], |m| m.envelope);
1979 }
1980
1981 #[test]
1989 fn test_adsr_memo_bit_identical() {
1990 let mut memoized = Adsr::new(44100.0);
1991 let mut forced = Adsr::new(44100.0);
1992 let mut inputs = PortValues::new();
1993 let mut out_m = PortValues::new();
1994 let mut out_f = PortValues::new();
1995
1996 for n in 0..30_000u32 {
1997 let t = n as f64;
1998 let gate = if (n % 10_000) < 6_000 { 5.0 } else { 0.0 };
2000 inputs.set(0, gate);
2001 inputs.set(6, if n < 15_000 { 0.0 } else { 5.0 });
2003 if n < 20_000 {
2004 inputs.set(2, 0.15);
2006 inputs.set(3, 0.25);
2007 inputs.set(5, 0.35);
2008 } else {
2009 inputs.set(2, 0.15 + 0.1 * Libm::<f64>::sin(t * 0.002));
2011 }
2012 inputs.set(4, 0.6);
2013
2014 memoized.tick(&inputs, &mut out_m);
2015 forced.time_memo.invalidate();
2016 forced.tick(&inputs, &mut out_f);
2017
2018 for &id in &[10u32, 11, 12] {
2019 assert_eq!(
2020 out_m.get(id).unwrap().to_bits(),
2021 out_f.get(id).unwrap().to_bits(),
2022 "ADSR output {id} diverged at sample {n}"
2023 );
2024 }
2025 }
2026 assert!(memoized.time_memo.recompute_count() <= 10_001);
2027 assert_eq!(forced.time_memo.recompute_count(), 30_000);
2028 }
2029
2030 #[test]
2033 fn test_adsr_memo_recompute_count() {
2034 let mut adsr = Adsr::new(44100.0);
2035 let mut inputs = PortValues::new();
2036 let mut outputs = PortValues::new();
2037 inputs.set(0, 5.0);
2038 for _ in 0..1000 {
2039 adsr.tick(&inputs, &mut outputs);
2040 }
2041 assert_eq!(adsr.time_memo.recompute_count(), 1);
2042
2043 adsr.set_sample_rate(48000.0);
2044 adsr.tick(&inputs, &mut outputs);
2045 assert_eq!(adsr.time_memo.recompute_count(), 2);
2046 }
2047
2048 #[test]
2052 fn test_compressor_memo_bit_identical() {
2053 let mut memoized = Compressor::new(44100.0);
2054 let mut forced = Compressor::new(44100.0);
2055 let mut inputs = PortValues::new();
2056 let mut out_m = PortValues::new();
2057 let mut out_f = PortValues::new();
2058
2059 for n in 0..10_000u32 {
2060 let t = n as f64;
2061 inputs.set(0, Libm::<f64>::sin(t * 0.053) * 4.5);
2062 inputs.set(1, 0.3);
2063 inputs.set(2, 0.7);
2064 if n >= 5_000 {
2065 inputs.set(4, 0.3 + 0.2 * Libm::<f64>::sin(t * 0.004));
2067 }
2068
2069 memoized.tick(&inputs, &mut out_m);
2070 forced.coef_memo.invalidate();
2071 forced.tick(&inputs, &mut out_f);
2072
2073 for &id in &[10u32, 11] {
2074 assert_eq!(
2075 out_m.get(id).unwrap().to_bits(),
2076 out_f.get(id).unwrap().to_bits(),
2077 "Compressor output {id} diverged at sample {n}"
2078 );
2079 }
2080 }
2081 assert!(memoized.coef_memo.recompute_count() <= 5_001);
2082 }
2083}