nandi/jolt-nativepublic Fork 0
cfd3e3677bed92e80cfe447469a249cfdc0b1401
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

device.rs · 1032 lines · 39.5 KBRust Blame HistoryRaw
Lift freeq's AV media plane out of sleek 90f8b89 nandi 19d ago1use super::OSStatus;
2use super::Stream;
3use super::{asbd_from_config, check_os_status, frames_to_duration, host_time_to_stream_instant};
4use crate::host::coreaudio::macos::loopback::LoopbackDevice;
5use crate::host::coreaudio::macos::StreamInner;
6use crate::traits::DeviceTrait;
7use crate::{
8 BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data,
9 DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, InputCallbackInfo,
10 OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize,
11 SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError,
12};
13use coreaudio::audio_unit::render_callback::{self, data};
14use coreaudio::audio_unit::{AudioUnit, Element, Scope};
15use objc2_audio_toolbox::{
16 kAudioOutputUnitProperty_CurrentDevice, kAudioOutputUnitProperty_EnableIO,
17 kAudioUnitProperty_StreamFormat,
18};
19use objc2_core_audio::kAudioDevicePropertyDeviceUID;
20use objc2_core_audio::kAudioObjectPropertyElementMain;
21use objc2_core_audio::{
22 kAudioAggregateDeviceClassID, kAudioDevicePropertyAvailableNominalSampleRates,
23 kAudioDevicePropertyBufferFrameSize, kAudioDevicePropertyBufferFrameSizeRange,
24 kAudioDevicePropertyLatency, kAudioDevicePropertyNominalSampleRate,
25 kAudioDevicePropertySafetyOffset, kAudioDevicePropertyStreamConfiguration,
26 kAudioDevicePropertyStreamFormat, kAudioObjectPropertyClass, kAudioObjectPropertyElementMaster,
27 kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyScopeInput,
28 kAudioObjectPropertyScopeOutput, AudioClassID, AudioDeviceID, AudioObjectGetPropertyData,
29 AudioObjectGetPropertyDataSize, AudioObjectID, AudioObjectPropertyAddress,
30 AudioObjectPropertyScope, AudioObjectSetPropertyData,
31};
32use objc2_core_audio_types::{
33 AudioBuffer, AudioBufferList, AudioStreamBasicDescription, AudioValueRange,
34};
35use objc2_core_foundation::CFString;
36use objc2_core_foundation::Type;
37
38pub use super::enumerate::{
39 default_input_device, default_output_device, SupportedInputConfigs, SupportedOutputConfigs,
40};
41use std::fmt;
42use std::mem::{self, size_of};
43use std::ptr::{null, NonNull};
44use std::sync::mpsc::{channel, RecvTimeoutError};
45use std::sync::{Arc, Mutex};
46use std::time::{Duration, Instant};
47
48use super::invoke_error_callback;
49use super::property_listener::AudioObjectPropertyListener;
50use coreaudio::audio_unit::macos_helpers::get_device_name;
51
52/// Attempt to set the device sample rate to the provided rate.
53/// Return an error if the requested sample rate is not supported by the device.
54fn set_sample_rate(
55 audio_device_id: AudioObjectID,
56 target_sample_rate: SampleRate,
57) -> Result<(), BuildStreamError> {
58 // Get the current sample rate.
59 let mut property_address = AudioObjectPropertyAddress {
60 mSelector: kAudioDevicePropertyNominalSampleRate,
61 mScope: kAudioObjectPropertyScopeGlobal,
62 mElement: kAudioObjectPropertyElementMaster,
63 };
64 let mut sample_rate: f64 = 0.0;
65 let mut data_size = mem::size_of::<f64>() as u32;
66 let status = unsafe {
67 AudioObjectGetPropertyData(
68 audio_device_id,
69 NonNull::from(&property_address),
70 0,
71 null(),
72 NonNull::from(&mut data_size),
73 NonNull::from(&mut sample_rate).cast(),
74 )
75 };
76 coreaudio::Error::from_os_status(status)?;
77
78 // If the requested sample rate is different to the device sample rate, update the device.
79 if sample_rate as u32 != target_sample_rate {
80 // Get available sample rate ranges.
81 property_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates;
82 let mut data_size = 0u32;
83 let status = unsafe {
84 AudioObjectGetPropertyDataSize(
85 audio_device_id,
86 NonNull::from(&property_address),
87 0,
88 null(),
89 NonNull::from(&mut data_size),
90 )
91 };
92 coreaudio::Error::from_os_status(status)?;
93 let n_ranges = data_size as usize / mem::size_of::<AudioValueRange>();
94 let mut ranges: Vec<AudioValueRange> = Vec::with_capacity(n_ranges);
95 let status = unsafe {
96 AudioObjectGetPropertyData(
97 audio_device_id,
98 NonNull::from(&property_address),
99 0,
100 null(),
101 NonNull::from(&mut data_size),
102 NonNull::new(ranges.as_mut_ptr()).unwrap().cast(),
103 )
104 };
105 coreaudio::Error::from_os_status(status)?;
106 unsafe {
107 ranges.set_len(n_ranges);
108 }
109
110 // Now that we have the available ranges, pick the one matching the desired rate.
111 let sample_rate = target_sample_rate;
112 if !ranges
113 .iter()
114 .any(|r| sample_rate as f64 >= r.mMinimum && sample_rate as f64 <= r.mMaximum)
115 {
116 return Err(BuildStreamError::StreamConfigNotSupported);
117 }
118
119 let (send, recv) = channel::<Result<f64, coreaudio::Error>>();
120 let sample_rate_address = AudioObjectPropertyAddress {
121 mSelector: kAudioDevicePropertyNominalSampleRate,
122 mScope: kAudioObjectPropertyScopeGlobal,
123 mElement: kAudioObjectPropertyElementMaster,
124 };
125 // Send sample rate updates back on a channel.
126 let sample_rate_handler = move || {
127 let mut rate: f64 = 0.0;
128 let mut data_size = mem::size_of::<f64>() as u32;
129
130 let result = unsafe {
131 AudioObjectGetPropertyData(
132 audio_device_id,
133 NonNull::from(&sample_rate_address),
134 0,
135 null(),
136 NonNull::from(&mut data_size),
137 NonNull::from(&mut rate).cast(),
138 )
139 };
140 send.send(coreaudio::Error::from_os_status(result).map(|_| rate))
141 .ok();
142 };
143
144 let listener = AudioObjectPropertyListener::new(
145 audio_device_id,
146 sample_rate_address,
147 sample_rate_handler,
148 )?;
149
150 // Finally, set the sample rate.
151 property_address.mSelector = kAudioDevicePropertyNominalSampleRate;
152 // Set the nominal sample rate using a single f64 as required by CoreAudio.
153 let rate = sample_rate as f64;
154 let data_size = mem::size_of::<f64>() as u32;
155 let status = unsafe {
156 AudioObjectSetPropertyData(
157 audio_device_id,
158 NonNull::from(&property_address),
159 0,
160 null(),
161 data_size,
162 NonNull::from(&rate).cast(),
163 )
164 };
165 coreaudio::Error::from_os_status(status)?;
166
167 // Wait for the reported_rate to change.
168 //
169 // This should not take longer than a few ms, but we timeout after 1 sec just in case.
170 // We loop over potentially several events from the channel to ensure
171 // that we catch the expected change in sample rate.
172 let mut timeout = Duration::from_secs(1);
173 let start = Instant::now();
174
175 loop {
176 match recv.recv_timeout(timeout) {
177 Err(err) => {
178 let description = match err {
179 RecvTimeoutError::Disconnected => {
180 "sample rate listener channel disconnected unexpectedly"
181 }
182 RecvTimeoutError::Timeout => {
183 "timeout waiting for sample rate update for device"
184 }
185 }
186 .to_string();
187 return Err(BackendSpecificError { description }.into());
188 }
189 Ok(Ok(reported_sample_rate)) => {
190 if reported_sample_rate == target_sample_rate as f64 {
191 break;
192 }
193 }
194 Ok(Err(_)) => {
195 // TODO: should we consider collecting this error?
196 }
197 };
198 timeout = timeout
199 .checked_sub(start.elapsed())
200 .unwrap_or(Duration::ZERO);
201 }
202 listener.remove()?;
203 }
204 Ok(())
205}
206
207fn audio_unit_from_device(device: &Device, input: bool) -> Result<AudioUnit, coreaudio::Error> {
208 let output_type = if !input && is_default_output_device(device) {
209 coreaudio::audio_unit::IOType::DefaultOutput
210 } else {
211 coreaudio::audio_unit::IOType::HalOutput
212 };
213 let mut audio_unit = AudioUnit::new(output_type)?;
214
215 if input {
216 // Enable input processing.
217 let enable_input = 1u32;
218 audio_unit.set_property(
219 kAudioOutputUnitProperty_EnableIO,
220 Scope::Input,
221 Element::Input,
222 Some(&enable_input),
223 )?;
224
225 // Disable output processing.
226 let disable_output = 0u32;
227 audio_unit.set_property(
228 kAudioOutputUnitProperty_EnableIO,
229 Scope::Output,
230 Element::Output,
231 Some(&disable_output),
232 )?;
233 }
234
235 // Device selection is a device-level property: always use Scope::Global + Element::Output
236 audio_unit.set_property(
237 kAudioOutputUnitProperty_CurrentDevice,
238 Scope::Global,
239 Element::Output,
240 Some(&device.audio_device_id),
241 )?;
242
243 Ok(audio_unit)
244}
245
246fn get_io_buffer_frame_size_range(
247 audio_unit: &AudioUnit,
248) -> Result<SupportedBufferSize, coreaudio::Error> {
249 // Device-level property: always use Scope::Global + Element::Output
250 // regardless of whether this audio unit is configured for input or output
251 let buffer_size_range: AudioValueRange = audio_unit.get_property(
252 kAudioDevicePropertyBufferFrameSizeRange,
253 Scope::Global,
254 Element::Output,
255 )?;
256
257 Ok(SupportedBufferSize::Range {
258 min: buffer_size_range.mMinimum as u32,
259 max: buffer_size_range.mMaximum as u32,
260 })
261}
262
263impl DeviceTrait for Device {
264 type SupportedInputConfigs = SupportedInputConfigs;
265 type SupportedOutputConfigs = SupportedOutputConfigs;
266 type Stream = Stream;
267
268 fn description(&self) -> Result<crate::DeviceDescription, DeviceNameError> {
269 Device::description(self)
270 }
271
272 fn id(&self) -> Result<DeviceId, DeviceIdError> {
273 Device::id(self)
274 }
275
276 fn supported_input_configs(
277 &self,
278 ) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
279 Device::supported_input_configs(self)
280 }
281
282 fn supported_output_configs(
283 &self,
284 ) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> {
285 Device::supported_output_configs(self)
286 }
287
288 fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
289 Device::default_input_config(self)
290 }
291
292 fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
293 Device::default_output_config(self)
294 }
295
296 fn build_input_stream_raw<D, E>(
297 &self,
298 config: StreamConfig,
299 sample_format: SampleFormat,
300 data_callback: D,
301 error_callback: E,
302 timeout: Option<Duration>,
303 ) -> Result<Self::Stream, BuildStreamError>
304 where
305 D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
306 E: FnMut(StreamError) + Send + 'static,
307 {
308 Device::build_input_stream_raw(
309 self,
310 config,
311 sample_format,
312 data_callback,
313 error_callback,
314 timeout,
315 )
316 }
317
318 fn build_output_stream_raw<D, E>(
319 &self,
320 config: StreamConfig,
321 sample_format: SampleFormat,
322 data_callback: D,
323 error_callback: E,
324 timeout: Option<Duration>,
325 ) -> Result<Self::Stream, BuildStreamError>
326 where
327 D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
328 E: FnMut(StreamError) + Send + 'static,
329 {
330 Device::build_output_stream_raw(
331 self,
332 config,
333 sample_format,
334 data_callback,
335 error_callback,
336 timeout,
337 )
338 }
339}
340
341#[derive(Clone, Eq, Hash, PartialEq)]
342pub struct Device {
343 pub(crate) audio_device_id: AudioDeviceID,
344}
345
346fn is_default_input_device(device: &Device) -> bool {
347 default_input_device().is_some_and(|d| d.audio_device_id == device.audio_device_id)
348}
349
350fn is_default_output_device(device: &Device) -> bool {
351 default_output_device().is_some_and(|d| d.audio_device_id == device.audio_device_id)
352}
353
354impl Device {
355 /// Construct a new device given its ID.
356 /// Useful for constructing hidden devices.
357 pub fn new(audio_device_id: AudioDeviceID) -> Self {
358 Self { audio_device_id }
359 }
360
361 /// Checks if this device is an aggregate device.
362 ///
363 /// Aggregate devices combine multiple physical devices into a single logical device.
364 fn is_aggregate_device(&self) -> bool {
365 let property_address = AudioObjectPropertyAddress {
366 mSelector: kAudioObjectPropertyClass,
367 mScope: kAudioObjectPropertyScopeGlobal,
368 mElement: kAudioObjectPropertyElementMain,
369 };
370
371 let mut class_id: AudioClassID = 0;
372 let data_size = size_of::<AudioClassID>() as u32;
373
374 // SAFETY: AudioObjectGetPropertyData is documented to write an AudioClassID
375 // for kAudioObjectPropertyClass. We check the status before using the value.
376 let status = unsafe {
377 AudioObjectGetPropertyData(
378 self.audio_device_id,
379 NonNull::from(&property_address),
380 0,
381 null(),
382 NonNull::from(&data_size),
383 NonNull::from(&mut class_id).cast(),
384 )
385 };
386
387 // If successful, check if it's an aggregate device
388 status == 0 && class_id == kAudioAggregateDeviceClassID
389 }
390
391 fn description(&self) -> Result<crate::DeviceDescription, DeviceNameError> {
392 let name = get_device_name(self.audio_device_id).map_err(|err| {
393 DeviceNameError::BackendSpecific {
394 err: BackendSpecificError {
395 description: err.to_string(),
396 },
397 }
398 })?;
399
400 let input_configs = self
401 .supported_input_configs()
402 .map(|configs| configs.count() as ChannelCount)
403 .ok();
404 let output_configs = self
405 .supported_output_configs()
406 .map(|configs| configs.count() as ChannelCount)
407 .ok();
408
409 let direction =
410 crate::device_description::direction_from_counts(input_configs, output_configs);
411
412 let mut builder = crate::DeviceDescriptionBuilder::new(name).direction(direction);
413
414 // Check if this is an aggregate device
415 if self.is_aggregate_device() {
416 builder = builder.interface_type(crate::InterfaceType::Aggregate);
417 }
418
419 Ok(builder.build())
420 }
421
422 fn id(&self) -> Result<DeviceId, DeviceIdError> {
423 let property_address = AudioObjectPropertyAddress {
424 mSelector: kAudioDevicePropertyDeviceUID,
425 mScope: kAudioObjectPropertyScopeGlobal,
426 mElement: kAudioObjectPropertyElementMain,
427 };
428
429 // CFString is copied from the audio object, use wrap_under_create_rule
430 let mut uid: *mut CFString = std::ptr::null_mut();
431 let mut data_size = size_of::<*mut CFString>() as u32;
432
433 // SAFETY: AudioObjectGetPropertyData is documented to write a CFString pointer
434 // for kAudioDevicePropertyDeviceUID. We check the status code before use.
435 let status = unsafe {
436 AudioObjectGetPropertyData(
437 self.audio_device_id,
438 NonNull::from(&property_address),
439 0,
440 null(),
441 NonNull::from(&mut data_size),
442 NonNull::from(&mut uid).cast(),
443 )
444 };
445 check_os_status(status)?;
446
447 // SAFETY: Status was successful, meaning the API call succeeded.
448 // We now check if the returned uid is non-null before use.
449 if !uid.is_null() {
450 let uid_string = unsafe { CFString::wrap_under_create_rule(uid).to_string() };
451 Ok(DeviceId(crate::platform::HostId::CoreAudio, uid_string))
452 } else {
453 Err(DeviceIdError::BackendSpecific {
454 err: BackendSpecificError {
455 description: "Device UID is null".to_string(),
456 },
457 })
458 }
459 }
460
461 // Logic re-used between `supported_input_configs` and `supported_output_configs`.
462 #[allow(clippy::cast_ptr_alignment)]
463 fn supported_configs(
464 &self,
465 scope: AudioObjectPropertyScope,
466 ) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> {
467 let mut property_address = AudioObjectPropertyAddress {
468 mSelector: kAudioDevicePropertyStreamConfiguration,
469 mScope: scope,
470 mElement: kAudioObjectPropertyElementMaster,
471 };
472
473 unsafe {
474 // Retrieve the devices audio buffer list.
475 let mut data_size = 0u32;
476 let status = AudioObjectGetPropertyDataSize(
477 self.audio_device_id,
478 NonNull::from(&property_address),
479 0,
480 null(),
481 NonNull::from(&mut data_size),
482 );
483 check_os_status(status)?;
484
485 let mut audio_buffer_list: Vec<u8> = vec![];
486 audio_buffer_list.reserve_exact(data_size as usize);
487 let status = AudioObjectGetPropertyData(
488 self.audio_device_id,
489 NonNull::from(&property_address),
490 0,
491 null(),
492 NonNull::from(&mut data_size),
493 NonNull::new(audio_buffer_list.as_mut_ptr()).unwrap().cast(),
494 );
495 check_os_status(status)?;
496
497 let audio_buffer_list = audio_buffer_list.as_mut_ptr() as *mut AudioBufferList;
498
499 // Read the number of buffers without assuming alignment (avoid UB).
500 let nb_ptr = core::ptr::addr_of!((*audio_buffer_list).mNumberBuffers);
501 let n_buffers = core::ptr::read_unaligned(nb_ptr) as usize;
502 // If there are no buffers, skip.
503 if n_buffers == 0 {
504 return Ok(vec![].into_iter());
505 }
506
507 // Count the number of channels as the sum of all channels in all output buffers.
508 let first_buf_ptr =
509 core::ptr::addr_of!((*audio_buffer_list).mBuffers) as *const AudioBuffer;
510 let mut n_channels = 0usize;
511 for i in 0..n_buffers {
512 let buf_ptr = first_buf_ptr.add(i);
513 // Read potentially unaligned
514 let buf: AudioBuffer = core::ptr::read_unaligned(buf_ptr);
515 n_channels += buf.mNumberChannels as usize;
516 }
517
518 // TODO: macOS should support U8, I16, I32, F32 and F64. This should allow for using
519 // I16 but just use F32 for now as it's the default anyway.
520 let sample_format = SampleFormat::F32;
521
522 // Get available sample rate ranges.
523 // The property "kAudioDevicePropertyAvailableNominalSampleRates" returns a list of pairs of
524 // minimum and maximum sample rates but most of the devices returns pairs of same values though the underlying mechanism is unclear.
525 // This may cause issues when, for example, sorting the configs by the sample rates.
526 // We follows the implementation of RtAudio, which returns single element of config
527 // when all the pairs have the same values and returns multiple elements otherwise.
528 // See https://github.com/thestk/rtaudio/blob/master/RtAudio.cpp#L1369C1-L1375C39
529
530 property_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates;
531 let mut data_size = 0u32;
532 let status = AudioObjectGetPropertyDataSize(
533 self.audio_device_id,
534 NonNull::from(&property_address),
535 0,
536 null(),
537 NonNull::from(&mut data_size),
538 );
539 check_os_status(status)?;
540
541 let n_ranges = data_size as usize / mem::size_of::<AudioValueRange>();
542 let mut ranges: Vec<AudioValueRange> = Vec::with_capacity(n_ranges);
543 let status = AudioObjectGetPropertyData(
544 self.audio_device_id,
545 NonNull::from(&property_address),
546 0,
547 null(),
548 NonNull::from(&mut data_size),
549 NonNull::new(ranges.as_mut_ptr()).unwrap().cast(),
550 );
551 check_os_status(status)?;
552
553 ranges.set_len(n_ranges);
554
555 #[allow(non_upper_case_globals)]
556 let input = match scope {
557 kAudioObjectPropertyScopeInput => Ok(true),
558 kAudioObjectPropertyScopeOutput => Ok(false),
559 _ => Err(BackendSpecificError {
560 description: format!("unexpected scope (neither input nor output): {scope:?}"),
561 }),
562 }?;
563 let audio_unit = audio_unit_from_device(self, input)?;
564 let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?;
565
566 // Collect the supported formats for the device.
567
568 let contains_different_sample_rates = ranges.iter().any(|r| r.mMinimum != r.mMaximum);
569 if ranges.is_empty() {
570 Ok(vec![].into_iter())
571 } else if contains_different_sample_rates {
572 let res = ranges.iter().map(|range| SupportedStreamConfigRange {
573 channels: n_channels as ChannelCount,
574 min_sample_rate: range.mMinimum as u32,
575 max_sample_rate: range.mMaximum as u32,
576 buffer_size,
577 sample_format,
578 });
579 Ok(res.collect::<Vec<_>>().into_iter())
580 } else {
581 let fmt = SupportedStreamConfigRange {
582 channels: n_channels as ChannelCount,
583 min_sample_rate: ranges
584 .iter()
585 .map(|v| v.mMinimum as u32)
586 .min()
587 .expect("the list must not be empty"),
588 max_sample_rate: ranges
589 .iter()
590 .map(|v| v.mMaximum as u32)
591 .max()
592 .expect("the list must not be empty"),
593 buffer_size,
594 sample_format,
595 };
596
597 Ok(vec![fmt].into_iter())
598 }
599 }
600 }
601
602 fn supported_input_configs(
603 &self,
604 ) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> {
605 self.supported_configs(kAudioObjectPropertyScopeInput)
606 }
607
608 fn supported_output_configs(
609 &self,
610 ) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> {
611 self.supported_configs(kAudioObjectPropertyScopeOutput)
612 }
613
614 fn default_config(
615 &self,
616 scope: AudioObjectPropertyScope,
617 ) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
618 fn default_config_error_from_os_status(
619 status: OSStatus,
620 ) -> Result<(), DefaultStreamConfigError> {
621 let err = match coreaudio::Error::from_os_status(status) {
622 Err(err) => err,
623 Ok(_) => return Ok(()),
624 };
625 match err {
626 coreaudio::Error::AudioUnit(
627 coreaudio::error::AudioUnitError::FormatNotSupported,
628 )
629 | coreaudio::Error::AudioCodec(_)
630 | coreaudio::Error::AudioFormat(_) => {
631 Err(DefaultStreamConfigError::StreamTypeNotSupported)
632 }
633 coreaudio::Error::AudioUnit(coreaudio::error::AudioUnitError::NoConnection) => {
634 Err(DefaultStreamConfigError::DeviceNotAvailable)
635 }
636 err => {
637 let description = format!("{err}");
638 let err = BackendSpecificError { description };
639 Err(err.into())
640 }
641 }
642 }
643
644 let property_address = AudioObjectPropertyAddress {
645 mSelector: kAudioDevicePropertyStreamFormat,
646 mScope: scope,
647 mElement: kAudioObjectPropertyElementMaster,
648 };
649
650 unsafe {
651 let mut asbd: AudioStreamBasicDescription = mem::zeroed();
652 let mut data_size = mem::size_of::<AudioStreamBasicDescription>() as u32;
653 let status = AudioObjectGetPropertyData(
654 self.audio_device_id,
655 NonNull::from(&property_address),
656 0,
657 null(),
658 NonNull::from(&mut data_size),
659 NonNull::from(&mut asbd).cast(),
660 );
661 default_config_error_from_os_status(status)?;
662
663 let sample_format = {
664 let audio_format = coreaudio::audio_unit::AudioFormat::from_format_and_flag(
665 asbd.mFormatID,
666 Some(asbd.mFormatFlags),
667 );
668 let flags = match audio_format {
669 Some(coreaudio::audio_unit::AudioFormat::LinearPCM(flags)) => flags,
670 _ => return Err(DefaultStreamConfigError::StreamTypeNotSupported),
671 };
672 let maybe_sample_format =
673 coreaudio::audio_unit::SampleFormat::from_flags_and_bits_per_sample(
674 flags,
675 asbd.mBitsPerChannel,
676 );
677 match maybe_sample_format {
678 Some(coreaudio::audio_unit::SampleFormat::F32) => SampleFormat::F32,
679 Some(coreaudio::audio_unit::SampleFormat::I16) => SampleFormat::I16,
680 _ => return Err(DefaultStreamConfigError::StreamTypeNotSupported),
681 }
682 };
683
684 #[allow(non_upper_case_globals)]
685 let input = match scope {
686 kAudioObjectPropertyScopeInput => Ok(true),
687 kAudioObjectPropertyScopeOutput => Ok(false),
688 _ => Err(BackendSpecificError {
689 description: format!("unexpected scope (neither input nor output): {scope:?}"),
690 }),
691 }?;
692 let audio_unit = audio_unit_from_device(self, input)?;
693 let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?;
694
695 let config = SupportedStreamConfig {
696 sample_rate: asbd.mSampleRate as _,
697 channels: asbd.mChannelsPerFrame as _,
698 buffer_size,
699 sample_format,
700 };
701 Ok(config)
702 }
703 }
704
705 fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
706 self.default_config(kAudioObjectPropertyScopeInput)
707 }
708
709 fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
710 self.default_config(kAudioObjectPropertyScopeOutput)
711 }
712
713 /// Check if this device supports input (recording).
714 fn supports_input(&self) -> bool {
715 // Check if the device has input channels by trying to get its input configuration
716 self.supported_input_configs()
717 .map(|mut configs| configs.next().is_some())
718 .unwrap_or(false)
719 }
720}
721
722impl fmt::Debug for Device {
723 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
724 f.debug_struct("Device")
725 .field("audio_device_id", &self.audio_device_id)
726 .field("name", &self.name())
727 .finish()
728 }
729}
730
731impl Device {
732 #[allow(clippy::cast_ptr_alignment)]
733 #[allow(clippy::while_immutable_condition)]
734 #[allow(clippy::float_cmp)]
735 fn build_input_stream_raw<D, E>(
736 &self,
737 config: StreamConfig,
738 sample_format: SampleFormat,
739 mut data_callback: D,
740 error_callback: E,
741 _timeout: Option<Duration>,
742 ) -> Result<Stream, BuildStreamError>
743 where
744 D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
745 E: FnMut(StreamError) + Send + 'static,
746 {
747 // The scope and element for working with a device's input stream.
748 let scope = Scope::Output;
749 let element = Element::Input;
750
751 // Potentially change the device sample rate to match the config.
752 set_sample_rate(self.audio_device_id, config.sample_rate)?;
753
754 let mut loopback_aggregate: Option<LoopbackDevice> = None;
755 let mut audio_unit = if self.supports_input() {
756 audio_unit_from_device(self, true)?
757 } else {
758 loopback_aggregate.replace(LoopbackDevice::from_device(self)?);
759 audio_unit_from_device(&loopback_aggregate.as_ref().unwrap().aggregate_device, true)?
760 };
761
762 // Configure stream format and buffer size for predictable callback behavior.
763 configure_stream_format_and_buffer(&mut audio_unit, config, sample_format, scope, element)?;
764
765 let error_callback = Arc::new(Mutex::new(error_callback));
766 let error_callback_disconnect = error_callback.clone();
767
768 // Register the callback that is being called by coreaudio whenever it needs data to be
769 // fed to the audio buffer.
770 let (bytes_per_channel, sample_rate, device_buffer_frames, extra_latency_frames) =
771 setup_callback_vars(&audio_unit, config, sample_format, Scope::Input);
772
773 type Args = render_callback::Args<data::Raw>;
774 audio_unit.set_input_callback(move |args: Args| unsafe {
775 // SAFETY: We configure the stream format as interleaved (via asbd_from_config which
776 // does not set kAudioFormatFlagIsNonInterleaved). Interleaved format always has
777 // exactly one buffer containing all channels, so mBuffers[0] is always valid.
778 let AudioBuffer {
779 mNumberChannels: channels,
780 mDataByteSize: data_byte_size,
781 mData: data,
782 } = (*args.data.data).mBuffers[0];
783
784 let data = data as *mut ();
785 let len = data_byte_size as usize / bytes_per_channel;
786 let data = Data::from_parts(data, len, sample_format);
787
788 let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) {
789 Err(err) => {
790 invoke_error_callback(&error_callback, err.into());
791 return Err(());
792 }
793 Ok(cb) => cb,
794 };
795 let buffer_frames = len / channels as usize;
796 let latency_frames =
797 device_buffer_frames.unwrap_or(buffer_frames) + extra_latency_frames;
798 let delay = frames_to_duration(latency_frames, sample_rate);
799 let capture = callback
800 .sub(delay)
801 .expect("`capture` occurs before origin of alsa `StreamInstant`");
802 let timestamp = crate::InputStreamTimestamp { callback, capture };
803
804 let info = InputCallbackInfo { timestamp };
805 data_callback(&data, &info);
806 Ok(())
807 })?;
808
809 // Create error callback for stream - either dummy or real based on device type
810 let error_callback_for_stream: super::ErrorCallback = if is_default_input_device(self) {
811 Box::new(|_: StreamError| {})
812 } else {
813 let error_callback_clone = error_callback_disconnect.clone();
814 Box::new(move |err: StreamError| {
815 invoke_error_callback(&error_callback_clone, err);
816 })
817 };
818
819 let stream = Stream::new(
820 StreamInner {
821 playing: true,
822 audio_unit,
823 device_id: self.audio_device_id,
824 _loopback_device: loopback_aggregate,
825 },
826 error_callback_for_stream,
827 )?;
828
829 stream
830 .inner
831 .lock()
832 .map_err(|_| BuildStreamError::BackendSpecific {
833 err: BackendSpecificError {
834 description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(),
835 },
836 })?
837 .audio_unit
838 .start()?;
839
840 Ok(stream)
841 }
842
843 fn build_output_stream_raw<D, E>(
844 &self,
845 config: StreamConfig,
846 sample_format: SampleFormat,
847 mut data_callback: D,
848 error_callback: E,
849 _timeout: Option<Duration>,
850 ) -> Result<Stream, BuildStreamError>
851 where
852 D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
853 E: FnMut(StreamError) + Send + 'static,
854 {
855 let mut audio_unit = audio_unit_from_device(self, false)?;
856
857 // The scope and element for working with a device's output stream.
858 let scope = Scope::Input;
859 let element = Element::Output;
860
861 // Configure device buffer (see comprehensive documentation in input stream above)
862 configure_stream_format_and_buffer(&mut audio_unit, config, sample_format, scope, element)?;
863
864 let error_callback = Arc::new(Mutex::new(error_callback));
865 let error_callback_disconnect = error_callback.clone();
866
867 // Register the callback that is being called by coreaudio whenever it needs data to be
868 // fed to the audio buffer.
869 let (bytes_per_channel, sample_rate, device_buffer_frames, extra_latency_frames) =
870 setup_callback_vars(&audio_unit, config, sample_format, Scope::Output);
871
872 type Args = render_callback::Args<data::Raw>;
873 audio_unit.set_render_callback(move |args: Args| unsafe {
874 // SAFETY: We configure the stream format as interleaved (via asbd_from_config which
875 // does not set kAudioFormatFlagIsNonInterleaved). Interleaved format always has
876 // exactly one buffer containing all channels, so mBuffers[0] is always valid.
877 let AudioBuffer {
878 mNumberChannels: channels,
879 mDataByteSize: data_byte_size,
880 mData: data,
881 } = (*args.data.data).mBuffers[0];
882
883 let data = data as *mut ();
884 let len = data_byte_size as usize / bytes_per_channel;
885 let mut data = Data::from_parts(data, len, sample_format);
886
887 let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) {
888 Err(err) => {
889 invoke_error_callback(&error_callback, err.into());
890 return Err(());
891 }
892 Ok(cb) => cb,
893 };
894 let buffer_frames = len / channels as usize;
895 // Use device buffer size for latency calculation if available
896 let latency_frames =
897 device_buffer_frames.unwrap_or(buffer_frames) + extra_latency_frames;
898 let delay = frames_to_duration(latency_frames, sample_rate);
899 let playback = callback
900 .add(delay)
901 .expect("`playback` occurs beyond representation supported by `StreamInstant`");
902 let timestamp = crate::OutputStreamTimestamp { callback, playback };
903
904 let info = OutputCallbackInfo { timestamp };
905 data_callback(&mut data, &info);
906 Ok(())
907 })?;
908
909 // Create error callback for stream - either dummy or real based on device type
910 let error_callback_for_stream: super::ErrorCallback = if is_default_output_device(self) {
911 Box::new(|_: StreamError| {})
912 } else {
913 let error_callback_clone = error_callback_disconnect.clone();
914 Box::new(move |err: StreamError| {
915 invoke_error_callback(&error_callback_clone, err);
916 })
917 };
918
919 let stream = Stream::new(
920 StreamInner {
921 playing: true,
922 audio_unit,
923 device_id: self.audio_device_id,
924 _loopback_device: None,
925 },
926 error_callback_for_stream,
927 )?;
928
929 stream
930 .inner
931 .lock()
932 .map_err(|_| BuildStreamError::BackendSpecific {
933 err: BackendSpecificError {
934 description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(),
935 },
936 })?
937 .audio_unit
938 .start()?;
939
940 Ok(stream)
941 }
942}
943
944/// Configure stream format and buffer size for CoreAudio stream.
945///
946/// This handles the common setup tasks for both input and output streams:
947/// - Sets the stream format (ASBD)
948/// - Configures buffer size for Fixed buffer size requests
949fn configure_stream_format_and_buffer(
950 audio_unit: &mut AudioUnit,
951 config: StreamConfig,
952 sample_format: SampleFormat,
953 scope: Scope,
954 element: Element,
955) -> Result<(), BuildStreamError> {
956 // Set the stream format using stream-specific scope/element
957 // - Input streams: scope=Output, element=Input (configuring output format of input element)
958 // - Output streams: scope=Input, element=Output (configuring input format of output element)
959 let asbd = asbd_from_config(config, sample_format);
960 audio_unit.set_property(kAudioUnitProperty_StreamFormat, scope, element, Some(&asbd))?;
961
962 // Configure device buffer size if requested
963 if let BufferSize::Fixed(buffer_size) = config.buffer_size {
964 // IMPORTANT: Buffer frame size is a DEVICE-LEVEL property, not stream-specific.
965 // Unlike stream format above, we ALWAYS use Scope::Global + Element::Output
966 // for device properties, regardless of whether this is an input or output stream.
967 // This is consistent with other device properties like:
968 // - kAudioOutputUnitProperty_CurrentDevice
969 // - kAudioDevicePropertyBufferFrameSizeRange
970 // The Element::Output here doesn't mean "output stream only" - it's the
971 // canonical element used for device-wide properties in Core Audio.
972 audio_unit.set_property(
973 kAudioDevicePropertyBufferFrameSize,
974 Scope::Global,
975 Element::Output,
976 Some(&buffer_size),
977 )?;
978 }
979
980 Ok(())
981}
982
983/// Returns the sum of the device latency and safety offset in frames.
984fn get_device_extra_latency_frames(audio_unit: &AudioUnit, scope: Scope) -> usize {
985 let device_latency: u32 = audio_unit
986 .get_property(kAudioDevicePropertyLatency, scope, Element::Output)
987 .unwrap_or(0);
988 let safety_offset: u32 = audio_unit
989 .get_property(kAudioDevicePropertySafetyOffset, scope, Element::Output)
990 .unwrap_or(0);
991 (device_latency + safety_offset) as usize
992}
993
994/// Setup common callback variables, querying both the I/O buffer size and extra hardware latency.
995///
996/// Returns `(bytes_per_channel, sample_rate, device_buffer_frames, extra_latency_frames)`
997fn setup_callback_vars(
998 audio_unit: &AudioUnit,
999 config: StreamConfig,
1000 sample_format: SampleFormat,
1001 scope: Scope,
1002) -> (usize, crate::SampleRate, Option<usize>, usize) {
1003 let bytes_per_channel = sample_format.sample_size();
1004 let sample_rate = config.sample_rate;
1005
1006 let device_buffer_frames = get_device_buffer_frame_size(audio_unit).ok();
1007 let extra_latency_frames = get_device_extra_latency_frames(audio_unit, scope);
1008
1009 (
1010 bytes_per_channel,
1011 sample_rate,
1012 device_buffer_frames,
1013 extra_latency_frames,
1014 )
1015}
1016
1017/// Query the current device buffer frame size from CoreAudio.
1018///
1019/// Buffer frame size is a device-level property that always uses Scope::Global + Element::Output,
1020/// regardless of whether the audio unit is configured for input or output streams.
1021pub(crate) fn get_device_buffer_frame_size(
1022 audio_unit: &AudioUnit,
1023) -> Result<usize, coreaudio::Error> {
1024 // Device-level property: always use Scope::Global + Element::Output
1025 // This is consistent with how we set the buffer size and query the buffer size range
1026 let frames: u32 = audio_unit.get_property(
1027 kAudioDevicePropertyBufferFrameSize,
1028 Scope::Global,
1029 Element::Output,
1030 )?;
1031 Ok(frames as usize)
1032}