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}