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