Skip to main content

rill_core_model/wdf/
moog_ladder.rs

1use rill_core::math::vector::traits::Vector as VecTrait;
2use rill_core::traits::{Algorithm, AlgorithmCategory, AlgorithmMetadata, ProcessResult};
3use rill_core::Transcendental;
4
5// First-order WDF lowpass section.
6//
7// Models an RC pole in the wave digital domain.
8// The parameter `alpha` = π·fc/fs / (1 + π·fc/fs) controls the cutoff.
9//
10// This is functionally equivalent to `Series<Resistor, Capacitor>`
11// where the capacitor's scattering has been simplified into a single
12// coefficient. The compose macro (`wdf_compose!`) handles static
13// Series/Parallel networks correctly, but for filters with memory
14// (capacitors, inductors) the explicit scattering form is needed
15// for correct state tracking.
16crate::wdf_element! {
17    name: RcPole<T>,
18    params: { alpha: T },
19    state: { state: T },
20    port_resistance: |_s| T::ONE,
21    scattering: |s, a| {
22        let b = s.state + s.alpha * (a - s.state);
23        s.state = b + s.alpha * (a - b);
24        b
25    },
26    update: |_s| {},
27    reset: |s| { s.state = T::ZERO; },
28}
29
30// 4-pole Moog ladder filter with resonance feedback.
31crate::wdf_cascade! {
32    name: MoogLadder<T>,
33    section: RcPole<T>,
34    count: 4,
35    params: { cutoff: T, resonance: T, sample_rate: T },
36    state: { feedback_prev: T },
37    feedback: |s, input, fb_prev| {
38        let k = s.resonance * T::from_f32(4.0);
39        let fb = fb_prev * k;
40        input - fb.clamp(-T::ONE, T::ONE)
41    },
42    update: |s| {
43        let g = T::PI * s.cutoff / s.sample_rate;
44        let alpha = g / (T::ONE + g);
45        for p in &mut s.poles { p.alpha = alpha; }
46    },
47}
48
49impl<T: Transcendental> MoogLadder<T> {
50    /// Process 4 independent voices via `ScalarVector4`.
51    ///
52    /// Each lane of the input vector represents one voice; each lane
53    /// of the output vector is the corresponding filtered sample.
54    /// Reduces function-call overhead in polyphonic synthesis.
55    pub fn process_4_voices(
56        &mut self,
57        inputs: rill_core::math::vector::scalar::ScalarVector4<T>,
58    ) -> rill_core::math::vector::scalar::ScalarVector4<T> {
59        rill_core::math::vector::scalar::ScalarVector4::from_fn(|i| {
60            self.process_sample(inputs.extract(i))
61        })
62    }
63}
64
65impl<T: Transcendental> crate::WdfElement<T> for MoogLadder<T> {
66    fn port_resistance(&self) -> T {
67        T::ONE
68    }
69    fn process_incident(&mut self, a: T) -> T {
70        self.process_sample(a)
71    }
72    fn update_state(&mut self) {}
73    fn voltage(&self) -> T {
74        self.feedback_prev
75    }
76    fn current(&self) -> T {
77        T::ZERO
78    }
79    fn reset(&mut self) {
80        self.reset();
81    }
82}
83
84impl<T: Transcendental> Algorithm<T> for MoogLadder<T> {
85    fn init(&mut self, sample_rate: f32) {
86        self.sample_rate = T::from_f32(sample_rate);
87        self.update_coeffs();
88    }
89
90    fn reset(&mut self) {
91        self.reset();
92    }
93
94    fn process(&mut self, input: Option<&[T]>, output: &mut [T]) -> ProcessResult<()> {
95        let input = input.unwrap_or(&[]);
96        let len = input.len().min(output.len());
97        for i in 0..len {
98            output[i] = self.process_sample(input[i]);
99        }
100        Ok(())
101    }
102
103    fn metadata(&self) -> AlgorithmMetadata {
104        AlgorithmMetadata {
105            name: "WDF Moog Ladder Filter",
106            category: AlgorithmCategory::Filter,
107            description: "WDF-based 4-pole Moog transistor ladder VCF with resonance",
108            author: "Rill",
109            version: env!("CARGO_PKG_VERSION"),
110        }
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    fn make_filter(sample_rate: f64) -> MoogLadder<f64> {
119        let pole = RcPole::new(0.0);
120        let mut filter = MoogLadder::new(pole, 1000.0, 0.0, sample_rate);
121        filter.update_coeffs();
122        filter
123    }
124
125    #[test]
126    fn test_lp_section_dc() {
127        let fs = 44100.0_f64;
128        let fc = 1000.0_f64;
129        let g = core::f64::consts::PI * fc / fs;
130        let alpha = g / (1.0 + g);
131        let mut pole = RcPole::new(alpha);
132        let mut out = 0.0_f64;
133        for _ in 0..5000 {
134            out = crate::WdfElement::process_incident(&mut pole, 1.0);
135        }
136        assert!((out - 1.0).abs() < 0.01, "DC gain should approach 1.0");
137    }
138
139    #[test]
140    fn test_moog_ladder_creation() {
141        let filter = make_filter(44100.0);
142        assert!((filter.cutoff() - 1000.0).abs() < 1e-6);
143    }
144
145    #[test]
146    fn test_moog_ladder_dc() {
147        let mut filter = make_filter(44100.0);
148        filter.set_cutoff(100.0);
149        let mut out = 0.0_f64;
150        for _ in 0..5000 {
151            out = filter.process_sample(1.0_f64);
152        }
153        assert!((out - 1.0_f64).abs() < 0.01, "DC gain should be near 1.0");
154    }
155
156    #[test]
157    fn test_moog_ladder_cutoff_clamp() {
158        let mut filter = make_filter(44100.0);
159        filter.set_cutoff(10.0);
160        assert!((filter.cutoff() - 20.0).abs() < 1e-6);
161        filter.set_cutoff(50000.0);
162        assert!((filter.cutoff() - 22050.0).abs() < 1e-6);
163    }
164
165    #[test]
166    fn test_moog_ladder_algorithm_process() {
167        let mut filter = make_filter(44100.0);
168        filter.set_cutoff(100.0);
169        let input = vec![1.0f64; 64];
170        let mut output = vec![0.0f64; 64];
171        for _ in 0..500 {
172            filter.process(Some(&input), &mut output).unwrap();
173        }
174        for &o in &output {
175            assert!(o.is_finite());
176            assert!((o - 1.0).abs() < 0.05);
177        }
178    }
179
180    #[test]
181    fn test_moog_ladder_algorithm_reset() {
182        let mut filter = make_filter(44100.0);
183        let input = vec![1.0f64; 64];
184        let mut output = vec![0.0f64; 64];
185        filter.process(Some(&input), &mut output).unwrap();
186        filter.reset();
187        filter.process(Some(&input), &mut output).unwrap();
188        assert!(output[0] >= 0.0);
189    }
190}