sva_samples/measure/
spectrum.rs1use crate::fft::fft;
4
5const MAX_FRAME: usize = 8192;
6const MIN_FRAME: usize = 64;
7
8pub const MAX_PINNED_FRAME: usize = 1 << 20;
10
11pub fn pinned_frame(secs: f64, sample_rate: f64) -> usize {
13 let asked = (secs * sample_rate).round().max(0.0);
14 if !asked.is_finite() || asked > MAX_PINNED_FRAME as f64 {
15 return MAX_PINNED_FRAME * 2;
16 }
17 (asked as usize).next_power_of_two().max(MIN_FRAME)
18}
19const FLOOR_DB: f64 = -160.0;
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, Copy, Debug, PartialEq)]
29pub struct Peak {
30 pub hz: f64,
31 pub db: f64,
32}
33
34#[derive(Clone, Debug, PartialEq)]
35pub struct Spectrum {
36 pub frame_size: usize,
37 pub frames: usize,
38 pub resolution_hz: f64,
39 pub rms: f64,
40 pub centroid_hz: f64,
41 pub rolloff85_hz: f64,
42 pub bands: Vec<Band>,
43 pub peaks: Vec<Peak>,
44}
45
46pub fn analyze(
47 samples: &[f64],
48 sample_rate: f64,
49 max_peaks: usize,
50 frame_secs: Option<f64>,
51) -> Spectrum {
52 let (mags, bin_hz, frame_size, frames) = magnitudes(samples, sample_rate, frame_secs);
53 let power: Vec<f64> = mags.iter().map(|m| m * m).collect();
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 = if samples.is_empty() {
78 0.0
79 } else {
80 (samples.iter().map(|&x| x * x).sum::<f64>() / samples.len() as f64).sqrt()
81 };
82
83 Spectrum {
84 frame_size,
85 frames,
86 resolution_hz: bin_hz,
87 rms,
88 centroid_hz,
89 rolloff85_hz,
90 bands: bands(&power, bin_hz, sample_rate),
91 peaks: peaks(&mags, bin_hz, max_peaks),
92 }
93}
94
95pub fn magnitudes(
99 samples: &[f64],
100 sample_rate: f64,
101 frame_secs: Option<f64>,
102) -> (Vec<f64>, f64, usize, usize) {
103 let frame_size = match frame_secs {
104 Some(secs) => pinned_frame(secs, sample_rate).min(MAX_PINNED_FRAME),
105 None => samples
106 .len()
107 .next_power_of_two()
108 .clamp(MIN_FRAME, MAX_FRAME),
109 };
110 let hop = frame_size / 2;
111 let bins = frame_size / 2 + 1;
112 let window = crate::stft::hann_periodic(frame_size);
113
114 let mut acc = vec![0f64; bins];
115 let mut frames = 0usize;
116 let mut start = 0usize;
117 loop {
118 let mut re = vec![0f64; frame_size];
119 let mut im = vec![0f64; frame_size];
120 for n in 0..frame_size {
121 let s = samples.get(start + n).copied().unwrap_or(0.0);
122 re[n] = s * window[n];
123 }
124 fft(&mut re, &mut im);
125 for (k, a) in acc.iter_mut().enumerate() {
126 *a += re[k] * re[k] + im[k] * im[k];
127 }
128 frames += 1;
129 start += hop;
130 if start + frame_size > samples.len() {
131 break;
132 }
133 }
134
135 let scale = 4.0 / frame_size as f64;
136 let mags = acc
137 .iter()
138 .map(|p| (p / frames as f64).sqrt() * scale)
139 .collect();
140 (mags, sample_rate / frame_size as f64, frame_size, frames)
141}
142
143pub fn db(amplitude: f64) -> f64 {
144 if amplitude <= 0.0 {
145 FLOOR_DB
146 } else {
147 (20.0 * amplitude.log10()).max(FLOOR_DB)
148 }
149}
150
151pub fn third_octave_edges(sample_rate: f64) -> Vec<(f64, f64)> {
153 let nyquist = sample_rate / 2.0;
154 let ratio = 2f64.powf(1.0 / 3.0);
155 let mut out = Vec::new();
156 let mut lo = 20.0;
157 while lo < nyquist {
158 let hi = (lo * ratio).min(nyquist);
159 out.push((lo, hi));
160 lo = hi;
161 }
162 out
163}
164
165fn bands(power: &[f64], bin_hz: f64, sample_rate: f64) -> Vec<Band> {
166 let mut out = Vec::new();
167 for (lo, hi) in third_octave_edges(sample_rate) {
168 let sum: f64 = power
169 .iter()
170 .enumerate()
171 .filter(|(k, _)| {
172 let hz = *k as f64 * bin_hz;
173 hz >= lo && hz < hi
174 })
175 .map(|(_, p)| p)
176 .sum();
177 out.push(Band {
178 lo_hz: lo,
179 hi_hz: hi,
180 db: db(sum.sqrt()),
181 });
182 }
183 out
184}
185
186pub fn peaks(mags: &[f64], bin_hz: f64, max_peaks: usize) -> Vec<Peak> {
189 let mut found: Vec<Peak> = Vec::new();
190 for k in 1..mags.len().saturating_sub(1) {
191 if mags[k] <= mags[k - 1] || mags[k] < mags[k + 1] || mags[k] <= 0.0 {
192 continue;
193 }
194 let (a, b, c) = (mags[k - 1], mags[k], mags[k + 1]);
195 let denom = a - 2.0 * b + c;
196 let delta = if denom.abs() > f64::EPSILON {
197 (0.5 * (a - c) / denom).clamp(-0.5, 0.5)
198 } else {
199 0.0
200 };
201 found.push(Peak {
202 hz: (k as f64 + delta) * bin_hz,
203 db: db(b - 0.25 * (a - c) * delta),
204 });
205 }
206 found.sort_by(|x, y| y.db.total_cmp(&x.db));
207 found.truncate(max_peaks);
208 found
209}