fix(audio): check real CoreAudio device rate with macOS Shared Mode
This commit is contained in:
1 file changed
+90
-2
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user