Skip to main content

koan_core/audio/
fade.rs

1use std::sync::Arc;
2use std::sync::atomic::{AtomicBool, AtomicU32, 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/// How long a quick fade takes: the dip a DSP change makes, out of the old
11/// processing and into the new. Long enough that neither edge is a step,
12/// short enough to pass for the change itself.
13pub const QUICK_FADE: std::time::Duration = std::time::Duration::from_millis(15);
14
15/// How long the callback takes to glide to a new sleep gain: the player sets
16/// one about this often while a sleep timer fades, so the level moves on
17/// without a step.
18const SLEEP_GLIDE_SECONDS: f64 = 0.1;
19
20/// What the player asks of a fade. Shared with the render callback, so atomics
21/// only.
22#[derive(Default)]
23pub struct FadeControl {
24    /// Where the gain is heading: full when set, silence when not.
25    audible: AtomicBool,
26    /// Set when the unit is started from stopped, so the first callback begins
27    /// the ramp at silence rather than wherever the gain was left.
28    from_silence: AtomicBool,
29    /// Written by the callback once a fade out has reached silence. The unit
30    /// can then be stopped without cutting anything off.
31    silent: AtomicBool,
32    /// A sleep timer's gain, as `f32` bits: 1.0 but while one fades. Applied
33    /// on top of the pause ramp.
34    sleep_gain: AtomicU32,
35    /// Take `sleep_gain` at once rather than gliding to it.
36    sleep_snap: AtomicBool,
37    /// `playback.muted`: every sample zeroed after the ramps.
38    muted: AtomicBool,
39    /// Ramp over `QUICK_FADE` rather than `FADE_SECONDS`. Set by the
40    /// quick fades, cleared by the others.
41    quick: AtomicBool,
42    /// How long one render callback asks for, in microseconds: how late after
43    /// a fade begins the callback may first see it.
44    period_us: AtomicU32,
45}
46
47impl FadeControl {
48    pub fn new() -> Arc<Self> {
49        Arc::new(Self {
50            audible: AtomicBool::new(true),
51            sleep_gain: AtomicU32::new(1.0f32.to_bits()),
52            ..Default::default()
53        })
54    }
55
56    /// A fade for an output unit, muted when `playback.muted` says so.
57    pub fn for_output() -> Arc<Self> {
58        let control = Self::new();
59        if crate::config::Config::cached().playback.muted {
60            log::info!("audio: muted (playback.muted)");
61            control.muted.store(true, Ordering::Release);
62        }
63        control
64    }
65
66    pub fn fade_out(&self) {
67        self.quick.store(false, Ordering::Release);
68        self.audible.store(false, Ordering::Release);
69    }
70
71    /// `fade_out` over `QUICK_FADE`.
72    pub fn fade_out_quickly(&self) {
73        self.quick.store(true, Ordering::Release);
74        self.audible.store(false, Ordering::Release);
75    }
76
77    /// Ramp back up. `from_silence` when the unit is about to be started from
78    /// stopped, where the callback has not been running to bring the gain down.
79    pub fn fade_in(&self, from_silence: bool) {
80        self.quick.store(false, Ordering::Release);
81        self.rise(from_silence);
82    }
83
84    /// `fade_in` over `QUICK_FADE`.
85    pub fn fade_in_quickly(&self, from_silence: bool) {
86        self.quick.store(true, Ordering::Release);
87        self.rise(from_silence);
88    }
89
90    fn rise(&self, from_silence: bool) {
91        self.silent.store(false, Ordering::Release);
92        if from_silence {
93            self.from_silence.store(true, Ordering::Release);
94        }
95        self.audible.store(true, Ordering::Release);
96    }
97
98    pub fn is_silent(&self) -> bool {
99        self.silent.load(Ordering::Acquire)
100    }
101
102    /// A fade out asked for and no fade in since, whether or not the callback
103    /// got as far as silence.
104    pub fn is_faded_out(&self) -> bool {
105        !self.audible.load(Ordering::Acquire)
106    }
107
108    /// One render callback's length, as the callback last asked for it.
109    /// Zero until the first callback.
110    pub fn period(&self) -> std::time::Duration {
111        std::time::Duration::from_micros(self.period_us.load(Ordering::Relaxed) as u64)
112    }
113
114    /// The sleep timer's gain, glided to over `SLEEP_GLIDE_SECONDS`, or with
115    /// `snap` taken at once.
116    pub fn set_sleep_gain(&self, gain: f32, snap: bool) {
117        if snap {
118            self.sleep_snap.store(true, Ordering::Release);
119        }
120        self.sleep_gain
121            .store(gain.clamp(0.0, 1.0).to_bits(), Ordering::Release);
122    }
123}
124
125/// The render callback's half of a fade: the gain itself, which only the
126/// callback touches.
127///
128/// At full level samples pass through untouched, so outside a fade the output
129/// stays bit-perfect.
130pub struct Fader {
131    control: Arc<FadeControl>,
132    /// Frames into the ramp, from 0 (silent) to `len` (full). Counted in whole
133    /// frames so a fade out ends on an exact frame.
134    pos: usize,
135    len: usize,
136    /// How far a quick fade moves `pos` each frame.
137    quick_step: usize,
138    sample_rate: f64,
139    /// The sleep gain where the callback has it, the target it was last
140    /// given, and how far it moves each frame on the way there.
141    sleep: f32,
142    sleep_target: u32,
143    sleep_step: f32,
144    glide: f32,
145}
146
147impl Fader {
148    pub fn new(control: Arc<FadeControl>, sample_rate: f64) -> Self {
149        let len = ((sample_rate * FADE_SECONDS) as usize).max(1);
150        let quick = ((sample_rate * QUICK_FADE.as_secs_f64()) as usize).max(1);
151        Self {
152            control,
153            pos: len,
154            len,
155            quick_step: len.div_ceil(quick),
156            sample_rate,
157            sleep: 1.0,
158            sleep_target: 1.0f32.to_bits(),
159            sleep_step: 0.0,
160            glide: ((sample_rate * SLEEP_GLIDE_SECONDS) as f32).max(1.0),
161        }
162    }
163
164    /// How many of `wanted` interleaved samples to take from the ring.
165    ///
166    /// A fade out stops reading exactly where it reaches silence, so the play
167    /// head rests on the last sample heard rather than on whatever was
168    /// consumed and thrown away.
169    pub fn readable(&mut self, wanted: usize, channels: usize) -> usize {
170        let frames = wanted / channels.max(1);
171        if frames > 0 {
172            let us = (frames as f64 * 1e6 / self.sample_rate) as u32;
173            self.control.period_us.store(us, Ordering::Relaxed);
174        }
175        if self.control.from_silence.swap(false, Ordering::AcqRel) {
176            self.pos = 0;
177        }
178        if self.control.audible.load(Ordering::Acquire) {
179            return wanted;
180        }
181        if self.pos == 0 {
182            self.control.silent.store(true, Ordering::Release);
183        }
184        wanted.min(self.pos.div_ceil(self.step()) * channels.max(1))
185    }
186
187    /// How far `pos` moves each frame of a ramp.
188    fn step(&self) -> usize {
189        if self.control.quick.load(Ordering::Relaxed) {
190            self.quick_step
191        } else {
192            1
193        }
194    }
195
196    /// A callback that plays silence in place of the ring. A fade out has
197    /// nothing audible to ramp down, so it is done at once; left pending, it
198    /// would ramp through the start of the track when the silence ended.
199    pub fn hold(&mut self) {
200        if !self.control.audible.load(Ordering::Acquire) {
201            self.pos = 0;
202            self.control.silent.store(true, Ordering::Release);
203        }
204    }
205
206    /// Take up a sleep gain the player has set since the last callback.
207    fn follow_sleep(&mut self) {
208        let bits = self.control.sleep_gain.load(Ordering::Acquire);
209        let snap = self.control.sleep_snap.swap(false, Ordering::AcqRel);
210        if bits == self.sleep_target && !snap {
211            return;
212        }
213        self.sleep_target = bits;
214        let target = f32::from_bits(bits);
215        if snap {
216            self.sleep = target;
217            self.sleep_step = 0.0;
218        } else {
219            self.sleep_step = (target - self.sleep) / self.glide;
220        }
221    }
222
223    /// Apply the ramps to interleaved samples just read from the ring.
224    pub fn apply(&mut self, samples: &mut [f32], channels: usize) {
225        self.ramp(samples, channels);
226        if self.control.muted.load(Ordering::Relaxed) {
227            samples.fill(0.0);
228        }
229    }
230
231    fn ramp(&mut self, samples: &mut [f32], channels: usize) {
232        self.follow_sleep();
233        let rising = self.control.audible.load(Ordering::Acquire);
234        let pausing = !((rising && self.pos == self.len) || (!rising && self.pos == 0));
235        let sleeping = self.sleep != 1.0 || self.sleep_step != 0.0;
236        if !pausing && !sleeping {
237            return;
238        }
239        let target = f32::from_bits(self.sleep_target);
240        let step = self.step();
241        for frame in samples.chunks_mut(channels.max(1)) {
242            if pausing {
243                self.pos = if rising {
244                    (self.pos + step).min(self.len)
245                } else {
246                    self.pos.saturating_sub(step)
247                };
248            }
249            if self.sleep_step != 0.0 {
250                self.sleep += self.sleep_step;
251                if (self.sleep_step > 0.0 && self.sleep >= target)
252                    || (self.sleep_step < 0.0 && self.sleep <= target)
253                {
254                    self.sleep = target;
255                    self.sleep_step = 0.0;
256                }
257            }
258            // Squared, so the ear hears an even fade rather than one that
259            // hangs loud and then drops away.
260            let level = self.pos as f32 / self.len as f32;
261            let gain = level * level * self.sleep;
262            for s in frame {
263                *s *= gain;
264            }
265        }
266        if !rising && self.pos == 0 {
267            self.control.silent.store(true, Ordering::Release);
268        }
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    const RATE: f64 = 1000.0; // 150 frames per fade
277
278    fn render(fader: &mut Fader, frames: usize) -> Vec<f32> {
279        let wanted = fader.readable(frames * 2, 2);
280        let mut out = vec![1.0f32; wanted];
281        fader.apply(&mut out, 2);
282        out
283    }
284
285    #[test]
286    fn a_fade_out_over_silence_is_silent_at_once() {
287        let control = FadeControl::new();
288        let mut fader = Fader::new(control.clone(), RATE);
289        fader.hold();
290        assert!(!control.is_silent(), "holding at full level fades nothing");
291
292        control.fade_out();
293        fader.hold();
294        assert!(control.is_silent());
295        assert_eq!(
296            fader.readable(64, 2),
297            0,
298            "nothing of the track plays after the pause"
299        );
300    }
301
302    #[test]
303    fn full_level_passes_samples_through() {
304        let control = FadeControl::new();
305        let mut fader = Fader::new(control, RATE);
306        let out = render(&mut fader, 64);
307        assert_eq!(out.len(), 128);
308        assert!(out.iter().all(|s| *s == 1.0));
309    }
310
311    #[test]
312    fn muted_reads_on_and_plays_silence() {
313        let control = FadeControl::new();
314        control.muted.store(true, Ordering::Release);
315        let mut fader = Fader::new(control, RATE);
316        let out = render(&mut fader, 64);
317        assert_eq!(out.len(), 128, "the track moves on as if heard");
318        assert!(out.iter().all(|s| *s == 0.0));
319    }
320
321    #[test]
322    fn fade_out_stops_reading_at_silence() {
323        let control = FadeControl::new();
324        let mut fader = Fader::new(control.clone(), RATE);
325        control.fade_out();
326
327        let first = render(&mut fader, 100);
328        assert_eq!(first.len(), 200);
329        assert!(first.windows(2).all(|w| w[1] <= w[0]), "gain only falls");
330        assert!(!control.is_silent());
331
332        let second = render(&mut fader, 100);
333        assert_eq!(second.len(), 100, "only the rest of the ramp is read");
334        assert_eq!(*second.last().unwrap(), 0.0);
335        assert!(control.is_silent());
336
337        assert!(
338            render(&mut fader, 100).is_empty(),
339            "nothing read once silent"
340        );
341    }
342
343    #[test]
344    fn fade_in_from_stopped_starts_at_silence() {
345        let control = FadeControl::new();
346        let mut fader = Fader::new(control.clone(), RATE);
347        control.fade_in(true);
348
349        let out = render(&mut fader, 200);
350        assert!(out[0] < 0.01);
351        assert!(out.windows(2).all(|w| w[1] >= w[0]), "gain only rises");
352        assert_eq!(*out.last().unwrap(), 1.0);
353    }
354
355    /// A sleep gain is glided to, not stepped; full level is left alone,
356    /// which keeps the output bit-perfect outside a sleep timer's fade.
357    #[test]
358    fn a_sleep_gain_glides_and_full_level_is_untouched() {
359        let control = FadeControl::new();
360        let mut fader = Fader::new(control.clone(), RATE);
361        assert!(render(&mut fader, 10).iter().all(|s| *s == 1.0));
362
363        control.set_sleep_gain(0.5, false);
364        let glide = render(&mut fader, 100); // the whole glide, 0.1 s
365        assert!(glide.windows(2).all(|w| w[1] <= w[0]), "only falls");
366        assert!(glide[0] < 1.0 && glide[0] > 0.99, "no step: {}", glide[0]);
367        assert!((glide.last().unwrap() - 0.5).abs() < 1e-4);
368        assert!(
369            render(&mut fader, 10)
370                .iter()
371                .all(|s| (*s - 0.5).abs() < 1e-6)
372        );
373
374        control.set_sleep_gain(1.0, true);
375        assert!(
376            render(&mut fader, 10).iter().all(|s| *s == 1.0),
377            "snapped back"
378        );
379    }
380
381    #[test]
382    fn resume_mid_fade_turns_around() {
383        let control = FadeControl::new();
384        let mut fader = Fader::new(control.clone(), RATE);
385        control.fade_out();
386        let down = render(&mut fader, 50);
387        control.fade_in(false);
388        let up = render(&mut fader, 10);
389        assert!(up[0] > *down.last().unwrap(), "rises from where it was");
390        assert!(!control.is_silent());
391    }
392
393    /// A DSP change: the old processing fades out quickly, stops reading at
394    /// silence, and a new session fades in from silence. Stitched together,
395    /// at a rate where the fades are real lengths, no sample jumps from the
396    /// one before by more than a full-scale 1 kHz sine moves on its own, and
397    /// the whole dip lasts twice `QUICK_FADE`.
398    #[test]
399    fn a_quick_fade_out_and_in_has_no_step() {
400        let rate = 48000.0;
401        let sine = |phase: f64, n: usize| {
402            (0..n)
403                .map(|i| (phase + std::f64::consts::TAU * 1000.0 * i as f64 / rate).sin() as f32)
404                .collect::<Vec<f32>>()
405        };
406        // One render callback: as much of `source` from `at` as the fader
407        // reads, ramped.
408        let callback = |fader: &mut Fader, source: &[f32], at: &mut usize, frames: usize| {
409            let take = fader.readable(frames, 1);
410            let mut out = source[*at..*at + take].to_vec();
411            *at += take;
412            fader.apply(&mut out, 1);
413            out
414        };
415
416        let old = FadeControl::new();
417        let mut fader = Fader::new(old.clone(), rate);
418        let playing = sine(0.0, 48000);
419        let mut at = 0;
420        // 492 frames: a quarter cycle past a whole one, so the fade starts at
421        // the peak, where a cut would be a step of 1.
422        let mut heard = callback(&mut fader, &playing, &mut at, 492);
423        old.fade_out_quickly();
424        let mut tail = Vec::new();
425        for _ in 0..20 {
426            tail.extend(callback(&mut fader, &playing, &mut at, 512));
427            if old.is_silent() {
428                break;
429            }
430        }
431        assert!(old.is_silent());
432        let quick = (rate * QUICK_FADE.as_secs_f64()) as usize;
433        assert!(tail.len() <= quick + 1, "faded in {} frames", tail.len());
434        heard.extend(tail);
435
436        let new = FadeControl::new();
437        let mut fader = Fader::new(new.clone(), rate);
438        new.fade_in_quickly(true);
439        let next = sine(2.9, 2048);
440        heard.extend(callback(&mut fader, &next, &mut 0, 2048));
441
442        // The ramp's own slope adds a little where the sine is near its peak.
443        let natural = (std::f64::consts::TAU * 1000.0 / rate) as f32 * 1.05;
444        let worst = heard
445            .windows(2)
446            .map(|w| (w[1] - w[0]).abs())
447            .fold(0.0, f32::max);
448        assert!(
449            worst <= natural,
450            "a step of {worst}, more than the sine's own {natural}"
451        );
452
453        // Quick fades are for DSP changes only: a pause after one takes the
454        // ordinary length.
455        new.fade_out();
456        let pause = callback(&mut fader, &playing, &mut 0, 48000);
457        assert_eq!(pause.len(), (rate * FADE_SECONDS) as usize);
458    }
459
460    /// The callback's length, which the player waits on beyond a quick fade,
461    /// as the buffers a Mac and PipeWire use by default ask for it.
462    #[test]
463    fn the_callback_reports_its_period() {
464        for (frames, rate) in [(512usize, 44100.0), (1024, 48000.0)] {
465            let control = FadeControl::new();
466            let mut fader = Fader::new(control.clone(), rate);
467            assert_eq!(control.period(), std::time::Duration::ZERO);
468            fader.readable(frames * 2, 2);
469            let want = frames as f64 / rate;
470            assert!(
471                (control.period().as_secs_f64() - want).abs() < 2e-6,
472                "{frames} frames"
473            );
474        }
475    }
476}