diff --git a/benches/effects.rs b/benches/effects.rs index ce67ff9c..10afb7fd 100644 --- a/benches/effects.rs +++ b/benches/effects.rs @@ -1,6 +1,7 @@ use std::time::Duration; use divan::Bencher; +use rodio::source::AutomaticGainControlSettings; use rodio::Source; mod shared; @@ -47,12 +48,7 @@ fn amplify(bencher: Bencher) { fn agc_enabled(bencher: Bencher) { bencher.with_inputs(music_wav).bench_values(|source| { source - .automatic_gain_control( - 1.0, // target_level - 4.0, // attack_time (in seconds) - 0.005, // release_time (in seconds) - 5.0, // absolute_max_gain - ) + .automatic_gain_control(AutomaticGainControlSettings::default()) .for_each(divan::black_box_drop) }) } @@ -62,12 +58,8 @@ fn agc_enabled(bencher: Bencher) { fn agc_disabled(bencher: Bencher) { bencher.with_inputs(music_wav).bench_values(|source| { // Create the AGC source - let amplified_source = source.automatic_gain_control( - 1.0, // target_level - 4.0, // attack_time (in seconds) - 0.005, // release_time (in seconds) - 5.0, // absolute_max_gain - ); + let amplified_source = + source.automatic_gain_control(AutomaticGainControlSettings::default()); // Get the control handle and disable AGC let agc_control = amplified_source.get_agc_control(); diff --git a/benches/pipeline.rs b/benches/pipeline.rs index 3164ca40..bf789f90 100644 --- a/benches/pipeline.rs +++ b/benches/pipeline.rs @@ -2,6 +2,7 @@ use std::num::NonZero; use std::time::Duration; use divan::Bencher; +use rodio::source::AutomaticGainControlSettings; use rodio::ChannelCount; use rodio::{source::UniformSourceIterator, Source}; @@ -19,12 +20,7 @@ fn long(bencher: Bencher) { .high_pass(300) .amplify(1.2) .speed(0.9) - .automatic_gain_control( - 1.0, // target_level - 4.0, // attack_time (in seconds) - 0.005, // release_time (in seconds) - 5.0, // absolute_max_gain - ) + .automatic_gain_control(AutomaticGainControlSettings::default()) .delay(Duration::from_secs_f32(0.5)) .fade_in(Duration::from_secs_f32(2.0)) .take_duration(Duration::from_secs(10)); diff --git a/examples/automatic_gain_control.rs b/examples/automatic_gain_control.rs index 884d046a..8887fcfc 100644 --- a/examples/automatic_gain_control.rs +++ b/examples/automatic_gain_control.rs @@ -1,4 +1,4 @@ -use rodio::source::Source; +use rodio::source::{AutomaticGainControlSettings, Source}; use rodio::Decoder; use std::error::Error; use std::fs::File; @@ -16,7 +16,7 @@ fn main() -> Result<(), Box> { let source = Decoder::try_from(file)?; // Apply automatic gain control to the source - let agc_source = source.automatic_gain_control(1.0, 4.0, 0.005, 5.0); + let agc_source = source.automatic_gain_control(AutomaticGainControlSettings::default()); // Make it so that the source checks if automatic gain control should be // enabled or disabled every 5 milliseconds. We must clone `agc_enabled`, diff --git a/examples/into_file.rs b/examples/into_file.rs index 1e3c9d4b..6f6f8cd3 100644 --- a/examples/into_file.rs +++ b/examples/into_file.rs @@ -1,3 +1,4 @@ +use rodio::source::AutomaticGainControlSettings; use rodio::{wav_to_file, Source}; use std::error::Error; @@ -7,7 +8,7 @@ use std::error::Error; fn main() -> Result<(), Box> { let file = std::fs::File::open("assets/music.mp3")?; let mut audio = rodio::Decoder::try_from(file)? - .automatic_gain_control(1.0, 4.0, 0.005, 3.0) + .automatic_gain_control(AutomaticGainControlSettings::default()) .speed(0.8); let wav_path = "music_mp3_converted.wav"; diff --git a/src/math.rs b/src/math.rs index a30721fa..e5e4b7ba 100644 --- a/src/math.rs +++ b/src/math.rs @@ -1,5 +1,8 @@ //! Math utilities for audio processing. +use crate::common::SampleRate; +use std::time::Duration; + /// Linear interpolation between two samples. /// /// The result should be equivalent to @@ -76,6 +79,28 @@ pub fn linear_to_db(linear: f32) -> f32 { linear.log2() * std::f32::consts::LOG10_2 * 20.0 } +/// Converts a time duration to a smoothing coefficient for exponential filtering. +/// +/// Used for both attack and release filtering in the limiter's envelope detector. +/// Creates a coefficient that determines how quickly the limiter responds to level changes: +/// * Longer times = higher coefficients (closer to 1.0) = slower, smoother response +/// * Shorter times = lower coefficients (closer to 0.0) = faster, more immediate response +/// +/// The coefficient is calculated using the formula: `e^(-1 / (duration_seconds * sample_rate))` +/// which provides exponential smoothing behavior suitable for audio envelope detection. +/// +/// # Arguments +/// +/// * `duration` - Desired response time (attack or release duration) +/// * `sample_rate` - Audio sample rate in Hz +/// +/// # Returns +/// +/// Smoothing coefficient in the range [0.0, 1.0] for use in exponential filters +pub(crate) fn duration_to_coefficient(duration: Duration, sample_rate: SampleRate) -> f32 { + f32::exp(-1.0 / (duration.as_secs_f32() * sample_rate.get() as f32)) +} + /// 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 index 4ac2f538..ad25bc46 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -14,6 +14,7 @@ // use super::SeekError; +use crate::math::duration_to_coefficient; use crate::Source; #[cfg(feature = "experimental")] use atomic_float::AtomicF32; @@ -40,6 +41,37 @@ const fn power_of_two(n: usize) -> usize { /// 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: f32, + /// 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: f32, +} + +impl Default for AutomaticGainControlSettings { + fn default() -> Self { + AutomaticGainControlSettings { + target_level: 1.0, // Default to original level + attack_time: Duration::from_secs_f32(4.0), // Recommended attack time + release_time: Duration::from_secs_f32(0f32), // Recommended release time + absolute_max_gain: 7.0, // Recommended max gain + } + } +} + #[cfg(feature = "experimental")] /// Automatic Gain Control filter for maintaining consistent output levels. /// @@ -49,11 +81,11 @@ const RMS_WINDOW_SIZE: usize = power_of_two(8192); pub struct AutomaticGainControl { input: I, target_level: Arc, + floor: f32, absolute_max_gain: Arc, current_gain: f32, attack_coeff: Arc, release_coeff: Arc, - min_attack_coeff: f32, peak_level: f32, rms_window: CircularBuffer, is_enabled: Arc, @@ -68,11 +100,11 @@ pub struct AutomaticGainControl { pub struct AutomaticGainControl { input: I, target_level: f32, + floor: f32, absolute_max_gain: f32, current_gain: f32, attack_coeff: f32, release_coeff: f32, - min_attack_coeff: f32, peak_level: f32, rms_window: CircularBuffer, is_enabled: bool, @@ -94,8 +126,8 @@ impl CircularBuffer { #[inline] fn new() -> Self { CircularBuffer { - buffer: Box::new([0.0; RMS_WINDOW_SIZE]), - sum: 0.0, + buffer: Box::new([0f32; RMS_WINDOW_SIZE]), + sum: 0f32, index: 0, } } @@ -136,28 +168,28 @@ impl CircularBuffer { pub(crate) fn automatic_gain_control( input: I, target_level: f32, - attack_time: f32, - release_time: f32, + attack_time: Duration, + release_time: Duration, absolute_max_gain: f32, ) -> AutomaticGainControl where I: Source, { - let sample_rate = input.sample_rate().get(); - let attack_coeff = (-1.0 / (attack_time * sample_rate as f32)).exp(); - let release_coeff = (-1.0 / (release_time * sample_rate as f32)).exp(); + 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")] { AutomaticGainControl { input, target_level: Arc::new(AtomicF32::new(target_level)), + floor: 0f32, absolute_max_gain: Arc::new(AtomicF32::new(absolute_max_gain)), current_gain: 1.0, attack_coeff: Arc::new(AtomicF32::new(attack_coeff)), release_coeff: Arc::new(AtomicF32::new(release_coeff)), - min_attack_coeff: release_time, - peak_level: 0.0, + peak_level: 0f32, rms_window: CircularBuffer::new(), is_enabled: Arc::new(AtomicBool::new(true)), } @@ -168,12 +200,12 @@ where AutomaticGainControl { input, target_level, + floor: 0f32, absolute_max_gain, current_gain: 1.0, attack_coeff, release_coeff, - min_attack_coeff: release_time, - peak_level: 0.0, + peak_level: 0f32, rms_window: CircularBuffer::new(), is_enabled: true, } @@ -320,22 +352,34 @@ where } } - /// Updates the peak level with an adaptive attack coefficient + /// 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. /// - /// This method adjusts the peak level using a variable attack coefficient. - /// It responds faster to sudden increases in signal level by using a - /// minimum attack coefficient of `min_attack_coeff` when the sample value exceeds the - /// current peak level. This adaptive behavior helps capture transients - /// more accurately while maintaining smoother behavior for gradual changes. + /// Passing `None` will disable the floor value (setting it to 0.0), allowing the + /// AGC gain to drop to very low levels. #[inline] - fn update_peak_level(&mut self, sample_value: f32) { - let attack_coeff = if sample_value > self.peak_level { - self.attack_coeff().min(self.min_attack_coeff) // User-defined attack time limited via release_time + pub fn set_floor(&mut self, floor: Option) { + self.floor = floor.unwrap_or(0f32); + } + + /// 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: f32, release_coeff: f32) { + let coeff = if sample_value > self.peak_level { + // Fast attack for rising peaks + 0f32 } else { - self.release_coeff() + // Slow release for falling peaks + release_coeff }; - self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value; + self.peak_level = self.peak_level * coeff + sample_value * (1.0 - coeff); } /// Updates the RMS (Root Mean Square) level using a circular buffer approach. @@ -353,37 +397,43 @@ where /// signal, considering the peak level. /// The peak level helps prevent sudden spikes in the output signal. #[inline] - fn calculate_peak_gain(&self) -> f32 { - if self.peak_level > 0.0 { - (self.target_level() / self.peak_level).min(self.absolute_max_gain()) + fn calculate_peak_gain(&self, target_level: f32, absolute_max_gain: f32) -> f32 { + if self.peak_level > 0f32 { + (target_level / self.peak_level).min(absolute_max_gain) } else { - self.absolute_max_gain() + 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 an adaptive attack coefficient - self.update_peak_level(sample_value); + // 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 { - self.target_level() / rms + let rms_gain = if rms > 0f32 { + target_level / rms } else { - self.absolute_max_gain() // Default to max gain if RMS is zero + absolute_max_gain // Default to max gain if RMS is zero }; // Calculate the peak limiting gain - let peak_gain = self.calculate_peak_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 - let desired_gain = rms_gain.min(peak_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) // @@ -410,16 +460,16 @@ where // 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 { - self.attack_coeff() + attack_coeff } else { - self.release_coeff() + 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, self.absolute_max_gain()); + 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")] @@ -429,12 +479,12 @@ where sample * self.current_gain } - /// Returns a mutable reference to the inner source. + /// Returns an immutable reference to the inner source. pub fn inner(&self) -> &I { &self.input } - /// Returns the inner source. + /// Returns a mutable reference to the inner source. pub fn inner_mut(&mut self) -> &mut I { &mut self.input } diff --git a/src/source/limit.rs b/src/source/limit.rs index 85b9a4b4..f3273cff 100644 --- a/src/source/limit.rs +++ b/src/source/limit.rs @@ -63,7 +63,8 @@ use std::time::Duration; use super::SeekError; use crate::{ common::{ChannelCount, Sample, SampleRate}, - math, Source, + math::{self, duration_to_coefficient}, + Source, }; /// Configuration settings for audio limiting. @@ -1116,29 +1117,6 @@ where } } -/// Converts a time duration to a smoothing coefficient for exponential filtering. -/// -/// Used for both attack and release filtering in the limiter's envelope detector. -/// Creates a coefficient that determines how quickly the limiter responds to level changes: -/// * Longer times = higher coefficients (closer to 1.0) = slower, smoother response -/// * Shorter times = lower coefficients (closer to 0.0) = faster, more immediate response -/// -/// The coefficient is calculated using the formula: `e^(-1 / (duration_seconds * sample_rate))` -/// which provides exponential smoothing behavior suitable for audio envelope detection. -/// -/// # Arguments -/// -/// * `duration` - Desired response time (attack or release duration) -/// * `sample_rate` - Audio sample rate in Hz -/// -/// # Returns -/// -/// Smoothing coefficient in the range [0.0, 1.0] for use in exponential filters -#[must_use] -fn duration_to_coefficient(duration: Duration, sample_rate: SampleRate) -> f32 { - f32::exp(-1.0 / (duration.as_secs_f32() * sample_rate.get() as f32)) -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/source/mod.rs b/src/source/mod.rs index ec6c3313..c7d469f8 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -11,7 +11,7 @@ use crate::{ use dasp_sample::FromSample; -pub use self::agc::AutomaticGainControl; +pub use self::agc::{AutomaticGainControl, AutomaticGainControlSettings}; pub use self::amplify::Amplify; pub use self::blt::BltFilter; pub use self::buffered::Buffered; @@ -380,12 +380,13 @@ pub trait Source: Iterator { /// /// ```rust /// // Apply Automatic Gain Control to the source (AGC is on by default) - /// use rodio::source::{Source, SineWave}; + /// use rodio::source::{Source, SineWave, AutomaticGainControlSettings}; /// use rodio::Sink; + /// use std::time::Duration; /// let source = SineWave::new(444.0); // An example. /// let (sink, output) = Sink::new(); // An example. /// - /// let agc_source = source.automatic_gain_control(1.0, 4.0, 0.0, 5.0); + /// let agc_source = source.automatic_gain_control(AutomaticGainControlSettings::default()); /// /// // Add the AGC-controlled source to the sink /// sink.append(agc_source); @@ -394,26 +395,21 @@ pub trait Source: Iterator { #[inline] fn automatic_gain_control( self, - target_level: f32, - attack_time: f32, - release_time: f32, - absolute_max_gain: f32, + agc_settings: AutomaticGainControlSettings, ) -> AutomaticGainControl where Self: Sized, { // Added Limits to prevent the AGC from blowing up. ;) - const MIN_ATTACK_TIME: f32 = 10.0; - const MIN_RELEASE_TIME: f32 = 10.0; - let attack_time = attack_time.min(MIN_ATTACK_TIME); - let release_time = release_time.min(MIN_RELEASE_TIME); + let attack_time_limited = agc_settings.attack_time.min(Duration::from_secs_f32(10.0)); + let release_time_limited = agc_settings.release_time.min(Duration::from_secs_f32(10.0)); agc::automatic_gain_control( self, - target_level, - attack_time, - release_time, - absolute_max_gain, + agc_settings.target_level, + attack_time_limited, + release_time_limited, + agc_settings.absolute_max_gain, ) }