Skip to main content

koan_core/audio/
fade.rs

1use std::sync::Arc;
2use std::sync::atomic::{AtomicBool, Ordering};
3
4/// How long pause takes to fall silent, and resume to come back.
5///
6/// Long enough that a pause lands as a fade rather than a cut, short enough
7/// that it still reads as the key having been pressed.
8const FADE_SECONDS: f64 = 0.15;
9
10/// What the player asks of a fade. Shared with the render callback, so atomics
11/// only.
12#[derive(Default)]
13pub struct FadeControl {
14    /// Where the gain is heading: full when set, silence when not.
15    audible: AtomicBool,
16    /// Set when the unit is started from stopped, so the first callback begins
17    /// the ramp at silence rather than wherever the gain was left.
18    from_silence: AtomicBool,
19    /// Written by the callback once a fade out has reached silence. The unit
20    /// can then be stopped without cutting anything off.
21    silent: AtomicBool,
22}
23
24impl FadeControl {
25    pub fn new() -> Arc<Self> {
26        Arc::new(Self {
27            audible: AtomicBool::new(true),
28            ..Default::default()
29        })
30    }
31
32    pub fn fade_out(&self) {
33        self.audible.store(false, Ordering::Release);
34    }
35
36    /// Ramp back up. `from_silence` when the unit is about to be started from
37    /// stopped, where the callback has not been running to bring the gain down.
38    pub fn fade_in(&self, from_silence: bool) {
39        self.silent.store(false, Ordering::Release);
40        if from_silence {
41            self.from_silence.store(true, Ordering::Release);
42        }
43        self.audible.store(true, Ordering::Release);
44    }
45
46    pub fn is_silent(&self) -> bool {
47        self.silent.load(Ordering::Acquire)
48    }
49}
50
51/// The render callback's half of a fade: the gain itself, which only the
52/// callback touches.
53///
54/// At full level samples pass through untouched, so outside a fade the output
55/// stays bit-perfect.
56pub struct Fader {
57    control: Arc<FadeControl>,
58    /// Frames into the ramp, from 0 (silent) to `len` (full). Counted in whole
59    /// frames so a fade out ends on an exact frame.
60    pos: usize,
61    len: usize,
62}
63
64impl Fader {
65    pub fn new(control: Arc<FadeControl>, sample_rate: f64) -> Self {
66        let len = ((sample_rate * FADE_SECONDS) as usize).max(1);
67        Self {
68            control,
69            pos: len,
70            len,
71        }
72    }
73
74    /// How many of `wanted` interleaved samples to take from the ring.
75    ///
76    /// A fade out stops reading exactly where it reaches silence, so the play
77    /// head rests on the last sample heard rather than on whatever was
78    /// consumed and thrown away.
79    pub fn readable(&mut self, wanted: usize, channels: usize) -> usize {
80        if self.control.from_silence.swap(false, Ordering::AcqRel) {
81            self.pos = 0;
82        }
83        if self.control.audible.load(Ordering::Acquire) {
84            return wanted;
85        }
86        if self.pos == 0 {
87            self.control.silent.store(true, Ordering::Release);
88        }
89        wanted.min(self.pos * channels.max(1))
90    }
91
92    /// A callback that plays silence in place of the ring. A fade out has
93    /// nothing audible to ramp down, so it is done at once; left pending, it
94    /// would ramp through the start of the track when the silence ended.
95    pub fn hold(&mut self) {
96        if !self.control.audible.load(Ordering::Acquire) {
97            self.pos = 0;
98            self.control.silent.store(true, Ordering::Release);
99        }
100    }
101
102    /// Apply the ramp to interleaved samples just read from the ring.
103    pub fn apply(&mut self, samples: &mut [f32], channels: usize) {
104        let rising = self.control.audible.load(Ordering::Acquire);
105        if (rising && self.pos == self.len) || (!rising && self.pos == 0) {
106            return;
107        }
108        for frame in samples.chunks_mut(channels.max(1)) {
109            self.pos = if rising {
110                (self.pos + 1).min(self.len)
111            } else {
112                self.pos.saturating_sub(1)
113            };
114            // Squared, so the ear hears an even fade rather than one that
115            // hangs loud and then drops away.
116            let level = self.pos as f32 / self.len as f32;
117            let gain = level * level;
118            for s in frame {
119                *s *= gain;
120            }
121        }
122        if !rising && self.pos == 0 {
123            self.control.silent.store(true, Ordering::Release);
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    const RATE: f64 = 1000.0; // 150 frames per fade
133
134    fn render(fader: &mut Fader, frames: usize) -> Vec<f32> {
135        let wanted = fader.readable(frames * 2, 2);
136        let mut out = vec![1.0f32; wanted];
137        fader.apply(&mut out, 2);
138        out
139    }
140
141    #[test]
142    fn a_fade_out_over_silence_is_silent_at_once() {
143        let control = FadeControl::new();
144        let mut fader = Fader::new(control.clone(), RATE);
145        fader.hold();
146        assert!(!control.is_silent(), "holding at full level fades nothing");
147
148        control.fade_out();
149        fader.hold();
150        assert!(control.is_silent());
151        assert_eq!(
152            fader.readable(64, 2),
153            0,
154            "nothing of the track plays after the pause"
155        );
156    }
157
158    #[test]
159    fn full_level_passes_samples_through() {
160        let control = FadeControl::new();
161        let mut fader = Fader::new(control, RATE);
162        let out = render(&mut fader, 64);
163        assert_eq!(out.len(), 128);
164        assert!(out.iter().all(|s| *s == 1.0));
165    }
166
167    #[test]
168    fn fade_out_stops_reading_at_silence() {
169        let control = FadeControl::new();
170        let mut fader = Fader::new(control.clone(), RATE);
171        control.fade_out();
172
173        let first = render(&mut fader, 100);
174        assert_eq!(first.len(), 200);
175        assert!(first.windows(2).all(|w| w[1] <= w[0]), "gain only falls");
176        assert!(!control.is_silent());
177
178        let second = render(&mut fader, 100);
179        assert_eq!(second.len(), 100, "only the rest of the ramp is read");
180        assert_eq!(*second.last().unwrap(), 0.0);
181        assert!(control.is_silent());
182
183        assert!(
184            render(&mut fader, 100).is_empty(),
185            "nothing read once silent"
186        );
187    }
188
189    #[test]
190    fn fade_in_from_stopped_starts_at_silence() {
191        let control = FadeControl::new();
192        let mut fader = Fader::new(control.clone(), RATE);
193        control.fade_in(true);
194
195        let out = render(&mut fader, 200);
196        assert!(out[0] < 0.01);
197        assert!(out.windows(2).all(|w| w[1] >= w[0]), "gain only rises");
198        assert_eq!(*out.last().unwrap(), 1.0);
199    }
200
201    #[test]
202    fn resume_mid_fade_turns_around() {
203        let control = FadeControl::new();
204        let mut fader = Fader::new(control.clone(), RATE);
205        control.fade_out();
206        let down = render(&mut fader, 50);
207        control.fade_in(false);
208        let up = render(&mut fader, 10);
209        assert!(up[0] > *down.last().unwrap(), "rises from where it was");
210        assert!(!control.is_silent());
211    }
212}