use crate::collider::Contact;
use crate::math::{Fix128, Vec3Fix};
#[cfg(not(feature = "std"))]
use alloc::vec;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AudioMaterial {
pub material_type: MaterialType,
pub density: Fix128,
pub hardness: Fix128,
pub resonance: Fix128,
pub damping: Fix128,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MaterialType {
Metal,
Wood,
Glass,
Stone,
Rubber,
Fabric,
Liquid,
Custom(u32),
}
impl AudioMaterial {
pub const METAL: Self = Self {
material_type: MaterialType::Metal,
density: Fix128 { hi: 7800, lo: 0 },
hardness: Fix128 {
hi: 0,
lo: 0xCCCCCCCCCCCCCCCC,
}, resonance: Fix128 {
hi: 0,
lo: 0xCCCCCCCCCCCCCCCC,
}, damping: Fix128 {
hi: 0,
lo: 0x3333333333333333,
}, };
pub const WOOD: Self = Self {
material_type: MaterialType::Wood,
density: Fix128 { hi: 600, lo: 0 },
hardness: Fix128 {
hi: 0,
lo: 0x999999999999999A,
}, resonance: Fix128 {
hi: 0,
lo: 0x6666666666666666,
}, damping: Fix128 {
hi: 0,
lo: 0x999999999999999A,
}, };
pub const STONE: Self = Self {
material_type: MaterialType::Stone,
density: Fix128 { hi: 2500, lo: 0 },
hardness: Fix128 {
hi: 0,
lo: 0xE666666666666666,
}, resonance: Fix128 {
hi: 0,
lo: 0x3333333333333333,
}, damping: Fix128 {
hi: 0,
lo: 0xB333333333333333,
}, };
pub const RUBBER: Self = Self {
material_type: MaterialType::Rubber,
density: Fix128 { hi: 1100, lo: 0 },
hardness: Fix128 {
hi: 0,
lo: 0x3333333333333333,
}, resonance: Fix128 {
hi: 0,
lo: 0x1999999999999999,
}, damping: Fix128 {
hi: 0,
lo: 0xE666666666666666,
}, };
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AudioEvent {
pub position: Vec3Fix,
pub volume: Fix128,
pub pitch: Fix128,
pub decay: Fix128,
pub brightness: Fix128,
pub roughness: Fix128,
pub event_type: AudioEventType,
pub material_a: MaterialType,
pub material_b: MaterialType,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AudioEventType {
Impact,
Slide,
Roll,
Scrape,
Break,
Splash,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AudioConfig {
pub min_velocity: Fix128,
pub max_velocity: Fix128,
pub velocity_pitch_factor: Fix128,
pub min_volume: Fix128,
pub slide_velocity_threshold: Fix128,
pub max_events_per_frame: usize,
}
impl Default for AudioConfig {
fn default() -> Self {
Self {
min_velocity: Fix128::from_ratio(1, 10),
max_velocity: Fix128::from_int(20),
velocity_pitch_factor: Fix128::from_ratio(1, 20),
min_volume: Fix128::from_ratio(1, 100),
slide_velocity_threshold: Fix128::from_ratio(1, 2),
max_events_per_frame: 32,
}
}
}
pub struct AudioGenerator {
pub config: AudioConfig,
pub materials: Vec<AudioMaterial>,
pub events: Vec<AudioEvent>,
}
impl AudioGenerator {
#[must_use]
pub fn new(num_bodies: usize, config: AudioConfig) -> Self {
Self {
config,
materials: vec![AudioMaterial::WOOD; num_bodies],
events: Vec::new(),
}
}
pub fn set_material(&mut self, body_idx: usize, material: AudioMaterial) {
if body_idx >= self.materials.len() {
self.materials.resize(body_idx + 1, AudioMaterial::WOOD);
}
self.materials[body_idx] = material;
}
pub fn begin_frame(&mut self) {
self.events.clear();
}
pub fn process_contact(
&mut self,
body_a: usize,
body_b: usize,
contact: &Contact,
relative_velocity: Vec3Fix,
is_new_contact: bool,
) {
if self.events.len() >= self.config.max_events_per_frame {
return;
}
let speed = relative_velocity.length();
if speed < self.config.min_velocity {
return;
}
let mat_a = self.get_material(body_a);
let mat_b = self.get_material(body_b);
let normal_speed = relative_velocity.dot(contact.normal).abs();
let tangential_sq = speed * speed - normal_speed * normal_speed;
let tangential_speed = if tangential_sq > Fix128::ZERO {
tangential_sq.sqrt()
} else {
Fix128::ZERO
};
let event_type = if is_new_contact {
AudioEventType::Impact
} else if tangential_speed > self.config.slide_velocity_threshold {
AudioEventType::Slide
} else {
AudioEventType::Roll
};
let volume = self.compute_volume(speed, &mat_a, &mat_b);
let pitch = self.compute_pitch(speed, &mat_a, &mat_b);
let decay = self.compute_decay(&mat_a, &mat_b);
let brightness = self.compute_brightness(speed, &mat_a, &mat_b);
let roughness = self.compute_roughness(tangential_speed, &mat_a, &mat_b);
if volume < self.config.min_volume {
return;
}
self.events.push(AudioEvent {
position: contact.point_a,
volume,
pitch,
decay,
brightness,
roughness,
event_type,
material_a: mat_a.material_type,
material_b: mat_b.material_type,
});
}
fn get_material(&self, body_idx: usize) -> AudioMaterial {
if body_idx < self.materials.len() {
self.materials[body_idx]
} else {
AudioMaterial::WOOD
}
}
fn compute_volume(
&self,
speed: Fix128,
_mat_a: &AudioMaterial,
_mat_b: &AudioMaterial,
) -> Fix128 {
let normalized = speed / self.config.max_velocity;
let clamped = if normalized > Fix128::ONE {
Fix128::ONE
} else {
normalized
};
clamped.sqrt()
}
fn compute_pitch(&self, speed: Fix128, mat_a: &AudioMaterial, mat_b: &AudioMaterial) -> Fix128 {
let base = Fix128::ONE + speed * self.config.velocity_pitch_factor;
let avg_density = (mat_a.density + mat_b.density).half();
if avg_density.is_zero() {
return base;
}
let density_factor = Fix128::from_int(1000) / avg_density;
let density_factor = if density_factor > Fix128::from_int(2) {
Fix128::from_int(2)
} else if density_factor < Fix128::from_ratio(1, 2) {
Fix128::from_ratio(1, 2)
} else {
density_factor
};
base * density_factor
}
#[allow(clippy::unused_self)]
fn compute_decay(&self, mat_a: &AudioMaterial, mat_b: &AudioMaterial) -> Fix128 {
let avg_resonance = (mat_a.resonance + mat_b.resonance).half();
let avg_damping = (mat_a.damping + mat_b.damping).half();
let base_decay = Fix128::from_ratio(1, 10); let resonance_bonus = avg_resonance * Fix128::from_int(2);
let damping_reduction = avg_damping;
base_decay + resonance_bonus * (Fix128::ONE - damping_reduction)
}
fn compute_brightness(
&self,
speed: Fix128,
mat_a: &AudioMaterial,
mat_b: &AudioMaterial,
) -> Fix128 {
let avg_hardness = (mat_a.hardness + mat_b.hardness).half();
let speed_factor = speed / self.config.max_velocity;
let speed_factor = if speed_factor > Fix128::ONE {
Fix128::ONE
} else {
speed_factor
};
avg_hardness * (Fix128::from_ratio(1, 2) + speed_factor.half())
}
fn compute_roughness(
&self,
tangential_speed: Fix128,
mat_a: &AudioMaterial,
mat_b: &AudioMaterial,
) -> Fix128 {
let avg_hardness = (mat_a.hardness + mat_b.hardness).half();
let speed_factor = tangential_speed / self.config.max_velocity;
let speed_factor = if speed_factor > Fix128::ONE {
Fix128::ONE
} else {
speed_factor
};
speed_factor * avg_hardness
}
#[must_use]
pub fn get_events(&self) -> &[AudioEvent] {
&self.events
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audio_generator() {
let mut gen = AudioGenerator::new(2, AudioConfig::default());
gen.set_material(0, AudioMaterial::METAL);
gen.set_material(1, AudioMaterial::STONE);
gen.begin_frame();
let contact = Contact {
depth: Fix128::from_ratio(1, 10),
normal: Vec3Fix::UNIT_Y,
point_a: Vec3Fix::ZERO,
point_b: Vec3Fix::ZERO,
};
gen.process_contact(0, 1, &contact, Vec3Fix::from_f32(0.0, -5.0, 0.0), true);
assert_eq!(gen.events.len(), 1);
let event = &gen.events[0];
assert_eq!(event.event_type, AudioEventType::Impact);
assert!(event.volume > Fix128::ZERO);
}
#[test]
fn test_material_presets() {
let metal = AudioMaterial::METAL;
let rubber = AudioMaterial::RUBBER;
assert!(metal.hardness > rubber.hardness);
assert!(metal.resonance > rubber.resonance);
}
#[test]
fn test_volume_scaling() {
let gen = AudioGenerator::new(2, AudioConfig::default());
let mat = AudioMaterial::WOOD;
let soft = gen.compute_volume(Fix128::from_int(1), &mat, &mat);
let hard = gen.compute_volume(Fix128::from_int(10), &mat, &mat);
assert!(hard > soft, "Harder impact should be louder");
}
#[test]
fn test_below_threshold() {
let mut gen = AudioGenerator::new(2, AudioConfig::default());
gen.begin_frame();
let contact = Contact {
depth: Fix128::from_ratio(1, 100),
normal: Vec3Fix::UNIT_Y,
point_a: Vec3Fix::ZERO,
point_b: Vec3Fix::ZERO,
};
gen.process_contact(0, 1, &contact, Vec3Fix::from_f32(0.0, -0.01, 0.0), true);
assert!(
gen.events.is_empty(),
"Below-threshold impacts should be silent"
);
}
#[test]
fn test_slide_detection() {
let mut gen = AudioGenerator::new(2, AudioConfig::default());
gen.begin_frame();
let contact = Contact {
depth: Fix128::from_ratio(1, 10),
normal: Vec3Fix::UNIT_Y,
point_a: Vec3Fix::ZERO,
point_b: Vec3Fix::ZERO,
};
gen.process_contact(
0,
1,
&contact,
Vec3Fix::from_f32(5.0, 0.0, 0.0), false, );
assert_eq!(gen.events.len(), 1);
assert_eq!(gen.events[0].event_type, AudioEventType::Slide);
}
}