Skip to main content

voirs_spatial/position/
source_manager.rs

1//! Spatial source management and Doppler processing
2
3use super::occlusion::{OcclusionDetector, OcclusionResult};
4use super::prediction::{MotionPredictor, MotionSnapshot};
5use super::spatial_grid::SpatialGrid;
6use super::types::SoundSource;
7use crate::types::Position3D;
8use std::collections::{HashMap, VecDeque};
9use std::time::{Duration, Instant};
10
11/// Dynamic source manager for spatial audio
12pub struct SpatialSourceManager {
13    /// Active sound sources
14    pub sources: HashMap<String, SoundSource>,
15    /// Spatial awareness grid for optimization
16    spatial_grid: SpatialGrid,
17    /// Occlusion detector
18    occlusion_detector: OcclusionDetector,
19    /// Maximum number of concurrent sources
20    max_sources: usize,
21    /// Distance-based culling threshold
22    pub culling_distance: f32,
23    /// Update frequency for spatial calculations
24    update_frequency: f32,
25}
26
27/// Doppler effect processor for moving sources and listeners
28#[derive(Debug, Clone)]
29pub struct DopplerProcessor {
30    /// Speed of sound (m/s)
31    speed_of_sound: f32,
32    /// Sample rate
33    sample_rate: f32,
34    /// Maximum Doppler shift factor (safety limit)
35    max_doppler_factor: f32,
36    /// Smoothing factor for Doppler transitions
37    smoothing_factor: f32,
38}
39
40/// Dynamic source manager for handling moving sources
41#[derive(Debug, Clone)]
42pub struct DynamicSourceManager {
43    /// Dynamic sources
44    sources: HashMap<String, DynamicSource>,
45    /// Doppler processor
46    doppler_processor: DopplerProcessor,
47    /// Motion predictor
48    motion_predictor: MotionPredictor,
49}
50
51/// Dynamic source with motion tracking and prediction
52#[derive(Debug, Clone)]
53pub struct DynamicSource {
54    /// Base source
55    pub base_source: SoundSource,
56    /// Velocity tracking
57    pub velocity: Position3D,
58    /// Acceleration tracking
59    pub acceleration: Position3D,
60    /// Motion history for prediction
61    pub motion_history: VecDeque<MotionSnapshot>,
62    /// Current Doppler factor
63    pub doppler_factor: f32,
64    /// Smoothed Doppler factor
65    pub smoothed_doppler_factor: f32,
66    /// Last Doppler update time
67    pub last_doppler_update: Option<Instant>,
68}
69
70impl SpatialSourceManager {
71    /// Create new spatial source manager
72    pub fn new(bounds: (Position3D, Position3D), cell_size: f32) -> Self {
73        Self {
74            sources: HashMap::new(),
75            spatial_grid: SpatialGrid::new(bounds, cell_size),
76            occlusion_detector: OcclusionDetector::new(),
77            max_sources: 64,
78            culling_distance: 100.0,
79            update_frequency: 60.0,
80        }
81    }
82
83    /// Add source to manager
84    pub fn add_source(&mut self, source: SoundSource) -> crate::Result<()> {
85        if self.sources.len() >= self.max_sources {
86            return Err(crate::Error::LegacyPosition(
87                "Maximum sources exceeded".to_string(),
88            ));
89        }
90
91        let source_id = source.id.clone();
92        let position = source.position();
93
94        // Add to spatial grid
95        self.spatial_grid.add_source(&source_id, position);
96
97        // Add to sources
98        self.sources.insert(source_id, source);
99
100        Ok(())
101    }
102
103    /// Remove source from manager
104    pub fn remove_source(&mut self, source_id: &str) -> Option<SoundSource> {
105        if let Some(source) = self.sources.remove(source_id) {
106            self.spatial_grid.remove_source(source_id);
107            Some(source)
108        } else {
109            None
110        }
111    }
112
113    /// Update source position
114    pub fn update_source_position(
115        &mut self,
116        source_id: &str,
117        position: Position3D,
118    ) -> crate::Result<()> {
119        if let Some(source) = self.sources.get_mut(source_id) {
120            let old_position = source.position();
121            source.set_position(position);
122
123            // Update spatial grid
124            self.spatial_grid
125                .move_source(source_id, old_position, position);
126
127            Ok(())
128        } else {
129            Err(crate::Error::LegacyPosition(format!(
130                "Source not found: {source_id}"
131            )))
132        }
133    }
134
135    /// Get nearby sources for listener
136    pub fn get_nearby_sources(
137        &self,
138        listener_position: Position3D,
139        radius: f32,
140    ) -> Vec<&SoundSource> {
141        let nearby_ids = self.spatial_grid.query_sphere(listener_position, radius);
142        nearby_ids
143            .iter()
144            .filter_map(|id| self.sources.get(id))
145            .collect()
146    }
147
148    /// Check occlusion for source
149    pub fn check_occlusion(
150        &self,
151        source_position: Position3D,
152        listener_position: Position3D,
153    ) -> OcclusionResult {
154        self.occlusion_detector
155            .check_occlusion(source_position, listener_position)
156    }
157
158    /// Cull distant sources
159    pub fn cull_distant_sources(&mut self, listener_position: Position3D) {
160        let culling_distance_sq = self.culling_distance * self.culling_distance;
161        let distant_sources: Vec<String> = self
162            .sources
163            .iter()
164            .filter(|(_, source)| {
165                let distance_sq = listener_position.distance_to(&source.position()).powi(2);
166                distance_sq > culling_distance_sq
167            })
168            .map(|(id, _)| id.clone())
169            .collect();
170
171        for source_id in distant_sources {
172            self.remove_source(&source_id);
173        }
174    }
175
176    /// Get all active sources
177    pub fn get_active_sources(&self) -> Vec<&SoundSource> {
178        self.sources.values().filter(|s| s.is_active()).collect()
179    }
180}
181
182impl DopplerProcessor {
183    /// Create new Doppler processor
184    pub fn new(sample_rate: f32) -> Self {
185        Self {
186            speed_of_sound: 343.0, // m/s at 20°C
187            sample_rate,
188            max_doppler_factor: 2.0, // Safety limit to prevent extreme shifts
189            smoothing_factor: 0.95,  // Smooth Doppler transitions
190        }
191    }
192
193    /// Create Doppler processor with custom speed of sound
194    pub fn with_speed_of_sound(sample_rate: f32, speed_of_sound: f32) -> Self {
195        Self {
196            speed_of_sound,
197            sample_rate,
198            max_doppler_factor: 2.0,
199            smoothing_factor: 0.95,
200        }
201    }
202
203    /// Calculate Doppler factor for source and listener
204    pub fn calculate_doppler_factor(
205        &self,
206        source_position: Position3D,
207        source_velocity: Position3D,
208        listener_position: Position3D,
209        listener_velocity: Position3D,
210    ) -> f32 {
211        // Vector from source to listener
212        let source_to_listener = Position3D::new(
213            listener_position.x - source_position.x,
214            listener_position.y - source_position.y,
215            listener_position.z - source_position.z,
216        );
217
218        let distance = source_to_listener.magnitude();
219        if distance < 0.001 {
220            return 1.0; // Avoid division by zero
221        }
222
223        // Unit vector from source to listener
224        let direction = source_to_listener.normalized();
225
226        // Radial velocities (positive means approaching for source, receding for listener)
227        let source_radial_velocity = source_velocity.dot(&direction); // Positive = towards listener
228        let listener_radial_velocity = -listener_velocity.dot(&direction); // Positive = towards source
229
230        // Classic Doppler formula: f' = f * (v + vr) / (v - vs)
231        // where vr is listener radial velocity, vs is source radial velocity
232        // Positive source velocity means approaching (should increase frequency)
233        let numerator = self.speed_of_sound + listener_radial_velocity;
234        let denominator = self.speed_of_sound - source_radial_velocity;
235
236        if denominator.abs() < 0.001 {
237            return 1.0; // Avoid division by zero
238        }
239
240        let doppler_factor = numerator / denominator;
241
242        // Clamp to reasonable limits
243        doppler_factor.clamp(1.0 / self.max_doppler_factor, self.max_doppler_factor)
244    }
245
246    /// Apply Doppler effect to audio buffer
247    pub fn process_doppler_effect(
248        &self,
249        input: &[f32],
250        output: &mut [f32],
251        doppler_factor: f32,
252    ) -> crate::Result<()> {
253        if input.len() != output.len() {
254            return Err(crate::Error::LegacyProcessing(
255                "Input and output buffers must have the same length".to_string(),
256            ));
257        }
258
259        if (doppler_factor - 1.0).abs() < 0.001 {
260            // No significant Doppler effect, just copy
261            output.copy_from_slice(input);
262            return Ok(());
263        }
264
265        // Simple pitch shifting using linear interpolation
266        // In production, would use more sophisticated algorithms like PSOLA or phase vocoder
267        let pitch_ratio = doppler_factor;
268        let mut read_pos = 0.0;
269
270        for i in 0..output.len() {
271            let read_index = read_pos as usize;
272            let read_frac = read_pos - read_index as f32;
273
274            if read_index + 1 < input.len() {
275                // Linear interpolation
276                let sample1 = input[read_index];
277                let sample2 = input[read_index + 1];
278                output[i] = sample1 + read_frac * (sample2 - sample1);
279            } else if read_index < input.len() {
280                output[i] = input[read_index];
281            } else {
282                output[i] = 0.0;
283            }
284
285            read_pos += pitch_ratio;
286            if read_pos >= input.len() as f32 {
287                // Fill remaining samples with zero
288                output[i + 1..].fill(0.0);
289                break;
290            }
291        }
292
293        Ok(())
294    }
295
296    /// Smooth Doppler factor transitions
297    pub fn smooth_doppler_factor(&self, current: f32, target: f32) -> f32 {
298        current * self.smoothing_factor + target * (1.0 - self.smoothing_factor)
299    }
300
301    /// Set speed of sound (useful for different atmospheric conditions)
302    pub fn set_speed_of_sound(&mut self, speed: f32) {
303        self.speed_of_sound = speed;
304    }
305
306    /// Get current speed of sound
307    pub fn speed_of_sound(&self) -> f32 {
308        self.speed_of_sound
309    }
310}
311
312impl DynamicSourceManager {
313    /// Create new dynamic source manager
314    pub fn new(sample_rate: f32) -> Self {
315        Self {
316            sources: HashMap::new(),
317            doppler_processor: DopplerProcessor::new(sample_rate),
318            motion_predictor: MotionPredictor::new(),
319        }
320    }
321
322    /// Add dynamic source
323    pub fn add_source(&mut self, source: SoundSource) -> crate::Result<()> {
324        let dynamic_source = DynamicSource::new(source);
325        self.sources
326            .insert(dynamic_source.base_source.id.clone(), dynamic_source);
327        Ok(())
328    }
329
330    /// Remove dynamic source
331    pub fn remove_source(&mut self, source_id: &str) -> Option<DynamicSource> {
332        self.sources.remove(source_id)
333    }
334
335    /// Update source motion
336    pub fn update_source_motion(
337        &mut self,
338        source_id: &str,
339        position: Position3D,
340        velocity: Position3D,
341        acceleration: Position3D,
342    ) -> crate::Result<()> {
343        if let Some(source) = self.sources.get_mut(source_id) {
344            source.update_motion(position, velocity, acceleration);
345            Ok(())
346        } else {
347            Err(crate::Error::LegacyPosition(format!(
348                "Source '{source_id}' not found"
349            )))
350        }
351    }
352
353    /// Process all dynamic sources with Doppler effects
354    pub async fn process_dynamic_sources(
355        &mut self,
356        listener_position: Position3D,
357        listener_velocity: Position3D,
358    ) -> crate::Result<()> {
359        for source in self.sources.values_mut() {
360            // Update Doppler factor
361            let doppler_factor = self.doppler_processor.calculate_doppler_factor(
362                source.base_source.position(),
363                source.velocity,
364                listener_position,
365                listener_velocity,
366            );
367
368            // Smooth the Doppler factor
369            source.smoothed_doppler_factor = self
370                .doppler_processor
371                .smooth_doppler_factor(source.smoothed_doppler_factor, doppler_factor);
372
373            source.doppler_factor = doppler_factor;
374            source.last_doppler_update = Some(Instant::now());
375        }
376
377        Ok(())
378    }
379
380    /// Get all dynamic sources
381    pub fn sources(&self) -> &HashMap<String, DynamicSource> {
382        &self.sources
383    }
384
385    /// Get dynamic source by ID
386    pub fn get_source(&self, source_id: &str) -> Option<&DynamicSource> {
387        self.sources.get(source_id)
388    }
389
390    /// Get mutable dynamic source by ID
391    pub fn get_source_mut(&mut self, source_id: &str) -> Option<&mut DynamicSource> {
392        self.sources.get_mut(source_id)
393    }
394
395    /// Predict source positions for latency compensation
396    pub fn predict_source_positions(
397        &self,
398        prediction_time: Duration,
399    ) -> HashMap<String, Position3D> {
400        let mut predictions = HashMap::new();
401
402        for (id, source) in &self.sources {
403            if let Some(predicted_pos) = self
404                .motion_predictor
405                .predict_position(&source.motion_history, prediction_time)
406            {
407                predictions.insert(id.clone(), predicted_pos);
408            }
409        }
410
411        predictions
412    }
413}
414
415impl DynamicSource {
416    /// Create new dynamic source
417    pub fn new(base_source: SoundSource) -> Self {
418        Self {
419            base_source,
420            velocity: Position3D::default(),
421            acceleration: Position3D::default(),
422            motion_history: VecDeque::with_capacity(100),
423            doppler_factor: 1.0,
424            smoothed_doppler_factor: 1.0,
425            last_doppler_update: None,
426        }
427    }
428
429    /// Update motion parameters
430    pub fn update_motion(
431        &mut self,
432        position: Position3D,
433        velocity: Position3D,
434        acceleration: Position3D,
435    ) {
436        self.velocity = velocity;
437        self.acceleration = acceleration;
438
439        // Update position in base source
440        self.base_source.set_position(position);
441
442        // Add to motion history
443        let snapshot = MotionSnapshot {
444            position,
445            velocity,
446            acceleration,
447            timestamp: std::time::SystemTime::now()
448                .duration_since(std::time::UNIX_EPOCH)
449                .unwrap_or_default()
450                .as_secs_f64(),
451        };
452
453        self.motion_history.push_back(snapshot);
454
455        // Keep history size manageable
456        while self.motion_history.len() > 100 {
457            self.motion_history.pop_front();
458        }
459    }
460
461    /// Get predicted position after given time
462    pub fn predict_position(&self, time_delta: Duration) -> Position3D {
463        let dt = time_delta.as_secs_f32();
464        let current_pos = self.base_source.position();
465
466        // Simple kinematic prediction: pos = pos0 + v*t + 0.5*a*t^2
467        Position3D::new(
468            current_pos.x + self.velocity.x * dt + 0.5 * self.acceleration.x * dt * dt,
469            current_pos.y + self.velocity.y * dt + 0.5 * self.acceleration.y * dt * dt,
470            current_pos.z + self.velocity.z * dt + 0.5 * self.acceleration.z * dt * dt,
471        )
472    }
473
474    /// Check if source is moving significantly
475    pub fn is_moving(&self) -> bool {
476        self.velocity.magnitude() > 0.1 // m/s threshold
477    }
478}