oddio/
gain.rs

1use alloc::sync::Arc;
2use core::sync::atomic::{AtomicU32, Ordering};
3
4use crate::{frame, math::Float, Frame, Seek, Signal, Smoothed};
5
6/// Amplifies a signal by a constant amount
7///
8/// Unlike [`Gain`], this can implement [`Seek`].
9pub struct FixedGain<T: ?Sized> {
10    gain: f32,
11    inner: T,
12}
13
14impl<T> FixedGain<T> {
15    /// Amplify `signal` by `db` decibels
16    ///
17    /// Decibels are perceptually linear. Negative values make the signal quieter.
18    pub fn new(signal: T, db: f32) -> Self {
19        Self {
20            gain: 10.0f32.powf(db / 20.0),
21            inner: signal,
22        }
23    }
24}
25
26impl<T: Signal + ?Sized> Signal for FixedGain<T>
27where
28    T::Frame: Frame,
29{
30    type Frame = T::Frame;
31
32    fn sample(&mut self, interval: f32, out: &mut [T::Frame]) {
33        self.inner.sample(interval, out);
34        for x in out {
35            *x = frame::scale(x, self.gain);
36        }
37    }
38
39    fn is_finished(&self) -> bool {
40        self.inner.is_finished()
41    }
42}
43
44impl<T: Seek + ?Sized> Seek for FixedGain<T>
45where
46    T::Frame: Frame,
47{
48    fn seek(&mut self, seconds: f32) {
49        self.inner.seek(seconds)
50    }
51}
52
53/// Amplifies a signal dynamically
54///
55/// To implement a volume control, place a gain combinator near the end of your pipeline where the
56/// input amplitude is initially in the range [0, 1] and pass decibels to [`GainControl::set_gain`],
57/// mapping the maximum volume to 0 decibels, and the minimum to e.g. -60.
58pub struct Gain<T: ?Sized> {
59    shared: Arc<AtomicU32>,
60    gain: Smoothed<f32>,
61    inner: T,
62}
63
64impl<T> Gain<T> {
65    /// Apply dynamic amplification to `signal`
66    pub fn new(signal: T) -> (GainControl, Self) {
67        let signal = Gain {
68            shared: Arc::new(AtomicU32::new(1.0f32.to_bits())),
69            gain: Smoothed::new(1.0),
70            inner: signal,
71        };
72        let handle = GainControl(signal.shared.clone());
73        (handle, signal)
74    }
75
76    /// Set the initial amplification to `db` decibels
77    ///
78    /// Perceptually linear. Negative values make the signal quieter.
79    ///
80    /// Equivalent to `self.set_amplitude_ratio(10.0f32.powf(db / 20.0))`.
81    pub fn set_gain(&mut self, db: f32) {
82        self.set_amplitude_ratio(10.0f32.powf(db / 20.0));
83    }
84
85    /// Set the initial amplitude scaling of the signal directly
86    ///
87    /// This is nonlinear in terms of both perception and power. Most users should prefer
88    /// `set_gain`. Unlike `set_gain`, this method allows a signal to be completely zeroed out if
89    /// needed, or even have its phase inverted with a negative factor.
90    pub fn set_amplitude_ratio(&mut self, factor: f32) {
91        self.shared.store(factor.to_bits(), Ordering::Relaxed);
92        self.gain = Smoothed::new(factor);
93    }
94}
95
96impl<T: Signal> Signal for Gain<T>
97where
98    T::Frame: Frame,
99{
100    type Frame = T::Frame;
101
102    #[allow(clippy::float_cmp)]
103    fn sample(&mut self, interval: f32, out: &mut [T::Frame]) {
104        self.inner.sample(interval, out);
105        let shared = f32::from_bits(self.shared.load(Ordering::Relaxed));
106        if self.gain.target() != &shared {
107            self.gain.set(shared);
108        }
109        if self.gain.progress() == 1.0 {
110            let g = self.gain.get();
111            if g != 1.0 {
112                for x in out {
113                    *x = frame::scale(x, g);
114                }
115            }
116            return;
117        }
118        for x in out {
119            *x = frame::scale(x, self.gain.get());
120            self.gain.advance(interval / SMOOTHING_PERIOD);
121        }
122    }
123
124    fn is_finished(&self) -> bool {
125        self.inner.is_finished()
126    }
127}
128
129/// Thread-safe control for a [`Gain`] filter
130pub struct GainControl(Arc<AtomicU32>);
131
132impl GainControl {
133    /// Get the current amplification in decibels
134    pub fn gain(&self) -> f32 {
135        20.0 * self.amplitude_ratio().log10()
136    }
137
138    /// Amplify the signal by `db` decibels
139    ///
140    /// Perceptually linear. Negative values make the signal quieter.
141    ///
142    /// Equivalent to `self.set_amplitude_ratio(10.0f32.powf(db / 20.0))`.
143    pub fn set_gain(&mut self, db: f32) {
144        self.set_amplitude_ratio(10.0f32.powf(db / 20.0));
145    }
146
147    /// Get the current amplitude scaling factor
148    pub fn amplitude_ratio(&self) -> f32 {
149        f32::from_bits(self.0.load(Ordering::Relaxed))
150    }
151
152    /// Scale the amplitude of the signal directly
153    ///
154    /// This is nonlinear in terms of both perception and power. Most users should prefer
155    /// `set_gain`. Unlike `set_gain`, this method allows a signal to be completely zeroed out if
156    /// needed, or even have its phase inverted with a negative factor.
157    pub fn set_amplitude_ratio(&mut self, factor: f32) {
158        self.0.store(factor.to_bits(), Ordering::Relaxed);
159    }
160}
161
162/// Number of seconds over which to smooth a change in gain
163const SMOOTHING_PERIOD: f32 = 0.1;
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::Constant;
169
170    #[test]
171    fn smoothing() {
172        let (mut c, mut s) = Gain::new(Constant(1.0));
173        let mut buf = [0.0; 6];
174        c.set_amplitude_ratio(5.0);
175        s.sample(0.025, &mut buf);
176        assert_eq!(buf, [1.0, 2.0, 3.0, 4.0, 5.0, 5.0]);
177        s.sample(0.025, &mut buf);
178        assert_eq!(buf, [5.0; 6]);
179    }
180}