Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Automatic Gain Control #621

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
85bfcbd
Init commit for automatic_gain_control
UnknownSuperficialNight Sep 26, 2024
625d0f2
Updated comments, refactored logic & added more member functions for …
UnknownSuperficialNight Sep 26, 2024
6b62544
Added simple flag to enable the debug temporarily during development
UnknownSuperficialNight Sep 27, 2024
611055c
Enhance AGC with asymmetric attack/release and safety limits
UnknownSuperficialNight Sep 27, 2024
97636d1
Add author credit to AGC implementation
UnknownSuperficialNight Sep 27, 2024
9497f5c
Merge branch 'master' into feature/automatic-gain-control
UnknownSuperficialNight Sep 28, 2024
1b27bcd
Add debug logging for AGC current gain value
UnknownSuperficialNight Sep 28, 2024
d9f7967
Better document comments for docs.rs
UnknownSuperficialNight Sep 28, 2024
ce3d7e0
Optimize AGC with CircularBuffer and enhance functionality
UnknownSuperficialNight Sep 28, 2024
28b3c4b
Removed MAX_PEAK_LEVEL now uses target_level as intended and styled d…
UnknownSuperficialNight Sep 29, 2024
d4a09f3
Merge branch 'master' into feature/automatic-gain-control
UnknownSuperficialNight Sep 29, 2024
f4bb729
Added benchmark for agc and inlines
UnknownSuperficialNight Sep 29, 2024
1d2a6fd
Removed bullet point from docs
UnknownSuperficialNight Sep 29, 2024
beeacf6
Added agc to CHANGELOG.md
UnknownSuperficialNight Sep 29, 2024
9bf97ac
Update benchmark to new default values
UnknownSuperficialNight Sep 29, 2024
a8a443b
Enhance AGC stability and flexibility
UnknownSuperficialNight Sep 30, 2024
68e1bd2
Pass min_attack_coeff directly
UnknownSuperficialNight Sep 30, 2024
2442aa0
Add real-time toggle for AGC processing
UnknownSuperficialNight Sep 30, 2024
b59533e
Add new benchmark for disabled_agc
UnknownSuperficialNight Sep 30, 2024
42fe832
Enhance automatic_gain_control documentation
UnknownSuperficialNight Sep 30, 2024
86cb156
Refactor CircularBuffer to use heap allocation to avoid large stack u…
UnknownSuperficialNight Oct 1, 2024
3e4bf8b
Implement thread-safe parameter control for AGC using AtomicF32
UnknownSuperficialNight Oct 1, 2024
cb85bce
Enforce RMS_WINDOW_SIZE is a power of two at compile time
UnknownSuperficialNight Oct 1, 2024
db0bfb0
Add better documentation for AutomaticGainControl's Implementations
UnknownSuperficialNight Oct 1, 2024
3ce64ef
Add experimental flag to enabled dynamic controls
UnknownSuperficialNight Oct 1, 2024
fd94703
Merge branch 'master' into feature/automatic-gain-control
UnknownSuperficialNight Oct 1, 2024
e2ee86e
Fix unused arc import
UnknownSuperficialNight Oct 1, 2024
ef60286
Trigger CI checks
UnknownSuperficialNight Oct 1, 2024
af210a6
Fix agc_disable benchmark
UnknownSuperficialNight Oct 1, 2024
2610a27
Add documentation to non experimental AutomaticGainControl
UnknownSuperficialNight Oct 1, 2024
f8cf3c5
Added getters
UnknownSuperficialNight Oct 2, 2024
5ce1fff
Added non-atomic is_enabled()
UnknownSuperficialNight Oct 2, 2024
bdbc159
Remove experimental bench comment
UnknownSuperficialNight Oct 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/conversions/sample.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand Down
267 changes: 267 additions & 0 deletions src/source/agc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
use super::SeekError;
use crate::{Sample, Source};
use std::time::Duration;

/// Constructs an `AutomaticGainControl` object with specified parameters.
///
UnknownSuperficialNight marked this conversation as resolved.
Show resolved Hide resolved
/// # 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<I>(
UnknownSuperficialNight marked this conversation as resolved.
Show resolved Hide resolved
input: I,
target_level: f32,
attack_time: f32,
absolute_max_gain: f32,
) -> AutomaticGainControl<I>
where
I: Source,
I::Item: Sample,
{
let sample_rate = input.sample_rate();

AutomaticGainControl {
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,
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<I> {
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<f32>,
rms_index: usize,
}

impl<I> AutomaticGainControl<I>
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 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<I> Iterator for AutomaticGainControl<I>
where
I: Source,
I::Item: Sample,
{
type Item = I::Item;

#[inline]
fn next(&mut self) -> Option<I::Item> {
UnknownSuperficialNight marked this conversation as resolved.
Show resolved Hide resolved
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);

// 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
};

// 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
self.current_gain =
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);

// 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);
}

// Apply the computed gain to the input sample and return the result
value.amplify(self.current_gain)
})
}

#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.input.size_hint()
}
}

impl<I> ExactSizeIterator for AutomaticGainControl<I>
where
I: Source + ExactSizeIterator,
I::Item: Sample,
{
}

impl<I> Source for AutomaticGainControl<I>
where
I: Source,
I::Item: Sample,
{
#[inline]
fn current_frame_len(&self) -> Option<usize> {
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<Duration> {
self.input.total_duration()
}

#[inline]
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
self.input.try_seek(pos)
}
}
37 changes: 36 additions & 1 deletion src/source/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -232,6 +234,39 @@ where
amplify::amplify(self, value)
}

/// Applies automatic gain control to the sound.
UnknownSuperficialNight marked this conversation as resolved.
Show resolved Hide resolved
///
/// Automatic Gain Control (AGC) adjusts the amplitude of the audio signal
/// to maintain a consistent output level.
///
/// # Parameters
UnknownSuperficialNight marked this conversation as resolved.
Show resolved Hide resolved
///
/// * `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,
target_level: f32,
attack_time: f32,
absolute_max_gain: f32,
) -> AutomaticGainControl<Self>
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)
}

/// 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.
Expand Down Expand Up @@ -445,7 +480,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.
Expand Down