1pub mod constants {
11 pub const SPEED_OF_SOUND_0C: f32 = 331.3;
13
14 pub const STANDARD_PRESSURE: f32 = 101325.0;
16
17 pub const REFERENCE_TEMP_C: f32 = 20.0;
19
20 pub const MOLAR_MASS_AIR: f32 = 0.02897;
22
23 pub const MOLAR_MASS_WATER: f32 = 0.01802;
25
26 pub const GAS_CONSTANT: f32 = 8.314;
28
29 pub const EAR_SPACING: f32 = 0.17;
31
32 pub const SPEED_IN_WATER: f32 = 1481.0;
34
35 pub const SPEED_IN_STEEL: f32 = 5960.0;
37
38 pub const SPEED_IN_ALUMINUM: f32 = 6420.0;
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum Medium {
45 #[default]
47 Air,
48 Water,
50 Steel,
52 Aluminum,
54 Custom,
56}
57
58#[derive(Debug, Clone)]
60pub struct MediumProperties {
61 pub speed_of_sound: f32,
63 pub density: f32,
65 pub base_damping: f32,
67 pub frequency_dependent: bool,
69 pub impedance: f32,
71}
72
73impl MediumProperties {
74 pub fn air(temperature_c: f32, humidity_percent: f32) -> Self {
76 let speed = speed_of_sound_in_air(temperature_c, humidity_percent);
77 let temp_k = temperature_c + 273.15;
79 let density = constants::STANDARD_PRESSURE * constants::MOLAR_MASS_AIR
80 / (constants::GAS_CONSTANT * temp_k);
81
82 Self {
83 speed_of_sound: speed,
84 density,
85 base_damping: 0.0001, frequency_dependent: true,
87 impedance: density * speed,
88 }
89 }
90
91 pub fn water(temperature_c: f32) -> Self {
93 let t = temperature_c;
97 let speed = 1402.7 + 5.0 * t - 0.055 * t * t + 0.00022 * t * t * t;
98 let density = 998.0 - 0.05 * (temperature_c - 20.0); Self {
101 speed_of_sound: speed,
102 density,
103 base_damping: 0.00002, frequency_dependent: true,
105 impedance: density * speed,
106 }
107 }
108
109 pub fn steel() -> Self {
111 Self {
112 speed_of_sound: constants::SPEED_IN_STEEL,
113 density: 7850.0,
114 base_damping: 0.00001, frequency_dependent: false,
116 impedance: 7850.0 * constants::SPEED_IN_STEEL,
117 }
118 }
119
120 pub fn aluminum() -> Self {
122 Self {
123 speed_of_sound: constants::SPEED_IN_ALUMINUM,
124 density: 2700.0,
125 base_damping: 0.000015,
126 frequency_dependent: false,
127 impedance: 2700.0 * constants::SPEED_IN_ALUMINUM,
128 }
129 }
130
131 pub fn custom(speed: f32, density: f32, damping: f32) -> Self {
133 Self {
134 speed_of_sound: speed,
135 density,
136 base_damping: damping,
137 frequency_dependent: false,
138 impedance: density * speed,
139 }
140 }
141}
142
143#[derive(Debug, Clone)]
145pub struct Environment {
146 pub temperature_c: f32,
148 pub humidity_percent: f32,
150 pub pressure_pa: f32,
152 pub medium: Medium,
154}
155
156impl Default for Environment {
157 fn default() -> Self {
158 Self {
159 temperature_c: 20.0,
160 humidity_percent: 50.0,
161 pressure_pa: constants::STANDARD_PRESSURE,
162 medium: Medium::Air,
163 }
164 }
165}
166
167impl Environment {
168 pub fn new() -> Self {
170 Self::default()
171 }
172
173 pub fn with_temperature(mut self, temp_c: f32) -> Self {
175 self.temperature_c = temp_c.clamp(-40.0, 60.0);
176 self
177 }
178
179 pub fn with_humidity(mut self, humidity: f32) -> Self {
181 self.humidity_percent = humidity.clamp(0.0, 100.0);
182 self
183 }
184
185 pub fn with_medium(mut self, medium: Medium) -> Self {
187 self.medium = medium;
188 self
189 }
190
191 pub fn medium_properties(&self) -> MediumProperties {
193 match self.medium {
194 Medium::Air => MediumProperties::air(self.temperature_c, self.humidity_percent),
195 Medium::Water => MediumProperties::water(self.temperature_c),
196 Medium::Steel => MediumProperties::steel(),
197 Medium::Aluminum => MediumProperties::aluminum(),
198 Medium::Custom => MediumProperties::custom(343.0, 1.2, 0.001),
199 }
200 }
201
202 pub fn speed_of_sound(&self) -> f32 {
204 self.medium_properties().speed_of_sound
205 }
206}
207
208pub fn speed_of_sound_in_air(temperature_c: f32, humidity_percent: f32) -> f32 {
215 let temp_k = temperature_c + 273.15;
216
217 let base_speed = constants::SPEED_OF_SOUND_0C * (temp_k / 273.15).sqrt();
219
220 let saturation_pressure = saturation_vapor_pressure(temperature_c);
224 let vapor_pressure = (humidity_percent / 100.0) * saturation_pressure;
225 let molar_fraction = vapor_pressure / constants::STANDARD_PRESSURE;
226
227 let humidity_correction = 0.14 * molar_fraction;
230
231 base_speed * (1.0 + humidity_correction)
232}
233
234pub fn saturation_vapor_pressure(temperature_c: f32) -> f32 {
238 let t = temperature_c;
240 611.21 * ((18.678 - t / 234.5) * (t / (257.14 + t))).exp()
241}
242
243#[derive(Debug, Clone)]
251pub struct AtmosphericAbsorption {
252 temperature_c: f32,
254 #[allow(dead_code)]
256 humidity_percent: f32,
257 pressure_pa: f32,
259 fr_o: f32,
261 fr_n: f32,
263}
264
265impl AtmosphericAbsorption {
266 pub fn new(temperature_c: f32, humidity_percent: f32, pressure_pa: f32) -> Self {
268 let temp_k = temperature_c + 273.15;
269 let temp_ratio = temp_k / 293.15; let psat = saturation_vapor_pressure(temperature_c);
273 let h = humidity_percent / 100.0 * psat / pressure_pa;
274
275 let fr_o = (pressure_pa / constants::STANDARD_PRESSURE)
277 * (24.0 + 4.04e4 * h * (0.02 + h) / (0.391 + h));
278
279 let fr_n = (pressure_pa / constants::STANDARD_PRESSURE)
281 * temp_ratio.powf(-0.5)
282 * (9.0 + 280.0 * h * (-4.17 * (temp_ratio.powf(-1.0 / 3.0) - 1.0)).exp());
283
284 Self {
285 temperature_c,
286 humidity_percent,
287 pressure_pa,
288 fr_o,
289 fr_n,
290 }
291 }
292
293 pub fn coefficient_db_per_meter(&self, frequency_hz: f32) -> f32 {
297 let temp_k = self.temperature_c + 273.15;
298 let temp_ratio = temp_k / 293.15;
299 let pressure_ratio = self.pressure_pa / constants::STANDARD_PRESSURE;
300
301 let f = frequency_hz;
302 let f_sq = f * f;
303
304 let alpha_classic = 1.84e-11 * pressure_ratio.recip() * temp_ratio.sqrt() * f_sq;
306
307 let alpha_o =
309 0.01275 * (-2239.1 / temp_k).exp() * (self.fr_o + f_sq / self.fr_o).recip() * f_sq;
310
311 let alpha_n =
313 0.1068 * (-3352.0 / temp_k).exp() * (self.fr_n + f_sq / self.fr_n).recip() * f_sq;
314
315 8.686 * (alpha_classic + temp_ratio.powf(-2.5) * (alpha_o + alpha_n))
317 }
318
319 pub fn coefficient_linear(&self, frequency_hz: f32) -> f32 {
323 let db_per_m = self.coefficient_db_per_meter(frequency_hz);
324 10.0_f32.powf(-db_per_m / 20.0)
325 }
326
327 pub fn absorption_at_distance(&self, frequency_hz: f32, distance_m: f32) -> f32 {
331 let db_per_m = self.coefficient_db_per_meter(frequency_hz);
332 10.0_f32.powf(-db_per_m * distance_m / 20.0)
333 }
334}
335
336#[derive(Debug, Clone)]
341pub struct MultiBandDamping {
342 pub center_frequencies: Vec<f32>,
344 pub damping_coefficients: Vec<f32>,
346 pub num_bands: usize,
348}
349
350impl MultiBandDamping {
351 pub fn from_environment(env: &Environment, time_step: f32, _cell_size: f32) -> Self {
355 let absorption =
356 AtmosphericAbsorption::new(env.temperature_c, env.humidity_percent, env.pressure_pa);
357
358 let center_frequencies: Vec<f32> = vec![
360 31.5, 63.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16000.0,
361 ];
362
363 let props = env.medium_properties();
364
365 let distance_per_step = props.speed_of_sound * time_step;
368
369 let damping_coefficients: Vec<f32> = center_frequencies
370 .iter()
371 .map(|&freq| {
372 if props.frequency_dependent {
373 let db_per_m = absorption.coefficient_db_per_meter(freq);
375 let db_per_step = db_per_m * distance_per_step;
376 10.0_f32.powf(-db_per_step / 20.0)
378 } else {
379 1.0 - props.base_damping * distance_per_step
381 }
382 })
383 .collect();
384
385 Self {
386 num_bands: center_frequencies.len(),
387 center_frequencies,
388 damping_coefficients,
389 }
390 }
391
392 pub fn single_band(damping_factor: f32) -> Self {
394 Self {
395 center_frequencies: vec![1000.0],
396 damping_coefficients: vec![1.0 - damping_factor],
397 num_bands: 1,
398 }
399 }
400
401 pub fn damping_for_frequency(&self, frequency: f32) -> f32 {
403 let mut min_diff = f32::MAX;
405 let mut best_idx = 0;
406 for (i, ¢er) in self.center_frequencies.iter().enumerate() {
407 let diff = (frequency - center).abs();
408 if diff < min_diff {
409 min_diff = diff;
410 best_idx = i;
411 }
412 }
413 self.damping_coefficients[best_idx]
414 }
415
416 pub fn average_damping(&self) -> f32 {
418 self.damping_coefficients.iter().sum::<f32>() / self.num_bands as f32
419 }
420}
421
422#[derive(Debug, Clone)]
427pub struct AcousticParams3D {
428 pub environment: Environment,
430 pub medium: MediumProperties,
432 pub cell_size: f32,
434 pub time_step: f32,
436 pub c_squared: f32,
438 pub damping: MultiBandDamping,
440 pub simple_damping: f32,
442}
443
444impl Default for AcousticParams3D {
445 fn default() -> Self {
446 Self::new(Environment::default(), 0.1) }
448}
449
450impl AcousticParams3D {
451 pub fn new(environment: Environment, cell_size: f32) -> Self {
460 let medium = environment.medium_properties();
461 let speed = medium.speed_of_sound;
462
463 let time_step = cell_size / (speed * 2.0);
466
467 let courant = speed * time_step / cell_size;
469 let c_squared = courant * courant;
470
471 let damping = MultiBandDamping::from_environment(&environment, time_step, cell_size);
473
474 let simple_damping = 1.0 - damping.average_damping().max(medium.base_damping);
476
477 Self {
478 environment,
479 medium,
480 cell_size,
481 time_step,
482 c_squared,
483 damping,
484 simple_damping: simple_damping.clamp(0.99, 0.9999),
485 }
486 }
487
488 pub fn for_medium(medium: Medium, cell_size: f32) -> Self {
490 Self::new(Environment::default().with_medium(medium), cell_size)
491 }
492
493 pub fn set_environment(&mut self, env: Environment) {
495 *self = Self::new(env, self.cell_size);
496 }
497
498 pub fn set_cell_size(&mut self, size: f32) {
500 *self = Self::new(self.environment.clone(), size);
501 }
502
503 pub fn courant_number(&self) -> f32 {
507 self.c_squared.sqrt()
508 }
509
510 pub fn is_stable(&self) -> bool {
512 self.courant_number() <= 1.0 / 3.0_f32.sqrt()
513 }
514
515 pub fn wavelength(&self, frequency: f32) -> f32 {
517 self.medium.speed_of_sound / frequency
518 }
519
520 pub fn cells_per_wavelength(&self, frequency: f32) -> f32 {
524 self.wavelength(frequency) / self.cell_size
525 }
526
527 pub fn max_accurate_frequency(&self) -> f32 {
531 self.medium.speed_of_sound / (10.0 * self.cell_size)
532 }
533}
534
535#[derive(Debug, Clone, Copy, PartialEq)]
537pub struct Position3D {
538 pub x: f32,
539 pub y: f32,
540 pub z: f32,
541}
542
543impl Position3D {
544 pub fn new(x: f32, y: f32, z: f32) -> Self {
545 Self { x, y, z }
546 }
547
548 pub fn origin() -> Self {
549 Self::new(0.0, 0.0, 0.0)
550 }
551
552 pub fn distance_to(&self, other: &Position3D) -> f32 {
554 let dx = self.x - other.x;
555 let dy = self.y - other.y;
556 let dz = self.z - other.z;
557 (dx * dx + dy * dy + dz * dz).sqrt()
558 }
559
560 pub fn to_grid_indices(&self, cell_size: f32) -> (usize, usize, usize) {
562 (
563 (self.x / cell_size).round() as usize,
564 (self.y / cell_size).round() as usize,
565 (self.z / cell_size).round() as usize,
566 )
567 }
568}
569
570#[derive(Debug, Clone, Copy)]
572pub struct Orientation3D {
573 pub yaw: f32,
575 pub pitch: f32,
577 pub roll: f32,
579}
580
581impl Default for Orientation3D {
582 fn default() -> Self {
583 Self {
584 yaw: 0.0,
585 pitch: 0.0,
586 roll: 0.0,
587 }
588 }
589}
590
591impl Orientation3D {
592 pub fn new(yaw: f32, pitch: f32, roll: f32) -> Self {
593 Self { yaw, pitch, roll }
594 }
595
596 pub fn forward(&self) -> (f32, f32, f32) {
598 let cy = self.yaw.cos();
599 let sy = self.yaw.sin();
600 let cp = self.pitch.cos();
601 let sp = self.pitch.sin();
602
603 (sy * cp, -sp, cy * cp)
604 }
605
606 pub fn right(&self) -> (f32, f32, f32) {
608 let cy = self.yaw.cos();
609 let sy = self.yaw.sin();
610 let cr = self.roll.cos();
611 let sr = self.roll.sin();
612
613 (cy * cr, sr, -sy * cr)
614 }
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620
621 #[test]
622 fn test_speed_of_sound_temperature() {
623 let speed_0c = speed_of_sound_in_air(0.0, 0.0);
625 assert!((speed_0c - 331.3).abs() < 1.0);
626
627 let speed_20c = speed_of_sound_in_air(20.0, 0.0);
629 assert!((speed_20c - 343.0).abs() < 2.0);
630
631 assert!(speed_20c > speed_0c);
633 }
634
635 #[test]
636 fn test_speed_humidity_effect() {
637 let speed_dry = speed_of_sound_in_air(20.0, 0.0);
639 let speed_humid = speed_of_sound_in_air(20.0, 100.0);
640
641 assert!(speed_humid > speed_dry);
643 assert!(speed_humid - speed_dry < 2.0); }
645
646 #[test]
647 fn test_atmospheric_absorption() {
648 let absorption = AtmosphericAbsorption::new(20.0, 50.0, constants::STANDARD_PRESSURE);
649
650 let low_freq = absorption.coefficient_db_per_meter(100.0);
652 assert!(low_freq < 0.01);
653
654 let high_freq = absorption.coefficient_db_per_meter(8000.0);
656 assert!(high_freq > low_freq);
657
658 let very_high = absorption.coefficient_db_per_meter(16000.0);
660 assert!(very_high > high_freq);
661 }
662
663 #[test]
664 fn test_medium_properties() {
665 let air = MediumProperties::air(20.0, 50.0);
666 assert!((air.speed_of_sound - 343.0).abs() < 5.0);
667
668 let water = MediumProperties::water(20.0);
669 assert!((water.speed_of_sound - 1481.0).abs() < 10.0);
670
671 let steel = MediumProperties::steel();
672 assert_eq!(steel.speed_of_sound, constants::SPEED_IN_STEEL);
673 }
674
675 #[test]
676 fn test_cfl_stability() {
677 let params = AcousticParams3D::default();
678 assert!(params.is_stable());
679 assert!(params.courant_number() <= 1.0 / 3.0_f32.sqrt());
680 }
681
682 #[test]
683 fn test_multiband_damping() {
684 let env = Environment::default();
685 let damping = MultiBandDamping::from_environment(&env, 0.0001, 0.01);
686
687 assert_eq!(damping.num_bands, 10);
689
690 let low_damping = damping.damping_for_frequency(100.0);
692 let high_damping = damping.damping_for_frequency(8000.0);
693 assert!(low_damping > high_damping);
694 }
695
696 #[test]
697 fn test_position_distance() {
698 let p1 = Position3D::new(0.0, 0.0, 0.0);
699 let p2 = Position3D::new(3.0, 4.0, 0.0);
700 assert!((p1.distance_to(&p2) - 5.0).abs() < 0.001);
701 }
702}