Skip to main content

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}