meadow-dsp-essentials 0.1.4

Liberally-licensed essential audio DSP library used in the Meadowlark DAW project
Documentation
#[cfg(not(feature = "std"))]
use num_traits::Float;

/// A struct that converts a value in decibels to a normalized range used in
/// meters.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DbMeterNormalizer {
    min_db: f32,
    range_recip: f32,
    factor: f32,
}

impl DbMeterNormalizer {
    /// * `min_db` - The minimum decibel value shown in the meter.
    /// * `max_db` - The maximum decibel value shown in the meter.
    /// * `center_db` - The decibel value that will appear halfway (0.5) in the
    /// normalized range. For example, if you had `min_db` as `-100.0` and
    /// `max_db` as `0.0`, then a good `center_db` value would be `-22`.
    pub fn new(min_db: f32, max_db: f32, center_db: f32) -> Self {
        assert!(max_db > min_db);
        assert!(center_db > min_db && center_db < max_db);

        let range_recip = (max_db - min_db).recip();
        let center_normalized = ((center_db - min_db) * range_recip).clamp(0.0, 1.0);

        Self {
            min_db,
            range_recip,
            factor: 0.5_f32.log(center_normalized),
        }
    }

    #[inline]
    pub fn normalize(&self, db: f32) -> f32 {
        ((db - self.min_db) * self.range_recip)
            .clamp(0.0, 1.0)
            .powf(self.factor)
    }
}

impl Default for DbMeterNormalizer {
    fn default() -> Self {
        Self::new(-100.0, 0.0, -22.0)
    }
}

/// The configuration for a [`PeakMeterSmoother`]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PeakMeterSmootherConfig {
    /// The rate of decay in seconds.
    ///
    /// By default this is set to `0.3` (300ms).
    pub decay_rate: f32,
    /// The rate at which this meter will refresh. This will typically
    /// match the display's frame rate.
    ///
    /// By default this is set to `60.0`.
    pub refresh_rate: f32,
    /// The number of frames that the values in `has_clipped` will
    /// hold their values before resetting to `false`.
    ///
    /// By default this is set to `60`.
    pub clip_hold_frames: usize,
}

impl Default for PeakMeterSmootherConfig {
    fn default() -> Self {
        Self {
            decay_rate: 0.3,
            refresh_rate: 60.0,
            clip_hold_frames: 60,
        }
    }
}

/// A helper struct to smooth out the output of peak/decibel values so
/// that it can be used to drive the animation of a peak/decibel meter
/// in a GUI.
#[derive(Debug, Clone, Copy)]
pub struct PeakMeterSmoother<const NUM_CHANNELS: usize> {
    /// The current smoothed peak value of each channel, in decibels.
    smoothed_peaks: [f32; NUM_CHANNELS],
    clipped_frames_left: [usize; NUM_CHANNELS],
    level_decay: f32,
    frame_interval: f32,
    frame_counter: f32,
    clip_hold_frames: usize,
}

impl<const NUM_CHANNELS: usize> PeakMeterSmoother<NUM_CHANNELS> {
    pub fn new(config: PeakMeterSmootherConfig) -> Self {
        assert!(config.decay_rate > 0.0);
        assert!(config.refresh_rate > 0.0);
        assert!(config.clip_hold_frames > 0);

        Self {
            smoothed_peaks: [-100.0; NUM_CHANNELS],
            clipped_frames_left: [0; NUM_CHANNELS],
            level_decay: 1.0 - (-1.0 / (config.refresh_rate * config.decay_rate)).exp(),
            frame_interval: config.refresh_rate.recip(),
            frame_counter: 0.0,
            clip_hold_frames: config.clip_hold_frames,
        }
    }

    pub fn reset(&mut self) {
        self.smoothed_peaks = [-100.0; NUM_CHANNELS];
        self.clipped_frames_left = [0; NUM_CHANNELS];
    }

    pub fn update(&mut self, peaks_db: [f32; NUM_CHANNELS], delta_seconds: f32) {
        for ((smoothed_peak, &in_peak), clipped_frames_left) in self
            .smoothed_peaks
            .iter_mut()
            .zip(peaks_db.iter())
            .zip(self.clipped_frames_left.iter_mut())
        {
            if in_peak >= *smoothed_peak {
                *smoothed_peak = in_peak;

                if in_peak > 0.0 {
                    *clipped_frames_left = self.clip_hold_frames;
                }
            }
        }

        self.frame_counter += delta_seconds;

        // Correct for cumulative errors.
        if (self.frame_counter - self.frame_interval).abs() < 0.0001 {
            self.frame_counter = self.frame_interval;
        }

        while self.frame_counter >= self.frame_interval {
            self.frame_counter -= self.frame_interval;

            // Correct for cumulative errors.
            if (self.frame_counter - self.frame_interval).abs() < 0.0001 {
                self.frame_counter = self.frame_interval;
            }

            for ((smoothed_peak, &in_peak), clipped_frames_left) in self
                .smoothed_peaks
                .iter_mut()
                .zip(peaks_db.iter())
                .zip(self.clipped_frames_left.iter_mut())
            {
                if in_peak + 0.001 < *smoothed_peak {
                    *smoothed_peak += ((in_peak - *smoothed_peak) * self.level_decay).max(-100.0);
                }

                if *smoothed_peak > 0.0 {
                    *clipped_frames_left = self.clip_hold_frames;
                } else if *clipped_frames_left > 0 {
                    *clipped_frames_left -= 1;
                }
            }
        }
    }

    pub fn has_clipped(&self) -> [bool; NUM_CHANNELS] {
        core::array::from_fn(|i| self.clipped_frames_left[i] > 0)
    }

    pub fn smoothed_peaks_db(&self) -> &[f32; NUM_CHANNELS] {
        &self.smoothed_peaks
    }

    pub fn smoothed_peak_db_mono(&self) -> f32 {
        let mut max_value: f32 = -100.0;
        for ch in self.smoothed_peaks {
            max_value = max_value.max(ch);
        }

        max_value
    }

    /// Get the peak values as a normalized value in the range `[0.0, 1.0]`.
    pub fn smoothed_peaks_normalized(&self, normalizer: &DbMeterNormalizer) -> [f32; NUM_CHANNELS] {
        core::array::from_fn(|i| normalizer.normalize(self.smoothed_peaks[i]))
    }

    pub fn smoothed_peaks_normalized_mono(&self, normalizer: &DbMeterNormalizer) -> f32 {
        normalizer.normalize(self.smoothed_peak_db_mono())
    }
}