1use crate::util::{self, pow_db};
5use ferraus_core::AudioBuffer;
6use serde::Serialize;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
10pub enum Resolution {
11 #[serde(rename = "1/3oct")]
12 ThirdOctave,
13 #[serde(rename = "1/6oct")]
14 SixthOctave,
15 #[serde(rename = "bins")]
16 Bins,
17}
18
19impl Resolution {
20 pub fn parse(s: &str) -> Option<Resolution> {
22 match s.trim().to_ascii_lowercase().as_str() {
23 "1/3oct" | "1/3" | "third" | "1/3-oct" => Some(Resolution::ThirdOctave),
24 "1/6oct" | "1/6" | "sixth" | "1/6-oct" => Some(Resolution::SixthOctave),
25 "bins" | "bin" | "fft" => Some(Resolution::Bins),
26 _ => None,
27 }
28 }
29}
30
31#[derive(Clone, Debug, Serialize, PartialEq)]
34pub struct BandLevel {
35 pub center_hz: f64,
36 pub low_hz: f64,
37 pub high_hz: f64,
38 pub db: f64,
39}
40
41#[derive(Clone, Debug, Serialize)]
44pub struct BinSpectrum {
45 pub bin_hz: f64,
46 pub fft_size: usize,
47 pub db: Vec<f64>,
48}
49
50#[derive(Clone, Debug, Serialize)]
52pub struct SpectrumReport {
53 pub resolution: Resolution,
54 #[serde(skip_serializing_if = "Vec::is_empty")]
55 pub bands: Vec<BandLevel>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub bins: Option<BinSpectrum>,
58 pub centroid_hz: f64,
60 pub rolloff_hz: f64,
62 pub flatness: f64,
64 pub peak_hz: f64,
66}
67
68#[derive(Clone, Debug)]
71pub struct Ltas {
72 pub bin_hz: f64,
73 pub fft_size: usize,
74 pub power: Vec<f64>,
76}
77
78pub fn ltas(buf: &AudioBuffer, n_fft: usize) -> Ltas {
80 let chans: Vec<&[f32]> = buf.data.iter().map(|c| c.as_slice()).collect();
81 let (p, scale) = util::welch_power(&chans, n_fft);
82 Ltas {
83 bin_hz: buf.sample_rate as f64 / n_fft as f64,
84 fft_size: n_fft,
85 power: p.into_iter().map(|v| v * scale).collect(),
86 }
87}
88
89fn band_fft_size(sr: u32) -> usize {
91 util::next_pow2((sr as f64 / 3.0) as usize).clamp(1024, 65536)
92}
93
94pub fn band_centers(fraction: u32, sample_rate: u32) -> Vec<(f64, f64, f64)> {
97 let f = fraction.max(1) as f64;
98 let nyq = sample_rate as f64 / 2.0;
99 let kmin = (f * (20.0f64 / 1000.0).log2()).round() as i32;
100 let kmax = (f * (20000.0f64 / 1000.0).log2()).round() as i32;
101 (kmin..=kmax)
102 .map(|k| {
103 let c = 1000.0 * 2f64.powf(k as f64 / f);
104 (c, c * 2f64.powf(-0.5 / f), c * 2f64.powf(0.5 / f))
105 })
106 .filter(|(c, _, _)| *c < nyq)
107 .map(|(c, lo, hi)| (c, lo, hi.min(nyq)))
108 .collect()
109}
110
111pub fn nominal_hz(c: f64) -> f64 {
113 let digits = if c < 100.0 { 2 } else { 3 };
114 let mag = 10f64.powi(c.log10().floor() as i32 - (digits - 1));
115 let r = (c / mag).round() * mag;
116 match r as i64 {
118 31 | 32 => 31.5,
119 _ => {
120 if (r - 3150.0).abs() < 60.0 {
121 3150.0
122 } else if (r - 315.0).abs() < 6.0 {
123 315.0
124 } else {
125 r
126 }
127 }
128 }
129}
130
131pub fn bands_from_ltas(l: &Ltas, fraction: u32, sample_rate: u32) -> Vec<BandLevel> {
134 let df = l.bin_hz;
135 band_centers(fraction, sample_rate)
136 .into_iter()
137 .map(|(c, lo, hi)| {
138 let k0 = ((lo / df) - 0.5).floor().max(1.0) as usize;
139 let k1 = (((hi / df) + 0.5).ceil() as usize).min(l.power.len() - 1);
140 let mut ms = 0.0;
141 for k in k0..=k1 {
142 let (a, b) = ((k as f64 - 0.5) * df, (k as f64 + 0.5) * df);
143 let ov = (b.min(hi) - a.max(lo)).max(0.0) / df;
144 ms += l.power[k] * ov;
145 }
146 BandLevel { center_hz: nominal_hz(c), low_hz: lo, high_hz: hi, db: (pow_db(ms) + 3.0103).max(-150.0) }
147 })
148 .collect()
149}
150
151pub fn band_levels(buf: &AudioBuffer, fraction: u32) -> Vec<BandLevel> {
153 let l = ltas(buf, band_fft_size(buf.sample_rate));
154 bands_from_ltas(&l, fraction, buf.sample_rate)
155}
156
157pub fn third_octave_levels(buf: &AudioBuffer) -> Vec<BandLevel> {
159 band_levels(buf, 3)
160}
161
162fn descriptors(l: &Ltas) -> (f64, f64, f64, f64) {
163 let p = &l.power;
164 let k0 = ((20.0 / l.bin_hz).ceil() as usize).max(1).min(p.len());
165 let total: f64 = p[k0..].iter().sum();
166 if total <= 1e-30 {
167 return (0.0, 0.0, 0.0, 0.0);
168 }
169 let centroid = p[k0..].iter().enumerate().map(|(i, v)| (i + k0) as f64 * l.bin_hz * v).sum::<f64>() / total;
170 let mut acc = 0.0;
171 let mut rolloff = 0.0;
172 for (i, v) in p[k0..].iter().enumerate() {
173 acc += v;
174 if acc >= 0.85 * total {
175 rolloff = (i + k0) as f64 * l.bin_hz;
176 break;
177 }
178 }
179 let n = (p.len() - k0) as f64;
180 let geo = (p[k0..].iter().map(|v| (v + 1e-30).ln()).sum::<f64>() / n).exp();
181 let flat = (geo / (total / n)).clamp(0.0, 1.0);
182 let (kmax, _) = p.iter().enumerate().skip(k0).fold((0, 0.0), |m, (i, v)| if *v > m.1 { (i, *v) } else { m });
183 (centroid, rolloff, flat, kmax as f64 * l.bin_hz)
184}
185
186pub fn spectrum(buf: &AudioBuffer, resolution: Resolution) -> SpectrumReport {
188 match resolution {
189 Resolution::Bins => {
190 let n_fft = 4096.min(util::next_pow2(buf.frames().max(256)));
191 let l = ltas(buf, n_fft);
192 let (centroid, rolloff, flatness, peak) = descriptors(&l);
193 let db = l.power.iter().map(|p| pow_db(p * 3.0).max(-150.0)).collect();
195 SpectrumReport {
196 resolution,
197 bands: Vec::new(),
198 bins: Some(BinSpectrum { bin_hz: l.bin_hz, fft_size: n_fft, db }),
199 centroid_hz: centroid,
200 rolloff_hz: rolloff,
201 flatness,
202 peak_hz: peak,
203 }
204 }
205 _ => {
206 let fraction = if resolution == Resolution::ThirdOctave { 3 } else { 6 };
207 let l = ltas(buf, band_fft_size(buf.sample_rate));
208 let (centroid, rolloff, flatness, peak) = descriptors(&l);
209 SpectrumReport {
210 resolution,
211 bands: bands_from_ltas(&l, fraction, buf.sample_rate),
212 bins: None,
213 centroid_hz: centroid,
214 rolloff_hz: rolloff,
215 flatness,
216 peak_hz: peak,
217 }
218 }
219 }
220}
221
222#[derive(Clone, Debug, Serialize)]
224pub struct BandDelta {
225 pub center_hz: f64,
226 pub level_db: f64,
227 pub reference_db: f64,
228 pub delta_db: f64,
230}
231
232#[derive(Clone, Debug, Serialize)]
235pub struct ToneBalance {
236 pub offset_db: f64,
238 pub bands: Vec<BandDelta>,
239 pub largest_deviations: Vec<BandDelta>,
240 pub rms_deviation_db: f64,
242}
243
244pub fn tone_balance(levels: &[BandLevel], reference: &[BandLevel]) -> ToneBalance {
246 let curve: Vec<(f64, f64)> = reference.iter().map(|b| (b.center_hz, b.db)).collect();
247 tone_balance_vs_curve(levels, &curve)
248}
249
250pub fn tone_balance_vs_curve(levels: &[BandLevel], curve: &[(f64, f64)]) -> ToneBalance {
253 let mut pts: Vec<(f64, f64)> = curve.iter().copied().filter(|p| p.0 > 0.0).collect();
254 pts.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
255 let at = |f: f64| -> f64 {
256 if pts.is_empty() {
257 return 0.0;
258 }
259 if f <= pts[0].0 {
260 return pts[0].1;
261 }
262 for w in pts.windows(2) {
263 if f <= w[1].0 {
264 let t = (f.ln() - w[0].0.ln()) / (w[1].0.ln() - w[0].0.ln());
265 return w[0].1 + t * (w[1].1 - w[0].1);
266 }
267 }
268 pts.last().unwrap().1
269 };
270 let raw: Vec<(f64, f64, f64)> = levels.iter().map(|b| (b.center_hz, b.db, at(b.center_hz))).collect();
271 let mid: Vec<f64> = raw
272 .iter()
273 .filter(|(c, l, r)| *c >= 60.0 && *c <= 12500.0 && *l > -140.0 && *r > -140.0)
274 .map(|(_, l, r)| l - r)
275 .collect();
276 let offset = util::mean(&mid);
277 let bands: Vec<BandDelta> = raw
278 .iter()
279 .map(|(c, l, r)| BandDelta { center_hz: *c, level_db: *l, reference_db: *r, delta_db: l - r - offset })
280 .collect();
281 let in_range: Vec<&BandDelta> = bands.iter().filter(|b| b.center_hz >= 60.0 && b.center_hz <= 12500.0).collect();
282 let rms = if in_range.is_empty() {
283 0.0
284 } else {
285 (in_range.iter().map(|b| b.delta_db.powi(2)).sum::<f64>() / in_range.len() as f64).sqrt()
286 };
287 let mut largest: Vec<BandDelta> = in_range.into_iter().cloned().collect();
288 largest.sort_by(|a, b| b.delta_db.abs().partial_cmp(&a.delta_db.abs()).unwrap_or(std::cmp::Ordering::Equal));
289 largest.truncate(3);
290 ToneBalance { offset_db: offset, bands, largest_deviations: largest, rms_deviation_db: rms }
291}
292
293pub fn band_power_fraction(l: &Ltas, lo: f64, hi: f64) -> f64 {
295 let total: f64 = l.power.iter().skip(1).sum();
296 if total <= 1e-30 {
297 return 0.0;
298 }
299 let k0 = ((lo / l.bin_hz).round() as usize).max(1);
300 let k1 = ((hi / l.bin_hz).round() as usize).min(l.power.len() - 1);
301 if k1 < k0 {
302 return 0.0;
303 }
304 l.power[k0..=k1].iter().sum::<f64>() / total
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use ferraus_core::testing::{add, noise, sine};
311
312 #[test]
313 fn third_octave_sine_lands_in_its_band() {
314 let b = sine(48000, 1000.0, 2.0, 0.5);
315 let bands = third_octave_levels(&b);
316 let k = bands.iter().position(|x| x.center_hz == 1000.0).unwrap();
317 assert!((bands[k].db - 20.0 * 0.5f64.log10()).abs() < 0.3, "{}", bands[k].db);
318 assert!(bands[k - 2].db < bands[k].db - 40.0);
319 assert_eq!(bands[0].center_hz, 20.0);
320 assert!(bands.iter().any(|b| b.center_hz == 31.5));
321 assert!(bands.iter().any(|b| b.center_hz == 3150.0));
322 }
323
324 #[test]
325 fn low_band_tone_is_measured() {
326 let b = sine(44100, 25.0, 3.0, 0.5);
327 let bands = third_octave_levels(&b);
328 let k = bands.iter().position(|x| x.center_hz == 25.0).unwrap();
329 assert!((bands[k].db + 6.02).abs() < 1.0, "{}", bands[k].db);
330 }
331
332 #[test]
333 fn bins_and_descriptors() {
334 let b = sine(48000, 3000.0, 1.0, 1.0);
335 let r = spectrum(&b, Resolution::Bins);
336 let bins = r.bins.unwrap();
337 let k = (3000.0 / bins.bin_hz).round() as usize;
338 assert!(bins.db[k] > -2.0 && bins.db[k] < 0.5, "{}", bins.db[k]);
339 assert!((r.peak_hz - 3000.0).abs() < bins.bin_hz);
340 assert!(r.flatness < 0.01);
341 let n = noise(48000, 1.0, 0.5, 1);
342 let r = spectrum(&n, Resolution::SixthOctave);
343 assert!(r.flatness > 0.5, "{}", r.flatness);
344 assert!(r.centroid_hz > 9000.0);
345 assert!(r.bands.len() > 55);
346 }
347
348 #[test]
349 fn tone_balance_finds_boost() {
350 let a = add(&noise(48000, 2.0, 0.3, 1), &sine(48000, 2000.0, 2.0, 0.3));
351 let r = noise(48000, 2.0, 0.15, 2);
352 let tb = tone_balance(&third_octave_levels(&a), &third_octave_levels(&r));
353 assert_eq!(tb.largest_deviations[0].center_hz, 2000.0);
354 assert!(tb.largest_deviations[0].delta_db > 6.0);
355 assert!(tb.offset_db > 4.0);
356 }
357
358 #[test]
359 fn silence_is_floored() {
360 let b = AudioBuffer::new(48000, 2, 4800);
361 let r = spectrum(&b, Resolution::ThirdOctave);
362 assert!(r.bands.iter().all(|b| b.db == -150.0));
363 }
364}