1use std::ops::Range;
12
13#[must_use]
17pub fn band_energy(frequency: &[f64], magnitude: &[f64], low: f64, high: f64) -> f64 {
18 let band = band(frequency, low, high);
19 if band.is_empty() {
20 return 0.0;
21 }
22
23 let step = step_of(frequency, &band);
24 magnitude[band]
25 .iter()
26 .map(|value| value * value * step)
27 .sum()
28}
29
30#[must_use]
35pub fn band_rms(frequency: &[f64], magnitude: &[f64], low: f64, high: f64) -> f64 {
36 let bandwidth = high - low;
37 if bandwidth <= 0.0 {
38 return 0.0;
39 }
40
41 (band_energy(frequency, magnitude, low, high) / bandwidth).sqrt()
42}
43
44#[must_use]
49pub fn centroid(frequency: &[f64], magnitude: &[f64], low: f64, high: f64) -> Option<f64> {
50 let band = band(frequency, low, high);
51 if band.is_empty() {
52 return None;
53 }
54
55 let (weighted, total) = frequency[band.clone()].iter().zip(&magnitude[band]).fold(
56 (0.0, 0.0),
57 |(weighted, total), (at, value)| {
58 let power = value * value;
59 (weighted + at * power, total + power)
60 },
61 );
62
63 (total > 0.0).then_some(weighted / total)
64}
65
66#[must_use]
71pub fn papr(frequency: &[f64], magnitude: &[f64], low: f64, high: f64) -> Option<f64> {
72 let band = &magnitude[band(frequency, low, high)];
73 if band.is_empty() {
74 return None;
75 }
76
77 let peak = band.iter().copied().fold(0.0_f64, f64::max);
78 let mean = band.iter().sum::<f64>() / band.len() as f64;
79
80 (mean > 0.0).then(|| (peak * peak) / (mean * mean))
81}
82
83#[must_use]
88pub fn rolloff(
89 frequency: &[f64],
90 magnitude: &[f64],
91 low: f64,
92 high: f64,
93 fraction: f64,
94) -> Option<f64> {
95 let band = band(frequency, low, high);
96 if band.is_empty() {
97 return None;
98 }
99
100 let step = step_of(frequency, &band);
101 let total: f64 = magnitude[band.clone()]
102 .iter()
103 .map(|value| value * value * step)
104 .sum();
105 if total <= 0.0 {
106 return None;
107 }
108
109 let target = total * fraction.clamp(0.0, 1.0);
110 let mut accumulated = 0.0;
111
112 for (at, value) in frequency[band.clone()].iter().zip(&magnitude[band.clone()]) {
113 accumulated += value * value * step;
114 if accumulated >= target {
115 return Some(*at);
116 }
117 }
118
119 frequency.get(band.end - 1).copied()
120}
121
122#[must_use]
129pub fn occupied_bandwidth(
130 frequency: &[f64],
131 magnitude: &[f64],
132 low: f64,
133 high: f64,
134 fraction: f64,
135) -> Option<(f64, f64, f64)> {
136 let band = band(frequency, low, high);
137 if band.is_empty() {
138 return None;
139 }
140
141 let step = step_of(frequency, &band);
142 let energies: Vec<f64> = magnitude[band.clone()]
143 .iter()
144 .map(|value| value * value * step)
145 .collect();
146 let total: f64 = energies.iter().sum();
147 if total <= 0.0 {
148 return None;
149 }
150
151 let target = total * fraction.clamp(0.0, 1.0);
152 let mut prefix = vec![0.0; energies.len() + 1];
154 for (index, energy) in energies.iter().enumerate() {
155 prefix[index + 1] = prefix[index] + energy;
156 }
157
158 let (mut lower, mut upper, mut width) = (0, energies.len() - 1, energies.len());
159 let mut left = 0;
160
161 for right in 0..energies.len() {
162 while left < right && prefix[right + 1] - prefix[left + 1] >= target {
165 left += 1;
166 }
167
168 if prefix[right + 1] - prefix[left] >= target && right - left + 1 < width {
169 width = right - left + 1;
170 lower = left;
171 upper = right;
172 }
173 }
174
175 let lower = frequency[band.start + lower];
176 let upper = frequency[band.start + upper];
177
178 Some((lower, upper, upper - lower + step))
179}
180
181fn band(frequency: &[f64], low: f64, high: f64) -> Range<usize> {
183 let start = frequency.partition_point(|at| *at < low);
184 let end = frequency.partition_point(|at| *at <= high);
185
186 start..end.max(start)
187}
188
189fn step_of(frequency: &[f64], band: &Range<usize>) -> f64 {
194 match band.len() {
195 0 => 1.0,
196 1 => match (band.start.checked_sub(1), frequency.get(band.end)) {
197 (Some(before), _) => frequency[band.start] - frequency[before],
198 (None, Some(after)) => after - frequency[band.start],
199 (None, None) => 1.0,
200 },
201 length => (frequency[band.end - 1] - frequency[band.start]) / (length - 1) as f64,
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 fn axis(start: f64, step: f64, count: usize) -> Vec<f64> {
211 (0..count).map(|bin| start + bin as f64 * step).collect()
212 }
213
214 #[test]
215 fn one_bin_of_energy_is_its_square_times_the_bin_width() {
216 let frequency = axis(0.0, 10.0, 3);
217
218 assert!((band_energy(&frequency, &[5.0, 0.0, 0.0], 0.0, 5.0) - 250.0).abs() < 1e-9);
219 }
220
221 #[test]
222 fn band_energy_adds_the_bins_it_covers() {
223 let frequency = axis(0.0, 10.0, 3);
224
225 assert!((band_energy(&frequency, &[1.0, 2.0, 3.0], 0.0, 30.0) - 140.0).abs() < 1e-9);
226 }
227
228 #[test]
229 fn a_band_with_no_bins_has_no_energy() {
230 let frequency = axis(100.0, 10.0, 3);
231
232 assert_eq!(band_energy(&frequency, &[1.0, 2.0, 3.0], 0.0, 50.0), 0.0);
233 }
234
235 #[test]
236 fn a_flat_band_rms_is_the_flat_amplitude() {
237 let frequency = axis(0.0, 10.0, 5);
238
239 assert!((band_rms(&frequency, &[5.0; 5], 0.0, 50.0) - 5.0).abs() < 1e-9);
240 assert_eq!(band_rms(&frequency, &[5.0; 5], 50.0, 50.0), 0.0);
241 }
242
243 #[test]
244 fn a_symmetric_band_balances_at_its_middle() {
245 let frequency = vec![50.0, 100.0, 150.0];
246
247 let centre = centroid(&frequency, &[1.0, 2.0, 1.0], 0.0, 200.0);
248 assert!(centre.is_some_and(|centre| (centre - 100.0).abs() < 1e-9));
249 }
250
251 #[test]
252 fn energy_pulls_the_centroid_towards_itself() {
253 let frequency = vec![100.0, 200.0];
254
255 let centre = centroid(&frequency, &[1.0, 3.0], 0.0, 300.0);
257 assert!(centre.is_some_and(|centre| (centre - 190.0).abs() < 1e-9));
258 }
259
260 #[test]
261 fn a_band_with_no_energy_has_no_centroid() {
262 assert!(centroid(&[100.0, 200.0], &[0.0, 0.0], 0.0, 300.0).is_none());
263 assert!(centroid(&[100.0], &[1.0], 200.0, 300.0).is_none());
264 }
265
266 #[test]
267 fn a_flat_band_peaks_at_its_own_average() {
268 let frequency = vec![50.0, 100.0, 150.0];
269
270 assert!(
271 papr(&frequency, &[5.0; 3], 0.0, 200.0).is_some_and(|papr| (papr - 1.0).abs() < 1e-9)
272 );
273 assert!(papr(&frequency, &[0.1, 10.0, 0.1], 0.0, 200.0).is_some_and(|papr| papr > 5.0));
274 assert!(papr(&frequency, &[5.0; 3], 300.0, 400.0).is_none());
275 }
276
277 #[test]
278 fn rolloff_walks_up_until_the_share_is_met() {
279 let frequency = axis(10.0, 10.0, 5);
280
281 let half = rolloff(&frequency, &[1.0; 5], 0.0, 60.0, 0.5);
282 assert!(half.is_some_and(|at| (20.0..=30.0).contains(&at)));
283
284 let all = rolloff(&frequency, &[1.0; 5], 0.0, 60.0, 1.0);
285 assert!(all.is_some_and(|at| (at - 50.0).abs() < 1e-9));
286 }
287
288 #[test]
289 fn energy_concentrated_low_rolls_off_immediately() {
290 let frequency = axis(10.0, 10.0, 5);
291
292 let at = rolloff(&frequency, &[10.0, 1.0, 1.0, 1.0, 1.0], 0.0, 60.0, 0.95);
293 assert!(at.is_some_and(|at| (at - 10.0).abs() < 1e-9));
294 }
295
296 #[test]
297 fn occupied_bandwidth_finds_the_peak_rather_than_starting_low() {
298 let frequency = axis(10.0, 10.0, 5);
299
300 let (lower, upper, width) =
301 occupied_bandwidth(&frequency, &[0.1, 0.1, 10.0, 0.1, 0.1], 0.0, 60.0, 0.95)
302 .expect("a band with energy has an occupied width");
303
304 assert!((lower - 30.0).abs() < 1e-9);
305 assert!((upper - 30.0).abs() < 1e-9);
306 assert!((width - 10.0).abs() < 1e-9);
307 }
308
309 #[test]
310 fn spread_energy_occupies_the_whole_band() {
311 let frequency = axis(10.0, 10.0, 5);
312
313 let (_, _, width) = occupied_bandwidth(&frequency, &[1.0; 5], 0.0, 60.0, 0.99)
314 .expect("a band with energy has an occupied width");
315
316 assert!(width >= 40.0);
317 }
318
319 #[test]
320 fn a_silent_band_occupies_nothing() {
321 assert!(occupied_bandwidth(&[10.0, 20.0], &[0.0, 0.0], 0.0, 30.0, 0.99).is_none());
322 assert!(occupied_bandwidth(&[10.0, 20.0], &[1.0, 1.0], 100.0, 200.0, 0.99).is_none());
323 }
324
325 #[test]
326 fn a_single_bin_band_borrows_a_neighbours_spacing() {
327 let frequency = axis(0.0, 10.0, 3);
328
329 assert!((band_energy(&frequency, &[1.0, 2.0, 3.0], 6.0, 14.0) - 40.0).abs() < 1e-9);
331 assert!((band_energy(&frequency, &[1.0, 2.0, 3.0], 0.0, 4.0) - 10.0).abs() < 1e-9);
332 }
333}