sva_samples/measure/
crest.rs1use sva_formula::filter::Shape;
4
5use crate::biquad::{State, design};
6use crate::measure::envelope::level;
7use crate::measure::spectrum::{db, third_octave_edges};
8
9const COUNTED_UNDER_DB: f64 = 60.0;
11
12const SETTLE_TAUS: f64 = 5.0;
15const CASCADE_Q: f64 = 1.55;
16
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct BandCrest {
19 pub lo_hz: f64,
20 pub hi_hz: f64,
21 pub centre_hz: f64,
22 pub peak: f64,
23 pub rms: f64,
24 pub crest_db: f64,
25 pub counted: bool,
26}
27
28#[derive(Clone, Debug, PartialEq)]
30pub struct Crest {
31 pub broadband_crest_db: f64,
32 pub spread_db: Option<f64>,
33 pub widest_band_hz: Option<f64>,
34 pub tightest_band_hz: Option<f64>,
35 pub counted_under_db: f64,
36 pub bands: Vec<BandCrest>,
37}
38
39pub fn analyze(planes: &[&[f64]], sample_rate: f64) -> Crest {
40 crate::measure::held(planes);
41 let mut bands: Vec<BandCrest> = third_octave_edges(sample_rate)
42 .into_iter()
43 .map(|(lo, hi)| band(planes, sample_rate, lo, hi))
44 .collect();
45
46 let loudest = bands.iter().map(|b| b.rms).fold(0.0, f64::max);
47 let floor = loudest * 10f64.powf(-COUNTED_UNDER_DB / 20.0);
48 for b in &mut bands {
49 b.counted = b.rms > floor && b.rms > 0.0;
50 }
51
52 let counted: Vec<&BandCrest> = bands.iter().filter(|b| b.counted).collect();
53 let widest = counted
54 .iter()
55 .max_by(|a, b| a.crest_db.total_cmp(&b.crest_db));
56 let tightest = counted
57 .iter()
58 .min_by(|a, b| a.crest_db.total_cmp(&b.crest_db));
59
60 Crest {
61 broadband_crest_db: crest_db(
62 planes
63 .iter()
64 .flat_map(|p| p.iter())
65 .fold(0.0, |a, x| a.max(x.abs())),
66 level(planes),
67 ),
68 spread_db: widest.zip(tightest).map(|(w, t)| w.crest_db - t.crest_db),
69 widest_band_hz: widest.map(|b| b.centre_hz),
70 tightest_band_hz: tightest.map(|b| b.centre_hz),
71 counted_under_db: COUNTED_UNDER_DB,
72 bands,
73 }
74}
75
76fn band(planes: &[&[f64]], sample_rate: f64, lo: f64, hi: f64) -> BandCrest {
78 let centre = (lo * hi).sqrt();
79 let q = centre / (hi - lo);
80 let coeffs = design(Shape::Bandpass, centre, q, 0.0, sample_rate);
81 let settle = settle_secs(centre, q) * sample_rate;
82 let len = planes[0].len();
83 let skip = (settle.round() as usize).min(len);
84 let mut peak = 0.0f64;
85 let powers: Vec<f64> = planes
86 .iter()
87 .map(|samples| {
88 let (mut first, mut second) = (State::default(), State::default());
89 let mut power = 0.0f64;
90 for (i, x) in samples.iter().enumerate() {
91 let y = second.step(&coeffs, first.step(&coeffs, *x));
92 if i < skip {
93 continue;
94 }
95 peak = peak.max(y.abs());
96 power += y * y;
97 }
98 power
99 })
100 .collect();
101 let power = powers[1..].iter().fold(powers[0], |sum, p| sum + p);
102 let rms = match (len - skip) * planes.len() {
103 0 => 0.0,
104 n => (power / n as f64).sqrt(),
105 };
106 BandCrest {
107 lo_hz: lo,
108 hi_hz: hi,
109 centre_hz: centre,
110 peak,
111 rms,
112 crest_db: crest_db(peak, rms),
113 counted: false,
114 }
115}
116
117fn settle_secs(centre_hz: f64, q: f64) -> f64 {
118 SETTLE_TAUS * CASCADE_Q * q / (std::f64::consts::PI * centre_hz)
119}
120
121fn crest_db(peak: f64, rms: f64) -> f64 {
122 if rms <= 0.0 { db(0.0) } else { db(peak / rms) }
123}