Skip to main content

tympan_aspl/
dispatch.rs

1//! Core Audio property dispatch.
2//!
3//! The property protocol is the bulk of an AudioServerPlugin's
4//! surface: `coreaudiod` discovers and configures the plug-in
5//! almost entirely by reading and writing properties on the objects
6//! in its [`ObjectMap`]. This module is the framework's answer to
7//! those reads — given an [`ObjectMap`], the driver's [`DriverInfo`],
8//! the device's runtime [`DeviceState`], an object id, and a
9//! [`PropertyAddress`], it produces the typed [`PropertyValue`] (or
10//! the [`OsStatus`] error the HAL expects).
11//!
12//! It is cross-platform pure logic — no FFI. The macOS `raw` layer
13//! (a later PR) will marshal the HAL's opaque byte buffers to and
14//! from these calls; isolating the logic here keeps every standard
15//! property's behaviour unit-testable on any host.
16//!
17//! ## Coverage
18//!
19//! The dispatcher answers the standard properties the HAL needs to
20//! enumerate and run a virtual device:
21//!
22//! - **Every object**: base class, class, owner, name, owned
23//!   objects.
24//! - **Plug-in**: manufacturer, device list, box list.
25//! - **Device**: manufacturer, UID, model UID, transport type,
26//!   alive / running flags, stream list, nominal and available
27//!   sample rates, latency, safety offset, zero-timestamp period.
28//! - **Stream**: direction, terminal type, starting channel,
29//!   virtual and physical format.
30//!
31//! Any selector outside this set yields
32//! [`OsStatus::UNKNOWN_PROPERTY`] — the correct Core Audio answer
33//! for "this object does not have that property".
34
35extern crate alloc;
36
37use alloc::string::String;
38use alloc::vec;
39
40use crate::driver::DriverInfo;
41use crate::error::OsStatus;
42use crate::object::{AudioObjectId, ObjectKind};
43use crate::objects::{Object, ObjectMap};
44use crate::property::{PropertyAddress, PropertySelector, PropertyValue, ValueRange};
45use crate::stream::StreamDirection;
46
47/// `kAudioDeviceTransportTypeVirtual` (`'virt'`) — the
48/// `kAudioDevicePropertyTransportType` value the framework reports
49/// for every device it exposes. The devices are virtual: they have
50/// no physical transport.
51pub const TRANSPORT_TYPE_VIRTUAL: u32 = u32::from_be_bytes(*b"virt");
52
53/// `kAudioStreamTerminalTypeUnknown` (`0`) — the
54/// `kAudioStreamPropertyTerminalType` value the framework reports
55/// for every stream. A virtual stream is not wired to any specific
56/// physical terminal (speaker, microphone, line-in, …).
57pub const TERMINAL_TYPE_UNKNOWN: u32 = 0;
58
59/// The mutable runtime state of a device, distinct from the
60/// immutable [`DeviceSpec`](crate::device::DeviceSpec) the driver
61/// declared.
62///
63/// A `DeviceSpec` says what a device *is*; a `DeviceState` says what
64/// it is *currently doing* — the sample rate it is running at (which
65/// the HAL can change via `kAudioDevicePropertyNominalSampleRate`)
66/// and whether its IO is running. The property dispatcher reads it
67/// for the handful of properties whose value is not fixed at
68/// construction.
69#[derive(Copy, Clone, PartialEq, Debug)]
70pub struct DeviceState {
71    /// The sample rate the device is currently running at, in hertz.
72    /// Starts at the spec's nominal rate; the HAL may change it.
73    pub sample_rate: f64,
74    /// `true` once the HAL has started the device's IO (`StartIO`),
75    /// `false` before that and after `StopIO`.
76    pub running: bool,
77}
78
79impl DeviceState {
80    /// The initial runtime state for `spec`: its nominal sample
81    /// rate, IO not yet running.
82    #[inline]
83    #[must_use]
84    pub fn from_spec(spec: &crate::device::DeviceSpec) -> Self {
85        Self {
86            sample_rate: spec.sample_rate(),
87            running: false,
88        }
89    }
90}
91
92/// Read a property's value.
93///
94/// Resolves `object_id` against `map`, then dispatches to the
95/// property table for that object's kind. Returns:
96///
97/// - [`OsStatus::BAD_OBJECT`] if `object_id` is not in the tree,
98/// - [`OsStatus::UNKNOWN_PROPERTY`] if the object does not have the
99///   addressed property,
100/// - the typed [`PropertyValue`] otherwise.
101///
102/// `address.element` is ignored: every property the framework
103/// dispatches is object-wide. `address.scope` matters only for the
104/// owned-objects and stream-list properties, which filter by
105/// direction.
106pub fn get_property_data(
107    map: &ObjectMap,
108    info: &DriverInfo,
109    state: &DeviceState,
110    object_id: AudioObjectId,
111    address: &PropertyAddress,
112) -> Result<PropertyValue, OsStatus> {
113    match map.resolve(object_id).ok_or(OsStatus::BAD_OBJECT)? {
114        Object::PlugIn => plugin_property(map, info, address),
115        Object::Device => device_property(map, state, address),
116        Object::Stream(direction) => stream_property(map, direction, address),
117    }
118}
119
120/// The size, in bytes, a property's value occupies in the HAL's
121/// buffer — the answer to a `GetPropertyDataSize` query.
122///
123/// Computed from [`get_property_data`], so it returns the same
124/// [`OsStatus::BAD_OBJECT`] / [`OsStatus::UNKNOWN_PROPERTY`] errors
125/// for an unknown object or property.
126pub fn property_data_size(
127    map: &ObjectMap,
128    info: &DriverInfo,
129    state: &DeviceState,
130    object_id: AudioObjectId,
131    address: &PropertyAddress,
132) -> Result<usize, OsStatus> {
133    get_property_data(map, info, state, object_id, address).map(|value| value.byte_size())
134}
135
136/// Whether a property can be written with `SetPropertyData`.
137///
138/// Returns [`OsStatus::BAD_OBJECT`] / [`OsStatus::UNKNOWN_PROPERTY`]
139/// for an unknown object or property (the existence check reuses
140/// [`get_property_data`]). For a property the object does have,
141/// returns `Ok(true)` only for the writable ones — currently just
142/// `kAudioDevicePropertyNominalSampleRate` — and `Ok(false)` for
143/// everything else.
144pub fn is_property_settable(
145    map: &ObjectMap,
146    info: &DriverInfo,
147    state: &DeviceState,
148    object_id: AudioObjectId,
149    address: &PropertyAddress,
150) -> Result<bool, OsStatus> {
151    // The property must exist on the object before "is it settable?"
152    // is a meaningful question; reuse the read path as the
153    // existence check.
154    get_property_data(map, info, state, object_id, address)?;
155    let object = map.resolve(object_id).ok_or(OsStatus::BAD_OBJECT)?;
156    Ok(matches!(
157        (object, address.selector),
158        (Object::Device, PropertySelector::DEVICE_NOMINAL_SAMPLE_RATE)
159    ))
160}
161
162/// Write a property's value.
163///
164/// `data` is the raw, native-endian `SetPropertyData` buffer the HAL
165/// supplied. The property must exist on the object and be settable;
166/// the only writable property today is the device's nominal sample
167/// rate, which `data` must carry as a `Float64`.
168///
169/// On success the change is applied to `state`. Returns:
170///
171/// - [`OsStatus::BAD_OBJECT`] if `object_id` is not in the tree,
172/// - [`OsStatus::UNKNOWN_PROPERTY`] if the object lacks the property,
173/// - [`OsStatus::ILLEGAL_OPERATION`] if the property exists but is
174///   read-only,
175/// - [`OsStatus::BAD_PROPERTY_SIZE`] if `data` is too short to hold
176///   the value,
177/// - [`OsStatus::UNSUPPORTED_FORMAT`] if the requested sample rate
178///   is not one the device offers.
179pub fn set_property_data(
180    map: &ObjectMap,
181    info: &DriverInfo,
182    state: &mut DeviceState,
183    object_id: AudioObjectId,
184    address: &PropertyAddress,
185    data: &[u8],
186) -> Result<(), OsStatus> {
187    // Existence + read-only checks. `is_property_settable` reuses the
188    // read path, so an unknown object / property surfaces the right
189    // error here too.
190    if !is_property_settable(map, info, state, object_id, address)? {
191        return Err(OsStatus::ILLEGAL_OPERATION);
192    }
193    // `is_property_settable` returns `true` for exactly one property:
194    // the device's nominal sample rate. The debug assertion documents
195    // that invariant — if the settable set grows, this function must
196    // grow with it.
197    debug_assert_eq!(
198        (
199            map.resolve(object_id),
200            address.selector == PropertySelector::DEVICE_NOMINAL_SAMPLE_RATE
201        ),
202        (Some(Object::Device), true),
203    );
204
205    let rate = read_f64(data)?;
206    // A fixed-rate virtual device offers exactly its spec's nominal
207    // rate; any other request is an unsupported format.
208    if rate != map.spec().sample_rate() {
209        return Err(OsStatus::UNSUPPORTED_FORMAT);
210    }
211    state.sample_rate = rate;
212    Ok(())
213}
214
215/// Read a native-endian `f64` from the head of a HAL property
216/// buffer, or [`OsStatus::BAD_PROPERTY_SIZE`] if `data` is shorter
217/// than eight bytes.
218fn read_f64(data: &[u8]) -> Result<f64, OsStatus> {
219    let bytes: [u8; 8] = data
220        .get(..8)
221        .ok_or(OsStatus::BAD_PROPERTY_SIZE)?
222        .try_into()
223        .expect("slice of length 8 converts to [u8; 8]");
224    Ok(f64::from_ne_bytes(bytes))
225}
226
227/// Number of frames between the device's zero timestamps —
228/// `kAudioDevicePropertyZeroTimeStampPeriod`.
229///
230/// The framework models a virtual device with a one-second ring
231/// buffer, so the period is the nominal sample rate rounded to a
232/// whole number of frames.
233fn zero_timestamp_period(spec: &crate::device::DeviceSpec) -> u32 {
234    spec.sample_rate() as u32
235}
236
237fn plugin_property(
238    map: &ObjectMap,
239    info: &DriverInfo,
240    address: &PropertyAddress,
241) -> Result<PropertyValue, OsStatus> {
242    use PropertySelector as S;
243    Ok(match address.selector {
244        S::BASE_CLASS => PropertyValue::U32(ObjectKind::PlugIn.base_class_id().as_u32()),
245        S::CLASS => PropertyValue::U32(ObjectKind::PlugIn.class_id().as_u32()),
246        // The plug-in object sits at the root of the tree — it has
247        // no owner.
248        S::OWNER => PropertyValue::ObjectId(AudioObjectId::UNKNOWN),
249        S::NAME => PropertyValue::Text(String::from(info.name)),
250        S::MANUFACTURER => PropertyValue::Text(String::from(info.manufacturer)),
251        S::OWNED_OBJECTS => {
252            PropertyValue::ObjectList(map.owned_objects(map.plugin_id(), address.scope))
253        }
254        S::PLUGIN_DEVICE_LIST => PropertyValue::ObjectList(vec![map.device_id()]),
255        // The framework does not model boxes yet.
256        S::PLUGIN_BOX_LIST => PropertyValue::ObjectList(vec![]),
257        _ => return Err(OsStatus::UNKNOWN_PROPERTY),
258    })
259}
260
261fn device_property(
262    map: &ObjectMap,
263    state: &DeviceState,
264    address: &PropertyAddress,
265) -> Result<PropertyValue, OsStatus> {
266    use PropertySelector as S;
267    let spec = map.spec();
268    Ok(match address.selector {
269        S::BASE_CLASS => PropertyValue::U32(ObjectKind::Device.base_class_id().as_u32()),
270        S::CLASS => PropertyValue::U32(ObjectKind::Device.class_id().as_u32()),
271        S::OWNER => PropertyValue::ObjectId(map.plugin_id()),
272        S::NAME => PropertyValue::Text(String::from(spec.name())),
273        S::MANUFACTURER => PropertyValue::Text(String::from(spec.manufacturer())),
274        S::OWNED_OBJECTS => {
275            PropertyValue::ObjectList(map.owned_objects(map.device_id(), address.scope))
276        }
277        S::DEVICE_UID => PropertyValue::Text(String::from(spec.uid())),
278        // The framework has no notion of a separate model identity,
279        // so the model UID reuses the device UID.
280        S::DEVICE_MODEL_UID => PropertyValue::Text(String::from(spec.uid())),
281        S::DEVICE_TRANSPORT_TYPE => PropertyValue::U32(TRANSPORT_TYPE_VIRTUAL),
282        // The device is always alive once the plug-in exposes it.
283        S::DEVICE_IS_ALIVE => PropertyValue::U32(1),
284        S::DEVICE_IS_RUNNING => PropertyValue::U32(u32::from(state.running)),
285        S::DEVICE_STREAMS => PropertyValue::ObjectList(map.device_streams(address.scope)),
286        S::DEVICE_NOMINAL_SAMPLE_RATE => PropertyValue::F64(state.sample_rate),
287        // A fixed-rate virtual device offers exactly its nominal
288        // rate — a single zero-width range.
289        S::DEVICE_AVAILABLE_SAMPLE_RATES => {
290            PropertyValue::RangeList(vec![ValueRange::point(spec.sample_rate())])
291        }
292        // A purely virtual device introduces no presentation
293        // latency and needs no safety offset.
294        S::DEVICE_LATENCY | S::DEVICE_SAFETY_OFFSET => PropertyValue::U32(0),
295        S::DEVICE_ZERO_TIMESTAMP_PERIOD => PropertyValue::U32(zero_timestamp_period(spec)),
296        _ => return Err(OsStatus::UNKNOWN_PROPERTY),
297    })
298}
299
300fn stream_property(
301    map: &ObjectMap,
302    direction: StreamDirection,
303    address: &PropertyAddress,
304) -> Result<PropertyValue, OsStatus> {
305    use PropertySelector as S;
306    let stream = map.stream_spec(direction).ok_or(OsStatus::BAD_STREAM)?;
307    Ok(match address.selector {
308        S::BASE_CLASS => PropertyValue::U32(ObjectKind::Stream.base_class_id().as_u32()),
309        S::CLASS => PropertyValue::U32(ObjectKind::Stream.class_id().as_u32()),
310        S::OWNER => PropertyValue::ObjectId(map.device_id()),
311        // A stream is a leaf of the object tree.
312        S::OWNED_OBJECTS => PropertyValue::ObjectList(vec![]),
313        S::STREAM_DIRECTION => PropertyValue::U32(direction.as_property_value()),
314        S::STREAM_TERMINAL_TYPE => PropertyValue::U32(TERMINAL_TYPE_UNKNOWN),
315        S::STREAM_STARTING_CHANNEL => PropertyValue::U32(stream.starting_channel()),
316        // The framework negotiates one fixed format, so the virtual
317        // and physical formats are the same.
318        S::STREAM_VIRTUAL_FORMAT | S::STREAM_PHYSICAL_FORMAT => {
319            PropertyValue::Format(stream.format())
320        }
321        _ => return Err(OsStatus::UNKNOWN_PROPERTY),
322    })
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use crate::device::DeviceSpec;
329    use crate::format::StreamFormat;
330    use crate::object::ObjectKind;
331    use crate::property::{PropertyScope, PropertyValue};
332    use crate::stream::StreamSpec;
333
334    fn loopback_spec() -> DeviceSpec {
335        let format = StreamFormat::float32(48_000.0, 2);
336        DeviceSpec::new("com.example.loopback", "Example Loopback", "Acme Audio")
337            .with_sample_rate(48_000.0)
338            .with_input(StreamSpec::input(format))
339            .with_output(StreamSpec::output(format))
340    }
341
342    fn info() -> DriverInfo {
343        DriverInfo {
344            name: "Example Driver",
345            manufacturer: "Acme Audio",
346            version: "1.0.0",
347        }
348    }
349
350    fn fixture() -> (ObjectMap, DriverInfo, DeviceState) {
351        let map = ObjectMap::new(loopback_spec());
352        let state = DeviceState::from_spec(map.spec());
353        (map, info(), state)
354    }
355
356    fn get(
357        map: &ObjectMap,
358        info: &DriverInfo,
359        state: &DeviceState,
360        id: AudioObjectId,
361        selector: PropertySelector,
362    ) -> Result<PropertyValue, OsStatus> {
363        get_property_data(map, info, state, id, &PropertyAddress::global(selector))
364    }
365
366    #[test]
367    fn unknown_object_is_bad_object() {
368        let (map, info, state) = fixture();
369        assert_eq!(
370            get(
371                &map,
372                &info,
373                &state,
374                AudioObjectId::from_u32(999),
375                PropertySelector::CLASS
376            ),
377            Err(OsStatus::BAD_OBJECT)
378        );
379    }
380
381    #[test]
382    fn unknown_selector_is_unknown_property() {
383        let (map, info, state) = fixture();
384        let bogus = PropertySelector::new(*b"zzzz");
385        assert_eq!(
386            get(&map, &info, &state, map.plugin_id(), bogus),
387            Err(OsStatus::UNKNOWN_PROPERTY)
388        );
389        assert_eq!(
390            get(&map, &info, &state, map.device_id(), bogus),
391            Err(OsStatus::UNKNOWN_PROPERTY)
392        );
393    }
394
395    #[test]
396    fn plugin_class_and_base_class() {
397        let (map, info, state) = fixture();
398        assert_eq!(
399            get(
400                &map,
401                &info,
402                &state,
403                map.plugin_id(),
404                PropertySelector::CLASS
405            ),
406            Ok(PropertyValue::U32(ObjectKind::PlugIn.class_id().as_u32()))
407        );
408        assert_eq!(
409            get(
410                &map,
411                &info,
412                &state,
413                map.plugin_id(),
414                PropertySelector::BASE_CLASS
415            ),
416            Ok(PropertyValue::U32(
417                ObjectKind::PlugIn.base_class_id().as_u32()
418            ))
419        );
420    }
421
422    #[test]
423    fn plugin_has_no_owner() {
424        let (map, info, state) = fixture();
425        assert_eq!(
426            get(
427                &map,
428                &info,
429                &state,
430                map.plugin_id(),
431                PropertySelector::OWNER
432            ),
433            Ok(PropertyValue::ObjectId(AudioObjectId::UNKNOWN))
434        );
435    }
436
437    #[test]
438    fn plugin_identity_comes_from_driver_info() {
439        let (map, info, state) = fixture();
440        assert_eq!(
441            get(&map, &info, &state, map.plugin_id(), PropertySelector::NAME),
442            Ok(PropertyValue::Text("Example Driver".into()))
443        );
444        assert_eq!(
445            get(
446                &map,
447                &info,
448                &state,
449                map.plugin_id(),
450                PropertySelector::MANUFACTURER
451            ),
452            Ok(PropertyValue::Text("Acme Audio".into()))
453        );
454    }
455
456    #[test]
457    fn plugin_device_list_and_owned_objects_point_at_the_device() {
458        let (map, info, state) = fixture();
459        let expected = PropertyValue::ObjectList(vec![map.device_id()]);
460        assert_eq!(
461            get(
462                &map,
463                &info,
464                &state,
465                map.plugin_id(),
466                PropertySelector::PLUGIN_DEVICE_LIST
467            ),
468            Ok(expected.clone())
469        );
470        assert_eq!(
471            get(
472                &map,
473                &info,
474                &state,
475                map.plugin_id(),
476                PropertySelector::OWNED_OBJECTS
477            ),
478            Ok(expected)
479        );
480    }
481
482    #[test]
483    fn plugin_box_list_is_empty() {
484        let (map, info, state) = fixture();
485        assert_eq!(
486            get(
487                &map,
488                &info,
489                &state,
490                map.plugin_id(),
491                PropertySelector::PLUGIN_BOX_LIST
492            ),
493            Ok(PropertyValue::ObjectList(vec![]))
494        );
495    }
496
497    #[test]
498    fn device_identity_comes_from_the_spec() {
499        let (map, info, state) = fixture();
500        let dev = map.device_id();
501        assert_eq!(
502            get(&map, &info, &state, dev, PropertySelector::NAME),
503            Ok(PropertyValue::Text("Example Loopback".into()))
504        );
505        assert_eq!(
506            get(&map, &info, &state, dev, PropertySelector::DEVICE_UID),
507            Ok(PropertyValue::Text("com.example.loopback".into()))
508        );
509        assert_eq!(
510            get(&map, &info, &state, dev, PropertySelector::DEVICE_MODEL_UID),
511            Ok(PropertyValue::Text("com.example.loopback".into()))
512        );
513        assert_eq!(
514            get(&map, &info, &state, dev, PropertySelector::MANUFACTURER),
515            Ok(PropertyValue::Text("Acme Audio".into()))
516        );
517    }
518
519    #[test]
520    fn device_owner_is_the_plugin() {
521        let (map, info, state) = fixture();
522        assert_eq!(
523            get(
524                &map,
525                &info,
526                &state,
527                map.device_id(),
528                PropertySelector::OWNER
529            ),
530            Ok(PropertyValue::ObjectId(map.plugin_id()))
531        );
532    }
533
534    #[test]
535    fn device_transport_type_is_virtual() {
536        let (map, info, state) = fixture();
537        assert_eq!(
538            get(
539                &map,
540                &info,
541                &state,
542                map.device_id(),
543                PropertySelector::DEVICE_TRANSPORT_TYPE
544            ),
545            Ok(PropertyValue::U32(u32::from_be_bytes(*b"virt")))
546        );
547    }
548
549    #[test]
550    fn device_is_alive_and_reflects_running_state() {
551        let (map, info, mut state) = fixture();
552        let dev = map.device_id();
553        assert_eq!(
554            get(&map, &info, &state, dev, PropertySelector::DEVICE_IS_ALIVE),
555            Ok(PropertyValue::U32(1))
556        );
557        assert_eq!(
558            get(
559                &map,
560                &info,
561                &state,
562                dev,
563                PropertySelector::DEVICE_IS_RUNNING
564            ),
565            Ok(PropertyValue::U32(0))
566        );
567        state.running = true;
568        assert_eq!(
569            get(
570                &map,
571                &info,
572                &state,
573                dev,
574                PropertySelector::DEVICE_IS_RUNNING
575            ),
576            Ok(PropertyValue::U32(1))
577        );
578    }
579
580    #[test]
581    fn device_sample_rate_tracks_runtime_state() {
582        let (map, info, mut state) = fixture();
583        let dev = map.device_id();
584        assert_eq!(
585            get(
586                &map,
587                &info,
588                &state,
589                dev,
590                PropertySelector::DEVICE_NOMINAL_SAMPLE_RATE
591            ),
592            Ok(PropertyValue::F64(48_000.0))
593        );
594        // The HAL changed the rate at runtime.
595        state.sample_rate = 96_000.0;
596        assert_eq!(
597            get(
598                &map,
599                &info,
600                &state,
601                dev,
602                PropertySelector::DEVICE_NOMINAL_SAMPLE_RATE
603            ),
604            Ok(PropertyValue::F64(96_000.0))
605        );
606        // The *available* rates still come from the immutable spec.
607        assert_eq!(
608            get(
609                &map,
610                &info,
611                &state,
612                dev,
613                PropertySelector::DEVICE_AVAILABLE_SAMPLE_RATES
614            ),
615            Ok(PropertyValue::RangeList(vec![ValueRange::point(48_000.0)]))
616        );
617    }
618
619    #[test]
620    fn device_streams_filter_by_scope() {
621        let (map, info, state) = fixture();
622        let dev = map.device_id();
623        let global = PropertyAddress::new(
624            PropertySelector::DEVICE_STREAMS,
625            PropertyScope::GLOBAL,
626            crate::property::PropertyElement::MAIN,
627        );
628        let input = PropertyAddress::new(
629            PropertySelector::DEVICE_STREAMS,
630            PropertyScope::INPUT,
631            crate::property::PropertyElement::MAIN,
632        );
633        assert_eq!(
634            get_property_data(&map, &info, &state, dev, &global),
635            Ok(PropertyValue::ObjectList(vec![
636                map.stream_id(StreamDirection::Input).unwrap(),
637                map.stream_id(StreamDirection::Output).unwrap(),
638            ]))
639        );
640        assert_eq!(
641            get_property_data(&map, &info, &state, dev, &input),
642            Ok(PropertyValue::ObjectList(vec![map
643                .stream_id(StreamDirection::Input)
644                .unwrap()]))
645        );
646    }
647
648    #[test]
649    fn device_latency_and_safety_offset_are_zero() {
650        let (map, info, state) = fixture();
651        let dev = map.device_id();
652        assert_eq!(
653            get(&map, &info, &state, dev, PropertySelector::DEVICE_LATENCY),
654            Ok(PropertyValue::U32(0))
655        );
656        assert_eq!(
657            get(
658                &map,
659                &info,
660                &state,
661                dev,
662                PropertySelector::DEVICE_SAFETY_OFFSET
663            ),
664            Ok(PropertyValue::U32(0))
665        );
666    }
667
668    #[test]
669    fn device_zero_timestamp_period_is_one_second_of_frames() {
670        let (map, info, state) = fixture();
671        assert_eq!(
672            get(
673                &map,
674                &info,
675                &state,
676                map.device_id(),
677                PropertySelector::DEVICE_ZERO_TIMESTAMP_PERIOD
678            ),
679            Ok(PropertyValue::U32(48_000))
680        );
681    }
682
683    #[test]
684    fn stream_class_owner_and_direction() {
685        let (map, info, state) = fixture();
686        let input = map.stream_id(StreamDirection::Input).unwrap();
687        let output = map.stream_id(StreamDirection::Output).unwrap();
688        assert_eq!(
689            get(&map, &info, &state, input, PropertySelector::CLASS),
690            Ok(PropertyValue::U32(ObjectKind::Stream.class_id().as_u32()))
691        );
692        assert_eq!(
693            get(&map, &info, &state, input, PropertySelector::OWNER),
694            Ok(PropertyValue::ObjectId(map.device_id()))
695        );
696        assert_eq!(
697            get(
698                &map,
699                &info,
700                &state,
701                input,
702                PropertySelector::STREAM_DIRECTION
703            ),
704            Ok(PropertyValue::U32(1))
705        );
706        assert_eq!(
707            get(
708                &map,
709                &info,
710                &state,
711                output,
712                PropertySelector::STREAM_DIRECTION
713            ),
714            Ok(PropertyValue::U32(0))
715        );
716    }
717
718    #[test]
719    fn stream_formats_come_from_the_stream_spec() {
720        let (map, info, state) = fixture();
721        let input = map.stream_id(StreamDirection::Input).unwrap();
722        let expected = PropertyValue::Format(StreamFormat::float32(48_000.0, 2));
723        assert_eq!(
724            get(
725                &map,
726                &info,
727                &state,
728                input,
729                PropertySelector::STREAM_VIRTUAL_FORMAT
730            ),
731            Ok(expected.clone())
732        );
733        assert_eq!(
734            get(
735                &map,
736                &info,
737                &state,
738                input,
739                PropertySelector::STREAM_PHYSICAL_FORMAT
740            ),
741            Ok(expected)
742        );
743    }
744
745    #[test]
746    fn stream_starting_channel_comes_from_the_spec() {
747        let format = StreamFormat::float32(48_000.0, 2);
748        let spec = DeviceSpec::new("uid", "name", "maker")
749            .with_output(StreamSpec::output(format).with_starting_channel(5));
750        let map = ObjectMap::new(spec);
751        let state = DeviceState::from_spec(map.spec());
752        let output = map.stream_id(StreamDirection::Output).unwrap();
753        assert_eq!(
754            get(
755                &map,
756                &info(),
757                &state,
758                output,
759                PropertySelector::STREAM_STARTING_CHANNEL
760            ),
761            Ok(PropertyValue::U32(5))
762        );
763    }
764
765    #[test]
766    fn property_data_size_matches_the_value_size() {
767        let (map, info, state) = fixture();
768        let dev = map.device_id();
769        // F64 sample rate → 8 bytes.
770        assert_eq!(
771            property_data_size(
772                &map,
773                &info,
774                &state,
775                dev,
776                &PropertyAddress::global(PropertySelector::DEVICE_NOMINAL_SAMPLE_RATE)
777            ),
778            Ok(8)
779        );
780        // Two streams → two AudioObjectIDs → 8 bytes.
781        assert_eq!(
782            property_data_size(
783                &map,
784                &info,
785                &state,
786                dev,
787                &PropertyAddress::global(PropertySelector::DEVICE_STREAMS)
788            ),
789            Ok(8)
790        );
791        // Stream format → AudioStreamBasicDescription → 40 bytes.
792        let input = map.stream_id(StreamDirection::Input).unwrap();
793        assert_eq!(
794            property_data_size(
795                &map,
796                &info,
797                &state,
798                input,
799                &PropertyAddress::global(PropertySelector::STREAM_VIRTUAL_FORMAT)
800            ),
801            Ok(40)
802        );
803    }
804
805    #[test]
806    fn property_data_size_propagates_errors() {
807        let (map, info, state) = fixture();
808        assert_eq!(
809            property_data_size(
810                &map,
811                &info,
812                &state,
813                AudioObjectId::from_u32(999),
814                &PropertyAddress::global(PropertySelector::CLASS)
815            ),
816            Err(OsStatus::BAD_OBJECT)
817        );
818    }
819
820    #[test]
821    fn only_the_sample_rate_is_settable() {
822        let (map, info, state) = fixture();
823        let dev = map.device_id();
824        assert_eq!(
825            is_property_settable(
826                &map,
827                &info,
828                &state,
829                dev,
830                &PropertyAddress::global(PropertySelector::DEVICE_NOMINAL_SAMPLE_RATE)
831            ),
832            Ok(true)
833        );
834        // Every other known property is read-only.
835        for selector in [
836            PropertySelector::DEVICE_UID,
837            PropertySelector::NAME,
838            PropertySelector::DEVICE_STREAMS,
839            PropertySelector::CLASS,
840        ] {
841            assert_eq!(
842                is_property_settable(&map, &info, &state, dev, &PropertyAddress::global(selector)),
843                Ok(false),
844                "{selector:?} should be read-only"
845            );
846        }
847    }
848
849    #[test]
850    fn is_property_settable_rejects_unknown_object_and_property() {
851        let (map, info, state) = fixture();
852        assert_eq!(
853            is_property_settable(
854                &map,
855                &info,
856                &state,
857                AudioObjectId::from_u32(999),
858                &PropertyAddress::global(PropertySelector::CLASS)
859            ),
860            Err(OsStatus::BAD_OBJECT)
861        );
862        assert_eq!(
863            is_property_settable(
864                &map,
865                &info,
866                &state,
867                map.device_id(),
868                &PropertyAddress::global(PropertySelector::new(*b"zzzz"))
869            ),
870            Err(OsStatus::UNKNOWN_PROPERTY)
871        );
872    }
873
874    #[test]
875    fn device_state_from_spec_is_idle_at_nominal_rate() {
876        let spec = loopback_spec();
877        let state = DeviceState::from_spec(&spec);
878        assert_eq!(state.sample_rate, 48_000.0);
879        assert!(!state.running);
880    }
881
882    #[test]
883    fn set_sample_rate_applies_a_supported_rate() {
884        let (map, info, mut state) = fixture();
885        // The device offers 48 kHz; setting it to 48 kHz succeeds.
886        assert_eq!(
887            set_property_data(
888                &map,
889                &info,
890                &mut state,
891                map.device_id(),
892                &PropertyAddress::global(PropertySelector::DEVICE_NOMINAL_SAMPLE_RATE),
893                &48_000.0_f64.to_ne_bytes(),
894            ),
895            Ok(())
896        );
897        assert_eq!(state.sample_rate, 48_000.0);
898    }
899
900    #[test]
901    fn set_sample_rate_rejects_an_unsupported_rate() {
902        let (map, info, mut state) = fixture();
903        assert_eq!(
904            set_property_data(
905                &map,
906                &info,
907                &mut state,
908                map.device_id(),
909                &PropertyAddress::global(PropertySelector::DEVICE_NOMINAL_SAMPLE_RATE),
910                &96_000.0_f64.to_ne_bytes(),
911            ),
912            Err(OsStatus::UNSUPPORTED_FORMAT)
913        );
914        // The state is left untouched on a rejected set.
915        assert_eq!(state.sample_rate, 48_000.0);
916    }
917
918    #[test]
919    fn set_sample_rate_rejects_an_undersized_buffer() {
920        let (map, info, mut state) = fixture();
921        assert_eq!(
922            set_property_data(
923                &map,
924                &info,
925                &mut state,
926                map.device_id(),
927                &PropertyAddress::global(PropertySelector::DEVICE_NOMINAL_SAMPLE_RATE),
928                &[0u8; 4],
929            ),
930            Err(OsStatus::BAD_PROPERTY_SIZE)
931        );
932    }
933
934    #[test]
935    fn set_read_only_property_is_illegal() {
936        let (map, info, mut state) = fixture();
937        // The device UID is a known property, but read-only.
938        assert_eq!(
939            set_property_data(
940                &map,
941                &info,
942                &mut state,
943                map.device_id(),
944                &PropertyAddress::global(PropertySelector::DEVICE_UID),
945                b"whatever",
946            ),
947            Err(OsStatus::ILLEGAL_OPERATION)
948        );
949    }
950
951    #[test]
952    fn set_rejects_unknown_object_and_property() {
953        let (map, info, mut state) = fixture();
954        assert_eq!(
955            set_property_data(
956                &map,
957                &info,
958                &mut state,
959                AudioObjectId::from_u32(999),
960                &PropertyAddress::global(PropertySelector::DEVICE_NOMINAL_SAMPLE_RATE),
961                &48_000.0_f64.to_ne_bytes(),
962            ),
963            Err(OsStatus::BAD_OBJECT)
964        );
965        assert_eq!(
966            set_property_data(
967                &map,
968                &info,
969                &mut state,
970                map.device_id(),
971                &PropertyAddress::global(PropertySelector::new(*b"zzzz")),
972                &48_000.0_f64.to_ne_bytes(),
973            ),
974            Err(OsStatus::UNKNOWN_PROPERTY)
975        );
976    }
977}