1use std::f64::consts::PI;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum Taper {
16 Rectangular,
18 Hamming,
20 Hann,
22 Blackman,
24}
25
26impl Taper {
27 #[must_use]
29 pub fn coefficient(&self, index: usize, length: usize) -> f64 {
30 let length = length as f64;
31 let index = index as f64;
32
33 match self {
34 Self::Rectangular => 1.0,
35 Self::Hamming => 0.54 - 0.46 * (2.0 * PI * index / length).cos(),
36 Self::Hann => 0.5 * (1.0 - (2.0 * PI * index / length).cos()),
37 Self::Blackman => {
38 0.42 - 0.5 * (2.0 * PI * index / length).cos()
39 + 0.08 * (4.0 * PI * index / length).cos()
40 }
41 }
42 }
43
44 #[must_use]
46 pub fn coefficients(&self, length: usize) -> Vec<f64> {
47 (0..length)
48 .map(|index| self.coefficient(index, length))
49 .collect()
50 }
51}
52
53#[derive(Clone, Copy, Debug, PartialEq)]
60pub enum Band {
61 Rectangular {
63 start: f64,
65 end: f64,
67 },
68 RaisedCosine {
70 start: f64,
72 end: f64,
74 transition: f64,
76 },
77 Kaiser {
79 start: f64,
81 end: f64,
83 beta: f64,
85 },
86 Gaussian {
88 start: f64,
90 end: f64,
92 sigma: f64,
94 },
95}
96
97impl Band {
98 #[must_use]
100 pub fn coefficient(&self, frequency: f64) -> f64 {
101 let (start, end) = self.bounds();
102 if frequency < start || frequency > end {
103 return 0.0;
104 }
105
106 match self {
107 Self::Rectangular { .. } => 1.0,
108 Self::RaisedCosine { transition, .. } => {
109 if frequency < start + transition {
110 0.5 * (1.0 - (PI * (frequency - start) / transition).cos())
111 } else if frequency > end - transition {
112 0.5 * (1.0 - (PI * (end - frequency) / transition).cos())
113 } else {
114 1.0
115 }
116 }
117 Self::Kaiser { beta, .. } => {
118 let half = (end - start) / 2.0;
119 let offset = (frequency - (start + end) / 2.0) / half;
120 bessel_i0(beta * (1.0 - offset * offset).sqrt()) / bessel_i0(*beta)
121 }
122 Self::Gaussian { sigma, .. } => {
123 let offset = (frequency - (start + end) / 2.0) / sigma;
124 (-0.5 * offset * offset).exp()
125 }
126 }
127 }
128
129 #[must_use]
131 pub fn bounds(&self) -> (f64, f64) {
132 match self {
133 Self::Rectangular { start, end }
134 | Self::RaisedCosine { start, end, .. }
135 | Self::Kaiser { start, end, .. }
136 | Self::Gaussian { start, end, .. } => (*start, *end),
137 }
138 }
139}
140
141fn bessel_i0(argument: f64) -> f64 {
146 let magnitude = argument.abs();
147
148 if magnitude < 3.75 {
149 let square = (argument / 3.75).powi(2);
150 1.0 + square
151 * (3.515_622_9
152 + square
153 * (3.089_942_4
154 + square
155 * (1.206_749_2
156 + square
157 * (0.265_973_2
158 + square * (0.036_076_8 + square * 0.004_581_3)))))
159 } else {
160 let ratio = 3.75 / magnitude;
161 (magnitude.exp() / magnitude.sqrt())
162 * (0.398_942_28
163 + ratio
164 * (0.013_285_92
165 + ratio
166 * (0.002_253_19
167 + ratio
168 * (-0.001_575_65
169 + ratio
170 * (0.009_162_81
171 + ratio
172 * (-0.020_577_06
173 + ratio
174 * (0.026_355_37
175 + ratio
176 * (-0.016_476_33
177 + ratio
178 * 0.003_923_77))))))))
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn a_rectangular_taper_is_no_taper() {
188 for index in 0..64 {
189 assert_eq!(Taper::Rectangular.coefficient(index, 64), 1.0);
190 }
191 }
192
193 #[test]
194 fn hann_starts_at_zero_and_hamming_at_its_pedestal() {
195 assert!(Taper::Hann.coefficient(0, 128) < 1e-12);
196 assert!((Taper::Hamming.coefficient(0, 128) - 0.08).abs() < 1e-12);
197 }
198
199 #[test]
200 fn every_taper_is_symmetric_about_its_centre() {
201 let length = 100;
202 for taper in [Taper::Hamming, Taper::Hann, Taper::Blackman] {
203 let left = taper.coefficient(length / 2 - 10, length);
204 let right = taper.coefficient(length / 2 + 10, length);
205 assert!((left - right).abs() < 1e-12, "{taper:?} is not symmetric");
206 }
207 }
208
209 #[test]
210 fn coefficients_are_the_whole_segment() {
211 let coefficients = Taper::Blackman.coefficients(32);
212
213 assert_eq!(coefficients.len(), 32);
214 assert_eq!(coefficients[7], Taper::Blackman.coefficient(7, 32));
215 }
216
217 #[test]
218 fn a_rectangular_band_is_a_brick_wall() {
219 let band = Band::Rectangular {
220 start: 100.0,
221 end: 200.0,
222 };
223
224 assert_eq!(band.coefficient(99.0), 0.0);
225 assert_eq!(band.coefficient(100.0), 1.0);
226 assert_eq!(band.coefficient(150.0), 1.0);
227 assert_eq!(band.coefficient(200.0), 1.0);
228 assert_eq!(band.coefficient(201.0), 0.0);
229 }
230
231 #[test]
232 fn a_raised_cosine_band_rises_and_falls_inside_its_edges() {
233 let band = Band::RaisedCosine {
234 start: 100.0,
235 end: 200.0,
236 transition: 20.0,
237 };
238
239 assert_eq!(band.coefficient(50.0), 0.0);
240 assert_eq!(band.coefficient(150.0), 1.0);
241 assert!((0.0..1.0).contains(&band.coefficient(110.0)));
242 assert!((0.0..1.0).contains(&band.coefficient(190.0)));
243 }
244
245 #[test]
246 fn a_kaiser_band_peaks_at_its_centre() {
247 let band = Band::Kaiser {
248 start: 0.0,
249 end: 100.0,
250 beta: 6.0,
251 };
252
253 assert!((band.coefficient(50.0) - 1.0).abs() < 1e-9);
254 assert!(band.coefficient(10.0) < band.coefficient(40.0));
255 assert_eq!(band.coefficient(101.0), 0.0);
256 }
257
258 #[test]
259 fn a_gaussian_band_falls_off_by_its_sigma() {
260 let band = Band::Gaussian {
261 start: 0.0,
262 end: 100.0,
263 sigma: 25.0,
264 };
265
266 assert!((band.coefficient(50.0) - 1.0).abs() < 1e-12);
267 assert!((band.coefficient(75.0) - (-0.5f64).exp()).abs() < 1e-12);
269 }
270
271 #[test]
272 fn every_band_reports_the_bounds_it_was_built_with() {
273 assert_eq!(
274 Band::Rectangular {
275 start: 1.0,
276 end: 2.0
277 }
278 .bounds(),
279 (1.0, 2.0)
280 );
281 assert_eq!(
282 Band::RaisedCosine {
283 start: 1.0,
284 end: 2.0,
285 transition: 0.1
286 }
287 .bounds(),
288 (1.0, 2.0)
289 );
290 assert_eq!(
291 Band::Kaiser {
292 start: 1.0,
293 end: 2.0,
294 beta: 6.0
295 }
296 .bounds(),
297 (1.0, 2.0)
298 );
299 assert_eq!(
300 Band::Gaussian {
301 start: 1.0,
302 end: 2.0,
303 sigma: 0.5
304 }
305 .bounds(),
306 (1.0, 2.0)
307 );
308 }
309}