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