mirror of
https://github.com/RustAudio/rodio
synced 2024-12-14 06:02:31 +00:00
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:
parent
86cb156e47
commit
3e4bf8b12b
3 changed files with 39 additions and 31 deletions
|
@ -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 }
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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")]
|
||||||
|
|
Loading…
Reference in a new issue