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        // WORKAROUND FOR HOT-RELOAD HANG:
55        //
56        // Do NOT use OnceLock or any locking primitive here. On macOS, when the
57        // subprocess calls dlopen() → wavecraft_get_params_json() → param_specs(),
58        // initialization of OnceLock statics can hang indefinitely (30s timeout).
59        //
60        // Instead, we allocate and leak the merged specs on EVERY call. This is
61        // acceptable because:
62        // 1. param_specs() is called at most once per plugin load (startup only)
63        // 2. The leak is ~hundreds of bytes (not per-sample, not per-frame)
64        // 3. Plugin lifetime = process lifetime (no meaningful leak)
65        // 4. Hot-reload works correctly (no 30s hang)
66        //
67        // This is a pragmatic trade-off: small memory leak vs. broken hot-reload.
68        // Future work: investigate root cause of OnceLock hang on macOS dlopen.
69
70        let first_specs = PA::param_specs();
71        let second_specs = PB::param_specs();
72
73        let mut merged = Vec::with_capacity(first_specs.len() + second_specs.len());
74
75        // Add first processor's params
76        for spec in first_specs {
77            merged.push(ParamSpec {
78                name: spec.name,
79                id_suffix: spec.id_suffix,
80                range: spec.range.clone(),
81                default: spec.default,
82                unit: spec.unit,
83                group: spec.group,
84            });
85        }
86
87        // Add second processor's params
88        for spec in second_specs {
89            merged.push(ParamSpec {
90                name: spec.name,
91                id_suffix: spec.id_suffix,
92                range: spec.range.clone(),
93                default: spec.default,
94                unit: spec.unit,
95                group: spec.group,
96            });
97        }
98
99        // Leak to get 'static reference (intentional - see comment above)
100        Box::leak(merged.into_boxed_slice())
101    }
102
103    fn from_param_defaults() -> Self {
104        Self {
105            first: PA::from_param_defaults(),
106            second: PB::from_param_defaults(),
107        }
108    }
109
110    fn apply_plain_values(&mut self, values: &[f32]) {
111        let first_count = PA::param_specs().len();
112        let split_at = first_count.min(values.len());
113        let (first_values, second_values) = values.split_at(split_at);
114
115        self.first.apply_plain_values(first_values);
116        self.second.apply_plain_values(second_values);
117    }
118}
119
120impl<A, B> Processor for Chain<A, B>
121where
122    A: Processor,
123    B: Processor,
124{
125    type Params = ChainParams<A::Params, B::Params>;
126
127    fn process(&mut self, buffer: &mut [&mut [f32]], transport: &Transport, params: &Self::Params) {
128        // Process first, then second (serial chain)
129        self.first.process(buffer, transport, &params.first);
130        self.second.process(buffer, transport, &params.second);
131    }
132
133    fn set_sample_rate(&mut self, sample_rate: f32) {
134        self.first.set_sample_rate(sample_rate);
135        self.second.set_sample_rate(sample_rate);
136    }
137
138    fn reset(&mut self) {
139        self.first.reset();
140        self.second.reset();
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::builtins::{GainDsp, GainParams, PassthroughDsp, PassthroughParams};
148    use std::sync::{
149        Arc,
150        atomic::{AtomicBool, AtomicU32, Ordering},
151    };
152
153    #[derive(Clone)]
154    struct TestParams;
155
156    impl Default for TestParams {
157        fn default() -> Self {
158            Self
159        }
160    }
161
162    impl ProcessorParams for TestParams {
163        fn param_specs() -> &'static [ParamSpec] {
164            &[]
165        }
166    }
167
168    struct LifecycleProbe {
169        set_sample_rate_calls: Arc<AtomicU32>,
170        reset_calls: Arc<AtomicU32>,
171        last_sample_rate_bits: Arc<AtomicU32>,
172        touched_process: Arc<AtomicBool>,
173    }
174
175    impl LifecycleProbe {
176        fn new() -> Self {
177            Self {
178                set_sample_rate_calls: Arc::new(AtomicU32::new(0)),
179                reset_calls: Arc::new(AtomicU32::new(0)),
180                last_sample_rate_bits: Arc::new(AtomicU32::new(0)),
181                touched_process: Arc::new(AtomicBool::new(false)),
182            }
183        }
184    }
185
186    impl Processor for LifecycleProbe {
187        type Params = TestParams;
188
189        fn process(
190            &mut self,
191            _buffer: &mut [&mut [f32]],
192            _transport: &Transport,
193            _params: &Self::Params,
194        ) {
195            self.touched_process.store(true, Ordering::SeqCst);
196        }
197
198        fn set_sample_rate(&mut self, sample_rate: f32) {
199            self.set_sample_rate_calls.fetch_add(1, Ordering::SeqCst);
200            self.last_sample_rate_bits
201                .store(sample_rate.to_bits(), Ordering::SeqCst);
202        }
203
204        fn reset(&mut self) {
205            self.reset_calls.fetch_add(1, Ordering::SeqCst);
206        }
207    }
208
209    #[test]
210    fn test_chain_processes_in_order() {
211        let mut chain = Chain {
212            first: GainDsp::default(),
213            second: GainDsp::default(),
214        };
215
216        let mut left = [1.0_f32, 1.0_f32];
217        let mut right = [1.0_f32, 1.0_f32];
218        let mut buffer = [&mut left[..], &mut right[..]];
219
220        let transport = Transport::default();
221        let params = ChainParams {
222            first: GainParams { level: 0.5 },
223            second: GainParams { level: 2.0 },
224        };
225
226        chain.process(&mut buffer, &transport, &params);
227
228        // Expected: 1.0 * 0.5 * 2.0 = 1.0
229        assert!((buffer[0][0] - 1.0_f32).abs() < 1e-6);
230        assert!((buffer[1][0] - 1.0_f32).abs() < 1e-6);
231    }
232
233    #[test]
234    fn test_chain_with_passthrough() {
235        let mut chain = Chain {
236            first: PassthroughDsp,
237            second: GainDsp::default(),
238        };
239
240        let mut left = [2.0_f32, 2.0_f32];
241        let mut right = [2.0_f32, 2.0_f32];
242        let mut buffer = [&mut left[..], &mut right[..]];
243
244        let transport = Transport::default();
245        let params = ChainParams {
246            first: PassthroughParams,
247            second: GainParams { level: 0.5 },
248        };
249
250        chain.process(&mut buffer, &transport, &params);
251
252        // Expected: 2.0 * 1.0 * 0.5 = 1.0
253        assert!((buffer[0][0] - 1.0_f32).abs() < 1e-6);
254    }
255
256    #[test]
257    fn test_chain_params_merge() {
258        let specs = <ChainParams<GainParams, GainParams>>::param_specs();
259        assert_eq!(specs.len(), 2); // Both gain params
260
261        // Both should have "Level" name
262        assert_eq!(specs[0].name, "Level");
263        assert_eq!(specs[1].name, "Level");
264    }
265
266    #[test]
267    fn test_chain_default() {
268        let _chain: Chain<GainDsp, PassthroughDsp> = Chain::default();
269        let _params: ChainParams<GainParams, PassthroughParams> = ChainParams::default();
270    }
271
272    #[test]
273    fn test_chain_from_param_defaults_uses_children_spec_defaults() {
274        let defaults = <ChainParams<GainParams, GainParams>>::from_param_defaults();
275        assert!((defaults.first.level - 1.0).abs() < 1e-6);
276        assert!((defaults.second.level - 1.0).abs() < 1e-6);
277    }
278
279    #[test]
280    fn test_chain_apply_plain_values_splits_by_child_param_count() {
281        let mut params = <ChainParams<GainParams, GainParams>>::from_param_defaults();
282        params.apply_plain_values(&[0.25, 1.75]);
283
284        assert!((params.first.level - 0.25).abs() < 1e-6);
285        assert!((params.second.level - 1.75).abs() < 1e-6);
286    }
287
288    #[test]
289    fn test_chain_propagates_set_sample_rate_to_both_processors() {
290        let first = LifecycleProbe::new();
291        let second = LifecycleProbe::new();
292
293        let first_calls = Arc::clone(&first.set_sample_rate_calls);
294        let second_calls = Arc::clone(&second.set_sample_rate_calls);
295        let first_sr = Arc::clone(&first.last_sample_rate_bits);
296        let second_sr = Arc::clone(&second.last_sample_rate_bits);
297
298        let mut chain = Chain { first, second };
299        chain.set_sample_rate(48_000.0);
300
301        assert_eq!(first_calls.load(Ordering::SeqCst), 1);
302        assert_eq!(second_calls.load(Ordering::SeqCst), 1);
303        assert_eq!(f32::from_bits(first_sr.load(Ordering::SeqCst)), 48_000.0);
304        assert_eq!(f32::from_bits(second_sr.load(Ordering::SeqCst)), 48_000.0);
305    }
306
307    #[test]
308    fn test_chain_propagates_reset_to_both_processors() {
309        let first = LifecycleProbe::new();
310        let second = LifecycleProbe::new();
311
312        let first_resets = Arc::clone(&first.reset_calls);
313        let second_resets = Arc::clone(&second.reset_calls);
314
315        let mut chain = Chain { first, second };
316        chain.reset();
317
318        assert_eq!(first_resets.load(Ordering::SeqCst), 1);
319        assert_eq!(second_resets.load(Ordering::SeqCst), 1);
320    }
321}