Skip to main content

sim_lib_stream_coreaudio/
backend.rs

1use sim_kernel::{Error, Result, Symbol};
2use sim_lib_stream_core::{BufferPolicy, StreamMedia};
3use sim_lib_stream_host::{
4    HostBackend, HostBackendCapability, HostBackendInfo, HostClockInfo, HostDeviceInventory,
5    HostDeviceSpec, HostDirection, HostLatencyInfo, HostOpenStream, HostPortSpec, HostStreamConfig,
6    HostStreamConfigRequest,
7};
8
9use crate::{CoreAudioDevice, CoreAudioTiming};
10
11/// Returns the `stream/host` symbol identifying the CoreAudio host backend.
12///
13/// This is the backend id carried by the backend `HostBackendInfo` and matched
14/// against an incoming `HostStreamConfigRequest` backend when routing opens.
15pub fn coreaudio_backend_symbol() -> Symbol {
16    Symbol::qualified("stream/host", "coreaudio")
17}
18
19/// Returns the `stream/transport` symbol for the CoreAudio transport surface.
20pub fn coreaudio_transport_symbol() -> Symbol {
21    Symbol::qualified("stream/transport", "coreaudio")
22}
23
24/// Returns the `clock` symbol stamped onto streams opened by this backend.
25pub fn coreaudio_clock_symbol() -> Symbol {
26    Symbol::qualified("clock", "coreaudio")
27}
28
29/// CoreAudio host backend with provider-supplied deterministic devices.
30#[derive(Clone, Debug)]
31pub struct CoreAudioBackend {
32    info: HostBackendInfo,
33    devices: Vec<CoreAudioDevice>,
34}
35
36impl Default for CoreAudioBackend {
37    fn default() -> Self {
38        Self::new(Vec::new())
39    }
40}
41
42impl CoreAudioBackend {
43    /// Builds a backend over the given provider-supplied devices.
44    ///
45    /// The advertised capabilities are derived from the device directions, and
46    /// the backend reports itself as a hardware (non-fake) backend.
47    pub fn new(devices: Vec<CoreAudioDevice>) -> Self {
48        Self {
49            info: HostBackendInfo::new(
50                coreaudio_backend_symbol(),
51                coreaudio_transport_symbol(),
52                StreamMedia::Pcm,
53                true,
54            )
55            .with_capabilities(capabilities_for(&devices, false)),
56            devices,
57        }
58    }
59
60    /// Builds a deterministic offline backend with a fake default output and
61    /// default input device.
62    ///
63    /// The backend reports the `Offline` and `Fake` capabilities so it can be
64    /// exercised in tests without Apple frameworks or audio hardware.
65    pub fn fake() -> Self {
66        let timing = CoreAudioTiming::default_low_latency();
67        let devices = vec![
68            CoreAudioDevice::output(
69                "coreaudio/default-output",
70                "Fake CoreAudio Default Output",
71                2,
72                timing,
73            )
74            .expect("valid fake output")
75            .with_default_output(),
76            CoreAudioDevice::input(
77                "coreaudio/default-input",
78                "Fake CoreAudio Default Input",
79                2,
80                timing,
81            )
82            .expect("valid fake input")
83            .with_default_input(),
84        ];
85        Self {
86            info: HostBackendInfo::new(
87                coreaudio_backend_symbol(),
88                coreaudio_transport_symbol(),
89                StreamMedia::Pcm,
90                false,
91            )
92            .with_capabilities(capabilities_for(&devices, true)),
93            devices,
94        }
95    }
96
97    /// Returns the devices known to this backend.
98    pub fn list_devices(&self) -> &[CoreAudioDevice] {
99        &self.devices
100    }
101
102    /// Returns the first device flagged as the default output, if any.
103    pub fn default_output(&self) -> Option<&CoreAudioDevice> {
104        self.devices.iter().find(|device| device.default_output())
105    }
106
107    /// Returns the first device flagged as the default input, if any.
108    pub fn default_input(&self) -> Option<&CoreAudioDevice> {
109        self.devices.iter().find(|device| device.default_input())
110    }
111
112    /// Opens an output stream on the default output device.
113    ///
114    /// `capacity` bounds the open request's buffer policy. Returns an error if
115    /// no default output device is present.
116    pub fn open_default_output(&self, capacity: usize) -> Result<HostOpenStream> {
117        let device = self
118            .default_output()
119            .ok_or_else(|| Error::Eval("CoreAudio default output was not found".to_owned()))?;
120        self.open(request(device, HostDirection::Output, capacity)?)
121    }
122
123    /// Opens an input stream on the default input device.
124    ///
125    /// `capacity` bounds the open request's buffer policy. Returns an error if
126    /// no default input device is present.
127    pub fn open_default_input(&self, capacity: usize) -> Result<HostOpenStream> {
128        let device = self
129            .default_input()
130            .ok_or_else(|| Error::Eval("CoreAudio default input was not found".to_owned()))?;
131        self.open(request(device, HostDirection::Input, capacity)?)
132    }
133
134    fn require_device(
135        &self,
136        device_id: &Symbol,
137        direction: HostDirection,
138    ) -> Result<&CoreAudioDevice> {
139        let Some(device) = self
140            .devices
141            .iter()
142            .find(|candidate| candidate.id() == device_id)
143        else {
144            return Err(Error::Eval(format!(
145                "CoreAudio device {device_id} was not found"
146            )));
147        };
148        if !device.is_compatible_with(direction) {
149            return Err(Error::TypeMismatch {
150                expected: "CoreAudio device with requested direction",
151                found: "CoreAudio device with another direction",
152            });
153        }
154        Ok(device)
155    }
156}
157
158impl HostBackend for CoreAudioBackend {
159    fn info(&self) -> &HostBackendInfo {
160        &self.info
161    }
162
163    fn enumerate(&self) -> Result<HostDeviceInventory> {
164        let devices = self
165            .devices
166            .iter()
167            .map(|device| {
168                Ok(HostDeviceSpec::new(
169                    device.id().clone(),
170                    coreaudio_backend_symbol(),
171                    StreamMedia::Pcm,
172                    device.direction(),
173                    coreaudio_clock_symbol(),
174                    BufferPolicy::bounded(device.timing().buffer_frames())?,
175                ))
176            })
177            .collect::<Result<Vec<_>>>()?;
178        let ports = self
179            .devices
180            .iter()
181            .map(|device| {
182                HostPortSpec::new(
183                    device.port_symbol(),
184                    device.id().clone(),
185                    coreaudio_backend_symbol(),
186                    StreamMedia::Pcm,
187                    device.direction(),
188                )
189            })
190            .collect();
191        Ok(HostDeviceInventory::new(coreaudio_backend_symbol())
192            .with_devices(devices)
193            .with_ports(ports))
194    }
195
196    fn open(&self, request: HostStreamConfigRequest) -> Result<HostOpenStream> {
197        if request.backend() != self.info.id() {
198            return Err(Error::Eval(format!(
199                "CoreAudio backend cannot open {} requests",
200                request.backend()
201            )));
202        }
203        if request.media() != StreamMedia::Pcm {
204            return Err(Error::TypeMismatch {
205                expected: "PCM stream request",
206                found: "non-PCM stream request",
207            });
208        }
209        let direction = request.direction();
210        let device = self.require_device(request.device(), direction)?;
211        let config = HostStreamConfig::from_request(
212            request,
213            latency_for(direction, device.timing()),
214            HostClockInfo::new(
215                coreaudio_clock_symbol(),
216                Some(device.timing().sample_rate_hz()),
217                true,
218            ),
219        );
220        Ok(HostOpenStream::new(config))
221    }
222}
223
224fn request(
225    device: &CoreAudioDevice,
226    direction: HostDirection,
227    capacity: usize,
228) -> Result<HostStreamConfigRequest> {
229    Ok(HostStreamConfigRequest::new(
230        coreaudio_backend_symbol(),
231        device.id().clone(),
232        StreamMedia::Pcm,
233        direction,
234        BufferPolicy::bounded(capacity)?,
235    ))
236}
237
238fn capabilities_for(devices: &[CoreAudioDevice], fake: bool) -> Vec<HostBackendCapability> {
239    let mut capabilities = Vec::new();
240    if devices
241        .iter()
242        .any(|device| device.is_compatible_with(HostDirection::Input))
243    {
244        capabilities.push(HostBackendCapability::AudioInput);
245    }
246    if devices
247        .iter()
248        .any(|device| device.is_compatible_with(HostDirection::Output))
249    {
250        capabilities.push(HostBackendCapability::AudioOutput);
251    }
252    if devices
253        .iter()
254        .any(|device| device.direction() == HostDirection::Duplex)
255    {
256        capabilities.push(HostBackendCapability::Duplex);
257    }
258    capabilities.push(HostBackendCapability::Hotplug);
259    capabilities.push(HostBackendCapability::Reconnect);
260    if fake {
261        capabilities.push(HostBackendCapability::Offline);
262        capabilities.push(HostBackendCapability::Fake);
263    }
264    capabilities
265}
266
267fn latency_for(direction: HostDirection, timing: CoreAudioTiming) -> HostLatencyInfo {
268    match direction {
269        HostDirection::Input => HostLatencyInfo::new(timing.input_latency_frames(), 0),
270        HostDirection::Output => HostLatencyInfo::new(0, timing.output_latency_frames()),
271        HostDirection::Duplex => HostLatencyInfo::new(
272            timing.input_latency_frames(),
273            timing.output_latency_frames(),
274        ),
275    }
276}