Skip to main content

wavecraft_dsp/combinators/
chain.rs

1//! Chain combinator for serial processor composition.
2
3use crate::traits::{ParamSpec, Processor, ProcessorParams, Transport};
4
5/// Processor wrapper that adds a standard per-instance bypass parameter.
6pub struct Bypassed<P> {
7    pub processor: P,
8    source_bypassed: bool,
9    target_bypassed: bool,
10    transition_phase: BypassTransitionPhase,
11    transition_samples: u32,
12}
13
14#[derive(Debug, Clone, Copy)]
15enum BypassTransitionPhase {
16    Stable,
17    FadeOut { remaining: u32 },
18    FadeIn { remaining: u32 },
19}
20
21const DEFAULT_BYPASS_TRANSITION_SAMPLES: u32 = 64;
22const MIN_BYPASS_TRANSITION_SAMPLES: u32 = 16;
23const MAX_BYPASS_TRANSITION_SAMPLES: u32 = 256;
24const BYPASS_TRANSITION_SECONDS: f32 = 0.002;
25
26impl<P> Bypassed<P> {
27    /// Creates a bypass wrapper around a processor instance.
28    pub fn new(processor: P) -> Self {
29        Self {
30            processor,
31            source_bypassed: false,
32            target_bypassed: false,
33            transition_phase: BypassTransitionPhase::Stable,
34            transition_samples: DEFAULT_BYPASS_TRANSITION_SAMPLES,
35        }
36    }
37
38    #[inline]
39    fn transition_samples_for_rate(sample_rate: f32) -> u32 {
40        if sample_rate <= 0.0 {
41            return DEFAULT_BYPASS_TRANSITION_SAMPLES;
42        }
43
44        ((sample_rate * BYPASS_TRANSITION_SECONDS).round() as u32)
45            .clamp(MIN_BYPASS_TRANSITION_SAMPLES, MAX_BYPASS_TRANSITION_SAMPLES)
46    }
47
48    #[inline]
49    fn apply_gain_ramp(buffer: &mut [&mut [f32]], start_gain: f32, end_gain: f32) {
50        let samples = buffer
51            .iter()
52            .map(|channel| channel.len())
53            .min()
54            .unwrap_or(0);
55        if samples == 0 {
56            return;
57        }
58
59        let denominator = samples.saturating_sub(1) as f32;
60
61        for channel in buffer.iter_mut() {
62            for (idx, sample) in channel.iter_mut().take(samples).enumerate() {
63                let t = if denominator <= 0.0 {
64                    1.0
65                } else {
66                    idx as f32 / denominator
67                };
68                let gain = start_gain + (end_gain - start_gain) * t;
69                *sample *= gain;
70            }
71        }
72    }
73}
74
75impl<P> Default for Bypassed<P>
76where
77    P: Default,
78{
79    fn default() -> Self {
80        Self::new(P::default())
81    }
82}
83
84impl<P> From<P> for Bypassed<P> {
85    fn from(processor: P) -> Self {
86        Self::new(processor)
87    }
88}
89
90/// Parameters for [`Bypassed`].
91///
92/// Includes wrapped processor parameters plus one boolean bypass flag.
93pub struct BypassedParams<PP> {
94    pub inner: PP,
95    pub bypassed: bool,
96}
97
98impl<PP> Default for BypassedParams<PP>
99where
100    PP: Default,
101{
102    fn default() -> Self {
103        Self {
104            inner: PP::default(),
105            bypassed: false,
106        }
107    }
108}
109
110impl<PP> ProcessorParams for BypassedParams<PP>
111where
112    PP: ProcessorParams,
113{
114    fn param_specs() -> &'static [ParamSpec] {
115        fn extend_specs(target: &mut Vec<ParamSpec>, source: &[ParamSpec]) {
116            for spec in source {
117                target.push(ParamSpec {
118                    name: spec.name,
119                    id_suffix: spec.id_suffix,
120                    range: spec.range.clone(),
121                    default: spec.default,
122                    unit: spec.unit,
123                    group: spec.group,
124                });
125            }
126        }
127
128        let inner_specs = PP::param_specs();
129        let mut merged = Vec::with_capacity(inner_specs.len() + 1);
130
131        extend_specs(&mut merged, inner_specs);
132        merged.push(ParamSpec {
133            name: "Bypass",
134            id_suffix: "bypass",
135            range: crate::ParamRange::Stepped { min: 0, max: 1 },
136            default: 0.0,
137            unit: "",
138            group: None,
139        });
140
141        // See comment in ChainParams::param_specs for rationale.
142        Box::leak(merged.into_boxed_slice())
143    }
144
145    fn from_param_defaults() -> Self {
146        Self {
147            inner: PP::from_param_defaults(),
148            bypassed: false,
149        }
150    }
151
152    fn plain_value_count() -> usize {
153        PP::plain_value_count() + 1
154    }
155
156    fn apply_plain_values(&mut self, values: &[f32]) {
157        let inner_count = PP::plain_value_count();
158        let split_at = inner_count.min(values.len());
159        let (inner_values, bypass_values) = values.split_at(split_at);
160
161        self.inner.apply_plain_values(inner_values);
162
163        if let Some(bypass_value) = bypass_values.first() {
164            self.bypassed = *bypass_value >= 0.5;
165        }
166    }
167}
168
169impl<P> Processor for Bypassed<P>
170where
171    P: Processor,
172{
173    type Params = BypassedParams<P::Params>;
174
175    fn process(&mut self, buffer: &mut [&mut [f32]], transport: &Transport, params: &Self::Params) {
176        if params.bypassed != self.target_bypassed {
177            self.target_bypassed = params.bypassed;
178            if self.source_bypassed != self.target_bypassed {
179                self.transition_phase = BypassTransitionPhase::FadeOut {
180                    remaining: self.transition_samples,
181                };
182            }
183        }
184
185        if !self.source_bypassed {
186            self.processor.process(buffer, transport, &params.inner);
187        }
188
189        let samples = buffer
190            .iter()
191            .map(|channel| channel.len())
192            .min()
193            .unwrap_or(0) as u32;
194        if samples == 0 {
195            return;
196        }
197
198        match self.transition_phase {
199            BypassTransitionPhase::Stable => {}
200            BypassTransitionPhase::FadeOut { remaining } => {
201                let used = remaining.min(samples);
202                let total = self.transition_samples.max(1) as f32;
203                let start_gain = remaining as f32 / total;
204                let end_gain = remaining.saturating_sub(used) as f32 / total;
205
206                Self::apply_gain_ramp(buffer, start_gain, end_gain);
207
208                let new_remaining = remaining.saturating_sub(used);
209                if new_remaining == 0 {
210                    self.source_bypassed = self.target_bypassed;
211                    self.transition_phase = BypassTransitionPhase::FadeIn {
212                        remaining: self.transition_samples,
213                    };
214                } else {
215                    self.transition_phase = BypassTransitionPhase::FadeOut {
216                        remaining: new_remaining,
217                    };
218                }
219            }
220            BypassTransitionPhase::FadeIn { remaining } => {
221                let used = remaining.min(samples);
222                let total = self.transition_samples.max(1) as f32;
223                let start_gain = 1.0 - (remaining as f32 / total);
224                let end_gain = 1.0 - (remaining.saturating_sub(used) as f32 / total);
225
226                Self::apply_gain_ramp(buffer, start_gain, end_gain);
227
228                let new_remaining = remaining.saturating_sub(used);
229                if new_remaining == 0 {
230                    self.transition_phase = BypassTransitionPhase::Stable;
231                } else {
232                    self.transition_phase = BypassTransitionPhase::FadeIn {
233                        remaining: new_remaining,
234                    };
235                }
236            }
237        }
238    }
239
240    fn set_sample_rate(&mut self, sample_rate: f32) {
241        self.transition_samples = Self::transition_samples_for_rate(sample_rate);
242        self.processor.set_sample_rate(sample_rate);
243    }
244
245    fn reset(&mut self) {
246        self.source_bypassed = self.target_bypassed;
247        self.transition_phase = BypassTransitionPhase::Stable;
248        self.processor.reset();
249    }
250}
251
252/// Combines two processors in series: A → B.
253///
254/// Audio flows through processor A, then through processor B.
255/// Parameters from both processors are merged.
256pub struct Chain<A, B> {
257    pub first: A,
258    pub second: B,
259}
260
261impl<A, B> Default for Chain<A, B>
262where
263    A: Default,
264    B: Default,
265{
266    fn default() -> Self {
267        Self {
268            first: A::default(),
269            second: B::default(),
270        }
271    }
272}
273
274/// Combined parameters for chained processors.
275///
276/// Merges parameter specs from both processors.
277pub struct ChainParams<PA, PB> {
278    pub first: PA,
279    pub second: PB,
280}
281
282impl<PA, PB> Default for ChainParams<PA, PB>
283where
284    PA: Default,
285    PB: Default,
286{
287    fn default() -> Self {
288        Self {
289            first: PA::default(),
290            second: PB::default(),
291        }
292    }
293}
294
295impl<PA, PB> ProcessorParams for ChainParams<PA, PB>
296where
297    PA: ProcessorParams,
298    PB: ProcessorParams,
299{
300    fn param_specs() -> &'static [ParamSpec] {
301        fn extend_specs(target: &mut Vec<ParamSpec>, source: &[ParamSpec]) {
302            for spec in source {
303                target.push(ParamSpec {
304                    name: spec.name,
305                    id_suffix: spec.id_suffix,
306                    range: spec.range.clone(),
307                    default: spec.default,
308                    unit: spec.unit,
309                    group: spec.group,
310                });
311            }
312        }
313
314        // WORKAROUND FOR HOT-RELOAD HANG:
315        //
316        // Do NOT use OnceLock or any locking primitive here. On macOS, when the
317        // subprocess calls dlopen() → wavecraft_get_params_json() → param_specs(),
318        // initialization of OnceLock statics can hang indefinitely (30s timeout).
319        //
320        // Instead, we allocate and leak the merged specs on EVERY call. This is
321        // acceptable because:
322        // 1. param_specs() is called at most once per plugin load (startup only)
323        // 2. The leak is ~hundreds of bytes (not per-sample, not per-frame)
324        // 3. Plugin lifetime = process lifetime (no meaningful leak)
325        // 4. Hot-reload works correctly (no 30s hang)
326        //
327        // This is a pragmatic trade-off: small memory leak vs. broken hot-reload.
328        // Future work: investigate root cause of OnceLock hang on macOS dlopen.
329
330        let first_specs = PA::param_specs();
331        let second_specs = PB::param_specs();
332
333        let mut merged = Vec::with_capacity(first_specs.len() + second_specs.len());
334
335        extend_specs(&mut merged, first_specs);
336        extend_specs(&mut merged, second_specs);
337
338        // Leak to get 'static reference (intentional - see comment above)
339        Box::leak(merged.into_boxed_slice())
340    }
341
342    fn from_param_defaults() -> Self {
343        Self {
344            first: PA::from_param_defaults(),
345            second: PB::from_param_defaults(),
346        }
347    }
348
349    fn plain_value_count() -> usize {
350        PA::plain_value_count() + PB::plain_value_count()
351    }
352
353    fn apply_plain_values(&mut self, values: &[f32]) {
354        let first_count = PA::plain_value_count();
355        let split_at = first_count.min(values.len());
356        let (first_values, second_values) = values.split_at(split_at);
357
358        self.first.apply_plain_values(first_values);
359        self.second.apply_plain_values(second_values);
360    }
361}
362
363impl<A, B> Processor for Chain<A, B>
364where
365    A: Processor,
366    B: Processor,
367{
368    type Params = ChainParams<A::Params, B::Params>;
369
370    fn process(&mut self, buffer: &mut [&mut [f32]], transport: &Transport, params: &Self::Params) {
371        // Process first, then second (serial chain)
372        self.first.process(buffer, transport, &params.first);
373        self.second.process(buffer, transport, &params.second);
374    }
375
376    fn set_sample_rate(&mut self, sample_rate: f32) {
377        self.first.set_sample_rate(sample_rate);
378        self.second.set_sample_rate(sample_rate);
379    }
380
381    fn reset(&mut self) {
382        self.first.reset();
383        self.second.reset();
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390    use std::sync::{
391        Arc,
392        atomic::{AtomicBool, AtomicU32, Ordering},
393    };
394
395    #[derive(Clone)]
396    struct TestGainParams {
397        level: f32,
398    }
399
400    impl Default for TestGainParams {
401        fn default() -> Self {
402            Self { level: 1.0 }
403        }
404    }
405
406    impl ProcessorParams for TestGainParams {
407        fn param_specs() -> &'static [ParamSpec] {
408            static SPECS: [ParamSpec; 1] = [ParamSpec {
409                name: "Level",
410                id_suffix: "level",
411                range: crate::ParamRange::Linear { min: 0.0, max: 2.0 },
412                default: 1.0,
413                unit: "x",
414                group: None,
415            }];
416            &SPECS
417        }
418
419        fn from_param_defaults() -> Self {
420            Self { level: 1.0 }
421        }
422
423        fn apply_plain_values(&mut self, values: &[f32]) {
424            if let Some(level) = values.first() {
425                self.level = *level;
426            }
427        }
428    }
429
430    #[derive(Default)]
431    struct TestGainDsp;
432
433    impl Processor for TestGainDsp {
434        type Params = TestGainParams;
435
436        fn process(
437            &mut self,
438            buffer: &mut [&mut [f32]],
439            _transport: &Transport,
440            params: &Self::Params,
441        ) {
442            for channel in buffer.iter_mut() {
443                for sample in channel.iter_mut() {
444                    *sample *= params.level;
445                }
446            }
447        }
448    }
449
450    #[derive(Clone, Default)]
451    struct TestPassthroughParams;
452
453    impl ProcessorParams for TestPassthroughParams {
454        fn param_specs() -> &'static [ParamSpec] {
455            &[]
456        }
457    }
458
459    #[derive(Default)]
460    struct TestPassthroughDsp;
461
462    impl Processor for TestPassthroughDsp {
463        type Params = TestPassthroughParams;
464
465        fn process(
466            &mut self,
467            _buffer: &mut [&mut [f32]],
468            _transport: &Transport,
469            _params: &Self::Params,
470        ) {
471        }
472    }
473
474    #[derive(Clone)]
475    struct TestParams;
476
477    impl Default for TestParams {
478        fn default() -> Self {
479            Self
480        }
481    }
482
483    impl ProcessorParams for TestParams {
484        fn param_specs() -> &'static [ParamSpec] {
485            &[]
486        }
487    }
488
489    #[derive(Clone, Default)]
490    struct PanicParamSpecsParams {
491        value: f32,
492    }
493
494    impl ProcessorParams for PanicParamSpecsParams {
495        fn param_specs() -> &'static [ParamSpec] {
496            panic!("param_specs must not be called from runtime split path")
497        }
498
499        fn plain_value_count() -> usize {
500            1
501        }
502
503        fn apply_plain_values(&mut self, values: &[f32]) {
504            if let Some(value) = values.first() {
505                self.value = *value;
506            }
507        }
508    }
509
510    struct LifecycleProbe {
511        set_sample_rate_calls: Arc<AtomicU32>,
512        reset_calls: Arc<AtomicU32>,
513        last_sample_rate_bits: Arc<AtomicU32>,
514        touched_process: Arc<AtomicBool>,
515    }
516
517    impl LifecycleProbe {
518        fn new() -> Self {
519            Self {
520                set_sample_rate_calls: Arc::new(AtomicU32::new(0)),
521                reset_calls: Arc::new(AtomicU32::new(0)),
522                last_sample_rate_bits: Arc::new(AtomicU32::new(0)),
523                touched_process: Arc::new(AtomicBool::new(false)),
524            }
525        }
526    }
527
528    impl Processor for LifecycleProbe {
529        type Params = TestParams;
530
531        fn process(
532            &mut self,
533            _buffer: &mut [&mut [f32]],
534            _transport: &Transport,
535            _params: &Self::Params,
536        ) {
537            self.touched_process.store(true, Ordering::SeqCst);
538        }
539
540        fn set_sample_rate(&mut self, sample_rate: f32) {
541            self.set_sample_rate_calls.fetch_add(1, Ordering::SeqCst);
542            self.last_sample_rate_bits
543                .store(sample_rate.to_bits(), Ordering::SeqCst);
544        }
545
546        fn reset(&mut self) {
547            self.reset_calls.fetch_add(1, Ordering::SeqCst);
548        }
549    }
550
551    #[test]
552    fn test_chain_processes_in_order() {
553        let mut chain = Chain {
554            first: TestGainDsp,
555            second: TestGainDsp,
556        };
557
558        let mut left = [1.0_f32, 1.0_f32];
559        let mut right = [1.0_f32, 1.0_f32];
560        let mut buffer = [&mut left[..], &mut right[..]];
561
562        let transport = Transport::default();
563        let params = ChainParams {
564            first: TestGainParams { level: 0.5 },
565            second: TestGainParams { level: 2.0 },
566        };
567
568        chain.process(&mut buffer, &transport, &params);
569
570        // Expected: 1.0 * 0.5 * 2.0 = 1.0
571        assert!((buffer[0][0] - 1.0_f32).abs() < 1e-6);
572        assert!((buffer[1][0] - 1.0_f32).abs() < 1e-6);
573    }
574
575    #[test]
576    fn test_chain_with_passthrough() {
577        let mut chain = Chain {
578            first: TestPassthroughDsp,
579            second: TestGainDsp,
580        };
581
582        let mut left = [2.0_f32, 2.0_f32];
583        let mut right = [2.0_f32, 2.0_f32];
584        let mut buffer = [&mut left[..], &mut right[..]];
585
586        let transport = Transport::default();
587        let params = ChainParams {
588            first: TestPassthroughParams,
589            second: TestGainParams { level: 0.5 },
590        };
591
592        chain.process(&mut buffer, &transport, &params);
593
594        // Expected: 2.0 * 1.0 * 0.5 = 1.0
595        assert!((buffer[0][0] - 1.0_f32).abs() < 1e-6);
596    }
597
598    #[test]
599    fn test_chain_params_merge() {
600        let specs = <ChainParams<TestGainParams, TestGainParams>>::param_specs();
601        assert_eq!(specs.len(), 2); // Both gain params
602
603        // Both should have "Level" name
604        assert_eq!(specs[0].name, "Level");
605        assert_eq!(specs[1].name, "Level");
606    }
607
608    #[test]
609    fn test_chain_default() {
610        let _chain: Chain<TestGainDsp, TestPassthroughDsp> = Chain::default();
611        let _params: ChainParams<TestGainParams, TestPassthroughParams> = ChainParams::default();
612    }
613
614    #[test]
615    fn test_chain_from_param_defaults_uses_children_spec_defaults() {
616        let defaults = <ChainParams<TestGainParams, TestGainParams>>::from_param_defaults();
617        assert!((defaults.first.level - 1.0).abs() < 1e-6);
618        assert!((defaults.second.level - 1.0).abs() < 1e-6);
619    }
620
621    #[test]
622    fn test_chain_apply_plain_values_splits_by_child_param_count() {
623        let mut params = <ChainParams<TestGainParams, TestGainParams>>::from_param_defaults();
624        params.apply_plain_values(&[0.25, 1.75]);
625
626        assert!((params.first.level - 0.25).abs() < 1e-6);
627        assert!((params.second.level - 1.75).abs() < 1e-6);
628    }
629
630    #[test]
631    fn test_bypassed_apply_plain_values_uses_plain_value_count_without_param_specs() {
632        let mut params = <BypassedParams<PanicParamSpecsParams>>::from_param_defaults();
633        params.apply_plain_values(&[0.5, 1.0]);
634
635        assert!((params.inner.value - 0.5).abs() < 1e-6);
636        assert!(params.bypassed);
637    }
638
639    #[test]
640    fn test_chain_apply_plain_values_uses_plain_value_count_without_param_specs() {
641        let mut params =
642            <ChainParams<PanicParamSpecsParams, PanicParamSpecsParams>>::from_param_defaults();
643        params.apply_plain_values(&[0.25, 0.75]);
644
645        assert!((params.first.value - 0.25).abs() < 1e-6);
646        assert!((params.second.value - 0.75).abs() < 1e-6);
647    }
648
649    #[test]
650    fn test_chain_propagates_set_sample_rate_to_both_processors() {
651        let first = LifecycleProbe::new();
652        let second = LifecycleProbe::new();
653
654        let first_calls = Arc::clone(&first.set_sample_rate_calls);
655        let second_calls = Arc::clone(&second.set_sample_rate_calls);
656        let first_sr = Arc::clone(&first.last_sample_rate_bits);
657        let second_sr = Arc::clone(&second.last_sample_rate_bits);
658
659        let mut chain = Chain { first, second };
660        chain.set_sample_rate(48_000.0);
661
662        assert_eq!(first_calls.load(Ordering::SeqCst), 1);
663        assert_eq!(second_calls.load(Ordering::SeqCst), 1);
664        assert_eq!(f32::from_bits(first_sr.load(Ordering::SeqCst)), 48_000.0);
665        assert_eq!(f32::from_bits(second_sr.load(Ordering::SeqCst)), 48_000.0);
666    }
667
668    #[test]
669    fn test_chain_propagates_reset_to_both_processors() {
670        let first = LifecycleProbe::new();
671        let second = LifecycleProbe::new();
672
673        let first_resets = Arc::clone(&first.reset_calls);
674        let second_resets = Arc::clone(&second.reset_calls);
675
676        let mut chain = Chain { first, second };
677        chain.reset();
678
679        assert_eq!(first_resets.load(Ordering::SeqCst), 1);
680        assert_eq!(second_resets.load(Ordering::SeqCst), 1);
681    }
682
683    #[test]
684    fn test_bypassed_param_specs_include_bypass_flag() {
685        let specs = <BypassedParams<TestGainParams>>::param_specs();
686        assert_eq!(specs.len(), 2);
687        assert_eq!(specs[0].id_suffix, "level");
688        assert_eq!(specs[1].id_suffix, "bypass");
689    }
690
691    #[test]
692    fn test_bypassed_process_skips_child_when_bypassed_after_transition() {
693        let touched = Arc::new(AtomicBool::new(false));
694
695        struct TouchProbe {
696            touched: Arc<AtomicBool>,
697        }
698
699        impl Processor for TouchProbe {
700            type Params = TestParams;
701
702            fn process(
703                &mut self,
704                _buffer: &mut [&mut [f32]],
705                _transport: &Transport,
706                _params: &Self::Params,
707            ) {
708                self.touched.store(true, Ordering::SeqCst);
709            }
710        }
711
712        let mut wrapped = Bypassed::new(TouchProbe {
713            touched: Arc::clone(&touched),
714        });
715
716        let mut left = [1.0_f32, 2.0_f32];
717        let mut right = [3.0_f32, 4.0_f32];
718        let mut buffer = [&mut left[..], &mut right[..]];
719
720        // First call starts transition, subsequent calls settle bypass.
721        for _ in 0..140 {
722            wrapped.process(
723                &mut buffer,
724                &Transport::default(),
725                &BypassedParams {
726                    inner: TestParams,
727                    bypassed: true,
728                },
729            );
730        }
731
732        touched.store(false, Ordering::SeqCst);
733
734        wrapped.process(
735            &mut buffer,
736            &Transport::default(),
737            &BypassedParams {
738                inner: TestParams,
739                bypassed: true,
740            },
741        );
742
743        let mut verify_left = [1.0_f32, 2.0_f32];
744        let mut verify_right = [3.0_f32, 4.0_f32];
745        let original_left = verify_left;
746        let original_right = verify_right;
747        let mut verify_buffer = [&mut verify_left[..], &mut verify_right[..]];
748
749        wrapped.process(
750            &mut verify_buffer,
751            &Transport::default(),
752            &BypassedParams {
753                inner: TestParams,
754                bypassed: true,
755            },
756        );
757
758        assert!(!touched.load(Ordering::SeqCst));
759        assert_eq!(verify_left, original_left);
760        assert_eq!(verify_right, original_right);
761    }
762
763    #[test]
764    fn test_bypassed_process_runs_child_when_active() {
765        let mut wrapped = Bypassed::new(TestGainDsp);
766
767        let mut left = [1.0_f32, 1.0_f32];
768        let mut right = [1.0_f32, 1.0_f32];
769        let mut buffer = [&mut left[..], &mut right[..]];
770
771        wrapped.process(
772            &mut buffer,
773            &Transport::default(),
774            &BypassedParams {
775                inner: TestGainParams { level: 0.5 },
776                bypassed: false,
777            },
778        );
779
780        assert!((left[0] - 0.5_f32).abs() < 1e-6);
781        assert!((right[0] - 0.5_f32).abs() < 1e-6);
782    }
783
784    #[derive(Default)]
785    struct PolarityFlip;
786
787    impl Processor for PolarityFlip {
788        type Params = TestPassthroughParams;
789
790        fn process(
791            &mut self,
792            buffer: &mut [&mut [f32]],
793            _transport: &Transport,
794            _params: &Self::Params,
795        ) {
796            for channel in buffer.iter_mut() {
797                for sample in channel.iter_mut() {
798                    *sample = -*sample;
799                }
800            }
801        }
802    }
803
804    #[test]
805    fn test_bypassed_transition_smooths_toggle_edges() {
806        let mut wrapped = Bypassed::new(PolarityFlip);
807
808        let active = BypassedParams {
809            inner: TestPassthroughParams,
810            bypassed: false,
811        };
812        let bypassed = BypassedParams {
813            inner: TestPassthroughParams,
814            bypassed: true,
815        };
816
817        // Warm up active state: output should be wet (-1.0).
818        let mut previous = -1.0_f32;
819        for _ in 0..4 {
820            let mut sample = [1.0_f32];
821            let mut buffer = [&mut sample[..]];
822            wrapped.process(&mut buffer, &Transport::default(), &active);
823            previous = sample[0];
824        }
825        assert!(previous < -0.95);
826
827        // Toggle to bypassed and ensure output does not jump directly to +1.0.
828        let mut max_step = 0.0_f32;
829        for _ in 0..160 {
830            let mut sample = [1.0_f32];
831            let mut buffer = [&mut sample[..]];
832            wrapped.process(&mut buffer, &Transport::default(), &bypassed);
833
834            let step = (sample[0] - previous).abs();
835            max_step = max_step.max(step);
836            previous = sample[0];
837        }
838
839        // Bounded transition: no full-scale discontinuity in one sample.
840        assert!(max_step < 1.0);
841        // Settles to dry signal after transition.
842        assert!(previous > 0.95);
843    }
844
845    #[test]
846    fn test_bypassed_transition_is_bidirectional() {
847        let mut wrapped = Bypassed::new(PolarityFlip);
848
849        let active = BypassedParams {
850            inner: TestPassthroughParams,
851            bypassed: false,
852        };
853        let bypassed = BypassedParams {
854            inner: TestPassthroughParams,
855            bypassed: true,
856        };
857
858        // Move into stable bypassed state.
859        let mut sample_out = 0.0_f32;
860        for _ in 0..160 {
861            let mut sample = [1.0_f32];
862            let mut buffer = [&mut sample[..]];
863            wrapped.process(&mut buffer, &Transport::default(), &bypassed);
864            sample_out = sample[0];
865        }
866        assert!(sample_out > 0.95);
867
868        // Toggle back to active and ensure transition settles to wet (-1.0).
869        let mut max_step = 0.0_f32;
870        let mut previous = sample_out;
871        for _ in 0..160 {
872            let mut sample = [1.0_f32];
873            let mut buffer = [&mut sample[..]];
874            wrapped.process(&mut buffer, &Transport::default(), &active);
875
876            let step = (sample[0] - previous).abs();
877            max_step = max_step.max(step);
878            previous = sample[0];
879        }
880
881        assert!(max_step < 1.0);
882        assert!(previous < -0.95);
883    }
884}