Skip to content

Commit 5684013

Browse files
afonsojramossimlay
andauthored
feat: add get_available_sample_rates and get_device_transport_type helpers (#149)
* feat: add get_available_sample_rates and get_device_transport_type helpers Add two new helper functions to macos_helpers: - get_available_sample_rates(device_id): queries kAudioDevicePropertyAvailableNominalSampleRates and returns the supported sample rate ranges as Vec<AudioValueRange> - get_device_transport_type(device_id): queries kAudioDevicePropertyTransportType and returns the transport type constant (USB, built-in, Bluetooth, etc.) These are commonly needed when building audio applications that need to probe device capabilities, for example to implement sample rate switching or to distinguish USB DACs from built-in speakers. * style: apply rustfmt formatting * refactor: minimize unsafe blocks and add SAFETY comments * Minor nit fix. --------- Co-authored-by: Sebastian Imlay <sebastian.imlay@gmail.com>
1 parent 84aef22 commit 5684013

1 file changed

Lines changed: 115 additions & 5 deletions

File tree

src/audio_unit/macos_helpers.rs

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ use objc2_core_audio::{
1818
kAudioDevicePropertyAvailableNominalSampleRates, kAudioDevicePropertyDeviceIsAlive,
1919
kAudioDevicePropertyDeviceNameCFString, kAudioDevicePropertyHogMode,
2020
kAudioDevicePropertyNominalSampleRate, kAudioDevicePropertyScopeOutput,
21-
kAudioDevicePropertyStreamConfiguration, kAudioHardwareNoError,
22-
kAudioHardwarePropertyDefaultInputDevice, kAudioHardwarePropertyDefaultOutputDevice,
23-
kAudioHardwarePropertyDevices, kAudioObjectPropertyElementMaster,
24-
kAudioObjectPropertyElementWildcard, kAudioObjectPropertyScopeGlobal,
25-
kAudioObjectPropertyScopeInput, kAudioObjectPropertyScopeOutput, kAudioObjectSystemObject,
21+
kAudioDevicePropertyStreamConfiguration, kAudioDevicePropertyTransportType,
22+
kAudioHardwareNoError, kAudioHardwarePropertyDefaultInputDevice,
23+
kAudioHardwarePropertyDefaultOutputDevice, kAudioHardwarePropertyDevices,
24+
kAudioObjectPropertyElementMaster, kAudioObjectPropertyElementWildcard,
25+
kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyScopeInput,
26+
kAudioObjectPropertyScopeOutput, kAudioObjectSystemObject,
2627
kAudioStreamPropertyAvailablePhysicalFormats, kAudioStreamPropertyPhysicalFormat,
2728
AudioDeviceID, AudioObjectAddPropertyListener, AudioObjectGetPropertyData,
2829
AudioObjectGetPropertyDataSize, AudioObjectID, AudioObjectPropertyAddress,
@@ -188,6 +189,37 @@ fn test_get_audio_device_ids() {
188189
let _ = get_audio_device_ids().expect("Failed to get audio device ids");
189190
}
190191

192+
#[test]
193+
fn test_get_available_sample_rates() {
194+
if let Some(device_id) = get_default_device_id(false) {
195+
let rates = get_available_sample_rates(device_id).expect("Failed to get sample rates");
196+
assert!(
197+
!rates.is_empty(),
198+
"Default output device should support at least one sample rate"
199+
);
200+
for range in &rates {
201+
assert!(
202+
range.mMinimum > 0.0,
203+
"Sample rate minimum should be positive"
204+
);
205+
assert!(
206+
range.mMaximum >= range.mMinimum,
207+
"Maximum should be >= minimum"
208+
);
209+
}
210+
}
211+
}
212+
213+
#[test]
214+
fn test_get_device_transport_type() {
215+
if let Some(device_id) = get_default_device_id(false) {
216+
let transport = get_device_transport_type(device_id).expect("Failed to get transport type");
217+
// Transport type should be a non-zero FourCC value (or 0 for unknown)
218+
// The default output device typically has a known transport type
219+
let _ = transport;
220+
}
221+
}
222+
191223
#[test]
192224
fn test_get_audio_device_ids_for_scope() {
193225
for scope in &[
@@ -577,6 +609,84 @@ pub fn get_supported_physical_stream_formats(
577609
Ok(allformats)
578610
}
579611

612+
/// Get the available nominal sample rate ranges for a device.
613+
///
614+
/// Returns a vector of [`AudioValueRange`] where each entry describes a supported range.
615+
/// For devices that support discrete rates, `mMinimum` and `mMaximum` will be equal.
616+
/// For devices that support continuous ranges, they will differ.
617+
pub fn get_available_sample_rates(device_id: AudioDeviceID) -> Result<Vec<AudioValueRange>, Error> {
618+
let property_address = AudioObjectPropertyAddress {
619+
mSelector: kAudioDevicePropertyAvailableNominalSampleRates,
620+
mScope: kAudioObjectPropertyScopeGlobal,
621+
mElement: kAudioObjectPropertyElementMaster,
622+
};
623+
624+
let mut data_size = 0u32;
625+
// SAFETY: AudioObjectGetPropertyDataSize is a CoreAudio C API that reads
626+
// the size of the property data into data_size. All pointers passed are
627+
// valid stack references via NonNull::from.
628+
let status = unsafe {
629+
AudioObjectGetPropertyDataSize(
630+
device_id,
631+
NonNull::from(&property_address),
632+
0,
633+
null(),
634+
NonNull::from(&mut data_size),
635+
)
636+
};
637+
Error::from_os_status(status)?;
638+
639+
let n_ranges = data_size as usize / mem::size_of::<AudioValueRange>();
640+
let mut ranges: Vec<AudioValueRange> = Vec::with_capacity(n_ranges);
641+
// SAFETY: AudioObjectGetPropertyData writes exactly n_ranges elements
642+
// (validated by data_size from the previous call) into the pre-allocated
643+
// buffer. AudioValueRange is a plain C struct with no drop semantics.
644+
let status = unsafe {
645+
ranges.set_len(n_ranges);
646+
AudioObjectGetPropertyData(
647+
device_id,
648+
NonNull::from(&property_address),
649+
0,
650+
null(),
651+
NonNull::from(&data_size),
652+
NonNull::new(ranges.as_mut_ptr()).unwrap().cast(),
653+
)
654+
};
655+
Error::from_os_status(status)?;
656+
Ok(ranges)
657+
}
658+
659+
/// Get the transport type of a device.
660+
///
661+
/// The returned value is one of the `kAudioDeviceTransportType*` constants
662+
/// (e.g. `kAudioDeviceTransportTypeUSB`, `kAudioDeviceTransportTypeBuiltIn`).
663+
pub fn get_device_transport_type(device_id: AudioDeviceID) -> Result<u32, Error> {
664+
let property_address = AudioObjectPropertyAddress {
665+
mSelector: kAudioDevicePropertyTransportType,
666+
mScope: kAudioObjectPropertyScopeGlobal,
667+
mElement: kAudioObjectPropertyElementMaster,
668+
};
669+
670+
let mut transport_type: u32 = 0;
671+
let data_size = mem::size_of::<u32>() as u32;
672+
// SAFETY: AudioObjectGetPropertyData is a CoreAudio C API that reads
673+
// the transport type (a u32) into transport_type. All pointers passed
674+
// are valid stack references via NonNull::from. data_size matches the
675+
// size of the output buffer.
676+
let status = unsafe {
677+
AudioObjectGetPropertyData(
678+
device_id,
679+
NonNull::from(&property_address),
680+
0,
681+
null(),
682+
NonNull::from(&data_size),
683+
NonNull::from(&mut transport_type).cast(),
684+
)
685+
};
686+
Error::from_os_status(status)?;
687+
Ok(transport_type)
688+
}
689+
580690
/// Changing the sample rate is an asynchronous process.
581691
/// A RateListener can be used to get notified when the rate is changed.
582692
pub struct RateListener {

0 commit comments

Comments
 (0)