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}