Files
game_engine/crates/voltex_audio/src/occlusion.rs
2026-03-26 16:33:04 +09:00

97 lines
3.1 KiB
Rust

/// Audio occlusion parameters.
#[derive(Debug, Clone, Copy)]
pub struct OcclusionResult {
pub volume_multiplier: f32, // 0.0-1.0, reduced by occlusion
pub lowpass_cutoff: f32, // Hz, lower = more muffled
}
/// Simple ray-based occlusion check.
/// `occlusion_factor`: 0.0 = no occlusion (direct line of sight), 1.0 = fully occluded.
pub fn calculate_occlusion(occlusion_factor: f32) -> OcclusionResult {
let factor = occlusion_factor.clamp(0.0, 1.0);
OcclusionResult {
volume_multiplier: 1.0 - factor * 0.7, // Max 70% volume reduction
lowpass_cutoff: 20000.0 * (1.0 - factor * 0.8), // 20kHz → 4kHz when fully occluded
}
}
/// Simple low-pass filter (one-pole IIR).
pub struct LowPassFilter {
prev_output: f32,
alpha: f32,
}
impl LowPassFilter {
pub fn new(cutoff_hz: f32, sample_rate: u32) -> Self {
let rc = 1.0 / (2.0 * std::f32::consts::PI * cutoff_hz);
let dt = 1.0 / sample_rate as f32;
let alpha = dt / (rc + dt);
LowPassFilter { prev_output: 0.0, alpha }
}
pub fn set_cutoff(&mut self, cutoff_hz: f32, sample_rate: u32) {
let rc = 1.0 / (2.0 * std::f32::consts::PI * cutoff_hz);
let dt = 1.0 / sample_rate as f32;
self.alpha = dt / (rc + dt);
}
pub fn process(&mut self, input: f32) -> f32 {
self.prev_output = self.prev_output + self.alpha * (input - self.prev_output);
self.prev_output
}
pub fn reset(&mut self) {
self.prev_output = 0.0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_occlusion() {
let r = calculate_occlusion(0.0);
assert!((r.volume_multiplier - 1.0).abs() < 1e-6);
assert!((r.lowpass_cutoff - 20000.0).abs() < 1.0);
}
#[test]
fn test_full_occlusion() {
let r = calculate_occlusion(1.0);
assert!((r.volume_multiplier - 0.3).abs() < 1e-6);
assert!((r.lowpass_cutoff - 4000.0).abs() < 1.0);
}
#[test]
fn test_partial_occlusion() {
let r = calculate_occlusion(0.5);
assert!(r.volume_multiplier > 0.3 && r.volume_multiplier < 1.0);
assert!(r.lowpass_cutoff > 4000.0 && r.lowpass_cutoff < 20000.0);
}
#[test]
fn test_lowpass_attenuates_high_freq() {
let mut lpf = LowPassFilter::new(100.0, 44100); // very low cutoff
// High frequency signal (alternating +1/-1) should be attenuated
let mut max_output = 0.0_f32;
for i in 0..100 {
let input = if i % 2 == 0 { 1.0 } else { -1.0 };
let output = lpf.process(input);
max_output = max_output.max(output.abs());
}
assert!(max_output < 0.5, "high freq should be attenuated, got {}", max_output);
}
#[test]
fn test_lowpass_passes_dc() {
let mut lpf = LowPassFilter::new(5000.0, 44100);
// DC signal (constant 1.0) should pass through
for _ in 0..1000 {
lpf.process(1.0);
}
let output = lpf.process(1.0);
assert!((output - 1.0).abs() < 0.01, "DC should pass, got {}", output);
}
}