Skip to main content

firewheel_nodes/
peak_meter.rs

1#[cfg(not(feature = "std"))]
2use num_traits::Float;
3
4use bevy_platform::sync::{Arc, atomic::Ordering};
5use firewheel_core::node::NodeError;
6use firewheel_core::{
7    atomic_float::AtomicF32,
8    channel_config::{ChannelConfig, ChannelCount},
9    diff::{Diff, Patch},
10    dsp::volume::{DbMeterNormalizer, amp_to_db},
11    event::ProcEvents,
12    node::{
13        AudioNode, AudioNodeInfo, AudioNodeProcessor, ConstructProcessorContext, EmptyConfig,
14        ProcBuffers, ProcExtra, ProcInfo, ProcessStatus,
15    },
16};
17
18/// The configuration for a [`PeakMeterSmoother`]
19#[derive(Debug, Clone, Copy, PartialEq)]
20#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
21#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub struct PeakMeterSmootherConfig {
24    /// The rate of decay in seconds.
25    ///
26    /// By default this is set to `0.3` (300ms).
27    pub decay_rate: f32,
28    /// The rate at which this meter will refresh. This will typically
29    /// match the display's frame rate.
30    ///
31    /// By default this is set to `60.0`.
32    pub refresh_rate: f32,
33    /// The number of frames that the values in `has_clipped` will
34    /// hold their values before resetting to `false`.
35    ///
36    /// By default this is set to `60`.
37    pub clip_hold_frames: usize,
38}
39
40impl Default for PeakMeterSmootherConfig {
41    fn default() -> Self {
42        Self {
43            decay_rate: 0.3,
44            refresh_rate: 60.0,
45            clip_hold_frames: 60,
46        }
47    }
48}
49
50pub type PeakMeterMonoSmoother = PeakMeterSmoother<1>;
51pub type PeakMeterStereoSmoother = PeakMeterSmoother<2>;
52
53/// A helper struct to smooth out the output of [`PeakMeterNode`]. This
54/// can be used to drive the animation of a peak meter in a GUI.
55#[derive(Debug, Clone, Copy)]
56pub struct PeakMeterSmoother<const NUM_CHANNELS: usize = 2> {
57    /// The current smoothed peak value of each channel, in decibels.
58    smoothed_peaks: [f32; NUM_CHANNELS],
59    clipped_frames_left: [usize; NUM_CHANNELS],
60    level_decay: f32,
61    frame_interval: f32,
62    frame_counter: f32,
63    clip_hold_frames: usize,
64}
65
66impl<const NUM_CHANNELS: usize> PeakMeterSmoother<NUM_CHANNELS> {
67    pub fn new(config: PeakMeterSmootherConfig) -> Self {
68        assert!(config.decay_rate > 0.0);
69        assert!(config.refresh_rate > 0.0);
70        assert!(config.clip_hold_frames > 0);
71
72        Self {
73            smoothed_peaks: [-100.0; NUM_CHANNELS],
74            clipped_frames_left: [0; NUM_CHANNELS],
75            level_decay: 1.0 - (-1.0 / (config.refresh_rate * config.decay_rate)).exp(),
76            frame_interval: config.refresh_rate.recip(),
77            frame_counter: 0.0,
78            clip_hold_frames: config.clip_hold_frames,
79        }
80    }
81
82    pub fn reset(&mut self) {
83        self.smoothed_peaks = [-100.0; NUM_CHANNELS];
84        self.clipped_frames_left = [0; NUM_CHANNELS];
85    }
86
87    pub fn update(&mut self, peaks_db: [f32; NUM_CHANNELS], delta_seconds: f32) {
88        for ((smoothed_peak, &in_peak), clipped_frames_left) in self
89            .smoothed_peaks
90            .iter_mut()
91            .zip(peaks_db.iter())
92            .zip(self.clipped_frames_left.iter_mut())
93        {
94            if in_peak >= *smoothed_peak {
95                *smoothed_peak = in_peak;
96
97                if in_peak > 0.0 {
98                    *clipped_frames_left = self.clip_hold_frames;
99                }
100            }
101        }
102
103        self.frame_counter += delta_seconds;
104
105        // Correct for cumulative errors.
106        if (self.frame_counter - self.frame_interval).abs() < 0.0001 {
107            self.frame_counter = self.frame_interval;
108        }
109
110        while self.frame_counter >= self.frame_interval {
111            self.frame_counter -= self.frame_interval;
112
113            // Correct for cumulative errors.
114            if (self.frame_counter - self.frame_interval).abs() < 0.0001 {
115                self.frame_counter = self.frame_interval;
116            }
117
118            for ((smoothed_peak, &in_peak), clipped_frames_left) in self
119                .smoothed_peaks
120                .iter_mut()
121                .zip(peaks_db.iter())
122                .zip(self.clipped_frames_left.iter_mut())
123            {
124                if in_peak + 0.001 < *smoothed_peak {
125                    *smoothed_peak += ((in_peak - *smoothed_peak) * self.level_decay).max(-100.0);
126                }
127
128                if *smoothed_peak > 0.0 {
129                    *clipped_frames_left = self.clip_hold_frames;
130                } else if *clipped_frames_left > 0 {
131                    *clipped_frames_left -= 1;
132                }
133            }
134        }
135    }
136
137    pub fn has_clipped(&self) -> [bool; NUM_CHANNELS] {
138        core::array::from_fn(|i| self.clipped_frames_left[i] > 0)
139    }
140
141    pub fn smoothed_peaks_db(&self) -> &[f32; NUM_CHANNELS] {
142        &self.smoothed_peaks
143    }
144
145    pub fn smoothed_peak_db_mono(&self) -> f32 {
146        let mut max_value: f32 = -100.0;
147        for ch in self.smoothed_peaks {
148            max_value = max_value.max(ch);
149        }
150
151        max_value
152    }
153
154    /// Get the peak values as a normalized value in the range `[0.0, 1.0]`.
155    pub fn smoothed_peaks_normalized(&self, normalizer: &DbMeterNormalizer) -> [f32; NUM_CHANNELS] {
156        core::array::from_fn(|i| normalizer.normalize(self.smoothed_peaks[i]))
157    }
158
159    pub fn smoothed_peaks_normalized_mono(&self, normalizer: &DbMeterNormalizer) -> f32 {
160        normalizer.normalize(self.smoothed_peak_db_mono())
161    }
162}
163
164pub type PeakMeterMonoNode = PeakMeterNode<1>;
165pub type PeakMeterStereoNode = PeakMeterNode<2>;
166
167/// A node that calculates the peak amplitude of a signal, and then sends that value
168/// to [`PeakMeterState`].
169#[derive(Diff, Patch, Debug, Clone, Copy, PartialEq, Eq)]
170#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
171#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
172#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
173pub struct PeakMeterNode<const NUM_CHANNELS: usize = 2> {
174    pub enabled: bool,
175}
176
177pub type PeakMeterMonoState = PeakMeterState<1>;
178pub type PeakMeterStereoState = PeakMeterState<2>;
179
180/// The state of a [`PeakMeterNode`]. This contains the calculated peak values.
181#[derive(Clone)]
182pub struct PeakMeterState<const NUM_CHANNELS: usize = 2> {
183    shared_state: Arc<SharedState<NUM_CHANNELS>>,
184}
185
186impl<const NUM_CHANNELS: usize> PeakMeterState<NUM_CHANNELS> {
187    fn new() -> Self {
188        assert_ne!(NUM_CHANNELS, 0);
189        assert!(NUM_CHANNELS <= 64);
190
191        Self {
192            shared_state: Arc::new(SharedState {
193                peak_gains: core::array::from_fn(|_| AtomicF32::new(0.0)),
194            }),
195        }
196    }
197
198    /// Get the latest peak values for each channel in decibels.
199    ///
200    /// * `min_db` - If a peak value is less than or equal to this value, then it
201    ///   will be clamped to `f32::NEG_INFINITY` (silence). (You can use
202    ///   [firewheel_core::dsp::volume::DEFAULT_MIN_DB].)
203    ///
204    /// If the node is currently disabled, then this will return a value
205    /// of `f32::NEG_INFINITY` (silence) for all channels.
206    pub fn peak_gain_db(&self, min_db: f32) -> [f32; NUM_CHANNELS] {
207        core::array::from_fn(|i| {
208            let db = amp_to_db(self.shared_state.peak_gains[i].load(Ordering::Relaxed));
209            if db <= min_db { f32::NEG_INFINITY } else { db }
210        })
211    }
212}
213
214impl<const NUM_CHANNELS: usize> AudioNode for PeakMeterNode<NUM_CHANNELS> {
215    type Configuration = EmptyConfig;
216
217    fn info(&self, _config: &Self::Configuration) -> Result<AudioNodeInfo, NodeError> {
218        Ok(AudioNodeInfo::new()
219            .debug_name("peak_meter")
220            .channel_config(ChannelConfig {
221                num_inputs: ChannelCount::new(NUM_CHANNELS as u32).unwrap(),
222                num_outputs: ChannelCount::new(NUM_CHANNELS as u32).unwrap(),
223            })
224            .custom_state(PeakMeterState::<NUM_CHANNELS>::new()))
225    }
226
227    fn construct_processor(
228        &self,
229        _config: &Self::Configuration,
230        cx: ConstructProcessorContext,
231    ) -> Result<impl AudioNodeProcessor, NodeError> {
232        Ok(Processor {
233            params: *self,
234            shared_state: Arc::clone(
235                &cx.custom_state::<PeakMeterState<NUM_CHANNELS>>()
236                    .unwrap()
237                    .shared_state,
238            ),
239        })
240    }
241}
242
243struct SharedState<const NUM_CHANNELS: usize> {
244    peak_gains: [AtomicF32; NUM_CHANNELS],
245}
246
247struct Processor<const NUM_CHANNELS: usize> {
248    params: PeakMeterNode<NUM_CHANNELS>,
249    shared_state: Arc<SharedState<NUM_CHANNELS>>,
250}
251
252impl<const NUM_CHANNELS: usize> Processor<NUM_CHANNELS> {
253    fn reset(&mut self) {
254        for ch in self.shared_state.peak_gains.iter() {
255            ch.store(0.0, Ordering::Relaxed);
256        }
257    }
258}
259
260impl<const NUM_CHANNELS: usize> AudioNodeProcessor for Processor<NUM_CHANNELS> {
261    fn events(&mut self, _info: &ProcInfo, events: &mut ProcEvents, _extra: &mut ProcExtra) {
262        let was_enabled = self.params.enabled;
263
264        for patch in events.drain_patches::<PeakMeterNode<NUM_CHANNELS>>() {
265            self.params.apply(patch);
266        }
267
268        if was_enabled && !self.params.enabled {
269            self.reset();
270        }
271    }
272
273    fn bypassed(&mut self, _bypassed: bool) {
274        self.reset();
275    }
276
277    fn process(
278        &mut self,
279        info: &ProcInfo,
280        buffers: ProcBuffers,
281        _extra: &mut ProcExtra,
282    ) -> ProcessStatus {
283        if !self.params.enabled {
284            return ProcessStatus::Bypass;
285        }
286
287        for (i, (in_ch, peak_shared)) in buffers
288            .inputs
289            .iter()
290            .zip(self.shared_state.peak_gains.iter())
291            .enumerate()
292        {
293            if info.in_silence_mask.is_channel_silent(i) {
294                peak_shared.store(0.0, Ordering::Relaxed);
295            } else {
296                peak_shared.store(
297                    firewheel_core::dsp::algo::max_peak(in_ch),
298                    Ordering::Relaxed,
299                );
300            }
301        }
302
303        ProcessStatus::Bypass
304    }
305}