Skip to main content

quiver/
introspection_impls.rs

1//! Module Introspection Implementations (Integration Layer)
2//!
3//! This module provides `ModuleIntrospection` implementations for all built-in modules,
4//! enabling GUIs to discover and control module parameters automatically.
5//!
6//! Most modules are fully CV-controlled and use the default empty implementation.
7//! Only modules with internal state parameters provide custom implementations.
8
9use alloc::vec;
10use alloc::vec::Vec;
11
12use crate::introspection::{ControlType, ModuleIntrospection, ParamCurve, ParamInfo, ValueFormat};
13
14use crate::analog::{AnalogVco, Saturator, Wavefolder};
15use crate::modules::{
16    Adsr, Arpeggiator, Attenuverter, BernoulliGate, Bitcrusher, ChordMemory, Chorus, Clock,
17    Comparator, Compressor, Crossfader, Crosstalk, DelayLine, DiodeLadderFilter, Distortion,
18    Ducker, EnvelopeFollower, Euclidean, Flanger, FormantOsc, Granular, GroundLoop, KarplusStrong,
19    Lfo, Limiter, LogicAnd, LogicNot, LogicOr, LogicXor, Max, MidSideDecode, MidSideEncode, Min,
20    Mixer, Multiple, NoiseGate, NoiseGenerator, Offset, Oversample, ParametricEq, Phaser,
21    PitchShifter, PrecisionAdder, Quantizer, Rectifier, Reverb, RingModulator, SampleAndHold,
22    SamplePlayer, Scale, ScaleQuantizer, SlewLimiter, StepSequencer, StereoOutput, Supersaw, Svf,
23    Tremolo, UnitDelay, VcSwitch, Vca, Vco, Vibrato, Vocoder, Wavetable,
24};
25
26// =============================================================================
27// CV-Controlled Modules (use default empty implementation)
28// =============================================================================
29
30// Oscillators
31impl ModuleIntrospection for Vco {}
32impl ModuleIntrospection for Lfo {}
33impl ModuleIntrospection for AnalogVco {}
34
35// Filters
36impl ModuleIntrospection for Svf {}
37impl ModuleIntrospection for DiodeLadderFilter {}
38
39// Envelopes & Amplifiers
40impl ModuleIntrospection for Adsr {}
41impl ModuleIntrospection for Vca {}
42
43// Utilities (CV-controlled)
44impl ModuleIntrospection for Mixer {}
45impl ModuleIntrospection for UnitDelay {}
46impl ModuleIntrospection for Attenuverter {}
47impl ModuleIntrospection for Multiple {}
48impl ModuleIntrospection for SlewLimiter {}
49impl ModuleIntrospection for SampleAndHold {}
50impl ModuleIntrospection for PrecisionAdder {}
51impl ModuleIntrospection for VcSwitch {}
52impl ModuleIntrospection for Min {}
53impl ModuleIntrospection for Max {}
54impl ModuleIntrospection for Crossfader {}
55impl ModuleIntrospection for MidSideEncode {}
56impl ModuleIntrospection for MidSideDecode {}
57
58// Effects (CV-controlled)
59impl ModuleIntrospection for RingModulator {}
60impl ModuleIntrospection for Rectifier {}
61impl ModuleIntrospection for Crosstalk {}
62
63// Logic & Random
64impl ModuleIntrospection for LogicAnd {}
65impl ModuleIntrospection for LogicOr {}
66impl ModuleIntrospection for LogicXor {}
67impl ModuleIntrospection for LogicNot {}
68impl ModuleIntrospection for Comparator {}
69impl ModuleIntrospection for BernoulliGate {}
70
71// Sequencing & I/O
72impl ModuleIntrospection for Clock {}
73impl ModuleIntrospection for StereoOutput {}
74impl ModuleIntrospection for Arpeggiator {}
75
76// Phase 4: Advanced DSP Modules (all CV-controlled)
77impl ModuleIntrospection for ChordMemory {}
78impl ModuleIntrospection for ParametricEq {}
79impl ModuleIntrospection for Wavetable {}
80impl ModuleIntrospection for FormantOsc {}
81impl ModuleIntrospection for PitchShifter {}
82impl ModuleIntrospection for Reverb {}
83impl ModuleIntrospection for Vocoder {}
84impl ModuleIntrospection for Granular {}
85
86// Effects & dynamics whose controllable quantities are all input ports (rate, depth, mix,
87// threshold, drive, …). They expose no non-port internal state, so the default empty impl
88// is correct: their parameters are discovered and driven through the port system, and the
89// `Patch` synthesizes `ParamInfo`s for those ports (see `Patch::param_infos`).
90impl ModuleIntrospection for Bitcrusher {}
91impl ModuleIntrospection for Chorus {}
92impl ModuleIntrospection for Compressor {}
93impl ModuleIntrospection for DelayLine {}
94impl ModuleIntrospection for EnvelopeFollower {}
95impl ModuleIntrospection for Euclidean {}
96impl ModuleIntrospection for Flanger {}
97impl ModuleIntrospection for KarplusStrong {}
98impl ModuleIntrospection for Limiter {}
99impl ModuleIntrospection for NoiseGate {}
100impl ModuleIntrospection for Phaser {}
101impl ModuleIntrospection for Supersaw {}
102impl ModuleIntrospection for Tremolo {}
103impl ModuleIntrospection for Vibrato {}
104// ScaleQuantizer's `root`/`scale` selection are input ports; its only genuinely internal
105// state is an optional custom-scale table (a `&[cents]` list, not a scalar value), which is
106// intentionally excluded from the value-typed parameter surface.
107impl ModuleIntrospection for ScaleQuantizer {}
108
109/// Map an [`Oversample`] factor (1/2/4) to a select index (0/1/2) and back. Shared by the
110/// waveshaping modules whose only non-port parameter is their opt-in oversampling factor.
111fn oversample_to_index(factor: usize) -> f64 {
112    match factor {
113        4 => 2.0,
114        2 => 1.0,
115        _ => 0.0,
116    }
117}
118
119fn oversample_from_index(value: f64) -> Oversample {
120    // `libm::round` rather than `f64::round` so this compiles on `no_std + alloc` (the
121    // introspection tier is alloc-gated, not std-gated).
122    match libm::round(value) as i64 {
123        2 => Oversample::X4,
124        1 => Oversample::X2,
125        _ => Oversample::Off,
126    }
127}
128
129impl ModuleIntrospection for Distortion {
130    fn param_infos(&self) -> Vec<ParamInfo> {
131        vec![ParamInfo::select("oversample", "Oversampling", 3)
132            .with_value(oversample_to_index(self.oversample_factor()))]
133    }
134
135    fn set_param_by_id(&mut self, id: &str, value: f64) -> bool {
136        match id {
137            "oversample" => {
138                self.set_oversample(oversample_from_index(value));
139                true
140            }
141            _ => false,
142        }
143    }
144}
145
146// =============================================================================
147// Modules with Parameters
148// =============================================================================
149
150impl ModuleIntrospection for Offset {
151    fn param_infos(&self) -> Vec<ParamInfo> {
152        vec![ParamInfo::new("offset", "Offset")
153            .with_range(-10.0, 10.0)
154            .with_default(0.0)
155            .with_value(self.offset)
156            .with_curve(ParamCurve::Linear)
157            .with_control(ControlType::Knob)
158            .with_unit("V")
159            .with_format(ValueFormat::Decimal { places: 2 })]
160    }
161
162    fn set_param_by_id(&mut self, id: &str, value: f64) -> bool {
163        match id {
164            "offset" => {
165                self.set_offset(value);
166                true
167            }
168            _ => false,
169        }
170    }
171}
172
173impl ModuleIntrospection for NoiseGenerator {
174    fn param_infos(&self) -> Vec<ParamInfo> {
175        vec![ParamInfo::percent("correlation", "Stereo Correlation")
176            .with_default(0.3)
177            .with_value(self.correlation)]
178    }
179
180    fn set_param_by_id(&mut self, id: &str, value: f64) -> bool {
181        match id {
182            "correlation" => {
183                self.correlation = value.clamp(0.0, 1.0);
184                true
185            }
186            _ => false,
187        }
188    }
189}
190
191impl ModuleIntrospection for StepSequencer {
192    fn param_infos(&self) -> Vec<ParamInfo> {
193        let mut params = Vec::with_capacity(16);
194
195        for i in 0..8 {
196            if let Some((voltage, _gate)) = self.get_step(i) {
197                params.push(
198                    ParamInfo::new(
199                        alloc::format!("step_{}_cv", i),
200                        alloc::format!("Step {} CV", i + 1),
201                    )
202                    .with_range(-5.0, 5.0)
203                    .with_default(0.0)
204                    .with_value(voltage)
205                    .with_curve(ParamCurve::Linear)
206                    .with_control(ControlType::Slider)
207                    .with_unit("V")
208                    .with_format(ValueFormat::NoteName),
209                );
210            }
211        }
212
213        for i in 0..8 {
214            if let Some((_voltage, gate)) = self.get_step(i) {
215                params.push(
216                    ParamInfo::toggle(
217                        alloc::format!("step_{}_gate", i),
218                        alloc::format!("Step {} Gate", i + 1),
219                    )
220                    // Gates default ON to match the constructor (`gates: [true; 8]`). Without
221                    // this the `ParamInfo::toggle` default of 0.0 would equal a legitimately
222                    // OFF gate, so `to_def`'s "differs from default" filter would silently drop
223                    // it and the gate would spring back ON on reload.
224                    .with_default(1.0)
225                    .with_value(if gate { 1.0 } else { 0.0 }),
226                );
227            }
228        }
229
230        params
231    }
232
233    fn set_param_by_id(&mut self, id: &str, value: f64) -> bool {
234        if let Some(rest) = id.strip_prefix("step_") {
235            if let Some((num_str, param_type)) = rest.split_once('_') {
236                if let Ok(step_idx) = num_str.parse::<usize>() {
237                    if step_idx < 8 {
238                        if let Some((current_cv, current_gate)) = self.get_step(step_idx) {
239                            match param_type {
240                                "cv" => {
241                                    self.set_step(step_idx, value, current_gate);
242                                    return true;
243                                }
244                                "gate" => {
245                                    self.set_step(step_idx, current_cv, value > 0.5);
246                                    return true;
247                                }
248                                _ => {}
249                            }
250                        }
251                    }
252                }
253            }
254        }
255        false
256    }
257}
258
259impl ModuleIntrospection for Quantizer {
260    fn param_infos(&self) -> Vec<ParamInfo> {
261        let scale_value = match self.scale {
262            Scale::Chromatic => 0.0,
263            Scale::Major => 1.0,
264            Scale::Minor => 2.0,
265            Scale::PentatonicMajor => 3.0,
266            Scale::PentatonicMinor => 4.0,
267            Scale::Dorian => 5.0,
268            Scale::Mixolydian => 6.0,
269            Scale::Blues => 7.0,
270        };
271        vec![ParamInfo::select("scale", "Scale", 8).with_value(scale_value)]
272    }
273
274    fn set_param_by_id(&mut self, id: &str, value: f64) -> bool {
275        match id {
276            "scale" => {
277                let scale = match value as u8 {
278                    0 => Scale::Chromatic,
279                    1 => Scale::Major,
280                    2 => Scale::Minor,
281                    3 => Scale::PentatonicMajor,
282                    4 => Scale::PentatonicMinor,
283                    5 => Scale::Dorian,
284                    6 => Scale::Mixolydian,
285                    7 => Scale::Blues,
286                    _ => return false,
287                };
288                self.set_scale(scale);
289                true
290            }
291            _ => false,
292        }
293    }
294}
295
296impl ModuleIntrospection for GroundLoop {
297    fn param_infos(&self) -> Vec<ParamInfo> {
298        vec![ParamInfo::select("frequency", "Mains Frequency", 2)
299            .with_default(1.0)
300            .with_value(if self.frequency == 60.0 { 1.0 } else { 0.0 })]
301    }
302
303    fn set_param_by_id(&mut self, id: &str, value: f64) -> bool {
304        match id {
305            "frequency" => {
306                self.frequency = if value > 0.5 { 60.0 } else { 50.0 };
307                true
308            }
309            _ => false,
310        }
311    }
312}
313
314impl ModuleIntrospection for Saturator {
315    fn param_infos(&self) -> Vec<ParamInfo> {
316        vec![ParamInfo::new("drive", "Drive")
317            .with_range(1.0, 10.0)
318            .with_default(1.0)
319            .with_value(self.drive)
320            .with_curve(ParamCurve::Exponential)
321            .with_control(ControlType::Knob)
322            .with_format(ValueFormat::Ratio)]
323    }
324
325    fn set_param_by_id(&mut self, id: &str, value: f64) -> bool {
326        match id {
327            "drive" => {
328                self.drive = value.clamp(1.0, 10.0);
329                true
330            }
331            _ => false,
332        }
333    }
334}
335
336impl ModuleIntrospection for Wavefolder {
337    fn param_infos(&self) -> Vec<ParamInfo> {
338        vec![
339            ParamInfo::new("threshold", "Fold Threshold")
340                .with_range(0.1, 5.0)
341                .with_default(1.0)
342                .with_value(self.threshold)
343                .with_curve(ParamCurve::Exponential)
344                .with_control(ControlType::Knob)
345                .with_unit("V")
346                .with_format(ValueFormat::Decimal { places: 2 }),
347            ParamInfo::select("oversample", "Oversampling", 3)
348                .with_value(oversample_to_index(self.oversample_factor())),
349        ]
350    }
351
352    fn set_param_by_id(&mut self, id: &str, value: f64) -> bool {
353        match id {
354            "threshold" => {
355                self.threshold = value.clamp(0.1, 5.0);
356                true
357            }
358            "oversample" => {
359                self.set_oversample(oversample_from_index(value));
360                true
361            }
362            _ => false,
363        }
364    }
365}
366
367impl ModuleIntrospection for SamplePlayer {
368    fn param_infos(&self) -> Vec<ParamInfo> {
369        vec![ParamInfo::percent("start", "Start Position")
370            .with_default(0.0)
371            .with_value(self.start_position())]
372    }
373
374    fn set_param_by_id(&mut self, id: &str, value: f64) -> bool {
375        match id {
376            "start" => {
377                self.set_start(value);
378                true
379            }
380            _ => false,
381        }
382    }
383}
384
385impl ModuleIntrospection for Ducker {
386    fn param_infos(&self) -> Vec<ParamInfo> {
387        // These knob ids deliberately differ from the CV input ports "amount"/"threshold"
388        // (dynamics.rs). A colliding id would be dropped by `Patch::param_infos`' shadow
389        // filter (port wins) and `set_param_by_id` would route to the CV-port base (default
390        // 0.0) instead of the depth/threshold knobs (defaults 1.0/0.2), leaving the primary
391        // controls invisible to GUIs and un-serializable. `depth`/`thresh` are the base knobs;
392        // the same-named ports remain the (bipolar CV) modulation inputs.
393        vec![
394            ParamInfo::percent("depth", "Duck Amount")
395                .with_default(1.0)
396                .with_value(self.amount()),
397            ParamInfo::percent("thresh", "Threshold")
398                .with_default(0.2)
399                .with_value(self.threshold()),
400        ]
401    }
402
403    fn set_param_by_id(&mut self, id: &str, value: f64) -> bool {
404        match id {
405            "depth" => {
406                self.set_amount(value);
407                true
408            }
409            "thresh" => {
410                self.set_threshold(value);
411                true
412            }
413            _ => false,
414        }
415    }
416}
417
418// =============================================================================
419// Tests
420// =============================================================================
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425
426    #[test]
427    fn test_offset_introspection() {
428        let mut offset = Offset::new(2.5);
429        let params = offset.param_infos();
430        assert_eq!(params.len(), 1);
431        assert_eq!(params[0].id, "offset");
432        assert_eq!(params[0].value, 2.5);
433
434        assert!(offset.set_param_by_id("offset", -3.0));
435        assert_eq!(offset.param_infos()[0].value, -3.0);
436        assert!(!offset.set_param_by_id("invalid", 0.0));
437    }
438
439    #[test]
440    fn test_step_sequencer_introspection() {
441        let mut seq = StepSequencer::new();
442        seq.set_step(0, 1.0, true);
443        seq.set_step(1, -0.5, false);
444
445        let params = seq.param_infos();
446        assert_eq!(params.len(), 16);
447
448        let step0_cv = params.iter().find(|p| p.id == "step_0_cv").unwrap();
449        assert_eq!(step0_cv.value, 1.0);
450
451        let step0_gate = params.iter().find(|p| p.id == "step_0_gate").unwrap();
452        assert_eq!(step0_gate.value, 1.0);
453
454        assert!(seq.set_param_by_id("step_2_cv", 2.5));
455        assert_eq!(seq.get_step(2).unwrap().0, 2.5);
456    }
457
458    #[test]
459    fn test_quantizer_introspection() {
460        let mut quant = Quantizer::major();
461        assert_eq!(quant.param_infos()[0].value, 1.0);
462
463        assert!(quant.set_param_by_id("scale", 2.0));
464        assert_eq!(quant.param_infos()[0].value, 2.0);
465    }
466
467    #[test]
468    fn test_noise_generator_introspection() {
469        let mut noise = NoiseGenerator::with_correlation(0.5);
470        assert!((noise.param_infos()[0].value - 0.5).abs() < 0.001);
471
472        assert!(noise.set_param_by_id("correlation", 0.8));
473        assert!((noise.param_infos()[0].value - 0.8).abs() < 0.001);
474    }
475
476    #[test]
477    fn test_saturator_introspection() {
478        let mut sat = Saturator::new(1.0);
479        assert_eq!(sat.param_infos()[0].id, "drive");
480
481        assert!(sat.set_param_by_id("drive", 5.0));
482        assert_eq!(sat.param_infos()[0].value, 5.0);
483    }
484
485    #[test]
486    fn test_wavefolder_introspection() {
487        let mut wf = Wavefolder::new(1.0);
488        assert_eq!(wf.param_infos()[0].id, "threshold");
489
490        assert!(wf.set_param_by_id("threshold", 2.0));
491        assert_eq!(wf.param_infos()[0].value, 2.0);
492    }
493
494    #[test]
495    fn test_ground_loop_introspection() {
496        let mut gl = GroundLoop::hz_50(44100.0);
497        assert_eq!(gl.param_infos()[0].value, 0.0);
498
499        assert!(gl.set_param_by_id("frequency", 1.0));
500        assert_eq!(gl.param_infos()[0].value, 1.0);
501    }
502
503    #[test]
504    fn test_cv_controlled_modules_have_no_params() {
505        assert!(Vco::default().param_infos().is_empty());
506        assert!(Lfo::default().param_infos().is_empty());
507        assert!(Svf::default().param_infos().is_empty());
508        assert!(Adsr::default().param_infos().is_empty());
509        assert!(Vca::default().param_infos().is_empty());
510        assert!(Clock::default().param_infos().is_empty());
511        assert!(LogicAnd::default().param_infos().is_empty());
512        // Mid/side utilities are fully port/CV-controlled.
513        assert!(MidSideEncode::default().param_infos().is_empty());
514        assert!(MidSideDecode::default().param_infos().is_empty());
515    }
516
517    #[test]
518    fn test_sample_player_introspection() {
519        let mut player = SamplePlayer::default();
520        let params = player.param_infos();
521        assert_eq!(params.len(), 1);
522        assert_eq!(params[0].id, "start");
523
524        assert!(player.set_param_by_id("start", 0.5));
525        assert!((player.param_infos()[0].value - 0.5).abs() < 1e-9);
526        assert!(!player.set_param_by_id("invalid", 0.0));
527    }
528
529    #[test]
530    fn test_ducker_introspection() {
531        let mut ducker = Ducker::default();
532        let params = ducker.param_infos();
533        assert_eq!(params.len(), 2);
534        // Knob ids are distinct from the same-named CV ports (see impl comment).
535        assert_eq!(params[0].id, "depth");
536        assert_eq!(params[1].id, "thresh");
537
538        assert!(ducker.set_param_by_id("depth", 0.25));
539        assert!(ducker.set_param_by_id("thresh", 0.6));
540        let p = ducker.param_infos();
541        assert!((p[0].value - 0.25).abs() < 1e-9);
542        assert!((p[1].value - 0.6).abs() < 1e-9);
543        assert!(!ducker.set_param_by_id("invalid", 0.0));
544    }
545}