1use crate::collider::Contact;
16use crate::math::{Fix128, Vec3Fix};
17
18#[cfg(not(feature = "std"))]
19use alloc::vec;
20#[cfg(not(feature = "std"))]
21use alloc::vec::Vec;
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub struct AudioMaterial {
30 pub material_type: MaterialType,
32 pub density: Fix128,
34 pub hardness: Fix128,
36 pub resonance: Fix128,
38 pub damping: Fix128,
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum MaterialType {
45 Metal,
47 Wood,
49 Glass,
51 Stone,
53 Rubber,
55 Fabric,
57 Liquid,
59 Custom(u32),
61}
62
63impl AudioMaterial {
64 pub const METAL: Self = Self {
66 material_type: MaterialType::Metal,
67 density: Fix128 { hi: 7800, lo: 0 },
68 hardness: Fix128 {
69 hi: 0,
70 lo: 0xCCCCCCCCCCCCCCCC,
71 }, resonance: Fix128 {
73 hi: 0,
74 lo: 0xCCCCCCCCCCCCCCCC,
75 }, damping: Fix128 {
77 hi: 0,
78 lo: 0x3333333333333333,
79 }, };
81
82 pub const WOOD: Self = Self {
84 material_type: MaterialType::Wood,
85 density: Fix128 { hi: 600, lo: 0 },
86 hardness: Fix128 {
87 hi: 0,
88 lo: 0x999999999999999A,
89 }, resonance: Fix128 {
91 hi: 0,
92 lo: 0x6666666666666666,
93 }, damping: Fix128 {
95 hi: 0,
96 lo: 0x999999999999999A,
97 }, };
99
100 pub const STONE: Self = Self {
102 material_type: MaterialType::Stone,
103 density: Fix128 { hi: 2500, lo: 0 },
104 hardness: Fix128 {
105 hi: 0,
106 lo: 0xE666666666666666,
107 }, resonance: Fix128 {
109 hi: 0,
110 lo: 0x3333333333333333,
111 }, damping: Fix128 {
113 hi: 0,
114 lo: 0xB333333333333333,
115 }, };
117
118 pub const RUBBER: Self = Self {
120 material_type: MaterialType::Rubber,
121 density: Fix128 { hi: 1100, lo: 0 },
122 hardness: Fix128 {
123 hi: 0,
124 lo: 0x3333333333333333,
125 }, resonance: Fix128 {
127 hi: 0,
128 lo: 0x1999999999999999,
129 }, damping: Fix128 {
131 hi: 0,
132 lo: 0xE666666666666666,
133 }, };
135}
136
137#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143pub struct AudioEvent {
144 pub position: Vec3Fix,
146 pub volume: Fix128,
148 pub pitch: Fix128,
150 pub decay: Fix128,
152 pub brightness: Fix128,
154 pub roughness: Fix128,
156 pub event_type: AudioEventType,
158 pub material_a: MaterialType,
160 pub material_b: MaterialType,
162}
163
164#[derive(Clone, Copy, Debug, PartialEq, Eq)]
166pub enum AudioEventType {
167 Impact,
169 Slide,
171 Roll,
173 Scrape,
175 Break,
177 Splash,
179}
180
181#[derive(Clone, Copy, Debug, PartialEq, Eq)]
187pub struct AudioConfig {
188 pub min_velocity: Fix128,
190 pub max_velocity: Fix128,
192 pub velocity_pitch_factor: Fix128,
194 pub min_volume: Fix128,
196 pub slide_velocity_threshold: Fix128,
198 pub max_events_per_frame: usize,
200}
201
202impl Default for AudioConfig {
203 fn default() -> Self {
204 Self {
205 min_velocity: Fix128::from_ratio(1, 10),
206 max_velocity: Fix128::from_int(20),
207 velocity_pitch_factor: Fix128::from_ratio(1, 20),
208 min_volume: Fix128::from_ratio(1, 100),
209 slide_velocity_threshold: Fix128::from_ratio(1, 2),
210 max_events_per_frame: 32,
211 }
212 }
213}
214
215pub struct AudioGenerator {
217 pub config: AudioConfig,
219 pub materials: Vec<AudioMaterial>,
221 pub events: Vec<AudioEvent>,
223}
224
225impl AudioGenerator {
226 #[must_use]
228 pub fn new(num_bodies: usize, config: AudioConfig) -> Self {
229 Self {
230 config,
231 materials: vec![AudioMaterial::WOOD; num_bodies],
232 events: Vec::new(),
233 }
234 }
235
236 pub fn set_material(&mut self, body_idx: usize, material: AudioMaterial) {
238 if body_idx >= self.materials.len() {
239 self.materials.resize(body_idx + 1, AudioMaterial::WOOD);
240 }
241 self.materials[body_idx] = material;
242 }
243
244 pub fn begin_frame(&mut self) {
246 self.events.clear();
247 }
248
249 pub fn process_contact(
251 &mut self,
252 body_a: usize,
253 body_b: usize,
254 contact: &Contact,
255 relative_velocity: Vec3Fix,
256 is_new_contact: bool,
257 ) {
258 if self.events.len() >= self.config.max_events_per_frame {
259 return;
260 }
261
262 let speed = relative_velocity.length();
263 if speed < self.config.min_velocity {
264 return;
265 }
266
267 let mat_a = self.get_material(body_a);
268 let mat_b = self.get_material(body_b);
269
270 let normal_speed = relative_velocity.dot(contact.normal).abs();
272 let tangential_sq = speed * speed - normal_speed * normal_speed;
273 let tangential_speed = if tangential_sq > Fix128::ZERO {
274 tangential_sq.sqrt()
275 } else {
276 Fix128::ZERO
277 };
278
279 let event_type = if is_new_contact {
280 AudioEventType::Impact
281 } else if tangential_speed > self.config.slide_velocity_threshold {
282 AudioEventType::Slide
283 } else {
284 AudioEventType::Roll
285 };
286
287 let volume = self.compute_volume(speed, &mat_a, &mat_b);
289 let pitch = self.compute_pitch(speed, &mat_a, &mat_b);
290 let decay = self.compute_decay(&mat_a, &mat_b);
291 let brightness = self.compute_brightness(speed, &mat_a, &mat_b);
292 let roughness = self.compute_roughness(tangential_speed, &mat_a, &mat_b);
293
294 if volume < self.config.min_volume {
295 return;
296 }
297
298 self.events.push(AudioEvent {
299 position: contact.point_a,
300 volume,
301 pitch,
302 decay,
303 brightness,
304 roughness,
305 event_type,
306 material_a: mat_a.material_type,
307 material_b: mat_b.material_type,
308 });
309 }
310
311 fn get_material(&self, body_idx: usize) -> AudioMaterial {
312 if body_idx < self.materials.len() {
313 self.materials[body_idx]
314 } else {
315 AudioMaterial::WOOD
316 }
317 }
318
319 fn speed_ratio(&self, speed: Fix128) -> Fix128 {
323 let max = self.config.max_velocity;
324 if max <= Fix128::ZERO {
325 return if speed > Fix128::ZERO {
326 Fix128::ONE
327 } else {
328 Fix128::ZERO
329 };
330 }
331 let ratio = speed / max;
332 if ratio > Fix128::ONE {
333 Fix128::ONE
334 } else {
335 ratio
336 }
337 }
338
339 fn compute_volume(
341 &self,
342 speed: Fix128,
343 _mat_a: &AudioMaterial,
344 _mat_b: &AudioMaterial,
345 ) -> Fix128 {
346 self.speed_ratio(speed).sqrt()
347 }
348
349 fn compute_pitch(&self, speed: Fix128, mat_a: &AudioMaterial, mat_b: &AudioMaterial) -> Fix128 {
351 let base = Fix128::ONE + speed * self.config.velocity_pitch_factor;
352
353 let avg_density = (mat_a.density + mat_b.density).half();
355 if avg_density.is_zero() {
356 return base;
357 }
358 let density_factor = Fix128::from_int(1000) / avg_density;
359 let density_factor = if density_factor > Fix128::from_int(2) {
360 Fix128::from_int(2)
361 } else if density_factor < Fix128::from_ratio(1, 2) {
362 Fix128::from_ratio(1, 2)
363 } else {
364 density_factor
365 };
366
367 base * density_factor
368 }
369
370 #[allow(clippy::unused_self)]
372 fn compute_decay(&self, mat_a: &AudioMaterial, mat_b: &AudioMaterial) -> Fix128 {
373 let avg_resonance = (mat_a.resonance + mat_b.resonance).half();
374 let avg_damping = (mat_a.damping + mat_b.damping).half();
375
376 let base_decay = Fix128::from_ratio(1, 10); let resonance_bonus = avg_resonance * Fix128::from_int(2);
379 let damping_reduction = avg_damping;
380
381 base_decay + resonance_bonus * (Fix128::ONE - damping_reduction)
382 }
383
384 fn compute_brightness(
386 &self,
387 speed: Fix128,
388 mat_a: &AudioMaterial,
389 mat_b: &AudioMaterial,
390 ) -> Fix128 {
391 let avg_hardness = (mat_a.hardness + mat_b.hardness).half();
392 let speed_factor = self.speed_ratio(speed);
393
394 avg_hardness * (Fix128::from_ratio(1, 2) + speed_factor.half())
395 }
396
397 fn compute_roughness(
399 &self,
400 tangential_speed: Fix128,
401 mat_a: &AudioMaterial,
402 mat_b: &AudioMaterial,
403 ) -> Fix128 {
404 let avg_hardness = (mat_a.hardness + mat_b.hardness).half();
405 let speed_factor = self.speed_ratio(tangential_speed);
406
407 speed_factor * avg_hardness
408 }
409
410 #[must_use]
412 pub fn get_events(&self) -> &[AudioEvent] {
413 &self.events
414 }
415}
416
417impl crate::solver::PhysicsWorld {
422 pub fn emit_contact_audio(&self, generator: &mut AudioGenerator) {
440 use crate::event::ContactEventType;
441 for e in self.contact_events() {
442 let is_new = match e.event_type {
443 ContactEventType::Begin => true,
444 ContactEventType::Persist => false,
445 ContactEventType::End => continue,
446 };
447 let (Some(a), Some(b)) = (self.bodies.get(e.body_a), self.bodies.get(e.body_b)) else {
448 continue;
449 };
450 let rel = a.velocity - b.velocity;
451 let tangential = rel - e.normal * rel.dot(e.normal);
452 let contact = Contact {
453 depth: e.depth,
454 normal: e.normal,
455 point_a: e.point,
456 point_b: e.point + e.normal * e.depth,
457 };
458 generator.process_contact(
459 e.body_a,
460 e.body_b,
461 &contact,
462 e.normal * e.relative_velocity + tangential,
463 is_new,
464 );
465 }
466 }
467}
468
469#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn test_audio_generator() {
479 let mut gen = AudioGenerator::new(2, AudioConfig::default());
480 gen.set_material(0, AudioMaterial::METAL);
481 gen.set_material(1, AudioMaterial::STONE);
482
483 gen.begin_frame();
484
485 let contact = Contact {
486 depth: Fix128::from_ratio(1, 10),
487 normal: Vec3Fix::UNIT_Y,
488 point_a: Vec3Fix::ZERO,
489 point_b: Vec3Fix::ZERO,
490 };
491
492 gen.process_contact(0, 1, &contact, Vec3Fix::from_f32(0.0, -5.0, 0.0), true);
493
494 assert_eq!(gen.events.len(), 1);
495 let event = &gen.events[0];
496 assert_eq!(event.event_type, AudioEventType::Impact);
497 assert!(event.volume > Fix128::ZERO);
498 }
499
500 #[test]
501 fn test_material_presets() {
502 let metal = AudioMaterial::METAL;
503 let rubber = AudioMaterial::RUBBER;
504
505 assert!(metal.hardness > rubber.hardness);
507 assert!(metal.resonance > rubber.resonance);
508 }
509
510 #[test]
511 fn test_volume_scaling() {
512 let gen = AudioGenerator::new(2, AudioConfig::default());
513 let mat = AudioMaterial::WOOD;
514
515 let soft = gen.compute_volume(Fix128::from_int(1), &mat, &mat);
516 let hard = gen.compute_volume(Fix128::from_int(10), &mat, &mat);
517
518 assert!(hard > soft, "Harder impact should be louder");
519 }
520
521 #[test]
522 fn test_below_threshold() {
523 let mut gen = AudioGenerator::new(2, AudioConfig::default());
524 gen.begin_frame();
525
526 let contact = Contact {
527 depth: Fix128::from_ratio(1, 100),
528 normal: Vec3Fix::UNIT_Y,
529 point_a: Vec3Fix::ZERO,
530 point_b: Vec3Fix::ZERO,
531 };
532
533 gen.process_contact(0, 1, &contact, Vec3Fix::from_f32(0.0, -0.01, 0.0), true);
535
536 assert!(
537 gen.events.is_empty(),
538 "Below-threshold impacts should be silent"
539 );
540 }
541
542 #[test]
543 fn test_slide_detection() {
544 let mut gen = AudioGenerator::new(2, AudioConfig::default());
545 gen.begin_frame();
546
547 let contact = Contact {
548 depth: Fix128::from_ratio(1, 10),
549 normal: Vec3Fix::UNIT_Y,
550 point_a: Vec3Fix::ZERO,
551 point_b: Vec3Fix::ZERO,
552 };
553
554 gen.process_contact(
556 0,
557 1,
558 &contact,
559 Vec3Fix::from_f32(5.0, 0.0, 0.0), false, );
562
563 assert_eq!(gen.events.len(), 1);
564 assert_eq!(gen.events[0].event_type, AudioEventType::Slide);
565 }
566
567 #[test]
568 fn get_events_returns_this_frames_events_in_order_and_respects_cap() {
569 let config = AudioConfig {
570 max_events_per_frame: 2,
571 ..AudioConfig::default()
572 };
573 let mut gen = AudioGenerator::new(3, config);
574 gen.set_material(1, AudioMaterial::METAL);
575 assert!(gen.get_events().is_empty());
576 gen.begin_frame();
577
578 let contact_at = |x: i64| Contact {
579 depth: Fix128::from_ratio(1, 10),
580 normal: Vec3Fix::UNIT_Y,
581 point_a: Vec3Fix::from_int(x, 0, 0),
582 point_b: Vec3Fix::from_int(x, -1, 0),
583 };
584 gen.process_contact(0, 1, &contact_at(1), Vec3Fix::from_int(0, -5, 0), true);
586 gen.process_contact(0, 2, &contact_at(2), Vec3Fix::from_int(5, 0, 0), false);
588 gen.process_contact(1, 2, &contact_at(3), Vec3Fix::from_int(0, -9, 0), true);
590
591 let events = gen.get_events();
592 assert_eq!(events.len(), 2);
593 assert_eq!(events, gen.events.as_slice());
594 assert_eq!(events[0].position, Vec3Fix::from_int(1, 0, 0));
596 assert_eq!(events[1].position, Vec3Fix::from_int(2, 0, 0));
597 assert_eq!(events[0].event_type, AudioEventType::Impact);
598 assert_eq!(events[1].event_type, AudioEventType::Slide);
599 assert_eq!(
600 (events[0].material_a, events[0].material_b),
601 (MaterialType::Wood, MaterialType::Metal)
602 );
603 assert_eq!(
604 (events[1].material_a, events[1].material_b),
605 (MaterialType::Wood, MaterialType::Wood)
606 );
607 let vol_err = (events[0].volume - Fix128::from_ratio(1, 2)).abs();
608 assert!(
609 vol_err < Fix128::from_ratio(1, 1_000_000),
610 "volume {:?}",
611 events[0].volume
612 );
613 assert_eq!(events[0].roughness, Fix128::ZERO);
615 let wood = AudioMaterial::WOOD;
616 assert_eq!(
617 events[1].roughness,
618 Fix128::from_ratio(1, 4) * wood.hardness
619 );
620
621 gen.begin_frame();
623 assert!(gen.get_events().is_empty());
624 gen.process_contact(1, 2, &contact_at(3), Vec3Fix::from_int(0, -9, 0), true);
625 assert_eq!(gen.get_events().len(), 1);
626 assert_eq!(gen.get_events()[0].position, Vec3Fix::from_int(3, 0, 0));
627 }
628
629 fn custom(density: i64, hardness: Fix128) -> AudioMaterial {
630 AudioMaterial {
631 material_type: MaterialType::Custom(7),
632 density: Fix128::from_int(density),
633 hardness,
634 resonance: Fix128::ZERO,
635 damping: Fix128::ZERO,
636 }
637 }
638
639 #[test]
642 fn set_material_grows_with_wood() {
643 let mut gen = AudioGenerator::new(1, AudioConfig::default());
644 gen.set_material(4, AudioMaterial::METAL);
645 assert_eq!(gen.materials.len(), 5);
646 assert_eq!(gen.materials[2], AudioMaterial::WOOD);
647 assert_eq!(gen.get_material(4), AudioMaterial::METAL);
648 assert_eq!(gen.get_material(99), AudioMaterial::WOOD);
649 }
650
651 fn assert_near(got: Fix128, want: Fix128) {
653 assert!(
654 (got - want).abs() < Fix128::from_ratio(1, 1_000_000_000_000_000),
655 "{got:?} vs {want:?}"
656 );
657 }
658
659 #[test]
666 fn pitch_density_clamp_and_saturation() {
667 let gen = AudioGenerator::new(2, AudioConfig::default());
668 let v = Fix128::from_int(2);
669 let h = Fix128::from_ratio(1, 2);
670 let zero = custom(0, h);
671 assert_near(
672 gen.compute_pitch(v, &zero, &zero),
673 Fix128::from_ratio(11, 10),
674 );
675 let light = custom(100, h);
676 assert_near(
677 gen.compute_pitch(v, &light, &light),
678 Fix128::from_ratio(11, 5),
679 );
680 let heavy = custom(4000, h);
681 assert_near(
682 gen.compute_pitch(v, &heavy, &heavy),
683 Fix128::from_ratio(11, 20),
684 );
685 let fast = Fix128::from_int(40);
686 assert_eq!(gen.compute_volume(fast, &light, &light), Fix128::ONE);
687 assert_eq!(gen.compute_brightness(fast, &light, &light), h);
688 assert_eq!(gen.compute_roughness(fast, &light, &light), h);
689 }
690
691 #[test]
695 fn roll_and_quiet_events() {
696 let contact = Contact {
697 depth: Fix128::from_ratio(1, 10),
698 normal: Vec3Fix::UNIT_Y,
699 point_a: Vec3Fix::ZERO,
700 point_b: Vec3Fix::ZERO,
701 };
702 let mut gen = AudioGenerator::new(2, AudioConfig::default());
703 gen.process_contact(0, 1, &contact, Vec3Fix::from_int(0, -2, 0), false);
704 assert_eq!(gen.events.len(), 1);
705 assert_eq!(gen.events[0].event_type, AudioEventType::Roll);
706 assert_eq!(gen.events[0].roughness, Fix128::ZERO);
707
708 let mut quiet = AudioGenerator::new(
709 2,
710 AudioConfig {
711 min_volume: Fix128::from_ratio(1, 2),
712 ..AudioConfig::default()
713 },
714 );
715 quiet.process_contact(0, 1, &contact, Vec3Fix::from_int(0, -1, 0), true);
716 assert!(quiet.events.is_empty());
717 }
718
719 #[test]
726 fn world_contact_events_become_audio() {
727 use crate::event::{ContactEvent, ContactEventType};
728 let mut world = crate::solver::PhysicsWorld::new(crate::solver::SolverConfig::default());
729 let mut a = crate::solver::RigidBody::new(Vec3Fix::ZERO, Fix128::ONE);
730 a.velocity = Vec3Fix::from_int(3, 0, 0);
731 world.add_body(a);
732 world.add_body(crate::solver::RigidBody::new_static(Vec3Fix::from_int(
733 0, -1, 0,
734 )));
735 let ev = |event_type, body_b| ContactEvent {
736 body_a: 0,
737 body_b,
738 event_type,
739 normal: Vec3Fix::UNIT_Y,
740 point: Vec3Fix::from_int(1, 2, 3),
741 depth: Fix128::from_ratio(1, 10),
742 relative_velocity: Fix128::from_int(-5),
743 };
744 world.events.contact_events = vec![
745 ev(ContactEventType::Begin, 1),
746 ev(ContactEventType::End, 1),
747 ev(ContactEventType::Begin, 9),
748 ev(ContactEventType::Persist, 1),
749 ];
750 let mut gen = AudioGenerator::new(2, AudioConfig::default());
751 gen.begin_frame();
752 world.emit_contact_audio(&mut gen);
753 assert_eq!(gen.events.len(), 2);
754 assert_eq!(gen.events[0].event_type, AudioEventType::Impact);
755 assert_eq!(gen.events[1].event_type, AudioEventType::Slide);
756 let want = (34.0_f64.sqrt() / 20.0).sqrt();
757 for e in &gen.events {
758 assert_eq!(e.position, Vec3Fix::from_int(1, 2, 3));
759 assert!((e.volume.to_f64() - want).abs() < 1e-9, "{:?}", e.volume);
760 }
761 }
762}