1use crate::fft::fft;
4pub use crate::measure::peaks::{Peak, db, peaks};
5
6pub const MAX_FRAME: usize = 8192;
7pub const MIN_FRAME: usize = 64;
8
9pub const MAX_PINNED_FRAME: usize = 1 << 20;
11
12pub fn pinned_frame(secs: f64, sample_rate: f64) -> usize {
14 let asked = (secs * sample_rate).round().max(0.0);
15 if !asked.is_finite() || asked > MAX_PINNED_FRAME as f64 {
16 return MAX_PINNED_FRAME * 2;
17 }
18 (asked as usize).next_power_of_two().max(MIN_FRAME)
19}
20
21pub fn fitted_frame(len: usize) -> usize {
23 len.next_power_of_two().clamp(MIN_FRAME, MAX_FRAME)
24}
25
26#[derive(Clone, Copy, Debug, PartialEq)]
27pub struct Band {
28 pub lo_hz: f64,
29 pub hi_hz: f64,
30 pub db: f64,
31}
32
33#[derive(Clone, Debug, PartialEq)]
34pub struct Spectrum {
35 pub frame_size: usize,
36 pub frames: usize,
37 pub resolution_hz: f64,
38 pub rms: f64,
39 pub centroid_hz: f64,
40 pub rolloff85_hz: f64,
41 pub bands: Vec<Band>,
42 pub peaks: Vec<Peak>,
43}
44
45pub fn analyze(
46 planes: &[&[f64]],
47 sample_rate: f64,
48 max_peaks: usize,
49 frame_secs: Option<f64>,
50) -> Spectrum {
51 let Framed {
52 mags,
53 bin_hz,
54 frame_size,
55 frames,
56 spanned,
57 } = magnitudes_of(planes, sample_rate, frame_secs);
58 let power = one_sided(&mags);
59 let total: f64 = power.iter().sum();
60
61 let centroid_hz = if total > 0.0 {
62 power
63 .iter()
64 .enumerate()
65 .map(|(k, p)| k as f64 * bin_hz * p)
66 .sum::<f64>()
67 / total
68 } else {
69 0.0
70 };
71
72 let mut running = 0.0;
73 let mut rolloff85_hz = 0.0;
74 for (k, p) in power.iter().enumerate() {
75 running += p;
76 if total > 0.0 && running >= 0.85 * total {
77 rolloff85_hz = k as f64 * bin_hz;
78 break;
79 }
80 }
81
82 let rms = crate::measure::envelope::level(planes);
83
84 Spectrum {
85 frame_size,
86 frames,
87 resolution_hz: bin_hz,
88 rms,
89 centroid_hz,
90 rolloff85_hz,
91 bands: bands(
92 &power,
93 1.5 * frame_size as f64 / spanned as f64,
94 bin_hz,
95 sample_rate,
96 ),
97 peaks: crate::measure::peaks::resolved(&mags, bin_hz, spanned, max_peaks),
98 }
99}
100
101fn one_sided(mags: &[f64]) -> Vec<f64> {
103 let last = mags.len() - 1;
104 mags.iter()
105 .enumerate()
106 .map(|(k, m)| match k == 0 || k == last {
107 true => m * m / 2.0,
108 false => m * m,
109 })
110 .collect()
111}
112
113pub fn magnitudes(
116 samples: &[f64],
117 sample_rate: f64,
118 frame_secs: Option<f64>,
119) -> (Vec<f64>, f64, usize, usize) {
120 let f = magnitudes_of(&[samples], sample_rate, frame_secs);
121 (f.mags, f.bin_hz, f.frame_size, f.frames)
122}
123
124pub struct Framed {
125 pub mags: Vec<f64>,
126 pub bin_hz: f64,
127 pub frame_size: usize,
128 pub frames: usize,
129 pub spanned: usize,
131}
132
133pub fn magnitudes_of(planes: &[&[f64]], sample_rate: f64, frame_secs: Option<f64>) -> Framed {
134 let len = crate::measure::held(planes);
135 let frame_size = match frame_secs {
136 Some(secs) => pinned_frame(secs, sample_rate).min(MAX_PINNED_FRAME),
137 None => fitted_frame(len),
138 };
139 let bins = frame_size / 2 + 1;
140 let spanned = len.clamp(1, frame_size);
141 let window = crate::stft::hann_periodic(spanned);
142
143 let starts = starts(len, frame_size);
144 let mut acc = vec![0f64; bins];
145 for start in &starts {
146 for samples in planes {
147 let mut re = vec![0f64; frame_size];
148 let mut im = vec![0f64; frame_size];
149 for (n, w) in window.iter().enumerate() {
150 re[n] = samples.get(start + n).copied().unwrap_or(0.0) * w;
151 }
152 fft(&mut re, &mut im);
153 for (k, a) in acc.iter_mut().enumerate() {
154 *a += re[k] * re[k] + im[k] * im[k];
155 }
156 }
157 }
158
159 let scale = 4.0 / spanned as f64;
160 let averaged = (starts.len() * planes.len()) as f64;
161 Framed {
162 mags: acc.iter().map(|p| (p / averaged).sqrt() * scale).collect(),
163 bin_hz: sample_rate / frame_size as f64,
164 frame_size,
165 frames: starts.len(),
166 spanned,
167 }
168}
169
170pub(crate) fn starts(len: usize, frame: usize) -> Vec<usize> {
172 let Some(past) = len.checked_sub(frame).filter(|past| *past > 0) else {
173 return vec![0];
174 };
175 let gaps = past.div_ceil(frame / 2);
176 (0..=gaps)
177 .map(|i| (2 * i * past + gaps) / (2 * gaps))
178 .collect()
179}
180
181pub fn third_octave_edges(sample_rate: f64) -> Vec<(f64, f64)> {
183 let nyquist = sample_rate / 2.0;
184 let ratio = 2f64.powf(1.0 / 3.0);
185 let mut out = Vec::new();
186 let mut lo = 20.0;
187 while lo < nyquist {
188 let hi = (lo * ratio).min(nyquist);
189 out.push((lo, hi));
190 lo = hi;
191 }
192 out
193}
194
195fn bands(power: &[f64], enbw: f64, bin_hz: f64, sample_rate: f64) -> Vec<Band> {
197 let mut out = Vec::new();
198 for (lo, hi) in third_octave_edges(sample_rate) {
199 let sum: f64 = power
200 .iter()
201 .enumerate()
202 .filter(|(k, _)| {
203 let hz = *k as f64 * bin_hz;
204 hz >= lo && hz < hi
205 })
206 .map(|(_, p)| p)
207 .sum();
208 out.push(Band {
209 lo_hz: lo,
210 hi_hz: hi,
211 db: db((sum / enbw).sqrt()),
212 });
213 }
214 out
215}