Skip to main content

rustriff_lib/services/processors/gain/
gain_processor.rs

1use crate::domain::audio_processor::AudioProcessor;
2use atomic_float::AtomicF32;
3use std::sync::atomic::Ordering;
4use std::sync::Arc;
5use tracing::info;
6
7/// An audio processor that applies gain with smooth transitions.
8///
9/// `GainProcessor` implements the [`AudioProcessor`] trait and applies a gain factor
10/// to audio samples. When the gain value changes, it smoothly transitions from the
11/// current value to the new target using a simple one-pole low-pass filter, preventing
12/// audible clicks and pops.
13///
14/// The gain value is read from an [`Arc<AtomicF32>`] that can be safely updated
15/// from other threads (e.g., the UI thread) without requiring locks.
16pub struct GainProcessor {
17    gain: Arc<AtomicF32>,
18    current: f32,
19}
20
21impl GainProcessor {
22    /// Creates a new `GainProcessor` with the given atomic gain value.
23    ///
24    /// Initializes the internal smoothing state (`current`) to the initial gain value
25    /// loaded from the atomic.
26    ///
27    /// # Arguments
28    ///
29    /// * `gain` - An [`Arc<AtomicF32>`] that holds the target gain value.
30    ///   This value can be updated from other threads, and changes
31    ///   will be smoothly transitioned.
32    pub fn new(gain: Arc<AtomicF32>) -> Self {
33        info!("initi gain processor");
34        let initial = gain.load(Ordering::Relaxed);
35        Self {
36            gain,
37            current: initial,
38        }
39    }
40}
41
42impl AudioProcessor for GainProcessor {
43    #[cfg_attr(doc, aquamarine::aquamarine)]
44    /// Processes a single audio sample with the current gain factor.
45    ///
46    /// Reads the target gain value from the atomic and smoothly transitions the
47    /// internal state toward it using a one-pole smoothing algorithm.
48    ///
49    /// ### Gain Smoothing Visualization
50    /// The red line shows the instantaneous jump (Target), while the curve
51    /// shows the gradual adjustment of the multiplier (Current).
52    ///
53    /// ```mermaid
54    /// xychart-beta
55    ///     title "Instantaneous Jump vs. One-Pole Smoothing"
56    ///     x-axis "Time (Samples)" [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
57    ///     y-axis "Gain Factor" 0 --> 1.2
58    ///     line [0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
59    ///     line [0.0, 0.4, 0.64, 0.78, 0.87, 0.92, 0.95, 0.97, 0.98, 0.99, 1.0]
60    /// ```
61    ///
62    /// # Arguments
63    ///
64    /// * `sample` - The input audio sample to be scaled by the gain.
65    ///
66    /// # Returns
67    ///
68    /// The gain-scaled audio sample.
69    fn process(&mut self, sample: f32) -> f32 {
70        let target = self.gain.load(Ordering::Relaxed);
71
72        // Simple one-pole smoothing
73        self.current += (target - self.current) * 0.001;
74
75        sample * self.current
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use atomic_float::AtomicF32;
83    use std::sync::atomic::Ordering;
84    use std::sync::Arc;
85
86    fn make_processor(initial_gain: f32) -> GainProcessor {
87        let gain = Arc::new(AtomicF32::new(initial_gain));
88        GainProcessor::new(gain)
89    }
90
91    #[cfg(test)]
92    mod success_path {
93        use super::*;
94        #[test]
95        fn transition_to_target_should_be_smooth_up() {
96            let gain = Arc::new(AtomicF32::new(0.0));
97            let mut processor = GainProcessor::new(gain.clone());
98
99            gain.store(1.0, Ordering::Relaxed);
100
101            for _ in 0..5_000 {
102                processor.process(1.0);
103            }
104
105            assert!(processor.current > 0.9);
106            assert!(processor.current < 1.0);
107        }
108
109        #[test]
110        fn transition_to_target_should_be_smooth_down() {
111            let gain = Arc::new(AtomicF32::new(1.0));
112            let mut processor = GainProcessor::new(gain.clone());
113
114            gain.store(0.0, Ordering::Relaxed);
115
116            for _ in 0..5_000 {
117                processor.process(1.0);
118            }
119
120            assert!(processor.current < 0.1);
121            assert!(processor.current > 0.0);
122        }
123
124        #[test]
125        fn steady_state_does_not_change_value() {
126            let mut processor = make_processor(1.0);
127
128            for _ in 0..1_000 {
129                processor.process(1.0);
130            }
131
132            assert!((processor.current - 1.0).abs() < 1e-6);
133        }
134
135        #[test]
136        fn output_is_scaled_by_current_gain() {
137            let gain = Arc::new(AtomicF32::new(1.0));
138            let mut processor = GainProcessor::new(gain.clone());
139
140            gain.store(0.5, Ordering::Relaxed);
141
142            for _ in 0..1_000 {
143                processor.process(1.0);
144            }
145
146            let output = processor.process(1.0);
147
148            assert!((output - processor.current).abs() < 1e-6);
149        }
150    }
151    //This part of the code does not have a failure path
152}