Skip to main content

audio_lab/dsp/
window.rs

1use std::f64::consts::PI;
2
3use ndarray::Array1;
4
5/// Public window function identifiers used across generic DSP helpers.
6#[derive(Debug, Clone, Copy, PartialEq, Default)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8pub enum WindowFn {
9    #[default]
10    Hann,
11    Hamming,
12    Blackman,
13    Rectangular,
14}
15
16/// Generate a symmetric window: both ends have the same value (zero for Hann
17/// and Blackman). Use it for FIR filter design, smoothing kernels and fades.
18/// For spectral analysis use [`generate_periodic_window`].
19pub fn generate_window(window_fn: WindowFn, size: usize) -> Array1<f64> {
20    window_with_period(window_fn, size, size.saturating_sub(1))
21}
22
23/// Generate a periodic ("DFT-even") window: the first `size` samples of a
24/// symmetric window of `size + 1` samples. This is the standard window for
25/// FFT-based spectral analysis such as [`stft`](crate::dsp::stft()), the default
26/// in scipy, librosa and PyTorch. A periodic Hann window at 50 % or 75 % overlap
27/// sums to a constant, and none of these windows is all zeros.
28pub fn generate_periodic_window(window_fn: WindowFn, size: usize) -> Array1<f64> {
29    window_with_period(window_fn, size, size)
30}
31
32/// `size` samples of `window_fn`, where sample `period` would complete one
33/// cycle. Windows of 0 or 1 samples are all ones.
34fn window_with_period(window_fn: WindowFn, size: usize, period: usize) -> Array1<f64> {
35    if size <= 1 {
36        return Array1::ones(size);
37    }
38
39    let n = period as f64;
40    Array1::from_iter((0..size).map(|i| {
41        let x = i as f64;
42        match window_fn {
43            WindowFn::Hann => 0.5 * (1.0 - (2.0 * PI * x / n).cos()),
44            WindowFn::Hamming => 0.54 - 0.46 * (2.0 * PI * x / n).cos(),
45            WindowFn::Blackman => {
46                0.42 - 0.5 * (2.0 * PI * x / n).cos() + 0.08 * (4.0 * PI * x / n).cos()
47            }
48            WindowFn::Rectangular => 1.0,
49        }
50    }))
51}
52
53/// Apply a full Hann taper to a slice range in-place.
54///
55/// This is the public replacement for ad-hoc `window_hanning` helpers used in
56/// internal prototypes.
57pub fn apply_hann(data: &mut [f64], start: usize, len: usize) {
58    let start = start.min(data.len());
59    let len = len.min(data.len().saturating_sub(start));
60    if len == 0 {
61        return;
62    }
63
64    data.iter_mut()
65        .skip(start)
66        .take(len)
67        .zip(generate_window(WindowFn::Hann, len))
68        .for_each(|(sample, weight)| *sample *= weight);
69}
70
71/// Apply the rising half of a Hann taper to a slice range in-place.
72pub fn apply_hann_left(data: &mut [f64], start: usize, len: usize) {
73    let start = start.min(data.len());
74    let len = len.min(data.len().saturating_sub(start));
75    if len == 0 {
76        return;
77    }
78
79    data.iter_mut()
80        .skip(start)
81        .take(len)
82        .zip(
83            generate_window(WindowFn::Hann, len * 2)
84                .into_iter()
85                .take(len),
86        )
87        .for_each(|(sample, weight)| *sample *= weight);
88}
89
90/// Apply the falling half of a Hann taper to a slice range in-place.
91pub fn apply_hann_right(data: &mut [f64], start: usize, len: usize) {
92    let start = start.min(data.len());
93    let len = len.min(data.len().saturating_sub(start));
94    if len == 0 {
95        return;
96    }
97
98    data.iter_mut()
99        .skip(start)
100        .take(len)
101        .zip(
102            generate_window(WindowFn::Hann, len * 2)
103                .into_iter()
104                .skip(len),
105        )
106        .for_each(|(sample, weight)| *sample *= weight);
107}
108
109#[cfg(test)]
110mod tests {
111    use approx::assert_abs_diff_eq;
112
113    use super::*;
114
115    #[test]
116    fn apply_hann_matches_expected_full_window_taper() {
117        let mut data = [1.0; 5];
118        let len = data.len();
119
120        apply_hann(&mut data, 0, len);
121
122        let expected = [0.0, 0.5, 1.0, 0.5, 0.0];
123        for (sample, expected) in data.into_iter().zip(expected) {
124            assert_abs_diff_eq!(sample, expected, epsilon = 1e-12);
125        }
126    }
127
128    #[test]
129    fn apply_hann_left_matches_rising_half_window_taper() {
130        let mut data = [1.0; 4];
131        let len = data.len();
132
133        apply_hann_left(&mut data, 0, len);
134
135        let expected = generate_window(WindowFn::Hann, 8);
136        for (sample, expected) in data.into_iter().zip(expected.into_iter().take(4)) {
137            assert_abs_diff_eq!(sample, expected, epsilon = 1e-12);
138        }
139    }
140
141    #[test]
142    fn apply_hann_right_matches_falling_half_window_taper() {
143        let mut data = [1.0; 4];
144        let len = data.len();
145
146        apply_hann_right(&mut data, 0, len);
147
148        let expected = generate_window(WindowFn::Hann, 8);
149        for (sample, expected) in data.into_iter().zip(expected.into_iter().skip(4)) {
150            assert_abs_diff_eq!(sample, expected, epsilon = 1e-12);
151        }
152    }
153
154    #[test]
155    fn apply_hann_matches_generated_window_for_subslice() {
156        let mut data = [1.0; 8];
157        apply_hann(&mut data, 2, 4);
158
159        let expected = generate_window(WindowFn::Hann, 4);
160        assert_abs_diff_eq!(data[0], 1.0, epsilon = 1e-12);
161        assert_abs_diff_eq!(data[1], 1.0, epsilon = 1e-12);
162        assert_abs_diff_eq!(data[2], expected[0], epsilon = 1e-12);
163        assert_abs_diff_eq!(data[3], expected[1], epsilon = 1e-12);
164        assert_abs_diff_eq!(data[4], expected[2], epsilon = 1e-12);
165        assert_abs_diff_eq!(data[5], expected[3], epsilon = 1e-12);
166        assert_abs_diff_eq!(data[6], 1.0, epsilon = 1e-12);
167        assert_abs_diff_eq!(data[7], 1.0, epsilon = 1e-12);
168    }
169
170    #[test]
171    fn periodic_window_is_symmetric_window_one_sample_longer_without_the_last() {
172        for window_fn in [
173            WindowFn::Hann,
174            WindowFn::Hamming,
175            WindowFn::Blackman,
176            WindowFn::Rectangular,
177        ] {
178            for size in [2, 3, 8, 9, 64] {
179                let periodic = generate_periodic_window(window_fn, size);
180                let symmetric = generate_window(window_fn, size + 1);
181                for (p, s) in periodic.iter().zip(symmetric.iter()) {
182                    assert_abs_diff_eq!(*p, *s, epsilon = 1e-12);
183                }
184            }
185        }
186        assert_eq!(
187            generate_periodic_window(WindowFn::Hann, 1),
188            Array1::<f64>::ones(1)
189        );
190        assert_eq!(
191            generate_periodic_window(WindowFn::Hann, 0),
192            Array1::<f64>::ones(0)
193        );
194    }
195
196    #[test]
197    fn periodic_hann_sums_to_a_constant_at_50_and_75_percent_overlap() {
198        let size = 16;
199        let window = generate_periodic_window(WindowFn::Hann, size);
200        for hop in [size / 2, size / 4] {
201            let expected = size as f64 / (2 * hop) as f64;
202            for offset in 0..hop {
203                let sum: f64 = window.iter().skip(offset).step_by(hop).sum();
204                assert_abs_diff_eq!(sum, expected, epsilon = 1e-12);
205            }
206        }
207    }
208
209    #[test]
210    fn periodic_windows_are_never_all_zero() {
211        // The symmetric 2-sample Hann and Blackman windows are all zeros.
212        for window_fn in [WindowFn::Hann, WindowFn::Blackman] {
213            assert!(
214                generate_window(window_fn, 2)
215                    .iter()
216                    .all(|w| w.abs() < 1e-12)
217            );
218            assert!(generate_periodic_window(window_fn, 2).sum() > 0.4);
219        }
220    }
221}