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