Skip to main content

wavecraft_metering/
lib.rs

1//! Real-time safe metering for audio→UI communication.
2//!
3//! Provides lock-free SPSC ring buffers for transferring peak/RMS meter data
4//! from the audio thread to the UI thread without allocations or blocking.
5
6/// Frame of stereo metering data.
7///
8/// Sent from audio thread → UI thread via SPSC ring buffer.
9/// All values are linear (not dB) for real-time efficiency.
10#[derive(Clone, Copy, Default, Debug)]
11#[repr(C)]
12pub struct MeterFrame {
13    /// Left channel peak (linear, 0.0 to 1.0+)
14    pub peak_l: f32,
15    /// Right channel peak (linear, 0.0 to 1.0+)
16    pub peak_r: f32,
17    /// Left channel RMS (linear, 0.0 to 1.0+)
18    pub rms_l: f32,
19    /// Right channel RMS (linear, 0.0 to 1.0+)
20    pub rms_r: f32,
21    /// Sample timestamp (monotonic, for UI interpolation)
22    pub timestamp: u64,
23}
24
25/// Producer side of meter channel (audio thread).
26///
27/// Real-time safe: no allocations, no locks.
28pub struct MeterProducer {
29    producer: rtrb::Producer<MeterFrame>,
30}
31
32impl MeterProducer {
33    /// Push a meter frame to the ring buffer.
34    ///
35    /// Fails silently if buffer is full (drops oldest frame).
36    /// This is acceptable for metering; UI will get next frame.
37    #[inline]
38    pub fn push(&mut self, frame: MeterFrame) {
39        // Non-blocking write; if full, we just skip this frame
40        let _ = self.producer.push(frame);
41    }
42
43    /// Returns the number of frames that can be written without blocking.
44    #[inline]
45    pub fn available_write(&self) -> usize {
46        self.producer.slots()
47    }
48}
49
50/// Consumer side of meter channel (UI thread).
51///
52/// Not real-time safe (can allocate), but non-blocking on audio thread.
53pub struct MeterConsumer {
54    consumer: rtrb::Consumer<MeterFrame>,
55}
56
57impl MeterConsumer {
58    /// Read the latest meter frame, discarding all older frames.
59    ///
60    /// Returns `None` if no frames are available.
61    /// Efficient for UI polling: only processes most recent data.
62    pub fn read_latest(&mut self) -> Option<MeterFrame> {
63        let mut latest = None;
64        while let Ok(frame) = self.consumer.pop() {
65            latest = Some(frame);
66        }
67        latest
68    }
69
70    /// Pop the oldest meter frame from the buffer.
71    ///
72    /// Returns `None` if no frames are available.
73    pub fn pop(&mut self) -> Option<MeterFrame> {
74        self.consumer.pop().ok()
75    }
76
77    /// Returns the number of frames available to read.
78    pub fn available_read(&self) -> usize {
79        self.consumer.slots()
80    }
81}
82
83/// Create a pair of meter producer/consumer with the given buffer capacity.
84///
85/// Capacity should be large enough to handle UI polling delays without drops,
86/// but small enough to avoid stale data.
87///
88/// Recommended: 32-128 frames (enough for 60 Hz UI @ 512-sample audio blocks).
89pub fn create_meter_channel(capacity: usize) -> (MeterProducer, MeterConsumer) {
90    let (producer, consumer) = rtrb::RingBuffer::new(capacity);
91    (MeterProducer { producer }, MeterConsumer { consumer })
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn meter_ring_push_pop() {
100        let (mut producer, mut consumer) = create_meter_channel(4);
101
102        let frame = MeterFrame {
103            peak_l: 0.5,
104            peak_r: 0.6,
105            rms_l: 0.3,
106            rms_r: 0.4,
107            timestamp: 1000,
108        };
109
110        producer.push(frame);
111        let read = consumer.pop().expect("should read frame");
112
113        assert_eq!(read.peak_l, 0.5);
114        assert_eq!(read.peak_r, 0.6);
115        assert_eq!(read.rms_l, 0.3);
116        assert_eq!(read.rms_r, 0.4);
117        assert_eq!(read.timestamp, 1000);
118    }
119
120    #[test]
121    fn meter_ring_overflow() {
122        let (mut producer, mut consumer) = create_meter_channel(2);
123
124        // Fill buffer
125        producer.push(MeterFrame {
126            peak_l: 1.0,
127            ..Default::default()
128        });
129        producer.push(MeterFrame {
130            peak_l: 2.0,
131            ..Default::default()
132        });
133
134        // Overflow silently drops (ring buffer behavior)
135        producer.push(MeterFrame {
136            peak_l: 3.0,
137            ..Default::default()
138        });
139
140        // First two frames should still be readable
141        assert_eq!(consumer.pop().unwrap().peak_l, 1.0);
142        assert_eq!(consumer.pop().unwrap().peak_l, 2.0);
143
144        // Third frame was dropped
145        assert!(consumer.pop().is_none());
146    }
147
148    #[test]
149    fn read_latest_discards_old() {
150        let (mut producer, mut consumer) = create_meter_channel(8);
151
152        // Push multiple frames
153        for i in 0..5 {
154            producer.push(MeterFrame {
155                peak_l: i as f32,
156                ..Default::default()
157            });
158        }
159
160        // read_latest should return only the newest frame
161        let latest = consumer.read_latest().expect("should have frame");
162        assert_eq!(latest.peak_l, 4.0);
163
164        // All frames consumed
165        assert!(consumer.pop().is_none());
166    }
167
168    #[test]
169    fn empty_buffer_returns_none() {
170        let (_, mut consumer) = create_meter_channel(4);
171        assert!(consumer.pop().is_none());
172        assert!(consumer.read_latest().is_none());
173    }
174
175    #[test]
176    fn available_counts() {
177        let (mut producer, mut consumer) = create_meter_channel(4);
178
179        assert_eq!(consumer.available_read(), 0);
180
181        producer.push(MeterFrame::default());
182        producer.push(MeterFrame::default());
183
184        assert_eq!(consumer.available_read(), 2);
185
186        consumer.pop();
187        assert_eq!(consumer.available_read(), 1);
188    }
189}