Skip to main content

firewheel_nodes/
peak_meter.rs

1#[cfg(not(feature = "std"))]
2use num_traits::Float;
3
4use bevy_platform::sync::atomic::Ordering;
5use firewheel_core::{
6    atomic_float::AtomicF32,
7    channel_config::{ChannelConfig, ChannelCount},
8    collector::ArcGc,
9    diff::{Diff, Patch},
10    dsp::volume::{amp_to_db, DbMeterNormalizer},
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: ArcGc<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: ArcGc::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    /// * `db_epsilon` - 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_DB_EPSILON].)
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, db_epsilon: 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 <= db_epsilon {
210                f32::NEG_INFINITY
211            } else {
212                db
213            }
214        })
215    }
216}
217
218impl<const NUM_CHANNELS: usize> AudioNode for PeakMeterNode<NUM_CHANNELS> {
219    type Configuration = EmptyConfig;
220
221    fn info(&self, _config: &Self::Configuration) -> AudioNodeInfo {
222        AudioNodeInfo::new()
223            .debug_name("peak_meter")
224            .channel_config(ChannelConfig {
225                num_inputs: ChannelCount::new(NUM_CHANNELS as u32).unwrap(),
226                num_outputs: ChannelCount::new(NUM_CHANNELS as u32).unwrap(),
227            })
228            .custom_state(PeakMeterState::<NUM_CHANNELS>::new())
229    }
230
231    fn construct_processor(
232        &self,
233        _config: &Self::Configuration,
234        cx: ConstructProcessorContext,
235    ) -> impl AudioNodeProcessor {
236        Processor {
237            params: self.clone(),
238            shared_state: ArcGc::clone(
239                &cx.custom_state::<PeakMeterState<NUM_CHANNELS>>()
240                    .unwrap()
241                    .shared_state,
242            ),
243        }
244    }
245}
246
247struct SharedState<const NUM_CHANNELS: usize> {
248    peak_gains: [AtomicF32; NUM_CHANNELS],
249}
250
251struct Processor<const NUM_CHANNELS: usize> {
252    params: PeakMeterNode<NUM_CHANNELS>,
253    shared_state: ArcGc<SharedState<NUM_CHANNELS>>,
254}
255
256impl<const NUM_CHANNELS: usize> AudioNodeProcessor for Processor<NUM_CHANNELS> {
257    fn process(
258        &mut self,
259        info: &ProcInfo,
260        buffers: ProcBuffers,
261        events: &mut ProcEvents,
262        _extra: &mut ProcExtra,
263    ) -> ProcessStatus {
264        let was_enabled = self.params.enabled;
265
266        for patch in events.drain_patches::<PeakMeterNode<NUM_CHANNELS>>() {
267            self.params.apply(patch);
268        }
269
270        if was_enabled && !self.params.enabled {
271            for ch in self.shared_state.peak_gains.iter() {
272                ch.store(0.0, Ordering::Relaxed);
273            }
274        }
275
276        if !self.params.enabled {
277            return ProcessStatus::Bypass;
278        }
279
280        for (i, (in_ch, peak_shared)) in buffers
281            .inputs
282            .iter()
283            .zip(self.shared_state.peak_gains.iter())
284            .enumerate()
285        {
286            if info.in_silence_mask.is_channel_silent(i) {
287                peak_shared.store(0.0, Ordering::Relaxed);
288            } else {
289                peak_shared.store(
290                    firewheel_core::dsp::algo::max_peak(in_ch),
291                    Ordering::Relaxed,
292                );
293            }
294        }
295
296        ProcessStatus::Bypass
297    }
298}