Skip to main content

alpha2_audio/
automation.rs

1/// One sample-addressed automation point.
2#[derive(Copy, Clone, Debug, PartialEq)]
3pub struct AutomationPoint {
4    /// Absolute project-rate sample index.
5    pub sample: i64,
6    /// Finite parameter value.
7    pub value: f32,
8}
9
10/// Sample-accurate gain driven by an immutable automation curve.
11#[derive(Clone, Debug)]
12pub struct AutomatedGain {
13    curve: AutomationCurve,
14    fallback: f32,
15}
16
17impl AutomatedGain {
18    /// Creates an automated gain processor.
19    ///
20    /// `fallback` is the gain used when the curve has no points; a non-finite fallback becomes
21    /// silence.
22    #[must_use]
23    pub fn new(curve: AutomationCurve, fallback: f32) -> Self {
24        Self {
25            curve,
26            fallback: if fallback.is_finite() { fallback } else { 0.0 },
27        }
28    }
29}
30
31impl Processor for AutomatedGain {
32    fn prepare(&mut self, _sample_rate: u32, _max_block: usize) {}
33    fn process(&mut self, ctx: &ProcessCtx, inputs: &[&[f32]], outputs: &mut [&mut [f32]]) {
34        for channel in 0..inputs.len().min(outputs.len()) {
35            for (index, (&input, output)) in inputs[channel]
36                .iter()
37                .zip(outputs[channel].iter_mut())
38                .enumerate()
39            {
40                let sample = ctx
41                    .sample_position
42                    .saturating_add(i64::try_from(index).unwrap_or(i64::MAX));
43                *output = input * self.curve.value_at(sample, self.fallback);
44            }
45        }
46    }
47    fn reset(&mut self) {}
48    fn save_state(&self) -> StateBlob {
49        StateBlob::default()
50    }
51    fn load_state(&mut self, state: &StateBlob) -> Result<(), StateError> {
52        if state.as_bytes().is_empty() {
53            Ok(())
54        } else {
55            Err(StateError::Mismatch)
56        }
57    }
58}
59
60/// Immutable piecewise-linear automation prepared off the realtime thread.
61#[derive(Clone, Debug, Default, PartialEq)]
62pub struct AutomationCurve {
63    points: Vec<AutomationPoint>,
64}
65
66impl AutomationCurve {
67    /// Creates a sorted curve, rejecting duplicate positions and non-finite values.
68    ///
69    /// # Errors
70    /// Returns a stable message for non-finite values or duplicate positions.
71    pub fn new(mut points: Vec<AutomationPoint>) -> Result<Self, &'static str> {
72        if points.iter().any(|point| !point.value.is_finite()) {
73            return Err("automation values must be finite");
74        }
75        points.sort_by_key(|point| point.sample);
76        if points
77            .windows(2)
78            .any(|pair| pair[0].sample == pair[1].sample)
79        {
80            return Err("automation positions must be unique");
81        }
82        Ok(Self { points })
83    }
84
85    /// Evaluates the curve at `sample` without allocating.
86    ///
87    /// Interpolates linearly between points and holds the first and last values outside them.
88    /// Returns `fallback` only when the curve is empty.
89    #[must_use]
90    pub fn value_at(&self, sample: i64, fallback: f32) -> f32 {
91        let index = self.points.partition_point(|point| point.sample <= sample);
92        match index {
93            0 => self.points.first().map_or(fallback, |point| point.value),
94            index if index == self.points.len() => self.points[index - 1].value,
95            index => {
96                let left = self.points[index - 1];
97                let right = self.points[index];
98                #[allow(clippy::cast_precision_loss)]
99                let t = (i128::from(sample) - i128::from(left.sample)) as f64
100                    / (i128::from(right.sample) - i128::from(left.sample)) as f64;
101                #[allow(clippy::cast_possible_truncation)]
102                let value = (f64::from(left.value)
103                    + (f64::from(right.value) - f64::from(left.value)) * t)
104                    as f32;
105                value
106            }
107        }
108    }
109}
110use crate::{ProcessCtx, Processor, StateBlob, StateError};