Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 4 additions & 12 deletions benches/effects.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::time::Duration;

use divan::Bencher;
use rodio::source::AutomaticGainControlSettings;
use rodio::Source;

mod shared;
Expand Down Expand Up @@ -47,12 +48,7 @@ fn amplify(bencher: Bencher) {
fn agc_enabled(bencher: Bencher) {
bencher.with_inputs(music_wav).bench_values(|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
)
.automatic_gain_control(AutomaticGainControlSettings::default())
.for_each(divan::black_box_drop)
})
}
Expand All @@ -62,12 +58,8 @@ fn agc_enabled(bencher: Bencher) {
fn agc_disabled(bencher: Bencher) {
bencher.with_inputs(music_wav).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
);
let amplified_source =
source.automatic_gain_control(AutomaticGainControlSettings::default());

// Get the control handle and disable AGC
let agc_control = amplified_source.get_agc_control();
Expand Down
8 changes: 2 additions & 6 deletions benches/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::num::NonZero;
use std::time::Duration;

use divan::Bencher;
use rodio::source::AutomaticGainControlSettings;
use rodio::ChannelCount;
use rodio::{source::UniformSourceIterator, Source};

Expand All @@ -19,12 +20,7 @@ fn long(bencher: Bencher) {
.high_pass(300)
.amplify(1.2)
.speed(0.9)
.automatic_gain_control(
1.0, // target_level
4.0, // attack_time (in seconds)
0.005, // release_time (in seconds)
5.0, // absolute_max_gain
)
.automatic_gain_control(AutomaticGainControlSettings::default())
.delay(Duration::from_secs_f32(0.5))
.fade_in(Duration::from_secs_f32(2.0))
.take_duration(Duration::from_secs(10));
Expand Down
4 changes: 2 additions & 2 deletions examples/automatic_gain_control.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use rodio::source::Source;
use rodio::source::{AutomaticGainControlSettings, Source};
use rodio::Decoder;
use std::error::Error;
use std::fs::File;
Expand All @@ -16,7 +16,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let source = Decoder::try_from(file)?;

// Apply automatic gain control to the source
let agc_source = source.automatic_gain_control(1.0, 4.0, 0.005, 5.0);
let agc_source = source.automatic_gain_control(AutomaticGainControlSettings::default());

// Make it so that the source checks if automatic gain control should be
// enabled or disabled every 5 milliseconds. We must clone `agc_enabled`,
Expand Down
3 changes: 2 additions & 1 deletion examples/into_file.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use rodio::source::AutomaticGainControlSettings;
use rodio::{wav_to_file, Source};
use std::error::Error;

Expand All @@ -7,7 +8,7 @@ use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let file = std::fs::File::open("assets/music.mp3")?;
let mut audio = rodio::Decoder::try_from(file)?
.automatic_gain_control(1.0, 4.0, 0.005, 3.0)
.automatic_gain_control(AutomaticGainControlSettings::default())
.speed(0.8);

let wav_path = "music_mp3_converted.wav";
Expand Down
25 changes: 25 additions & 0 deletions src/math.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
//! Math utilities for audio processing.

use crate::common::SampleRate;
use std::time::Duration;

/// Linear interpolation between two samples.
///
/// The result should be equivalent to
Expand Down Expand Up @@ -76,6 +79,28 @@ pub fn linear_to_db(linear: f32) -> f32 {
linear.log2() * std::f32::consts::LOG10_2 * 20.0
}

/// Converts a time duration to a smoothing coefficient for exponential filtering.
///
/// Used for both attack and release filtering in the limiter's envelope detector.
/// Creates a coefficient that determines how quickly the limiter responds to level changes:
/// * Longer times = higher coefficients (closer to 1.0) = slower, smoother response
/// * Shorter times = lower coefficients (closer to 0.0) = faster, more immediate response
///
/// The coefficient is calculated using the formula: `e^(-1 / (duration_seconds * sample_rate))`
/// which provides exponential smoothing behavior suitable for audio envelope detection.
///
/// # Arguments
///
/// * `duration` - Desired response time (attack or release duration)
/// * `sample_rate` - Audio sample rate in Hz
///
/// # Returns
///
/// Smoothing coefficient in the range [0.0, 1.0] for use in exponential filters
pub(crate) fn duration_to_coefficient(duration: Duration, sample_rate: SampleRate) -> f32 {
f32::exp(-1.0 / (duration.as_secs_f32() * sample_rate.get() as f32))
}

/// Utility macro for getting a `NonZero` from a literal. Especially
/// useful for passing in `ChannelCount` and `Samplerate`.
/// Equivalent to: `const { core::num::NonZero::new($n).unwrap() }`
Expand Down
132 changes: 91 additions & 41 deletions src/source/agc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
//

use super::SeekError;
use crate::math::duration_to_coefficient;
use crate::Source;
#[cfg(feature = "experimental")]
use atomic_float::AtomicF32;
Expand All @@ -40,6 +41,37 @@ const fn power_of_two(n: usize) -> usize {
/// A larger size provides more stable RMS values but increases latency.
const RMS_WINDOW_SIZE: usize = power_of_two(8192);

/// Settings for the Automatic Gain Control (AGC).
///
/// This struct contains parameters that define how the AGC will function,
/// allowing users to customise its behaviour.
#[derive(Debug, Clone)]
pub struct AutomaticGainControlSettings {
/// The desired output level that the AGC tries to maintain.
/// A value of 1.0 means no change to the original level.
pub target_level: f32,
/// Time constant for gain increases (how quickly the AGC responds to level increases).
/// Longer durations result in slower, more gradual gain increases.
pub attack_time: Duration,
/// Time constant for gain decreases (how quickly the AGC responds to level decreases).
/// Shorter durations allow for faster response to sudden loud signals.
pub release_time: Duration,
/// Maximum allowable gain multiplication to prevent excessive amplification.
/// This acts as a safety limit to avoid distortion from over-amplification.
pub absolute_max_gain: f32,
}

impl Default for AutomaticGainControlSettings {
fn default() -> Self {
AutomaticGainControlSettings {
target_level: 1.0, // Default to original level
attack_time: Duration::from_secs_f32(4.0), // Recommended attack time
release_time: Duration::from_secs_f32(0f32), // Recommended release time
absolute_max_gain: 7.0, // Recommended max gain
}
}
}

#[cfg(feature = "experimental")]
/// Automatic Gain Control filter for maintaining consistent output levels.
///
Expand All @@ -49,11 +81,11 @@ const RMS_WINDOW_SIZE: usize = power_of_two(8192);
pub struct AutomaticGainControl<I> {
input: I,
target_level: Arc<AtomicF32>,
floor: f32,
absolute_max_gain: Arc<AtomicF32>,
current_gain: f32,
attack_coeff: Arc<AtomicF32>,
release_coeff: Arc<AtomicF32>,
min_attack_coeff: f32,
peak_level: f32,
rms_window: CircularBuffer,
is_enabled: Arc<AtomicBool>,
Expand All @@ -68,11 +100,11 @@ pub struct AutomaticGainControl<I> {
pub struct AutomaticGainControl<I> {
input: I,
target_level: f32,
floor: f32,
absolute_max_gain: f32,
current_gain: f32,
attack_coeff: f32,
release_coeff: f32,
min_attack_coeff: f32,
peak_level: f32,
rms_window: CircularBuffer,
is_enabled: bool,
Expand All @@ -94,8 +126,8 @@ impl CircularBuffer {
#[inline]
fn new() -> Self {
CircularBuffer {
buffer: Box::new([0.0; RMS_WINDOW_SIZE]),
sum: 0.0,
buffer: Box::new([0f32; RMS_WINDOW_SIZE]),
sum: 0f32,
index: 0,
}
}
Expand Down Expand Up @@ -136,28 +168,28 @@ impl CircularBuffer {
pub(crate) fn automatic_gain_control<I>(
input: I,
target_level: f32,
attack_time: f32,
release_time: f32,
attack_time: Duration,
release_time: Duration,
absolute_max_gain: f32,
) -> AutomaticGainControl<I>
where
I: Source,
{
let sample_rate = input.sample_rate().get();
let attack_coeff = (-1.0 / (attack_time * sample_rate as f32)).exp();
let release_coeff = (-1.0 / (release_time * sample_rate as f32)).exp();
let sample_rate = input.sample_rate();
let attack_coeff = duration_to_coefficient(attack_time, sample_rate);
let release_coeff = duration_to_coefficient(release_time, sample_rate);

#[cfg(feature = "experimental")]
{
AutomaticGainControl {
input,
target_level: Arc::new(AtomicF32::new(target_level)),
floor: 0f32,
absolute_max_gain: Arc::new(AtomicF32::new(absolute_max_gain)),
current_gain: 1.0,
attack_coeff: Arc::new(AtomicF32::new(attack_coeff)),
release_coeff: Arc::new(AtomicF32::new(release_coeff)),
min_attack_coeff: release_time,
peak_level: 0.0,
peak_level: 0f32,
rms_window: CircularBuffer::new(),
is_enabled: Arc::new(AtomicBool::new(true)),
}
Expand All @@ -168,12 +200,12 @@ where
AutomaticGainControl {
input,
target_level,
floor: 0f32,
absolute_max_gain,
current_gain: 1.0,
attack_coeff,
release_coeff,
min_attack_coeff: release_time,
peak_level: 0.0,
peak_level: 0f32,
rms_window: CircularBuffer::new(),
is_enabled: true,
}
Expand Down Expand Up @@ -320,22 +352,34 @@ where
}
}

/// Updates the peak level with an adaptive attack coefficient
/// Set the floor value for the AGC
///
/// This method sets the floor value for the AGC. The floor value is the minimum
/// gain that the AGC will allow. The gain will not drop below this value.
///
/// 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
/// current peak level. This adaptive behavior helps capture transients
/// more accurately while maintaining smoother behavior for gradual changes.
/// Passing `None` will disable the floor value (setting it to 0.0), allowing the
/// AGC gain to drop to very low levels.
#[inline]
fn update_peak_level(&mut self, sample_value: f32) {
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
pub fn set_floor(&mut self, floor: Option<f32>) {
self.floor = floor.unwrap_or(0f32);
}

/// Updates the peak level using instant attack and slow release behaviour
///
/// This method uses instant response (0.0 coefficient) when the signal is increasing
/// and the release coefficient when the signal is decreasing, providing
/// appropriate tracking behaviour for peak detection.
#[inline]
fn update_peak_level(&mut self, sample_value: f32, release_coeff: f32) {
let coeff = if sample_value > self.peak_level {
// Fast attack for rising peaks
0f32
} else {
self.release_coeff()
// Slow release for falling peaks
release_coeff
};

self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value;
self.peak_level = self.peak_level * coeff + sample_value * (1.0 - coeff);
}

/// Updates the RMS (Root Mean Square) level using a circular buffer approach.
Expand All @@ -353,37 +397,43 @@ where
/// 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).min(self.absolute_max_gain())
fn calculate_peak_gain(&self, target_level: f32, absolute_max_gain: f32) -> f32 {
if self.peak_level > 0f32 {
(target_level / self.peak_level).min(absolute_max_gain)
} else {
self.absolute_max_gain()
absolute_max_gain
}
}

#[inline]
fn process_sample(&mut self, sample: I::Item) -> I::Item {
// Cache atomic loads at the start - avoids repeated atomic operations
let target_level = self.target_level();
let absolute_max_gain = self.absolute_max_gain();
let attack_coeff = self.attack_coeff();
let release_coeff = self.release_coeff();

// Convert the sample to its absolute float value for level calculations
let sample_value = sample.abs();

// Dynamically adjust peak level using an adaptive attack coefficient
self.update_peak_level(sample_value);
// Dynamically adjust peak level using cached release coefficient
self.update_peak_level(sample_value, release_coeff);

// 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
let rms_gain = if rms > 0f32 {
target_level / rms
} else {
self.absolute_max_gain() // Default to max gain if RMS is zero
absolute_max_gain // Default to max gain if RMS is zero
};

// Calculate the peak limiting gain
let peak_gain = self.calculate_peak_gain();
let peak_gain = self.calculate_peak_gain(target_level, absolute_max_gain);

// Use RMS for general adjustments, but limit by peak gain to prevent clipping
let desired_gain = rms_gain.min(peak_gain);
// Use RMS for general adjustments, but limit by peak gain to prevent clipping and apply a minimum floor value
let desired_gain = rms_gain.min(peak_gain).max(self.floor);

// Adaptive attack/release speed for AGC (Automatic Gain Control)
//
Expand All @@ -410,16 +460,16 @@ 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 {
self.attack_coeff()
attack_coeff
} else {
self.release_coeff()
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());
self.current_gain = self.current_gain.clamp(0.1, absolute_max_gain);

// Output current gain value for developers to fine tune their inputs to automatic_gain_control
#[cfg(feature = "tracing")]
Expand All @@ -429,12 +479,12 @@ where
sample * self.current_gain
}

/// Returns a mutable reference to the inner source.
/// Returns an immutable reference to the inner source.
pub fn inner(&self) -> &I {
&self.input
}

/// Returns the inner source.
/// Returns a mutable reference to the inner source.
pub fn inner_mut(&mut self) -> &mut I {
&mut self.input
}
Expand Down
Loading