audio-lab 0.1.0

Toolkit for audio research and testing
Documentation
use crate::signal::{
    data::RealData,
    math::{db_to_gain, gain_to_db},
};

impl RealData {
    pub fn max_per_channel(&self) -> Vec<(f64, f64)> {
        self.y_data()
            .outer_iter()
            .map(|channel| {
                let (max_index, max_value) = channel
                    .iter()
                    .enumerate()
                    .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
                    .expect("RealData channels always have consistent lengths");

                (self.x_axis().get(max_index), *max_value)
            })
            .collect()
    }

    pub fn max_overall(&self) -> (f64, f64) {
        *self
            .max_per_channel()
            .iter()
            .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
            .expect("RealData must contain at least one sample")
    }

    pub fn max_abs_per_channel(&self) -> Vec<(f64, f64)> {
        self.y_data()
            .outer_iter()
            .map(|channel| {
                let (max_index, max_abs_value) = channel
                    .iter()
                    .enumerate()
                    .map(|(idx, &value)| (idx, value.abs()))
                    .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
                    .expect("RealData channels always have consistent lengths");

                (self.x_axis().get(max_index), max_abs_value)
            })
            .collect()
    }

    pub fn max_abs_overall(&self) -> (f64, f64) {
        *self
            .max_abs_per_channel()
            .iter()
            .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
            .expect("RealData must contain at least one sample")
    }

    pub fn min_max_per_channel(&self) -> Vec<((f64, f64), (f64, f64))> {
        self.y_data()
            .outer_iter()
            .map(|channel| {
                let (min_index, min_value) = channel
                    .iter()
                    .enumerate()
                    .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
                    .expect("RealData channels always have consistent lengths");
                let (max_index, max_value) = channel
                    .iter()
                    .enumerate()
                    .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
                    .expect("RealData channels always have consistent lengths");

                (
                    (self.x_axis().get(min_index), *min_value),
                    (self.x_axis().get(max_index), *max_value),
                )
            })
            .collect()
    }

    pub fn min_max_overall(&self) -> ((f64, f64), (f64, f64)) {
        let per_channel = self.min_max_per_channel();

        let min = per_channel
            .iter()
            .map(|&(min, _)| min)
            .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
            .expect("RealData must contain at least one sample");
        let max = per_channel
            .iter()
            .map(|&(_, max)| max)
            .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
            .expect("RealData must contain at least one sample");

        (min, max)
    }

    pub fn to_decibels(&self) -> RealData {
        RealData::from_axis(self.x_axis().clone(), self.y_data().mapv(gain_to_db))
            .expect("x/y data originated from RealData")
    }

    pub fn to_gain(&self) -> RealData {
        RealData::from_axis(self.x_axis().clone(), self.y_data().mapv(db_to_gain))
            .expect("x/y data originated from RealData")
    }
}

#[cfg(test)]
mod tests {
    use approx::assert_abs_diff_eq;
    use ndarray::{arr2, aview2};

    use crate::signal::data::RealData;

    #[test]
    fn extrema_helpers() {
        let data = RealData::new(
            ndarray::arr1(&[0.0, 1.0, 2.0, 3.0, 4.0]),
            arr2(&[[0.0, 1.0, 5.0, -11.0, 1.0], [0.0, -10.0, 0.0, 6.0, 1.0]]),
        )
        .unwrap();

        assert_eq!(data.max_per_channel(), vec![(2.0, 5.0), (3.0, 6.0)]);
        assert_eq!(data.max_overall(), (3.0, 6.0));
        assert_eq!(data.max_abs_per_channel(), vec![(3.0, 11.0), (1.0, 10.0)]);
        assert_eq!(data.max_abs_overall(), (3.0, 11.0));
        assert_eq!(
            data.min_max_per_channel(),
            vec![((3.0, -11.0), (2.0, 5.0)), ((1.0, -10.0), (3.0, 6.0))]
        );
        assert_eq!(data.min_max_overall(), ((3.0, -11.0), (3.0, 6.0)));
    }

    #[test]
    fn db_gain_conversion_helpers() {
        let gain = RealData::new(ndarray::arr1(&[1.0, 2.0]), arr2(&[[1.0, 10.0]])).unwrap();
        let db = gain.to_decibels();
        assert_abs_diff_eq!(db.channel(0)[0], 0.0, epsilon = 1e-12);
        assert_abs_diff_eq!(db.channel(0)[1], 20.0, epsilon = 1e-12);

        let roundtrip = db.to_gain();
        assert!(
            roundtrip
                .y_data()
                .abs_diff_eq(&aview2(&[[1.0, 10.0]]), 1e-12)
        );
    }
}