Skip to main content

sim_lib_stream_coreaudio/
model.rs

1use sim_kernel::{Error, Result, Symbol};
2use sim_lib_stream_audio::PcmSpec;
3use sim_lib_stream_host::HostDirection;
4
5use crate::coreaudio_backend_symbol;
6
7/// Timing metadata accepted by a CoreAudio render callback.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub struct CoreAudioTiming {
10    sample_rate_hz: u32,
11    buffer_frames: usize,
12    input_latency_frames: u32,
13    output_latency_frames: u32,
14}
15
16/// SIM-visible CoreAudio device metadata.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct CoreAudioDevice {
19    id: Symbol,
20    name: String,
21    direction: HostDirection,
22    channels: usize,
23    timing: CoreAudioTiming,
24    default_output: bool,
25    default_input: bool,
26}
27
28impl CoreAudioTiming {
29    /// Builds timing metadata from a sample rate, buffer size, and the input
30    /// and output latencies, all measured in frames.
31    ///
32    /// Returns an error when the sample rate or buffer size is zero.
33    ///
34    /// # Examples
35    ///
36    /// ```
37    /// use sim_lib_stream_coreaudio::CoreAudioTiming;
38    ///
39    /// let timing = CoreAudioTiming::new(48_000, 128, 128, 128).unwrap();
40    /// assert_eq!(timing.sample_rate_hz(), 48_000);
41    /// assert!(CoreAudioTiming::new(0, 128, 0, 0).is_err());
42    /// ```
43    pub fn new(
44        sample_rate_hz: u32,
45        buffer_frames: usize,
46        input_latency_frames: u32,
47        output_latency_frames: u32,
48    ) -> Result<Self> {
49        if sample_rate_hz == 0 {
50            return Err(Error::Eval(
51                "CoreAudio sample rate must be greater than zero".to_owned(),
52            ));
53        }
54        if buffer_frames == 0 {
55            return Err(Error::Eval(
56                "CoreAudio buffer size must be greater than zero".to_owned(),
57            ));
58        }
59        Ok(Self {
60            sample_rate_hz,
61            buffer_frames,
62            input_latency_frames,
63            output_latency_frames,
64        })
65    }
66
67    /// Returns a default low-latency timing: 48 kHz, 128-frame buffer, and
68    /// 128-frame input and output latencies.
69    pub fn default_low_latency() -> Self {
70        Self::new(48_000, 128, 128, 128).expect("valid CoreAudio timing")
71    }
72
73    /// Returns the sample rate in hertz.
74    pub fn sample_rate_hz(self) -> u32 {
75        self.sample_rate_hz
76    }
77
78    /// Returns the callback buffer size in frames.
79    pub fn buffer_frames(self) -> usize {
80        self.buffer_frames
81    }
82
83    /// Returns the input latency in frames.
84    pub fn input_latency_frames(self) -> u32 {
85        self.input_latency_frames
86    }
87
88    /// Returns the output latency in frames.
89    pub fn output_latency_frames(self) -> u32 {
90        self.output_latency_frames
91    }
92}
93
94impl CoreAudioDevice {
95    /// Builds an output-direction device. See [`CoreAudioDevice::new`].
96    pub fn output(
97        id: impl Into<String>,
98        name: impl Into<String>,
99        channels: usize,
100        timing: CoreAudioTiming,
101    ) -> Result<Self> {
102        Self::new(id, name, HostDirection::Output, channels, timing)
103    }
104
105    /// Builds an input-direction device. See [`CoreAudioDevice::new`].
106    pub fn input(
107        id: impl Into<String>,
108        name: impl Into<String>,
109        channels: usize,
110        timing: CoreAudioTiming,
111    ) -> Result<Self> {
112        Self::new(id, name, HostDirection::Input, channels, timing)
113    }
114
115    /// Builds a duplex (input and output) device. See [`CoreAudioDevice::new`].
116    pub fn duplex(
117        id: impl Into<String>,
118        name: impl Into<String>,
119        channels: usize,
120        timing: CoreAudioTiming,
121    ) -> Result<Self> {
122        Self::new(id, name, HostDirection::Duplex, channels, timing)
123    }
124
125    /// Builds a device from its id, display name, direction, channel count, and
126    /// timing.
127    ///
128    /// The device starts flagged as neither the default input nor the default
129    /// output. Returns an error when `channels` is zero.
130    pub fn new(
131        id: impl Into<String>,
132        name: impl Into<String>,
133        direction: HostDirection,
134        channels: usize,
135        timing: CoreAudioTiming,
136    ) -> Result<Self> {
137        if channels == 0 {
138            return Err(Error::Eval(
139                "CoreAudio device channel count must be greater than zero".to_owned(),
140            ));
141        }
142        Ok(Self {
143            id: Symbol::new(id.into()),
144            name: name.into(),
145            direction,
146            channels,
147            timing,
148            default_output: false,
149            default_input: false,
150        })
151    }
152
153    /// Returns this device flagged as the default output device.
154    pub fn with_default_output(mut self) -> Self {
155        self.default_output = true;
156        self
157    }
158
159    /// Returns this device flagged as the default input device.
160    pub fn with_default_input(mut self) -> Self {
161        self.default_input = true;
162        self
163    }
164
165    /// Returns the device's identifying symbol.
166    pub fn id(&self) -> &Symbol {
167        &self.id
168    }
169
170    /// Returns the device's human-readable display name.
171    pub fn name(&self) -> &str {
172        &self.name
173    }
174
175    /// Returns the device's I/O direction.
176    pub fn direction(&self) -> HostDirection {
177        self.direction
178    }
179
180    /// Returns the device's channel count.
181    pub fn channels(&self) -> usize {
182        self.channels
183    }
184
185    /// Returns the device's timing metadata.
186    pub fn timing(&self) -> CoreAudioTiming {
187        self.timing
188    }
189
190    /// Returns whether this device is flagged as the default output.
191    pub fn default_output(&self) -> bool {
192        self.default_output
193    }
194
195    /// Returns whether this device is flagged as the default input.
196    pub fn default_input(&self) -> bool {
197        self.default_input
198    }
199
200    /// Returns the f32 PCM spec for this device's channels and sample rate.
201    pub fn spec(&self) -> Result<PcmSpec> {
202        PcmSpec::f32(self.channels, self.timing.sample_rate_hz)
203    }
204
205    /// Returns whether this device can serve a stream in the requested
206    /// direction.
207    ///
208    /// A duplex device is compatible with any requested direction.
209    pub fn is_compatible_with(&self, requested: HostDirection) -> bool {
210        self.direction == requested || self.direction == HostDirection::Duplex
211    }
212
213    /// Returns the `<id>/port` symbol naming this device's host port.
214    pub fn port_symbol(&self) -> Symbol {
215        Symbol::new(format!("{}/port", self.id))
216    }
217
218    /// Returns the CoreAudio backend symbol this device belongs to.
219    pub fn backend(&self) -> Symbol {
220        coreaudio_backend_symbol()
221    }
222}
223
224/// Preferred macOS PCM backend order: portable first, native when needed.
225pub fn macos_audio_backend_priority() -> Vec<Symbol> {
226    vec![
227        Symbol::qualified("stream/host", "portaudio"),
228        Symbol::qualified("stream/host", "rtaudio"),
229        coreaudio_backend_symbol(),
230    ]
231}
232
233/// Preferred macOS MIDI backend order keeps RtMidi first.
234pub fn macos_midi_backend_priority() -> Vec<Symbol> {
235    vec![
236        Symbol::qualified("stream/host", "rtmidi"),
237        Symbol::qualified("stream/host", "coremidi"),
238    ]
239}