#![deny(
clippy::all,
clippy::cargo,
clippy::nursery,
clippy::must_use_candidate,
// clippy::restriction,
// clippy::pedantic
)]
#![allow(
clippy::suboptimal_flops,
clippy::redundant_pub_crate,
clippy::fallible_impl_from
)]
#![deny(missing_debug_implementations)]
#![deny(rustdoc::all)]
#![no_std]
#[cfg_attr(test, macro_use)]
#[cfg(test)]
extern crate std;
use core::fmt::{Debug, Display};
use core::ops::{Add, AddAssign, Div, Mul, Neg, RangeInclusive, Sub};
mod sealed {
pub trait Sealed {}
impl Sealed for f32 {}
impl Sealed for f64 {}
}
pub trait Sample:
sealed::Sealed
+ Copy
+ PartialOrd
+ Debug
+ Display
+ Add<Output = Self>
+ AddAssign
+ Sub<Output = Self>
+ Mul<Output = Self>
+ Div<Output = Self>
+ Neg<Output = Self>
{
const ZERO: Self;
const ONE: Self;
const TWO: Self;
const PI: Self;
#[must_use]
fn clamp(self, min: Self, max: Self) -> Self;
}
impl Sample for f32 {
const ZERO: Self = 0.0;
const ONE: Self = 1.0;
const TWO: Self = 2.0;
const PI: Self = core::f32::consts::PI;
#[inline]
fn clamp(self, min: Self, max: Self) -> Self {
Self::clamp(self, min, max)
}
}
impl Sample for f64 {
const ZERO: Self = 0.0;
const ONE: Self = 1.0;
const TWO: Self = 2.0;
const PI: Self = core::f64::consts::PI;
#[inline]
fn clamp(self, min: Self, max: Self) -> Self {
Self::clamp(self, min, max)
}
}
#[derive(Debug, Clone)]
pub struct LowpassFilter<T> {
alpha: T,
beta: T,
prev: T,
next_is_first: bool,
}
impl<T: Sample> LowpassFilter<T> {
#[must_use]
pub fn new(sample_rate_hz: T, cutoff_frequency_hz: T) -> Self {
assert!(cutoff_frequency_hz * T::TWO <= sample_rate_hz);
let rc = T::ONE / (cutoff_frequency_hz * T::TWO * T::PI);
let dt = T::ONE / sample_rate_hz;
let alpha = dt / (rc + dt);
Self {
alpha,
beta: T::ONE - alpha,
prev: T::ZERO,
next_is_first: true,
}
}
#[inline]
pub fn run(&mut self, input: T) -> T {
let range: RangeInclusive<T> = -T::ONE..=T::ONE;
debug_assert!(
range.contains(&input),
"samples must be in range {range:?}: {input}"
);
let value = if self.next_is_first {
self.next_is_first = false;
self.prev = input;
input * self.alpha
} else {
self.prev = self.alpha * input + self.beta * self.prev;
self.prev
};
value.clamp(-T::ONE, T::ONE)
}
pub fn run_slice(&mut self, samples: &mut [T]) {
const LANES: usize = 8;
let mut samples = samples;
if self.next_is_first {
if let Some((first, rest)) = samples.split_first_mut() {
*first = self.run(*first);
samples = rest;
} else {
return;
}
}
let mut pow = [T::ONE; LANES];
for k in 1..LANES {
pow[k] = pow[k - 1] * self.beta;
}
let carry_coeffs = pow.map(|p| p * self.beta);
let mut cols = [[T::ZERO; LANES]; LANES];
for (j, col) in cols.iter_mut().enumerate() {
for (i, weight) in col.iter_mut().enumerate().skip(j) {
*weight = self.alpha * pow[i - j];
}
}
let (chunks, remainder) = samples.as_chunks_mut::<LANES>();
for chunk in chunks {
let mut acc = [T::ZERO; LANES];
for (col, &sample) in cols.iter().zip(chunk.iter()) {
for (acc, &coeff) in acc.iter_mut().zip(col.iter()) {
*acc += coeff * sample;
}
}
for (acc, &coeff) in acc.iter_mut().zip(carry_coeffs.iter()) {
*acc += coeff * self.prev;
}
self.prev = acc[LANES - 1];
for (sample, acc) in chunk.iter_mut().zip(acc.iter()) {
*sample = acc.clamp(-T::ONE, T::ONE);
}
}
for sample in remainder {
*sample = self.run(*sample);
}
}
pub const fn reset(&mut self) {
self.prev = T::ZERO;
self.next_is_first = true;
}
}
#[inline]
pub fn lowpass_filter<'a, I: IntoIterator<Item = &'a mut f32>>(
sample_iter: I,
sample_rate_hz: f32,
cutoff_frequency_hz: f32,
) {
let mut filter = LowpassFilter::<f32>::new(sample_rate_hz, cutoff_frequency_hz);
for sample in sample_iter.into_iter() {
let new_sample = filter.run(*sample);
*sample = new_sample;
}
}
#[inline]
pub fn lowpass_filter_f64<'a, I: IntoIterator<Item = &'a mut f64>>(
sample_iter: I,
sample_rate_hz: f64,
cutoff_frequency_hz: f64,
) {
let mut filter = LowpassFilter::<f64>::new(sample_rate_hz, cutoff_frequency_hz);
for sample in sample_iter.into_iter() {
let new_sample = filter.run(*sample);
*sample = new_sample;
}
}
#[inline]
pub fn lowpass_filter_slice(samples: &mut [f32], sample_rate_hz: f32, cutoff_frequency_hz: f32) {
let mut filter = LowpassFilter::<f32>::new(sample_rate_hz, cutoff_frequency_hz);
filter.run_slice(samples);
}
#[inline]
pub fn lowpass_filter_slice_f64(
samples: &mut [f64],
sample_rate_hz: f64,
cutoff_frequency_hz: f64,
) {
let mut filter = LowpassFilter::<f64>::new(sample_rate_hz, cutoff_frequency_hz);
filter.run_slice(samples);
}
#[cfg(test)]
mod test_util;
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::{calculate_power, sine_wave_samples, target_dir_test_artifacts};
use audio_visualizer::Channels;
use audio_visualizer::waveform::plotters_png_file::waveform_static_plotters_png_visualize;
use std::vec::Vec;
#[test]
fn test_lpf_and_visualize() {
let samples_l_orig = sine_wave_samples(120.0, 44100.0);
let samples_h_orig = sine_wave_samples(350.0, 44100.0);
waveform_static_plotters_png_visualize(
&samples_l_orig.iter().map(|x| *x as i16).collect::<Vec<_>>(),
Channels::Mono,
target_dir_test_artifacts().to_str().unwrap(),
"test_lpf_l_orig.png",
);
waveform_static_plotters_png_visualize(
&samples_h_orig.iter().map(|x| *x as i16).collect::<Vec<_>>(),
Channels::Mono,
target_dir_test_artifacts().to_str().unwrap(),
"test_lpf_h_orig.png",
);
let mut samples_l_lowpassed = samples_l_orig.clone();
let mut samples_h_lowpassed = samples_h_orig.clone();
let power_l_orig = calculate_power(&samples_l_orig);
let power_h_orig = calculate_power(&samples_h_orig);
lowpass_filter_f64(samples_l_lowpassed.as_mut_slice(), 44100.0, 90.0);
lowpass_filter_f64(samples_h_lowpassed.as_mut_slice(), 44100.0, 90.0);
let power_l_lowpassed = calculate_power(&samples_l_lowpassed);
let power_h_lowpassed = calculate_power(&samples_h_lowpassed);
waveform_static_plotters_png_visualize(
&samples_l_lowpassed
.iter()
.map(|x| *x as i16)
.collect::<Vec<_>>(),
Channels::Mono,
target_dir_test_artifacts().to_str().unwrap(),
"test_lpf_l_after.png",
);
waveform_static_plotters_png_visualize(
&samples_h_lowpassed
.iter()
.map(|x| *x as i16)
.collect::<Vec<_>>(),
Channels::Mono,
target_dir_test_artifacts().to_str().unwrap(),
"test_lpf_h_after.png",
);
assert!(power_h_lowpassed < power_h_orig);
assert!(power_l_lowpassed < power_l_orig);
assert!(
power_h_lowpassed * 3.0 <= power_l_lowpassed,
"LPF must actively remove frequencies above threshold"
);
}
#[test]
fn test_run_slice_matches_run() {
for n in [0_usize, 1, 3, 7, 8, 9, 16, 17, 41, 1003] {
let samples_f64 = (0..n)
.map(|i| (i as f64 * 0.37).sin() * 0.9)
.collect::<Vec<_>>();
let samples_f32 = samples_f64.iter().map(|&x| x as f32).collect::<Vec<_>>();
let mut expected_f32 = samples_f32.clone();
let mut actual_f32 = samples_f32.clone();
lowpass_filter(expected_f32.as_mut_slice(), 44100.0, 120.0);
lowpass_filter_slice(actual_f32.as_mut_slice(), 44100.0, 120.0);
for (i, (e, a)) in expected_f32.iter().zip(&actual_f32).enumerate() {
assert!((e - a).abs() < 1e-5, "f32, n={n}, i={i}: {e} vs {a}");
}
let mut expected_f64 = samples_f64.clone();
let mut actual_f64 = samples_f64.clone();
lowpass_filter_f64(expected_f64.as_mut_slice(), 44100.0, 120.0);
lowpass_filter_slice_f64(actual_f64.as_mut_slice(), 44100.0, 120.0);
for (i, (e, a)) in expected_f64.iter().zip(&actual_f64).enumerate() {
assert!((e - a).abs() < 1e-12, "f64, n={n}, i={i}: {e} vs {a}");
}
}
}
#[test]
fn test_run_slice_chunked_equals_whole() {
let samples = (0..500)
.map(|i| (i as f32 * 0.37).sin() * 0.9)
.collect::<Vec<_>>();
let mut whole = samples.clone();
let mut filter = LowpassFilter::<f32>::new(44100.0, 120.0);
filter.run_slice(whole.as_mut_slice());
let mut chunked = samples;
let mut filter = LowpassFilter::<f32>::new(44100.0, 120.0);
for chunk in chunked.chunks_mut(13) {
filter.run_slice(chunk);
}
for (i, (w, c)) in whole.iter().zip(&chunked).enumerate() {
assert!((w - c).abs() < 1e-5, "i={i}: {w} vs {c}");
}
}
#[test]
fn test_lpf_f32_f64() {
let samples_h_orig = sine_wave_samples(350.0, 44100.0);
let mut lowpassed_f32 = samples_h_orig.iter().map(|x| *x as f32).collect::<Vec<_>>();
#[allow(clippy::redundant_clone)]
let mut lowpassed_f64 = samples_h_orig.clone();
lowpass_filter(lowpassed_f32.as_mut_slice(), 44100.0, 90.0);
lowpass_filter_f64(lowpassed_f64.as_mut_slice(), 44100.0, 90.0);
let power_f32 =
calculate_power(&lowpassed_f32.iter().map(|x| *x as f64).collect::<Vec<_>>());
let power_f64 = calculate_power(&lowpassed_f64);
assert!((power_f32 - power_f64).abs() <= 0.00024);
}
}