Implement thread-safe parameter control for AGC using AtomicF32

- Replace static parameters with AtomicF32 for thread-safe access
- Add methods to get Arc<AtomicF32> for release_coeff, attack_coeff, absolute_max_gain, and target_level
- Enable real-time modification of AGC parameters during playback
- Use Ordering::Relaxed for optimal low-latency performance
- Remove set_* methods in favor of direct atomic access
- Update internal methods to use atomic loads consistently

This change allows for dynamic adjustment of AGC parameters
without interrupting audio playback, improving real-time control
and responsiveness of the Automatic Gain Control system.
This commit is contained in:
UnknownSuperficialNight 2024-10-01 20:29:22 +13:00
parent 86cb156e47
commit 3e4bf8b12b
3 changed files with 39 additions and 31 deletions

View file

@ -17,6 +17,7 @@ lewton = { version = "0.10", optional = true }
minimp3_fixed = { version = "0.5.4", optional = true} minimp3_fixed = { version = "0.5.4", optional = true}
symphonia = { version = "0.5.4", optional = true, default-features = false } symphonia = { version = "0.5.4", optional = true, default-features = false }
crossbeam-channel = { version = "0.5.8", optional = true } crossbeam-channel = { version = "0.5.8", optional = true }
atomic_float = "1.1.0"
thiserror = "1.0.49" thiserror = "1.0.49"
tracing = { version = "0.1.40", optional = true } tracing = { version = "0.1.40", optional = true }

View file

