lasprs 0.14.3

Library for Acoustic Signal Processing (Rust edition, with optional Python bindings via pyo3)
Documentation
use crate::config::*;
use std::time::Duration;

use strum::EnumMessage;
use strum_macros::Display;
/// Time weighting to use in level detection of Sound Level Meter.
#[derive(Clone, Copy, Debug, PartialEq, Display)]
#[cfg_attr(
    feature = "python-bindings",
    gen_stub_pyclass_complex_enum,
    pyclass(eq, from_py_object)
)]
pub enum TimeWeighting {
    // I know that the curly braces here are not required and add some
    // boilerplate, but this is the only way Pyo3 swallows complex enums at the
    // moment.
    /// Slow time weighting ~ 1 s
    Slow {},
    /// Fast time weighting ~ 1/8 s
    Fast {},
    /// Impulse time weighting ~ 30 ms
    Impulse {},
    /// A custom symmetric time weighting
    CustomSymmetric {
        /// Custom time constant [s]
        t: Flt,
    },
    /// Ten seconds, is very slow
    Tens {},

    /// A custom symmetric time weighting
    CustomAsymmetric {
        /// Time weighting when level is increasing
        tup: Flt,
        /// Time weighting when level is decreasing
        tdown: Flt,
    },
}

#[cfg(feature = "python-bindings")]
#[cfg_attr(feature = "python-bindings", gen_stub_pymethods, pymethods)]
impl TimeWeighting {
    fn __str__(&self) -> String {
        format!("{self}")
    }
    #[staticmethod]
    fn all_standards() -> Vec<TimeWeighting> {
        use TimeWeighting::*;
        vec![Slow {}, Fast {}, Impulse {}]
    }
    #[staticmethod]
    fn for_realtime() -> Vec<TimeWeighting> {
        use TimeWeighting::*;
        vec![Slow {}, Fast {}, Impulse {}, Tens {}]
    }

    #[pyo3(name = "getLowpassPoles")]
    fn getLowpassPoles_py(&self) -> (Flt, Option<Flt>) {
        self.getLowpassPoles()
    }
}

impl Default for TimeWeighting {
    fn default() -> Self {
        TimeWeighting::Fast {}
    }
}
impl TimeWeighting {
    /// Time required for the time weighting filter to settle before the
    /// levels-vs-time output is valid: 3 times the rising time constant.
    /// Slow → 3 s, Fast → 3/8 s, Impulse → 105 ms, Tens → 30 s.
    pub fn warmupTime(&self) -> Duration {
        let (pole_up, _) = self.getLowpassPoles();
        let tau = 1. / pole_up.abs();
        Duration::from_secs_f64(3. * tau)
    }

    /// get the analog poles of the single pole lowpass filter required for
    /// getting the 'rectified' level (detection phase of SLM).
    pub fn getLowpassPoles(&self) -> (Flt, Option<Flt>) {
        use TimeWeighting::*;
        match self {
            Slow {} => {
                // Time constant is 1 s, pole is at -1 rad/s
                (-1.0, None)
            }
            Fast {} => {
                // Time constant is 1/8 s, pole is at -8 rad/s
                (-8., None)
            }
            Tens {} => {
                // Time constant is 10 s, pole is at -0.1 rad/s
                (-0.1, None)
            }
            Impulse {} => {
                // For the impulse time weighting, some source says ~ 2.9 dB/s
                // drop for the decay
                // [https://www.nti-audio.com/en/support/know-how/fast-slow-impulse-time-weighting-what-do-they-mean].
                //
                // Other source
                // [https://support.dewesoft.com/en/support/solutions/articles/14000139949-exponential-averaging-fast-f-slow-s-impulse-i-]
                // say a time constant of 1.5 s. Are they compatible?

                // Compute decay rate in dB/s from the filter time constant. An
                // initial value drops as exp(-t/tau). So in 1 s the level drops
                // with 10*log10(exp(-1.0/tau)) = -10/ln(10)/tau ≅ -4.34/tau
                // dB/s where ln denotes the natural logarithm. So suppose we
                // have 1.5 s, we indeed get a decay rate of 2.9 dB/s
                (-1. / 35e-3, Some(-1. / 1.5))
            }
            CustomSymmetric { t } => {
                assert!(*t > 0.);
                (-*t, None)
            }
            CustomAsymmetric { tup, tdown } => {
                assert!(*tup > 0.);
                assert!(*tdown > 0.);
                (-1. / (*tup), Some(-1. / (*tdown)))
            }
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_tw() {
        println!("Impulse: {}", TimeWeighting::Impulse {});
        println!("Fast   : {}", TimeWeighting::Fast {});
        println!("Slow   : {}", TimeWeighting::Slow {});
    }
}