Skip to main content

alpha2_audio/
processor.rs

1use alpha2_time::MediaTime;
2
3/// Validated channel organization used at every host boundary.
4#[derive(Copy, Clone, Debug, Eq, PartialEq)]
5pub enum ChannelLayout {
6    /// One centered channel.
7    Mono,
8    /// Left and right channels.
9    Stereo,
10}
11
12impl ChannelLayout {
13    /// Validates and converts a channel count.
14    #[must_use]
15    pub const fn from_channels(channels: usize) -> Option<Self> {
16        match channels {
17            1 => Some(Self::Mono),
18            2 => Some(Self::Stereo),
19            _ => None,
20        }
21    }
22
23    /// Returns the number of planar channels.
24    #[must_use]
25    pub const fn channels(self) -> usize {
26        match self {
27            Self::Mono => 1,
28            Self::Stereo => 2,
29        }
30    }
31}
32
33/// Current host transport state supplied without consulting a platform clock.
34#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
35pub enum TransportState {
36    /// Processing is paused or stopped.
37    #[default]
38    Stopped,
39    /// Timeline time advances with generated samples.
40    Playing,
41}
42
43/// Immutable information for one DSP invocation.
44#[derive(Copy, Clone, Debug, Eq, PartialEq)]
45pub struct ProcessCtx {
46    /// Timeline position of the first sample in this block.
47    pub position: MediaTime,
48    /// Exact project-rate sample index of the first sample in this block.
49    ///
50    /// Exact even at rates that do not divide [`alpha2_time::TICKS_PER_SECOND`].
51    pub sample_position: i64,
52    /// Project sample rate. Resampling belongs at the host boundary.
53    pub sample_rate: u32,
54    /// Current transport state.
55    pub transport: TransportState,
56}
57
58impl ProcessCtx {
59    /// This context moved by `samples` (negative moves earlier), with `position` recomputed and
60    /// rounded down to a tick. Zero leaves it unchanged.
61    #[must_use]
62    pub fn offset_by(&self, samples: i64) -> Self {
63        if samples == 0 {
64            return *self;
65        }
66        let sample_position = self.sample_position.saturating_add(samples);
67        let ticks = (i128::from(sample_position) * i128::from(alpha2_time::TICKS_PER_SECOND))
68            .div_euclid(i128::from(self.sample_rate.max(1)));
69        let ticks = i64::try_from(ticks).unwrap_or(if ticks < 0 { i64::MIN } else { i64::MAX });
70        Self {
71            position: MediaTime::from_ticks(ticks),
72            sample_position,
73            ..*self
74        }
75    }
76}
77
78/// Opaque processor state saved outside the real-time process path.
79#[derive(Clone, Debug, Default, Eq, PartialEq)]
80pub struct StateBlob(Vec<u8>);
81
82impl StateBlob {
83    /// Creates an opaque state value from owned bytes.
84    #[must_use]
85    pub fn new(bytes: Vec<u8>) -> Self {
86        Self(bytes)
87    }
88
89    /// Returns the serialized bytes.
90    #[must_use]
91    pub fn as_bytes(&self) -> &[u8] {
92        &self.0
93    }
94}
95
96/// Why a processor, [`crate::ProcessorChain`] or [`crate::AudioGraph`] refuses a saved state.
97/// A refused state changes nothing.
98#[derive(Copy, Clone, Debug, Eq, PartialEq)]
99#[non_exhaustive]
100pub enum StateError {
101    /// The state was saved by a different processor, preparation or chain, or is truncated.
102    Mismatch,
103    /// The state fits but holds impossible values, such as an out-of-range cursor or a
104    /// non-finite value.
105    Invalid,
106}
107
108impl core::fmt::Display for StateError {
109    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
110        formatter.write_str(match self {
111            Self::Mismatch => "the saved state was made by a processor prepared otherwise",
112            Self::Invalid => "the saved state holds a value the processor never keeps",
113        })
114    }
115}
116
117impl std::error::Error for StateError {}
118
119/// Stateful DSP unit whose process method performs no allocation or locking.
120pub trait Processor: Send {
121    /// Configures non-real-time state before processing begins.
122    fn prepare(&mut self, sample_rate: u32, max_block: usize);
123
124    /// Processes planar channel slices supplied by the host.
125    ///
126    /// A processor with one input per output writes every sample of each output that has an
127    /// input, up to the shortest, since a [`crate::ProcessorChain`] feeds it to the next node.
128    fn process(&mut self, ctx: &ProcessCtx, inputs: &[&[f32]], outputs: &mut [&mut [f32]]);
129
130    /// Input channels read per output channel: 1 for an effect, 2 with a second bus (sidechain or
131    /// crossfade). A [`crate::ProcessorChain`] accepts only processors that read one.
132    fn inputs_per_output(&self) -> usize {
133        1
134    }
135
136    /// Clears signal history without changing authored parameters.
137    fn reset(&mut self);
138
139    /// Algorithmic delay that the graph host must compensate.
140    fn latency_samples(&self) -> u32 {
141        0
142    }
143
144    /// Additional source history for a cold seek, excluding algorithmic delay. For recursive
145    /// processors this approximates uninterrupted playback rather than matching it bit for bit.
146    fn history_samples(&self) -> u32 {
147        0
148    }
149
150    /// Serializes history at an offline chunk boundary.
151    fn save_state(&self) -> StateBlob;
152
153    /// Restores history saved by a processor of the same type and preparation, atomically.
154    ///
155    /// A processor's own saved state must always load, since [`crate::ProcessorChain`] relies on
156    /// it to roll back.
157    ///
158    /// # Errors
159    /// Returns [`StateError`], leaving the processor unchanged.
160    fn load_state(&mut self, state: &StateBlob) -> Result<(), StateError>;
161}
162
163/// Thirty-two exponential time constants leave e^-32 of an initial state in
164/// exact arithmetic. Floating-point rounding and cascaded gain remain separate.
165#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
166pub(crate) fn settling_samples(coefficient: f64) -> u32 {
167    if coefficient <= 0.0 {
168        0
169    } else if coefficient >= 1.0 || !coefficient.is_finite() {
170        u32::MAX
171    } else {
172        libm::ceil(-32.0 / libm::log(coefficient)) as u32
173    }
174}