1use crate::object::AudioObjectId;
16use crate::stream::{StreamDirection, StreamSpec};
17
18#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
24#[repr(transparent)]
25pub struct DeviceId(pub AudioObjectId);
26
27impl DeviceId {
28 #[inline]
30 #[must_use]
31 pub const fn from_u32(value: u32) -> Self {
32 Self(AudioObjectId::from_u32(value))
33 }
34
35 #[inline]
37 #[must_use]
38 pub const fn object_id(self) -> AudioObjectId {
39 self.0
40 }
41
42 #[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#[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 #[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 #[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 #[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 #[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 #[inline]
134 #[must_use]
135 pub const fn uid(&self) -> &'static str {
136 self.uid
137 }
138
139 #[inline]
141 #[must_use]
142 pub const fn name(&self) -> &'static str {
143 self.name
144 }
145
146 #[inline]
148 #[must_use]
149 pub const fn manufacturer(&self) -> &'static str {
150 self.manufacturer
151 }
152
153 #[inline]
155 #[must_use]
156 pub const fn sample_rate(&self) -> f64 {
157 self.sample_rate
158 }
159
160 #[inline]
162 #[must_use]
163 pub const fn input(&self) -> Option<StreamSpec> {
164 self.input
165 }
166
167 #[inline]
169 #[must_use]
170 pub const fn output(&self) -> Option<StreamSpec> {
171 self.output
172 }
173
174 #[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 #[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 #[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#[derive(Copy, Clone, PartialEq, Debug)]
207pub struct Device {
208 id: DeviceId,
209 spec: DeviceSpec,
210}
211
212impl Device {
213 #[inline]
216 #[must_use]
217 pub const fn new(id: DeviceId, spec: DeviceSpec) -> Self {
218 Self { id, spec }
219 }
220
221 #[inline]
223 #[must_use]
224 pub const fn id(&self) -> DeviceId {
225 self.id
226 }
227
228 #[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 assert_eq!(spec.output().unwrap().channels(), 2);
314 }
315}