Skip to main content

audio_lab/signal/ops/
real.rs

1use crate::signal::{
2    data::RealData,
3    math::{db_to_gain, gain_to_db},
4};
5
6impl RealData {
7    pub fn max_per_channel(&self) -> Vec<(f64, f64)> {
8        self.y_data()
9            .outer_iter()
10            .map(|channel| {
11                let (max_index, max_value) = channel
12                    .iter()
13                    .enumerate()
14                    .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
15                    .expect("RealData channels always have consistent lengths");
16
17                (self.x_axis().get(max_index), *max_value)
18            })
19            .collect()
20    }
21
22    pub fn max_overall(&self) -> (f64, f64) {
23        *self
24            .max_per_channel()
25            .iter()
26            .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
27            .expect("RealData must contain at least one sample")
28    }
29
30    pub fn max_abs_per_channel(&self) -> Vec<(f64, f64)> {
31        self.y_data()
32            .outer_iter()
33            .map(|channel| {
34                let (max_index, max_abs_value) = channel
35                    .iter()
36                    .enumerate()
37                    .map(|(idx, &value)| (idx, value.abs()))
38                    .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
39                    .expect("RealData channels always have consistent lengths");
40
41                (self.x_axis().get(max_index), max_abs_value)
42            })
43            .collect()
44    }
45
46    pub fn max_abs_overall(&self) -> (f64, f64) {
47        *self
48            .max_abs_per_channel()
49            .iter()
50            .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
51            .expect("RealData must contain at least one sample")
52    }
53
54    pub fn min_max_per_channel(&self) -> Vec<((f64, f64), (f64, f64))> {
55        self.y_data()
56            .outer_iter()
57            .map(|channel| {
58                let (min_index, min_value) = channel
59                    .iter()
60                    .enumerate()
61                    .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
62                    .expect("RealData channels always have consistent lengths");
63                let (max_index, max_value) = channel
64                    .iter()
65                    .enumerate()
66                    .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
67                    .expect("RealData channels always have consistent lengths");
68
69                (
70                    (self.x_axis().get(min_index), *min_value),
71                    (self.x_axis().get(max_index), *max_value),
72                )
73            })
74            .collect()
75    }
76
77    pub fn min_max_overall(&self) -> ((f64, f64), (f64, f64)) {
78        let per_channel = self.min_max_per_channel();
79
80        let min = per_channel
81            .iter()
82            .map(|&(min, _)| min)
83            .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
84            .expect("RealData must contain at least one sample");
85        let max = per_channel
86            .iter()
87            .map(|&(_, max)| max)
88            .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
89            .expect("RealData must contain at least one sample");
90
91        (min, max)
92    }
93
94    pub fn to_decibels(&self) -> RealData {
95        RealData::from_axis(self.x_axis().clone(), self.y_data().mapv(gain_to_db))
96            .expect("x/y data originated from RealData")
97    }
98
99    pub fn to_gain(&self) -> RealData {
100        RealData::from_axis(self.x_axis().clone(), self.y_data().mapv(db_to_gain))
101            .expect("x/y data originated from RealData")
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use approx::assert_abs_diff_eq;
108    use ndarray::{arr2, aview2};
109
110    use crate::signal::data::RealData;
111
112    #[test]
113    fn extrema_helpers() {
114        let data = RealData::new(
115            ndarray::arr1(&[0.0, 1.0, 2.0, 3.0, 4.0]),
116            arr2(&[[0.0, 1.0, 5.0, -11.0, 1.0], [0.0, -10.0, 0.0, 6.0, 1.0]]),
117        )
118        .unwrap();
119
120        assert_eq!(data.max_per_channel(), vec![(2.0, 5.0), (3.0, 6.0)]);
121        assert_eq!(data.max_overall(), (3.0, 6.0));
122        assert_eq!(data.max_abs_per_channel(), vec![(3.0, 11.0), (1.0, 10.0)]);
123        assert_eq!(data.max_abs_overall(), (3.0, 11.0));
124        assert_eq!(
125            data.min_max_per_channel(),
126            vec![((3.0, -11.0), (2.0, 5.0)), ((1.0, -10.0), (3.0, 6.0))]
127        );
128        assert_eq!(data.min_max_overall(), ((3.0, -11.0), (3.0, 6.0)));
129    }
130
131    #[test]
132    fn db_gain_conversion_helpers() {
133        let gain = RealData::new(ndarray::arr1(&[1.0, 2.0]), arr2(&[[1.0, 10.0]])).unwrap();
134        let db = gain.to_decibels();
135        assert_abs_diff_eq!(db.channel(0)[0], 0.0, epsilon = 1e-12);
136        assert_abs_diff_eq!(db.channel(0)[1], 20.0, epsilon = 1e-12);
137
138        let roundtrip = db.to_gain();
139        assert!(
140            roundtrip
141                .y_data()
142                .abs_diff_eq(&aview2(&[[1.0, 10.0]]), 1e-12)
143        );
144    }
145}