//! Lock-free meter state shared from the audio thread to the editor. //! //! `process()` is the single writer (one store per value per block — decimated, not per sample); //! the editor is the single reader (once per frame). All access is wait-free via atomics, so the //! realtime thread never blocks. Values are plain scalars (no streaming history yet) — enough for //! the per-channel level + gain-reduction bars and the ceiling lamp. use nih_plug::prelude::AtomicF32; use std::sync::atomic::Ordering; /// Metered channels: low, mid, high, then the 'All' aggregate — same order as the compressors. pub const NUM_CHANNELS: usize = 4; 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). Stored separately so the planned /// `|L|GR|R|` layout is a pure editor change; the current bars render `max(L, R)`. 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)** — drives the ceiling lamp. pub limiter_gr_db: AtomicF32, // --- Scrolling plot feed: instantaneous block peaks, NOT decayed. The editor samples these // each frame into its own history ring. Per channel: input level (entering the compressor), // output level, and gain reduction. /// Mono input level per channel (linear peak, post pre-gain, pre-compressor). pub plot_in: [AtomicF32; NUM_CHANNELS], /// Mono output level per channel (linear peak, post-compressor). pub plot_out: [AtomicF32; NUM_CHANNELS], /// Gain reduction per channel in dB (>= 0). pub plot_gr: [AtomicF32; NUM_CHANNELS], } 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), plot_in: std::array::from_fn(|_| AtomicF32::new(0.0)), plot_out: std::array::from_fn(|_| AtomicF32::new(0.0)), plot_gr: std::array::from_fn(|_| AtomicF32::new(0.0)), } } } impl Meters { /// Zero every meter. Called from the plugin's `reset()` (transport restart / sample-rate /// change) so the display starts from silence rather than stale values. 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.plot_in[i].store(0.0, Ordering::Relaxed); self.plot_out[i].store(0.0, Ordering::Relaxed); self.plot_gr[i].store(0.0, Ordering::Relaxed); } self.limiter_gr_db.store(0.0, Ordering::Relaxed); } } /// Store an instantaneous value (no smoothing) — used for the scrolling-plot feed, which the /// editor smooths/decimates on its own. pub fn store_instant(meter: &AtomicF32, value: f32) { meter.store(value, 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); }