audio_viz/
waveform.rs

1// SPDX-FileCopyrightText: The audio-viz authors
2// SPDX-License-Identifier: MPL-2.0
3
4#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Default)]
5#[repr(transparent)]
6pub struct WaveformVal(pub u8);
7
8impl WaveformVal {
9    const MIN_VAL: u8 = u8::MIN;
10    const MAX_VAL: u8 = u8::MAX;
11
12    pub(crate) fn from_f32(val: f32) -> Self {
13        debug_assert!(val >= f32::from(Self::MIN_VAL));
14        let mapped = (val * (f32::from(Self::MAX_VAL) + 1.0)).min(f32::from(Self::MAX_VAL));
15        debug_assert!(mapped >= f32::from(Self::MIN_VAL));
16        debug_assert!(mapped <= f32::from(Self::MAX_VAL));
17        #[expect(clippy::cast_possible_truncation)]
18        #[expect(clippy::cast_sign_loss)]
19        Self(mapped as u8)
20    }
21
22    #[must_use]
23    pub fn to_f32(self) -> f32 {
24        f32::from(self.0) / f32::from(Self::MAX_VAL)
25    }
26
27    #[must_use]
28    pub const fn is_zero(self) -> bool {
29        self.0 == 0
30    }
31}
32
33impl From<WaveformVal> for u8 {
34    fn from(value: WaveformVal) -> Self {
35        value.0
36    }
37}
38
39#[derive(Debug, Clone, Copy, Default)]
40pub struct FilteredWaveformVal {
41    pub all: WaveformVal,
42    pub low: WaveformVal,
43    pub mid: WaveformVal,
44    pub high: WaveformVal,
45}
46
47impl FilteredWaveformVal {
48    /// RGB color with full brightness
49    #[must_use]
50    pub fn spectral_rgb_color(self) -> (f32, f32, f32) {
51        self.spectral_rgb_color_normalized(0.0)
52    }
53
54    /// RGB color with brightness limited by [`Self::all`]
55    #[must_use]
56    pub fn spectral_rgb_color_all(self) -> (f32, f32, f32) {
57        self.spectral_rgb_color_normalized(self.all.to_f32())
58    }
59
60    #[must_use]
61    fn spectral_rgb_color_normalized(self, max: f32) -> (f32, f32, f32) {
62        let Self {
63            all: _,
64            low,
65            mid,
66            high,
67        } = self;
68        let low = low.to_f32();
69        let mid = mid.to_f32();
70        let high = high.to_f32();
71        // The `max` value is used to control the brightness of the resulting color.
72        // Otherwise we would only reach the edges of the RGB space with one component
73        // always maxed out.
74        let denom = max.max(low).max(mid).max(high);
75        if denom == 0.0 {
76            return (0.0, 0.0, 0.0);
77        }
78        let red = low / denom;
79        let green = mid / denom;
80        let blue = high / denom;
81        (red, green, blue)
82    }
83}
84
85#[derive(Debug, Clone, Copy, Default)]
86pub struct WaveformBin {
87    /// Clamped, absolute peak value in the range `0..=1`
88    pub peak: WaveformVal,
89
90    /// Clamped and scaled RMS value in the range `0..=1`.
91    pub energy: WaveformVal,
92}
93
94#[derive(Debug, Clone, Default)]
95pub struct FilteredWaveformBin {
96    pub all: WaveformBin,
97    pub low: WaveformBin,
98    pub mid: WaveformBin,
99    pub high: WaveformBin,
100}
101
102impl FilteredWaveformBin {
103    /// Peak values
104    #[must_use]
105    pub const fn peak(&self) -> FilteredWaveformVal {
106        let Self {
107            all,
108            low,
109            mid,
110            high,
111        } = self;
112        FilteredWaveformVal {
113            all: all.peak,
114            low: low.peak,
115            mid: mid.peak,
116            high: high.peak,
117        }
118    }
119
120    /// Scaled RMS values
121    #[must_use]
122    pub const fn energy(&self) -> FilteredWaveformVal {
123        let Self {
124            all,
125            low,
126            mid,
127            high,
128        } = self;
129        FilteredWaveformVal {
130            all: all.energy,
131            low: low.energy,
132            mid: mid.energy,
133            high: high.energy,
134        }
135    }
136
137    /// <https://en.wikipedia.org/wiki/Spectral_flatness>
138    #[must_use]
139    pub fn spectral_flatness(&self) -> f32 {
140        let FilteredWaveformVal {
141            all: _,
142            low,
143            mid,
144            high,
145        } = self.energy();
146        let low = low.to_f32();
147        let mid = mid.to_f32();
148        let high = high.to_f32();
149        let arithmetic_mean = (low + mid + high) / 3.0;
150        if arithmetic_mean == 0.0 {
151            // Perfectly flat spectrum.
152            return 1.0;
153        }
154        debug_assert!(arithmetic_mean > 0.0);
155        debug_assert!(arithmetic_mean <= 1.0);
156        let geometric_mean = (low * mid * high).powf(1.0 / 3.0);
157        debug_assert!(geometric_mean >= 0.0);
158        debug_assert!(geometric_mean <= 1.0);
159        geometric_mean / arithmetic_mean
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::WaveformVal;
166
167    #[test]
168    fn waveform_val_from_f32() {
169        assert_eq!(
170            WaveformVal::from_f32(0.0),
171            WaveformVal(WaveformVal::MIN_VAL)
172        );
173        assert_eq!(WaveformVal::from_f32(0.25), WaveformVal(64));
174        assert_eq!(WaveformVal::from_f32(0.5), WaveformVal(128));
175        assert_eq!(WaveformVal::from_f32(0.75), WaveformVal(192));
176        assert_eq!(
177            WaveformVal::from_f32(1.0),
178            WaveformVal(WaveformVal::MAX_VAL)
179        );
180    }
181
182    #[test]
183    fn waveform_val_to_from_f32() {
184        for val in WaveformVal::MIN_VAL..=WaveformVal::MAX_VAL {
185            let val = WaveformVal(val);
186            assert_eq!(val, WaveformVal::from_f32(val.to_f32()));
187        }
188    }
189
190    #[test]
191    fn spectral_flatness_one() {
192        for val in WaveformVal::MIN_VAL..=WaveformVal::MAX_VAL {
193            let val = WaveformVal(val);
194            let bin = super::FilteredWaveformBin {
195                all: super::WaveformBin {
196                    peak: val,
197                    energy: val,
198                },
199                low: super::WaveformBin {
200                    peak: val,
201                    energy: val,
202                },
203                mid: super::WaveformBin {
204                    peak: val,
205                    energy: val,
206                },
207                high: super::WaveformBin {
208                    peak: val,
209                    energy: val,
210                },
211            };
212            let spectral_flatness = bin.spectral_flatness();
213            assert!(spectral_flatness > 0.999_999);
214        }
215    }
216}