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 1 commit
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
175 changes: 175 additions & 0 deletions src/source/agc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
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,
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,
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.
#[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<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| {
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);
UnknownSuperficialNight marked this conversation as resolved.
Show resolved Hide resolved

// Apply calculated gain to the sample
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)
}
}
18 changes: 17 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,20 @@ where
amplify::amplify(self, value)
}

/// Applies automatic gain control to the sound.
UnknownSuperficialNight marked this conversation as resolved.
Show resolved Hide resolved
#[inline]
fn automatic_gain_control(
self,
target_level: f32,
attack_time: f32,
absolute_max_gain: f32,
) -> AutomaticGainControl<Self>
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.
Expand Down Expand Up @@ -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.
Expand Down
Loading