lasprs 0.14.0

Library for Acoustic Signal Processing (Rust edition, with optional Python bindings via pyo3)
//! Parametric equalizer implementation details
use super::super::{Biquad, DummyFilter, Filter, Result, SeriesBiquad};
use super::FilterGenerator;
use crate::*;
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;

#[cfg_attr(
    feature = "python-bindings",
    gen_stub_pyclass,
    pyclass(get_all, set_all, from_py_object)
)]
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
/// Parametric equalizer configuration. This is a list of filter descriptors
/// that generate a series of biquad.
pub struct ParametricEqualizer {
    /// List of parametric filter descriptors.
    pub descriptors: Vec<ParametricFilterDescriptor>,
}

impl ParametricEqualizer {
    /// Create a new parametric equalizer with the given descriptors.
    pub fn new() -> Self {
        Self {
            descriptors: Vec::new(),
        }
    }
}

#[cfg(feature = "python-bindings")]
#[cfg_attr(feature = "python-bindings", gen_stub_pymethods, pymethods)]
impl ParametricEqualizer {
    /// Create a new parametric equalizer with the given descriptors.
    #[new]
    pub fn new_py() -> Self {
        Self::new()
    }
}

impl FilterGenerator for ParametricEqualizer {
    fn genFilter(&self, fs: StrictlyPositive) -> Result<Filter> {
        if self.descriptors.is_empty() {
            // No filters configured — return a bypass (no-op) filter
            return Ok(Filter::Dummy(DummyFilter));
        }
        let mut filters = Vec::with_capacity(self.descriptors.len());
        for desc in &self.descriptors {
            filters.push(desc.genFilter(fs)?);
        }
        Ok(SeriesBiquad::newFromBiqs(filters).into())
    }
}

#[cfg_attr(
    feature = "python-bindings",
    gen_stub_pyclass,
    pyclass(get_all, set_all, from_py_object)
)]
#[derive(Debug, Clone, Serialize, Deserialize)]
/// Filter descriptor for a single Audio EQ filter. Can be any of the following:
/// - LowPass: Second order low pass filter.
/// - HighPass: Second order high pass filter.
/// - LowShelf: Second order low shelf filter.
/// - HighShelf: Second order high shelf filter.
/// - Peaking: Second order peaking filter.
/// - Notch: Second order notch filter.
/// - Bypass: Bypass filter.
///
/// For some of the filters, not all settings are used.
pub struct ParametricFilterDescriptor {
    /// Center frequency of the filter in Hz. For a Notch filter, this is the
    /// frequency of the notch.
    pub f0: StrictlyPositive,
    /// Gain of the filter in **dB**. May be zero or negative. Used for
    /// Peaking, LowShelf and HighShelf filter types.
    pub gain: Flt,
    /// Quality factor of the filter.
    pub Q: StrictlyPositive,
    /// Type of the filter.
    pub filter_type: ParametricFilterType,
    /// Whether the filter is active or not, if not it is bypassed.
    pub active: bool,
    /// Additional metadata for this descriptor
    pub metadata: String,
}

#[cfg(feature = "python-bindings")]
#[gen_stub_pymethods]
#[pymethods]
impl ParametricFilterDescriptor {
    #[new]
    /// Generate a new ParametricFilterDescriptor, with some arbitrary default
    /// settings.
    pub fn new() -> Self {
        Self::default()
    }

    /// Generate a biquad filter from the descriptor.
    ///
    /// # Args
    /// * `fs` - Sampling frequency in Hz
    #[pyo3(name = "genFilter")]
    pub fn genFilter_py(&self, fs: Flt) -> PyResult<Biquad> {
        Ok(self.genFilter(fs.try_into()?)?)
    }
}

impl Default for ParametricFilterDescriptor {
    fn default() -> Self {
        Self {
            f0: 1e3.try_into().unwrap(),
            gain: 1.0,
            Q: 1.0.try_into().unwrap(),
            filter_type: ParametricFilterType::LowPass,
            active: true,
            metadata: String::new(),
        }
    }
}

