//! Lock-free meter state shared from the audio thread to the editor. //! //! Two feeds, both written by `process()` (single producer) and read by the editor (single //! consumer), all wait-free: //! //! * **Bar meters** — decayed scalars per channel ([`Meters::level_l`] etc.), one store per block. //! * **Scrolling plot** — a [`ScopeRing`] of raw buckets clocked at [`BUCKET_HZ`] (independent of //! the GUI frame rate), so the plot's horizontal resolution isn't capped by the ~60 fps repaint. use nih_plug::prelude::AtomicF32; use std::sync::atomic::{AtomicU64, Ordering}; /// Metered channels: low, mid, high, then the 'All' aggregate — same order as the compressors. pub const NUM_CHANNELS: usize = 4; /// Rate the audio thread emits plot buckets at (Hz). Sets the plot's max horizontal resolution, /// decoupled from the editor frame rate. ~5 ms per bucket. pub const BUCKET_HZ: u32 = 200; /// Buckets buffered between GUI drains. At [`BUCKET_HZ`] this is ~2.5 s of slack — far more than /// the frame interval needs; if the GUI ever stalls longer, the oldest buckets are dropped. const RING_N: usize = 512; pub struct Meters { /// Left output level per channel as a **linear** peak. Peak-with-decay. pub level_l: [AtomicF32; NUM_CHANNELS], /// Right output level per channel (== left for mono signals). pub level_r: [AtomicF32; NUM_CHANNELS], /// Compressor gain reduction per channel in **dB (>= 0)**. Mono by design — detection is /// stereo-linked, so the same gain applies to both channels. pub gain_reduction_db: [AtomicF32; NUM_CHANNELS], /// Output limiter gain reduction in **dB (>= 0)** — feeds the ALL channel's ceiling lamp. pub limiter_gr_db: AtomicF32, /// Bucket stream feeding the scrolling in/out/GR plot. pub scope: ScopeRing, } impl Default for Meters { fn default() -> Self { Self { level_l: std::array::from_fn(|_| AtomicF32::new(0.0)), level_r: std::array::from_fn(|_| AtomicF32::new(0.0)), gain_reduction_db: std::array::from_fn(|_| AtomicF32::new(0.0)), limiter_gr_db: AtomicF32::new(0.0), scope: ScopeRing::default(), } } } impl Meters { /// Zero the bar meters. Called from the plugin's `reset()` (transport restart / sample-rate /// change) so the bars start from silence. The plot ring is left alone — it's continuous and /// reflects the new (silent) buckets as they arrive. Real-time safe. pub fn clear(&self) { for i in 0..NUM_CHANNELS { self.level_l[i].store(0.0, Ordering::Relaxed); self.level_r[i].store(0.0, Ordering::Relaxed); self.gain_reduction_db[i].store(0.0, Ordering::Relaxed); } self.limiter_gr_db.store(0.0, Ordering::Relaxed); } } /// Update a meter atomic with a new block value using peak-hold-with-decay: jump instantly to a /// louder value, ease back down by `decay_weight` (0..1, closer to 1 = slower fall). Keeps meters /// from flickering while staying responsive to transients. pub fn decay_store(meter: &AtomicF32, block_value: f32, decay_weight: f32) { let current = meter.load(Ordering::Relaxed); let next = if block_value > current { block_value } else { current * decay_weight + block_value * (1.0 - decay_weight) }; meter.store(next, Ordering::Relaxed); } /// Lock-free single-producer/single-consumer ring of plot buckets. Each bucket holds a per-channel /// (input level, output level, gain reduction) triple. The producer (audio thread) appends with /// [`push`](ScopeRing::push); the consumer (GUI) reads new buckets with [`drain`](ScopeRing::drain), /// tracking its own cursor. Per-field atomics avoid tearing; the consumer leaves one slot of margin /// from the slot being written, so it never races the producer. If the consumer falls more than the /// ring behind, the oldest buckets are silently dropped (a visual gap at worst). pub struct ScopeRing { /// `slot * NUM_CHANNELS + ch`, indexed by `bucket_index % RING_N`. in_lin: Vec, out_lin: Vec, gr_db: Vec, /// Per-bucket (not per-channel) flag: `1.0` if the output limiter hit the ceiling in this /// bucket, else `0.0`. Indexed by `bucket_index % RING_N`. hit: Vec, /// Monotonic count of buckets ever written. write: AtomicU64, } impl Default for ScopeRing { fn default() -> Self { let make = || (0..RING_N * NUM_CHANNELS).map(|_| AtomicF32::new(0.0)).collect(); Self { in_lin: make(), out_lin: make(), gr_db: make(), hit: (0..RING_N).map(|_| AtomicF32::new(0.0)).collect(), write: AtomicU64::new(0), } } } impl ScopeRing { /// Producer (audio thread): append one bucket of per-channel (in_lin, out_lin, gr_db). pub fn push( &self, in_lin: &[f32; NUM_CHANNELS], out_lin: &[f32; NUM_CHANNELS], gr_db: &[f32; NUM_CHANNELS], hit: f32, ) { let w = self.write.load(Ordering::Relaxed); // producer is the sole writer of `write` let slot = w as usize % RING_N; let base = slot * NUM_CHANNELS; for ch in 0..NUM_CHANNELS { self.in_lin[base + ch].store(in_lin[ch], Ordering::Relaxed); self.out_lin[base + ch].store(out_lin[ch], Ordering::Relaxed); self.gr_db[base + ch].store(gr_db[ch], Ordering::Relaxed); } self.hit[slot].store(hit, Ordering::Relaxed); // Publish the bucket: the Release pairs with the consumer's Acquire so the stores above are // visible before the new count. self.write.store(w + 1, Ordering::Release); } /// Consumer (GUI): call `on_bucket` for each bucket in `*cursor..write`, advancing `cursor`. /// Skips ahead (dropping oldest) if the consumer fell more than the ring behind. pub fn drain( &self, cursor: &mut u64, mut on_bucket: impl FnMut(&[f32; NUM_CHANNELS], &[f32; NUM_CHANNELS], &[f32; NUM_CHANNELS], f32), ) { let w = self.write.load(Ordering::Acquire); if *cursor > w { *cursor = w; // counter went backwards (shouldn't happen) — resync } // Stay one slot clear of the slot currently being written. let oldest = w.saturating_sub((RING_N - 1) as u64); if *cursor < oldest { *cursor = oldest; } let mut in_buf = [0.0f32; NUM_CHANNELS]; let mut out_buf = [0.0f32; NUM_CHANNELS]; let mut gr_buf = [0.0f32; NUM_CHANNELS]; while *cursor < w { let slot = *cursor as usize % RING_N; let base = slot * NUM_CHANNELS; for ch in 0..NUM_CHANNELS { in_buf[ch] = self.in_lin[base + ch].load(Ordering::Relaxed); out_buf[ch] = self.out_lin[base + ch].load(Ordering::Relaxed); gr_buf[ch] = self.gr_db[base + ch].load(Ordering::Relaxed); } let hit = self.hit[slot].load(Ordering::Relaxed); on_bucket(&in_buf, &out_buf, &gr_buf, hit); *cursor += 1; } } /// Current write high-water mark (for a fresh consumer to start from "now"). pub fn write_index(&self) -> u64 { self.write.load(Ordering::Acquire) } }