1use alpha2_time::{MediaTime, TICKS_PER_SECOND};
2
3use crate::{ProcessCtx, Processor, StateBlob, StateError};
4
5#[derive(Copy, Clone, Debug, Eq, PartialEq)]
7pub enum FadeDirection {
8 In,
10 Out,
12}
13
14#[derive(Copy, Clone, Debug, Eq, PartialEq)]
16pub struct LinearFade {
17 start: MediaTime,
18 end: MediaTime,
19 direction: FadeDirection,
20}
21
22impl LinearFade {
23 #[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}