impl ParametricFilterDescriptor {
    /// Generate a biquad filter from the descriptor.
    ///
    /// # Args
    /// * `fs` - Sampling frequency in Hz
    pub fn genFilter(&self, fs: StrictlyPositive) -> Result<Biquad> {
        let w0 = 2.0 * pi * *self.f0 / *fs;
        if !self.active {
            // Return a unit filter if the filter is inactive
            return Ok(Biquad::unit());
        }

        let alpha = Flt::sin(w0) / *self.Q / 2.;
        match &self.filter_type {
            ParametricFilterType::Bypass => Ok(Biquad::unit()),
            ParametricFilterType::Peaking => {
                let A = Flt::sqrt(Flt::powf(10., self.gain / 20.));
                let b0 = 1. + alpha * A;
                let b1 = -2. * Flt::cos(w0);
                let b2 = 1. - alpha * A;
                let a0 = 1. + alpha / A;
                let a1 = -2. * Flt::cos(w0);
                let a2 = 1. - alpha / A;
                Ok(Biquad::fromNotNormalized(&[b0, b1, b2, a0, a1, a2])?)
            }
            ParametricFilterType::LowPass => {
                let b0 = (1. - Flt::cos(w0)) / 2.;
                let b1 = 1. - Flt::cos(w0);
                let b2 = (1. - Flt::cos(w0)) / 2.;
                let a0 = 1. + alpha;
                let a1 = -2. * Flt::cos(w0);
                let a2 = 1. - alpha;
                Ok(Biquad::fromNotNormalized(&[b0, b1, b2, a0, a1, a2])?)
            }
            ParametricFilterType::HighPass => {
                let b0 = (1. + Flt::cos(w0)) / 2.;
                let b1 = -(1. + Flt::cos(w0));
                let b2 = (1. + Flt::cos(w0)) / 2.;
                let a0 = 1. + alpha;
                let a1 = -2. * Flt::cos(w0);
                let a2 = 1. - alpha;
                Ok(Biquad::fromNotNormalized(&[b0, b1, b2, a0, a1, a2])?)
            }
            ParametricFilterType::LowShelf => {
                let A = Flt::powf(10., self.gain / 40.);
                let b0 = A * ((A + 1.) - (A - 1.) * Flt::cos(w0) + 2. * Flt::sqrt(A) * alpha);
                let b1 = 2. * A * ((A - 1.) - (A + 1.) * Flt::cos(w0));
                let b2 = A * ((A + 1.) - (A - 1.) * Flt::cos(w0) - 2. * Flt::sqrt(A) * alpha);
                let a0 = (A + 1.) + (A - 1.) * Flt::cos(w0) + 2. * Flt::sqrt(A) * alpha;
                let a1 = -2. * ((A - 1.) + (A + 1.) * Flt::cos(w0));
                let a2 = (A + 1.) + (A - 1.) * Flt::cos(w0) - 2. * Flt::sqrt(A) * alpha;
                Ok(Biquad::fromNotNormalized(&[b0, b1, b2, a0, a1, a2])?)
            }
            ParametricFilterType::HighShelf => {
                let A = Flt::powf(10., self.gain / 40.);
                let b0 = A * ((A + 1.) + (A - 1.) * Flt::cos(w0) + 2. * Flt::sqrt(A) * alpha);
                let b1 = -2. * A * ((A - 1.) + (A + 1.) * Flt::cos(w0));
                let b2 = A * ((A + 1.) + (A - 1.) * Flt::cos(w0) - 2. * Flt::sqrt(A) * alpha);
                let a0 = (A + 1.) - (A - 1.) * Flt::cos(w0) + 2. * Flt::sqrt(A) * alpha;
                let a1 = 2. * ((A - 1.) - (A + 1.) * Flt::cos(w0));
                let a2 = (A + 1.) - (A - 1.) * Flt::cos(w0) - 2. * Flt::sqrt(A) * alpha;
                Ok(Biquad::fromNotNormalized(&[b0, b1, b2, a0, a1, a2])?)
            }
            ParametricFilterType::Notch => {
                let b0 = 1.;
                let b1 = -2. * Flt::cos(w0);
                let b2 = 1.;
                let a0 = 1. + alpha;
                let a1 = -2. * Flt::cos(w0);
                let a2 = 1. - alpha;
                Ok(Biquad::fromNotNormalized(&[b0, b1, b2, a0, a1, a2])?)
            }
        }
    }
}

#[cfg_attr(
    feature = "python-bindings",
    gen_stub_pyclass_enum,
    pyclass(from_py_object)
)]
#[derive(
    Debug, Copy, Clone, Serialize, Deserialize, strum_macros::EnumIter, strum_macros::Display,
)]
#[non_exhaustive] // Possibly add more later
#[repr(u32)]
/// The filter type of a parametric filter.
pub enum ParametricFilterType {
    /// Second order low pass filter.
    LowPass = 0,
    /// Second order high pass filter.
    HighPass = 1,
    /// Second order low shelf filter.
    LowShelf = 2,
    /// Second order high shelf filter.
    HighShelf = 3,
    /// Second order peaking filter.
    Peaking = 4,
    /// Second order notch filter.
    Notch = 5,
    /// Bypass filter.
    Bypass = 6,
}

#[cfg(feature = "python-bindings")]
#[cfg_attr(feature = "python-bindings", gen_stub_pymethods, pymethods)]
impl ParametricFilterType {
    #[staticmethod]
    fn all() -> Vec<ParametricFilterType> {
        Self::iter().collect()
    }

    fn __str__(&self) -> String {
        format!("{:?}", self)
    }
    fn type_index(&self) -> u32 {
        *self as u32
    }
}