mod butterworth;
mod notch;
mod sos;
pub use butterworth::{bessel_sos, butterworth_sos, chebyshev1_sos};
pub use notch::notch_sos;
pub use sos::{Section, Sos, DEFAULT_SETTLING_TOLERANCE};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FilterBand {
Lowpass(f64),
Highpass(f64),
Bandpass(f64, f64),
Bandstop(f64, f64),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FilterMode {
Forward,
#[default]
ForwardBackward,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FilterStart {
#[default]
Rest,
SteadyState,
}
#[derive(Debug, Clone, PartialEq)]
pub enum FilterDesign {
Butterworth { order: usize, band: FilterBand },
Chebyshev1 { order: usize, ripple_db: f64, band: FilterBand },
Bessel { order: usize, band: FilterBand },
Notch { freq_hz: f64, q: f64 },
Sos(Sos),
}
#[derive(Debug, Clone, PartialEq)]
pub struct FilterSpec {
pub design: FilterDesign,
pub mode: FilterMode,
pub start: FilterStart,
}
pub const DEFAULT_BUTTERWORTH_ORDER: usize = 5;
impl FilterSpec {
pub fn butterworth(order: usize, band: FilterBand) -> Self {
Self { design: FilterDesign::Butterworth { order, band }, mode: FilterMode::default(), start: FilterStart::default() }
}
pub fn bandpass(low_hz: f64, high_hz: f64) -> Self {
Self::butterworth(DEFAULT_BUTTERWORTH_ORDER, FilterBand::Bandpass(low_hz, high_hz))
}
pub fn highpass(cutoff_hz: f64) -> Self {
Self::butterworth(DEFAULT_BUTTERWORTH_ORDER, FilterBand::Highpass(cutoff_hz))
}
pub fn lowpass(cutoff_hz: f64) -> Self {
Self::butterworth(DEFAULT_BUTTERWORTH_ORDER, FilterBand::Lowpass(cutoff_hz))
}
pub fn bandstop(low_hz: f64, high_hz: f64) -> Self {
Self::butterworth(DEFAULT_BUTTERWORTH_ORDER, FilterBand::Bandstop(low_hz, high_hz))
}
pub fn chebyshev1(order: usize, ripple_db: f64, band: FilterBand) -> Self {
Self { design: FilterDesign::Chebyshev1 { order, ripple_db, band }, mode: FilterMode::default(), start: FilterStart::default() }
}
pub fn bessel(order: usize, band: FilterBand) -> Self {
Self { design: FilterDesign::Bessel { order, band }, mode: FilterMode::default(), start: FilterStart::default() }
}
pub fn notch(freq_hz: f64, q: f64) -> Self {
Self { design: FilterDesign::Notch { freq_hz, q }, mode: FilterMode::default(), start: FilterStart::default() }
}
pub fn sos(sos: Sos) -> Self {
Self { design: FilterDesign::Sos(sos), mode: FilterMode::default(), start: FilterStart::default() }
}
pub fn with_order(mut self, new_order: usize) -> Self {
if let FilterDesign::Butterworth { order, .. } | FilterDesign::Chebyshev1 { order, .. } | FilterDesign::Bessel { order, .. } = &mut self.design {
*order = new_order;
}
self
}
pub fn with_mode(mut self, mode: FilterMode) -> Self {
self.mode = mode;
self
}
pub fn with_start(mut self, start: FilterStart) -> Self {
self.start = start;
self
}
pub fn design(&self, sample_rate: f64) -> Result<Sos, FilterError> {
match &self.design {
FilterDesign::Butterworth { order, band } => butterworth_sos(*order, *band, sample_rate),
FilterDesign::Chebyshev1 { order, ripple_db, band } => chebyshev1_sos(*order, *ripple_db, *band, sample_rate),
FilterDesign::Bessel { order, band } => bessel_sos(*order, *band, sample_rate),
FilterDesign::Notch { freq_hz, q } => notch_sos(*freq_hz, *q, sample_rate),
FilterDesign::Sos(sos) => {
sos.validate()?;
Ok(sos.clone())
}
}
}
pub fn settling(&self, sample_rate: f64) -> Result<(usize, usize), FilterError> {
let s = self.design(sample_rate)?.settling_samples(DEFAULT_SETTLING_TOLERANCE);
Ok(match self.mode {
FilterMode::Forward => (s, 0),
FilterMode::ForwardBackward => (s, s),
})
}
}
#[derive(Debug, Clone, PartialEq, Error)]
pub enum FilterError {
#[error("sample rate must be positive and finite, got {0}")]
InvalidSampleRate(f64),
#[error("filter order must be at least 1")]
InvalidOrder,
#[error("cutoff {cutoff_hz} Hz must lie in (0, {nyquist_hz}) Hz")]
CutoffOutOfRange { cutoff_hz: f64, nyquist_hz: f64 },
#[error("band edges must satisfy low < high, got {low_hz} Hz and {high_hz} Hz")]
InvalidBand { low_hz: f64, high_hz: f64 },
#[error("quality factor must be positive and finite, got {0}")]
InvalidQ(f64),
#[error("pass-band ripple must be positive and finite dB, got {0}")]
InvalidRipple(f64),
#[error("second-order sections must be non-empty, finite, with a0 = 1 and stable poles")]
InvalidSections,
#[error("forward-backward filters need future samples and cannot run on a live stream")]
ForwardBackwardOnLiveStream,
}
pub(crate) fn check_sample_rate(fs: f64) -> Result<(), FilterError> {
if fs.is_finite() && fs > 0.0 { Ok(()) } else { Err(FilterError::InvalidSampleRate(fs)) }
}
pub(crate) fn check_cutoff(cutoff_hz: f64, fs: f64) -> Result<(), FilterError> {
let nyquist_hz = fs / 2.0;
if cutoff_hz.is_finite() && cutoff_hz > 0.0 && cutoff_hz < nyquist_hz {
Ok(())
} else {
Err(FilterError::CutoffOutOfRange { cutoff_hz, nyquist_hz })
}
}