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}