truce_core/meters.rs
1//! Shared meter storage - the mediation channel between the audio
2//! thread (which publishes meter values from inside `process()`) and
3//! the GUI thread (which reads them at frame rate).
4//!
5//! The store lives outside the plugin instance, behind an `Arc`, for
6//! the same reason `params_arc` does: the audio thread holds
7//! `&mut P` for the full duration of a block, so a GUI closure that
8//! dereferenced the instance to call `get_meter` would violate
9//! `&mut` exclusivity. Reading a shared atomic slot has no such
10//! problem, and is what the LV2 wrapper always did - this type makes
11//! that pattern the only one.
12
13use std::sync::Arc;
14use std::sync::atomic::{AtomicU32, Ordering};
15
16use truce_params::METER_ID_BASE;
17
18/// Number of meter slots. Meters count upward from
19/// [`METER_ID_BASE`]; 256 per plugin is far above any real surface.
20const NUM_SLOTS: usize = 256;
21
22/// Fixed array of f32-bit atomic meter slots, indexed by meter id.
23///
24/// Writers (the shells' `meter_fn`, called from `process()`) and
25/// readers (editor `get_meter` closures) address slots by the meter's
26/// param-space id (`METER_ID_BASE + index`); ids outside the slot
27/// range read as `0.0` and write as a no-op, so a stale or
28/// out-of-range id can't panic on either thread.
29pub struct MeterStore {
30 slots: [AtomicU32; NUM_SLOTS],
31}
32
33impl MeterStore {
34 #[must_use]
35 pub fn new() -> Arc<Self> {
36 Arc::new(Self {
37 slots: std::array::from_fn(|_| AtomicU32::new(0)),
38 })
39 }
40
41 /// Read the meter value for `meter_id`. `0.0` for ids outside
42 /// the slot range.
43 #[must_use]
44 pub fn read(&self, meter_id: u32) -> f32 {
45 // `wrapping_sub` keeps ids below `METER_ID_BASE` from
46 // panicking - they wrap to a huge index and fall through to
47 // the `None` arm.
48 let idx = meter_id.wrapping_sub(METER_ID_BASE) as usize;
49 self.slots
50 .get(idx)
51 .map_or(0.0, |slot| f32::from_bits(slot.load(Ordering::Relaxed)))
52 }
53
54 /// Publish the meter value for `meter_id`. No-op for ids outside
55 /// the slot range.
56 pub fn write(&self, meter_id: u32, value: f32) {
57 let idx = meter_id.wrapping_sub(METER_ID_BASE) as usize;
58 if let Some(slot) = self.slots.get(idx) {
59 slot.store(value.to_bits(), Ordering::Relaxed);
60 }
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn round_trips_by_meter_id() {
70 let store = MeterStore::new();
71 store.write(METER_ID_BASE, 0.5);
72 store.write(METER_ID_BASE + 255, -1.0);
73 assert!((store.read(METER_ID_BASE) - 0.5).abs() < f32::EPSILON);
74 assert!((store.read(METER_ID_BASE + 255) + 1.0).abs() < f32::EPSILON);
75 }
76
77 #[test]
78 fn out_of_range_ids_are_inert() {
79 let store = MeterStore::new();
80 store.write(0, 1.0);
81 store.write(METER_ID_BASE + 256, 1.0);
82 assert!(store.read(0).abs() < f32::EPSILON);
83 assert!(store.read(METER_ID_BASE + 256).abs() < f32::EPSILON);
84 }
85}