1#[must_use]
13pub fn peaks(magnitude: &[f64]) -> Vec<usize> {
14 let mut found: Vec<usize> = Vec::new();
15
16 if magnitude.len() >= 2 {
17 if magnitude[0] > magnitude[1] {
18 found.push(0);
19 }
20 found.extend(
21 magnitude
22 .windows(3)
23 .enumerate()
24 .filter(|(_, window)| window[1] > window[0] && window[1] > window[2])
25 .map(|(index, _)| index + 1),
26 );
27 let last = magnitude.len() - 1;
28 if magnitude[last] > magnitude[last - 1] {
29 found.push(last);
30 }
31 }
32
33 found.sort_by(|left, right| magnitude[*right].total_cmp(&magnitude[*left]));
34 found
35}
36
37#[must_use]
42pub fn harmonics(magnitude: &[f64], fraction: f64) -> Vec<usize> {
43 let strongest = magnitude.iter().copied().fold(0.0_f64, f64::max);
44 let floor = fraction * strongest;
45
46 let mut found: Vec<usize> = peaks(magnitude)
47 .into_iter()
48 .filter(|bin| magnitude[*bin] >= floor)
49 .collect();
50
51 found.sort_unstable();
52 found
53}
54
55#[must_use]
62pub fn fundamental(frequency: &[f64], magnitude: &[f64]) -> Option<usize> {
63 let guard = step_of(frequency) * 0.5;
64
65 peaks(magnitude)
66 .into_iter()
67 .find(|bin| frequency.get(*bin).is_some_and(|at| *at > guard))
68}
69
70#[must_use]
76pub fn thd(frequency: &[f64], magnitude: &[f64]) -> Option<f64> {
77 let bin = fundamental(frequency, magnitude)?;
78 let strongest = magnitude[bin];
79 if strongest < f64::EPSILON {
80 return None;
81 }
82
83 let tolerance = step_of(frequency) * 0.5;
84 let distortion: f64 = magnitude
85 .iter()
86 .zip(frequency)
87 .filter(|(_, at)| **at > tolerance && (**at - frequency[bin]).abs() > tolerance)
88 .map(|(value, _)| value * value)
89 .sum();
90
91 Some(distortion.sqrt() / strongest * 100.0)
92}
93
94fn step_of(frequency: &[f64]) -> f64 {
96 match frequency {
97 [first, second, ..] => second - first,
98 _ => 1.0,
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 fn spectrum(length: usize, filled: &[(usize, f64)]) -> Vec<f64> {
108 let mut magnitude = vec![0.0; length];
109 for (bin, value) in filled {
110 magnitude[*bin] = *value;
111 }
112 magnitude
113 }
114
115 #[test]
116 fn peaks_come_back_strongest_first() {
117 let magnitude = spectrum(101, &[(10, 5.0), (30, 10.0), (50, 7.0), (70, 2.0)]);
118
119 assert_eq!(peaks(&magnitude), vec![30, 50, 10, 70]);
120 }
121
122 #[test]
123 fn a_run_with_no_local_maximum_has_no_peaks() {
124 assert!(peaks(&[1.0, 1.0, 1.0, 1.0]).is_empty());
125 assert!(peaks(&[]).is_empty());
126 assert!(peaks(&[1.0]).is_empty());
127 }
128
129 #[test]
130 fn the_edges_qualify_on_their_one_neighbour() {
131 assert_eq!(peaks(&[5.0, 1.0, 0.0]), vec![0]);
132 assert_eq!(peaks(&[0.0, 1.0, 5.0]), vec![2]);
133 }
134
135 #[test]
136 fn harmonics_keep_what_clears_the_fraction_in_bin_order() {
137 let magnitude = spectrum(201, &[(0, 5.0), (50, 10.0), (150, 3.0)]);
138
139 assert_eq!(harmonics(&magnitude, 0.1), vec![0, 50, 150]);
140 assert_eq!(harmonics(&magnitude, 0.4), vec![0, 50]);
141 assert_eq!(harmonics(&magnitude, 0.9), vec![50]);
142 }
143
144 #[test]
145 fn the_fundamental_is_the_strongest_peak_off_dc() {
146 let frequency = vec![0.0, 50.0, 100.0, 150.0];
147 let magnitude = vec![5.0, 20.0, 10.0, 5.0];
148
149 assert_eq!(fundamental(&frequency, &magnitude), Some(1));
150 }
151
152 #[test]
153 fn dc_leakage_as_strong_as_dc_is_still_not_the_fundamental() {
154 let frequency: Vec<f64> = (0..5001).map(|bin| f64::from(bin) * 1000.0).collect();
157 let magnitude = spectrum(5001, &[(0, 75.0), (1, 75.0), (50, 5.0)]);
158
159 assert_eq!(fundamental(&frequency, &magnitude), Some(50));
160 }
161
162 #[test]
163 fn dc_leakage_that_merely_decays_is_never_a_peak() {
164 let frequency: Vec<f64> = (0..100).map(|bin| f64::from(bin) * 100.0).collect();
165 let magnitude = spectrum(100, &[(0, 50.0), (1, 30.0), (2, 10.0), (40, 8.0)]);
166
167 assert_eq!(fundamental(&frequency, &magnitude), Some(40));
168 }
169
170 #[test]
171 fn a_pure_tone_distorts_by_nothing() {
172 let frequency: Vec<f64> = (0..100).map(|bin| f64::from(bin) * 10.0).collect();
173 let magnitude = spectrum(100, &[(10, 4.0)]);
174
175 assert!(thd(&frequency, &magnitude).is_some_and(|thd| thd.abs() < 1e-12));
176 }
177
178 #[test]
179 fn one_harmonic_at_half_the_fundamental_is_fifty_percent() {
180 let frequency: Vec<f64> = (0..100).map(|bin| f64::from(bin) * 10.0).collect();
181 let magnitude = spectrum(100, &[(10, 4.0), (20, 2.0)]);
182
183 assert!(thd(&frequency, &magnitude).is_some_and(|thd| (thd - 50.0).abs() < 1e-9));
184 }
185
186 #[test]
187 fn a_spectrum_with_no_peak_has_no_distortion_to_report() {
188 assert!(thd(&[0.0, 10.0, 20.0], &[0.0, 0.0, 0.0]).is_none());
189 }
190}