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    /// Apply the ramp to interleaved samples just read from the ring.
93    pub fn apply(&mut self, samples: &mut [f32], channels: usize) {
94        let rising = self.control.audible.load(Ordering::Acquire);
95        if (rising && self.pos == self.len) || (!rising && self.pos == 0) {
96            return;
97        }
98        for frame in samples.chunks_mut(channels.max(1)) {
99            self.pos = if rising {
100                (self.pos + 1).min(self.len)
101            } else {
102                self.pos.saturating_sub(1)
103            };
104            // Squared, so the ear hears an even fade rather than one that
105            // hangs loud and then drops away.
106            let level = self.pos as f32 / self.len as f32;
107            let gain = level * level;
108            for s in frame {
109                *s *= gain;
110            }
111        }
112        if !rising && self.pos == 0 {
113            self.control.silent.store(true, Ordering::Release);
114        }
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    const RATE: f64 = 1000.0; // 150 frames per fade
123
124    fn render(fader: &mut Fader, frames: usize) -> Vec<f32> {
125        let wanted = fader.readable(frames * 2, 2);
126        let mut out = vec![1.0f32; wanted];
127        fader.apply(&mut out, 2);
128        out
129    }
130
131    #[test]
132    fn full_level_passes_samples_through() {
133        let control = FadeControl::new();
134        let mut fader = Fader::new(control, RATE);
135        let out = render(&mut fader, 64);
136        assert_eq!(out.len(), 128);
137        assert!(out.iter().all(|s| *s == 1.0));
138    }
139
140    #[test]
141    fn fade_out_stops_reading_at_silence() {
142        let control = FadeControl::new();
143        let mut fader = Fader::new(control.clone(), RATE);
144        control.fade_out();
145
146        let first = render(&mut fader, 100);
147        assert_eq!(first.len(), 200);
148        assert!(first.windows(2).all(|w| w[1] <= w[0]), "gain only falls");
149        assert!(!control.is_silent());
150
151        let second = render(&mut fader, 100);
152        assert_eq!(second.len(), 100, "only the rest of the ramp is read");
153        assert_eq!(*second.last().unwrap(), 0.0);
154        assert!(control.is_silent());
155
156        assert!(
157            render(&mut fader, 100).is_empty(),
158            "nothing read once silent"
159        );
160    }
161
162    #[test]
163    fn fade_in_from_stopped_starts_at_silence() {
164        let control = FadeControl::new();
165        let mut fader = Fader::new(control.clone(), RATE);
166        control.fade_in(true);
167
168        let out = render(&mut fader, 200);
169        assert!(out[0] < 0.01);
170        assert!(out.windows(2).all(|w| w[1] >= w[0]), "gain only rises");
171        assert_eq!(*out.last().unwrap(), 1.0);
172    }
173
174    #[test]
175    fn resume_mid_fade_turns_around() {
176        let control = FadeControl::new();
177        let mut fader = Fader::new(control.clone(), RATE);
178        control.fade_out();
179        let down = render(&mut fader, 50);
180        control.fade_in(false);
181        let up = render(&mut fader, 10);
182        assert!(up[0] > *down.last().unwrap(), "rises from where it was");
183        assert!(!control.is_silent());
184    }
185}