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}