1use crate::{
4 C,
5 decay::{energy, first_max, fit_slope, median, percentile},
6};
7use secs_dsp::{
8 fft, interp,
9 response::gd_curve_with_peak,
10 smooth::{Fraction, smooth_complex, smooth_power},
11};
12use std::f64::consts::PI;
13
14#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct Metrics {
17 pub level: f64,
18 pub in1_20: f64,
19 pub in2_20: f64,
20 pub in1_200: f64,
21 pub in2_200: f64,
22 pub in1_2000: f64,
23 pub in2_2000: f64,
24 pub turns: f64,
25 pub gd_20: f64,
26 pub gd_100: f64,
27 pub gd_500: f64,
28 pub gd24_abs: f64,
29 pub gd24_abs_hz: f64,
30 pub gd24_max: f64,
31 pub gd24_span: f64,
32 pub gd_30: f64,
33 pub pre_db: f64,
34}
35
36fn geomspace(start: f64, stop: f64, n: usize) -> Vec<f64> {
37 let lo = start.log10();
38 let step = (stop.log10() - lo) / (n - 1) as f64;
39 let mut out: Vec<_> = (0..n)
40 .map(|i| 10.0_f64.powf(i as f64 * step + lo))
41 .collect();
42 out[0] = start;
43 out[n - 1] = stop;
44 out
45}
46
47fn unwrap(phase: &mut [f64]) {
48 let mut previous = phase[0];
49 let mut correction = 0.0;
50 for p in &mut phase[1..] {
51 let d = *p - previous;
52 previous = *p;
53 let mut dm = (d + PI).rem_euclid(2.0 * PI) - PI;
54 if dm == -PI && d > 0.0 {
55 dm = PI;
56 }
57 if d.abs() >= PI {
58 correction += dm - d;
59 }
60 *p += correction;
61 }
62}
63
64fn gradient(y: &[f64], x: &[f64]) -> Vec<f64> {
65 let n = x.len();
66 let mut out = vec![0.0; n];
67 out[0] = (y[1] - y[0]) / (x[1] - x[0]);
68 out[n - 1] = (y[n - 1] - y[n - 2]) / (x[n - 1] - x[n - 2]);
69 for i in 1..n - 1 {
70 let d1 = x[i] - x[i - 1];
71 let d2 = x[i + 1] - x[i];
72 let a = -d2 / (d1 * (d1 + d2));
73 let b = (d2 - d1) / (d1 * d2);
74 let c = d1 / (d2 * (d1 + d2));
75 out[i] = a * y[i - 1] + b * y[i] + c * y[i + 1];
76 }
77 out
78}
79
80fn band(x: &[f64], y: &[f64], lo: f64, hi: f64) -> Vec<f64> {
81 x.iter()
82 .zip(y)
83 .filter(|(x, _)| **x >= lo && **x <= hi)
84 .map(|(_, y)| *y)
85 .collect()
86}
87
88pub fn metrics(h: &[C], f: &[f64], target_db: &[f64], norm: (f64, f64)) -> Metrics {
91 assert!(h.len() >= 2 && h.len() == f.len() && f.len() == target_db.len());
92 let (level, dev) = amplitude_deviation(h, f, target_db, norm);
93 let mut percentages = Vec::new();
94 for (lo, hi) in [(20.0, 200.0), (200.0, 2000.0), (2000.0, 16000.0)] {
95 let values = band(f, &dev, lo, hi);
96 for threshold in [1.0, 2.0] {
97 percentages.push(
98 100.0
99 * (values.iter().filter(|v| v.abs() <= threshold).count() as f64
100 / values.len() as f64),
101 );
102 }
103 }
104 let ir = fft::irfft(h, 2 * (h.len() - 1));
105 let peak = first_max(&ir.iter().map(|v| v.abs()).collect::<Vec<_>>());
106 let fs = 2.0 * f[f.len() - 1];
107 let (g, gd, turns) = phase_metrics(h, f, peak, fs);
108 let span = |lo, hi| {
109 let v = band(&g, &gd, lo, hi);
110 percentile(&v, 90.0) - percentile(&v, 10.0)
111 };
112 let g24 = geomspace(20.0, 500.0, 1500);
113 let gd24 = gd_curve_with_peak(h, f, &g24, Fraction::Scalar(24.0), peak).unwrap();
114 let k24 = first_max(&gd24);
115 let gd24_abs = gd24[k24];
116 let center = median(&gd24);
117 let centered: Vec<_> = gd24.iter().map(|d| d - center).collect();
118 let a3 = (0.003 * fs) as isize;
119 let a50 = (0.05 * fs) as isize;
120 let wrapped = |a: isize, b: isize| {
121 energy(
122 &(a..b)
123 .map(|i| ir[i.rem_euclid(ir.len() as isize) as usize])
124 .collect::<Vec<_>>(),
125 )
126 };
127 let pre = wrapped(peak as isize - a50, peak as isize - a3);
128 let main = wrapped(peak as isize - a3, peak as isize + a50);
129 Metrics {
130 level,
131 in1_20: percentages[0],
132 in2_20: percentages[1],
133 in1_200: percentages[2],
134 in2_200: percentages[3],
135 in1_2000: percentages[4],
136 in2_2000: percentages[5],
137 turns,
138 gd_20: span(20.0, 100.0),
139 gd_100: span(100.0, 500.0),
140 gd_500: span(500.0, 5000.0),
141 gd24_abs,
142 gd24_abs_hz: g24[k24],
143 gd24_max: gd24_abs - center,
144 gd24_span: percentile(¢ered, 95.0) - percentile(¢ered, 5.0),
145 gd_30: span(30.0, 100.0),
146 pre_db: 10.0 * (pre / (main + 1e-30) + 1e-30).log10(),
147 }
148}
149
150fn amplitude_deviation(h: &[C], f: &[f64], target_db: &[f64], norm: (f64, f64)) -> (f64, Vec<f64>) {
153 let power: Vec<_> = h
154 .iter()
155 .map(|h| {
156 let a = h.re.hypot(h.im);
157 a * a
158 })
159 .collect();
160 let db: Vec<_> = smooth_power(f, &power, Fraction::Scalar(6.0))
161 .unwrap()
162 .iter()
163 .zip(target_db)
164 .map(|(p, t)| 10.0 * (p + 1e-30).log10() - t)
165 .collect();
166 let level = median(&band(f, &db, norm.0, norm.1));
167 let dev: Vec<f64> = db.iter().map(|d| d - level).collect();
168 (level, dev)
169}
170
171fn phase_metrics(h: &[C], f: &[f64], peak: usize, fs: f64) -> (Vec<f64>, Vec<f64>, f64) {
172 let t = peak as f64 / fs;
173 let shifted: Vec<_> = h
174 .iter()
175 .zip(f)
176 .map(|(h, f)| h * C::from_polar(1.0, 0.0 + (2.0 * PI * f) * t))
177 .collect();
178 let hs = smooth_complex(f, &shifted, Fraction::Scalar(6.0)).unwrap();
179 let re: Vec<_> = hs.iter().map(|h| h.re).collect();
180 let im: Vec<_> = hs.iter().map(|h| h.im).collect();
181 let g = geomspace(20.0, 20000.0, 2000);
182 let mut phase: Vec<_> = g
183 .iter()
184 .map(|g| {
185 (C::new(interp(*g, f, &re), 0.0) + C::new(0.0, 1.0) * C::new(interp(*g, f, &im), 0.0))
186 .arg()
187 })
188 .collect();
189 unwrap(&mut phase);
190 let slope = fit_slope(
191 &band(&g, &g, 1000.0, 10000.0),
192 &band(&g, &phase, 1000.0, 10000.0),
193 );
194 for (p, g) in phase.iter_mut().zip(&g) {
195 *p -= slope * g;
196 }
197 let turns = (phase.iter().copied().fold(f64::NEG_INFINITY, f64::max)
198 - phase.iter().copied().fold(f64::INFINITY, f64::min))
199 / (2.0 * PI);
200 let x: Vec<_> = g.iter().map(|g| 2.0 * PI * g).collect();
201 let gd: Vec<_> = gradient(&phase, &x).iter().map(|v| -v * 1000.0).collect();
202 (g, gd, turns)
203}
204
205pub(crate) fn flatness_20(h: &[C], f: &[f64], target_db: &[f64], norm: (f64, f64)) -> f64 {
206 assert!(h.len() >= 2 && h.len() == f.len() && f.len() == target_db.len());
207 let (_, dev) = amplitude_deviation(h, f, target_db, norm);
208 let values = band(f, &dev, 20.0, 200.0);
209 100.0 * (values.iter().filter(|v| v.abs() <= 1.0).count() as f64 / values.len() as f64)
210}
211
212pub(crate) fn gd_span_20(h: &[C], f: &[f64]) -> f64 {
213 assert!(h.len() >= 2 && h.len() == f.len());
214 let ir = fft::irfft(h, 2 * (h.len() - 1));
215 let peak = first_max(&ir.iter().map(|v| v.abs()).collect::<Vec<_>>());
216 let (g, gd, _) = phase_metrics(h, f, peak, 2.0 * f[f.len() - 1]);
217 let v = band(&g, &gd, 20.0, 100.0);
218 percentile(&v, 90.0) - percentile(&v, 10.0)
219}
220
221#[cfg(test)]
222mod resource_tests {
223 use super::*;
224
225 #[test]
226 fn xo_partial_metrics_match_full() {
227 let f = crate::grid::design_freqs();
228 let h = secs_dsp::response::peq_response(f, &[(67.0, 2.1, -5.0), (123.0, 0.9, 3.0)]);
229 let target: Vec<f64> = f
230 .iter()
231 .map(|f| -0.1 * (f.max(1.0) / 20.0).log2())
232 .collect();
233 let all = metrics(&h, f, &target, (100.0, 400.0));
234 assert_eq!(
235 flatness_20(&h, f, &target, (100.0, 400.0)).to_bits(),
236 all.in1_20.to_bits()
237 );
238 assert_eq!(gd_span_20(&h, f).to_bits(), all.gd_20.to_bits());
239 }
240}