rodio/source/
agc.rs

1//
2//      Automatic Gain Control (AGC) Algorithm
3//      Designed by @UnknownSuperficialNight
4//
5//   Features:
6//   • Adaptive peak detection
7//   • RMS-based level estimation
8//   • Asymmetric attack/release
9//   • RMS-based general adjustments with peak limiting
10//
11//   Optimized for smooth and responsive gain control
12//
13//   Crafted with love. Enjoy! :)
14//
15
16use super::SeekError;
17use crate::Source;
18#[cfg(feature = "experimental")]
19use atomic_float::AtomicF32;
20#[cfg(feature = "experimental")]
21use std::sync::atomic::{AtomicBool, Ordering};
22#[cfg(feature = "experimental")]
23use std::sync::Arc;
24use std::time::Duration;
25
26use crate::common::{ChannelCount, SampleRate};
27#[cfg(feature = "tracing")]
28use tracing;
29
30/// Ensures `RMS_WINDOW_SIZE` is a power of two
31const fn power_of_two(n: usize) -> usize {
32    assert!(
33        n.is_power_of_two(),
34        "RMS_WINDOW_SIZE must be a power of two"
35    );
36    n
37}
38
39/// Size of the circular buffer used for RMS calculation.
40/// A larger size provides more stable RMS values but increases latency.
41const RMS_WINDOW_SIZE: usize = power_of_two(8192);
42
43#[cfg(feature = "experimental")]
44/// Automatic Gain Control filter for maintaining consistent output levels.
45///
46/// This struct implements an AGC algorithm that dynamically adjusts audio levels
47/// based on both **peak** and **RMS** (Root Mean Square) measurements.
48#[derive(Clone, Debug)]
49pub struct AutomaticGainControl<I> {
50    input: I,
51    target_level: Arc<AtomicF32>,
52    absolute_max_gain: Arc<AtomicF32>,
53    current_gain: f32,
54    attack_coeff: Arc<AtomicF32>,
55    release_coeff: Arc<AtomicF32>,
56    min_attack_coeff: f32,
57    peak_level: f32,
58    rms_window: CircularBuffer,
59    is_enabled: Arc<AtomicBool>,
60}
61
62#[cfg(not(feature = "experimental"))]
63/// Automatic Gain Control filter for maintaining consistent output levels.
64///
65/// This struct implements an AGC algorithm that dynamically adjusts audio levels
66/// based on both **peak** and **RMS** (Root Mean Square) measurements.
67#[derive(Clone, Debug)]
68pub struct AutomaticGainControl<I> {
69    input: I,
70    target_level: f32,
71    absolute_max_gain: f32,
72    current_gain: f32,
73    attack_coeff: f32,
74    release_coeff: f32,
75    min_attack_coeff: f32,
76    peak_level: f32,
77    rms_window: CircularBuffer,
78    is_enabled: bool,
79}
80
81/// A circular buffer for efficient RMS calculation over a sliding window.
82///
83/// This structure allows for constant-time updates and mean calculations,
84/// which is crucial for real-time audio processing.
85#[derive(Clone, Debug)]
86struct CircularBuffer {
87    buffer: Box<[f32; RMS_WINDOW_SIZE]>,
88    sum: f32,
89    index: usize,
90}
91
92impl CircularBuffer {
93    /// Creates a new `CircularBuffer` with a fixed size determined at compile time.
94    #[inline]
95    fn new() -> Self {
96        CircularBuffer {
97            buffer: Box::new([0.0; RMS_WINDOW_SIZE]),
98            sum: 0.0,
99            index: 0,
100        }
101    }
102
103    /// Pushes a new value into the buffer and returns the old value.
104    ///
105    /// This method maintains a running sum for efficient mean calculation.
106    #[inline]
107    fn push(&mut self, value: f32) -> f32 {
108        let old_value = self.buffer[self.index];
109        // Update the sum by first subtracting the old value and then adding the new value; this is more accurate.
110        self.sum = self.sum - old_value + value;
111        self.buffer[self.index] = value;
112        // Use bitwise AND for efficient index wrapping since RMS_WINDOW_SIZE is a power of two.
113        self.index = (self.index + 1) & (RMS_WINDOW_SIZE - 1);
114        old_value
115    }
116
117    /// Calculates the mean of all values in the buffer.
118    ///
119    /// This operation is `O(1)` due to the maintained running sum.
120    #[inline]
121    fn mean(&self) -> f32 {
122        self.sum / RMS_WINDOW_SIZE as f32
123    }
124}
125
126/// Constructs an `AutomaticGainControl` object with specified parameters.
127///
128/// # Arguments
129///
130/// * `input` - The input audio source
131/// * `target_level` - The desired output level
132/// * `attack_time` - Time constant for gain increase
133/// * `release_time` - Time constant for gain decrease
134/// * `absolute_max_gain` - Maximum allowable gain
135#[inline]
136pub(crate) fn automatic_gain_control<I>(
137    input: I,
138    target_level: f32,
139    attack_time: f32,
140    release_time: f32,
141    absolute_max_gain: f32,
142) -> AutomaticGainControl<I>
143where
144    I: Source,
145{
146    let sample_rate = input.sample_rate();
147    let attack_coeff = (-1.0 / (attack_time * sample_rate as f32)).exp();
148    let release_coeff = (-1.0 / (release_time * sample_rate as f32)).exp();
149
150    #[cfg(feature = "experimental")]
151    {
152        AutomaticGainControl {
153            input,
154            target_level: Arc::new(AtomicF32::new(target_level)),
155            absolute_max_gain: Arc::new(AtomicF32::new(absolute_max_gain)),
156            current_gain: 1.0,
157            attack_coeff: Arc::new(AtomicF32::new(attack_coeff)),
158            release_coeff: Arc::new(AtomicF32::new(release_coeff)),
159            min_attack_coeff: release_time,
160            peak_level: 0.0,
161            rms_window: CircularBuffer::new(),
162            is_enabled: Arc::new(AtomicBool::new(true)),
163        }
164    }
165
166    #[cfg(not(feature = "experimental"))]
167    {
168        AutomaticGainControl {
169            input,
170            target_level,
171            absolute_max_gain,
172            current_gain: 1.0,
173            attack_coeff,
174            release_coeff,
175            min_attack_coeff: release_time,
176            peak_level: 0.0,
177            rms_window: CircularBuffer::new(),
178            is_enabled: true,
179        }
180    }
181}
182
183impl<I> AutomaticGainControl<I>
184where
185    I: Source,
186{
187    #[inline]
188    fn target_level(&self) -> f32 {
189        #[cfg(feature = "experimental")]
190        {
191            self.target_level.load(Ordering::Relaxed)
192        }
193        #[cfg(not(feature = "experimental"))]
194        {
195            self.target_level
196        }
197    }
198
199    #[inline]
200    fn absolute_max_gain(&self) -> f32 {
201        #[cfg(feature = "experimental")]
202        {
203            self.absolute_max_gain.load(Ordering::Relaxed)
204        }
205        #[cfg(not(feature = "experimental"))]
206        {
207            self.absolute_max_gain
208        }
209    }
210
211    #[inline]
212    fn attack_coeff(&self) -> f32 {
213        #[cfg(feature = "experimental")]
214        {
215            self.attack_coeff.load(Ordering::Relaxed)
216        }
217        #[cfg(not(feature = "experimental"))]
218        {
219            self.attack_coeff
220        }
221    }
222
223    #[inline]
224    fn release_coeff(&self) -> f32 {
225        #[cfg(feature = "experimental")]
226        {
227            self.release_coeff.load(Ordering::Relaxed)
228        }
229        #[cfg(not(feature = "experimental"))]
230        {
231            self.release_coeff
232        }
233    }
234
235    #[inline]
236    fn is_enabled(&self) -> bool {
237        #[cfg(feature = "experimental")]
238        {
239            self.is_enabled.load(Ordering::Relaxed)
240        }
241        #[cfg(not(feature = "experimental"))]
242        {
243            self.is_enabled
244        }
245    }
246
247    #[cfg(feature = "experimental")]
248    /// Access the target output level for real-time adjustment.
249    ///
250    /// Use this to dynamically modify the AGC's target level while audio is processing.
251    /// Adjust this value to control the overall output amplitude of the processed signal.
252    #[inline]
253    pub fn get_target_level(&self) -> Arc<AtomicF32> {
254        Arc::clone(&self.target_level)
255    }
256
257    #[cfg(feature = "experimental")]
258    /// Access the maximum gain limit for real-time adjustment.
259    ///
260    /// Use this to dynamically modify the AGC's maximum allowable gain during runtime.
261    /// Adjusting this value helps prevent excessive amplification in low-level signals.
262    #[inline]
263    pub fn get_absolute_max_gain(&self) -> Arc<AtomicF32> {
264        Arc::clone(&self.absolute_max_gain)
265    }
266
267    #[cfg(feature = "experimental")]
268    /// Access the attack coefficient for real-time adjustment.
269    ///
270    /// Use this to dynamically modify how quickly the AGC responds to level increases.
271    /// Smaller values result in faster response, larger values in slower response.
272    /// Adjust during runtime to fine-tune AGC behavior for different audio content.
273    #[inline]
274    pub fn get_attack_coeff(&self) -> Arc<AtomicF32> {
275        Arc::clone(&self.attack_coeff)
276    }
277
278    #[cfg(feature = "experimental")]
279    /// Access the release coefficient for real-time adjustment.
280    ///
281    /// Use this to dynamically modify how quickly the AGC responds to level decreases.
282    /// Smaller values result in faster response, larger values in slower response.
283    /// Adjust during runtime to optimize AGC behavior for varying audio dynamics.
284    #[inline]
285    pub fn get_release_coeff(&self) -> Arc<AtomicF32> {
286        Arc::clone(&self.release_coeff)
287    }
288
289    #[cfg(feature = "experimental")]
290    /// Access the AGC on/off control.
291    /// Use this to dynamically enable or disable AGC processing during runtime.
292    ///
293    /// AGC is on by default. `false` is disabled state, `true` is enabled.
294    /// In disabled state the sound is passed through AGC unchanged.
295    ///
296    /// In particular, this control is useful for comparing processed and unprocessed audio.
297    #[inline]
298    pub fn get_agc_control(&self) -> Arc<AtomicBool> {
299        Arc::clone(&self.is_enabled)
300    }
301
302    /// Enable or disable AGC processing.
303    ///
304    /// Use this to enable or disable AGC processing.
305    /// Useful for comparing processed and unprocessed audio or for disabling/enabling AGC.
306    #[inline]
307    pub fn set_enabled(&mut self, enabled: bool) {
308        #[cfg(feature = "experimental")]
309        {
310            self.is_enabled.store(enabled, Ordering::Relaxed);
311        }
312        #[cfg(not(feature = "experimental"))]
313        {
314            self.is_enabled = enabled;
315        }
316    }
317
318    /// Updates the peak level with an adaptive attack coefficient
319    ///
320    /// This method adjusts the peak level using a variable attack coefficient.
321    /// It responds faster to sudden increases in signal level by using a
322    /// minimum attack coefficient of `min_attack_coeff` when the sample value exceeds the
323    /// current peak level. This adaptive behavior helps capture transients
324    /// more accurately while maintaining smoother behavior for gradual changes.
325    #[inline]
326    fn update_peak_level(&mut self, sample_value: f32) {
327        let attack_coeff = if sample_value > self.peak_level {
328            self.attack_coeff().min(self.min_attack_coeff) // User-defined attack time limited via release_time
329        } else {
330            self.release_coeff()
331        };
332
333        self.peak_level = attack_coeff * self.peak_level + (1.0 - attack_coeff) * sample_value;
334    }
335
336    /// Updates the RMS (Root Mean Square) level using a circular buffer approach.
337    /// This method calculates a moving average of the squared input samples,
338    /// providing a measure of the signal's average power over time.
339    #[inline]
340    fn update_rms(&mut self, sample_value: f32) -> f32 {
341        let squared_sample = sample_value * sample_value;
342        self.rms_window.push(squared_sample);
343        self.rms_window.mean().sqrt()
344    }
345
346    /// Calculate gain adjustments based on peak levels
347    /// This method determines the appropriate gain level to apply to the audio
348    /// signal, considering the peak level.
349    /// The peak level helps prevent sudden spikes in the output signal.
350    #[inline]
351    fn calculate_peak_gain(&self) -> f32 {
352        if self.peak_level > 0.0 {
353            (self.target_level() / self.peak_level).min(self.absolute_max_gain())
354        } else {
355            self.absolute_max_gain()
356        }
357    }
358
359    #[inline]
360    fn process_sample(&mut self, sample: I::Item) -> I::Item {
361        // Convert the sample to its absolute float value for level calculations
362        let sample_value = sample.abs();
363
364        // Dynamically adjust peak level using an adaptive attack coefficient
365        self.update_peak_level(sample_value);
366
367        // Calculate the current RMS (Root Mean Square) level using a sliding window approach
368        let rms = self.update_rms(sample_value);
369
370        // Compute the gain adjustment required to reach the target level based on RMS
371        let rms_gain = if rms > 0.0 {
372            self.target_level() / rms
373        } else {
374            self.absolute_max_gain() // Default to max gain if RMS is zero
375        };
376
377        // Calculate the peak limiting gain
378        let peak_gain = self.calculate_peak_gain();
379
380        // Use RMS for general adjustments, but limit by peak gain to prevent clipping
381        let desired_gain = rms_gain.min(peak_gain);
382
383        // Adaptive attack/release speed for AGC (Automatic Gain Control)
384        //
385        // This mechanism implements an asymmetric approach to gain adjustment:
386        // 1. **Slow increase**: Prevents abrupt amplification of noise during quiet periods.
387        // 2. **Fast decrease**: Rapidly attenuates sudden loud signals to avoid distortion.
388        //
389        // The asymmetry is crucial because:
390        // - Gradual gain increases sound more natural and less noticeable to listeners.
391        // - Quick gain reductions are necessary to prevent clipping and maintain audio quality.
392        //
393        // This approach addresses several challenges associated with high attack times:
394        // 1. **Slow response**: With a high attack time, the AGC responds very slowly to changes in input level.
395        //    This means it takes longer for the gain to adjust to new signal levels.
396        // 2. **Initial gain calculation**: When the audio starts or after a period of silence, the initial gain
397        //    calculation might result in a very high gain value, especially if the input signal starts quietly.
398        // 3. **Overshooting**: As the gain slowly increases (due to the high attack time), it might overshoot
399        //    the desired level, causing the signal to become too loud.
400        // 4. **Overcorrection**: The AGC then tries to correct this by reducing the gain, but due to the slow response,
401        //    it might reduce the gain too much, causing the sound to drop to near-zero levels.
402        // 5. **Slow recovery**: Again, due to the high attack time, it takes a while for the gain to increase
403        //    back to the appropriate level.
404        //
405        // By using a faster release time for decreasing gain, we can mitigate these issues and provide
406        // more responsive control over sudden level increases while maintaining smooth gain increases.
407        let attack_speed = if desired_gain > self.current_gain {
408            self.attack_coeff()
409        } else {
410            self.release_coeff()
411        };
412
413        // Gradually adjust the current gain towards the desired gain for smooth transitions
414        self.current_gain = self.current_gain * attack_speed + desired_gain * (1.0 - attack_speed);
415
416        // Ensure the calculated gain stays within the defined operational range
417        self.current_gain = self.current_gain.clamp(0.1, self.absolute_max_gain());
418
419        // Output current gain value for developers to fine tune their inputs to automatic_gain_control
420        #[cfg(feature = "tracing")]
421        tracing::debug!("AGC gain: {}", self.current_gain,);
422
423        // Apply the computed gain to the input sample and return the result
424        sample * self.current_gain
425    }
426
427    /// Returns a mutable reference to the inner source.
428    pub fn inner(&self) -> &I {
429        &self.input
430    }
431
432    /// Returns the inner source.
433    pub fn inner_mut(&mut self) -> &mut I {
434        &mut self.input
435    }
436}
437
438impl<I> Iterator for AutomaticGainControl<I>
439where
440    I: Source,
441{
442    type Item = I::Item;
443
444    #[inline]
445    fn next(&mut self) -> Option<Self::Item> {
446        self.input.next().map(|sample| {
447            if self.is_enabled() {
448                self.process_sample(sample)
449            } else {
450                sample
451            }
452        })
453    }
454
455    #[inline]
456    fn size_hint(&self) -> (usize, Option<usize>) {
457        self.input.size_hint()
458    }
459}
460
461impl<I> ExactSizeIterator for AutomaticGainControl<I> where I: Source + ExactSizeIterator {}
462
463impl<I> Source for AutomaticGainControl<I>
464where
465    I: Source,
466{
467    #[inline]
468    fn current_span_len(&self) -> Option<usize> {
469        self.input.current_span_len()
470    }
471
472    #[inline]
473    fn channels(&self) -> ChannelCount {
474        self.input.channels()
475    }
476
477    #[inline]
478    fn sample_rate(&self) -> SampleRate {
479        self.input.sample_rate()
480    }
481
482    #[inline]
483    fn total_duration(&self) -> Option<Duration> {
484        self.input.total_duration()
485    }
486
487    #[inline]
488    fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
489        self.input.try_seek(pos)
490    }
491}