lowpass_filter/
lib.rs

1/*
2MIT License
3
4Copyright (c) 2021 Philipp Schuster
5
6Permission is hereby granted, free of charge, to any person obtaining a copy
7of this software and associated documentation files (the "Software"), to deal
8in the Software without restriction, including without limitation the rights
9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10copies of the Software, and to permit persons to whom the Software is
11furnished to do so, subject to the following conditions:
12
13The above copyright notice and this permission notice shall be included in all
14copies or substantial portions of the Software.
15
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22SOFTWARE.
23*/
24//! Simple first-order digital lowpass filters, compatible with `no_std`. You can
25//! use it, for example, to get the low frequencies from a song.
26//!
27//! ## Difference to `biquad`
28//!
29//! **⚠ TL;DR: `biquad` might be a better option in some use-cases.**
30//!
31//! This crate provides a basic and simple to understand, first order lowpass
32//! filter. The [biquad](https://crates.io/crates/biquad) crate offers second order
33//! filters, with higher accuracy. From my testing, a lowpass filter created with
34//! `biquad` has higher computational costs as this crate, but offers a
35//! **better resolution for actually cutting of signals above the cut-off frequency
36//! while the preserved signal will be less attenuated**.
37//!
38//! So for production use-cases, please also consider using `biquad`. You can run
39//! benchmark and check your data (e.g., by plotting) to make that decision.
40//!
41//! ## Usage
42//!
43//! You can either use the [`LowpassFilter`] type to integrate the filter in
44//! iterator chains or you can use a convenient function such as
45//! [`lowpass_filter`] and [`lowpass_filter_f64`]. The first approach is more
46//! flexible.
47//!
48//! ### Example with `LowpassFilter` type
49//!
50//! See implementation of [`lowpass_filter`].
51//!
52//! ### Example with `lowpass_filter` function
53//! ```rust,no_run
54//! use lowpass_filter::lowpass_filter;
55//!
56//! // some samples
57//! let mut mono_audio_data = [0.0, 1.0, -5.0, 1551.0, 141.0, 24.0];
58//! // mutates the input buffer
59//! lowpass_filter(&mut mono_audio_data, 44100.0, 120.0);
60//! ```
61
62#![deny(
63    clippy::all,
64    clippy::cargo,
65    clippy::nursery,
66    clippy::must_use_candidate,
67    // clippy::restriction,
68    // clippy::pedantic
69)]
70// now allow a few rules which are denied by the above statement
71// --> they are ridiculous and not necessary
72#![allow(
73    clippy::suboptimal_flops,
74    clippy::redundant_pub_crate,
75    clippy::fallible_impl_from
76)]
77#![deny(missing_debug_implementations)]
78#![deny(rustdoc::all)]
79#![no_std]
80
81#[cfg_attr(test, macro_use)]
82#[cfg(test)]
83extern crate std;
84
85use core::ops::RangeInclusive;
86
87/// A single-order lowpass filter with single precision that consumes and emits
88/// items one by one.
89///
90/// It is mandatory to operate on f32 values in range `-1.0..=1.0`, which is
91/// also the default in DSP.
92///
93/// # More Info
94/// - <https://en.wikipedia.org/wiki/Low-pass_filter#Simple_infinite_impulse_response_filter>
95#[derive(Debug, Clone)]
96pub struct LowpassFilter<T> {
97    alpha: T,
98    prev: T,
99    next_is_first: bool,
100}
101
102macro_rules! impl_lowpass_filter {
103    ($t:ty, $pi:expr) => {
104        impl LowpassFilter<$t> {
105            /// Create a new lowpass filter.
106            ///
107            /// # Arguments
108            /// - `sample_rate_hz`: Sample rate in Hz (e.g., 48000.0).
109            /// - `cutoff_frequency_hz`: Cutoff frequency in Hz (e.g., 1000.0).
110            #[must_use]
111            pub fn new(sample_rate_hz: $t, cutoff_frequency_hz: $t) -> Self {
112                // Nyquist rule
113                assert!(cutoff_frequency_hz * 2.0 <= sample_rate_hz);
114
115                let rc = 1.0 / (cutoff_frequency_hz * 2.0 * $pi);
116                let dt = 1.0 / sample_rate_hz;
117                let alpha = dt / (rc + dt);
118
119                Self {
120                    alpha,
121                    prev: 0.0,
122                    next_is_first: true,
123                }
124            }
125
126            /// Filter a single sample and return the filtered result.
127            ///
128            /// It is mandatory to operate on f32 values in range
129            /// `-1.0..=1.0`, which is also the default in DSP. The returned
130            /// value is also guaranteed to be in that range.
131            #[inline]
132            pub fn run(&mut self, input: $t) -> $t {
133                const RANGE: RangeInclusive<$t> = -1.0..=1.0;
134                debug_assert!(
135                    RANGE.contains(&input),
136                    "samples must be in range {RANGE:?}: {input}"
137                );
138
139                let value = if self.next_is_first {
140                    self.next_is_first = false;
141                    self.prev = input;
142                    input * self.alpha
143                } else {
144                    self.prev = self.prev + self.alpha * (input - self.prev);
145                    self.prev
146                };
147
148                // very small deviations caused by floating point operations
149                // are tolerable; just truncate the value
150                value.clamp(-1.0, 1.0)
151            }
152
153            /// Reset the internal filter state.
154            pub const fn reset(&mut self) {
155                self.prev = 0.0;
156                self.next_is_first = true;
157            }
158        }
159    };
160}
161
162impl_lowpass_filter!(f32, core::f32::consts::PI);
163impl_lowpass_filter!(f64, core::f64::consts::PI);
164
165/// Applies a [`LowpassFilter`] to the data provided in the mutable buffer and
166/// changes the items in-place.
167///
168/// It is mandatory to operate on f32 values in range `-1.0..=1.0`, which is
169/// also the default in DSP.
170///
171/// # Arguments
172/// - `sample_iter`: Iterator over the samples. This can also be a
173///   `[1.0, ...]`-style slice
174/// - `sample_rate_hz`: Sample rate in Hz (e.g., 48000.0).
175/// - `cutoff_frequency_hz`: Cutoff frequency in Hz (e.g., 1000.0).
176#[inline]
177pub fn lowpass_filter<'a, I: IntoIterator<Item = &'a mut f32>>(
178    sample_iter: I,
179    sample_rate_hz: f32,
180    cutoff_frequency_hz: f32,
181) {
182    let mut filter = LowpassFilter::<f32>::new(sample_rate_hz, cutoff_frequency_hz);
183
184    for sample in sample_iter.into_iter() {
185        let new_sample = filter.run(*sample);
186        *sample = new_sample;
187    }
188}
189
190/// Applies a [`LowpassFilter`] to the data provided in the mutable buffer and
191/// changes the items in-place.
192///
193/// It is mandatory to operate on f32 values in range `-1.0..=1.0`, which is
194/// also the default in DSP.
195///
196/// # Arguments
197/// - `sample_iter`: Iterator over the samples. This can also be a
198///   `[1.0, ...]`-style slice
199/// - `sample_rate_hz`: Sample rate in Hz (e.g., 48000.0).
200/// - `cutoff_frequency_hz`: Cutoff frequency in Hz (e.g., 1000.0).
201#[inline]
202pub fn lowpass_filter_f64<'a, I: IntoIterator<Item = &'a mut f64>>(
203    sample_iter: I,
204    sample_rate_hz: f64,
205    cutoff_frequency_hz: f64,
206) {
207    let mut filter = LowpassFilter::<f64>::new(sample_rate_hz, cutoff_frequency_hz);
208
209    for sample in sample_iter.into_iter() {
210        let new_sample = filter.run(*sample);
211        *sample = new_sample;
212    }
213}
214
215#[cfg(test)]
216mod test_util;
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::test_util::{calculate_power, sine_wave_samples, target_dir_test_artifacts};
222    use audio_visualizer::Channels;
223    use audio_visualizer::waveform::plotters_png_file::waveform_static_plotters_png_visualize;
224    use std::vec::Vec;
225
226    #[test]
227    fn test_lpf_and_visualize() {
228        let samples_l_orig = sine_wave_samples(120.0, 44100.0);
229        let samples_h_orig = sine_wave_samples(350.0, 44100.0);
230
231        waveform_static_plotters_png_visualize(
232            &samples_l_orig.iter().map(|x| *x as i16).collect::<Vec<_>>(),
233            Channels::Mono,
234            target_dir_test_artifacts().to_str().unwrap(),
235            "test_lpf_l_orig.png",
236        );
237        waveform_static_plotters_png_visualize(
238            &samples_h_orig.iter().map(|x| *x as i16).collect::<Vec<_>>(),
239            Channels::Mono,
240            target_dir_test_artifacts().to_str().unwrap(),
241            "test_lpf_h_orig.png",
242        );
243
244        let mut samples_l_lowpassed = samples_l_orig.clone();
245        let mut samples_h_lowpassed = samples_h_orig.clone();
246
247        let power_l_orig = calculate_power(&samples_l_orig);
248        let power_h_orig = calculate_power(&samples_h_orig);
249
250        lowpass_filter_f64(samples_l_lowpassed.as_mut_slice(), 44100.0, 90.0);
251        lowpass_filter_f64(samples_h_lowpassed.as_mut_slice(), 44100.0, 90.0);
252
253        let power_l_lowpassed = calculate_power(&samples_l_lowpassed);
254        let power_h_lowpassed = calculate_power(&samples_h_lowpassed);
255
256        waveform_static_plotters_png_visualize(
257            &samples_l_lowpassed
258                .iter()
259                .map(|x| *x as i16)
260                .collect::<Vec<_>>(),
261            Channels::Mono,
262            target_dir_test_artifacts().to_str().unwrap(),
263            "test_lpf_l_after.png",
264        );
265        waveform_static_plotters_png_visualize(
266            &samples_h_lowpassed
267                .iter()
268                .map(|x| *x as i16)
269                .collect::<Vec<_>>(),
270            Channels::Mono,
271            target_dir_test_artifacts().to_str().unwrap(),
272            "test_lpf_h_after.png",
273        );
274
275        assert!(power_h_lowpassed < power_h_orig);
276        assert!(power_l_lowpassed < power_l_orig);
277
278        assert!(power_h_lowpassed <= 3.0 * power_h_lowpassed);
279        assert!(
280            power_h_lowpassed * 3.0 <= power_l_lowpassed,
281            "LPF must actively remove frequencies above threshold"
282        );
283    }
284
285    /// Tests if the functions with f32 and f64 behave similar.
286    #[test]
287    fn test_lpf_f32_f64() {
288        let samples_h_orig = sine_wave_samples(350.0, 44100.0);
289        let mut lowpassed_f32 = samples_h_orig.iter().map(|x| *x as f32).collect::<Vec<_>>();
290        #[allow(clippy::redundant_clone)]
291        let mut lowpassed_f64 = samples_h_orig.clone();
292
293        lowpass_filter(lowpassed_f32.as_mut_slice(), 44100.0, 90.0);
294        lowpass_filter_f64(lowpassed_f64.as_mut_slice(), 44100.0, 90.0);
295
296        let power_f32 =
297            calculate_power(&lowpassed_f32.iter().map(|x| *x as f64).collect::<Vec<_>>());
298        let power_f64 = calculate_power(&lowpassed_f64);
299
300        assert!((power_f32 - power_f64).abs() <= 0.00024);
301    }
302}