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