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}