feat(macos): enhance CpalDefaultBackend with device capabilities

On macOS, enumerate_devices() now populates supported_sample_rates,
max_sample_rate, device_bus, and is_hardware via CoreAudio probing.
create_output_stream() switches the device sample rate to match the
track before opening the CPAL stream.
This commit is contained in:
Afonso Jorge Ramos committed 2026-03-31 02:20:08 +02:00
1 parent c51f07fe65
commit 4c15a43200
1 file changed
+132 -4
+132 -4
View File
@@ -391,15 +391,29 @@ impl AudioBackend for CpalDefaultBackend {
.map(|desc| desc.name().to_string())
.unwrap_or_else(|_| "Unknown Device".to_string());
let is_default = name == default_name;
// On macOS, probe device capabilities via CoreAudio
#[cfg(target_os = "macos")]
let (supported_rates, max_rate, bus_type, is_hw) = {
Self::probe_macos_device(&name)
};
#[cfg(not(target_os = "macos"))]
let (supported_rates, max_rate, bus_type, is_hw): (
Option<Vec<u32>>,
Option<u32>,
Option<String>,
bool,
) = (None, None, None, false);
devices.push(AudioDevice {
id: name.clone(),
name,
description: None,
is_default,
max_sample_rate: None,
supported_sample_rates: None,
device_bus: None,
is_hardware: false,
max_sample_rate: max_rate,
supported_sample_rates: supported_rates,
device_bus: bus_type,
is_hardware: is_hw,
});
}
@@ -407,6 +421,17 @@ impl AudioBackend for CpalDefaultBackend {
}
fn create_output_stream(&self, config: &BackendConfig) -> BackendResult<MixerDeviceSink> {
// On macOS, switch device sample rate to match the requested rate
#[cfg(target_os = "macos")]
{
if let Some(ref device_id) = config.device_id {
Self::switch_sample_rate_if_needed(device_id, config.sample_rate);
} else {
// Switch default device rate
Self::switch_default_device_rate_if_needed(config.sample_rate);
}
}
let device = if let Some(ref device_id) = config.device_id {
self.host
.output_devices()
@@ -443,3 +468,106 @@ impl AudioBackend for CpalDefaultBackend {
self
}
}
#[cfg(target_os = "macos")]
impl CpalDefaultBackend {
/// Probe a macOS audio device for capabilities via CoreAudio APIs.
/// Returns (supported_rates, max_rate, bus_type, is_hardware).
fn probe_macos_device(
device_name: &str,
) -> (Option<Vec<u32>>, Option<u32>, Option<String>, bool) {
use crate::coreaudio_direct;
let device_id = match coreaudio_direct::find_device_by_name(device_name) {
Ok(Some(id)) => {
log::info!("[CoreAudio] Found device '{}' with ID {}", device_name, id);
id
}
Ok(None) => {
log::debug!("[CoreAudio] Device '{}' not found via CoreAudio", device_name);
return (None, None, None, false);
}
Err(e) => {
log::debug!("[CoreAudio] Error finding device '{}': {}", device_name, e);
return (None, None, None, false);
}
};
let supported_rates = coreaudio_direct::query_supported_sample_rates(device_id)
.inspect(|rates| log::info!("[CoreAudio] Device '{}' supported rates: {:?}", device_name, rates))
.inspect_err(|e| log::warn!("[CoreAudio] Failed to query rates for '{}': {}", device_name, e))
.ok()
.filter(|r| !r.is_empty());
let max_rate = supported_rates
.as_ref()
.and_then(|rates| rates.iter().max().copied());
let bus_type = coreaudio_direct::get_device_transport_type(device_id);
let is_hardware = bus_type
.as_deref()
.is_some_and(|t| t == "usb" || t == "built-in" || t == "thunderbolt" || t == "firewire");
(supported_rates, max_rate, bus_type, is_hardware)
}
/// Switch device sample rate before stream creation (if device supports the target rate).
fn switch_sample_rate_if_needed(device_name: &str, target_rate: u32) {
use crate::coreaudio_direct;
log::info!("[CoreAudio] Rate switch requested: device='{}' target={}Hz", device_name, target_rate);
let device_id = match coreaudio_direct::find_device_by_name(device_name) {
Ok(Some(id)) => id,
Ok(None) => {
log::warn!("[CoreAudio] Cannot switch rate: device '{}' not found", device_name);
return;
}
Err(e) => {
log::warn!("[CoreAudio] Cannot switch rate: {}", e);
return;
}
};
// Check if device supports the target rate
if let Ok(rates) = coreaudio_direct::query_supported_sample_rates(device_id) {
if !rates.contains(&target_rate) {
log::debug!(
"[CoreAudio] Device '{}' does not support {}Hz, skipping rate switch",
device_name,
target_rate
);
return;
}
}
if let Err(e) = coreaudio_direct::set_nominal_sample_rate(device_id, target_rate) {
log::warn!("[CoreAudio] Failed to switch sample rate: {}", e);
}
}
/// Switch the default output device's sample rate.
fn switch_default_device_rate_if_needed(target_rate: u32) {
use crate::coreaudio_direct;
let device_id = match coreaudio_direct::get_default_output_device() {
Ok(id) => id,
Err(e) => {
log::debug!("[CoreAudio] Could not get default device for rate switch: {}", e);
return;
}
};
if let Ok(rates) = coreaudio_direct::query_supported_sample_rates(device_id) {
if !rates.contains(&target_rate) {
log::debug!(
"[CoreAudio] Default device does not support {}Hz, skipping rate switch",
target_rate
);
return;
}
}
if let Err(e) = coreaudio_direct::set_nominal_sample_rate(device_id, target_rate) {
log::warn!("[CoreAudio] Failed to switch default device sample rate: {}", e);
}
}
}