voirs_spatial/visual_audio/
processor.rs1use super::analyzer::VisualAudioAnalyzer;
4use super::config::VisualAudioConfig;
5use super::effects::VisualEffectLibrary;
6use super::mapping::ScalingCurve;
7use super::sync::VisualSyncState;
8use super::types::{
9 DirectionZone, SpatialVisualEvent, VisualAudioMetrics, VisualDisplay, VisualEffect, VisualEvent,
10};
11use crate::{types::AudioChannel, Position3D, Result};
12use std::collections::HashMap;
13use std::sync::{Arc, RwLock};
14use std::time::Instant;
15
16pub struct VisualAudioProcessor {
18 config: VisualAudioConfig,
20
21 displays: Arc<RwLock<HashMap<String, Box<dyn VisualDisplay>>>>,
23
24 active_effects: Arc<RwLock<HashMap<String, ActiveVisualEffect>>>,
26
27 audio_analyzer: VisualAudioAnalyzer,
29
30 effect_library: VisualEffectLibrary,
32
33 sync_state: VisualSyncState,
35
36 metrics: VisualAudioMetrics,
38}
39
40#[derive(Debug)]
42struct ActiveVisualEffect {
43 effect: VisualEffect,
45
46 start_time: Instant,
48
49 current_element: usize,
51
52 audio_source_id: Option<String>,
54
55 current_position: Position3D,
57
58 intensity_scale: f32,
60
61 distance: f32,
63}
64
65impl VisualAudioProcessor {
66 pub fn new(config: VisualAudioConfig) -> Self {
68 Self {
69 config,
70 displays: Arc::new(RwLock::new(HashMap::new())),
71 active_effects: Arc::new(RwLock::new(HashMap::new())),
72 audio_analyzer: VisualAudioAnalyzer::new(),
73 effect_library: VisualEffectLibrary::new(),
74 sync_state: VisualSyncState::new(),
75 metrics: VisualAudioMetrics::default(),
76 }
77 }
78
79 pub fn add_display(&mut self, display: Box<dyn VisualDisplay>) -> Result<()> {
81 let display_id = display.display_id();
82 let mut displays = self.displays.write().map_err(|e| {
83 crate::Error::LegacyProcessing(format!(
84 "Failed to acquire write lock on displays: {}",
85 e
86 ))
87 })?;
88 displays.insert(display_id, display);
89 Ok(())
90 }
91
92 pub fn remove_display(&mut self, display_id: &str) -> Result<()> {
94 let mut displays = self.displays.write().map_err(|e| {
95 crate::Error::LegacyProcessing(format!(
96 "Failed to acquire write lock on displays: {}",
97 e
98 ))
99 })?;
100 displays.remove(display_id);
101 Ok(())
102 }
103
104 pub fn process_audio_frame(
106 &mut self,
107 audio_samples: &[f32],
108 audio_channel_type: AudioChannel,
109 spatial_positions: &[(String, Position3D)],
110 listener_position: Position3D,
111 ) -> Result<()> {
112 if !self.config.enabled {
113 return Ok(());
114 }
115
116 let visual_events = self.audio_analyzer.analyze_frame(
118 audio_samples,
119 audio_channel_type,
120 &self.config.audio_mapping,
121 )?;
122
123 let spatial_visual_events =
125 self.apply_spatial_processing(visual_events, spatial_positions, listener_position)?;
126
127 for event in spatial_visual_events {
129 self.trigger_visual_event(event)?;
130 }
131
132 self.update_active_effects()?;
134
135 self.render_to_displays()?;
137
138 self.update_metrics();
140
141 Ok(())
142 }
143
144 pub fn trigger_effect(
146 &mut self,
147 effect_id: &str,
148 position: Position3D,
149 intensity_scale: f32,
150 ) -> Result<()> {
151 if let Some(effect) = self.effect_library.get_effect(effect_id) {
152 let active_effect = ActiveVisualEffect {
153 effect: effect.clone(),
154 start_time: Instant::now(),
155 current_element: 0,
156 audio_source_id: None,
157 current_position: position,
158 intensity_scale,
159 distance: calculate_distance(
160 position,
161 Position3D {
162 x: 0.0,
163 y: 0.0,
164 z: 0.0,
165 },
166 ),
167 };
168
169 let mut active_effects = self.active_effects.write().map_err(|e| {
170 crate::Error::LegacyProcessing(format!(
171 "Failed to acquire write lock on active_effects: {}",
172 e
173 ))
174 })?;
175 active_effects.insert(effect_id.to_string(), active_effect);
176 }
177
178 Ok(())
179 }
180
181 pub fn clear_all_effects(&mut self) -> Result<()> {
183 let mut active_effects = self.active_effects.write().map_err(|e| {
185 crate::Error::LegacyProcessing(format!(
186 "Failed to acquire write lock on active_effects: {}",
187 e
188 ))
189 })?;
190 active_effects.clear();
191
192 let mut displays = self.displays.write().map_err(|e| {
194 crate::Error::LegacyProcessing(format!(
195 "Failed to acquire write lock on displays: {}",
196 e
197 ))
198 })?;
199 for display in displays.values_mut() {
200 display.clear_all()?;
201 }
202
203 Ok(())
204 }
205
206 pub fn metrics(&self) -> &VisualAudioMetrics {
208 &self.metrics
209 }
210
211 pub fn update_config(&mut self, config: VisualAudioConfig) {
213 self.config = config;
214 }
215
216 fn apply_spatial_processing(
219 &self,
220 events: Vec<VisualEvent>,
221 spatial_positions: &[(String, Position3D)],
222 listener_position: Position3D,
223 ) -> Result<Vec<SpatialVisualEvent>> {
224 let mut spatial_events = Vec::new();
225
226 for event in events {
227 let source_position = spatial_positions
229 .iter()
230 .find(|(id, _)| id == &event.source_id)
231 .map(|(_, pos)| *pos)
232 .unwrap_or(Position3D {
233 x: 0.0,
234 y: 0.0,
235 z: 0.0,
236 });
237
238 let distance = calculate_distance(listener_position, source_position);
240 let attenuation = self.calculate_visual_distance_attenuation(distance);
241
242 let spatial_event = SpatialVisualEvent {
243 base_event: event,
244 position: source_position,
245 distance,
246 attenuation,
247 direction_zone: self.calculate_direction_zone(listener_position, source_position),
248 };
249
250 spatial_events.push(spatial_event);
251 }
252
253 Ok(spatial_events)
254 }
255
256 pub(crate) fn calculate_visual_distance_attenuation(&self, distance: f32) -> f32 {
257 let attenuation = &self.config.distance_attenuation;
258
259 if distance <= attenuation.min_distance {
260 return 1.0;
261 }
262
263 if distance >= attenuation.max_distance {
264 return 0.0;
265 }
266
267 let normalized_distance = (distance - attenuation.min_distance)
268 / (attenuation.max_distance - attenuation.min_distance);
269
270 match attenuation.curve_type {
271 ScalingCurve::Linear => 1.0 - normalized_distance,
272 ScalingCurve::Logarithmic => (1.0 - normalized_distance).ln().abs().min(1.0),
273 ScalingCurve::Exponential => (-normalized_distance * 2.0).exp(),
274 ScalingCurve::Power(p) => (1.0 - normalized_distance).powf(p),
275 ScalingCurve::Custom => 1.0 - normalized_distance, }
277 }
278
279 pub(crate) fn calculate_direction_zone(
280 &self,
281 listener_pos: Position3D,
282 source_pos: Position3D,
283 ) -> DirectionZone {
284 let dx = source_pos.x - listener_pos.x;
285 let dy = source_pos.y - listener_pos.y;
286 let dz = source_pos.z - listener_pos.z;
287
288 let azimuth = dx.atan2(dy).to_degrees();
290 let normalized_azimuth = if azimuth < 0.0 {
291 azimuth + 360.0
292 } else {
293 azimuth
294 };
295
296 let elevation = dz.atan2((dx * dx + dy * dy).sqrt()).to_degrees();
298 if elevation > 45.0 {
299 return DirectionZone::Above;
300 } else if elevation < -45.0 {
301 return DirectionZone::Below;
302 }
303
304 match normalized_azimuth {
306 a if a >= 315.0 || a < 45.0 => DirectionZone::Front,
307 a if a >= 45.0 && a < 135.0 => DirectionZone::Right,
308 a if a >= 135.0 && a < 225.0 => DirectionZone::Back,
309 a if a >= 225.0 && a < 315.0 => DirectionZone::Left,
310 _ => DirectionZone::Front,
311 }
312 }
313
314 fn trigger_visual_event(&mut self, event: SpatialVisualEvent) -> Result<()> {
315 let effect = self.effect_library.select_effect_for_event(&event)?;
317
318 let mut scaled_effect = effect;
320 for element in &mut scaled_effect.elements {
321 element.intensity *= event.attenuation * self.config.master_intensity;
322 element.distance_attenuation = event.attenuation;
323
324 if self.config.audio_mapping.directional_cues.enabled {
326 if let Some(direction_color) = self
327 .config
328 .audio_mapping
329 .directional_cues
330 .direction_colors
331 .get(&event.direction_zone)
332 {
333 element.color.r = (element.color.r + direction_color.r) * 0.5;
335 element.color.g = (element.color.g + direction_color.g) * 0.5;
336 element.color.b = (element.color.b + direction_color.b) * 0.5;
337 }
338 }
339 }
340
341 scaled_effect.position = event.position;
343
344 let active_effect = ActiveVisualEffect {
346 effect: scaled_effect,
347 start_time: Instant::now(),
348 current_element: 0,
349 audio_source_id: Some(event.base_event.source_id.clone()),
350 current_position: event.position,
351 intensity_scale: event.attenuation * self.config.master_intensity,
352 distance: event.distance,
353 };
354
355 let mut active_effects = self.active_effects.write().map_err(|e| {
357 crate::Error::LegacyProcessing(format!(
358 "Failed to acquire write lock on active_effects: {}",
359 e
360 ))
361 })?;
362 let effect_id = format!(
363 "{}_{}",
364 event.base_event.source_id,
365 active_effect.start_time.elapsed().as_millis()
366 );
367 active_effects.insert(effect_id, active_effect);
368
369 Ok(())
370 }
371
372 fn update_active_effects(&mut self) -> Result<()> {
373 let mut active_effects = self.active_effects.write().map_err(|e| {
374 crate::Error::LegacyProcessing(format!(
375 "Failed to acquire write lock on active_effects: {}",
376 e
377 ))
378 })?;
379 let current_time = Instant::now();
380
381 active_effects.retain(|_, effect| {
383 let elapsed = current_time.duration_since(effect.start_time);
384 elapsed < effect.effect.duration || effect.effect.looping
385 });
386
387 for effect in active_effects.values_mut() {
389 let elapsed = current_time.duration_since(effect.start_time);
390
391 while effect.current_element < effect.effect.elements.len() {
393 let element = &effect.effect.elements[effect.current_element];
394 if elapsed >= element.start_time {
395 effect.current_element += 1;
396 } else {
397 break;
398 }
399 }
400 }
401
402 Ok(())
403 }
404
405 fn render_to_displays(&mut self) -> Result<()> {
406 let active_effects = self.active_effects.read().map_err(|e| {
407 crate::Error::LegacyProcessing(format!(
408 "Failed to acquire read lock on active_effects: {}",
409 e
410 ))
411 })?;
412 let mut displays = self.displays.write().map_err(|e| {
413 crate::Error::LegacyProcessing(format!(
414 "Failed to acquire write lock on displays: {}",
415 e
416 ))
417 })?;
418
419 for display in displays.values_mut() {
421 if !display.is_ready() {
422 continue;
423 }
424
425 display.clear_all()?;
427
428 for effect in active_effects.values() {
430 display.render_effect(&effect.effect)?;
431 }
432
433 display.update()?;
435 }
436
437 Ok(())
438 }
439
440 fn update_metrics(&mut self) {
441 if let Ok(active_effects) = self.active_effects.read() {
442 if let Ok(displays) = self.displays.read() {
443 self.metrics.active_effects = active_effects.len();
444 self.metrics.resource_usage.active_displays = displays.len();
445 self.metrics.resource_usage.effect_library_size = self.effect_library.size();
446
447 self.metrics.processing_latency = 8.0; self.metrics.sync_accuracy = 3.0; self.metrics.frame_rate = 60.0; self.metrics.gpu_utilization = 45.0; self.metrics.cache_hit_rate = 90.0; }
454 }
455 }
456}
457
458pub(crate) fn calculate_distance(pos1: Position3D, pos2: Position3D) -> f32 {
462 let dx = pos1.x - pos2.x;
463 let dy = pos1.y - pos2.y;
464 let dz = pos1.z - pos2.z;
465 (dx * dx + dy * dy + dz * dz).sqrt()
466}