Skip to main content

rill_core_model/modal/
mod.rs

1//! Modal resonator — parallel bank of 2-pole filters.
2//!
3//! Implements modal synthesis: an object is modeled as a sum of N
4//! exponentially decaying sinusoidal modes, each represented by a
5//! 2-pole resonator `H(z) = 1 / (1 - 2r·cos(ω)·z⁻¹ + r²·z⁻²)`.
6//! Excitation is an impulse at the fundamental frequency.
7
8mod params;
9
10pub use params::{bell_modes, marimba_modes, ModalParams, ModeParams};
11
12use rill_core::traits::algorithm::{
13    Algorithm, AlgorithmCategory, AlgorithmMetadata, ParameterizedAlgorithm,
14};
15use rill_core::traits::ParamValue;
16use rill_core::Transcendental;
17
18/// Internal state for one resonant mode.
19#[derive(Debug, Clone, Copy)]
20struct ModeState<T: Transcendental> {
21    prev_out: T,
22    prev_prev_out: T,
23    r: T,
24    cos_omega: T,
25    amplitude: T,
26}
27
28impl<T: Transcendental> Default for ModeState<T> {
29    fn default() -> Self {
30        Self {
31            prev_out: T::ZERO,
32            prev_prev_out: T::ZERO,
33            r: T::from_f32(0.99),
34            cos_omega: T::ONE,
35            amplitude: T::ZERO,
36        }
37    }
38}
39
40/// Modal resonator — parallel resonant filter bank.
41///
42/// Pre-allocates `MAX_MODES` mode states at construction. The `process()`
43/// method evaluates all active modes in parallel — RT-safe, no allocation.
44#[derive(Debug, Clone)]
45pub struct ModalModel<T: Transcendental, const MAX_MODES: usize> {
46    params: ModalParams<T, MAX_MODES>,
47    mode_states: [ModeState<T>; MAX_MODES],
48    excitation: T,
49    sample_rate: f32,
50}
51
52impl<T: Transcendental, const MAX_MODES: usize> ModalModel<T, MAX_MODES> {
53    /// Create a modal model with the given parameters.
54    pub fn new(params: ModalParams<T, MAX_MODES>, sample_rate: f32) -> Self {
55        let mut model = Self {
56            params,
57            mode_states: [ModeState::default(); MAX_MODES],
58            excitation: T::ZERO,
59            sample_rate,
60        };
61        model.recompute_coeffs();
62        model
63    }
64
65    /// Excite the resonator (strike, pluck, hammer).
66    pub fn strike(&mut self, strength: T) {
67        self.excitation = strength;
68    }
69
70    fn recompute_coeffs(&mut self) {
71        let sr = T::from_f32(self.sample_rate);
72        let two_pi = T::from_f32(2.0 * std::f32::consts::PI);
73        for i in 0..self.params.num_modes.min(MAX_MODES) {
74            let mode = &self.params.modes[i];
75            let freq = mode.freq_ratio * self.params.fundamental;
76            let decay = mode.decay_time * self.params.damping;
77            let omega = two_pi * freq / sr;
78            let r = if decay > T::ZERO {
79                (-T::ONE / (decay * sr)).exp()
80            } else {
81                T::from_f32(0.999)
82            };
83            self.mode_states[i] = ModeState {
84                r,
85                cos_omega: omega.cos(),
86                amplitude: mode.amplitude,
87                ..self.mode_states[i]
88            };
89        }
90    }
91
92    fn process_sample(&mut self, _input: T) -> T {
93        if self.sample_rate == 0.0 {
94            return T::ZERO;
95        }
96        let mut output = T::ZERO;
97        let active = self.params.num_modes.min(MAX_MODES);
98        for i in 0..active {
99            let state = &mut self.mode_states[i];
100            let two_r_cos = T::from_f32(2.0) * state.r * state.cos_omega;
101            let r2 = state.r * state.r;
102            let y = self.excitation * state.amplitude + two_r_cos * state.prev_out
103                - r2 * state.prev_prev_out;
104            output += y;
105            state.prev_prev_out = state.prev_out;
106            state.prev_out = y;
107        }
108        self.excitation *= T::from_f32(0.99);
109        output
110    }
111}
112
113impl<T: Transcendental, const MAX_MODES: usize> Algorithm<T> for ModalModel<T, MAX_MODES> {
114    fn process(
115        &mut self,
116        input: Option<&[T]>,
117        output: &mut [T],
118    ) -> rill_core::traits::ProcessResult<()> {
119        for (i, out) in output.iter_mut().enumerate() {
120            let inp = input
121                .map(|s| s.get(i).copied().unwrap_or(T::ZERO))
122                .unwrap_or(T::ZERO);
123            *out = self.process_sample(inp);
124        }
125        Ok(())
126    }
127
128    fn reset(&mut self) {
129        self.mode_states = [ModeState::default(); MAX_MODES];
130        self.excitation = T::ZERO;
131        self.recompute_coeffs();
132    }
133
134    fn init(&mut self, sample_rate: f32) {
135        self.sample_rate = sample_rate;
136        self.recompute_coeffs();
137    }
138
139    fn metadata(&self) -> AlgorithmMetadata {
140        AlgorithmMetadata {
141            name: "Modal Resonator",
142            category: AlgorithmCategory::Generator,
143            description: "Parallel resonant filter bank for modal synthesis",
144            author: "Rill",
145            version: "0.5",
146        }
147    }
148}
149
150impl<T: Transcendental, const MAX_MODES: usize> ParameterizedAlgorithm<T>
151    for ModalModel<T, MAX_MODES>
152{
153    type Params = ModalParams<T, MAX_MODES>;
154
155    fn params(&self) -> &Self::Params {
156        &self.params
157    }
158
159    fn set_params(&mut self, params: Self::Params) {
160        self.params = params;
161        self.recompute_coeffs();
162    }
163
164    fn set_parameter(&mut self, name: &str, value: ParamValue) -> Result<(), &'static str> {
165        match name {
166            "fundamental" => {
167                let mut p = self.params.clone();
168                p.fundamental = T::from_f32(value.as_f32().unwrap_or(440.0));
169                self.set_params(p);
170                Ok(())
171            }
172            "damping" => {
173                let mut p = self.params.clone();
174                p.damping = T::from_f32(value.as_f32().unwrap_or(1.0));
175                self.set_params(p);
176                Ok(())
177            }
178            _ => Err("Unknown parameter"),
179        }
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn test_modal_creation() {
189        let params = ModalParams::<f64, 8>::default();
190        let model = ModalModel::<f64, 8>::new(params, 44100.0);
191        assert_eq!(model.params.num_modes, 1);
192    }
193
194    #[test]
195    fn test_modal_algorithm_process() {
196        let params = ModalParams::<f64, 8>::default();
197        let mut model = ModalModel::<f64, 8>::new(params, 44100.0);
198        model.strike(1.0.into());
199        let mut output = [0.0f64; 64];
200        model.process(None, &mut output).unwrap();
201        let max_abs = output.iter().map(|x| x.abs()).fold(0.0, f64::max);
202        assert!(max_abs > 0.0);
203    }
204
205    #[test]
206    fn test_modal_decay() {
207        let params = ModalParams::<f64, 8> {
208            modes: {
209                let arr = [ModeParams {
210                    freq_ratio: 1.0.into(),
211                    amplitude: 1.0.into(),
212                    decay_time: 0.002.into(),
213                }; 8];
214                arr
215            },
216            num_modes: 1,
217            fundamental: 440.0.into(),
218            damping: 1.0.into(),
219        };
220        let mut model = ModalModel::<f64, 8>::new(params, 44100.0);
221        model.strike(1.0.into());
222        let mut blocks = Vec::new();
223        for _ in 0..10 {
224            let mut out = [0.0f64; 64];
225            model.process(None, &mut out).unwrap();
226            blocks.push(out.iter().map(|x| x.abs()).fold(0.0, f64::max));
227        }
228        assert!(blocks[9] < blocks[0] * 0.1);
229    }
230
231    #[test]
232    fn test_bell_modes() {
233        let params: ModalParams<f64, 8> = bell_modes();
234        let model = ModalModel::<f64, 8>::new(params, 44100.0);
235        assert_eq!(model.params.num_modes, 5);
236    }
237
238    #[test]
239    fn test_marimba_modes() {
240        let params: ModalParams<f64, 8> = marimba_modes();
241        let model = ModalModel::<f64, 8>::new(params, 44100.0);
242        assert_eq!(model.params.num_modes, 3);
243    }
244
245    #[test]
246    fn test_modal_params() {
247        let params = ModalParams::<f64, 8>::default();
248        let mut model = ModalModel::<f64, 8>::new(params.clone(), 44100.0);
249        let mut new_params = params.clone();
250        new_params.fundamental = 220.0.into();
251        model.set_params(new_params);
252        assert!((model.params.fundamental - 220.0).abs() < 1e-10);
253    }
254}