fix(audio): check real CoreAudio device rate with macOS Shared Mode

This commit is contained in:
Vudgekek committed 2026-05-07 14:51:51 -05:00
1 parent 88e072a4ac
commit 5f10b4baa4
1 file changed
+90 -2
+90 -2
View File
@@ -504,8 +504,36 @@ impl AudioBackend for CpalDefaultBackend {
.ok_or_else(|| "No default output device found".to_string())?
};
let mixer_sink = DeviceSinkBuilder::from_device(device)
.map_err(|e| format!("Failed to create device sink builder: {}", e))?
#[cfg(target_os = "macos")]
let shared_mode_override = if !config.exclusive_mode {
Self::shared_mode_nominal_stream_config(
&device,
effective_device_id.map(|name| name.as_str()),
)
} else {
None
};
let builder = DeviceSinkBuilder::from_device(device)
.map_err(|e| format!("Failed to create device sink builder: {}", e))?;
#[cfg(target_os = "macos")]
let mixer_sink = if let Some(override_config) = shared_mode_override {
let buffer_size =
rodio::cpal::BufferSize::Fixed((override_config.sample_rate() / 20).max(64));
builder
.with_supported_config(&override_config)
.with_buffer_size(buffer_size)
.open_stream()
.map_err(|e| format!("Failed to create output stream: {}", e))?
} else {
builder
.open_stream()
.map_err(|e| format!("Failed to create output stream: {}", e))?
};
#[cfg(not(target_os = "macos"))]
let mixer_sink = builder
.open_stream()
.map_err(|e| format!("Failed to create output stream: {}", e))?;
@@ -561,6 +589,66 @@ impl AudioBackend for CpalDefaultBackend {
#[cfg(target_os = "macos")]
impl CpalDefaultBackend {
/// In macOS shared mode, CPAL's default config can briefly report a stale
/// sample rate after CoreAudio changes the device's nominal rate. If we
/// trust the stale rate, playback can run at the wrong speed until the
/// stream is recreated. Prefer opening the CPAL stream at CoreAudio's
/// current nominal rate when the two disagree.
fn shared_mode_nominal_stream_config(
device: &rodio::cpal::Device,
effective_device_name: Option<&str>,
) -> Option<rodio::cpal::SupportedStreamConfig> {
use crate::coreaudio_direct;
let device_id = match effective_device_name {
Some(name) => coreaudio_direct::find_device_by_name(name).ok().flatten(),
None => coreaudio_direct::get_default_output_device().ok(),
}?;
let nominal_rate = coreaudio_direct::get_nominal_sample_rate(device_id).ok()?;
let default_config = device.default_output_config().ok()?;
let default_rate = default_config.sample_rate();
if nominal_rate == default_rate {
return None;
}
let supported_configs: Vec<_> = device.supported_output_configs().ok()?.collect();
let matching_config = supported_configs
.iter()
.find_map(|range| {
if range.channels() == default_config.channels()
&& range.sample_format() == default_config.sample_format()
{
(*range).try_with_sample_rate(nominal_rate)
} else {
None
}
})
.or_else(|| {
supported_configs
.iter()
.find_map(|range| (*range).try_with_sample_rate(nominal_rate))
});
let device_label = effective_device_name.unwrap_or("System Default");
if matching_config.is_some() {
log::warn!(
"[CoreAudio] Shared-mode rate mismatch on '{}': CPAL default {}Hz vs CoreAudio nominal {}Hz. Opening stream at the nominal rate to avoid wrong-speed playback.",
device_label,
default_rate,
nominal_rate
);
} else {
log::warn!(
"[CoreAudio] Shared-mode rate mismatch on '{}': CPAL default {}Hz vs CoreAudio nominal {}Hz, but no supported CPAL config matched the nominal rate.",
device_label,
default_rate,
nominal_rate
);
}
matching_config
}
/// Probe a macOS audio device for capabilities via CoreAudio APIs.
/// Returns (supported_rates, max_rate, bus_type, is_hardware).
fn probe_macos_device(