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/// Combines two processors in series: A → B.
6///
7/// Audio flows through processor A, then through processor B.
8/// Parameters from both processors are merged.
9pub struct Chain<A, B> {
10    pub first: A,
11    pub second: B,
12}
13
14impl<A, B> Default for Chain<A, B>
15where
16    A: Default,
17    B: Default,
18{
19    fn default() -> Self {
20        Self {
21            first: A::default(),
22            second: B::default(),
23        }
24    }
25}
26
27/// Combined parameters for chained processors.
28///
29/// Merges parameter specs from both processors, prefixing IDs to avoid collisions.
30pub struct ChainParams<PA, PB> {
31    pub first: PA,
32    pub second: PB,
33}
34
35impl<PA, PB> Default for ChainParams<PA, PB>
36where
37    PA: Default,
38    PB: Default,
39{
40    fn default() -> Self {
41        Self {
42            first: PA::default(),
43            second: PB::default(),
44        }
45    }
46}
47
48impl<PA, PB> ProcessorParams for ChainParams<PA, PB>
49where
50    PA: ProcessorParams,
51    PB: ProcessorParams,
52{
53    fn param_specs() -> &'static [ParamSpec] {
54        // Allocate merged specs at initialization time (not audio thread)
55        use std::sync::OnceLock;
56
57        static MERGED_SPECS: OnceLock<Vec<ParamSpec>> = OnceLock::new();
58
59        MERGED_SPECS.get_or_init(|| {
60            let first_specs = PA::param_specs();
61            let second_specs = PB::param_specs();
62
63            let mut merged = Vec::with_capacity(first_specs.len() + second_specs.len());
64
65            // Add first processor's params with "a_" prefix
66            for spec in first_specs {
67                merged.push(ParamSpec {
68                    name: spec.name,
69                    id_suffix: spec.id_suffix, // Keep original suffix for now
70                    range: spec.range.clone(),
71                    default: spec.default,
72                    unit: spec.unit,
73                    group: spec.group,
74                });
75            }
76
77            // Add second processor's params with "b_" prefix
78            for spec in second_specs {
79                merged.push(ParamSpec {
80                    name: spec.name,
81                    id_suffix: spec.id_suffix, // Keep original suffix for now
82                    range: spec.range.clone(),
83                    default: spec.default,
84                    unit: spec.unit,
85                    group: spec.group,
86                });
87            }
88
89            merged
90        })
91    }
92}
93
94impl<A, B> Processor for Chain<A, B>
95where
96    A: Processor,
97    B: Processor,
98{
99    type Params = ChainParams<A::Params, B::Params>;
100
101    fn process(&mut self, buffer: &mut [&mut [f32]], transport: &Transport, params: &Self::Params) {
102        // Process first, then second (serial chain)
103        self.first.process(buffer, transport, &params.first);
104        self.second.process(buffer, transport, &params.second);
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::builtins::{GainDsp, GainParams, PassthroughDsp, PassthroughParams};
112
113    #[test]
114    fn test_chain_processes_in_order() {
115        let mut chain = Chain {
116            first: GainDsp::default(),
117            second: GainDsp::default(),
118        };
119
120        let mut left = [1.0_f32, 1.0_f32];
121        let mut right = [1.0_f32, 1.0_f32];
122        let mut buffer = [&mut left[..], &mut right[..]];
123
124        let transport = Transport::default();
125        let params = ChainParams {
126            first: GainParams { level: 0.5 },
127            second: GainParams { level: 2.0 },
128        };
129
130        chain.process(&mut buffer, &transport, &params);
131
132        // Expected: 1.0 * 0.5 * 2.0 = 1.0
133        assert!((buffer[0][0] - 1.0_f32).abs() < 1e-6);
134        assert!((buffer[1][0] - 1.0_f32).abs() < 1e-6);
135    }
136
137    #[test]
138    fn test_chain_with_passthrough() {
139        let mut chain = Chain {
140            first: PassthroughDsp,
141            second: GainDsp::default(),
142        };
143
144        let mut left = [2.0_f32, 2.0_f32];
145        let mut right = [2.0_f32, 2.0_f32];
146        let mut buffer = [&mut left[..], &mut right[..]];
147
148        let transport = Transport::default();
149        let params = ChainParams {
150            first: PassthroughParams,
151            second: GainParams { level: 0.5 },
152        };
153
154        chain.process(&mut buffer, &transport, &params);
155
156        // Expected: 2.0 * 1.0 * 0.5 = 1.0
157        assert!((buffer[0][0] - 1.0_f32).abs() < 1e-6);
158    }
159
160    #[test]
161    fn test_chain_params_merge() {
162        let specs = <ChainParams<GainParams, GainParams>>::param_specs();
163        assert_eq!(specs.len(), 2); // Both gain params
164
165        // Both should have "Level" name
166        assert_eq!(specs[0].name, "Level");
167        assert_eq!(specs[1].name, "Level");
168    }
169
170    #[test]
171    fn test_chain_default() {
172        let _chain: Chain<GainDsp, PassthroughDsp> = Chain::default();
173        let _params: ChainParams<GainParams, PassthroughParams> = ChainParams::default();
174    }
175}