Skip to main content

tympan_aspl/
stream.rs

1//! Stream abstraction.
2//!
3//! A *stream* is one direction of audio flow on a device — an
4//! AudioServerPlugin device owns one input stream, one output
5//! stream, or both. Each stream carries its own [`StreamFormat`]
6//! and occupies a contiguous range of channels within the device.
7//!
8//! This module is cross-platform plain data; the FFI that surfaces
9//! a [`StreamSpec`] to the HAL as an `kAudioStreamClassID` object
10//! lands in the `raw` module in a later PR.
11
12use crate::format::StreamFormat;
13use crate::property::PropertyScope;
14
15/// The direction a stream carries audio.
16#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
17pub enum StreamDirection {
18    /// A capture stream — audio flows from the device into the
19    /// system (a virtual microphone).
20    Input,
21    /// A playback stream — audio flows from the system into the
22    /// device (a virtual speaker).
23    Output,
24}
25
26impl StreamDirection {
27    /// The `kAudioStreamPropertyDirection` value the HAL expects:
28    /// `1` for input, `0` for output.
29    #[inline]
30    #[must_use]
31    pub const fn as_property_value(self) -> u32 {
32        match self {
33            Self::Input => 1,
34            Self::Output => 0,
35        }
36    }
37
38    /// Reconstruct a direction from a `kAudioStreamPropertyDirection`
39    /// value. Any non-zero value is treated as input, matching the
40    /// HAL's boolean interpretation.
41    #[inline]
42    #[must_use]
43    pub const fn from_property_value(value: u32) -> Self {
44        if value == 0 {
45            Self::Output
46        } else {
47            Self::Input
48        }
49    }
50
51    /// The [`PropertyScope`] a property access on this stream's
52    /// direction is keyed to.
53    #[inline]
54    #[must_use]
55    pub const fn scope(self) -> PropertyScope {
56        match self {
57            Self::Input => PropertyScope::INPUT,
58            Self::Output => PropertyScope::OUTPUT,
59        }
60    }
61
62    /// The opposite direction.
63    #[inline]
64    #[must_use]
65    pub const fn opposite(self) -> Self {
66        match self {
67            Self::Input => Self::Output,
68            Self::Output => Self::Input,
69        }
70    }
71}
72
73/// A declarative description of one stream on a device.
74///
75/// The framework turns a [`StreamSpec`] into an
76/// `kAudioStreamClassID` audio object, answers the HAL's property
77/// queries about it (direction, starting channel, virtual format),
78/// and routes the stream's samples into
79/// [`Driver::process_io`](crate::Driver::process_io). Build one with
80/// [`StreamSpec::input`] / [`StreamSpec::output`] and the
81/// builder-style setters.
82#[derive(Copy, Clone, PartialEq, Debug)]
83pub struct StreamSpec {
84    direction: StreamDirection,
85    starting_channel: u32,
86    format: StreamFormat,
87}
88
89impl StreamSpec {
90    /// An input stream carrying `format`, starting at channel `1`.
91    #[inline]
92    #[must_use]
93    pub const fn input(format: StreamFormat) -> Self {
94        Self {
95            direction: StreamDirection::Input,
96            starting_channel: 1,
97            format,
98        }
99    }
100
101    /// An output stream carrying `format`, starting at channel `1`.
102    #[inline]
103    #[must_use]
104    pub const fn output(format: StreamFormat) -> Self {
105        Self {
106            direction: StreamDirection::Output,
107            starting_channel: 1,
108            format,
109        }
110    }
111
112    /// Builder-style: set the 1-based channel this stream's first
113    /// channel maps to within the owning device.
114    #[inline]
115    #[must_use]
116    pub const fn with_starting_channel(mut self, channel: u32) -> Self {
117        self.starting_channel = channel;
118        self
119    }
120
121    /// Builder-style: replace the stream format.
122    #[inline]
123    #[must_use]
124    pub const fn with_format(mut self, format: StreamFormat) -> Self {
125        self.format = format;
126        self
127    }
128
129    /// The direction this stream carries audio.
130    #[inline]
131    #[must_use]
132    pub const fn direction(&self) -> StreamDirection {
133        self.direction
134    }
135
136    /// The 1-based starting channel
137    /// (`kAudioStreamPropertyStartingChannel`).
138    #[inline]
139    #[must_use]
140    pub const fn starting_channel(&self) -> u32 {
141        self.starting_channel
142    }
143
144    /// The stream's [`StreamFormat`]
145    /// (`kAudioStreamPropertyVirtualFormat`).
146    #[inline]
147    #[must_use]
148    pub const fn format(&self) -> StreamFormat {
149        self.format
150    }
151
152    /// The stream's channel count, taken from its format.
153    #[inline]
154    #[must_use]
155    pub const fn channels(&self) -> u32 {
156        self.format.channels()
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn direction_property_values_match_core_audio() {
166        assert_eq!(StreamDirection::Input.as_property_value(), 1);
167        assert_eq!(StreamDirection::Output.as_property_value(), 0);
168    }
169
170    #[test]
171    fn direction_round_trips_through_property_value() {
172        assert_eq!(
173            StreamDirection::from_property_value(1),
174            StreamDirection::Input
175        );
176        assert_eq!(
177            StreamDirection::from_property_value(0),
178            StreamDirection::Output
179        );
180        // Any non-zero value reads back as input.
181        assert_eq!(
182            StreamDirection::from_property_value(99),
183            StreamDirection::Input
184        );
185    }
186
187    #[test]
188    fn direction_scopes_and_opposites() {
189        assert_eq!(StreamDirection::Input.scope(), PropertyScope::INPUT);
190        assert_eq!(StreamDirection::Output.scope(), PropertyScope::OUTPUT);
191        assert_eq!(StreamDirection::Input.opposite(), StreamDirection::Output);
192        assert_eq!(StreamDirection::Output.opposite(), StreamDirection::Input);
193    }
194
195    #[test]
196    fn input_stream_defaults() {
197        let s = StreamSpec::input(StreamFormat::float32(48_000.0, 2));
198        assert_eq!(s.direction(), StreamDirection::Input);
199        assert_eq!(s.starting_channel(), 1);
200        assert_eq!(s.channels(), 2);
201        assert!(s.format().is_canonical());
202    }
203
204    #[test]
205    fn output_stream_defaults() {
206        let s = StreamSpec::output(StreamFormat::float32(44_100.0, 1));
207        assert_eq!(s.direction(), StreamDirection::Output);
208        assert_eq!(s.starting_channel(), 1);
209        assert_eq!(s.channels(), 1);
210    }
211
212    #[test]
213    fn builder_setters_apply() {
214        let s = StreamSpec::output(StreamFormat::float32(48_000.0, 2))
215            .with_starting_channel(3)
216            .with_format(StreamFormat::float32(96_000.0, 4));
217        assert_eq!(s.starting_channel(), 3);
218        assert_eq!(s.format().sample_rate(), 96_000.0);
219        assert_eq!(s.channels(), 4);
220    }
221}