Skip to main content

tympan_aspl/
object.rs

1//! Audio object identifiers and the AudioServerPlugin object model.
2//!
3//! Every entity an AudioServerPlugin exposes — the plug-in itself,
4//! its boxes, devices, streams, and controls — is an *audio object*
5//! addressed by a 32-bit [`AudioObjectId`]. The HAL hands the
6//! plug-in a single well-known id at startup
7//! ([`AudioObjectId::PLUGIN`]) and the plug-in mints ids for every
8//! object it creates.
9//!
10//! The object graph is a tree: the plug-in owns boxes and devices,
11//! a device owns streams and controls. [`ObjectKind`] tags which
12//! layer of that tree a given id sits at; the framework's property
13//! dispatch uses it to route a request to the right handler.
14
15use crate::fourcc::FourCharCode;
16
17/// A Core Audio object identifier.
18///
19/// Layout-compatible with the C `AudioObjectID` (`u32`).
20/// [`AudioObjectId::UNKNOWN`] (`0`) is the reserved "no object"
21/// sentinel; [`AudioObjectId::PLUGIN`] (`1`) is the fixed id the
22/// HAL uses to address the plug-in object itself. Every other id is
23/// minted by the plug-in as it creates boxes, devices, and streams.
24#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
25#[repr(transparent)]
26pub struct AudioObjectId(pub u32);
27
28impl AudioObjectId {
29    /// `kAudioObjectUnknown` — the reserved "no object" id.
30    pub const UNKNOWN: Self = Self(0);
31
32    /// `kAudioObjectPlugInObject` — the fixed id by which the HAL
33    /// addresses the plug-in object. The plug-in does not mint this
34    /// id; it is assigned by Core Audio before the first property
35    /// call.
36    pub const PLUGIN: Self = Self(1);
37
38    /// The first id the plug-in is free to mint for its own
39    /// objects. Ids `0` and `1` are reserved (see [`Self::UNKNOWN`]
40    /// and [`Self::PLUGIN`]).
41    pub const FIRST_DYNAMIC: Self = Self(2);
42
43    /// Wrap a raw `AudioObjectID`.
44    #[inline]
45    #[must_use]
46    pub const fn from_u32(value: u32) -> Self {
47        Self(value)
48    }
49
50    /// The raw `u32`, ready for the FFI boundary.
51    #[inline]
52    #[must_use]
53    pub const fn as_u32(self) -> u32 {
54        self.0
55    }
56
57    /// `true` iff this is [`Self::UNKNOWN`].
58    #[inline]
59    #[must_use]
60    pub const fn is_unknown(self) -> bool {
61        self.0 == 0
62    }
63
64    /// `true` iff this is [`Self::PLUGIN`].
65    #[inline]
66    #[must_use]
67    pub const fn is_plugin(self) -> bool {
68        self.0 == Self::PLUGIN.0
69    }
70}
71
72impl From<u32> for AudioObjectId {
73    #[inline]
74    fn from(value: u32) -> Self {
75        Self(value)
76    }
77}
78
79impl From<AudioObjectId> for u32 {
80    #[inline]
81    fn from(value: AudioObjectId) -> Self {
82        value.0
83    }
84}
85
86/// Which layer of the AudioServerPlugin object tree an id sits at.
87///
88/// The values mirror the `kAudio*ClassID` four-character codes from
89/// `<CoreAudio/AudioHardwareBase.h>`. The framework's property
90/// dispatch reads this off an id to pick the property table that
91/// applies — global object properties, plug-in properties, device
92/// properties, and so on.
93#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
94#[non_exhaustive]
95pub enum ObjectKind {
96    /// `kAudioPlugInClassID` (`'aplg'`) — the plug-in object.
97    PlugIn,
98    /// `kAudioBoxClassID` (`'abox'`) — a box: a container that
99    /// groups devices, modelling a physical or virtual enclosure.
100    Box,
101    /// `kAudioDeviceClassID` (`'adev'`) — a device exposed to the
102    /// system as an audio endpoint.
103    Device,
104    /// `kAudioStreamClassID` (`'astr'`) — one direction of audio
105    /// flow on a device.
106    Stream,
107    /// `kAudioControlClassID` (`'actl'`) — a control (volume, mute,
108    /// data source, …) attached to a device or stream.
109    Control,
110}
111
112impl ObjectKind {
113    /// The `kAudio*ClassID` four-character code for this kind.
114    #[inline]
115    #[must_use]
116    pub const fn class_id(self) -> FourCharCode {
117        match self {
118            Self::PlugIn => FourCharCode::new(*b"aplg"),
119            Self::Box => FourCharCode::new(*b"abox"),
120            Self::Device => FourCharCode::new(*b"adev"),
121            Self::Stream => FourCharCode::new(*b"astr"),
122            Self::Control => FourCharCode::new(*b"actl"),
123        }
124    }
125
126    /// The `kAudio*ClassID` of this kind's base class, or `None` for
127    /// the root of the hierarchy.
128    ///
129    /// Core Audio's `kAudioObjectPropertyBaseClass` walks this
130    /// chain; every class but `AudioObject` itself ultimately bases
131    /// on `kAudioObjectClassID` (`'aobj'`).
132    #[inline]
133    #[must_use]
134    pub const fn base_class_id(self) -> FourCharCode {
135        // Every first-class object in this model bases directly on
136        // `kAudioObjectClassID`; the deeper inheritance chains
137        // Core Audio defines (e.g. volume control → level control →
138        // control) are not yet modelled.
139        FourCharCode::new(*b"aobj")
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn reserved_ids_have_fixed_values() {
149        assert_eq!(AudioObjectId::UNKNOWN.as_u32(), 0);
150        assert_eq!(AudioObjectId::PLUGIN.as_u32(), 1);
151        assert_eq!(AudioObjectId::FIRST_DYNAMIC.as_u32(), 2);
152    }
153
154    #[test]
155    fn predicates_classify_reserved_ids() {
156        assert!(AudioObjectId::UNKNOWN.is_unknown());
157        assert!(!AudioObjectId::UNKNOWN.is_plugin());
158        assert!(AudioObjectId::PLUGIN.is_plugin());
159        assert!(!AudioObjectId::PLUGIN.is_unknown());
160        assert!(!AudioObjectId::from_u32(42).is_unknown());
161        assert!(!AudioObjectId::from_u32(42).is_plugin());
162    }
163
164    #[test]
165    fn round_trips_through_u32() {
166        for raw in [0, 1, 2, 99, u32::MAX] {
167            let id = AudioObjectId::from_u32(raw);
168            assert_eq!(id.as_u32(), raw);
169            let back: u32 = id.into();
170            assert_eq!(back, raw);
171            assert_eq!(AudioObjectId::from(raw), id);
172        }
173    }
174
175    #[test]
176    fn class_ids_match_core_audio_codes() {
177        assert_eq!(ObjectKind::PlugIn.class_id(), FourCharCode::new(*b"aplg"));
178        assert_eq!(ObjectKind::Box.class_id(), FourCharCode::new(*b"abox"));
179        assert_eq!(ObjectKind::Device.class_id(), FourCharCode::new(*b"adev"));
180        assert_eq!(ObjectKind::Stream.class_id(), FourCharCode::new(*b"astr"));
181        assert_eq!(ObjectKind::Control.class_id(), FourCharCode::new(*b"actl"));
182    }
183
184    #[test]
185    fn every_kind_bases_on_audio_object() {
186        for kind in [
187            ObjectKind::PlugIn,
188            ObjectKind::Box,
189            ObjectKind::Device,
190            ObjectKind::Stream,
191            ObjectKind::Control,
192        ] {
193            assert_eq!(kind.base_class_id(), FourCharCode::new(*b"aobj"));
194        }
195    }
196
197    #[test]
198    fn ids_order_numerically() {
199        assert!(AudioObjectId::UNKNOWN < AudioObjectId::PLUGIN);
200        assert!(AudioObjectId::PLUGIN < AudioObjectId::FIRST_DYNAMIC);
201    }
202
203    #[test]
204    fn layout_is_transparent_u32() {
205        use core::mem::{align_of, size_of};
206        assert_eq!(size_of::<AudioObjectId>(), size_of::<u32>());
207        assert_eq!(align_of::<AudioObjectId>(), align_of::<u32>());
208    }
209}