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    /// This avoids losing fractional tick positions at sample rates that do not
51    /// divide [`alpha2_time::TICKS_PER_SECOND`].
52    pub sample_position: i64,
53    /// Project sample rate. Resampling belongs at the host boundary.
54    pub sample_rate: u32,
55    /// Current transport state.
56    pub transport: TransportState,
57}
58
59impl ProcessCtx {
60    /// The context of the audio `samples` later on the timeline (earlier,
61    /// for a negative count): the sample position moved that far, and the
62    /// position recomputed from it at the context's rate, rounded down to a
63    /// tick. Zero leaves the context exactly as it is.
64    #[must_use]
65    pub fn offset_by(&self, samples: i64) -> Self {
66        if samples == 0 {
67            return *self;
68        }
69        let sample_position = self.sample_position.saturating_add(samples);
70        let ticks = (i128::from(sample_position) * i128::from(alpha2_time::TICKS_PER_SECOND))
71            .div_euclid(i128::from(self.sample_rate.max(1)));
72        let ticks = i64::try_from(ticks).unwrap_or(if ticks < 0 { i64::MIN } else { i64::MAX });
73        Self {
74            position: MediaTime::from_ticks(ticks),
75            sample_position,
76            ..*self
77        }
78    }
79}
80
81/// Opaque processor state saved outside the real-time process path.
82#[derive(Clone, Debug, Default, Eq, PartialEq)]
83pub struct StateBlob(Vec<u8>);
84
85impl StateBlob {
86    /// Creates an opaque state value from owned bytes.
87    #[must_use]
88    pub fn new(bytes: Vec<u8>) -> Self {
89        Self(bytes)
90    }
91
92    /// Returns the serialized bytes.
93    #[must_use]
94    pub fn as_bytes(&self) -> &[u8] {
95        &self.0
96    }
97}
98
99/// Why a processor, a [`crate::ProcessorChain`] or an
100/// [`crate::AudioGraph`] refuses a saved state. A refused state changes
101/// nothing: what refuses it is left as it was.
102#[derive(Copy, Clone, Debug, Eq, PartialEq)]
103#[non_exhaustive]
104pub enum StateError {
105    /// The state does not fit: saved by another kind of processor, by one
106    /// prepared otherwise (another rate, lookahead or layout, so its history
107    /// has another length), by a chain of other nodes, or cut short.
108    Mismatch,
109    /// The state fits but holds what the processor never keeps: a cursor
110    /// past its history, or a value that is not finite.
111    Invalid,
112}
113
114impl core::fmt::Display for StateError {
115    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
116        formatter.write_str(match self {
117            Self::Mismatch => "the saved state was made by a processor prepared otherwise",
118            Self::Invalid => "the saved state holds a value the processor never keeps",
119        })
120    }
121}
122
123impl std::error::Error for StateError {}
124
125/// Stateful DSP unit whose process method performs no allocation or locking.
126pub trait Processor: Send {
127    /// Configures non-real-time state before processing begins.
128    fn prepare(&mut self, sample_rate: u32, max_block: usize);
129
130    /// Processes planar channel slices supplied by the host.
131    ///
132    /// A prepared processor that reads one input per output
133    /// ([`Self::inputs_per_output`]) writes every sample of each output it
134    /// has an input for, up to the shortest of them: a
135    /// [`crate::ProcessorChain`] hands that output on as the next node's
136    /// input.
137    fn process(&mut self, ctx: &ProcessCtx, inputs: &[&[f32]], outputs: &mut [&mut [f32]]);
138
139    /// How many input channels the processor reads for each output channel:
140    /// 1 for an effect, 2 for one that also reads a second bus after the
141    /// first (a sidechain, or the other side of a crossfade). A
142    /// [`crate::ProcessorChain`] gives each node only its own channels, so
143    /// it takes only processors that read one.
144    fn inputs_per_output(&self) -> usize {
145        1
146    }
147
148    /// Clears signal history without changing authored parameters.
149    fn reset(&mut self);
150
151    /// Algorithmic delay that the graph host must compensate.
152    fn latency_samples(&self) -> u32 {
153        0
154    }
155
156    /// Additional source history for a cold seek, excluding algorithmic delay.
157    /// Built-in recursive processors use a finite settling horizon; this is an
158    /// approximation to uninterrupted playback, not a bit-exact state snapshot.
159    fn history_samples(&self) -> u32 {
160        0
161    }
162
163    /// Serializes history at an offline chunk boundary.
164    fn save_state(&self) -> StateBlob;
165
166    /// Restores history saved by a processor of the same type, prepared
167    /// alike, whole or not at all.
168    ///
169    /// A state the processor saved itself always loads back: a
170    /// [`crate::ProcessorChain`] relies on that to undo the nodes it has
171    /// loaded when a later one refuses its part.
172    ///
173    /// # Errors
174    /// Returns [`StateError`], and leaves the processor as it was, for a
175    /// state that does not fit it or holds a value it never keeps.
176    fn load_state(&mut self, state: &StateBlob) -> Result<(), StateError>;
177}
178
179/// Thirty-two exponential time constants leave e^-32 of an initial state in
180/// exact arithmetic. Floating-point rounding and cascaded gain remain separate.
181#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
182pub(crate) fn settling_samples(coefficient: f64) -> u32 {
183    if coefficient <= 0.0 {
184        0
185    } else if coefficient >= 1.0 || !coefficient.is_finite() {
186        u32::MAX
187    } else {
188        libm::ceil(-32.0 / libm::log(coefficient)) as u32
189    }
190}