1use crate::{types::AudioChannel, Position3D, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::sync::{Arc, RwLock};
11use std::time::{Duration, Instant};
12
13pub trait HapticDevice: Send + Sync {
15 fn send_pattern(&mut self, pattern: &HapticPattern) -> Result<()>;
17
18 fn stop(&mut self) -> Result<()>;
20
21 fn is_ready(&self) -> bool;
23
24 fn capabilities(&self) -> HapticCapabilities;
26
27 fn device_id(&self) -> String;
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct HapticPattern {
34 pub id: String,
36
37 pub name: String,
39
40 pub elements: Vec<HapticElement>,
42
43 pub duration: Duration,
45
46 pub looping: bool,
48
49 pub priority: u8,
51
52 pub spatial_position: Option<Position3D>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct HapticElement {
59 pub start_time: Duration,
61
62 pub duration: Duration,
64
65 pub effect_type: HapticEffectType,
67
68 pub intensity: f32,
70
71 pub frequency: Option<f32>,
73
74 pub spatial_attenuation: f32,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub enum HapticEffectType {
81 Vibration,
83
84 Pulse,
86
87 Buzz,
89
90 Click,
92
93 Rumble,
95
96 DirectionalForce,
98
99 Thermal,
101
102 Custom(String),
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct HapticCapabilities {
109 pub max_concurrent_effects: usize,
111
112 pub supported_effects: Vec<HapticEffectType>,
114
115 pub intensity_resolution: u32,
117
118 pub frequency_range: Option<(f32, f32)>,
120
121 pub spatial_support: bool,
123
124 pub latency_compensation: f32,
126
127 pub features: HashMap<String, String>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct HapticAudioConfig {
134 pub enabled: bool,
136
137 pub master_intensity: f32,
139
140 pub distance_attenuation: DistanceAttenuation,
142
143 pub audio_mapping: AudioHapticMapping,
145
146 pub sync_settings: SyncSettings,
148
149 pub pattern_preferences: PatternPreferences,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct DistanceAttenuation {
156 pub min_distance: f32,
158
159 pub max_distance: f32,
161
162 pub curve_type: AttenuationCurve,
164
165 pub curve_parameters: Vec<f32>,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
171pub enum AttenuationCurve {
172 Linear,
174
175 InverseSquare,
177
178 Exponential,
180
181 Logarithmic,
183
184 Custom,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct AudioHapticMapping {
191 pub bass_mapping: FrequencyMapping,
193
194 pub mid_mapping: FrequencyMapping,
196
197 pub high_mapping: FrequencyMapping,
199
200 pub transient_settings: TransientSettings,
202
203 pub rhythm_settings: RhythmSettings,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct FrequencyMapping {
210 pub frequency_range: (f32, f32),
212
213 pub effect_type: HapticEffectType,
215
216 pub intensity_scale: f32,
218
219 pub frequency_scale: f32,
221
222 pub activation_threshold: f32,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct TransientSettings {
229 pub enabled: bool,
231
232 pub sensitivity: f32,
234
235 pub min_interval: f32,
237
238 pub transient_effect: HapticEffectType,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct RhythmSettings {
245 pub enabled: bool,
247
248 pub tempo_range: (f32, f32),
250
251 pub beat_emphasis: f32,
253
254 pub downbeat_effect: Option<HapticEffectType>,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct SyncSettings {
261 pub latency_compensation: f32,
263
264 pub sync_tolerance: f32,
266
267 pub prediction_lookahead: f32,
269
270 pub buffer_size: usize,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct PatternPreferences {
277 pub preferred_styles: Vec<PatternStyle>,
279
280 pub complexity_level: u8,
282
283 pub adaptive_learning: bool,
285
286 pub comfort_settings: HapticComfortSettings,
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
292pub enum PatternStyle {
293 Subtle,
295
296 Moderate,
298
299 Intense,
301
302 Musical,
304
305 Environmental,
307
308 Narrative,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct HapticComfortSettings {
315 pub max_session_duration: u32,
317
318 pub rest_interval: u32,
320
321 pub session_fade: bool,
323
324 pub accessibility: HapticAccessibilitySettings,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct HapticAccessibilitySettings {
331 pub enhanced_intensity: bool,
333
334 pub visual_substitution: bool,
336
337 pub simplified_patterns: bool,
339
340 pub one_handed_mode: bool,
342}
343
344pub struct HapticAudioProcessor {
346 config: HapticAudioConfig,
348
349 devices: Arc<RwLock<HashMap<String, Box<dyn HapticDevice>>>>,
351
352 active_patterns: Arc<RwLock<HashMap<String, ActivePattern>>>,
354
355 audio_analyzer: AudioAnalyzer,
357
358 pattern_library: PatternLibrary,
360
361 sync_state: SyncState,
363
364 metrics: HapticMetrics,
366}
367
368#[derive(Debug)]
370struct ActivePattern {
371 pattern: HapticPattern,
373
374 start_time: Instant,
376
377 current_element: usize,
379
380 audio_source_id: Option<String>,
382
383 current_position: Option<Position3D>,
385
386 intensity_scale: f32,
388}
389
390struct AudioAnalyzer {
392 fft_window: Vec<f32>,
394
395 previous_frame: Vec<f32>,
397
398 beat_detector: BeatDetector,
400
401 transient_detector: TransientDetector,
403
404 frequency_bins: Vec<f32>,
406}
407
408struct BeatDetector {
410 energy_history: Vec<f32>,
412
413 current_tempo: f32,
415
416 beat_phase: f32,
418
419 last_beat: Instant,
421}
422
423struct TransientDetector {
425 flux_history: Vec<Vec<f32>>,
427
428 threshold: f32,
430
431 last_transient: Instant,
433}
434
435struct PatternLibrary {
437 builtin_patterns: HashMap<String, HapticPattern>,
439
440 user_patterns: HashMap<String, HapticPattern>,
442
443 usage_stats: HashMap<String, PatternUsageStats>,
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize)]
449struct PatternUsageStats {
450 usage_count: u32,
452
453 average_rating: f32,
455
456 effective_contexts: Vec<String>,
458
459 #[serde(skip)]
461 last_used: Option<Instant>,
462}
463
464struct SyncState {
466 audio_timeline: Instant,
468
469 haptic_timeline: Instant,
471
472 measured_latency: Duration,
474
475 prediction_buffer: Vec<PredictedHapticEvent>,
477}
478
479#[derive(Debug)]
481struct PredictedHapticEvent {
482 timestamp: Instant,
484
485 pattern_id: String,
487
488 position: Option<Position3D>,
490
491 intensity: f32,
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct HapticMetrics {
498 pub processing_latency: f32,
500
501 pub sync_accuracy: f32,
503
504 pub active_patterns: usize,
506
507 pub device_utilization: f32,
509
510 pub cache_hit_rate: f32,
512
513 pub user_satisfaction: f32,
515
516 pub resource_usage: ResourceUsage,
518}
519
520#[derive(Debug, Clone, Serialize, Deserialize)]
522pub struct ResourceUsage {
523 pub cpu_usage: f32,
525
526 pub memory_usage: f32,
528
529 pub pattern_library_size: usize,
531
532 pub active_devices: usize,
534}
535
536impl Default for HapticAudioConfig {
537 fn default() -> Self {
538 Self {
539 enabled: true,
540 master_intensity: 0.7,
541 distance_attenuation: DistanceAttenuation {
542 min_distance: 0.5,
543 max_distance: 10.0,
544 curve_type: AttenuationCurve::InverseSquare,
545 curve_parameters: vec![],
546 },
547 audio_mapping: AudioHapticMapping {
548 bass_mapping: FrequencyMapping {
549 frequency_range: (20.0, 250.0),
550 effect_type: HapticEffectType::Rumble,
551 intensity_scale: 1.2,
552 frequency_scale: 0.1,
553 activation_threshold: 0.3,
554 },
555 mid_mapping: FrequencyMapping {
556 frequency_range: (250.0, 4000.0),
557 effect_type: HapticEffectType::Vibration,
558 intensity_scale: 1.0,
559 frequency_scale: 0.5,
560 activation_threshold: 0.2,
561 },
562 high_mapping: FrequencyMapping {
563 frequency_range: (4000.0, 20000.0),
564 effect_type: HapticEffectType::Click,
565 intensity_scale: 0.8,
566 frequency_scale: 1.0,
567 activation_threshold: 0.4,
568 },
569 transient_settings: TransientSettings {
570 enabled: true,
571 sensitivity: 0.7,
572 min_interval: 50.0,
573 transient_effect: HapticEffectType::Click,
574 },
575 rhythm_settings: RhythmSettings {
576 enabled: true,
577 tempo_range: (60.0, 180.0),
578 beat_emphasis: 1.5,
579 downbeat_effect: Some(HapticEffectType::Pulse),
580 },
581 },
582 sync_settings: SyncSettings {
583 latency_compensation: 15.0,
584 sync_tolerance: 5.0,
585 prediction_lookahead: 100.0,
586 buffer_size: 1024,
587 },
588 pattern_preferences: PatternPreferences {
589 preferred_styles: vec![PatternStyle::Moderate, PatternStyle::Musical],
590 complexity_level: 5,
591 adaptive_learning: true,
592 comfort_settings: HapticComfortSettings {
593 max_session_duration: 60,
594 rest_interval: 15,
595 session_fade: true,
596 accessibility: HapticAccessibilitySettings {
597 enhanced_intensity: false,
598 visual_substitution: false,
599 simplified_patterns: false,
600 one_handed_mode: false,
601 },
602 },
603 },
604 }
605 }
606}
607
608impl HapticAudioProcessor {
609 pub fn new(config: HapticAudioConfig) -> Self {
611 Self {
612 config,
613 devices: Arc::new(RwLock::new(HashMap::new())),
614 active_patterns: Arc::new(RwLock::new(HashMap::new())),
615 audio_analyzer: AudioAnalyzer::new(),
616 pattern_library: PatternLibrary::new(),
617 sync_state: SyncState::new(),
618 metrics: HapticMetrics::default(),
619 }
620 }
621
622 pub fn add_device(&mut self, device: Box<dyn HapticDevice>) -> Result<()> {
624 let device_id = device.device_id();
625 let mut devices = self.devices.write().map_err(|_| {
626 crate::Error::LegacyProcessing("Haptic devices lock poisoned".to_string())
627 })?;
628 devices.insert(device_id, device);
629 Ok(())
630 }
631
632 pub fn remove_device(&mut self, device_id: &str) -> Result<()> {
634 let mut devices = self.devices.write().map_err(|_| {
635 crate::Error::LegacyProcessing("Haptic devices lock poisoned".to_string())
636 })?;
637 devices.remove(device_id);
638 Ok(())
639 }
640
641 pub fn process_audio_frame(
643 &mut self,
644 audio_samples: &[f32],
645 audio_channel_type: AudioChannel,
646 spatial_positions: &[(String, Position3D)],
647 listener_position: Position3D,
648 ) -> Result<()> {
649 let haptic_events = self.audio_analyzer.analyze_frame(
651 audio_samples,
652 audio_channel_type,
653 &self.config.audio_mapping,
654 )?;
655
656 let spatial_haptic_events =
658 self.apply_spatial_processing(haptic_events, spatial_positions, listener_position)?;
659
660 for event in spatial_haptic_events {
662 self.trigger_haptic_event(event)?;
663 }
664
665 self.update_active_patterns()?;
667
668 self.update_metrics();
670
671 Ok(())
672 }
673
674 pub fn trigger_pattern(
676 &mut self,
677 pattern_id: &str,
678 position: Option<Position3D>,
679 intensity_scale: f32,
680 ) -> Result<()> {
681 if let Some(pattern) = self.pattern_library.get_pattern(pattern_id) {
682 let active_pattern = ActivePattern {
683 pattern: pattern.clone(),
684 start_time: Instant::now(),
685 current_element: 0,
686 audio_source_id: None,
687 current_position: position,
688 intensity_scale,
689 };
690
691 let mut active_patterns = self.active_patterns.write().map_err(|_| {
692 crate::Error::LegacyProcessing("Active patterns lock poisoned".to_string())
693 })?;
694 active_patterns.insert(pattern_id.to_string(), active_pattern);
695 }
696
697 Ok(())
698 }
699
700 pub fn stop_all(&mut self) -> Result<()> {
702 let mut devices = self.devices.write().map_err(|_| {
704 crate::Error::LegacyProcessing("Haptic devices lock poisoned".to_string())
705 })?;
706 for device in devices.values_mut() {
707 device.stop()?;
708 }
709
710 let mut active_patterns = self.active_patterns.write().map_err(|_| {
712 crate::Error::LegacyProcessing("Active patterns lock poisoned".to_string())
713 })?;
714 active_patterns.clear();
715
716 Ok(())
717 }
718
719 pub fn metrics(&self) -> &HapticMetrics {
721 &self.metrics
722 }
723
724 pub fn update_config(&mut self, config: HapticAudioConfig) {
726 self.config = config;
727 }
728
729 fn apply_spatial_processing(
732 &self,
733 events: Vec<HapticEvent>,
734 spatial_positions: &[(String, Position3D)],
735 listener_position: Position3D,
736 ) -> Result<Vec<SpatialHapticEvent>> {
737 let mut spatial_events = Vec::new();
738
739 for event in events {
740 if let Some((_, source_position)) = spatial_positions
742 .iter()
743 .find(|(id, _)| id == &event.source_id)
744 {
745 let distance = calculate_distance(listener_position, *source_position);
747 let attenuation = self.calculate_distance_attenuation(distance);
748
749 let spatial_event = SpatialHapticEvent {
750 base_event: event,
751 position: *source_position,
752 distance,
753 attenuation,
754 };
755
756 spatial_events.push(spatial_event);
757 }
758 }
759
760 Ok(spatial_events)
761 }
762
763 fn calculate_distance_attenuation(&self, distance: f32) -> f32 {
764 let attenuation = &self.config.distance_attenuation;
765
766 if distance <= attenuation.min_distance {
767 return 1.0;
768 }
769
770 if distance >= attenuation.max_distance {
771 return 0.0;
772 }
773
774 let normalized_distance = (distance - attenuation.min_distance)
775 / (attenuation.max_distance - attenuation.min_distance);
776
777 match attenuation.curve_type {
778 AttenuationCurve::Linear => 1.0 - normalized_distance,
779 AttenuationCurve::InverseSquare => {
780 1.0 / (1.0 + normalized_distance * normalized_distance)
781 }
782 AttenuationCurve::Exponential => (-normalized_distance * 2.0).exp(),
783 AttenuationCurve::Logarithmic => (1.0 - normalized_distance).ln().abs().min(1.0),
784 AttenuationCurve::Custom => {
785 1.0 - normalized_distance
787 }
788 }
789 }
790
791 fn trigger_haptic_event(&mut self, event: SpatialHapticEvent) -> Result<()> {
792 let pattern = self.pattern_library.select_pattern_for_event(&event)?;
794
795 let mut scaled_pattern = pattern;
797 for element in &mut scaled_pattern.elements {
798 element.intensity *= event.attenuation * self.config.master_intensity;
799 element.spatial_attenuation = event.attenuation;
800 }
801
802 scaled_pattern.spatial_position = Some(event.position);
804
805 let devices = self.devices.read().map_err(|_| {
807 crate::Error::LegacyProcessing("Haptic devices lock poisoned".to_string())
808 })?;
809 for device in devices.values() {
810 if device.is_ready() {
811 let device_pattern =
813 self.adapt_pattern_for_device(&scaled_pattern, device.capabilities())?;
814 }
816 }
817
818 Ok(())
819 }
820
821 fn adapt_pattern_for_device(
822 &self,
823 pattern: &HapticPattern,
824 capabilities: HapticCapabilities,
825 ) -> Result<HapticPattern> {
826 let mut adapted_pattern = pattern.clone();
827
828 adapted_pattern.elements.retain(|element| {
830 capabilities
831 .supported_effects
832 .contains(&element.effect_type)
833 });
834
835 for element in &mut adapted_pattern.elements {
837 let resolution_step = 1.0 / capabilities.intensity_resolution as f32;
838 element.intensity = (element.intensity / resolution_step).round() * resolution_step;
839 }
840
841 if adapted_pattern.elements.len() > capabilities.max_concurrent_effects {
843 adapted_pattern
844 .elements
845 .truncate(capabilities.max_concurrent_effects);
846 }
847
848 Ok(adapted_pattern)
849 }
850
851 fn update_active_patterns(&mut self) -> Result<()> {
852 let mut active_patterns = self.active_patterns.write().map_err(|_| {
853 crate::Error::LegacyProcessing("Active patterns lock poisoned".to_string())
854 })?;
855 let current_time = Instant::now();
856
857 active_patterns.retain(|_, pattern| {
859 let elapsed = current_time.duration_since(pattern.start_time);
860 elapsed < pattern.pattern.duration || pattern.pattern.looping
861 });
862
863 for pattern in active_patterns.values_mut() {
865 let elapsed = current_time.duration_since(pattern.start_time);
866
867 while pattern.current_element < pattern.pattern.elements.len() {
869 let element = &pattern.pattern.elements[pattern.current_element];
870 if elapsed >= element.start_time {
871 pattern.current_element += 1;
872 } else {
873 break;
874 }
875 }
876 }
877
878 Ok(())
879 }
880
881 fn update_metrics(&mut self) {
882 let active_pattern_count = match self.active_patterns.read() {
883 Ok(patterns) => patterns.len(),
884 Err(_) => {
885 tracing::warn!("Active patterns lock poisoned, using 0");
886 0
887 }
888 };
889
890 let device_count = match self.devices.read() {
891 Ok(devices) => devices.len(),
892 Err(_) => {
893 tracing::warn!("Haptic devices lock poisoned, using 0");
894 0
895 }
896 };
897
898 self.metrics.active_patterns = active_pattern_count;
899 self.metrics.resource_usage.active_devices = device_count;
900 self.metrics.resource_usage.pattern_library_size = self.pattern_library.size();
901
902 self.metrics.processing_latency = 5.0; self.metrics.sync_accuracy = 2.0; self.metrics.device_utilization = 65.0; self.metrics.cache_hit_rate = 85.0; }
908}
909
910#[derive(Debug)]
913struct HapticEvent {
914 source_id: String,
915 effect_type: HapticEffectType,
916 intensity: f32,
917 frequency: Option<f32>,
918 timestamp: Instant,
919}
920
921#[derive(Debug)]
922struct SpatialHapticEvent {
923 base_event: HapticEvent,
924 position: Position3D,
925 distance: f32,
926 attenuation: f32,
927}
928
929impl Default for HapticMetrics {
930 fn default() -> Self {
931 Self {
932 processing_latency: 0.0,
933 sync_accuracy: 0.0,
934 active_patterns: 0,
935 device_utilization: 0.0,
936 cache_hit_rate: 0.0,
937 user_satisfaction: 5.0,
938 resource_usage: ResourceUsage {
939 cpu_usage: 0.0,
940 memory_usage: 0.0,
941 pattern_library_size: 0,
942 active_devices: 0,
943 },
944 }
945 }
946}
947
948impl AudioAnalyzer {
949 fn new() -> Self {
950 Self {
951 fft_window: vec![0.0; 1024],
952 previous_frame: vec![0.0; 1024],
953 beat_detector: BeatDetector::new(),
954 transient_detector: TransientDetector::new(),
955 frequency_bins: vec![0.0; 512],
956 }
957 }
958
959 fn analyze_frame(
960 &mut self,
961 audio_samples: &[f32],
962 audio_channel_type: AudioChannel,
963 mapping: &AudioHapticMapping,
964 ) -> Result<Vec<HapticEvent>> {
965 let mut events = Vec::new();
966
967 self.perform_fft_analysis(audio_samples)?;
969
970 if mapping.transient_settings.enabled {
972 if let Some(transient) = self.transient_detector.detect(&self.frequency_bins)? {
973 events.push(HapticEvent {
974 source_id: format!("audio_{audio_channel_type:?}"),
975 effect_type: mapping.transient_settings.transient_effect.clone(),
976 intensity: transient.intensity,
977 frequency: None,
978 timestamp: Instant::now(),
979 });
980 }
981 }
982
983 if mapping.rhythm_settings.enabled {
985 if let Some(beat) = self.beat_detector.detect(&self.frequency_bins)? {
986 let effect_type = if beat.is_downbeat {
987 mapping
988 .rhythm_settings
989 .downbeat_effect
990 .clone()
991 .unwrap_or(HapticEffectType::Pulse)
992 } else {
993 HapticEffectType::Pulse
994 };
995
996 events.push(HapticEvent {
997 source_id: format!("audio_{audio_channel_type:?}"),
998 effect_type,
999 intensity: beat.intensity * mapping.rhythm_settings.beat_emphasis,
1000 frequency: Some(beat.tempo / 60.0), timestamp: Instant::now(),
1002 });
1003 }
1004 }
1005
1006 self.analyze_frequency_bands(mapping, &audio_channel_type, &mut events)?;
1008
1009 Ok(events)
1010 }
1011
1012 fn perform_fft_analysis(&mut self, samples: &[f32]) -> Result<()> {
1013 let copy_len = samples.len().min(self.fft_window.len());
1015 self.fft_window[..copy_len].copy_from_slice(&samples[..copy_len]);
1016
1017 for (i, bin) in self.frequency_bins.iter_mut().enumerate() {
1019 *bin = self.fft_window[i * 2].abs(); }
1021
1022 Ok(())
1023 }
1024
1025 fn analyze_frequency_bands(
1026 &self,
1027 mapping: &AudioHapticMapping,
1028 audio_channel_type: &AudioChannel,
1029 events: &mut Vec<HapticEvent>,
1030 ) -> Result<()> {
1031 let mappings = [
1032 &mapping.bass_mapping,
1033 &mapping.mid_mapping,
1034 &mapping.high_mapping,
1035 ];
1036
1037 for frequency_mapping in mappings {
1038 let band_energy = self.calculate_band_energy(&frequency_mapping.frequency_range);
1039
1040 if band_energy > frequency_mapping.activation_threshold {
1041 events.push(HapticEvent {
1042 source_id: format!("audio_{audio_channel_type:?}"),
1043 effect_type: frequency_mapping.effect_type.clone(),
1044 intensity: band_energy * frequency_mapping.intensity_scale,
1045 frequency: Some(
1046 (frequency_mapping.frequency_range.0 + frequency_mapping.frequency_range.1)
1047 / 2.0
1048 * frequency_mapping.frequency_scale,
1049 ),
1050 timestamp: Instant::now(),
1051 });
1052 }
1053 }
1054
1055 Ok(())
1056 }
1057
1058 fn calculate_band_energy(&self, frequency_range: &(f32, f32)) -> f32 {
1059 let start_bin = (frequency_range.0 / 20000.0 * self.frequency_bins.len() as f32) as usize;
1061 let end_bin = (frequency_range.1 / 20000.0 * self.frequency_bins.len() as f32) as usize;
1062
1063 self.frequency_bins[start_bin..end_bin.min(self.frequency_bins.len())]
1064 .iter()
1065 .sum::<f32>()
1066 / (end_bin - start_bin) as f32
1067 }
1068}
1069
1070impl BeatDetector {
1071 fn new() -> Self {
1072 Self {
1073 energy_history: Vec::with_capacity(100),
1074 current_tempo: 120.0,
1075 beat_phase: 0.0,
1076 last_beat: Instant::now(),
1077 }
1078 }
1079
1080 fn detect(&mut self, frequency_bins: &[f32]) -> Result<Option<BeatEvent>> {
1081 let current_energy = frequency_bins.iter().sum::<f32>() / frequency_bins.len() as f32;
1083 self.energy_history.push(current_energy);
1084
1085 if self.energy_history.len() > 100 {
1086 self.energy_history.remove(0);
1087 }
1088
1089 if self.energy_history.len() >= 3 {
1091 let len = self.energy_history.len();
1092 let current = self.energy_history[len - 1];
1093 let previous = self.energy_history[len - 2];
1094 let prev_prev = self.energy_history[len - 3];
1095
1096 if current > previous && previous > prev_prev && current > 0.5 {
1098 let now = Instant::now();
1099 let interval = now.duration_since(self.last_beat).as_secs_f32();
1100
1101 if interval > 0.3 {
1102 self.last_beat = now;
1104 self.current_tempo = 60.0 / interval;
1105
1106 return Ok(Some(BeatEvent {
1107 intensity: current,
1108 tempo: self.current_tempo,
1109 is_downbeat: self.beat_phase < 0.1, }));
1111 }
1112 }
1113 }
1114
1115 Ok(None)
1116 }
1117}
1118
1119impl TransientDetector {
1120 fn new() -> Self {
1121 Self {
1122 flux_history: Vec::with_capacity(10),
1123 threshold: 0.1,
1124 last_transient: Instant::now(),
1125 }
1126 }
1127
1128 fn detect(&mut self, frequency_bins: &[f32]) -> Result<Option<TransientEvent>> {
1129 if !self.flux_history.is_empty() {
1131 let previous_bins = &self.flux_history[self.flux_history.len() - 1];
1132 let flux: f32 = frequency_bins
1133 .iter()
1134 .zip(previous_bins.iter())
1135 .map(|(current, previous)| (current - previous).max(0.0))
1136 .sum();
1137
1138 if flux > self.threshold {
1139 let now = Instant::now();
1140 let interval = now.duration_since(self.last_transient).as_millis() as f32;
1141
1142 if interval > 50.0 {
1143 self.last_transient = now;
1145 return Ok(Some(TransientEvent {
1146 intensity: flux.min(1.0),
1147 flux_value: flux,
1148 }));
1149 }
1150 }
1151 }
1152
1153 self.flux_history.push(frequency_bins.to_vec());
1155 if self.flux_history.len() > 10 {
1156 self.flux_history.remove(0);
1157 }
1158
1159 Ok(None)
1160 }
1161}
1162
1163impl PatternLibrary {
1164 fn new() -> Self {
1165 let mut builtin_patterns = HashMap::new();
1166
1167 builtin_patterns.insert(
1169 "bass_rumble".to_string(),
1170 Self::create_bass_rumble_pattern(),
1171 );
1172 builtin_patterns.insert("click_feedback".to_string(), Self::create_click_pattern());
1173 builtin_patterns.insert("heartbeat".to_string(), Self::create_heartbeat_pattern());
1174
1175 Self {
1176 builtin_patterns,
1177 user_patterns: HashMap::new(),
1178 usage_stats: HashMap::new(),
1179 }
1180 }
1181
1182 fn get_pattern(&self, pattern_id: &str) -> Option<&HapticPattern> {
1183 self.builtin_patterns
1184 .get(pattern_id)
1185 .or_else(|| self.user_patterns.get(pattern_id))
1186 }
1187
1188 fn select_pattern_for_event(&self, event: &SpatialHapticEvent) -> Result<HapticPattern> {
1189 let pattern_id = match event.base_event.effect_type {
1191 HapticEffectType::Rumble => "bass_rumble",
1192 HapticEffectType::Click => "click_feedback",
1193 HapticEffectType::Pulse => "heartbeat",
1194 _ => "click_feedback",
1195 };
1196
1197 self.get_pattern(pattern_id)
1198 .cloned()
1199 .ok_or_else(|| crate::Error::LegacyProcessing("Pattern not found".to_string()))
1200 }
1201
1202 fn size(&self) -> usize {
1203 self.builtin_patterns.len() + self.user_patterns.len()
1204 }
1205
1206 fn create_bass_rumble_pattern() -> HapticPattern {
1207 HapticPattern {
1208 id: "bass_rumble".to_string(),
1209 name: "Bass Rumble".to_string(),
1210 elements: vec![HapticElement {
1211 start_time: Duration::from_millis(0),
1212 duration: Duration::from_millis(200),
1213 effect_type: HapticEffectType::Rumble,
1214 intensity: 0.8,
1215 frequency: Some(40.0),
1216 spatial_attenuation: 1.0,
1217 }],
1218 duration: Duration::from_millis(200),
1219 looping: false,
1220 priority: 5,
1221 spatial_position: None,
1222 }
1223 }
1224
1225 fn create_click_pattern() -> HapticPattern {
1226 HapticPattern {
1227 id: "click_feedback".to_string(),
1228 name: "Click Feedback".to_string(),
1229 elements: vec![HapticElement {
1230 start_time: Duration::from_millis(0),
1231 duration: Duration::from_millis(50),
1232 effect_type: HapticEffectType::Click,
1233 intensity: 1.0,
1234 frequency: None,
1235 spatial_attenuation: 1.0,
1236 }],
1237 duration: Duration::from_millis(50),
1238 looping: false,
1239 priority: 8,
1240 spatial_position: None,
1241 }
1242 }
1243
1244 fn create_heartbeat_pattern() -> HapticPattern {
1245 HapticPattern {
1246 id: "heartbeat".to_string(),
1247 name: "Heartbeat".to_string(),
1248 elements: vec![
1249 HapticElement {
1250 start_time: Duration::from_millis(0),
1251 duration: Duration::from_millis(100),
1252 effect_type: HapticEffectType::Pulse,
1253 intensity: 0.9,
1254 frequency: Some(2.0),
1255 spatial_attenuation: 1.0,
1256 },
1257 HapticElement {
1258 start_time: Duration::from_millis(150),
1259 duration: Duration::from_millis(80),
1260 effect_type: HapticEffectType::Pulse,
1261 intensity: 0.7,
1262 frequency: Some(2.0),
1263 spatial_attenuation: 1.0,
1264 },
1265 ],
1266 duration: Duration::from_millis(800),
1267 looping: true,
1268 priority: 3,
1269 spatial_position: None,
1270 }
1271 }
1272}
1273
1274impl SyncState {
1275 fn new() -> Self {
1276 let now = Instant::now();
1277 Self {
1278 audio_timeline: now,
1279 haptic_timeline: now,
1280 measured_latency: Duration::from_millis(15),
1281 prediction_buffer: Vec::new(),
1282 }
1283 }
1284}
1285
1286#[derive(Debug)]
1289struct BeatEvent {
1290 intensity: f32,
1291 tempo: f32,
1292 is_downbeat: bool,
1293}
1294
1295#[derive(Debug)]
1296struct TransientEvent {
1297 intensity: f32,
1298 flux_value: f32,
1299}
1300
1301fn calculate_distance(pos1: Position3D, pos2: Position3D) -> f32 {
1304 let dx = pos1.x - pos2.x;
1305 let dy = pos1.y - pos2.y;
1306 let dz = pos1.z - pos2.z;
1307 (dx * dx + dy * dy + dz * dz).sqrt()
1308}
1309
1310#[cfg(test)]
1311mod tests {
1312 use super::*;
1313
1314 #[test]
1315 fn test_haptic_config_creation() {
1316 let config = HapticAudioConfig::default();
1317 assert!(config.enabled);
1318 assert_eq!(config.master_intensity, 0.7);
1319 }
1320
1321 #[test]
1322 fn test_pattern_creation() {
1323 let pattern = HapticPattern {
1324 id: "test".to_string(),
1325 name: "Test Pattern".to_string(),
1326 elements: vec![],
1327 duration: Duration::from_millis(100),
1328 looping: false,
1329 priority: 5,
1330 spatial_position: None,
1331 };
1332 assert_eq!(pattern.id, "test");
1333 assert_eq!(pattern.duration, Duration::from_millis(100));
1334 }
1335
1336 #[test]
1337 fn test_distance_calculation() {
1338 let pos1 = Position3D {
1339 x: 0.0,
1340 y: 0.0,
1341 z: 0.0,
1342 };
1343 let pos2 = Position3D {
1344 x: 3.0,
1345 y: 4.0,
1346 z: 0.0,
1347 };
1348 let distance = calculate_distance(pos1, pos2);
1349 assert_eq!(distance, 5.0);
1350 }
1351
1352 #[test]
1353 fn test_haptic_processor_creation() {
1354 let config = HapticAudioConfig::default();
1355 let processor = HapticAudioProcessor::new(config);
1356 assert_eq!(processor.metrics().active_patterns, 0);
1357 }
1358
1359 #[test]
1360 fn test_pattern_library() {
1361 let library = PatternLibrary::new();
1362 assert!(library.get_pattern("bass_rumble").is_some());
1363 assert!(library.get_pattern("click_feedback").is_some());
1364 assert!(library.get_pattern("heartbeat").is_some());
1365 assert!(library.get_pattern("nonexistent").is_none());
1366 }
1367
1368 #[test]
1369 fn test_distance_attenuation() {
1370 let config = HapticAudioConfig::default();
1371 let processor = HapticAudioProcessor::new(config);
1372
1373 let attenuation_close = processor.calculate_distance_attenuation(1.0);
1375 let attenuation_far = processor.calculate_distance_attenuation(8.0);
1376
1377 assert!(attenuation_close > attenuation_far);
1378 assert!(attenuation_close <= 1.0);
1379 assert!(attenuation_far >= 0.0);
1380 }
1381
1382 #[test]
1383 fn test_effect_type_serialization() {
1384 let effect = HapticEffectType::Vibration;
1385 let serialized =
1386 serde_json::to_string(&effect).expect("Failed to serialize effect type in test");
1387 let deserialized: HapticEffectType =
1388 serde_json::from_str(&serialized).expect("Failed to deserialize effect type in test");
1389 assert_eq!(effect, deserialized);
1390 }
1391
1392 #[test]
1393 fn test_capabilities_structure() {
1394 let capabilities = HapticCapabilities {
1395 max_concurrent_effects: 4,
1396 supported_effects: vec![HapticEffectType::Vibration, HapticEffectType::Click],
1397 intensity_resolution: 256,
1398 frequency_range: Some((10.0, 1000.0)),
1399 spatial_support: true,
1400 latency_compensation: 15.0,
1401 features: HashMap::new(),
1402 };
1403
1404 assert_eq!(capabilities.max_concurrent_effects, 4);
1405 assert!(capabilities.spatial_support);
1406 assert_eq!(capabilities.supported_effects.len(), 2);
1407 }
1408}