Skip to main content

proteus_lib/playback/
output_meter.rs

1//! Output meter for tracking playback levels.
2
3#[cfg(feature = "output-meter")]
4mod enabled {
5    use std::collections::VecDeque;
6
7    use rodio::buffer::SamplesBuffer;
8    use rodio::Source;
9
10    #[derive(Debug)]
11    struct Frame {
12        peak: Vec<f32>,
13        avg: Vec<f32>,
14        len_samples: usize,
15    }
16
17    #[derive(Debug)]
18    pub struct OutputMeter {
19        sample_rate: u32,
20        channels: usize,
21        refresh_hz: f32,
22        frame_samples_per_channel: usize,
23        sample_remainder: f64,
24        current_frame_remaining: usize,
25        levels: Vec<f32>,
26        averages: Vec<f32>,
27        queue: VecDeque<Frame>,
28    }
29
30    impl OutputMeter {
31        pub fn new(channels: usize, sample_rate: u32, refresh_hz: f32) -> Self {
32            let channels = channels.max(1);
33            let sample_rate = sample_rate.max(1);
34            let refresh_hz = refresh_hz.max(1.0);
35            Self {
36                sample_rate,
37                channels,
38                refresh_hz,
39                frame_samples_per_channel: frame_samples_per_channel(sample_rate, refresh_hz),
40                sample_remainder: 0.0,
41                current_frame_remaining: 0,
42                levels: vec![0.0; channels],
43                averages: vec![0.0; channels],
44                queue: VecDeque::new(),
45            }
46        }
47
48        pub fn reset(&mut self) {
49            self.queue.clear();
50            self.sample_remainder = 0.0;
51            self.current_frame_remaining = 0;
52            self.levels.fill(0.0);
53            self.averages.fill(0.0);
54        }
55
56        pub fn set_refresh_hz(&mut self, refresh_hz: f32) {
57            let refresh_hz = refresh_hz.max(1.0);
58            if (refresh_hz - self.refresh_hz).abs() <= f32::EPSILON {
59                return;
60            }
61            self.refresh_hz = refresh_hz;
62            self.frame_samples_per_channel =
63                frame_samples_per_channel(self.sample_rate, self.refresh_hz);
64            self.reset();
65        }
66
67        pub fn push_samples(&mut self, buffer: &SamplesBuffer) {
68            let channels = buffer.channels().max(1) as usize;
69            let sample_rate = buffer.sample_rate().max(1);
70            if channels != self.channels {
71                self.channels = channels;
72                self.levels = vec![0.0; channels];
73                self.averages = vec![0.0; channels];
74            }
75            if sample_rate != self.sample_rate {
76                self.sample_rate = sample_rate;
77                self.frame_samples_per_channel =
78                    frame_samples_per_channel(self.sample_rate, self.refresh_hz);
79                self.reset();
80            }
81
82            let frame_len_samples = self.frame_samples_per_channel * channels;
83            let mut peak = vec![0.0_f32; channels];
84            let mut sum = vec![0.0_f32; channels];
85            let mut count = vec![0_usize; channels];
86            let mut in_frame = 0_usize;
87
88            for (idx, sample) in buffer.clone().enumerate() {
89                let ch = idx % channels;
90                let value = sample.abs();
91                if value > peak[ch] {
92                    peak[ch] = value;
93                }
94                sum[ch] += value;
95                count[ch] += 1;
96                in_frame += 1;
97
98                if in_frame >= frame_len_samples {
99                    self.queue
100                        .push_back(finalize_frame(&peak, &sum, &count, in_frame));
101                    peak.fill(0.0);
102                    sum.fill(0.0);
103                    count.fill(0);
104                    in_frame = 0;
105                }
106            }
107
108            if in_frame > 0 {
109                self.queue
110                    .push_back(finalize_frame(&peak, &sum, &count, in_frame));
111            }
112        }
113
114        pub fn advance(&mut self, elapsed_seconds: f64) {
115            if elapsed_seconds <= 0.0 {
116                return;
117            }
118
119            let mut samples = elapsed_seconds * self.sample_rate as f64 * self.channels as f64;
120            samples += self.sample_remainder;
121            let mut samples_to_advance = samples.floor() as usize;
122            self.sample_remainder = samples - samples_to_advance as f64;
123
124            while samples_to_advance > 0 {
125                if self.current_frame_remaining == 0 {
126                    let Some(frame) = self.queue.pop_front() else {
127                        break;
128                    };
129                    self.levels = frame.peak;
130                    self.averages = frame.avg;
131                    self.current_frame_remaining = frame.len_samples;
132                }
133
134                let take = samples_to_advance.min(self.current_frame_remaining);
135                self.current_frame_remaining -= take;
136                samples_to_advance -= take;
137            }
138        }
139
140        pub fn levels(&self) -> Vec<f32> {
141            self.levels.clone()
142        }
143
144        pub fn averages(&self) -> Vec<f32> {
145            self.averages.clone()
146        }
147    }
148
149    fn frame_samples_per_channel(sample_rate: u32, refresh_hz: f32) -> usize {
150        ((sample_rate as f32 / refresh_hz).round() as usize).max(1)
151    }
152
153    fn finalize_frame(peak: &[f32], sum: &[f32], count: &[usize], len_samples: usize) -> Frame {
154        let mut avg = Vec::with_capacity(sum.len());
155        for (idx, value) in sum.iter().enumerate() {
156            let denom = count[idx].max(1) as f32;
157            avg.push(value / denom);
158        }
159        Frame {
160            peak: peak.to_vec(),
161            avg,
162            len_samples,
163        }
164    }
165}
166
167#[cfg(not(feature = "output-meter"))]
168mod disabled {
169    use rodio::buffer::SamplesBuffer;
170
171    #[derive(Debug)]
172    pub struct OutputMeter {
173        channels: usize,
174    }
175
176    impl OutputMeter {
177        pub fn new(channels: usize, _sample_rate: u32, _refresh_hz: f32) -> Self {
178            Self {
179                channels: channels.max(1),
180            }
181        }
182
183        pub fn reset(&mut self) {}
184
185        pub fn set_refresh_hz(&mut self, _refresh_hz: f32) {}
186
187        pub fn push_samples(&mut self, _buffer: &SamplesBuffer) {}
188
189        pub fn advance(&mut self, _elapsed_seconds: f64) {}
190
191        pub fn levels(&self) -> Vec<f32> {
192            vec![0.0; self.channels]
193        }
194
195        pub fn averages(&self) -> Vec<f32> {
196            vec![0.0; self.channels]
197        }
198    }
199}
200
201#[cfg(not(feature = "output-meter"))]
202pub use disabled::OutputMeter;
203#[cfg(feature = "output-meter")]
204pub use enabled::OutputMeter;
205
206#[cfg(test)]
207mod tests {
208    use super::OutputMeter;
209
210    #[cfg(feature = "output-meter")]
211    #[test]
212    fn output_meter_tracks_peak_and_avg() {
213        use rodio::buffer::SamplesBuffer;
214
215        let mut meter = OutputMeter::new(2, 10, 1.0);
216        let samples = vec![
217            0.1_f32, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 0.2, 0.1, 0.4, 0.3, 0.6, 0.5,
218            0.8, 0.7, 1.0, 0.9,
219        ];
220        let buffer = SamplesBuffer::new(2, 10, samples);
221        meter.push_samples(&buffer);
222        meter.advance(1.0);
223
224        let levels = meter.levels();
225        let avg = meter.averages();
226        assert_eq!(levels.len(), 2);
227        assert_eq!(avg.len(), 2);
228        assert!((levels[0] - 1.0).abs() < 1e-6);
229        assert!((levels[1] - 1.0).abs() < 1e-6);
230        assert!(avg[0] > 0.0);
231        assert!(avg[1] > 0.0);
232    }
233
234    #[cfg(not(feature = "output-meter"))]
235    #[test]
236    fn output_meter_disabled_returns_zeroes() {
237        let meter = OutputMeter::new(2, 48_000, 10.0);
238        assert_eq!(meter.levels(), vec![0.0, 0.0]);
239        assert_eq!(meter.averages(), vec![0.0, 0.0]);
240    }
241}