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