Skip to main content

dioxus_audio/
analysis.rs

1//! Platform-independent audio analysis helpers.
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4pub enum AnalysisDomain {
5    Waveform,
6    Spectrum,
7}
8
9/// Opaque, cheap-to-clone reader for live audio analysis data.
10#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
11#[derive(Clone, Debug, PartialEq)]
12pub struct AudioAnalyser {
13    node: web_sys::AnalyserNode,
14}
15
16#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
17impl AudioAnalyser {
18    pub(crate) fn new(node: web_sys::AnalyserNode) -> Self {
19        Self { node }
20    }
21
22    pub fn read(&self, domain: AnalysisDomain) -> Vec<f32> {
23        match domain {
24            AnalysisDomain::Waveform => {
25                let mut samples = vec![0_u8; self.node.fft_size() as usize];
26                self.node.get_byte_time_domain_data(&mut samples);
27                samples
28                    .into_iter()
29                    .map(|sample| ((sample as f32 - 128.0) / 128.0).clamp(-1.0, 1.0))
30                    .collect()
31            }
32            AnalysisDomain::Spectrum => {
33                let samples = js_sys::Uint8Array::new_with_length(self.node.frequency_bin_count());
34                self.node.get_byte_frequency_data_with_u8_array(&samples);
35                let mut values = vec![0_u8; samples.length() as usize];
36                samples.copy_to(&mut values);
37                values
38                    .into_iter()
39                    .map(|sample| sample as f32 / 255.0)
40                    .collect()
41            }
42        }
43    }
44
45    pub fn level(&self) -> f32 {
46        let waveform = self.read(AnalysisDomain::Waveform);
47        if waveform.is_empty() {
48            return 0.0;
49        }
50        let mean_square =
51            waveform.iter().map(|sample| sample * sample).sum::<f32>() / waveform.len() as f32;
52        mean_square.sqrt().clamp(0.0, 1.0)
53    }
54
55    pub(crate) fn node(&self) -> &web_sys::AnalyserNode {
56        &self.node
57    }
58}
59
60#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
61#[derive(Clone, Debug, PartialEq)]
62pub struct AudioAnalyser {
63    _private: (),
64}
65
66#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
67impl AudioAnalyser {
68    pub fn read(&self, _domain: AnalysisDomain) -> Vec<f32> {
69        Vec::new()
70    }
71
72    pub fn level(&self) -> f32 {
73        0.0
74    }
75}
76
77/// Reduce amplitude peaks to at most `buckets` values, preserving the maximum
78/// value from each source window.
79pub fn downsample_peaks(peaks: &[u8], buckets: usize) -> Vec<u8> {
80    let bucket_count = peaks.len().min(buckets);
81    if bucket_count == 0 {
82        return Vec::new();
83    }
84
85    (0..bucket_count)
86        .map(|index| {
87            let start = index * peaks.len() / bucket_count;
88            let end = (index + 1) * peaks.len() / bucket_count;
89            peaks[start..end].iter().copied().max().unwrap_or(0)
90        })
91        .collect()
92}
93
94/// Return the largest distance from Web Audio's unsigned silence value (128),
95/// normalized to the full `u8` range.
96pub fn peak_amplitude(samples: &[u8]) -> u8 {
97    let distance = samples
98        .iter()
99        .map(|sample| (*sample as i16 - 128).unsigned_abs())
100        .max()
101        .unwrap_or(0);
102
103    ((u32::from(distance) * 255 + 64) / 128).min(255) as u8
104}
105
106/// A normalized selection within an audio timeline.
107#[derive(Clone, Copy, Debug, PartialEq)]
108pub struct WaveformSelection {
109    start: f64,
110    end: f64,
111}
112
113impl WaveformSelection {
114    pub fn new(start: f64, end: f64) -> Self {
115        let start = if start.is_finite() {
116            start.clamp(0.0, 1.0)
117        } else {
118            0.0
119        };
120        let end = if end.is_finite() {
121            end.clamp(0.0, 1.0)
122        } else {
123            1.0
124        };
125
126        Self {
127            start: start.min(end),
128            end: start.max(end),
129        }
130    }
131
132    pub fn start(self) -> f64 {
133        self.start
134    }
135
136    pub fn end(self) -> f64 {
137        self.end
138    }
139
140    pub fn with_start(self, start: f64) -> Self {
141        let start = if start.is_finite() {
142            start.clamp(0.0, self.end)
143        } else {
144            self.start
145        };
146        Self {
147            start,
148            end: self.end,
149        }
150    }
151
152    pub fn with_end(self, end: f64) -> Self {
153        let end = if end.is_finite() {
154            end.clamp(self.start, 1.0)
155        } else {
156            self.end
157        };
158        Self {
159            start: self.start,
160            end,
161        }
162    }
163}
164
165/// Trim interleaved PCM without splitting channel frames.
166pub fn trim_interleaved_pcm<T: Clone>(
167    samples: &[T],
168    channels: usize,
169    selection: WaveformSelection,
170) -> Vec<T> {
171    if channels == 0 {
172        return Vec::new();
173    }
174    if selection.start() == selection.end() {
175        return Vec::new();
176    }
177
178    let frame_count = samples.len() / channels;
179    let first_frame = (selection.start() * frame_count as f64).floor() as usize;
180    let end_frame = (selection.end() * frame_count as f64).ceil() as usize;
181    samples[first_frame * channels..end_frame.min(frame_count) * channels].to_vec()
182}