Skip to main content

rustmotion_core/engine/renderer/
audio_analysis.rs

1use dashmap::DashMap;
2use std::sync::{Arc, OnceLock};
3
4#[derive(Debug, Clone)]
5pub struct AudioAnalysis {
6    pub frame_rate: u32,
7    pub amplitude: Vec<f32>,
8    pub bands: Vec<[f32; 16]>,
9    pub start: f64,
10    pub end: Option<f64>,
11}
12
13impl AudioAnalysis {
14    fn track_time(&self, time: f64) -> Option<f64> {
15        if time < self.start {
16            return None;
17        }
18        if let Some(end) = self.end {
19            if time >= end {
20                return None;
21            }
22        }
23        Some(time - self.start)
24    }
25
26    pub fn amplitude_at(&self, time: f64) -> f32 {
27        let Some(time) = self.track_time(time) else {
28            return 0.0;
29        };
30        let idx = (time * self.frame_rate as f64) as usize;
31        self.amplitude
32            .get(idx.min(self.amplitude.len().saturating_sub(1)))
33            .copied()
34            .unwrap_or(0.0)
35    }
36
37    pub fn band_at(&self, time: f64, band: u8) -> f32 {
38        let Some(time) = self.track_time(time) else {
39            return 0.0;
40        };
41        let idx = (time * self.frame_rate as f64) as usize;
42        let idx = idx.min(self.bands.len().saturating_sub(1));
43        self.bands
44            .get(idx)
45            .map(|b| b[band.min(15) as usize])
46            .unwrap_or(0.0)
47    }
48
49    pub fn amplitude_smoothed(&self, time: f64, smoothing_frames: u32) -> f32 {
50        if smoothing_frames == 0 || self.amplitude.is_empty() {
51            return self.amplitude_at(time);
52        }
53        let Some(time) = self.track_time(time) else {
54            return 0.0;
55        };
56        let end_idx = (time * self.frame_rate as f64) as usize;
57        let end_idx = end_idx.min(self.amplitude.len().saturating_sub(1));
58        let start_idx = end_idx.saturating_sub(smoothing_frames as usize);
59        let window = &self.amplitude[start_idx..=end_idx];
60        if window.is_empty() {
61            return 0.0;
62        }
63        window.iter().sum::<f32>() / window.len() as f32
64    }
65
66    pub fn band_smoothed(&self, time: f64, band: u8, smoothing_frames: u32) -> f32 {
67        if smoothing_frames == 0 || self.bands.is_empty() {
68            return self.band_at(time, band);
69        }
70        let Some(time) = self.track_time(time) else {
71            return 0.0;
72        };
73        let end_idx = (time * self.frame_rate as f64) as usize;
74        let end_idx = end_idx.min(self.bands.len().saturating_sub(1));
75        let start_idx = end_idx.saturating_sub(smoothing_frames as usize);
76        let window = &self.bands[start_idx..=end_idx];
77        if window.is_empty() {
78            return 0.0;
79        }
80        window.iter().map(|b| b[band.min(15) as usize]).sum::<f32>() / window.len() as f32
81    }
82}
83
84type AudioCacheMap = Arc<DashMap<String, Arc<AudioAnalysis>>>;
85
86static AUDIO_ANALYSIS_CACHE: OnceLock<AudioCacheMap> = OnceLock::new();
87
88pub fn audio_analysis_cache() -> &'static AudioCacheMap {
89    AUDIO_ANALYSIS_CACHE.get_or_init(|| Arc::new(DashMap::new()))
90}