Skip to main content

voirs_spatial/position/
types.rs

1//! Core types for position tracking and listener/source management
2
3use crate::types::Position3D;
4use serde::{Deserialize, Serialize};
5use std::time::{Duration, Instant};
6
7/// Listener in 3D space with orientation and movement tracking
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Listener {
10    /// Current position
11    position: Position3D,
12    /// Orientation (yaw, pitch, roll) in radians
13    orientation: (f32, f32, f32),
14    /// Velocity vector
15    velocity: Position3D,
16    /// Head radius for HRTF calculations
17    head_radius: f32,
18    /// Inter-aural distance
19    interaural_distance: f32,
20    /// Movement tracking
21    movement_history: Vec<PositionSnapshot>,
22    /// Last update time
23    #[serde(skip)]
24    last_update: Option<Instant>,
25}
26
27/// Sound source in 3D space
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct SoundSource {
30    /// Unique identifier
31    pub id: String,
32    /// Current position
33    position: Position3D,
34    /// Velocity vector
35    velocity: Position3D,
36    /// Source orientation (for directional sources)
37    orientation: Option<(f32, f32, f32)>,
38    /// Source type
39    source_type: SourceType,
40    /// Attenuation parameters
41    attenuation: AttenuationParams,
42    /// Directivity pattern
43    directivity: Option<DirectivityPattern>,
44    /// Movement tracking
45    movement_history: Vec<PositionSnapshot>,
46    /// Active state
47    is_active: bool,
48    /// Last update time
49    #[serde(skip)]
50    last_update: Option<Instant>,
51}
52
53/// Position snapshot for movement tracking
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct PositionSnapshot {
56    /// Position at this time
57    pub position: Position3D,
58    /// Timestamp
59    pub timestamp: f64, // Serializable timestamp
60    /// Velocity at this time
61    pub velocity: Position3D,
62}
63
64/// Source type enumeration
65#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
66pub enum SourceType {
67    /// Point source (omnidirectional)
68    Point,
69    /// Directional source
70    Directional,
71    /// Area source
72    Area {
73        /// Width of the area source in meters
74        width: f32,
75        /// Height of the area source in meters
76        height: f32,
77    },
78    /// Line source
79    Line {
80        /// Length of the line source in meters
81        length: f32,
82    },
83    /// Ambient source (environment)
84    Ambient,
85}
86
87/// Attenuation parameters for sound sources
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct AttenuationParams {
90    /// Reference distance (distance at which attenuation begins)
91    pub reference_distance: f32,
92    /// Maximum distance (beyond which sound is inaudible)
93    pub max_distance: f32,
94    /// Rolloff factor (how quickly sound attenuates)
95    pub rolloff_factor: f32,
96    /// Attenuation model
97    pub model: AttenuationModel,
98}
99
100/// Attenuation model types
101#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
102pub enum AttenuationModel {
103    /// No attenuation
104    None,
105    /// Linear attenuation
106    Linear,
107    /// Inverse distance law
108    Inverse,
109    /// Inverse square law
110    InverseSquare,
111    /// Exponential attenuation
112    Exponential,
113    /// Custom curve
114    Custom,
115}
116
117/// Directivity pattern for directional sources
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct DirectivityPattern {
120    /// Front gain (0 degrees)
121    pub front_gain: f32,
122    /// Back gain (180 degrees)
123    pub back_gain: f32,
124    /// Side gain (90/270 degrees)
125    pub side_gain: f32,
126    /// Directivity index (sharpness of pattern)
127    pub directivity_index: f32,
128    /// Frequency-dependent directivity
129    pub frequency_response: Vec<FrequencyGain>,
130}
131
132/// Frequency-dependent gain
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct FrequencyGain {
135    /// Frequency in Hz
136    pub frequency: f32,
137    /// Gain multiplier
138    pub gain: f32,
139}
140
141/// Orientation snapshot for tracking
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct OrientationSnapshot {
144    /// Orientation at this time (yaw, pitch, roll)
145    pub orientation: (f32, f32, f32),
146    /// Timestamp
147    pub timestamp: f64,
148    /// Angular velocity (rad/s)
149    pub angular_velocity: (f32, f32, f32),
150}
151
152/// 3D bounding box for occlusion detection
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct Box3D {
155    /// Minimum corner
156    pub min: Position3D,
157    /// Maximum corner
158    pub max: Position3D,
159    /// Material ID
160    pub material_id: String,
161}
162
163/// Navigation modes for different use cases
164#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
165pub enum NavigationMode {
166    /// Free 6DOF movement
167    FreeFlight,
168    /// Walking/ground-based movement
169    Walking,
170    /// Seated experience with head tracking only
171    Seated,
172    /// Teleport-based movement
173    Teleport,
174    /// Vehicle/third-person movement
175    Vehicle,
176}
177
178/// Comfort settings for VR movement
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct ComfortSettings {
181    /// Motion sickness reduction (0.0 = disabled, 1.0 = maximum)
182    pub motion_sickness_reduction: f32,
183    /// Snap turning enabled
184    pub snap_turn: bool,
185    /// Snap turn degrees
186    pub snap_turn_degrees: f32,
187    /// Vignetting during movement
188    pub movement_vignetting: bool,
189    /// Ground reference enabled
190    pub ground_reference: bool,
191    /// Movement speed multiplier
192    pub speed_multiplier: f32,
193}
194
195/// Movement constraints and boundaries
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct MovementConstraints {
198    /// Boundary box for movement
199    pub boundary: Option<Box3D>,
200    /// Maximum movement speed (m/s)
201    pub max_speed: f32,
202    /// Maximum acceleration (m/s²)
203    pub max_acceleration: f32,
204    /// Ground height constraint
205    pub ground_height: Option<f32>,
206    /// Ceiling height constraint
207    pub ceiling_height: Option<f32>,
208}
209
210/// Movement performance metrics
211#[derive(Debug, Clone, Default)]
212pub struct MovementMetrics {
213    /// Total distance traveled
214    pub total_distance: f32,
215    /// Average speed
216    pub average_speed: f32,
217    /// Peak speed
218    pub peak_speed: f32,
219    /// Movement duration
220    pub movement_duration: Duration,
221    /// Number of position updates
222    pub update_count: usize,
223    /// Prediction accuracy (when available)
224    pub prediction_accuracy: f32,
225}
226
227/// Supported VR/AR platforms
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
229pub enum PlatformType {
230    /// Generic 6DOF tracking
231    Generic,
232    /// Oculus/Meta platforms
233    Oculus,
234    /// SteamVR/OpenVR
235    SteamVR,
236    /// Apple ARKit
237    ARKit,
238    /// Google ARCore
239    ARCore,
240    /// Microsoft Mixed Reality
241    WMR,
242    /// Custom platform
243    Custom,
244}
245
246/// Platform-specific tracking data
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct PlatformData {
249    /// Device ID or name
250    pub device_id: String,
251    /// Platform-specific pose data
252    pub pose_data: Vec<f32>,
253    /// Tracking confidence (0.0 = lost, 1.0 = perfect)
254    pub tracking_confidence: f32,
255    /// Platform timestamp
256    pub platform_timestamp: u64,
257    /// Additional platform-specific properties
258    pub properties: std::collections::HashMap<String, String>,
259}
260
261/// Calibration data for spatial audio
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct CalibrationData {
264    /// Head circumference for HRTF adjustment
265    pub head_circumference: Option<f32>,
266    /// Inter-pupillary distance
267    pub ipd: Option<f32>,
268    /// Height offset from tracking origin
269    pub height_offset: f32,
270    /// Forward offset from tracking origin
271    pub forward_offset: f32,
272    /// Custom HRTF profile if available
273    pub custom_hrtf_profile: Option<String>,
274}
275
276// Implementations
277
278impl Listener {
279    /// Create new listener at origin
280    pub fn new() -> Self {
281        Self {
282            position: Position3D::default(),
283            orientation: (0.0, 0.0, 0.0),
284            velocity: Position3D::default(),
285            head_radius: 0.0875,        // ~8.75cm average head radius
286            interaural_distance: 0.175, // ~17.5cm average interaural distance
287            movement_history: Vec::new(),
288            last_update: None,
289        }
290    }
291
292    /// Create listener at specific position
293    pub fn at_position(position: Position3D) -> Self {
294        let mut listener = Self::new();
295        listener.position = position;
296        listener
297    }
298
299    /// Get current position
300    pub fn position(&self) -> Position3D {
301        self.position
302    }
303
304    /// Set position and update velocity
305    pub fn set_position(&mut self, position: Position3D) {
306        let now = Instant::now();
307
308        // Calculate velocity if we have a previous update
309        if let Some(last_time) = self.last_update {
310            let time_delta = now.duration_since(last_time).as_secs_f32();
311            if time_delta > 0.0 {
312                self.velocity = Position3D::new(
313                    (position.x - self.position.x) / time_delta,
314                    (position.y - self.position.y) / time_delta,
315                    (position.z - self.position.z) / time_delta,
316                );
317            }
318        }
319
320        // Add to movement history
321        self.movement_history.push(PositionSnapshot {
322            position: self.position,
323            timestamp: now.elapsed().as_secs_f64(),
324            velocity: self.velocity,
325        });
326
327        // Limit history size
328        if self.movement_history.len() > 100 {
329            self.movement_history.remove(0);
330        }
331
332        self.position = position;
333        self.last_update = Some(now);
334    }
335
336    /// Get current orientation
337    pub fn orientation(&self) -> (f32, f32, f32) {
338        self.orientation
339    }
340
341    /// Set orientation
342    pub fn set_orientation(&mut self, orientation: (f32, f32, f32)) {
343        self.orientation = orientation;
344    }
345
346    /// Get current velocity
347    pub fn velocity(&self) -> Position3D {
348        self.velocity
349    }
350
351    /// Get head radius
352    pub fn head_radius(&self) -> f32 {
353        self.head_radius
354    }
355
356    /// Set head radius
357    pub fn set_head_radius(&mut self, radius: f32) {
358        self.head_radius = radius;
359    }
360
361    /// Get interaural distance
362    pub fn interaural_distance(&self) -> f32 {
363        self.interaural_distance
364    }
365
366    /// Set interaural distance
367    pub fn set_interaural_distance(&mut self, distance: f32) {
368        self.interaural_distance = distance;
369    }
370
371    /// Get movement history
372    pub fn movement_history(&self) -> &[PositionSnapshot] {
373        &self.movement_history
374    }
375
376    /// Predict future position based on current velocity
377    pub fn predict_position(&self, time_ahead: Duration) -> Position3D {
378        let delta_time = time_ahead.as_secs_f32();
379        Position3D::new(
380            self.position.x + self.velocity.x * delta_time,
381            self.position.y + self.velocity.y * delta_time,
382            self.position.z + self.velocity.z * delta_time,
383        )
384    }
385
386    /// Calculate left ear position
387    pub fn left_ear_position(&self) -> Position3D {
388        let (yaw, _pitch, _roll) = self.orientation;
389        let offset_x = -self.interaural_distance / 2.0 * yaw.cos();
390        let offset_z = -self.interaural_distance / 2.0 * yaw.sin();
391
392        Position3D::new(
393            self.position.x + offset_x,
394            self.position.y,
395            self.position.z + offset_z,
396        )
397    }
398
399    /// Calculate right ear position
400    pub fn right_ear_position(&self) -> Position3D {
401        let (yaw, _pitch, _roll) = self.orientation;
402        let offset_x = self.interaural_distance / 2.0 * yaw.cos();
403        let offset_z = self.interaural_distance / 2.0 * yaw.sin();
404
405        Position3D::new(
406            self.position.x + offset_x,
407            self.position.y,
408            self.position.z + offset_z,
409        )
410    }
411}
412
413impl Default for Listener {
414    fn default() -> Self {
415        Self::new()
416    }
417}
418
419impl SoundSource {
420    /// Create new point source
421    pub fn new_point(id: String, position: Position3D) -> Self {
422        Self {
423            id,
424            position,
425            velocity: Position3D::default(),
426            orientation: None,
427            source_type: SourceType::Point,
428            attenuation: AttenuationParams::default(),
429            directivity: None,
430            movement_history: Vec::new(),
431            is_active: true,
432            last_update: None,
433        }
434    }
435
436    /// Create new directional source
437    pub fn new_directional(
438        id: String,
439        position: Position3D,
440        orientation: (f32, f32, f32),
441        directivity: DirectivityPattern,
442    ) -> Self {
443        Self {
444            id,
445            position,
446            velocity: Position3D::default(),
447            orientation: Some(orientation),
448            source_type: SourceType::Directional,
449            attenuation: AttenuationParams::default(),
450            directivity: Some(directivity),
451            movement_history: Vec::new(),
452            is_active: true,
453            last_update: None,
454        }
455    }
456
457    /// Get current position
458    pub fn position(&self) -> Position3D {
459        self.position
460    }
461
462    /// Set position and update velocity
463    pub fn set_position(&mut self, position: Position3D) {
464        let now = Instant::now();
465
466        // Calculate velocity if we have a previous update
467        if let Some(last_time) = self.last_update {
468            let time_delta = now.duration_since(last_time).as_secs_f32();
469            if time_delta > 0.0 {
470                self.velocity = Position3D::new(
471                    (position.x - self.position.x) / time_delta,
472                    (position.y - self.position.y) / time_delta,
473                    (position.z - self.position.z) / time_delta,
474                );
475            }
476        }
477
478        // Add to movement history
479        self.movement_history.push(PositionSnapshot {
480            position: self.position,
481            timestamp: now.elapsed().as_secs_f64(),
482            velocity: self.velocity,
483        });
484
485        // Limit history size
486        if self.movement_history.len() > 100 {
487            self.movement_history.remove(0);
488        }
489
490        self.position = position;
491        self.last_update = Some(now);
492    }
493
494    /// Get current velocity
495    pub fn velocity(&self) -> Position3D {
496        self.velocity
497    }
498
499    /// Get orientation
500    pub fn orientation(&self) -> Option<(f32, f32, f32)> {
501        self.orientation
502    }
503
504    /// Set orientation
505    pub fn set_orientation(&mut self, orientation: (f32, f32, f32)) {
506        self.orientation = Some(orientation);
507    }
508
509    /// Get source type
510    pub fn source_type(&self) -> SourceType {
511        self.source_type
512    }
513
514    /// Get attenuation parameters
515    pub fn attenuation(&self) -> &AttenuationParams {
516        &self.attenuation
517    }
518
519    /// Set attenuation parameters
520    pub fn set_attenuation(&mut self, attenuation: AttenuationParams) {
521        self.attenuation = attenuation;
522    }
523
524    /// Get directivity pattern
525    pub fn directivity(&self) -> Option<&DirectivityPattern> {
526        self.directivity.as_ref()
527    }
528
529    /// Set directivity pattern
530    pub fn set_directivity(&mut self, directivity: DirectivityPattern) {
531        self.directivity = Some(directivity);
532    }
533
534    /// Check if source is active
535    pub fn is_active(&self) -> bool {
536        self.is_active
537    }
538
539    /// Set active state
540    pub fn set_active(&mut self, active: bool) {
541        self.is_active = active;
542    }
543
544    /// Calculate gain based on direction for directional sources
545    pub fn calculate_directional_gain(&self, listener_position: Position3D) -> f32 {
546        if let (Some(orientation), Some(directivity)) = (&self.orientation, &self.directivity) {
547            // Calculate direction from source to listener
548            let direction = Position3D::new(
549                listener_position.x - self.position.x,
550                listener_position.y - self.position.y,
551                listener_position.z - self.position.z,
552            );
553
554            // Normalize direction vector
555            let distance = self.position.distance_to(&listener_position);
556            if distance == 0.0 {
557                return 1.0;
558            }
559
560            let normalized_dir = Position3D::new(
561                direction.x / distance,
562                direction.y / distance,
563                direction.z / distance,
564            );
565
566            // Calculate angle between source orientation and listener direction
567            let (yaw, _pitch, _roll) = *orientation;
568            let source_forward = Position3D::new(yaw.cos(), 0.0, yaw.sin());
569
570            // Dot product for angle calculation
571            let dot_product =
572                source_forward.x * normalized_dir.x + source_forward.z * normalized_dir.z;
573            let angle = dot_product.acos();
574
575            // Interpolate gain based on angle
576            let angle_degrees = angle.to_degrees();
577            if angle_degrees <= 45.0 {
578                directivity.front_gain
579            } else if angle_degrees <= 135.0 {
580                directivity.side_gain
581            } else {
582                directivity.back_gain
583            }
584        } else {
585            1.0 // Omnidirectional
586        }
587    }
588
589    /// Predict future position based on current velocity
590    pub fn predict_position(&self, time_ahead: Duration) -> Position3D {
591        let delta_time = time_ahead.as_secs_f32();
592        Position3D::new(
593            self.position.x + self.velocity.x * delta_time,
594            self.position.y + self.velocity.y * delta_time,
595            self.position.z + self.velocity.z * delta_time,
596        )
597    }
598}
599
600impl Default for AttenuationParams {
601    fn default() -> Self {
602        Self {
603            reference_distance: 1.0,
604            max_distance: 100.0,
605            rolloff_factor: 1.0,
606            model: AttenuationModel::Inverse,
607        }
608    }
609}
610
611impl DirectivityPattern {
612    /// Create omnidirectional pattern
613    pub fn omnidirectional() -> Self {
614        Self {
615            front_gain: 1.0,
616            back_gain: 1.0,
617            side_gain: 1.0,
618            directivity_index: 0.0,
619            frequency_response: Vec::new(),
620        }
621    }
622
623    /// Create cardioid pattern
624    pub fn cardioid() -> Self {
625        Self {
626            front_gain: 1.0,
627            back_gain: 0.0,
628            side_gain: 0.5,
629            directivity_index: 3.0,
630            frequency_response: Vec::new(),
631        }
632    }
633
634    /// Create hypercardioid pattern
635    pub fn hypercardioid() -> Self {
636        Self {
637            front_gain: 1.0,
638            back_gain: 0.25,
639            side_gain: 0.375,
640            directivity_index: 6.0,
641            frequency_response: Vec::new(),
642        }
643    }
644}
645
646impl Default for ComfortSettings {
647    fn default() -> Self {
648        Self {
649            motion_sickness_reduction: 0.3,
650            snap_turn: false,
651            snap_turn_degrees: 30.0,
652            movement_vignetting: false,
653            ground_reference: true,
654            speed_multiplier: 1.0,
655        }
656    }
657}
658
659impl Default for MovementConstraints {
660    fn default() -> Self {
661        Self {
662            boundary: None,
663            max_speed: 10.0,        // 10 m/s max speed
664            max_acceleration: 20.0, // 20 m/s² max acceleration
665            ground_height: Some(0.0),
666            ceiling_height: None,
667        }
668    }
669}