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