1mod 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#[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#[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 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 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}