@ -221,10 +221,10 @@ impl Sink {
/// ///
/// # Errors /// # Errors
/// This function will return [`SeekError::NotSupported`] if one of the underlying /// This function will return [`SeekError::NotSupported`] if one of the underlying
/// sources does not support seeking. /// sources does not support seeking.
/// ///
/// It will return an error if an implementation ran /// It will return an error if an implementation ran
/// into one during the seek. /// into one during the seek.
/// ///
/// When seeking beyond the end of a source this /// When seeking beyond the end of a source this
/// function might return an error if the duration of the source is not known. /// function might return an error if the duration of the source is not known.

View file

@ -15,6 +15,7 @@
use super::SeekError; use super::SeekError;
use crate::{Sample, Source}; use crate::{Sample, Source};
use atomic_float::AtomicF32;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@ -33,11 +34,11 @@ const RMS_WINDOW_SIZE: usize = 8192;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct AutomaticGainControl<I> { pub struct AutomaticGainControl<I> {
input: I, input: I,
target_level: f32, target_level: Arc<AtomicF32>,
absolute_max_gain: f32, absolute_max_gain: Arc<AtomicF32>,
current_gain: f32, current_gain: f32,
attack_coeff: f32, attack_coeff: Arc<AtomicF32>,
release_coeff: f32, release_coeff: Arc<AtomicF32>,
min_attack_coeff: f32, min_attack_coeff: f32,
peak_level: f32, peak_level: f32,
rms_window: CircularBuffer, rms_window: CircularBuffer,
@ -99,7 +100,7 @@ impl CircularBuffer {
/// * `release_time` - Time constant for gain decrease /// * `release_time` - Time constant for gain decrease
/// * `absolute_max_gain` - Maximum allowable gain /// * `absolute_max_gain` - Maximum allowable gain
#[inline] #[inline]
pub fn automatic_gain_control<I>( pub(crate) fn automatic_gain_control<I>(
input: I, input: I,
target_level: f32, target_level: f32,
attack_time: f32, attack_time: f32,
@ -111,14 +112,16 @@ where
I::Item: Sample, I::Item: Sample,
{ {
let sample_rate = input.sample_rate(); let sample_rate = input.sample_rate();
let attack_coeff = (-1.0 / (attack_time * sample_rate as f32)).exp();
let release_coeff = (-1.0 / (release_time * sample_rate as f32)).exp();
AutomaticGainControl { AutomaticGainControl {
input, input,
target_level, target_level: Arc::new(AtomicF32::new(target_level)),
absolute_max_gain, absolute_max_gain: Arc::new(AtomicF32::new(absolute_max_gain)),
current_gain: 1.0, current_gain: 1.0,
attack_coeff: (-1.0 / (attack_time * sample_rate as f32)).exp(), attack_coeff: Arc::new(AtomicF32::new(attack_coeff)),
release_coeff: (-1.0 / (release_time * sample_rate as f32)).exp(), release_coeff: Arc::new(AtomicF32::new(release_coeff)),
min_attack_coeff: release_time, min_attack_coeff: release_time,
peak_level: 0.0, peak_level: 0.0,
rms_window: CircularBuffer::new(), rms_window: CircularBuffer::new(),
@ -137,32 +140,30 @@ where
/// for the Automatic Gain Control. The target level determines the /// for the Automatic Gain Control. The target level determines the
/// desired amplitude of the processed audio signal. /// desired amplitude of the processed audio signal.
#[inline] #[inline]
pub fn set_target_level(&mut self, level: f32) { pub fn get_target_level(&self) -> Arc<AtomicF32> {
self.target_level = level; Arc::clone(&self.target_level)
} }
/// Sets a new absolute maximum gain limit. /// Sets a new absolute maximum gain limit.
#[inline] #[inline]
pub fn set_absolute_max_gain(&mut self, max_gain: f32) { pub fn get_absolute_max_gain(&self) -> Arc<AtomicF32> {
self.absolute_max_gain = max_gain; Arc::clone(&self.absolute_max_gain)
} }
/// This method allows changing the attack coefficient dynamically. /// This method allows changing the attack coefficient dynamically.
/// The attack coefficient determines how quickly the AGC responds to level increases. /// 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. /// A smaller value results in faster response, while a larger value gives a slower response.
#[inline] #[inline]
pub fn set_attack_coeff(&mut self, attack_time: f32) { pub fn get_attack_coeff(&self) -> Arc<AtomicF32> {
let sample_rate = self.input.sample_rate(); Arc::clone(&self.attack_coeff)
self.attack_coeff = (-1.0 / (attack_time * sample_rate as f32)).exp();
} }
/// This method allows changing the release coefficient dynamically. /// This method allows changing the release coefficient dynamically.
/// The release coefficient determines how quickly the AGC responds to level decreases. /// 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. /// A smaller value results in faster response, while a larger value gives a slower response.
#[inline] #[inline]
pub fn set_release_coeff(&mut self, release_time: f32) { pub fn get_release_coeff(&self) -> Arc<AtomicF32> {
let sample_rate = self.input.sample_rate(); Arc::clone(&self.release_coeff)
self.release_coeff = (-1.0 / (release_time * sample_rate as f32)).exp();
} }
/// Returns a handle to control AGC on/off state. /// Returns a handle to control AGC on/off state.
@ -183,9 +184,11 @@ where
#[inline] #[inline]
fn update_peak_level(&mut self, sample_value: f32) { fn update_peak_level(&mut self, sample_value: f32) {
let attack_coeff = if sample_value > self.peak_level { let attack_coeff = if sample_value > self.peak_level {
self.attack_coeff.min(self.min_attack_coeff) // User-defined attack time limited via release_time self.attack_coeff
.load(Ordering::Relaxed)
.min(self.min_attack_coeff) // User-defined attack time limited via release_time
} else { } else {
self.release_coeff self.release_coeff.load(Ordering::Relaxed)
}; };
self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value; self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value;
} }
@ -207,9 +210,10 @@ where
#[inline] #[inline]
fn calculate_peak_gain(&self) -> f32 { fn calculate_peak_gain(&self) -> f32 {
if self.peak_level > 0.0 { if self.peak_level > 0.0 {
(self.target_level / self.peak_level).min(self.absolute_max_gain) (self.target_level.load(Ordering::Relaxed) / self.peak_level)
.min(self.absolute_max_gain.load(Ordering::Relaxed))
} else { } else {
self.absolute_max_gain self.absolute_max_gain.load(Ordering::Relaxed)
} }
} }
@ -226,9 +230,9 @@ where
// Compute the gain adjustment required to reach the target level based on RMS // Compute the gain adjustment required to reach the target level based on RMS
let rms_gain = if rms > 0.0 { let rms_gain = if rms > 0.0 {
self.target_level / rms self.target_level.load(Ordering::Relaxed) / rms
} else { } else {
self.absolute_max_gain // Default to max gain if RMS is zero self.absolute_max_gain.load(Ordering::Relaxed) // Default to max gain if RMS is zero
}; };
// Calculate the peak limiting gain // Calculate the peak limiting gain
@ -262,16 +266,19 @@ where
// By using a faster release time for decreasing gain, we can mitigate these issues and provide // 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. // more responsive control over sudden level increases while maintaining smooth gain increases.
let attack_speed = if desired_gain > self.current_gain { let attack_speed = if desired_gain > self.current_gain {
self.attack_coeff &self.attack_coeff
} else { } else {
self.release_coeff &self.release_coeff
}; };
// Gradually adjust the current gain towards the desired gain for smooth transitions // 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); self.current_gain = self.current_gain * attack_speed.load(Ordering::Relaxed)
+ desired_gain * (1.0 - attack_speed.load(Ordering::Relaxed));
// Ensure the calculated gain stays within the defined operational range // Ensure the calculated gain stays within the defined operational range
self.current_gain = self.current_gain.clamp(0.1, self.absolute_max_gain); self.current_gain = self
.current_gain
.clamp(0.1, self.absolute_max_gain.load(Ordering::Relaxed));
// Output current gain value for developers to fine tune their inputs to automatic_gain_control // Output current gain value for developers to fine tune their inputs to automatic_gain_control
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]