Skip to main content

alpha2_audio/
fade.rs

1use alpha2_time::{MediaTime, TICKS_PER_SECOND};
2
3use crate::{ProcessCtx, Processor, StateBlob, StateError};
4
5/// Direction of a linear amplitude fade.
6#[derive(Copy, Clone, Debug, Eq, PartialEq)]
7pub enum FadeDirection {
8    /// Silence to unity.
9    In,
10    /// Unity to silence.
11    Out,
12}
13
14/// Sample-addressed linear fade independent of host block size.
15#[derive(Copy, Clone, Debug, Eq, PartialEq)]
16pub struct LinearFade {
17    start: MediaTime,
18    end: MediaTime,
19    direction: FadeDirection,
20}
21
22impl LinearFade {
23    /// Creates a fade over a non-empty half-open timeline range.
24    #[must_use]
25    pub fn new(start: MediaTime, end: MediaTime, direction: FadeDirection) -> Option<Self> {
26        (start < end).then_some(Self {
27            start,
28            end,
29            direction,
30        })
31    }
32
33    fn gain_at(self, time: MediaTime) -> f32 {
34        let numerator = time.ticks().saturating_sub(self.start.ticks());
35        let denominator = self.end.ticks().saturating_sub(self.start.ticks());
36        #[allow(clippy::cast_precision_loss)]
37        let progress = (numerator as f32 / denominator as f32).clamp(0.0, 1.0);
38        match self.direction {
39            FadeDirection::In => progress,
40            FadeDirection::Out => 1.0 - progress,
41        }
42    }
43}
44
45impl Processor for LinearFade {
46    fn prepare(&mut self, _sample_rate: u32, _max_block: usize) {}
47
48    fn process(&mut self, ctx: &ProcessCtx, inputs: &[&[f32]], outputs: &mut [&mut [f32]]) {
49        if ctx.sample_rate == 0 {
50            return;
51        }
52        for channel in 0..inputs.len().min(outputs.len()) {
53            for (index, (source, target)) in inputs[channel]
54                .iter()
55                .zip(outputs[channel].iter_mut())
56                .enumerate()
57            {
58                let Ok(index) = i64::try_from(index) else {
59                    *target = 0.0;
60                    continue;
61                };
62                let sample = ctx.sample_position.saturating_add(index);
63                let ticks = sample
64                    .saturating_mul(TICKS_PER_SECOND)
65                    .div_euclid(i64::from(ctx.sample_rate));
66                *target = *source * self.gain_at(MediaTime(ticks));
67            }
68        }
69    }
70
71    fn reset(&mut self) {}
72
73    fn save_state(&self) -> StateBlob {
74        StateBlob::new(Vec::new())
75    }
76
77    fn load_state(&mut self, state: &StateBlob) -> Result<(), StateError> {
78        if state.as_bytes().is_empty() {
79            Ok(())
80        } else {
81            Err(StateError::Mismatch)
82        }
83    }
84}