Skip to main content

tympan_aspl/
device.rs

1//! Device abstraction.
2//!
3//! A *device* is the audio endpoint an AudioServerPlugin exposes to
4//! the system — what shows up in System Settings ▸ Sound. A device
5//! owns one or more [`StreamSpec`]s (its input and/or output) and
6//! carries the metadata the HAL surfaces through the device
7//! property protocol: the UID, the human-readable name, the nominal
8//! sample rate.
9//!
10//! This module is cross-platform plain data. A [`DeviceSpec`] is the
11//! declarative description a [`Driver`](crate::Driver) hands the
12//! framework; the framework assigns it a [`DeviceId`] and answers
13//! the HAL's property queries against it.
14
15use crate::object::AudioObjectId;
16use crate::stream::{StreamDirection, StreamSpec};
17
18/// A device's identifier within the plug-in's object tree.
19///
20/// A thin newtype over an [`AudioObjectId`]. The framework mints one
21/// per device when it builds the object tree from the driver's
22/// [`DeviceSpec`]s.
23#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
24#[repr(transparent)]
25pub struct DeviceId(pub AudioObjectId);
26
27impl DeviceId {
28    /// Wrap a raw `AudioObjectID`.
29    #[inline]
30    #[must_use]
31    pub const fn from_u32(value: u32) -> Self {
32        Self(AudioObjectId::from_u32(value))
33    }
34
35    /// The underlying [`AudioObjectId`].
36    #[inline]
37    #[must_use]
38    pub const fn object_id(self) -> AudioObjectId {
39        self.0
40    }
41
42    /// The raw `u32`, ready for the FFI boundary.
43    #[inline]
44    #[must_use]
45    pub const fn as_u32(self) -> u32 {
46        self.0.as_u32()
47    }
48}
49
50impl From<AudioObjectId> for DeviceId {
51    #[inline]
52    fn from(value: AudioObjectId) -> Self {
53        Self(value)
54    }
55}
56
57/// A declarative description of one device a driver exposes.
58///
59/// The framework turns a `DeviceSpec` into a `kAudioDeviceClassID`
60/// audio object, answers the HAL's property queries about it (UID,
61/// name, sample rate, stream list), and routes the device's IO into
62/// [`Driver::process_io`](crate::Driver::process_io).
63///
64/// Build one with [`DeviceSpec::new`] and the builder-style stream
65/// setters. A device with only an output stream is a virtual
66/// speaker; with only an input stream, a virtual microphone; with
67/// both, a loopback device.
68#[derive(Copy, Clone, PartialEq, Debug)]
69pub struct DeviceSpec {
70    uid: &'static str,
71    name: &'static str,
72    manufacturer: &'static str,
73    sample_rate: f64,
74    input: Option<StreamSpec>,
75    output: Option<StreamSpec>,
76}
77
78impl DeviceSpec {
79    /// Begin a device description with the mandatory identity
80    /// fields. The device starts with no streams; add them with
81    /// [`Self::with_input`] / [`Self::with_output`].
82    ///
83    /// - `uid` is the stable, globally-unique device identifier
84    ///   (`kAudioDevicePropertyDeviceUID`). It must not change
85    ///   across launches — the system keeps per-device settings
86    ///   keyed on it.
87    /// - `name` is the human-readable name shown in the Sound
88    ///   settings UI (`kAudioObjectPropertyName`).
89    /// - `manufacturer` is shown alongside the name
90    ///   (`kAudioObjectPropertyManufacturer`).
91    #[inline]
92    #[must_use]
93    pub const fn new(uid: &'static str, name: &'static str, manufacturer: &'static str) -> Self {
94        Self {
95            uid,
96            name,
97            manufacturer,
98            sample_rate: 48_000.0,
99            input: None,
100            output: None,
101        }
102    }
103
104    /// Builder-style: set the nominal sample rate
105    /// (`kAudioDevicePropertyNominalSampleRate`). Defaults to
106    /// 48 000 Hz.
107    #[inline]
108    #[must_use]
109    pub const fn with_sample_rate(mut self, sample_rate: f64) -> Self {
110        self.sample_rate = sample_rate;
111        self
112    }
113
114    /// Builder-style: attach an input stream. Replaces any
115    /// previously-set input stream.
116    #[inline]
117    #[must_use]
118    pub const fn with_input(mut self, stream: StreamSpec) -> Self {
119        self.input = Some(stream);
120        self
121    }
122
123    /// Builder-style: attach an output stream. Replaces any
124    /// previously-set output stream.
125    #[inline]
126    #[must_use]
127    pub const fn with_output(mut self, stream: StreamSpec) -> Self {
128        self.output = Some(stream);
129        self
130    }
131
132    /// The stable device UID.
133    #[inline]
134    #[must_use]
135    pub const fn uid(&self) -> &'static str {
136        self.uid
137    }
138
139    /// The human-readable device name.
140    #[inline]
141    #[must_use]
142    pub const fn name(&self) -> &'static str {
143        self.name
144    }
145
146    /// The manufacturer string.
147    #[inline]
148    #[must_use]
149    pub const fn manufacturer(&self) -> &'static str {
150        self.manufacturer
151    }
152
153    /// The nominal sample rate, in hertz.
154    #[inline]
155    #[must_use]
156    pub const fn sample_rate(&self) -> f64 {
157        self.sample_rate
158    }
159
160    /// The input stream, if this device has one.
161    #[inline]
162    #[must_use]
163    pub const fn input(&self) -> Option<StreamSpec> {
164        self.input
165    }
166
167    /// The output stream, if this device has one.
168    #[inline]
169    #[must_use]
170    pub const fn output(&self) -> Option<StreamSpec> {
171        self.output
172    }
173
174    /// The stream for `direction`, if this device has one.
175    #[inline]
176    #[must_use]
177    pub const fn stream(&self, direction: StreamDirection) -> Option<StreamSpec> {
178        match direction {
179            StreamDirection::Input => self.input,
180            StreamDirection::Output => self.output,
181        }
182    }
183
184    /// Number of streams on this device (`0`, `1`, or `2`).
185    #[inline]
186    #[must_use]
187    pub const fn stream_count(&self) -> usize {
188        self.input.is_some() as usize + self.output.is_some() as usize
189    }
190
191    /// `true` iff this device has both an input and an output
192    /// stream — i.e. it is a loopback device.
193    #[inline]
194    #[must_use]
195    pub const fn is_loopback(&self) -> bool {
196        self.input.is_some() && self.output.is_some()
197    }
198}
199
200/// A device the framework has admitted into the object tree.
201///
202/// Pairs the [`DeviceId`] the framework assigned with the
203/// [`DeviceSpec`] the driver declared. The framework constructs
204/// these; drivers describe their devices with [`DeviceSpec`] and
205/// never build a `Device` directly.
206#[derive(Copy, Clone, PartialEq, Debug)]
207pub struct Device {
208    id: DeviceId,
209    spec: DeviceSpec,
210}
211
212impl Device {
213    /// Pair an id with a spec. Called by the framework when it
214    /// builds the object tree.
215    #[inline]
216    #[must_use]
217    pub const fn new(id: DeviceId, spec: DeviceSpec) -> Self {
218        Self { id, spec }
219    }
220
221    /// The framework-assigned device id.
222    #[inline]
223    #[must_use]
224    pub const fn id(&self) -> DeviceId {
225        self.id
226    }
227
228    /// The declarative spec the driver supplied.
229    #[inline]
230    #[must_use]
231    pub const fn spec(&self) -> &DeviceSpec {
232        &self.spec
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use crate::format::StreamFormat;
240
241    fn loopback_spec() -> DeviceSpec {
242        DeviceSpec::new("com.example.loopback", "Example Loopback", "tympan-aspl")
243            .with_sample_rate(48_000.0)
244            .with_input(StreamSpec::input(StreamFormat::float32(48_000.0, 2)))
245            .with_output(StreamSpec::output(StreamFormat::float32(48_000.0, 2)))
246    }
247
248    #[test]
249    fn device_id_round_trips() {
250        let id = DeviceId::from_u32(7);
251        assert_eq!(id.as_u32(), 7);
252        assert_eq!(id.object_id(), AudioObjectId::from_u32(7));
253        assert_eq!(DeviceId::from(AudioObjectId::from_u32(7)), id);
254    }
255
256    #[test]
257    fn new_device_starts_streamless_at_48k() {
258        let spec = DeviceSpec::new("uid", "name", "maker");
259        assert_eq!(spec.sample_rate(), 48_000.0);
260        assert_eq!(spec.stream_count(), 0);
261        assert!(spec.input().is_none());
262        assert!(spec.output().is_none());
263        assert!(!spec.is_loopback());
264    }
265
266    #[test]
267    fn identity_fields_round_trip() {
268        let spec = loopback_spec();
269        assert_eq!(spec.uid(), "com.example.loopback");
270        assert_eq!(spec.name(), "Example Loopback");
271        assert_eq!(spec.manufacturer(), "tympan-aspl");
272    }
273
274    #[test]
275    fn loopback_has_both_streams() {
276        let spec = loopback_spec();
277        assert_eq!(spec.stream_count(), 2);
278        assert!(spec.is_loopback());
279        assert_eq!(
280            spec.stream(StreamDirection::Input).unwrap().direction(),
281            StreamDirection::Input
282        );
283        assert_eq!(
284            spec.stream(StreamDirection::Output).unwrap().direction(),
285            StreamDirection::Output
286        );
287    }
288
289    #[test]
290    fn output_only_device_is_not_loopback() {
291        let spec = DeviceSpec::new("uid", "Speaker", "maker")
292            .with_output(StreamSpec::output(StreamFormat::float32(48_000.0, 2)));
293        assert_eq!(spec.stream_count(), 1);
294        assert!(!spec.is_loopback());
295        assert!(spec.input().is_none());
296        assert!(spec.output().is_some());
297    }
298
299    #[test]
300    fn device_pairs_id_and_spec() {
301        let spec = loopback_spec();
302        let device = Device::new(DeviceId::from_u32(2), spec);
303        assert_eq!(device.id(), DeviceId::from_u32(2));
304        assert_eq!(device.spec().uid(), "com.example.loopback");
305    }
306
307    #[test]
308    fn with_setters_replace_streams() {
309        let spec = DeviceSpec::new("uid", "name", "maker")
310            .with_output(StreamSpec::output(StreamFormat::float32(48_000.0, 1)))
311            .with_output(StreamSpec::output(StreamFormat::float32(48_000.0, 2)));
312        // The second `with_output` replaces the first.
313        assert_eq!(spec.output().unwrap().channels(), 2);
314    }
315}