diff --git a/src/math.rs b/src/math.rs index 5a1fa2794..a4804838f 100644 --- a/src/math.rs +++ b/src/math.rs @@ -154,6 +154,21 @@ pub(crate) fn duration_from_secs(secs: Float) -> Duration { } } +/// Fast approximation of `exp(x)` using Horner's Method for Polynomial Evaluation. +/// This function approximates the exponential function by evaluating the +/// third-order Taylor polynomial using Horner's scheme, which reduces the +/// number of multiplications and improves numerical stability. +/// +/// This approximation is valid for small values of `x` (near zero) and is +/// used in the AGC algorithm to efficiently compute the release coefficient. +/// It provides a good balance between speed and accuracy, resulting in +/// faster benchmark times compared to the standard `exp` function. +#[inline] +pub(crate) fn fast_exp(x: Float) -> Float { + // Horner's method: 1 + x*(1 + x*(0.5 + x/6)) + 1.0 + x * (1.0 + x * (0.5 + x / 6.0)) +} + /// Utility macro for getting a `NonZero` from a literal. Especially /// useful for passing in `ChannelCount` and `Samplerate`. /// Equivalent to: `const { core::num::NonZero::new($n).unwrap() }` diff --git a/src/source/agc.rs b/src/source/agc.rs deleted file mode 100644 index 0d10a517b..000000000 --- a/src/source/agc.rs +++ /dev/null @@ -1,605 +0,0 @@ -// -// Automatic Gain Control (AGC) Algorithm -// Designed by @UnknownSuperficialNight -// -// Features: -// • Adaptive peak detection -// • RMS-based level estimation -// • Asymmetric attack/release -// • RMS-based general adjustments with peak limiting -// -// Optimized for smooth and responsive gain control -// -// Crafted with love. Enjoy! :) -// - -use std::time::Duration; - -use super::{SeekError, SpanTracker}; -use crate::{math::duration_to_coefficient, ChannelCount, Float, Sample, SampleRate, Source}; - -#[cfg(feature = "tracing")] -use tracing; - -#[cfg(feature = "experimental")] -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; - -#[cfg(all(feature = "experimental", not(feature = "64bit")))] -use atomic_float::AtomicF32; -#[cfg(all(feature = "experimental", feature = "64bit"))] -use atomic_float::AtomicF64; - -#[cfg(all(feature = "experimental", not(feature = "64bit")))] -type AtomicFloat = AtomicF32; -#[cfg(all(feature = "experimental", feature = "64bit"))] -type AtomicFloat = AtomicF64; - -/// Ensures `RMS_WINDOW_SIZE` is a power of two -const fn power_of_two(n: usize) -> usize { - assert!( - n.is_power_of_two(), - "RMS_WINDOW_SIZE must be a power of two" - ); - n -} - -/// Size of the circular buffer used for RMS calculation. -/// A larger size provides more stable RMS values but increases latency. -const RMS_WINDOW_SIZE: usize = power_of_two(8192); - -/// Settings for the Automatic Gain Control (AGC). -/// -/// This struct contains parameters that define how the AGC will function, -/// allowing users to customise its behaviour. -#[derive(Debug, Clone)] -pub struct AutomaticGainControlSettings { - /// The desired output level that the AGC tries to maintain. - /// A value of 1.0 means no change to the original level. - pub target_level: Float, - /// Time constant for gain increases (how quickly the AGC responds to level increases). - /// Longer durations result in slower, more gradual gain increases. - pub attack_time: Duration, - /// Time constant for gain decreases (how quickly the AGC responds to level decreases). - /// Shorter durations allow for faster response to sudden loud signals. - pub release_time: Duration, - /// Maximum allowable gain multiplication to prevent excessive amplification. - /// This acts as a safety limit to avoid distortion from over-amplification. - pub absolute_max_gain: Float, -} - -impl Default for AutomaticGainControlSettings { - fn default() -> Self { - AutomaticGainControlSettings { - target_level: 1.0, // Default to original level - attack_time: Duration::from_secs(4), // Recommended attack time - release_time: Duration::from_secs(0), // Recommended release time - absolute_max_gain: 7.0, // Recommended max gain - } - } -} - -#[cfg(feature = "experimental")] -/// Automatic Gain Control filter for maintaining consistent output levels. -/// -/// This struct implements an AGC algorithm that dynamically adjusts audio levels -/// based on both **peak** and **RMS** (Root Mean Square) measurements. -#[derive(Clone, Debug)] -pub struct AutomaticGainControl { - input: I, - target_level: Arc, - floor: Float, - absolute_max_gain: Arc, - attack_time: Duration, - release_time: Duration, - current_gain: Float, - attack_coeff: Arc, - release_coeff: Arc, - peak_level: Float, - rms_window: CircularBuffer, - is_enabled: Arc, - span: SpanTracker, -} - -#[cfg(not(feature = "experimental"))] -/// Automatic Gain Control filter for maintaining consistent output levels. -/// -/// This struct implements an AGC algorithm that dynamically adjusts audio levels -/// based on both **peak** and **RMS** (Root Mean Square) measurements. -#[derive(Clone, Debug)] -pub struct AutomaticGainControl { - input: I, - target_level: Float, - floor: Float, - absolute_max_gain: Float, - attack_time: Duration, - release_time: Duration, - current_gain: Float, - attack_coeff: Float, - release_coeff: Float, - peak_level: Float, - rms_window: CircularBuffer, - is_enabled: bool, - span: SpanTracker, -} - -/// A circular buffer for efficient RMS calculation over a sliding window. -/// -/// This structure allows for constant-time updates and mean calculations, -/// which is crucial for real-time audio processing. -#[derive(Clone, Debug)] -struct CircularBuffer { - buffer: Box<[Float; RMS_WINDOW_SIZE]>, - sum: Float, - index: usize, -} - -impl CircularBuffer { - /// Creates a new `CircularBuffer` with a fixed size determined at compile time. - #[inline] - fn new() -> Self { - CircularBuffer { - buffer: Box::new([0.0; RMS_WINDOW_SIZE]), - sum: 0.0, - index: 0, - } - } - - /// Pushes a new value into the buffer and returns the old value. - /// - /// This method maintains a running sum for efficient mean calculation. - #[inline] - fn push(&mut self, value: Float) -> Float { - let old_value = self.buffer[self.index]; - // Update the sum by first subtracting the old value and then adding the new value; this is more accurate. - self.sum = self.sum - old_value + value; - self.buffer[self.index] = value; - // Use bitwise AND for efficient index wrapping since RMS_WINDOW_SIZE is a power of two. - self.index = (self.index + 1) & (RMS_WINDOW_SIZE - 1); - old_value - } - - /// Calculates the mean of all values in the buffer. - /// - /// This operation is `O(1)` due to the maintained running sum. - #[inline] - fn mean(&self) -> Float { - self.sum / RMS_WINDOW_SIZE as Float - } -} - -/// Constructs an `AutomaticGainControl` object with specified parameters. -/// -/// # Arguments -/// -/// * `input` - The input audio source -/// * `target_level` - The desired output level -/// * `attack_time` - Time constant for gain increase -/// * `release_time` - Time constant for gain decrease -/// * `absolute_max_gain` - Maximum allowable gain -#[inline] -pub(crate) fn automatic_gain_control( - input: I, - target_level: Float, - attack_time: Duration, - release_time: Duration, - absolute_max_gain: Float, -) -> AutomaticGainControl -where - I: Source, -{ - let sample_rate = input.sample_rate(); - let attack_coeff = duration_to_coefficient(attack_time, sample_rate); - let release_coeff = duration_to_coefficient(release_time, sample_rate); - - #[cfg(feature = "experimental")] - { - let channels = input.channels(); - AutomaticGainControl { - input, - target_level: Arc::new(AtomicFloat::new(target_level)), - floor: 0.0, - absolute_max_gain: Arc::new(AtomicFloat::new(absolute_max_gain)), - attack_time, - release_time, - current_gain: 1.0, - attack_coeff: Arc::new(AtomicFloat::new(attack_coeff)), - release_coeff: Arc::new(AtomicFloat::new(release_coeff)), - peak_level: 0.0, - rms_window: CircularBuffer::new(), - is_enabled: Arc::new(AtomicBool::new(true)), - span: SpanTracker::new(sample_rate, channels), - } - } - - #[cfg(not(feature = "experimental"))] - { - let channels = input.channels(); - AutomaticGainControl { - input, - target_level, - floor: 0.0, - absolute_max_gain, - attack_time, - release_time, - current_gain: 1.0, - attack_coeff, - release_coeff, - peak_level: 0.0, - rms_window: CircularBuffer::new(), - is_enabled: true, - span: SpanTracker::new(sample_rate, channels), - } - } -} - -impl AutomaticGainControl -where - I: Source, -{ - #[inline] - fn target_level(&self) -> Float { - #[cfg(feature = "experimental")] - { - self.target_level.load(Ordering::Relaxed) - } - #[cfg(not(feature = "experimental"))] - { - self.target_level - } - } - - #[inline] - fn absolute_max_gain(&self) -> Float { - #[cfg(feature = "experimental")] - { - self.absolute_max_gain.load(Ordering::Relaxed) - } - #[cfg(not(feature = "experimental"))] - { - self.absolute_max_gain - } - } - - #[inline] - fn attack_coeff(&self) -> Float { - #[cfg(feature = "experimental")] - { - self.attack_coeff.load(Ordering::Relaxed) - } - #[cfg(not(feature = "experimental"))] - { - self.attack_coeff - } - } - - #[inline] - fn release_coeff(&self) -> Float { - #[cfg(feature = "experimental")] - { - self.release_coeff.load(Ordering::Relaxed) - } - #[cfg(not(feature = "experimental"))] - { - self.release_coeff - } - } - - #[inline] - fn is_enabled(&self) -> bool { - #[cfg(feature = "experimental")] - { - self.is_enabled.load(Ordering::Relaxed) - } - #[cfg(not(feature = "experimental"))] - { - self.is_enabled - } - } - - #[cfg(feature = "experimental")] - /// Access the target output level for real-time adjustment. - /// - /// Use this to dynamically modify the AGC's target level while audio is processing. - /// Adjust this value to control the overall output amplitude of the processed signal. - #[inline] - pub fn get_target_level(&self) -> Arc { - Arc::clone(&self.target_level) - } - - #[cfg(feature = "experimental")] - /// Access the maximum gain limit for real-time adjustment. - /// - /// Use this to dynamically modify the AGC's maximum allowable gain during runtime. - /// Adjusting this value helps prevent excessive amplification in low-level signals. - #[inline] - pub fn get_absolute_max_gain(&self) -> Arc { - Arc::clone(&self.absolute_max_gain) - } - - #[cfg(feature = "experimental")] - /// Access the attack coefficient for real-time adjustment. - /// - /// Use this to dynamically modify how quickly the AGC responds to level increases. - /// Smaller values result in faster response, larger values in slower response. - /// Adjust during runtime to fine-tune AGC behavior for different audio content. - /// - /// Note: if the sample rate or channel count changes, any value set through this handle will - /// be overwritten with the attack time that this AGC was constructed with. - #[inline] - pub fn get_attack_coeff(&self) -> Arc { - Arc::clone(&self.attack_coeff) - } - - #[cfg(feature = "experimental")] - /// Access the release coefficient for real-time adjustment. - /// - /// Use this to dynamically modify how quickly the AGC responds to level decreases. - /// Smaller values result in faster response, larger values in slower response. - /// Adjust during runtime to optimize AGC behavior for varying audio dynamics. - /// - /// Note: if the sample rate or channel count changes, any value set through this handle will - /// be overwritten with the release time that this AGC was constructed with. - #[inline] - pub fn get_release_coeff(&self) -> Arc { - Arc::clone(&self.release_coeff) - } - - #[cfg(feature = "experimental")] - /// Access the AGC on/off control. - /// Use this to dynamically enable or disable AGC processing during runtime. - /// - /// AGC is on by default. `false` is disabled state, `true` is enabled. - /// In disabled state the sound is passed through AGC unchanged. - /// - /// In particular, this control is useful for comparing processed and unprocessed audio. - #[inline] - pub fn get_agc_control(&self) -> Arc { - Arc::clone(&self.is_enabled) - } - - /// Enable or disable AGC processing. - /// - /// Use this to enable or disable AGC processing. - /// Useful for comparing processed and unprocessed audio or for disabling/enabling AGC. - #[inline] - pub fn set_enabled(&mut self, enabled: bool) { - #[cfg(feature = "experimental")] - { - self.is_enabled.store(enabled, Ordering::Relaxed); - } - #[cfg(not(feature = "experimental"))] - { - self.is_enabled = enabled; - } - } - - /// Set the floor value for the AGC - /// - /// This method sets the floor value for the AGC. The floor value is the minimum - /// gain that the AGC will allow. The gain will not drop below this value. - /// - /// Passing `None` will disable the floor value (setting it to 0.0), allowing the - /// AGC gain to drop to very low levels. - #[inline] - pub fn set_floor(&mut self, floor: Option) { - self.floor = floor.unwrap_or(0.0); - } - - /// Updates the peak level using instant attack and slow release behaviour - /// - /// This method uses instant response (0.0 coefficient) when the signal is increasing - /// and the release coefficient when the signal is decreasing, providing - /// appropriate tracking behaviour for peak detection. - #[inline] - fn update_peak_level(&mut self, sample_value: Sample, release_coeff: Float) { - let coeff = if sample_value > self.peak_level { - // Fast attack for rising peaks - 0.0 - } else { - // Slow release for falling peaks - release_coeff - }; - - self.peak_level = self.peak_level * coeff + sample_value * (1.0 - coeff); - } - - /// Updates the RMS (Root Mean Square) level using a circular buffer approach. - /// This method calculates a moving average of the squared input samples, - /// providing a measure of the signal's average power over time. - #[inline] - fn update_rms(&mut self, sample_value: Sample) -> Float { - let squared_sample = sample_value * sample_value; - self.rms_window.push(squared_sample); - self.rms_window.mean().sqrt() - } - - /// Calculate gain adjustments based on peak levels - /// This method determines the appropriate gain level to apply to the audio - /// signal, considering the peak level. - /// The peak level helps prevent sudden spikes in the output signal. - #[inline] - fn calculate_peak_gain(&self, target_level: Float, absolute_max_gain: Float) -> Float { - if self.peak_level > 0.0 { - (target_level / self.peak_level).min(absolute_max_gain) - } else { - absolute_max_gain - } - } - - #[inline] - fn process_sample(&mut self, sample: I::Item) -> I::Item { - // Cache atomic loads at the start - avoids repeated atomic operations - let target_level = self.target_level(); - let absolute_max_gain = self.absolute_max_gain(); - let attack_coeff = self.attack_coeff(); - let release_coeff = self.release_coeff(); - - // Convert the sample to its absolute float value for level calculations - let sample_value = sample.abs(); - - // Dynamically adjust peak level using cached release coefficient - self.update_peak_level(sample_value, release_coeff); - - // Calculate the current RMS (Root Mean Square) level using a sliding window approach - let rms = self.update_rms(sample_value); - - // Compute the gain adjustment required to reach the target level based on RMS - let rms_gain = if rms > 0.0 { - target_level / rms - } else { - absolute_max_gain // Default to max gain if RMS is zero - }; - - // Calculate the peak limiting gain - let peak_gain = self.calculate_peak_gain(target_level, absolute_max_gain); - - // Use RMS for general adjustments, but limit by peak gain to prevent clipping and apply a minimum floor value - let desired_gain = rms_gain.min(peak_gain).max(self.floor); - - // Adaptive attack/release speed for AGC (Automatic Gain Control) - // - // This mechanism implements an asymmetric approach to gain adjustment: - // 1. **Slow increase**: Prevents abrupt amplification of noise during quiet periods. - // 2. **Fast decrease**: Rapidly attenuates sudden loud signals to avoid distortion. - // - // The asymmetry is crucial because: - // - Gradual gain increases sound more natural and less noticeable to listeners. - // - Quick gain reductions are necessary to prevent clipping and maintain audio quality. - // - // This approach addresses several challenges associated with high attack times: - // 1. **Slow response**: With a high attack time, the AGC responds very slowly to changes in input level. - // This means it takes longer for the gain to adjust to new signal levels. - // 2. **Initial gain calculation**: When the audio starts or after a period of silence, the initial gain - // calculation might result in a very high gain value, especially if the input signal starts quietly. - // 3. **Overshooting**: As the gain slowly increases (due to the high attack time), it might overshoot - // the desired level, causing the signal to become too loud. - // 4. **Overcorrection**: The AGC then tries to correct this by reducing the gain, but due to the slow response, - // it might reduce the gain too much, causing the sound to drop to near-zero levels. - // 5. **Slow recovery**: Again, due to the high attack time, it takes a while for the gain to increase - // back to the appropriate level. - // - // By using a faster release time for decreasing gain, we can mitigate these issues and provide - // more responsive control over sudden level increases while maintaining smooth gain increases. - let attack_speed = if desired_gain > self.current_gain { - attack_coeff - } else { - release_coeff - }; - - // Gradually adjust the current gain towards the desired gain for smooth transitions - self.current_gain = self.current_gain * attack_speed + desired_gain * (1.0 - attack_speed); - - // Ensure the calculated gain stays within the defined operational range - self.current_gain = self.current_gain.clamp(0.1, absolute_max_gain); - - // Output current gain value for developers to fine tune their inputs to automatic_gain_control - #[cfg(feature = "tracing")] - tracing::debug!("AGC gain: {}", self.current_gain,); - - // Apply the computed gain to the input sample and return the result - sample * self.current_gain - } - - /// Returns an immutable reference to the inner source. - pub fn inner(&self) -> &I { - &self.input - } - - /// Returns a mutable reference to the inner source. - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } -} - -impl Iterator for AutomaticGainControl -where - I: Source, -{ - type Item = I::Item; - - #[inline] - fn next(&mut self) -> Option { - let detection = self.span.advance(&self.input); - - if detection.at_span_boundary && detection.parameters_changed { - let current_sample_rate = self.input.sample_rate(); - // Recalculate coefficients for new sample rate - #[cfg(feature = "experimental")] - { - let attack_coeff = duration_to_coefficient(self.attack_time, current_sample_rate); - let release_coeff = duration_to_coefficient(self.release_time, current_sample_rate); - self.attack_coeff.store(attack_coeff, Ordering::Relaxed); - self.release_coeff.store(release_coeff, Ordering::Relaxed); - } - #[cfg(not(feature = "experimental"))] - { - self.attack_coeff = duration_to_coefficient(self.attack_time, current_sample_rate); - self.release_coeff = - duration_to_coefficient(self.release_time, current_sample_rate); - } - - // Reset RMS window to avoid mixing samples from different parameter sets - self.rms_window = CircularBuffer::new(); - self.peak_level = 0.0; - self.current_gain = 1.0; - } - - let sample = self.input.next()?; - - let output = if self.is_enabled() { - self.process_sample(sample) - } else { - sample - }; - Some(output) - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - self.input.size_hint() - } -} - -impl ExactSizeIterator for AutomaticGainControl where I: Source + ExactSizeIterator {} - -impl Source for AutomaticGainControl -where - I: Source, -{ - #[inline] - fn current_span_len(&self) -> Option { - self.input.current_span_len() - } - - #[inline] - fn channels(&self) -> ChannelCount { - self.input.channels() - } - - #[inline] - fn sample_rate(&self) -> SampleRate { - self.input.sample_rate() - } - - #[inline] - fn total_duration(&self) -> Option { - self.input.total_duration() - } - - #[inline] - fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - self.input.try_seek(pos)?; - self.span.seek(pos, &self.input); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::math::nz; - use crate::source::test_utils::TestSource; -} diff --git a/src/source/automatic_gain_control/agc.rs b/src/source/automatic_gain_control/agc.rs new file mode 100644 index 000000000..2afec0202 --- /dev/null +++ b/src/source/automatic_gain_control/agc.rs @@ -0,0 +1,560 @@ +// +// Automatic Gain Control (AGC) Algorithm +// Designed by @UnknownSuperficialNight +// +// Features: +// • Adaptive peak detection with exponential smoothing (EMA) +// • O(1) RMS level estimation via circular buffer +// • Combined RMS and peak limiting with adaptive slowdown +// • Asymmetric attack/release with per-sample clamping +// • Configurable floor value for minimum gain threshold +// • Atomic operations support (experimental) +// • Fast release coefficient via 3rd‑order Taylor approximation (evaluated with Horner's method) +// • Power-of-two window sizing for efficiency +// • Presets for music and speech +// +// Optimised for smooth and responsive gain control +// +// Crafted with love. Enjoy! :) +// + +use super::{div_or_fallback, CircularBufferRMS, SeekError, SlowDownState, SpanTracker}; +use crate::math::{duration_to_coefficient, duration_to_float}; +use crate::{ChannelCount, Float, Sample, SampleRate, Source}; +use std::time::Duration; + +#[cfg(feature = "tracing")] +use tracing; + +#[cfg(feature = "experimental")] +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +#[cfg(all(feature = "experimental", not(feature = "64bit")))] +use atomic_float::AtomicF32; +#[cfg(all(feature = "experimental", feature = "64bit"))] +use atomic_float::AtomicF64; + +#[cfg(all(feature = "experimental", not(feature = "64bit")))] +type AtomicFloat = AtomicF32; +#[cfg(all(feature = "experimental", feature = "64bit"))] +type AtomicFloat = AtomicF64; + +/// Size of the target circular buffer used for RMS calculation, in milliseconds. +/// A larger size provides more stable RMS values but increases latency. +const RMS_WINDOW_MS: Duration = Duration::from_millis(20); + +#[cfg(feature = "experimental")] +/// Automatic Gain Control filter for maintaining consistent output levels. +/// +/// This struct implements an AGC algorithm that dynamically adjusts audio levels +/// based on both **peak** and **RMS** (Root Mean Square) measurements. +#[derive(Clone, Debug)] +pub struct AutomaticGainControl { + input: I, + + // Core gain values + target_level: Arc, + floor: Float, + absolute_max_gain: Arc, + peak_tracking_window: Duration, + current_gain: Float, + + // Timing parameters + attack_duration: Arc, + release_duration: Arc, + + // Signal analysis state + peak_level: Float, + release_coefficient: Float, + rms_window: CircularBufferRMS, + + // Control flags + is_enabled: Arc, + span: SpanTracker, + + // Slowdown tracking + slow_down_state: SlowDownState, +} + +#[cfg(not(feature = "experimental"))] +/// Automatic Gain Control filter for maintaining consistent output levels. +/// +/// This struct implements an AGC algorithm that dynamically adjusts audio levels +/// based on both **peak** and **RMS** (Root Mean Square) measurements. +#[derive(Clone, Debug)] +pub struct AutomaticGainControl { + input: I, + + // Core gain values + target_level: Float, + floor: Float, + absolute_max_gain: Float, + peak_tracking_window: Duration, + current_gain: Float, + + // Timing parameters + attack_duration: Float, + release_duration: Float, + + // Signal analysis state + peak_level: Float, + release_coefficient: Float, + rms_window: CircularBufferRMS, + + // Control flags + is_enabled: bool, + span: SpanTracker, + + // Slowdown tracking + slow_down_state: SlowDownState, +} + +impl AutomaticGainControl +where + I: Source, +{ + /// Constructs an `AutomaticGainControl` object with specified parameters. + /// + /// # Arguments + /// + /// `input` - The input audio source + /// `target_level` - The desired output level + /// `attack_time` - Time constant for gain increase + /// `release_time` - Time constant for gain decrease + /// `absolute_max_gain` - Maximum allowable gain + /// `peak_tracking_window` - Duration over which to track peak level + /// `floor` - The minimum output level (gain floor) that the AGC will not go below + #[inline] + pub(crate) fn new( + input: I, + target_level: Float, + attack_time: Duration, + release_time: Duration, + absolute_max_gain: Float, + peak_tracking_window: Duration, + floor: Float, + ) -> AutomaticGainControl + where + I: Source, + { + let sample_rate = input.sample_rate(); + let attack_duration = duration_to_float(attack_time); + let release_duration = duration_to_float(release_time); + + let release_coefficient = duration_to_coefficient(peak_tracking_window, sample_rate); + + #[cfg(feature = "experimental")] + { + let channels = input.channels(); + AutomaticGainControl { + input, + target_level: Arc::new(AtomicFloat::new(target_level)), + floor, + absolute_max_gain: Arc::new(AtomicFloat::new(absolute_max_gain)), + peak_tracking_window, + current_gain: 1.0, + attack_duration: Arc::new(AtomicFloat::new(attack_duration)), + release_duration: Arc::new(AtomicFloat::new(release_duration)), + peak_level: 0.7, + release_coefficient, + rms_window: CircularBufferRMS::new(sample_rate, RMS_WINDOW_MS), + is_enabled: Arc::new(AtomicBool::new(true)), + span: SpanTracker::new(sample_rate, channels), + slow_down_state: SlowDownState::new(sample_rate), + } + } + + #[cfg(not(feature = "experimental"))] + { + let channels = input.channels(); + AutomaticGainControl { + input, + target_level, + floor, + absolute_max_gain, + peak_tracking_window, + current_gain: 1.0, + attack_duration, + release_duration, + peak_level: 0.7, + release_coefficient, + rms_window: CircularBufferRMS::new(sample_rate, RMS_WINDOW_MS), + is_enabled: true, + span: SpanTracker::new(sample_rate, channels), + slow_down_state: SlowDownState::new(sample_rate), + } + } + } + + #[inline] + fn target_level(&self) -> Float { + #[cfg(feature = "experimental")] + { + self.target_level.load(Ordering::Relaxed) + } + #[cfg(not(feature = "experimental"))] + { + self.target_level + } + } + + #[inline] + fn absolute_max_gain(&self) -> Float { + #[cfg(feature = "experimental")] + { + self.absolute_max_gain.load(Ordering::Relaxed) + } + #[cfg(not(feature = "experimental"))] + { + self.absolute_max_gain + } + } + + #[inline] + fn attack_duration(&self) -> Float { + #[cfg(feature = "experimental")] + { + self.attack_duration.load(Ordering::Relaxed) + } + #[cfg(not(feature = "experimental"))] + { + self.attack_duration + } + } + + #[inline] + fn release_duration(&self) -> Float { + #[cfg(feature = "experimental")] + { + self.release_duration.load(Ordering::Relaxed) + } + #[cfg(not(feature = "experimental"))] + { + self.release_duration + } + } + + #[inline] + fn is_enabled(&self) -> bool { + #[cfg(feature = "experimental")] + { + self.is_enabled.load(Ordering::Relaxed) + } + #[cfg(not(feature = "experimental"))] + { + self.is_enabled + } + } + + #[cfg(feature = "experimental")] + /// Access the target output level for real-time adjustment. + /// + /// Use this to dynamically modify the AGC's target level while audio is processing. + /// Adjust this value to control the overall output amplitude of the processed signal. + #[inline] + pub fn get_target_level(&self) -> Arc { + Arc::clone(&self.target_level) + } + + #[cfg(feature = "experimental")] + /// Access the maximum gain limit for real-time adjustment. + /// + /// Use this to dynamically modify the AGC's maximum allowable gain during runtime. + /// Adjusting this value helps prevent excessive amplification in low-level signals. + #[inline] + pub fn get_absolute_max_gain(&self) -> Arc { + Arc::clone(&self.absolute_max_gain) + } + + #[cfg(feature = "experimental")] + /// Access the attack coefficient for real-time adjustment. + /// + /// Use this to dynamically modify how quickly the AGC responds to level increases. + /// Smaller values result in faster response, larger values in slower response. + /// Adjust during runtime to fine-tune AGC behavior for different audio content. + /// + /// Note: if the sample rate or channel count changes, any value set through this handle will + /// be overwritten with the attack time that this AGC was constructed with. + #[inline] + pub fn get_attack_duration(&self) -> Arc { + Arc::clone(&self.attack_duration) + } + + #[cfg(feature = "experimental")] + /// Access the release coefficient for real-time adjustment. + /// + /// Use this to dynamically modify how quickly the AGC responds to level decreases. + /// Smaller values result in faster response, larger values in slower response. + /// Adjust during runtime to optimize AGC behavior for varying audio dynamics. + /// + /// Note: if the sample rate or channel count changes, any value set through this handle will + /// be overwritten with the release time that this AGC was constructed with. + #[inline] + pub fn get_release_duration(&self) -> Arc { + Arc::clone(&self.release_duration) + } + + #[cfg(feature = "experimental")] + /// Access the AGC on/off control. + /// Use this to dynamically enable or disable AGC processing during runtime. + /// + /// AGC is on by default. `false` is disabled state, `true` is enabled. + /// In disabled state the sound is passed through AGC unchanged. + /// + /// In particular, this control is useful for comparing processed and unprocessed audio. + #[inline] + pub fn get_agc_control(&self) -> Arc { + Arc::clone(&self.is_enabled) + } + + /// Enable or disable AGC processing. + /// + /// Use this to enable or disable AGC processing. + /// Useful for comparing processed and unprocessed audio or for disabling/enabling AGC. + #[inline] + pub fn set_enabled(&mut self, enabled: bool) { + #[cfg(feature = "experimental")] + { + self.is_enabled.store(enabled, Ordering::Relaxed); + } + #[cfg(not(feature = "experimental"))] + { + self.is_enabled = enabled; + } + } + + /// Set the floor value for the AGC + /// + /// This method sets the floor value for the AGC. The floor value is the minimum + /// gain that the AGC will allow. The gain will not drop below this value. + /// + /// Passing `None` will disable the floor value (setting it to 0.0), allowing the + /// AGC gain to drop to very low levels. + #[inline] + pub fn set_floor(&mut self, floor: Option) { + self.floor = floor.unwrap_or(0.0); + } + + /// Updates the peak level using exponential smoothing (EMA) to blend the current + /// value toward the previous level using the release coefficient, then taking + /// the maximum of the current sample. + /// + /// This provides a stable peak measurement that doesn't react to every sample, + /// preventing excessive gain adjustments when the signal is momentarily loud. + /// The peak serves as an absolute maximum safeguard to prevent output clipping + /// even when RMS-based gain calculations suggest aggressive amplification. + #[inline] + fn update_peak_level(&mut self, sample_value: Float, release_coefficient: Float) { + // Compute the exponentially smoothed estimate of the previous peak level. + // The EMA smooths peak tracking over time, preventing sudden jumps when + // loud transients occur, which would otherwise cause extreme gain reductions. + let peak_release = + self.peak_level * release_coefficient + sample_value * (1.0 - release_coefficient); + + // Take maximum to ensure the peak is always an upper bound. + // This guarantees that peak_level never decreases below the current sample, + // preserving the safety mechanism against clipping. + self.peak_level = sample_value.max(peak_release); + } + + /// Updates the RMS (Root Mean Square) level using a circular buffer approach. + /// This method calculates a moving average of the squared input samples, + /// providing a measure of the signal's average power over time. + #[inline] + fn update_rms(&mut self, sample_value: Sample) -> Float { + self.rms_window.push(sample_value); + + // Calculate RMS safely + let rms = self.rms_window.rms(); + if rms.is_nan() || rms <= 0.0 { + 0.0 // Default to 0 if RMS is invalid + } else { + rms + } + } + + #[inline] + fn process_sample(&mut self, sample: I::Item) -> I::Item { + // Cache atomic loads at the start - avoids repeated atomic operations + let target_level = self.target_level(); + let absolute_max_gain = self.absolute_max_gain(); + let attack_time_in_seconds = self.attack_duration(); + let release_duration = self.release_duration(); + let sample_rate = self.sample_rate().get() as Float; // Sample rate in Hz + + // Convert the sample to its absolute float value for level calculations + // We use abs() to work with signal magnitude regardless of polarity + // This is crucial because RMS and peak detection care about energy, + // not whether the signal is positive or negative + let sample_value = sample.abs(); + + // Increment the sample counter + self.slow_down_state.increment_sample_counter(); + + // Dynamically adjust peak level using cached release coefficient + self.update_peak_level(sample_value, self.release_coefficient); + + // Calculate the current RMS (Root Mean Square) level using a sliding window approach + let rms = self.update_rms(sample_value); + + // Compute the gain adjustment required to reach the adjusted target level + // When rms is 0.0 (silence), we fall back to current_gain as the default + // This keeps the gain stable during silence without any hard reset + // The gain will only change gradually when peaks occur or signal returns + let rms_gain = div_or_fallback(target_level, rms, self.current_gain); + + // Calculate gain adjustments based on peak levels + // We divide target_level by peak_level to find the gain multiplier needed + // to scale the signal's peaks to match the target. If peak_level is high + // (loud signal), this gives us a gain < 1.0 (attenuation). If peak_level + // is low (quiet signal), this gives us a gain > 1.0 (amplification). + // The peak level acts as a safety mechanism to prevent output spikes + // that could exceed the target level. + let peak_gain = div_or_fallback(target_level, self.peak_level, 1.0).min(absolute_max_gain); + + // Combine RMS and peak gains by taking the minimum. We use min() because + // we need to choose a single gain value that respects both constraints. + // Think of it like this: RMS gain might suggest "amplify by 5x" based on + // average signal level, but peak gain might suggest "attenuate by 0.5x" + // to prevent output spikes. Since these goals conflict (amplify vs reduce), + // we pick the more conservative one: min() selects 0.5x (attenuation) over + // 5x (amplification). This ensures we don't blindly amplify and risk + // output spikes, even when the average signal seems quiet. + // Then we apply the floor to ensure we never drop below the minimum allowed gain. + let desired_gain = rms_gain.min(peak_gain).max(self.floor); + + if self.slow_down_state.sample_counter == 0 { + self.slow_down_state.compute_slowdown_factor( + desired_gain, + self.current_gain, + rms, + self.peak_level, + ); + } + + let dynamic_attack_time = attack_time_in_seconds * self.slow_down_state.slowdown_factor; + + // Calculate max gain change per sample based on dynamic attack/release times + let max_attack_gain_change_per_sample = 1.0 / (dynamic_attack_time * sample_rate); + let max_release_gain_change_per_sample = 1.0 / (release_duration * sample_rate); + + // Determine gain difference + let gain_diff = desired_gain - self.current_gain; + + // Clamp gain change based on attack or release phase + let gain_change = if gain_diff > 0.0 { + // Attack phase: Clamp the gain change to the maximum allowed per sample + gain_diff.clamp(0.0, max_attack_gain_change_per_sample) + } else { + // Release phase: Clamp the gain change to the maximum allowed per sample + gain_diff.clamp(-max_release_gain_change_per_sample, 0.0) + }; + + // Update current gain + self.current_gain += gain_change; + + #[cfg(feature = "tracing")] + if self.slow_down_state.sample_counter == 0 { + tracing::debug!( + "RMS: {:.4}, Peak: {:.4}, Desired Gain: {:.4}, Current Gain: {:.4}, Release Coefficient: {}, Attack Time: {:.4}", + rms, self.peak_level, desired_gain, self.current_gain, self.release_coefficient, dynamic_attack_time, + ); + } + + // Apply gain to sample and return + sample * self.current_gain + } + + /// Returns an immutable reference to the inner source. + pub fn inner(&self) -> &I { + &self.input + } + + /// Returns a mutable reference to the inner source. + pub fn inner_mut(&mut self) -> &mut I { + &mut self.input + } +} + +impl Iterator for AutomaticGainControl +where + I: Source, +{ + type Item = I::Item; + + #[inline] + fn next(&mut self) -> Option { + let detection = self.span.advance(&self.input); + + if detection.at_span_boundary && detection.parameters_changed { + let current_sample_rate = self.input.sample_rate(); + + // Recalculate coefficients for new sample rate + self.release_coefficient = + duration_to_coefficient(self.peak_tracking_window, current_sample_rate); + + // Reset RMS window to avoid mixing samples from different parameter sets + self.rms_window = CircularBufferRMS::new(current_sample_rate, RMS_WINDOW_MS); + self.peak_level = 0.7; + self.current_gain = 1.0; + } + + let sample = self.input.next()?; + + let output = if self.is_enabled() { + self.process_sample(sample) + } else { + sample + }; + Some(output) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.input.size_hint() + } +} + +impl ExactSizeIterator for AutomaticGainControl where I: Source + ExactSizeIterator {} + +impl Source for AutomaticGainControl +where + I: Source, +{ + #[inline] + fn current_span_len(&self) -> Option { + self.input.current_span_len() + } + + #[inline] + fn channels(&self) -> ChannelCount { + self.input.channels() + } + + #[inline] + fn sample_rate(&self) -> SampleRate { + self.input.sample_rate() + } + + #[inline] + fn total_duration(&self) -> Option { + self.input.total_duration() + } + + #[inline] + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + self.input.try_seek(pos)?; + self.span.seek(pos, &self.input); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::math::nz; + use crate::source::test_utils::TestSource; +} diff --git a/src/source/automatic_gain_control/config.rs b/src/source/automatic_gain_control/config.rs new file mode 100644 index 000000000..69773e6f3 --- /dev/null +++ b/src/source/automatic_gain_control/config.rs @@ -0,0 +1,68 @@ +use crate::Float; +use std::time::Duration; + +/// Settings for the Automatic Gain Control (AGC). +/// +/// This struct contains parameters that define how the AGC will function, +/// allowing users to customise its behaviour. +#[derive(Debug, Clone)] +pub struct AutomaticGainControlSettings { + /// The desired output level that the AGC tries to maintain. + /// A value of 1.0 means no change to the original level. + pub target_level: Float, + /// Time constant for gain increases (how quickly the AGC responds to level increases). + /// Longer durations result in slower, more gradual gain increases. + pub attack_time: Duration, + /// Time constant for gain decreases (how quickly the AGC responds to level decreases). + /// Shorter durations allow for faster response to sudden loud signals. + pub release_time: Duration, + /// Maximum allowable gain multiplication to prevent excessive amplification. + /// This acts as a safety limit to avoid distortion from over-amplification. + pub absolute_max_gain: Float, + /// Duration of the peak tracking smoothing window. + /// Controls how much peak level measurements are smoothed before being used for gain calculation. + /// Larger values provide more stable peak detection but add latency to peak tracking. + /// Smaller values respond faster to sudden peaks but may allow more transient clipping. + pub peak_tracking_window: Duration, + /// The minimum output level (gain floor) that the AGC will not go below. + /// A value of 1.0 preserves loud passages at source level without additional amplification (amplification only). + /// A value of 0.0 allows unlimited amplification (pure AGC behaviour). + pub floor: Float, +} + +impl AutomaticGainControlSettings { + /// Returns a preset optimised for music content. + /// + /// Values tuned through empirical testing and are intended as good defaults for general music processing. + pub fn music_preset() -> Self { + AutomaticGainControlSettings { + target_level: 1.0, + attack_time: Duration::from_millis(500), + release_time: Duration::from_micros(500), + absolute_max_gain: 7.0, + peak_tracking_window: Duration::from_millis(10), + floor: 1.0, + } + } + + /// Returns a preset optimised for speech content. + /// + /// Values tuned through empirical testing and are intended as good defaults for general speech processing. + pub fn speech_preset() -> Self { + AutomaticGainControlSettings { + target_level: 1.0, + attack_time: Duration::from_millis(250), + release_time: Duration::from_micros(50), + absolute_max_gain: 7.0, + peak_tracking_window: Duration::from_millis(10), + floor: 0.0, + } + } +} + +impl Default for AutomaticGainControlSettings { + // Music preset is the default + fn default() -> Self { + Self::music_preset() + } +} diff --git a/src/source/automatic_gain_control/helpers.rs b/src/source/automatic_gain_control/helpers.rs new file mode 100644 index 000000000..df52a0a8c --- /dev/null +++ b/src/source/automatic_gain_control/helpers.rs @@ -0,0 +1,12 @@ +use crate::Float; + +/// Divide `a` by `b` unless `b` is NaN, infinite, or <= 0, +/// in which case `fallback` is returned. +#[inline(always)] +pub(super) fn div_or_fallback(a: Float, b: Float, fallback: Float) -> Float { + if b.is_finite() && b > 0.0 { + a / b + } else { + fallback + } +} diff --git a/src/source/automatic_gain_control/mod.rs b/src/source/automatic_gain_control/mod.rs new file mode 100644 index 000000000..6eb3dd7a6 --- /dev/null +++ b/src/source/automatic_gain_control/mod.rs @@ -0,0 +1,14 @@ +use super::{SeekError, SpanTracker}; + +mod agc; +mod config; +mod helpers; +mod rms; +mod slowdown; + +pub use agc::AutomaticGainControl; +pub use config::AutomaticGainControlSettings; + +use helpers::div_or_fallback; +use rms::CircularBufferRMS; +use slowdown::SlowDownState; diff --git a/src/source/automatic_gain_control/rms.rs b/src/source/automatic_gain_control/rms.rs new file mode 100644 index 000000000..af35a8618 --- /dev/null +++ b/src/source/automatic_gain_control/rms.rs @@ -0,0 +1,74 @@ +use std::{num::NonZeroU32, time::Duration}; + +use crate::Float; + +/// A circular buffer optimised for RMS calculation over a sliding window. +/// +/// Maintains a running sum of squares with O(1) updates and retrieval, +/// avoiding the need to scan stored samples for mean calculations. +#[derive(Clone, Debug)] +pub(super) struct CircularBufferRMS { + buffer: Box<[Float]>, // Runtime-sized window so RMS spans the same time range at any sample rate + sum_of_squares: Float, // Keeps a running square-sum so RMS can be updated without re-scanning the entire buffer + index: usize, // Marks the current slot; each new sample overwrites the oldest one as we advance + mask: usize, // Lets the index wrap with `&` instead of `%`, which is faster because the size is a power of two + reciprocal_len: Float, // Stores `1 / len` so RMS normalizes with multiplication instead of division +} + +impl CircularBufferRMS { + /// Calculates the buffer size from the sample rate and target window length. + /// + /// The window is expressed in milliseconds, converted to samples, then rounded + /// up to the next power of two for efficient index wrapping using bitwise arithmetic. + #[inline] + fn calculate_rms_buffer_size(sample_rate: NonZeroU32, window_ms: Duration) -> usize { + // Convert the time window into the number of samples for this sample rate + // Example: 44,100 × 20 ms / 1,000 -> 882 samples + let samples = (sample_rate.get() as usize * window_ms.as_millis() as usize).div_ceil(1000); + + // Ensure minimum 1 sample, then round up to nearest power of two + // Result: 882 samples -> 1024-sample buffer + // Which is: 1024 samples at 44,100 Hz ≈ 23.2 ms + samples.max(1).next_power_of_two() + } + + /// Creates a new `CircularBufferRMS` from a sample rate and target window size in milliseconds. + /// + /// The buffer size is computed from the requested duration and rounded up to a power of two + /// so wrapping can use bitwise arithmetic instead of modulo. + #[inline] + pub(super) fn new(sample_rate: NonZeroU32, window_ms: Duration) -> Self { + // Calculate the buffer size from the sample_rate and target window + let size = Self::calculate_rms_buffer_size(sample_rate, window_ms); + + CircularBufferRMS { + buffer: vec![0.0; size].into_boxed_slice(), // [T; N] requires const N; Vec allows runtime size + sum_of_squares: 0.0, + index: 0, + mask: size - 1, + reciprocal_len: 1.0 / size as Float, + } + } + + /// Adds a sample to the buffer and updates the running sum of squares. + /// + /// Maintains an incremental sum of squares for O(1) RMS computation + /// without recalculating from stored samples. + #[inline] + pub(super) fn push(&mut self, value: Float) { + let old_value = self.buffer[self.index]; + // Update the sum of squares by subtracting the square of the old value and adding the square of the new value. + self.sum_of_squares = (self.sum_of_squares - (old_value * old_value)) + (value * value); + self.buffer[self.index] = value; + // Use bitwise for efficient index wrapping since the buffer size is a power of two. + self.index = (self.index + 1) & self.mask; + } + + /// Calculate the RMS (Root Mean Square) value of all values in the buffer. + /// + /// RMS provides a measure of the signal's effective or average magnitude. + #[inline] + pub(super) fn rms(&self) -> Float { + (self.sum_of_squares * self.reciprocal_len).sqrt() + } +} diff --git a/src/source/automatic_gain_control/slowdown.rs b/src/source/automatic_gain_control/slowdown.rs new file mode 100644 index 000000000..dabecfaa9 --- /dev/null +++ b/src/source/automatic_gain_control/slowdown.rs @@ -0,0 +1,72 @@ +use super::div_or_fallback; +use crate::math::fast_exp; +use crate::{Float, SampleRate}; + +/// State for adaptive slowdown of gain changes. +/// +/// This struct holds the state for managing the slowdown of gain changes based on signal conditions. +/// The `slowdown_factor` determines how quickly or slowly the gain can change: +/// - When the signal is quiet and we're close to target, changes are allowed normally +/// - When the signal peaks significantly, changes are slowed down exponentially +/// - This prevents abrupt loudness jumps during automatic gain control adjustments. +#[derive(Clone, Debug)] +pub(super) struct SlowDownState { + block_size: usize, + pub(super) sample_counter: usize, + pub(super) slowdown_factor: Float, +} + +impl SlowDownState { + #[inline] + pub(super) fn new(sample_rate: SampleRate) -> Self { + // Calculate and cache block size based on sample rate + let block_size = (sample_rate.get() as usize / 1000) * 2; // 2ms blocks + + Self { + block_size, + sample_counter: 0, + slowdown_factor: 0.0, + } + } + + #[inline] + pub(super) fn increment_sample_counter(&mut self) { + self.sample_counter = (self.sample_counter + 1) % self.block_size; + } + + /// Computes the slowdown factor for adaptive gain changes. + /// + /// The slowdown factor determines how quickly or slowly the gain can change based on the current signal conditions. + /// - When the desired gain is close to the current gain, the slowdown factor increases, preventing abrupt loudness jumps during automatic gain control adjustments. + /// - When the signal deviates significantly from the target, the slowdown factor remains high to maintain stability. + #[inline] + pub(super) fn compute_slowdown_factor( + &mut self, + desired_gain: Float, + current_gain: Float, + rms: Float, + peak_level: Float, + ) { + // Calculate the absolute difference between the desired gain and the current gain + let distance_from_target = (desired_gain - current_gain).abs(); + + // Calculate the maximum distance as the sum of RMS and peak level + let max_distance = rms + peak_level; + + // Normalise distance clamped between [0,1] with a fallback of 1.0 + let normalise_distance = div_or_fallback(distance_from_target, max_distance, 1.0).min(1.0); + + // Compute the exponential slowdown factor based on the normalised distance + // The multiplier is scaled by the square root of the sum of peak level and RMS + let exp_multiplier = 10.0 * (peak_level + rms).sqrt(); + let exp_slowdown = fast_exp(1.0 + exp_multiplier * (1.0 - normalise_distance)); + + // Create a mask that is 1.0 if the distance is within the max_distance, otherwise 0.0 + // This mask is used to blend the exponential slowdown factor with a linear factor + let mask = ((max_distance - distance_from_target).max(0.0) / max_distance).min(1.0); + + // Blend the slowdown factor: when mask=1 use exp_slowdown, else 1.0 + // This ensures that the slowdown factor increases when the signal deviates from the target + self.slowdown_factor = 1.0 + mask * (exp_slowdown - 1.0); + } +} diff --git a/src/source/mod.rs b/src/source/mod.rs index 63d5233e9..e6b9f604b 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -11,8 +11,8 @@ use crate::{ use dasp_sample::FromSample; -pub use self::agc::{AutomaticGainControl, AutomaticGainControlSettings}; pub use self::amplify::Amplify; +pub use self::automatic_gain_control::{AutomaticGainControl, AutomaticGainControlSettings}; pub use self::blt::BltFilter; pub use self::buffered::Buffered; pub use self::channel_volume::ChannelVolume; @@ -48,8 +48,8 @@ pub use self::triangle::TriangleWave; pub use self::uniform::UniformSourceIterator; pub use self::zero::{Zero, ZeroError}; -mod agc; mod amplify; +mod automatic_gain_control; mod blt; mod buffered; mod channel_volume; @@ -352,74 +352,6 @@ pub trait Source: Iterator { /// /// Automatic Gain Control (AGC) adjusts the amplitude of the audio signal /// to maintain a consistent output level. - /// - /// # Parameters - /// - /// `target_level`: - /// **TL;DR**: Desired output level. 1.0 = original level, > 1.0 amplifies, < 1.0 reduces. - /// - /// The desired output level, where 1.0 represents the original sound level. - /// Values above 1.0 will amplify the sound, while values below 1.0 will lower it. - /// For example, a target_level of 1.4 means that at normal sound levels, the AGC - /// will aim to increase the gain by a factor of 1.4, resulting in a minimum 40% amplification. - /// A recommended level is `1.0`, which maintains the original sound level. - /// - /// `attack_time`: - /// **TL;DR**: Response time for volume increases. Shorter = faster but may cause abrupt changes. **Recommended: `4.0` seconds**. - /// - /// The time (in seconds) for the AGC to respond to input level increases. - /// Shorter times mean faster response but may cause abrupt changes. Longer times result - /// in smoother transitions but slower reactions to sudden volume changes. Too short can - /// lead to overreaction to peaks, causing unnecessary adjustments. Too long can make the - /// AGC miss important volume changes or react too slowly to sudden loud passages. Very - /// high values might result in excessively loud output or sluggish response, as the AGC's - /// adjustment speed is limited by the attack time. Balance is key for optimal performance. - /// A recommended attack_time of `4.0` seconds provides a sweet spot for most applications. - /// - /// `release_time`: - /// **TL;DR**: Response time for volume decreases. Shorter = faster gain reduction. **Recommended: `0.0` seconds**. - /// - /// The time (in seconds) for the AGC to respond to input level decreases. - /// This parameter controls how quickly the gain is reduced when the signal level drops. - /// Shorter release times result in faster gain reduction, which can be useful for quick - /// adaptation to quieter passages but may lead to pumping effects. Longer release times - /// provide smoother transitions but may be slower to respond to sudden decreases in volume. - /// However, if the release_time is too high, the AGC may not be able to lower the gain - /// quickly enough, potentially leading to clipping and distorted sound before it can adjust. - /// Finding the right balance is crucial for maintaining natural-sounding dynamics and - /// preventing distortion. A recommended release_time of `0.0` seconds works well for - /// general use, allowing the AGC to decrease the gain immediately with no delay, ensuring there is no clipping. - /// - /// `absolute_max_gain`: - /// **TL;DR**: Maximum allowed gain. Prevents over-amplification. **Recommended: `5.0`**. - /// - /// The maximum gain that can be applied to the signal. - /// This parameter acts as a safeguard against excessive amplification of quiet signals - /// or background noise. It establishes an upper boundary for the AGC's signal boost, - /// effectively preventing distortion or overamplification of low-level sounds. - /// This is crucial for maintaining audio quality and preventing unexpected volume spikes. - /// A recommended value for `absolute_max_gain` is `5`, which provides a good balance between - /// amplification capability and protection against distortion in most scenarios. - /// - /// `automatic_gain_control` example in this project shows a pattern you can use - /// to enable/disable the AGC filter dynamically. - /// - /// # Example (Quick start) - /// - /// ```rust - /// // Apply Automatic Gain Control to the source (AGC is on by default) - /// use rodio::source::{Source, SineWave, AutomaticGainControlSettings}; - /// use rodio::Player; - /// use std::time::Duration; - /// let source = SineWave::new(444.0); // An example. - /// let (player, output) = Player::new(); // An example. - /// - /// let agc_source = source.automatic_gain_control(AutomaticGainControlSettings::default()); - /// - /// // Add the AGC-controlled source to the sink - /// player.append(agc_source); - /// - /// ``` #[inline] fn automatic_gain_control( self, @@ -432,12 +364,14 @@ pub trait Source: Iterator { let attack_time_limited = agc_settings.attack_time.min(Duration::from_secs(10)); let release_time_limited = agc_settings.release_time.min(Duration::from_secs(10)); - agc::automatic_gain_control( + AutomaticGainControl::new( self, agc_settings.target_level, attack_time_limited, release_time_limited, agc_settings.absolute_max_gain, + agc_settings.peak_tracking_window, + agc_settings.floor, ) }