use crate::{Result, VoirsError};
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Clone, PartialEq)]
pub struct Position3D {
pub x: f32,
pub y: f32,
pub z: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Orientation3D {
pub yaw: f32,
pub pitch: f32,
pub roll: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Velocity3D {
pub x: f32,
pub y: f32,
pub z: f32,
}
#[derive(Debug, Clone)]
pub struct AudioSource3D {
pub position: Position3D,
pub orientation: Orientation3D,
pub velocity: Velocity3D,
pub volume: f32,
pub directivity: DirectivityPattern,
}
#[derive(Debug, Clone)]
pub struct AudioListener3D {
pub position: Position3D,
pub orientation: Orientation3D,
pub velocity: Velocity3D,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DirectivityPattern {
Omnidirectional,
Cardioid,
Bidirectional,
Custom(Vec<f32>),
}
#[derive(Debug, Clone)]
pub struct RoomAcoustics {
pub dimensions: Position3D,
pub absorption: FrequencyResponse,
pub reverb_time: f32,
pub early_reflections_delay: f32,
pub diffusion: f32,
}
#[derive(Debug, Clone)]
pub struct FrequencyResponse {
pub frequencies: Vec<f32>,
pub responses: Vec<f32>,
}
#[derive(Debug, Clone)]
pub struct HrtfConfig {
pub dataset: HrtfDataset,
pub head_circumference: f32,
pub interpupillary_distance: f32,
pub crossfeed_enabled: bool,
pub crossfeed_strength: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum HrtfDataset {
Generic,
Kemar,
Cipic,
Custom(String),
}
#[derive(Debug, Clone)]
pub struct BinauralConfig {
pub hrtf: HrtfConfig,
pub compression_enabled: bool,
pub compression_ratio: f32,
pub sample_rate: u32,
pub buffer_size: usize,
}
#[derive(Debug, Clone)]
pub struct SpatialAudioConfig {
pub enabled: bool,
pub binaural: BinauralConfig,
pub room_acoustics: Option<RoomAcoustics>,
pub speed_of_sound: f32,
pub doppler_strength: f32,
pub distance_model: DistanceModel,
pub max_distance: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DistanceModel {
None,
Linear,
Inverse,
InverseSquare,
}
#[derive(Debug, Clone)]
pub struct SpatialAudioResult {
pub audio: crate::audio::AudioBuffer,
pub config: SpatialAudioConfig,
pub stats: SpatialAudioStats,
}
#[derive(Debug, Clone)]
pub struct SpatialAudioStats {
pub active_sources: usize,
pub latency_ms: f32,
pub cpu_usage: f32,
pub hrtf_quality: f32,
}
#[derive(Debug, Clone)]
pub struct SpatialAudioController {
config: Arc<RwLock<SpatialAudioConfig>>,
sources: Arc<RwLock<Vec<AudioSource3D>>>,
listener: Arc<RwLock<AudioListener3D>>,
}
impl SpatialAudioController {
pub async fn new() -> Result<Self> {
Ok(Self {
config: Arc::new(RwLock::new(SpatialAudioConfig::default())),
sources: Arc::new(RwLock::new(Vec::new())),
listener: Arc::new(RwLock::new(AudioListener3D::default())),
})
}
pub async fn with_config(config: SpatialAudioConfig) -> Result<Self> {
Ok(Self {
config: Arc::new(RwLock::new(config)),
sources: Arc::new(RwLock::new(Vec::new())),
listener: Arc::new(RwLock::new(AudioListener3D::default())),
})
}
pub async fn set_listener(
&self,
position: Position3D,
orientation: Orientation3D,
) -> Result<()> {
let mut listener = self.listener.write().await;
listener.position = position;
listener.orientation = orientation;
Ok(())
}
pub async fn add_source(&self, source: AudioSource3D) -> Result<usize> {
let mut sources = self.sources.write().await;
sources.push(source);
Ok(sources.len() - 1) }
pub async fn update_source_position(
&self,
source_id: usize,
position: Position3D,
) -> Result<()> {
let mut sources = self.sources.write().await;
if let Some(source) = sources.get_mut(source_id) {
source.position = position;
Ok(())
} else {
Err(VoirsError::ConfigError {
field: "source_id".to_string(),
message: format!("Source ID {} not found", source_id),
})
}
}
pub async fn remove_source(&self, source_id: usize) -> Result<()> {
let mut sources = self.sources.write().await;
if source_id < sources.len() {
sources.remove(source_id);
Ok(())
} else {
Err(VoirsError::ConfigError {
field: "source_id".to_string(),
message: format!("Source ID {} not found", source_id),
})
}
}
pub async fn process_spatial_audio(
&self,
audio: &crate::audio::AudioBuffer,
) -> Result<SpatialAudioResult> {
let config = self.config.read().await;
if !config.enabled {
return Err(VoirsError::ConfigError {
field: "spatial".to_string(),
message: "Spatial audio processing is disabled".to_string(),
});
}
let sources = self.sources.read().await;
let listener = self.listener.read().await;
let processed_audio = self
.render_binaural_audio(audio, &sources, &listener, &config)
.await?;
Ok(SpatialAudioResult {
audio: processed_audio,
config: config.clone(),
stats: SpatialAudioStats {
active_sources: sources.len(),
latency_ms: 12.0,
cpu_usage: 15.0,
hrtf_quality: 0.92,
},
})
}
pub async fn set_room_acoustics(&self, room: RoomAcoustics) -> Result<()> {
let mut config = self.config.write().await;
config.room_acoustics = Some(room);
Ok(())
}
pub async fn set_hrtf_config(&self, hrtf: HrtfConfig) -> Result<()> {
let mut config = self.config.write().await;
config.binaural.hrtf = hrtf;
Ok(())
}
pub async fn set_enabled(&self, enabled: bool) -> Result<()> {
let mut config = self.config.write().await;
config.enabled = enabled;
Ok(())
}
pub async fn is_enabled(&self) -> bool {
let config = self.config.read().await;
config.enabled
}
pub async fn get_config(&self) -> SpatialAudioConfig {
self.config.read().await.clone()
}
pub async fn get_sources(&self) -> Vec<AudioSource3D> {
self.sources.read().await.clone()
}
pub async fn get_listener(&self) -> AudioListener3D {
self.listener.read().await.clone()
}
pub async fn apply_preset(&self, preset_name: &str) -> Result<()> {
let config = match preset_name {
"gaming" => SpatialAudioConfig {
enabled: true,
binaural: BinauralConfig {
hrtf: HrtfConfig {
dataset: HrtfDataset::Generic,
head_circumference: 56.0,
interpupillary_distance: 6.4,
crossfeed_enabled: false,
crossfeed_strength: 0.0,
},
compression_enabled: true,
compression_ratio: 3.0,
sample_rate: 44100,
buffer_size: 512,
},
room_acoustics: None,
speed_of_sound: 343.0,
doppler_strength: 0.8,
distance_model: DistanceModel::InverseSquare,
max_distance: 100.0,
},
"cinema" => SpatialAudioConfig {
enabled: true,
binaural: BinauralConfig {
hrtf: HrtfConfig {
dataset: HrtfDataset::Kemar,
head_circumference: 57.0,
interpupillary_distance: 6.5,
crossfeed_enabled: true,
crossfeed_strength: 0.3,
},
compression_enabled: false,
compression_ratio: 1.0,
sample_rate: 48000,
buffer_size: 1024,
},
room_acoustics: Some(RoomAcoustics::default()),
speed_of_sound: 343.0,
doppler_strength: 0.6,
distance_model: DistanceModel::Inverse,
max_distance: 50.0,
},
"vr" => SpatialAudioConfig {
enabled: true,
binaural: BinauralConfig {
hrtf: HrtfConfig {
dataset: HrtfDataset::Cipic,
head_circumference: 55.0,
interpupillary_distance: 6.2,
crossfeed_enabled: false,
crossfeed_strength: 0.0,
},
compression_enabled: true,
compression_ratio: 2.0,
sample_rate: 48000,
buffer_size: 256,
},
room_acoustics: Some(RoomAcoustics::default()),
speed_of_sound: 343.0,
doppler_strength: 1.0,
distance_model: DistanceModel::InverseSquare,
max_distance: 200.0,
},
_ => {
return Err(VoirsError::ConfigError {
field: "preset".to_string(),
message: format!("Unknown spatial audio preset: {}", preset_name),
})
}
};
{
let mut config_guard = self.config.write().await;
*config_guard = config;
}
Ok(())
}
pub fn list_presets(&self) -> Vec<String> {
vec!["gaming".to_string(), "cinema".to_string(), "vr".to_string()]
}
async fn render_binaural_audio(
&self,
audio: &crate::audio::AudioBuffer,
sources: &[AudioSource3D],
listener: &AudioListener3D,
config: &SpatialAudioConfig,
) -> Result<crate::audio::AudioBuffer> {
let mono_samples = audio.samples();
let sample_count = mono_samples.len();
let mut left_channel = vec![0.0f32; sample_count];
let mut right_channel = vec![0.0f32; sample_count];
for source in sources {
let distance = self.calculate_distance(&source.position, &listener.position);
let attenuation = self.calculate_attenuation(distance, &config.distance_model);
let (azimuth, elevation) = self.calculate_direction(&source.position, listener);
let (left_gain, right_gain, left_delay, right_delay) =
self.calculate_hrtf_parameters(azimuth, elevation, &config.binaural.hrtf);
for (i, &sample) in mono_samples.iter().enumerate() {
let spatialized_sample = sample * attenuation * source.volume;
let left_idx = i.saturating_sub(left_delay);
let right_idx = i.saturating_sub(right_delay);
if left_idx < sample_count {
left_channel[left_idx] += spatialized_sample * left_gain;
}
if right_idx < sample_count {
right_channel[right_idx] += spatialized_sample * right_gain;
}
}
}
if config.binaural.hrtf.crossfeed_enabled {
self.apply_crossfeed(
&mut left_channel,
&mut right_channel,
config.binaural.hrtf.crossfeed_strength,
);
}
let mut stereo_samples = Vec::with_capacity(sample_count * 2);
for i in 0..sample_count {
stereo_samples.push(left_channel[i].clamp(-1.0, 1.0));
stereo_samples.push(right_channel[i].clamp(-1.0, 1.0));
}
Ok(crate::audio::AudioBuffer::new(
stereo_samples,
audio.sample_rate(),
2, ))
}
fn calculate_distance(&self, pos1: &Position3D, pos2: &Position3D) -> f32 {
let dx = pos1.x - pos2.x;
let dy = pos1.y - pos2.y;
let dz = pos1.z - pos2.z;
(dx * dx + dy * dy + dz * dz).sqrt()
}
fn calculate_attenuation(&self, distance: f32, model: &DistanceModel) -> f32 {
match model {
DistanceModel::None => 1.0,
DistanceModel::Linear => (1.0 - distance / 100.0).max(0.0),
DistanceModel::Inverse => 1.0 / (1.0 + distance),
DistanceModel::InverseSquare => 1.0 / (1.0 + distance * distance),
}
}
fn calculate_direction(
&self,
source_pos: &Position3D,
listener: &AudioListener3D,
) -> (f32, f32) {
use std::f32::consts::PI;
let dx = source_pos.x - listener.position.x;
let dy = source_pos.y - listener.position.y;
let dz = source_pos.z - listener.position.z;
let forward_angle = listener.orientation.yaw;
let rotated_x = dx * forward_angle.cos() - dz * forward_angle.sin();
let rotated_z = dx * forward_angle.sin() + dz * forward_angle.cos();
let azimuth = rotated_z.atan2(rotated_x);
let horizontal_dist = (rotated_x * rotated_x + rotated_z * rotated_z).sqrt();
let elevation = dy.atan2(horizontal_dist);
(azimuth, elevation)
}
fn calculate_hrtf_parameters(
&self,
azimuth: f32,
elevation: f32,
hrtf_config: &HrtfConfig,
) -> (f32, f32, usize, usize) {
use std::f32::consts::PI;
let head_radius = hrtf_config.head_circumference / (2.0 * PI);
let azimuth_normalized = azimuth / PI; let left_gain = (0.5 + 0.5 * (-azimuth_normalized)).powf(0.7); let right_gain = (0.5 + 0.5 * azimuth_normalized).powf(0.7);
let elevation_factor = (1.0 - elevation.abs() / (PI / 2.0)) * 0.5 + 0.5;
let left_gain = left_gain * elevation_factor;
let right_gain = right_gain * elevation_factor;
let speed_of_sound = 343.0; let max_itd_seconds = head_radius / speed_of_sound * 3.0; let max_itd_samples = (max_itd_seconds * 44100.0) as usize;
let itd = (azimuth.sin() * max_itd_samples as f32) as i32;
let (left_delay, right_delay) = if itd > 0 {
(0, itd as usize) } else {
((-itd) as usize, 0) };
(left_gain, right_gain, left_delay, right_delay)
}
fn apply_crossfeed(&self, left: &mut [f32], right: &mut [f32], strength: f32) {
let crossfeed_gain = strength * 0.3;
let delay_samples = 4;
for i in delay_samples..left.len() {
let left_original = left[i];
let right_original = right[i];
left[i] += right[i - delay_samples] * crossfeed_gain;
right[i] += left_original * crossfeed_gain;
}
}
}
#[derive(Debug, Clone)]
pub struct SpatialAudioControllerBuilder {
config: SpatialAudioConfig,
}
impl SpatialAudioControllerBuilder {
pub fn new() -> Self {
Self {
config: SpatialAudioConfig::default(),
}
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.config.enabled = enabled;
self
}
pub fn hrtf_config(mut self, hrtf: HrtfConfig) -> Self {
self.config.binaural.hrtf = hrtf;
self
}
pub fn room_acoustics(mut self, room: RoomAcoustics) -> Self {
self.config.room_acoustics = Some(room);
self
}
pub fn distance_model(mut self, model: DistanceModel) -> Self {
self.config.distance_model = model;
self
}
pub fn doppler_strength(mut self, strength: f32) -> Self {
self.config.doppler_strength = strength.clamp(0.0, 1.0);
self
}
pub fn max_distance(mut self, distance: f32) -> Self {
self.config.max_distance = distance;
self
}
pub async fn build(self) -> Result<SpatialAudioController> {
let controller = SpatialAudioController::with_config(self.config).await?;
Ok(controller)
}
}
impl Default for SpatialAudioControllerBuilder {
fn default() -> Self {
Self::new()
}
}
impl Default for SpatialAudioConfig {
fn default() -> Self {
Self {
enabled: true,
binaural: BinauralConfig::default(),
room_acoustics: None,
speed_of_sound: 343.0,
doppler_strength: 0.5,
distance_model: DistanceModel::InverseSquare,
max_distance: 100.0,
}
}
}
impl Default for BinauralConfig {
fn default() -> Self {
Self {
hrtf: HrtfConfig::default(),
compression_enabled: false,
compression_ratio: 1.0,
sample_rate: 44100,
buffer_size: 512,
}
}
}
impl Default for HrtfConfig {
fn default() -> Self {
Self {
dataset: HrtfDataset::Generic,
head_circumference: 56.0,
interpupillary_distance: 6.4,
crossfeed_enabled: false,
crossfeed_strength: 0.0,
}
}
}
impl Default for RoomAcoustics {
fn default() -> Self {
Self {
dimensions: Position3D {
x: 10.0,
y: 3.0,
z: 8.0,
},
absorption: FrequencyResponse {
frequencies: vec![125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0],
responses: vec![0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4],
},
reverb_time: 0.5,
early_reflections_delay: 20.0,
diffusion: 0.7,
}
}
}
impl Default for Position3D {
fn default() -> Self {
Self {
x: 0.0,
y: 0.0,
z: 0.0,
}
}
}
impl Default for Orientation3D {
fn default() -> Self {
Self {
yaw: 0.0,
pitch: 0.0,
roll: 0.0,
}
}
}
impl Default for Velocity3D {
fn default() -> Self {
Self {
x: 0.0,
y: 0.0,
z: 0.0,
}
}
}
impl Default for AudioListener3D {
fn default() -> Self {
Self {
position: Position3D::default(),
orientation: Orientation3D::default(),
velocity: Velocity3D::default(),
}
}
}
impl Position3D {
pub fn new(x: f32, y: f32, z: f32) -> Self {
Self { x, y, z }
}
pub fn origin() -> Self {
Self::default()
}
}
impl Orientation3D {
pub fn new(yaw: f32, pitch: f32, roll: f32) -> Self {
Self { yaw, pitch, roll }
}
pub fn identity() -> Self {
Self::default()
}
}
impl AudioSource3D {
pub fn new(position: Position3D, volume: f32) -> Self {
Self {
position,
orientation: Orientation3D::default(),
velocity: Velocity3D::default(),
volume: volume.clamp(0.0, 1.0),
directivity: DirectivityPattern::Omnidirectional,
}
}
pub fn with_directivity(mut self, pattern: DirectivityPattern) -> Self {
self.directivity = pattern;
self
}
pub fn with_velocity(mut self, velocity: Velocity3D) -> Self {
self.velocity = velocity;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_spatial_audio_controller_creation() {
let controller = SpatialAudioController::new().await.unwrap();
assert!(controller.is_enabled().await);
}
#[tokio::test]
async fn test_listener_positioning() {
let controller = SpatialAudioController::new().await.unwrap();
let position = Position3D::new(1.0, 2.0, 3.0);
let orientation = Orientation3D::new(0.5, 0.0, 0.0);
controller
.set_listener(position.clone(), orientation.clone())
.await
.unwrap();
let listener = controller.get_listener().await;
assert_eq!(listener.position, position);
assert_eq!(listener.orientation, orientation);
}
#[tokio::test]
async fn test_source_management() {
let controller = SpatialAudioController::new().await.unwrap();
let source = AudioSource3D::new(Position3D::new(5.0, 0.0, 0.0), 0.8);
let source_id = controller.add_source(source).await.unwrap();
assert_eq!(source_id, 0);
let sources = controller.get_sources().await;
assert_eq!(sources.len(), 1);
assert_eq!(sources[0].volume, 0.8);
controller.remove_source(source_id).await.unwrap();
let sources = controller.get_sources().await;
assert_eq!(sources.len(), 0);
}
#[tokio::test]
async fn test_preset_application() {
let controller = SpatialAudioController::new().await.unwrap();
controller.apply_preset("gaming").await.unwrap();
let config = controller.get_config().await;
assert_eq!(config.distance_model, DistanceModel::InverseSquare);
assert_eq!(config.doppler_strength, 0.8);
}
#[tokio::test]
async fn test_spatial_audio_builder() {
let controller = SpatialAudioControllerBuilder::new()
.enabled(true)
.doppler_strength(0.7)
.max_distance(150.0)
.distance_model(DistanceModel::Inverse)
.build()
.await
.unwrap();
assert!(controller.is_enabled().await);
let config = controller.get_config().await;
assert_eq!(config.doppler_strength, 0.7);
assert_eq!(config.max_distance, 150.0);
assert_eq!(config.distance_model, DistanceModel::Inverse);
}
#[tokio::test]
async fn test_position_distance_calculation() {
let controller = SpatialAudioController::new().await.unwrap();
let pos1 = Position3D::new(0.0, 0.0, 0.0);
let pos2 = Position3D::new(3.0, 4.0, 0.0);
let distance = controller.calculate_distance(&pos1, &pos2);
assert!((distance - 5.0).abs() < 0.001);
}
#[tokio::test]
async fn test_room_acoustics() {
let controller = SpatialAudioController::new().await.unwrap();
let room = RoomAcoustics::default();
controller.set_room_acoustics(room.clone()).await.unwrap();
let config = controller.get_config().await;
assert!(config.room_acoustics.is_some());
assert_eq!(config.room_acoustics.unwrap().reverb_time, 0.5);
}
#[tokio::test]
async fn test_preset_listing() {
let controller = SpatialAudioController::new().await.unwrap();
let presets = controller.list_presets();
assert!(presets.contains(&"gaming".to_string()));
assert!(presets.contains(&"cinema".to_string()));
assert!(presets.contains(&"vr".to_string()));
}
}