diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bcc2ace..cdcf20b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,17 +8,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- Support for *ALAC/AIFF* +- Support for *ALAC/AIFF* +- Add `automatic_gain_control` source for dynamic audio level adjustment. - New 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` @@ -44,7 +45,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`. @@ -52,7 +53,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/benches/effects.rs b/benches/effects.rs index 5f100112..1101bd3a 100644 --- a/benches/effects.rs +++ b/benches/effects.rs @@ -46,3 +46,41 @@ 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) + }) +} + +#[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/source/agc.rs b/src/source/agc.rs new file mode 100644 index 00000000..1aa1ae1a --- /dev/null +++ b/src/source/agc.rs @@ -0,0 +1,345 @@ +// +// 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}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +#[cfg(feature = "tracing")] +use tracing; + +/// 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 = 8192; + +/// 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: Arc, +} + +/// 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: [f32; RMS_WINDOW_SIZE], + index: usize, + sum: f32, +} + +impl CircularBuffer { + /// Creates a new CircularBuffer with a fixed size determined at compile time. + /// + /// The `_size` parameter is ignored as the buffer size is set by `RMS_WINDOW_SIZE`. + #[inline] + fn new(_size: usize) -> Self { + CircularBuffer { + buffer: [0.0; RMS_WINDOW_SIZE], + index: 0, + sum: 0.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]; + self.buffer[self.index] = value; + self.sum += value - old_value; + self.index = (self.index + 1) % self.buffer.len(); + 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 / self.buffer.len() 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 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(); + + AutomaticGainControl { + input, + target_level, + absolute_max_gain, + current_gain: 1.0, + attack_coeff: (-1.0 / (attack_time * sample_rate as f32)).exp(), + release_coeff: (-1.0 / (release_time * sample_rate as f32)).exp(), + min_attack_coeff: release_time, + peak_level: 0.0, + rms_window: CircularBuffer::new(RMS_WINDOW_SIZE), + is_enabled: Arc::new(AtomicBool::new(true)), + } +} + +impl AutomaticGainControl +where + I: Source, + I::Item: Sample, +{ + /// Sets a new target output level. + /// + /// This method allows dynamic adjustment of the target output level + /// for the Automatic Gain Control. The target level determines the + /// desired amplitude of the processed audio signal. + #[inline] + pub fn set_target_level(&mut self, level: f32) { + self.target_level = level; + } + + /// Sets a new absolute maximum gain limit. + #[inline] + pub fn set_absolute_max_gain(&mut self, max_gain: f32) { + self.absolute_max_gain = max_gain; + } + + /// This method allows changing the attack coefficient dynamically. + /// The attack coefficient determines how quickly the AGC responds to level increases. + /// A smaller value results in faster response, while a larger value gives a slower response. + #[inline] + pub fn set_attack_coeff(&mut self, attack_time: f32) { + let sample_rate = self.input.sample_rate(); + self.attack_coeff = (-1.0 / (attack_time * sample_rate as f32)).exp(); + } + + /// This method allows changing the release coefficient dynamically. + /// The release coefficient determines how quickly the AGC responds to level decreases. + /// A smaller value results in faster response, while a larger value gives a slower response. + #[inline] + pub fn set_release_coeff(&mut self, release_time: f32) { + let sample_rate = self.input.sample_rate(); + self.release_coeff = (-1.0 / (release_time * sample_rate as f32)).exp(); + } + + /// Returns a handle to control AGC on/off state. + /// + /// This allows real-time toggling of the AGC processing. + #[inline] + pub fn get_agc_control(&self) -> Arc { + Arc::clone(&self.is_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.load(Ordering::Relaxed) { + 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 1e85270b..75dd7e20 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; @@ -36,6 +37,7 @@ pub use self::take::TakeDuration; pub use self::uniform::UniformSourceIterator; pub use self::zero::Zero; +mod agc; mod amplify; mod blt; mod buffered; @@ -232,6 +234,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. @@ -445,7 +546,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.