microdsp/common/
window_processor.rs

1use alloc::{boxed::Box, vec};
2
3/// Provides fixed size windows extracted from
4/// a stream of arbitrarily sized input buffers. Supports
5/// downsampling and partially overlapping windows. Useful
6/// for implementing algorithms operating on
7/// consecutive windows of the same size.
8pub struct WindowProcessor {
9    downsampled_window: Box<[f32]>,
10    downsampling: usize,
11    downsampled_hop_size: usize,
12    // Downsampled window write index
13    write_index: usize,
14    wrapped_sample_counter: usize,
15}
16
17fn validate_sizes(downsampled_size: usize, downsampled_hop_size: usize, downsampling: usize) {
18    if downsampled_size == 0 {
19        panic!("Downsampled size must be greater than 0")
20    }
21    if downsampled_hop_size == 0 {
22        panic!("Downsampled hop size must be greater than 0")
23    }
24    if downsampling == 0 {
25        panic!("Downsampling must be greater than 0")
26    }
27    if downsampled_hop_size > downsampled_size {
28        panic!("Downsampled hop size must not be greater than downsampled size")
29    }
30}
31
32impl WindowProcessor {
33    /// Creates a new `WindowProcessor` instance.
34    /// # Arguments
35    ///
36    /// * `downsampling` - The downsampling factor (1 corresponds to no downsampling)
37    /// * `downsampled_window_size` - The window size _after downsampling_.
38    /// * `downsampled_hop_size` - The distance, _after downsampling_, between the start of windows. Must not be zero and not be greater than `downsampled_window_size`.
39    pub fn new(
40        downsampling: usize,
41        downsampled_window_size: usize,
42        downsampled_hop_size: usize,
43    ) -> Self {
44        validate_sizes(downsampled_window_size, downsampled_hop_size, downsampling);
45        WindowProcessor {
46            downsampled_window: vec![0.; downsampled_window_size].into_boxed_slice(),
47            downsampled_hop_size,
48            downsampling,
49            write_index: 0,
50            wrapped_sample_counter: 0,
51        }
52    }
53
54    pub fn reset(&mut self) {
55        self.write_index = 0;
56        self.wrapped_sample_counter = 0;
57    }
58
59    /// Returns the downsampling factor.
60    pub fn downsampling(&self) -> usize {
61        self.downsampling
62    }
63
64    /// Returns the hop size _after downsampling_.
65    pub fn downsampled_hop_size(&self) -> usize {
66        self.downsampled_hop_size
67    }
68
69    /// Returns the window size _after downsampling_.
70    pub fn downsampled_window_size(&self) -> usize {
71        self.downsampled_window.len()
72    }
73
74    /// Processes an arbitrarily sized buffer of input samples. Invokes
75    /// the provided handler with each newly filled window.
76    pub fn process<F>(&mut self, buffer: &[f32], mut handler: F)
77    where
78        F: FnMut(&[f32]),
79    {
80        let downsampled_window_size = self.downsampled_window.len();
81        let skip = (self.downsampling - self.wrapped_sample_counter) % self.downsampling;
82        for input in buffer.iter().skip(skip).step_by(self.downsampling) {
83            self.downsampled_window[self.write_index] = *input;
84            self.write_index += 1;
85            if self.write_index == downsampled_window_size {
86                handler(&self.downsampled_window);
87                self.downsampled_window
88                    .rotate_left(self.downsampled_hop_size);
89                self.write_index = downsampled_window_size - self.downsampled_hop_size;
90            }
91        }
92
93        self.wrapped_sample_counter =
94            (self.wrapped_sample_counter + buffer.len()) % self.downsampling
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use alloc::vec;
101    use alloc::vec::Vec;
102
103    use super::WindowProcessor;
104
105    #[test]
106    #[should_panic]
107    fn test_zero_window_size() {
108        WindowProcessor::new(1, 0, 256);
109    }
110
111    #[test]
112    #[should_panic]
113    fn test_zero_hop_size() {
114        WindowProcessor::new(1, 256, 0);
115    }
116
117    #[test]
118    #[should_panic]
119    fn test_too_large_hop_size() {
120        WindowProcessor::new(1, 256, 257);
121    }
122
123    #[test]
124    #[should_panic]
125    fn test_zero_downsampling() {
126        WindowProcessor::new(0, 256, 256);
127    }
128
129    #[test]
130    fn test_hop_size_equals_window_size() {
131        let hop_size = 128;
132        let window_size = 128;
133        let downsampling = 2;
134        let chunk_size = 256;
135        let window_count = 10;
136        let sample_count = chunk_size * window_count;
137        let samples = vec![0.0; sample_count];
138        let mut processor = WindowProcessor::new(downsampling, window_size, hop_size);
139        let mut first_idx = 0;
140        let mut winow_counter = 0;
141        while first_idx < sample_count {
142            let chunk = &samples[first_idx..(first_idx + chunk_size)];
143            processor.process(chunk, |_| {
144                winow_counter += 1;
145            });
146            first_idx += chunk_size;
147        }
148        assert_eq!(winow_counter, window_count);
149    }
150
151    #[test]
152    fn test_window_processing() {
153        let window_size = 15;
154
155        // An input buffer with values 0, 1, 2, 3, 4....
156        let input_buffer: Vec<f32> = (0..(5 * window_size)).map(|v| v as f32).collect();
157        assert_eq!(input_buffer.len(), 5 * window_size);
158
159        // Test various combinations of downsampling, hop size and chunk size.
160        for downsampling in 1..10 {
161            for hop_size in 1..=window_size {
162                for chunk_size in 1..5 * window_size {
163                    let mut processor = WindowProcessor::new(downsampling, window_size, hop_size);
164                    let mut processed_window_count = 0;
165                    let mut input_buffer_pos = 0;
166                    // Feed the processor chunks of chunk_size samples
167                    while input_buffer_pos < input_buffer.len() {
168                        let chunk_start_idx = input_buffer_pos;
169                        let current_chunk_size =
170                            chunk_size.min(input_buffer.len() - chunk_start_idx);
171                        let chunk_end_idx = input_buffer_pos + current_chunk_size;
172                        let current_chunk_size = chunk_size.min(chunk_end_idx - chunk_start_idx);
173                        let chunk = &input_buffer[chunk_start_idx..chunk_end_idx];
174                        assert_eq!(chunk.len(), current_chunk_size);
175
176                        processor.process(chunk, |window| {
177                            // Verify that the first sample of the extrated window
178                            // corresponds to the correct input_buffer value
179                            assert_eq!(
180                                window[0],
181                                input_buffer[downsampling * processed_window_count * hop_size]
182                            );
183                            assert_eq!(window.len(), window_size);
184                            processed_window_count += 1;
185                        });
186
187                        input_buffer_pos += chunk_size
188                    }
189                }
190            }
191        }
192    }
193}