From 85bfcbd40cca5296e520511ca1026c0542453cf4 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Fri, 27 Sep 2024 06:39:09 +1200 Subject: [PATCH 01/18] Init commit for automatic_gain_control --- src/conversions/sample.rs | 21 +++++ src/source/agc.rs | 175 ++++++++++++++++++++++++++++++++++++++ src/source/mod.rs | 18 +++- 3 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 src/source/agc.rs 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..89342eab --- /dev/null +++ b/src/source/agc.rs @@ -0,0 +1,175 @@ +use super::SeekError; +use crate::{Sample, Source}; +use std::time::Duration; + +/// 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 adjustment +/// * `absolute_max_gain` - Maximum allowable gain +pub fn automatic_gain_control( + input: I, + target_level: f32, + attack_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(), + peak_level: 0.0, + rms_level: 0.0, + rms_window: vec![0.0; 1024], + rms_index: 0, + } +} + +/// Automatic Gain Control filter for maintaining consistent output levels. +#[derive(Clone, Debug)] +pub struct AutomaticGainControl { + input: I, + target_level: f32, + absolute_max_gain: f32, + current_gain: f32, + attack_coeff: f32, + peak_level: f32, + rms_level: f32, + rms_window: Vec, + rms_index: usize, +} + +impl AutomaticGainControl +where + I: Source, + I::Item: Sample, +{ + // Sets a new target output level. + #[inline] + pub fn set_target_level(&mut self, level: f32) { + self.target_level = level; + } + + // Add this method to allow changing the attack coefficient + 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(); + } +} + +impl Iterator for AutomaticGainControl +where + I: Source, + I::Item: Sample, +{ + type Item = I::Item; + + #[inline] + fn next(&mut self) -> Option { + self.input.next().map(|value| { + let sample_value = value.to_f32().abs(); + + // Update peak level with adaptive attack coefficient + let attack_coeff = if sample_value > self.peak_level { + self.attack_coeff.min(0.1) // Faster response to sudden increases + } else { + self.attack_coeff + }; + self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value; + + // Update RMS level using a sliding window + self.rms_level -= self.rms_window[self.rms_index] / self.rms_window.len() as f32; + self.rms_window[self.rms_index] = sample_value * sample_value; + self.rms_level += self.rms_window[self.rms_index] / self.rms_window.len() as f32; + self.rms_index = (self.rms_index + 1) % self.rms_window.len(); + + let rms = self.rms_level.sqrt(); + + // Calculate gain adjustments based on peak and RMS levels + let peak_gain = if self.peak_level > 0.0 { + self.target_level / self.peak_level + } else { + 1.0 + }; + + let rms_gain = if rms > 0.0 { + self.target_level / rms + } else { + 1.0 + }; + + // Choose the more conservative gain adjustment + let desired_gain = peak_gain.min(rms_gain); + + // Set target gain to the middle of the allowable range + let target_gain = 1.0; // Midpoint between 0.1 and 3.0 + + // Smoothly adjust current gain towards the target + let adjustment_speed = 0.05; // Balance between responsiveness and stability + self.current_gain = self.current_gain * (1.0 - adjustment_speed) + + (desired_gain * target_gain) * adjustment_speed; + + // Constrain gain within predefined limits + self.current_gain = self.current_gain.clamp(0.1, self.absolute_max_gain); + + // Uncomment for debugging: + println!("Current gain: {}", self.current_gain); + + // Apply calculated gain to the sample + value.amplify(self.current_gain) + }) + } + + #[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..ceb49e8c 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,20 @@ where amplify::amplify(self, value) } + /// Applies automatic gain control to the sound. + #[inline] + fn automatic_gain_control( + self, + target_level: f32, + attack_time: f32, + absolute_max_gain: f32, + ) -> AutomaticGainControl + where + Self: Sized, + { + agc::automatic_gain_control(self, target_level, attack_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 +461,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. From 625d0f27b56a777af40e0dd2bb33fb622f29a1cd Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:35:44 +1200 Subject: [PATCH 02/18] Updated comments, refactored logic & added more member functions for simplicity --- src/source/agc.rs | 121 +++++++++++++++++++++++++++++++++------------- src/source/mod.rs | 15 ++++++ 2 files changed, 102 insertions(+), 34 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index 89342eab..0eb4270d 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -26,6 +26,7 @@ where input, target_level, absolute_max_gain, + attack_time, current_gain: 1.0, attack_coeff: (-1.0 / (attack_time * sample_rate as f32)).exp(), peak_level: 0.0, @@ -41,6 +42,7 @@ pub struct AutomaticGainControl { input: I, target_level: f32, absolute_max_gain: f32, + attack_time: f32, current_gain: f32, attack_coeff: f32, peak_level: f32, @@ -54,17 +56,82 @@ where I: Source, I::Item: Sample, { - // Sets a new target output level. + /// 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; } - // Add this method to allow changing the attack coefficient + /// 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 changes. + /// 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(); } + + /// 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 0.1 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(0.1) // Faster response to sudden increases + } else { + self.attack_coeff + }; + self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value; + } + + /// Calculate gain adjustments based on peak and RMS levels + /// This method determines the appropriate gain level to apply to the audio + /// signal, considering both peak and RMS (Root Mean Square) levels. + /// The peak level helps prevent sudden spikes, while the RMS level + /// provides a measure of the overall signal power over time. + #[inline] + fn calculate_peak_gain(&self) -> f32 { + if self.peak_level > 0.0 { + self.target_level / self.peak_level + } else { + 1.0 + } + } + + /// Updates the RMS (Root Mean Square) level using a sliding window 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 { + // Remove the oldest sample from the RMS calculation + self.rms_level -= self.rms_window[self.rms_index] / self.rms_window.len() as f32; + + // Add the new sample to the window + self.rms_window[self.rms_index] = sample_value * sample_value; + + // Add the new sample to the RMS calculation + self.rms_level += self.rms_window[self.rms_index] / self.rms_window.len() as f32; + + // Move the index to the next position + self.rms_index = (self.rms_index + 1) % self.rms_window.len(); + + // Calculate and return the RMS value + self.rms_level.sqrt() + } } impl Iterator for AutomaticGainControl @@ -77,55 +144,41 @@ where #[inline] fn next(&mut self) -> Option { self.input.next().map(|value| { + // Convert the sample to its absolute float value for level calculations let sample_value = value.to_f32().abs(); - // Update peak level with adaptive attack coefficient - let attack_coeff = if sample_value > self.peak_level { - self.attack_coeff.min(0.1) // Faster response to sudden increases - } else { - self.attack_coeff - }; - self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value; - - // Update RMS level using a sliding window - self.rms_level -= self.rms_window[self.rms_index] / self.rms_window.len() as f32; - self.rms_window[self.rms_index] = sample_value * sample_value; - self.rms_level += self.rms_window[self.rms_index] / self.rms_window.len() as f32; - self.rms_index = (self.rms_index + 1) % self.rms_window.len(); + // Dynamically adjust peak level using an adaptive attack coefficient + self.update_peak_level(sample_value); - let rms = self.rms_level.sqrt(); + // Calculate the current RMS (Root Mean Square) level using a sliding window approach + let rms = self.update_rms(sample_value); - // Calculate gain adjustments based on peak and RMS levels - let peak_gain = if self.peak_level > 0.0 { - self.target_level / self.peak_level - } else { - 1.0 - }; + // Determine the gain adjustment needed based on the current peak level + let peak_gain = self.calculate_peak_gain(); + // 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 { - 1.0 + 1.0 // Default to unity gain if RMS is zero to avoid division by zero }; - // Choose the more conservative gain adjustment + // Select the lower of peak and RMS gains to ensure conservative adjustment let desired_gain = peak_gain.min(rms_gain); - // Set target gain to the middle of the allowable range - let target_gain = 1.0; // Midpoint between 0.1 and 3.0 - - // Smoothly adjust current gain towards the target - let adjustment_speed = 0.05; // Balance between responsiveness and stability - self.current_gain = self.current_gain * (1.0 - adjustment_speed) - + (desired_gain * target_gain) * adjustment_speed; + // Gradually adjust the current gain towards the desired gain for smooth transitions + let adjustment_speed = self.attack_time; // Controls the trade-off between quick response and stability + self.current_gain = + self.current_gain * (1.0 - adjustment_speed) + desired_gain * adjustment_speed; - // Constrain gain within predefined limits + // Ensure the calculated gain stays within the defined operational range self.current_gain = self.current_gain.clamp(0.1, self.absolute_max_gain); - // Uncomment for debugging: + // Output current gain value for monitoring and debugging purposes + // Must be deleted before merge: println!("Current gain: {}", self.current_gain); - // Apply calculated gain to the sample + // Apply the computed gain to the input sample and return the result value.amplify(self.current_gain) }) } diff --git a/src/source/mod.rs b/src/source/mod.rs index ceb49e8c..71441804 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -235,6 +235,21 @@ where } /// 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`: The desired output level, typically between 0.9 and 1.0. + /// This is the level that the AGC will try to maintain. + /// + /// * `attack_time`: The time (in seconds) it takes for the AGC to respond to + /// an increase in input level. A shorter attack time means faster response + /// but may lead to more abrupt changes. + /// + /// * `absolute_max_gain`: The maximum gain that can be applied to the signal. + /// This prevents excessive amplification of quiet signals or background noise. #[inline] fn automatic_gain_control( self, From 6b62544cdeea28e26c9f5994c28b169922e3644c Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:04:48 +1200 Subject: [PATCH 03/18] Added simple flag to enable the debug temporarily during development --- src/source/agc.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index 0eb4270d..71e82289 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -176,7 +176,10 @@ where // Output current gain value for monitoring and debugging purposes // Must be deleted before merge: - println!("Current gain: {}", self.current_gain); + // Added flag so its usable without the debug temporarily during development + if std::env::args().any(|arg| arg == "--debug-gain") { + println!("Current gain: {}", self.current_gain); + } // Apply the computed gain to the input sample and return the result value.amplify(self.current_gain) From 611055c62df0d621d8a19d00e46c2dc264cdcc26 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:27:35 +1200 Subject: [PATCH 04/18] Enhance AGC with asymmetric attack/release and safety limits - Implement asymmetric attack/release - Introduce MIN_ATTACK_TIME limit to prevent AGC instability - Clamp attack_time to prevent instability - Faster decrease, slower increase for smoother sound - Safeguard against extreme gain fluctuations --- src/source/agc.rs | 40 ++++++++++++++++++++++++++++++++++++++-- src/source/mod.rs | 4 ++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index 71e82289..acb01bfd 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -166,10 +166,46 @@ where // Select the lower of peak and RMS gains to ensure conservative adjustment let desired_gain = peak_gain.min(rms_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 { + // Slower attack for increasing gain to avoid sudden amplification + self.attack_time.min(10.0) + } else { + // Faster release for decreasing gain to prevent overamplification + // Cap release time at 1.0 to ensure responsiveness + // This prevents issues with very high attack times: + // - Avoids overcorrection and near-zero sound levels + // - Ensures AGC can always correct itself in reasonable time + // - Maintains ability to quickly attenuate sudden loud signals + (self.attack_time * 0.1).min(1.0) // Capped faster release time + }; + // Gradually adjust the current gain towards the desired gain for smooth transitions - let adjustment_speed = self.attack_time; // Controls the trade-off between quick response and stability self.current_gain = - self.current_gain * (1.0 - adjustment_speed) + desired_gain * adjustment_speed; + self.current_gain * (1.0 - attack_speed) + desired_gain * 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); diff --git a/src/source/mod.rs b/src/source/mod.rs index 71441804..65a38ce9 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -260,6 +260,10 @@ where where Self: Sized, { + // Added Limits to prevent the AGC from blowing up. ;) + const MIN_ATTACK_TIME: f32 = 10.0; + let attack_time = attack_time.min(MIN_ATTACK_TIME); + agc::automatic_gain_control(self, target_level, attack_time, absolute_max_gain) } From 97636d163eb30e6652e5a6b4ca988b27c2638e49 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Sat, 28 Sep 2024 11:10:50 +1200 Subject: [PATCH 05/18] Add author credit to AGC implementation --- src/source/agc.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/source/agc.rs b/src/source/agc.rs index acb01bfd..cda5e816 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -1,3 +1,17 @@ +// +// Automatic Gain Control (AGC) Algorithm +// Designed by @UnknownSuperficialNight +// +// Features: +// • Adaptive peak detection +// • RMS-based level estimation +// • Asymmetric attack/release +// +// Optimized for smooth and responsive gain control +// +// Crafted with love. Enjoy! :) +// + use super::SeekError; use crate::{Sample, Source}; use std::time::Duration; From 1b27bcd335b852f0ac2687d332e1e54f7b9611c4 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Sat, 28 Sep 2024 13:52:42 +1200 Subject: [PATCH 06/18] Add debug logging for AGC current gain value --- src/source/agc.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index cda5e816..98e6ef55 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -16,6 +16,9 @@ use super::SeekError; use crate::{Sample, Source}; use std::time::Duration; +#[cfg(feature = "tracing")] +use tracing; + /// Constructs an `AutomaticGainControl` object with specified parameters. /// /// # Arguments @@ -224,12 +227,9 @@ where // 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 monitoring and debugging purposes - // Must be deleted before merge: - // Added flag so its usable without the debug temporarily during development - if std::env::args().any(|arg| arg == "--debug-gain") { - println!("Current gain: {}", self.current_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 value.amplify(self.current_gain) From d9f7967fd287396935d391b937b6e1d9c8b091e1 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Sat, 28 Sep 2024 17:19:13 +1200 Subject: [PATCH 07/18] Better document comments for docs.rs --- src/source/mod.rs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/source/mod.rs b/src/source/mod.rs index 65a38ce9..55847118 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -241,15 +241,28 @@ where /// /// # Parameters /// - /// * `target_level`: The desired output level, typically between 0.9 and 1.0. - /// This is the level that the AGC will try to maintain. + /// * `target_level`: 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`: The time (in seconds) it takes for the AGC to respond to - /// an increase in input level. A shorter attack time means faster response - /// but may lead to more abrupt changes. + /// * `attack_time`: 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 2.0 seconds provides a sweet spot for most applications. /// /// * `absolute_max_gain`: The maximum gain that can be applied to the signal. - /// This prevents excessive amplification of quiet signals or background noise. + /// 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 4, which provides a good balance between + /// amplification capability and protection against distortion in most scenarios. #[inline] fn automatic_gain_control( self, From ce3d7e0bd2a81b9414aa4eb70d08da9443b9ef68 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Sat, 28 Sep 2024 23:47:34 +1200 Subject: [PATCH 08/18] Optimize AGC with CircularBuffer and enhance functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace Vec-based RMS calculation with efficient CircularBuffer - Add separate release_time for asymmetric gain control - Implement MAX_PEAK_LEVEL constant to prevent clipping - Revise gain calculation logic: • Separate RMS and peak gain calculations • Use RMS for general adjustments, peak for limiting • Implement smoother transitions between gain levels • Improve handling of edge cases (e.g., zero RMS) - Improve code organization and documentation --- src/source/agc.rs | 169 +++++++++++++++++++++++++++++----------------- src/source/mod.rs | 42 ++++++++++-- 2 files changed, 144 insertions(+), 67 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index 98e6ef55..c82088bf 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -6,6 +6,7 @@ // • Adaptive peak detection // • RMS-based level estimation // • Asymmetric attack/release +// • RMS-based general adjustments with peak limiting // // Optimized for smooth and responsive gain control // @@ -19,18 +20,89 @@ 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 = 1024; + +/// Minimum attack coefficient for rapid response to sudden level increases. +/// Balances between responsiveness and stability. +const MIN_ATTACK_COEFF: f32 = 0.05; + +/// Maximum allowed peak level to prevent clipping +const MAX_PEAK_LEVEL: f32 = 0.99; + +/// 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, + peak_level: f32, + rms_window: CircularBuffer, +} + +/// 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`. + 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. + 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. + 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 adjustment +/// * `attack_time` - Time constant for gain increase +/// * `release_time` - Time constant for gain decrease /// * `absolute_max_gain` - Maximum allowable gain pub fn automatic_gain_control( input: I, target_level: f32, attack_time: f32, + release_time: f32, absolute_max_gain: f32, ) -> AutomaticGainControl where @@ -43,31 +115,14 @@ where input, target_level, absolute_max_gain, - attack_time, 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(), peak_level: 0.0, - rms_level: 0.0, - rms_window: vec![0.0; 1024], - rms_index: 0, + rms_window: CircularBuffer::new(RMS_WINDOW_SIZE), } } -/// Automatic Gain Control filter for maintaining consistent output levels. -#[derive(Clone, Debug)] -pub struct AutomaticGainControl { - input: I, - target_level: f32, - absolute_max_gain: f32, - attack_time: f32, - current_gain: f32, - attack_coeff: f32, - peak_level: f32, - rms_level: f32, - rms_window: Vec, - rms_index: usize, -} - impl AutomaticGainControl where I: Source, @@ -90,7 +145,7 @@ where } /// This method allows changing the attack coefficient dynamically. - /// The attack coefficient determines how quickly the AGC responds to level changes. + /// 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) { @@ -98,56 +153,53 @@ where 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(); + } + /// 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 0.1 when the sample value exceeds the + /// 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(0.1) // Faster response to sudden increases + self.attack_coeff.min(MIN_ATTACK_COEFF) // Faster response to sudden increases } else { - self.attack_coeff + self.release_coeff }; self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value; } - /// Calculate gain adjustments based on peak and RMS levels + /// Calculate gain adjustments based on peak levels /// This method determines the appropriate gain level to apply to the audio - /// signal, considering both peak and RMS (Root Mean Square) levels. - /// The peak level helps prevent sudden spikes, while the RMS level - /// provides a measure of the overall signal power over time. + /// 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 + (MAX_PEAK_LEVEL / self.peak_level).min(self.absolute_max_gain) } else { - 1.0 + self.absolute_max_gain } } - /// Updates the RMS (Root Mean Square) level using a sliding window approach. + /// 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 { - // Remove the oldest sample from the RMS calculation - self.rms_level -= self.rms_window[self.rms_index] / self.rms_window.len() as f32; - - // Add the new sample to the window - self.rms_window[self.rms_index] = sample_value * sample_value; - - // Add the new sample to the RMS calculation - self.rms_level += self.rms_window[self.rms_index] / self.rms_window.len() as f32; - - // Move the index to the next position - self.rms_index = (self.rms_index + 1) % self.rms_window.len(); - - // Calculate and return the RMS value - self.rms_level.sqrt() + let squared_sample = sample_value * sample_value; + self.rms_window.push(squared_sample); + self.rms_window.mean().sqrt() } } @@ -170,18 +222,18 @@ where // Calculate the current RMS (Root Mean Square) level using a sliding window approach let rms = self.update_rms(sample_value); - // Determine the gain adjustment needed based on the current peak level - let peak_gain = self.calculate_peak_gain(); - // 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 { - 1.0 // Default to unity gain if RMS is zero to avoid division by zero + self.absolute_max_gain // Default to max gain if RMS is zero }; - // Select the lower of peak and RMS gains to ensure conservative adjustment - let desired_gain = peak_gain.min(rms_gain); + // 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) // @@ -208,28 +260,21 @@ 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 { - // Slower attack for increasing gain to avoid sudden amplification - self.attack_time.min(10.0) + self.attack_coeff } else { - // Faster release for decreasing gain to prevent overamplification - // Cap release time at 1.0 to ensure responsiveness - // This prevents issues with very high attack times: - // - Avoids overcorrection and near-zero sound levels - // - Ensures AGC can always correct itself in reasonable time - // - Maintains ability to quickly attenuate sudden loud signals - (self.attack_time * 0.1).min(1.0) // Capped faster release time + self.release_coeff }; // Gradually adjust the current gain towards the desired gain for smooth transitions self.current_gain = - self.current_gain * (1.0 - attack_speed) + desired_gain * attack_speed; + 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); + tracing::debug!("AGC gain: {}", self.current_gain,); // Apply the computed gain to the input sample and return the result value.amplify(self.current_gain) diff --git a/src/source/mod.rs b/src/source/mod.rs index 55847118..1aa5565e 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -241,13 +241,19 @@ where /// /// # Parameters /// - /// * `target_level`: The desired output level, where 1.0 represents the original sound level. + /// * `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`: The time (in seconds) for the AGC to respond to input level increases. + /// * `attack_time`: + /// TL;DR: Response time for volume increases. Shorter = faster but may cause abrupt changes. Recommended: 2.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 @@ -256,7 +262,24 @@ where /// adjustment speed is limited by the attack time. Balance is key for optimal performance. /// A recommended attack_time of 2.0 seconds provides a sweet spot for most applications. /// - /// * `absolute_max_gain`: The maximum gain that can be applied to the signal. + /// * `release_time`: + /// TL;DR: Response time for volume decreases. Shorter = faster gain reduction. Recommended: 0.01 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.01 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: 4.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. @@ -268,6 +291,7 @@ where self, target_level: f32, attack_time: f32, + release_time: f32, absolute_max_gain: f32, ) -> AutomaticGainControl where @@ -275,9 +299,17 @@ where { // 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); - - agc::automatic_gain_control(self, target_level, attack_time, absolute_max_gain) + 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. From 28b3c4b80c4a4d99c3b42bbc844802d0f30862c6 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Sun, 29 Sep 2024 13:36:55 +1300 Subject: [PATCH 09/18] Removed MAX_PEAK_LEVEL now uses target_level as intended and styled documentation --- src/source/agc.rs | 5 +---- src/source/mod.rs | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index c82088bf..02777dd9 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -28,9 +28,6 @@ const RMS_WINDOW_SIZE: usize = 1024; /// Balances between responsiveness and stability. const MIN_ATTACK_COEFF: f32 = 0.05; -/// Maximum allowed peak level to prevent clipping -const MAX_PEAK_LEVEL: f32 = 0.99; - /// Automatic Gain Control filter for maintaining consistent output levels. /// /// This struct implements an AGC algorithm that dynamically adjusts audio levels @@ -186,7 +183,7 @@ where #[inline] fn calculate_peak_gain(&self) -> f32 { if self.peak_level > 0.0 { - (MAX_PEAK_LEVEL / self.peak_level).min(self.absolute_max_gain) + (self.target_level / self.peak_level).min(self.absolute_max_gain) } else { self.absolute_max_gain } diff --git a/src/source/mod.rs b/src/source/mod.rs index 1aa5565e..5ac1d724 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -241,17 +241,17 @@ where /// /// # Parameters /// - /// * `target_level`: - /// TL;DR: Desired output level. 1.0 = original level, > 1.0 amplifies, < 1.0 reduces. + ///* `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. + /// 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: 2.0 seconds. + ///* `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 @@ -260,10 +260,10 @@ where /// 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 2.0 seconds provides a sweet spot for most applications. + /// 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.01 seconds. + ///* `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. @@ -273,18 +273,18 @@ where /// 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.01 seconds often works well for + /// 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: 4.0. + ///* `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 4, which provides a good balance between + /// A recommended value for `absolute_max_gain` is `5`, which provides a good balance between /// amplification capability and protection against distortion in most scenarios. #[inline] fn automatic_gain_control( From f4bb729235259d185d68b79d35c850f854d6a230 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Sun, 29 Sep 2024 15:03:58 +1300 Subject: [PATCH 10/18] Added benchmark for agc and inlines --- benches/effects.rs | 16 ++++++++++++++++ src/source/agc.rs | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/benches/effects.rs b/benches/effects.rs index 5f100112..eba38fae 100644 --- a/benches/effects.rs +++ b/benches/effects.rs @@ -46,3 +46,19 @@ 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(bencher: Bencher) { + bencher + .with_inputs(|| TestSource::music_wav().to_f32s()) + .bench_values(|source| { + source + .automatic_gain_control( + 1.0, // target_level + 2.0, // attack_time (in seconds) + 0.01, // release_time (in seconds) + 5.0, // absolute_max_gain + ) + .for_each(divan::black_box_drop) + }) +} diff --git a/src/source/agc.rs b/src/source/agc.rs index 02777dd9..495c1b3c 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -59,6 +59,7 @@ 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], @@ -70,6 +71,7 @@ impl CircularBuffer { /// 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; @@ -81,6 +83,7 @@ impl CircularBuffer { /// 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 } @@ -95,6 +98,7 @@ impl CircularBuffer { /// * `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, From 1d2a6fdc94a057e2f573824328ea6687c6e4cdbf Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Sun, 29 Sep 2024 15:52:33 +1300 Subject: [PATCH 11/18] Removed bullet point from docs --- src/source/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/source/mod.rs b/src/source/mod.rs index 5ac1d724..da7c04bc 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -241,7 +241,7 @@ where /// /// # Parameters /// - ///* `target_level`: + /// `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. @@ -250,7 +250,7 @@ where /// 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`: + /// `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. @@ -262,7 +262,7 @@ where /// 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`: + /// `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. @@ -276,7 +276,7 @@ where /// 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`: + /// `absolute_max_gain`: /// **TL;DR**: Maximum allowed gain. Prevents over-amplification. **Recommended: `5.0`**. /// /// The maximum gain that can be applied to the signal. From beeacf6e4a53fbfbc7f14bc4669659b26454003d Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Sun, 29 Sep 2024 19:07:30 +1300 Subject: [PATCH 12/18] Added agc to CHANGELOG.md --- CHANGELOG.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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. From 9bf97acfa678f21113d460a48aa81d4fa262cde7 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Mon, 30 Sep 2024 12:44:55 +1300 Subject: [PATCH 13/18] Update benchmark to new default values --- benches/effects.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/benches/effects.rs b/benches/effects.rs index eba38fae..91c48b14 100644 --- a/benches/effects.rs +++ b/benches/effects.rs @@ -54,10 +54,10 @@ fn agc(bencher: Bencher) { .bench_values(|source| { source .automatic_gain_control( - 1.0, // target_level - 2.0, // attack_time (in seconds) - 0.01, // release_time (in seconds) - 5.0, // absolute_max_gain + 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) }) From a8a443ba511f1237235f67d23a4a5d1f4e9ed4bd Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Mon, 30 Sep 2024 19:46:46 +1300 Subject: [PATCH 14/18] Enhance AGC stability and flexibility - Increase RMS_WINDOW_SIZE for more stable measurements of very low frequencies - Replace MIN_ATTACK_COEFF with release_coeff for improved customizability These changes provide better handling of low-frequency content and allow developers more control over AGC behavior, particularly in attack/release time adjustments. --- src/source/agc.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index 495c1b3c..01344a1d 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -22,11 +22,7 @@ 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 = 1024; - -/// Minimum attack coefficient for rapid response to sudden level increases. -/// Balances between responsiveness and stability. -const MIN_ATTACK_COEFF: f32 = 0.05; +const RMS_WINDOW_SIZE: usize = 8192; /// Automatic Gain Control filter for maintaining consistent output levels. /// @@ -40,6 +36,7 @@ pub struct AutomaticGainControl { current_gain: f32, attack_coeff: f32, release_coeff: f32, + min_attack_coeff: f32, peak_level: f32, rms_window: CircularBuffer, } @@ -119,6 +116,7 @@ where 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), } @@ -171,9 +169,9 @@ where /// 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) { + fn update_peak_level(&mut self, sample_value: f32, release_time: f32) { let attack_coeff = if sample_value > self.peak_level { - self.attack_coeff.min(MIN_ATTACK_COEFF) // Faster response to sudden increases + self.attack_coeff.min(release_time) // Faster response to sudden increases } else { self.release_coeff }; @@ -218,7 +216,7 @@ where let sample_value = value.to_f32().abs(); // Dynamically adjust peak level using an adaptive attack coefficient - self.update_peak_level(sample_value); + self.update_peak_level(sample_value, self.min_attack_coeff); // Calculate the current RMS (Root Mean Square) level using a sliding window approach let rms = self.update_rms(sample_value); From 68e1bd21c0244d0279c5e8b89857ea4e84a729b0 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Mon, 30 Sep 2024 20:01:57 +1300 Subject: [PATCH 15/18] Pass min_attack_coeff directly --- src/source/agc.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index 01344a1d..d54b5972 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -165,13 +165,13 @@ where /// /// 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 + /// 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, release_time: f32) { + fn update_peak_level(&mut self, sample_value: f32) { let attack_coeff = if sample_value > self.peak_level { - self.attack_coeff.min(release_time) // Faster response to sudden increases + self.attack_coeff.min(self.min_attack_coeff) // User-defined attack time limited via release_time } else { self.release_coeff }; @@ -216,7 +216,7 @@ where let sample_value = value.to_f32().abs(); // Dynamically adjust peak level using an adaptive attack coefficient - self.update_peak_level(sample_value, self.min_attack_coeff); + 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); From 2442aa09e8eb3e48e44534b7108b0e3061e7f2a8 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Tue, 1 Oct 2024 00:45:48 +1300 Subject: [PATCH 16/18] Add real-time toggle for AGC processing Implement get_agc_control() to allow dynamic enabling/disabling of AGC during audio playback. --- src/source/agc.rs | 164 ++++++++++++++++++++++++++-------------------- 1 file changed, 92 insertions(+), 72 deletions(-) diff --git a/src/source/agc.rs b/src/source/agc.rs index d54b5972..1aa1ae1a 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -15,6 +15,8 @@ use super::SeekError; use crate::{Sample, Source}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::time::Duration; #[cfg(feature = "tracing")] @@ -39,6 +41,7 @@ pub struct AutomaticGainControl { min_attack_coeff: f32, peak_level: f32, rms_window: CircularBuffer, + is_enabled: Arc, } /// A circular buffer for efficient RMS calculation over a sliding window. @@ -119,6 +122,7 @@ where min_attack_coeff: release_time, peak_level: 0.0, rms_window: CircularBuffer::new(RMS_WINDOW_SIZE), + is_enabled: Arc::new(AtomicBool::new(true)), } } @@ -161,6 +165,14 @@ where 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. @@ -178,6 +190,16 @@ where 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. @@ -191,14 +213,72 @@ where } } - /// 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() + 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) } } @@ -211,72 +291,12 @@ where #[inline] fn next(&mut self) -> Option { - self.input.next().map(|value| { - // Convert the sample to its absolute float value for level calculations - let sample_value = value.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 + self.input.next().map(|sample| { + if self.is_enabled.load(Ordering::Relaxed) { + self.process_sample(sample) } 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 - value.amplify(self.current_gain) + sample + } }) } From b59533e3820f1181b1469d721795429efd56eb0c Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Tue, 1 Oct 2024 00:50:45 +1300 Subject: [PATCH 17/18] Add new benchmark for disabled_agc --- benches/effects.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/benches/effects.rs b/benches/effects.rs index 91c48b14..1101bd3a 100644 --- a/benches/effects.rs +++ b/benches/effects.rs @@ -48,7 +48,7 @@ fn amplify(bencher: Bencher) { } #[divan::bench] -fn agc(bencher: Bencher) { +fn agc_enabled(bencher: Bencher) { bencher .with_inputs(|| TestSource::music_wav().to_f32s()) .bench_values(|source| { @@ -62,3 +62,25 @@ fn agc(bencher: Bencher) { .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) + }) +} From 42fe8320aa24b3b4f62e5bf9a1003aac6c559761 Mon Sep 17 00:00:00 2001 From: UnknownSuperficialNight <88142731+UnknownSuperficialNight@users.noreply.github.com> Date: Tue, 1 Oct 2024 01:46:55 +1300 Subject: [PATCH 18/18] Enhance automatic_gain_control documentation - Add references to get_agc_control method in automatic_gain_control docs - Include a quick start example demonstrating usage of get_agc_control --- src/source/mod.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/source/mod.rs b/src/source/mod.rs index da7c04bc..75dd7e20 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -286,6 +286,27 @@ where /// 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,