1mod params;
11
12pub use params::{CavityArrayParams, HelmholtzParams};
13
14use rill_core::traits::algorithm::{
15 Algorithm, AlgorithmCategory, AlgorithmMetadata, ParameterizedAlgorithm,
16};
17use rill_core::traits::ParamValue;
18use rill_core::Transcendental;
19
20#[derive(Debug, Clone)]
35pub struct HelmholtzCavity<T: Transcendental> {
36 params: HelmholtzParams<T>,
37 prev_out: T,
38 prev_prev_out: T,
39 reed_state: T,
40 r: T,
41 cos_omega: T,
42 sample_rate: f32,
43}
44
45impl<T: Transcendental> HelmholtzCavity<T> {
46 pub fn new(params: HelmholtzParams<T>, sample_rate: f32) -> Self {
48 let mut cavity = Self {
49 params,
50 prev_out: T::ZERO,
51 prev_prev_out: T::ZERO,
52 reed_state: T::ZERO,
53 r: T::ONE,
54 cos_omega: T::ONE,
55 sample_rate,
56 };
57 cavity.recompute_coeffs();
58 cavity
59 }
60
61 pub fn resonant_frequency(&self) -> T {
63 let two_pi = T::from_f32(2.0 * std::f32::consts::PI);
64 let radius = (self.params.neck_area / T::PI).sqrt();
65 let l_eff = self.params.neck_length + T::from_f32(1.7) * radius;
66 self.params.sound_speed / two_pi
67 * (self.params.neck_area / (self.params.volume * l_eff)).sqrt()
68 }
69
70 fn recompute_coeffs(&mut self) {
71 if self.sample_rate == 0.0 {
72 return;
73 }
74 let sr = T::from_f32(self.sample_rate);
75 let f_res = self.resonant_frequency();
76 let omega = T::from_f32(2.0 * std::f32::consts::PI) * f_res / sr;
77 let damping = T::ONE - self.params.radiation_loss - self.params.viscous_loss;
78 self.r = damping;
79 self.cos_omega = omega.cos();
80 }
81
82 fn reed_flow(&mut self) -> T {
84 let closing = T::ONE - self.params.pressure - self.reed_state;
85 if closing > T::ZERO {
86 self.params.pressure * closing.sqrt()
87 } else {
88 T::ZERO
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 excitation = if self.params.excitation == 1 {
97 let flow = self.reed_flow();
98 self.reed_state = flow;
99 flow
100 } else {
101 input
102 };
103
104 let two_r_cos = T::from_f32(2.0) * self.r * self.cos_omega;
106 let r2 = self.r * self.r;
107 let y = excitation + two_r_cos * self.prev_out - r2 * self.prev_prev_out;
108
109 self.prev_prev_out = self.prev_out;
110 self.prev_out = y;
111
112 y
113 }
114}
115
116impl<T: Transcendental> Algorithm<T> for HelmholtzCavity<T> {
117 fn process(
118 &mut self,
119 input: Option<&[T]>,
120 output: &mut [T],
121 ) -> rill_core::traits::ProcessResult<()> {
122 for (i, out) in output.iter_mut().enumerate() {
123 let inp = input
124 .map(|s| s.get(i).copied().unwrap_or(T::ZERO))
125 .unwrap_or(T::ZERO);
126 *out = self.process_sample(inp);
127 }
128 Ok(())
129 }
130
131 fn reset(&mut self) {
132 self.prev_out = T::ZERO;
133 self.prev_prev_out = T::ZERO;
134 self.reed_state = T::ZERO;
135 self.recompute_coeffs();
136 }
137
138 fn init(&mut self, sample_rate: f32) {
139 self.sample_rate = sample_rate;
140 self.recompute_coeffs();
141 }
142
143 fn metadata(&self) -> AlgorithmMetadata {
144 AlgorithmMetadata {
145 name: "Helmholtz Cavity",
146 category: AlgorithmCategory::Filter,
147 description: "Single Helmholtz resonator with optional reed excitation",
148 author: "Rill",
149 version: "0.5",
150 }
151 }
152}
153
154impl<T: Transcendental> ParameterizedAlgorithm<T> for HelmholtzCavity<T> {
155 type Params = HelmholtzParams<T>;
156
157 fn params(&self) -> &Self::Params {
158 &self.params
159 }
160
161 fn set_params(&mut self, params: Self::Params) {
162 self.params = params;
163 self.recompute_coeffs();
164 }
165
166 fn set_parameter(&mut self, name: &str, value: ParamValue) -> Result<(), &'static str> {
167 match name {
168 "volume" => {
169 let mut p = self.params.clone();
170 p.volume = T::from_f64(value.as_f32().map(|v| v as f64).unwrap_or(0.001));
171 self.set_params(p);
172 Ok(())
173 }
174 "neck_area" => {
175 let mut p = self.params.clone();
176 p.neck_area = T::from_f64(value.as_f32().map(|v| v as f64).unwrap_or(0.0001));
177 self.set_params(p);
178 Ok(())
179 }
180 "neck_length" => {
181 let mut p = self.params.clone();
182 p.neck_length = T::from_f64(value.as_f32().map(|v| v as f64).unwrap_or(0.02));
183 self.set_params(p);
184 Ok(())
185 }
186 "pressure" => {
187 let mut p = self.params.clone();
188 p.pressure = T::from_f64(value.as_f32().map(|v| v as f64).unwrap_or(0.0));
189 self.set_params(p);
190 Ok(())
191 }
192 "excitation" => {
193 let mut p = self.params.clone();
194 p.excitation = value.as_i32().map(|v| v as u8).unwrap_or(0);
195 self.set_params(p);
196 Ok(())
197 }
198 _ => Err("Unknown parameter"),
199 }
200 }
201}
202
203#[derive(Debug, Clone)]
210pub struct CavityArray<T: Transcendental, const MAX_CAVITIES: usize> {
211 params: CavityArrayParams<T>,
212 cavities: [HelmholtzCavity<T>; MAX_CAVITIES],
213 prev_outputs: [T; MAX_CAVITIES],
214}
215
216impl<T: Transcendental, const MAX_CAVITIES: usize> CavityArray<T, MAX_CAVITIES> {
217 pub fn new(params: CavityArrayParams<T>, sample_rate: f32) -> Self {
219 let default_cavity = HelmholtzCavity::new(params.cavity_params.clone(), sample_rate);
220 let cavities = core::array::from_fn(|_| default_cavity.clone());
221 Self {
222 params,
223 cavities,
224 prev_outputs: [T::ZERO; MAX_CAVITIES],
225 }
226 }
227
228 fn process_sample(&mut self, input: T) -> T {
229 let n = self.params.num_cavities.min(MAX_CAVITIES);
230
231 let prev = self.prev_outputs;
233
234 let mut output = T::ZERO;
235 for i in 0..n {
236 let coupling_input = if i > 0 {
238 prev[i - 1] * self.params.coupling
239 } else {
240 T::ZERO
241 } + if i + 1 < n {
242 prev[i + 1] * self.params.coupling
243 } else {
244 T::ZERO
245 };
246
247 let ext_input = if i == self.params.input_index {
249 input
250 } else {
251 T::ZERO
252 };
253
254 let y = self.cavities[i].process_sample(coupling_input + ext_input);
255 self.prev_outputs[i] = y;
256
257 if i == self.params.output_index {
258 output = y;
259 }
260 }
261
262 output
263 }
264}
265
266impl<T: Transcendental, const MAX_CAVITIES: usize> Algorithm<T> for CavityArray<T, MAX_CAVITIES> {
267 fn process(
268 &mut self,
269 input: Option<&[T]>,
270 output: &mut [T],
271 ) -> rill_core::traits::ProcessResult<()> {
272 for (i, out) in output.iter_mut().enumerate() {
273 let inp = input
274 .map(|s| s.get(i).copied().unwrap_or(T::ZERO))
275 .unwrap_or(T::ZERO);
276 *out = self.process_sample(inp);
277 }
278 Ok(())
279 }
280
281 fn reset(&mut self) {
282 for cavity in self.cavities.iter_mut() {
283 cavity.reset();
284 }
285 self.prev_outputs = [T::ZERO; MAX_CAVITIES];
286 }
287
288 fn init(&mut self, sample_rate: f32) {
289 for cavity in self.cavities.iter_mut() {
290 cavity.init(sample_rate);
291 }
292 }
293
294 fn metadata(&self) -> AlgorithmMetadata {
295 AlgorithmMetadata {
296 name: "Cavity Array",
297 category: AlgorithmCategory::Filter,
298 description: "1D chain of coupled Helmholtz cavities for wave propagation",
299 author: "Rill",
300 version: "0.5",
301 }
302 }
303}
304
305impl<T: Transcendental, const MAX_CAVITIES: usize> ParameterizedAlgorithm<T>
306 for CavityArray<T, MAX_CAVITIES>
307{
308 type Params = CavityArrayParams<T>;
309
310 fn params(&self) -> &Self::Params {
311 &self.params
312 }
313
314 fn set_params(&mut self, params: Self::Params) {
315 self.params = params;
316 for cavity in self.cavities.iter_mut() {
317 cavity.set_params(self.params.cavity_params.clone());
318 }
319 }
320
321 fn set_parameter(&mut self, name: &str, value: ParamValue) -> Result<(), &'static str> {
322 match name {
323 "coupling" => {
324 let mut p = self.params.clone();
325 p.coupling = T::from_f64(value.as_f32().map(|v| v as f64).unwrap_or(0.1));
326 self.set_params(p);
327 Ok(())
328 }
329 _ => Err("Unknown parameter: use HelmholtzCavity for per-cavity params"),
330 }
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
341 fn test_helmholtz_creation() {
342 let params = HelmholtzParams::<f64>::default();
343 let cavity = HelmholtzCavity::<f64>::new(params, 44100.0);
344 let f = cavity.resonant_frequency();
345 assert!(f.to_f64() > 0.0);
346 assert!(f.to_f64() < 44100.0 / 2.0);
347 }
348
349 #[test]
350 fn test_helmholtz_frequency_known_bottle() {
351 let params = HelmholtzParams {
353 volume: 0.001.into(),
354 neck_area: 0.0001.into(),
355 neck_length: 0.02.into(),
356 sound_speed: 343.0.into(),
357 ..Default::default()
358 };
359 let cavity = HelmholtzCavity::<f64>::new(params, 44100.0);
360 let f = cavity.resonant_frequency().to_f64();
361 assert!(f > 50.0);
362 assert!(f < 300.0);
363 }
364
365 #[test]
366 fn test_helmholtz_algorithm_process() {
367 let params = HelmholtzParams::<f64>::default();
369 let mut cavity = HelmholtzCavity::<f64>::new(params.clone(), 44100.0);
370 let f_res = cavity.resonant_frequency().to_f64();
371 let mut output = [0.0f64; 128];
372 let input: Vec<f64> = (0..128)
373 .map(|i| (2.0 * std::f64::consts::PI * f_res * i as f64 / 44100.0).sin() * 0.5)
374 .collect();
375 cavity.process(Some(&input), &mut output).unwrap();
376 let rms = (output.iter().map(|x| x * x).sum::<f64>() / 128.0).sqrt();
377 assert!(
378 rms > 0.01,
379 "RMS should be non-zero at resonance: {:.6}",
380 rms
381 );
382 }
383
384 #[test]
385 fn test_helmholtz_reed_self_oscillation() {
386 let params = HelmholtzParams {
388 pressure: 0.5.into(),
389 excitation: 1,
390 ..Default::default()
391 };
392 let mut cavity = HelmholtzCavity::<f64>::new(params, 44100.0);
393 let mut output = [0.0f64; 128];
394 cavity.process(None, &mut output).unwrap();
395 let max_abs = output.iter().map(|x| x.abs()).fold(0.0, f64::max);
396 assert!(max_abs > 0.0, "Reed excitation should produce output");
397 }
398
399 #[test]
400 fn test_helmholtz_params() {
401 let params = HelmholtzParams::<f64>::default();
402 let mut cavity = HelmholtzCavity::<f64>::new(params, 44100.0);
403 let new_params = HelmholtzParams {
404 volume: 0.002.into(),
405 ..HelmholtzParams::default()
406 };
407 cavity.set_params(new_params);
408 assert!((cavity.params.volume.to_f64() - 0.002).abs() < 1e-10);
409 }
410
411 #[test]
414 fn test_cavity_array_creation() {
415 let params = CavityArrayParams::<f64>::default();
416 let array = CavityArray::<f64, 8>::new(params, 44100.0);
417 assert!(array.params.num_cavities == 4);
418 }
419
420 #[test]
421 fn test_cavity_array_wave_propagation() {
422 let params = CavityArrayParams {
424 num_cavities: 4,
425 coupling: 0.3.into(),
426 input_index: 0,
427 output_index: 3,
428 ..Default::default()
429 };
430 let mut array = CavityArray::<f64, 8>::new(params, 44100.0);
431 let mut output = [0.0f64; 256];
432 let input: Vec<f64> = (0..256)
433 .map(|i| (2.0 * std::f64::consts::PI * 440.0 * i as f64 / 44100.0).sin() * 0.5)
434 .collect();
435 array.process(Some(&input), &mut output).unwrap();
436 let rms = (output.iter().map(|x| x * x).sum::<f64>() / 256.0).sqrt();
437 assert!(
438 rms > 0.001,
439 "Wave should propagate through coupled cavities"
440 );
441 }
442
443 #[test]
444 fn test_cavity_array_zero_coupling() {
445 let params = CavityArrayParams {
447 num_cavities: 4,
448 coupling: 0.0.into(),
449 input_index: 0,
450 output_index: 3,
451 ..Default::default()
452 };
453 let mut array = CavityArray::<f64, 8>::new(params, 44100.0);
454 let mut output = [0.0f64; 256];
455 let input: Vec<f64> = (0..256)
456 .map(|i| (2.0 * std::f64::consts::PI * 440.0 * i as f64 / 44100.0).sin() * 0.5)
457 .collect();
458 array.process(Some(&input), &mut output).unwrap();
459 let rms = (output.iter().map(|x| x * x).sum::<f64>() / 256.0).sqrt();
460 assert!(rms < 0.01, "Zero coupling should block propagation");
461 }
462}