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}