diff --git a/CHANGELOG.md b/CHANGELOG.md index c8b4a720..95767126 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,25 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support for *ALAC/AIFF* +- Add `automatic_gain_control` source for dynamic audio level adjustment. - New test signal generator sources: - `SignalGenerator` source generates a sine, triangle, square wave or sawtooth of a given frequency and sample rate. - - `Chirp` source generates a sine wave with a linearly-increasing + - `Chirp` source generates a sine wave with a linearly-increasing frequency over a given frequency range and duration. - - `white` and `pink` generate white or pink noise, respectively. These - sources depend on the `rand` crate and are guarded with the "noise" + - `white` and `pink` generate white or pink noise, respectively. These + sources depend on the `rand` crate and are guarded with the "noise" feature. - Documentation for the "noise" feature has been added to `lib.rs`. - New Fade and Crossfade sources: - `fade_out` fades an input out using a linear gain fade. - `linear_gain_ramp` applies a linear gain change to a sound over a given duration. `fade_out` is implemented as a `linear_gain_ramp` and - `fade_in` has been refactored to use the `linear_gain_ramp` + `fade_in` has been refactored to use the `linear_gain_ramp` implementation. ### Fixed - `Sink.try_seek` now updates `controls.position` before returning. Calls to `Sink.get_pos` - done immediately after a seek will now return the correct value. + done immediately after a seek will now return the correct value. ### Changed - `SamplesBuffer` is now `Clone` @@ -53,7 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Source` trait is now also implemented for `Box` and `&mut Source` - `fn new_vorbis` is now also available when the `symphonia-vorbis` feature is enabled -### Added +### Added - Adds a new method `try_seek` to all sources. It returns either an error or seeks to the given position. A few sources are "unsupported" they return the error `Unsupported`. @@ -61,7 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - channel upscaling now follows the 'WAVEFORMATEXTENSIBLE' format and no longer - repeats the last source channel on all extra output channels. + repeats the last source channel on all extra output channels. Stereo content playing on a 5.1 speaker set will now only use the front left and front right speaker instead of repeating the right sample on all speakers except the front left one. diff --git a/Cargo.toml b/Cargo.toml index 6e55dc33..3ca08666 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,9 +22,12 @@ thiserror = "1.0.49" rand = { version = "0.8.5", features = ["small_rng"], optional = true } tracing = { version = "0.1.40", optional = true } +atomic_float = { version = "1.1.0", optional = true } + [features] default = ["flac", "vorbis", "wav", "mp3"] tracing = ["dep:tracing"] +experimental = ["dep:atomic_float"] flac = ["claxon"] vorbis = ["lewton"] diff --git a/benches/effects.rs b/benches/effects.rs index 5f100112..2849fc82 100644 --- a/benches/effects.rs +++ b/benches/effects.rs @@ -46,3 +46,42 @@ fn amplify(bencher: Bencher) { .with_inputs(|| TestSource::music_wav().to_f32s()) .bench_values(|source| source.amplify(0.8).for_each(divan::black_box_drop)) } + +#[divan::bench] +fn agc_enabled(bencher: Bencher) { + bencher + .with_inputs(|| TestSource::music_wav().to_f32s()) + .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 + ) + .for_each(divan::black_box_drop) + }) +} + +#[cfg(feature = "experimental")] +#[divan::bench] +fn agc_disabled(bencher: Bencher) { + bencher + .with_inputs(|| TestSource::music_wav().to_f32s()) + .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 + ); + + // Get the control handle and disable AGC + let agc_control = amplified_source.get_agc_control(); + agc_control.store(false, std::sync::atomic::Ordering::Relaxed); + + // Process the audio stream with AGC disabled + amplified_source.for_each(divan::black_box_drop) + }) +} diff --git a/src/conversions/sample.rs b/src/conversions/sample.rs index 7f593f7a..a78f5c14 100644 --- a/src/conversions/sample.rs +++ b/src/conversions/sample.rs @@ -80,6 +80,9 @@ pub trait Sample: CpalSample { /// Multiplies the value of this sample by the given amount. fn amplify(self, value: f32) -> Self; + /// Converts the sample to an f32 value. + fn to_f32(self) -> f32; + /// Calls `saturating_add` on the sample. fn saturating_add(self, other: Self) -> Self; @@ -102,6 +105,12 @@ impl Sample for u16 { ((self as f32) * value) as u16 } + #[inline] + fn to_f32(self) -> f32 { + // Convert u16 to f32 in the range [-1.0, 1.0] + (self as f32 - 32768.0) / 32768.0 + } + #[inline] fn saturating_add(self, other: u16) -> u16 { self.saturating_add(other) @@ -125,6 +134,12 @@ impl Sample for i16 { ((self as f32) * value) as i16 } + #[inline] + fn to_f32(self) -> f32 { + // Convert i16 to f32 in the range [-1.0, 1.0] + self as f32 / 32768.0 + } + #[inline] fn saturating_add(self, other: i16) -> i16 { self.saturating_add(other) @@ -147,6 +162,12 @@ impl Sample for f32 { self * value } + #[inline] + fn to_f32(self) -> f32 { + // f32 is already in the correct format + self + } + #[inline] fn saturating_add(self, other: f32) -> f32 { self + other diff --git a/src/sink.rs b/src/sink.rs index 148a1588..c63ff72f 100644 --- a/src/sink.rs +++ b/src/sink.rs @@ -221,10 +221,10 @@ impl Sink { /// /// # Errors /// This function will return [`SeekError::NotSupported`] if one of the underlying - /// sources does not support seeking. + /// sources does not support seeking. /// /// It will return an error if an implementation ran - /// into one during the seek. + /// into one during the seek. /// /// When seeking beyond the end of a source this /// function might return an error if the duration of the source is not known. diff --git a/src/source/agc.rs b/src/source/agc.rs new file mode 100644 index 00000000..6dfbaf11 --- /dev/null +++ b/src/source/agc.rs @@ -0,0 +1,480 @@ +// +// 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 super::SeekError; +use crate::{Sample, Source}; +#[cfg(feature = "experimental")] +use atomic_float::AtomicF32; +#[cfg(feature = "experimental")] +use std::sync::atomic::{AtomicBool, Ordering}; +#[cfg(feature = "experimental")] +use std::sync::Arc; +use std::time::Duration; + +#[cfg(feature = "tracing")] +use tracing; + +/// 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); + +#[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, + 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, +} + +#[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: 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, +} + +/// 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<[f32; RMS_WINDOW_SIZE]>, + sum: f32, + 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: f32) -> f32 { + 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) -> f32 { + self.sum / RMS_WINDOW_SIZE as f32 + } +} + +/// 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: f32, + attack_time: f32, + release_time: f32, + absolute_max_gain: f32, +) -> AutomaticGainControl +where + I: Source, + I::Item: Sample, +{ + let sample_rate = input.sample_rate(); + let attack_coeff = (-1.0 / (attack_time * sample_rate as f32)).exp(); + let release_coeff = (-1.0 / (release_time * sample_rate as f32)).exp(); + + #[cfg(feature = "experimental")] + { + AutomaticGainControl { + input, + target_level: Arc::new(AtomicF32::new(target_level)), + 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, + rms_window: CircularBuffer::new(), + is_enabled: Arc::new(AtomicBool::new(true)), + } + } + + #[cfg(not(feature = "experimental"))] + { + AutomaticGainControl { + input, + target_level, + absolute_max_gain, + current_gain: 1.0, + attack_coeff, + release_coeff, + min_attack_coeff: release_time, + peak_level: 0.0, + rms_window: CircularBuffer::new(), + is_enabled: true, + } + } +} + +impl AutomaticGainControl +where + I: Source, + I::Item: Sample, +{ + #[inline] + fn target_level(&self) -> f32 { + #[cfg(feature = "experimental")] + { + self.target_level.load(Ordering::Relaxed) + } + #[cfg(not(feature = "experimental"))] + { + self.target_level + } + } + + #[inline] + fn absolute_max_gain(&self) -> f32 { + #[cfg(feature = "experimental")] + { + self.absolute_max_gain.load(Ordering::Relaxed) + } + #[cfg(not(feature = "experimental"))] + { + self.absolute_max_gain + } + } + + #[inline] + fn attack_coeff(&self) -> f32 { + #[cfg(feature = "experimental")] + { + self.attack_coeff.load(Ordering::Relaxed) + } + #[cfg(not(feature = "experimental"))] + { + self.attack_coeff + } + } + + #[inline] + fn release_coeff(&self) -> f32 { + #[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. + #[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. + #[inline] + pub fn get_release_coeff(&self) -> Arc { + Arc::clone(&self.release_coeff) + } + + #[cfg(feature = "experimental")] + /// Access the AGC on/off control for real-time adjustment. + /// + /// Use this to dynamically enable or disable AGC processing during runtime. + /// Useful for comparing processed and unprocessed audio or for disabling/enabling AGC at runtime. + #[inline] + pub fn get_agc_control(&self) -> Arc { + Arc::clone(&self.is_enabled) + } + + #[cfg(not(feature = "experimental"))] + /// 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) { + self.is_enabled = enabled; + } + + /// Updates the peak level with an adaptive attack coefficient + /// + /// 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. + #[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 + } else { + self.release_coeff() + }; + + self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value; + } + + /// 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: f32) -> f32 { + 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) -> f32 { + if self.peak_level > 0.0 { + (self.target_level() / self.peak_level).min(self.absolute_max_gain()) + } else { + self.absolute_max_gain() + } + } + + #[inline] + fn process_sample(&mut self, sample: I::Item) -> I::Item { + // Convert the sample to its absolute float value for level calculations + let sample_value = sample.to_f32().abs(); + + // Dynamically adjust peak level using an adaptive attack coefficient + self.update_peak_level(sample_value); + + // 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 + } else { + self.absolute_max_gain() // Default to max gain if RMS is zero + }; + + // Calculate the peak limiting gain + let peak_gain = self.calculate_peak_gain(); + + // Use RMS for general adjustments, but limit by peak gain to prevent clipping + let desired_gain = rms_gain.min(peak_gain); + + // 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 { + self.attack_coeff() + } else { + self.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()); + + // 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.amplify(self.current_gain) + } +} + +impl Iterator for AutomaticGainControl +where + I: Source, + I::Item: Sample, +{ + type Item = I::Item; + + #[inline] + fn next(&mut self) -> Option { + self.input.next().map(|sample| { + if self.is_enabled() { + self.process_sample(sample) + } else { + sample + } + }) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.input.size_hint() + } +} + +impl ExactSizeIterator for AutomaticGainControl +where + I: Source + ExactSizeIterator, + I::Item: Sample, +{ +} + +impl Source for AutomaticGainControl +where + I: Source, + I::Item: Sample, +{ + #[inline] + fn current_frame_len(&self) -> Option { + self.input.current_frame_len() + } + + #[inline] + fn channels(&self) -> u16 { + self.input.channels() + } + + #[inline] + fn sample_rate(&self) -> u32 { + 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) + } +} diff --git a/src/source/mod.rs b/src/source/mod.rs index 932bec4c..816387ad 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -6,6 +6,7 @@ use cpal::FromSample; use crate::Sample; +pub use self::agc::AutomaticGainControl; pub use self::amplify::Amplify; pub use self::blt::BltFilter; pub use self::buffered::Buffered; @@ -38,6 +39,7 @@ pub use self::take::TakeDuration; pub use self::uniform::UniformSourceIterator; pub use self::zero::Zero; +mod agc; mod amplify; mod blt; mod buffered; @@ -241,6 +243,105 @@ where amplify::amplify(self, value) } + /// Applies automatic gain control to the sound. + /// + /// 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.005` 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.005` seconds often works well for + /// general use, providing a good balance between responsiveness and smooth transitions. + /// + /// `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. + /// + /// Use `get_agc_control` to obtain a handle for real-time enabling/disabling of the AGC. + /// + /// # Example (Quick start) + /// + /// ```rust + /// // Apply Automatic Gain Control to the source (AGC is on by default) + /// let agc_source = source.automatic_gain_control(1.0, 4.0, 0.005, 5.0); + /// + /// // Get a handle to control the AGC's enabled state (optional) + /// let agc_control = agc_source.get_agc_control(); + /// + /// // You can toggle AGC on/off at any time (optional) + /// agc_control.store(false, std::sync::atomic::Ordering::Relaxed); + /// + /// // Add the AGC-controlled source to the sink + /// sink.append(agc_source); + /// + /// // Note: Using agc_control is optional. If you don't need to toggle AGC, + /// // you can simply use the agc_source directly without getting agc_control. + /// ``` + #[inline] + fn automatic_gain_control( + self, + target_level: f32, + attack_time: f32, + release_time: f32, + absolute_max_gain: f32, + ) -> 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); + + agc::automatic_gain_control( + self, + target_level, + attack_time, + release_time, + absolute_max_gain, + ) + } + /// Mixes this sound fading out with another sound fading in for the given duration. /// /// Only the crossfaded portion (beginning of self, beginning of other) is returned. @@ -454,7 +555,7 @@ where /// sources does not support seeking. /// /// It will return an error if an implementation ran - /// into one during the seek. + /// into one during the seek. /// /// Seeking beyond the end of a source might return an error if the total duration of /// the source is not known.