fix(macos): harden CoreAudio exclusive stream handling
Address review feedback from the macOS Exclusive Mode PR. Pin the resolved CoreAudio output device before acquiring Hog Mode so System Default cannot drift between Hog Mode acquisition, sample-rate switching, and stream creation. Fall back to software volume when CoreAudio hardware volume is unavailable, which keeps the volume slider usable on knob-only DACs. Snapshot and restore CoreAudio hardware volume around Exclusive Mode so leaving Exclusive Mode does not leave the output device at QBZ's last hardware volume. Refuse shared-mode fallback when macOS Exclusive Mode stream creation fails, so the app does not silently play in non-exclusive mode while the UI still reports Exclusive Mode. Clean up Linux unused-variable warnings from the macOS-only guard plumbing.
This commit is contained in:
1 parent
96fd15cb33
commit
88e072a4ac
4 files changed
+238
-116
No files matched your search
@@ -520,20 +520,30 @@ impl AudioBackend for CpalDefaultBackend {
|
||||
MixerDeviceSink,
|
||||
Option<crate::coreaudio_direct::CoreAudioExclusiveGuard>,
|
||||
)> {
|
||||
let guard = if config.exclusive_mode {
|
||||
let device_id =
|
||||
crate::coreaudio_direct::resolve_output_device_id(config.device_id.as_deref())?;
|
||||
Some(crate::coreaudio_direct::CoreAudioExclusiveGuard::acquire(
|
||||
device_id,
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match self.create_output_stream(config) {
|
||||
Ok(sink) => Ok((sink, guard)),
|
||||
Err(e) => Err(e),
|
||||
if !config.exclusive_mode {
|
||||
return self.create_output_stream(config).map(|sink| (sink, None));
|
||||
}
|
||||
|
||||
// Resolve the target device ONCE before acquiring Hog Mode and pin its
|
||||
// name into the config we hand to `create_output_stream`. Without this,
|
||||
// the rate-switch and CPAL-stream paths inside `create_output_stream`
|
||||
// re-resolve the System Default by name — and macOS reassigns the
|
||||
// System Default the moment we hog the previous one (so other apps
|
||||
// still get audio). The result is Hog Mode held on device A while the
|
||||
// stream is created on device B, which on a multi-device machine
|
||||
// (e.g. an external DAC alongside built-in speakers) leaves the DAC
|
||||
// hogged-but-unused and audio routed to a device that may not even
|
||||
// support the requested sample rate.
|
||||
let device_id =
|
||||
crate::coreaudio_direct::resolve_output_device_id(config.device_id.as_deref())?;
|
||||
let device_name = crate::coreaudio_direct::get_device_name(device_id)?;
|
||||
let guard = crate::coreaudio_direct::CoreAudioExclusiveGuard::acquire(device_id)?;
|
||||
|
||||
let mut effective_config = config.clone();
|
||||
effective_config.device_id = Some(device_name);
|
||||
|
||||
self.create_output_stream(&effective_config)
|
||||
.map(|sink| (sink, Some(guard)))
|
||||
}
|
||||
|
||||
fn is_available(&self) -> bool {
|
||||
|
||||
@@ -234,6 +234,47 @@ pub fn set_hog_mode(device_id: AudioDeviceID, enabled: bool) -> Result<(), Strin
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a CoreAudio device's current hardware output volume as scalar 0.0..1.0.
|
||||
/// Tries master output first, then common stereo channel elements.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn get_hardware_volume(device_id: AudioDeviceID) -> Result<f32, String> {
|
||||
for element in [kAudioObjectPropertyElementMaster, 1, 2] {
|
||||
let property_address = AudioObjectPropertyAddress {
|
||||
mSelector: kAudioDevicePropertyVolumeScalar,
|
||||
mScope: kAudioObjectPropertyScopeOutput,
|
||||
mElement: element,
|
||||
};
|
||||
|
||||
let has_property =
|
||||
unsafe { AudioObjectHasProperty(device_id, NonNull::from(&property_address)) };
|
||||
if !has_property {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut value = 0.0_f32;
|
||||
let data_size = mem::size_of::<f32>() as u32;
|
||||
let status = unsafe {
|
||||
AudioObjectGetPropertyData(
|
||||
device_id,
|
||||
NonNull::from(&property_address),
|
||||
0,
|
||||
null(),
|
||||
NonNull::from(&data_size),
|
||||
NonNull::from(&mut value).cast(),
|
||||
)
|
||||
};
|
||||
|
||||
if status == 0 {
|
||||
return Ok(value.clamp(0.0, 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"CoreAudio device {} does not expose readable output hardware volume",
|
||||
device_id
|
||||
))
|
||||
}
|
||||
|
||||
/// Set a CoreAudio device's hardware output volume using scalar 0.0..1.0.
|
||||
/// Tries master output first, then common stereo channel elements.
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -298,21 +339,41 @@ pub fn set_hardware_volume(device_id: AudioDeviceID, volume: f32) -> Result<(),
|
||||
}
|
||||
|
||||
/// RAII owner for CoreAudio Hog Mode.
|
||||
///
|
||||
/// Captures the device's hardware volume on acquire and restores it
|
||||
/// when released, so leaving Exclusive Mode returns the device to the
|
||||
/// volume the user had set before QBZ took over.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[derive(Debug)]
|
||||
pub struct CoreAudioExclusiveGuard {
|
||||
device_id: AudioDeviceID,
|
||||
active: bool,
|
||||
original_hardware_volume: Option<f32>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl CoreAudioExclusiveGuard {
|
||||
/// Acquire CoreAudio Hog Mode for the given device.
|
||||
///
|
||||
/// The guard is constructed *before* the FFI call so that any
|
||||
/// partial-acquire failure (e.g. CoreAudio transfers ownership to
|
||||
/// us but the readback fails) still triggers `Drop`, which calls
|
||||
/// `set_hog_mode(false)`. That release is a no-op when we don't
|
||||
/// actually own the device, so it's safe in either outcome and
|
||||
/// avoids leaving the device hogged on error.
|
||||
pub fn acquire(device_id: AudioDeviceID) -> Result<Self, String> {
|
||||
set_hog_mode(device_id, true)?;
|
||||
Ok(Self {
|
||||
// Snapshot the current hardware volume before we touch anything,
|
||||
// so the user's pre-Exclusive level can be restored on release.
|
||||
// Devices without a readable volume property (knob-only DACs)
|
||||
// simply don't get a snapshot — restoration is best-effort.
|
||||
let original_hardware_volume = get_hardware_volume(device_id).ok();
|
||||
let guard = Self {
|
||||
device_id,
|
||||
active: true,
|
||||
})
|
||||
original_hardware_volume,
|
||||
};
|
||||
set_hog_mode(device_id, true)?;
|
||||
Ok(guard)
|
||||
}
|
||||
|
||||
pub fn release(&mut self) -> Result<(), String> {
|
||||
@@ -320,6 +381,19 @@ impl CoreAudioExclusiveGuard {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Restore the hardware volume *before* releasing Hog Mode, while
|
||||
// we still own the device. After release any other process can
|
||||
// change the volume, so doing it before keeps our restoration
|
||||
// authoritative.
|
||||
if let Some(original) = self.original_hardware_volume.take() {
|
||||
if let Err(e) = set_hardware_volume(self.device_id, original) {
|
||||
log::warn!(
|
||||
"[CoreAudio] Failed to restore hardware volume on release: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
set_hog_mode(self.device_id, false)?;
|
||||
self.active = false;
|
||||
Ok(())
|
||||
|
||||
@@ -451,47 +451,66 @@ fn create_output_stream_with_config(
|
||||
}
|
||||
}
|
||||
|
||||
/// Output stream type - either rodio or ALSA Direct
|
||||
/// Output stream type - either rodio or ALSA Direct.
|
||||
///
|
||||
/// On macOS, the Rodio variant carries an optional `exclusive_guard`
|
||||
/// whose `Drop` releases CoreAudio Hog Mode and is otherwise inert —
|
||||
/// so exclusive and shared modes share a single code path.
|
||||
enum StreamType {
|
||||
Rodio(MixerDeviceSink),
|
||||
#[cfg(target_os = "macos")]
|
||||
CoreAudio {
|
||||
Rodio {
|
||||
sink: MixerDeviceSink,
|
||||
_exclusive_guard: Option<qbz_audio::CoreAudioExclusiveGuard>,
|
||||
/// Holds CoreAudio Hog Mode for the lifetime of the stream.
|
||||
/// Load-bearing via `Drop`; reads happen through pattern matches
|
||||
/// (e.g., `set_coreaudio_hardware_volume`).
|
||||
#[cfg(target_os = "macos")]
|
||||
exclusive_guard: Option<qbz_audio::CoreAudioExclusiveGuard>,
|
||||
},
|
||||
#[cfg(target_os = "linux")]
|
||||
AlsaDirect(Arc<qbz_audio::AlsaDirectStream>),
|
||||
}
|
||||
|
||||
impl StreamType {
|
||||
fn rodio_sink(&self) -> Option<&MixerDeviceSink> {
|
||||
match self {
|
||||
StreamType::Rodio(sink) => Some(sink),
|
||||
/// Construct a shared-mode Rodio stream (no exclusive guard).
|
||||
fn rodio(sink: MixerDeviceSink) -> Self {
|
||||
StreamType::Rodio {
|
||||
sink,
|
||||
#[cfg(target_os = "macos")]
|
||||
StreamType::CoreAudio { sink, .. } => Some(sink),
|
||||
#[cfg(target_os = "linux")]
|
||||
StreamType::AlsaDirect(_) => None,
|
||||
exclusive_guard: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the volume to CoreAudio hardware if the device supports it.
|
||||
///
|
||||
/// Returns `true` only when the hardware accepted the change so the
|
||||
/// caller can pin the software stream to unity gain. Returns `false`
|
||||
/// for shared mode and for knob-only DACs (no settable volume
|
||||
/// property), letting the caller fall back to software volume.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn set_coreaudio_hardware_volume(&self, volume: f32) -> bool {
|
||||
match self {
|
||||
StreamType::CoreAudio {
|
||||
_exclusive_guard: Some(guard),
|
||||
StreamType::Rodio {
|
||||
exclusive_guard: Some(guard),
|
||||
..
|
||||
} => {
|
||||
if let Err(e) = guard.set_hardware_volume(volume) {
|
||||
log::warn!("[CoreAudio] Hardware volume failed: {}", e);
|
||||
} => match guard.set_hardware_volume(volume) {
|
||||
Ok(()) => true,
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"[CoreAudio] Hardware volume failed; falling back to software: {}",
|
||||
e
|
||||
);
|
||||
false
|
||||
}
|
||||
true
|
||||
}
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_engine_volume(stream_opt: &Option<StreamType>, engine: &PlaybackEngine, volume: f32) {
|
||||
fn apply_engine_volume(
|
||||
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))] stream_opt: &Option<StreamType>,
|
||||
engine: &PlaybackEngine,
|
||||
volume: f32,
|
||||
) {
|
||||
#[cfg(target_os = "macos")]
|
||||
if stream_opt
|
||||
.as_ref()
|
||||
@@ -588,7 +607,7 @@ fn try_init_stream_with_backend(
|
||||
|
||||
// Fallback to regular rodio stream (PipeWire, Pulse, ALSA via CPAL)
|
||||
match backend.create_output_stream_with_exclusive_guard(&config) {
|
||||
Ok((mixer_sink, exclusive_guard)) => {
|
||||
Ok((mixer_sink, _exclusive_guard)) => {
|
||||
log::info!(
|
||||
"Stream created via {:?} backend at {}Hz",
|
||||
backend_type,
|
||||
@@ -597,15 +616,15 @@ fn try_init_stream_with_backend(
|
||||
state.set_bit_perfect_mode(Some(BitPerfectMode::Disabled));
|
||||
#[cfg(target_os = "macos")]
|
||||
let stream = if backend_type == AudioBackendType::SystemDefault {
|
||||
StreamType::CoreAudio {
|
||||
StreamType::Rodio {
|
||||
sink: mixer_sink,
|
||||
_exclusive_guard: exclusive_guard,
|
||||
exclusive_guard: _exclusive_guard,
|
||||
}
|
||||
} else {
|
||||
StreamType::Rodio(mixer_sink)
|
||||
StreamType::rodio(mixer_sink)
|
||||
};
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let stream = StreamType::Rodio(mixer_sink);
|
||||
let stream = StreamType::rodio(mixer_sink);
|
||||
Some(Ok(stream))
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -1048,6 +1067,22 @@ impl Player {
|
||||
return Some(stream_type);
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
// On macOS, the backend path is the only one that
|
||||
// honors Exclusive Mode (CoreAudio Hog Mode). Falling
|
||||
// through to legacy CPAL here would silently create a
|
||||
// shared-mode stream while the UI still shows
|
||||
// Exclusive — surface the failure instead so the
|
||||
// user sees that audio is unavailable rather than
|
||||
// unknowingly playing in shared mode.
|
||||
#[cfg(target_os = "macos")]
|
||||
if settings.exclusive_mode {
|
||||
log::error!(
|
||||
"macOS Exclusive Mode init failed: {} — refusing to fall back to shared CPAL",
|
||||
e
|
||||
);
|
||||
state.set_current_device(None);
|
||||
return None;
|
||||
}
|
||||
log::warn!(
|
||||
"Backend system init failed: {}, falling back to legacy",
|
||||
e
|
||||
@@ -1108,7 +1143,7 @@ impl Player {
|
||||
{
|
||||
Ok(mixer_sink) => {
|
||||
log::info!("Audio output initialized successfully");
|
||||
Some(StreamType::Rodio(mixer_sink))
|
||||
Some(StreamType::rodio(mixer_sink))
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
@@ -1118,7 +1153,7 @@ impl Player {
|
||||
match DeviceSinkBuilder::open_default_sink() {
|
||||
Ok(mixer_sink) => {
|
||||
log::info!("Fallback to default audio output succeeded");
|
||||
Some(StreamType::Rodio(mixer_sink))
|
||||
Some(StreamType::rodio(mixer_sink))
|
||||
}
|
||||
Err(e2) => {
|
||||
log::error!("Failed to create default audio output: {}", e2);
|
||||
@@ -1370,7 +1405,7 @@ impl Player {
|
||||
channels,
|
||||
dac_passthrough,
|
||||
)
|
||||
.map(StreamType::Rodio)
|
||||
.map(StreamType::rodio)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1413,7 +1448,7 @@ impl Player {
|
||||
channels,
|
||||
dac_passthrough,
|
||||
)
|
||||
.map(StreamType::Rodio)
|
||||
.map(StreamType::rodio)
|
||||
};
|
||||
|
||||
// Handle stream creation result
|
||||
@@ -1497,8 +1532,7 @@ impl Player {
|
||||
|
||||
// Create PlaybackEngine from StreamType
|
||||
let mut engine = match stream {
|
||||
stream if stream.rodio_sink().is_some() => {
|
||||
let mixer_sink = stream.rodio_sink().expect("rodio sink");
|
||||
StreamType::Rodio { sink: mixer_sink, .. } => {
|
||||
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
||||
Ok(e) => {
|
||||
*consecutive_sink_failures = 0;
|
||||
@@ -1562,9 +1596,6 @@ impl Player {
|
||||
hardware_volume,
|
||||
)
|
||||
}
|
||||
_ => unreachable!(
|
||||
"non-rodio stream handled by platform-specific arms"
|
||||
),
|
||||
};
|
||||
|
||||
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
||||
@@ -1808,7 +1839,7 @@ impl Player {
|
||||
channels,
|
||||
dac_passthrough,
|
||||
)
|
||||
.map(StreamType::Rodio)
|
||||
.map(StreamType::rodio)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1826,7 +1857,7 @@ impl Player {
|
||||
channels,
|
||||
dac_passthrough,
|
||||
)
|
||||
.map(StreamType::Rodio)
|
||||
.map(StreamType::rodio)
|
||||
};
|
||||
|
||||
match stream_result {
|
||||
@@ -1871,8 +1902,7 @@ impl Player {
|
||||
|
||||
// Create PlaybackEngine
|
||||
let mut engine = match stream {
|
||||
stream if stream.rodio_sink().is_some() => {
|
||||
let mixer_sink = stream.rodio_sink().expect("rodio sink");
|
||||
StreamType::Rodio { sink: mixer_sink, .. } => {
|
||||
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
||||
Ok(e) => {
|
||||
*consecutive_sink_failures = 0;
|
||||
@@ -1900,9 +1930,6 @@ impl Player {
|
||||
hardware_volume,
|
||||
)
|
||||
}
|
||||
_ => unreachable!(
|
||||
"non-rodio stream handled by platform-specific arms"
|
||||
),
|
||||
};
|
||||
|
||||
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
||||
@@ -2101,8 +2128,7 @@ impl Player {
|
||||
};
|
||||
|
||||
let mut engine = match stream {
|
||||
stream if stream.rodio_sink().is_some() => {
|
||||
let mixer_sink = stream.rodio_sink().expect("rodio sink");
|
||||
StreamType::Rodio { sink: mixer_sink, .. } => {
|
||||
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
@@ -2126,9 +2152,6 @@ impl Player {
|
||||
hardware_volume,
|
||||
)
|
||||
}
|
||||
_ => unreachable!(
|
||||
"non-rodio stream handled by platform-specific arms"
|
||||
),
|
||||
};
|
||||
|
||||
let volume =
|
||||
@@ -2311,8 +2334,7 @@ impl Player {
|
||||
}
|
||||
|
||||
let mut engine = match stream {
|
||||
stream if stream.rodio_sink().is_some() => {
|
||||
let mixer_sink = stream.rodio_sink().expect("rodio sink");
|
||||
StreamType::Rodio { sink: mixer_sink, .. } => {
|
||||
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
@@ -2336,9 +2358,6 @@ impl Player {
|
||||
hardware_volume,
|
||||
)
|
||||
}
|
||||
_ => unreachable!(
|
||||
"non-rodio stream handled by platform-specific arms"
|
||||
),
|
||||
};
|
||||
|
||||
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
||||
|
||||
+69
-50
@@ -462,47 +462,66 @@ fn create_output_stream_with_config(
|
||||
}
|
||||
}
|
||||
|
||||
/// Output stream type - either rodio or ALSA Direct
|
||||
/// Output stream type - either rodio or ALSA Direct.
|
||||
///
|
||||
/// On macOS, the Rodio variant carries an optional `exclusive_guard`
|
||||
/// whose `Drop` releases CoreAudio Hog Mode and is otherwise inert —
|
||||
/// so exclusive and shared modes share a single code path.
|
||||
enum StreamType {
|
||||
Rodio(MixerDeviceSink),
|
||||
#[cfg(target_os = "macos")]
|
||||
CoreAudio {
|
||||
Rodio {
|
||||
sink: MixerDeviceSink,
|
||||
_exclusive_guard: Option<crate::audio::CoreAudioExclusiveGuard>,
|
||||
/// Holds CoreAudio Hog Mode for the lifetime of the stream.
|
||||
/// Load-bearing via `Drop`; reads happen through pattern matches
|
||||
/// (e.g., `set_coreaudio_hardware_volume`).
|
||||
#[cfg(target_os = "macos")]
|
||||
exclusive_guard: Option<crate::audio::CoreAudioExclusiveGuard>,
|
||||
},
|
||||
#[cfg(target_os = "linux")]
|
||||
AlsaDirect(Arc<crate::audio::AlsaDirectStream>),
|
||||
}
|
||||
|
||||
impl StreamType {
|
||||
fn rodio_sink(&self) -> Option<&MixerDeviceSink> {
|
||||
match self {
|
||||
StreamType::Rodio(sink) => Some(sink),
|
||||
/// Construct a shared-mode Rodio stream (no exclusive guard).
|
||||
fn rodio(sink: MixerDeviceSink) -> Self {
|
||||
StreamType::Rodio {
|
||||
sink,
|
||||
#[cfg(target_os = "macos")]
|
||||
StreamType::CoreAudio { sink, .. } => Some(sink),
|
||||
#[cfg(target_os = "linux")]
|
||||
StreamType::AlsaDirect(_) => None,
|
||||
exclusive_guard: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the volume to CoreAudio hardware if the device supports it.
|
||||
///
|
||||
/// Returns `true` only when the hardware accepted the change so the
|
||||
/// caller can pin the software stream to unity gain. Returns `false`
|
||||
/// for shared mode and for knob-only DACs (no settable volume
|
||||
/// property), letting the caller fall back to software volume.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn set_coreaudio_hardware_volume(&self, volume: f32) -> bool {
|
||||
match self {
|
||||
StreamType::CoreAudio {
|
||||
_exclusive_guard: Some(guard),
|
||||
StreamType::Rodio {
|
||||
exclusive_guard: Some(guard),
|
||||
..
|
||||
} => {
|
||||
if let Err(e) = guard.set_hardware_volume(volume) {
|
||||
log::warn!("[CoreAudio] Hardware volume failed: {}", e);
|
||||
} => match guard.set_hardware_volume(volume) {
|
||||
Ok(()) => true,
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"[CoreAudio] Hardware volume failed; falling back to software: {}",
|
||||
e
|
||||
);
|
||||
false
|
||||
}
|
||||
true
|
||||
}
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_engine_volume(stream_opt: &Option<StreamType>, engine: &PlaybackEngine, volume: f32) {
|
||||
fn apply_engine_volume(
|
||||
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))] stream_opt: &Option<StreamType>,
|
||||
engine: &PlaybackEngine,
|
||||
volume: f32,
|
||||
) {
|
||||
#[cfg(target_os = "macos")]
|
||||
if stream_opt
|
||||
.as_ref()
|
||||
@@ -597,7 +616,7 @@ fn try_init_stream_with_backend(
|
||||
|
||||
// Fallback to regular rodio stream (PipeWire, Pulse, ALSA via CPAL)
|
||||
match backend.create_output_stream_with_exclusive_guard(&config) {
|
||||
Ok((mixer_sink, exclusive_guard)) => {
|
||||
Ok((mixer_sink, _exclusive_guard)) => {
|
||||
log::info!(
|
||||
"Stream created via {:?} backend at {}Hz",
|
||||
backend_type,
|
||||
@@ -605,15 +624,15 @@ fn try_init_stream_with_backend(
|
||||
);
|
||||
#[cfg(target_os = "macos")]
|
||||
let stream = if backend_type == AudioBackendType::SystemDefault {
|
||||
StreamType::CoreAudio {
|
||||
StreamType::Rodio {
|
||||
sink: mixer_sink,
|
||||
_exclusive_guard: exclusive_guard,
|
||||
exclusive_guard: _exclusive_guard,
|
||||
}
|
||||
} else {
|
||||
StreamType::Rodio(mixer_sink)
|
||||
StreamType::rodio(mixer_sink)
|
||||
};
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let stream = StreamType::Rodio(mixer_sink);
|
||||
let stream = StreamType::rodio(mixer_sink);
|
||||
Some(Ok(stream))
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -992,6 +1011,22 @@ impl Player {
|
||||
return Some(stream_type);
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
// On macOS, the backend path is the only one that
|
||||
// honors Exclusive Mode (CoreAudio Hog Mode). Falling
|
||||
// through to legacy CPAL here would silently create a
|
||||
// shared-mode stream while the UI still shows
|
||||
// Exclusive — surface the failure instead so the
|
||||
// user sees that audio is unavailable rather than
|
||||
// unknowingly playing in shared mode.
|
||||
#[cfg(target_os = "macos")]
|
||||
if settings.exclusive_mode {
|
||||
log::error!(
|
||||
"macOS Exclusive Mode init failed: {} — refusing to fall back to shared CPAL",
|
||||
e
|
||||
);
|
||||
state.set_current_device(None);
|
||||
return None;
|
||||
}
|
||||
log::warn!(
|
||||
"Backend system init failed: {}, falling back to legacy",
|
||||
e
|
||||
@@ -1052,7 +1087,7 @@ impl Player {
|
||||
{
|
||||
Ok(mixer_sink) => {
|
||||
log::info!("Audio output initialized successfully");
|
||||
Some(StreamType::Rodio(mixer_sink))
|
||||
Some(StreamType::rodio(mixer_sink))
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
@@ -1062,7 +1097,7 @@ impl Player {
|
||||
match DeviceSinkBuilder::open_default_sink() {
|
||||
Ok(mixer_sink) => {
|
||||
log::info!("Fallback to default audio output succeeded");
|
||||
Some(StreamType::Rodio(mixer_sink))
|
||||
Some(StreamType::rodio(mixer_sink))
|
||||
}
|
||||
Err(e2) => {
|
||||
log::error!("Failed to create default audio output: {}", e2);
|
||||
@@ -1297,7 +1332,7 @@ impl Player {
|
||||
channels,
|
||||
dac_passthrough,
|
||||
)
|
||||
.map(StreamType::Rodio)
|
||||
.map(StreamType::rodio)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1338,7 +1373,7 @@ impl Player {
|
||||
channels,
|
||||
dac_passthrough,
|
||||
)
|
||||
.map(StreamType::Rodio)
|
||||
.map(StreamType::rodio)
|
||||
};
|
||||
|
||||
// Handle stream creation result
|
||||
@@ -1418,8 +1453,7 @@ impl Player {
|
||||
|
||||
// Create PlaybackEngine from StreamType
|
||||
let mut engine = match stream {
|
||||
stream if stream.rodio_sink().is_some() => {
|
||||
let mixer_sink = stream.rodio_sink().expect("rodio sink");
|
||||
StreamType::Rodio { sink: mixer_sink, .. } => {
|
||||
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
||||
Ok(e) => {
|
||||
*consecutive_sink_failures = 0;
|
||||
@@ -1483,9 +1517,6 @@ impl Player {
|
||||
hardware_volume,
|
||||
)
|
||||
}
|
||||
_ => unreachable!(
|
||||
"non-rodio stream handled by platform-specific arms"
|
||||
),
|
||||
};
|
||||
|
||||
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
||||
@@ -1715,7 +1746,7 @@ impl Player {
|
||||
channels,
|
||||
dac_passthrough,
|
||||
)
|
||||
.map(StreamType::Rodio)
|
||||
.map(StreamType::rodio)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1733,7 +1764,7 @@ impl Player {
|
||||
channels,
|
||||
dac_passthrough,
|
||||
)
|
||||
.map(StreamType::Rodio)
|
||||
.map(StreamType::rodio)
|
||||
};
|
||||
|
||||
match stream_result {
|
||||
@@ -1775,8 +1806,7 @@ impl Player {
|
||||
|
||||
// Create PlaybackEngine
|
||||
let mut engine = match stream {
|
||||
stream if stream.rodio_sink().is_some() => {
|
||||
let mixer_sink = stream.rodio_sink().expect("rodio sink");
|
||||
StreamType::Rodio { sink: mixer_sink, .. } => {
|
||||
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
||||
Ok(e) => {
|
||||
*consecutive_sink_failures = 0;
|
||||
@@ -1804,9 +1834,6 @@ impl Player {
|
||||
hardware_volume,
|
||||
)
|
||||
}
|
||||
_ => unreachable!(
|
||||
"non-rodio stream handled by platform-specific arms"
|
||||
),
|
||||
};
|
||||
|
||||
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
||||
@@ -2005,8 +2032,7 @@ impl Player {
|
||||
};
|
||||
|
||||
let mut engine = match stream {
|
||||
stream if stream.rodio_sink().is_some() => {
|
||||
let mixer_sink = stream.rodio_sink().expect("rodio sink");
|
||||
StreamType::Rodio { sink: mixer_sink, .. } => {
|
||||
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
@@ -2030,9 +2056,6 @@ impl Player {
|
||||
hardware_volume,
|
||||
)
|
||||
}
|
||||
_ => unreachable!(
|
||||
"non-rodio stream handled by platform-specific arms"
|
||||
),
|
||||
};
|
||||
|
||||
let volume =
|
||||
@@ -2144,8 +2167,7 @@ impl Player {
|
||||
}
|
||||
|
||||
let mut engine = match stream {
|
||||
stream if stream.rodio_sink().is_some() => {
|
||||
let mixer_sink = stream.rodio_sink().expect("rodio sink");
|
||||
StreamType::Rodio { sink: mixer_sink, .. } => {
|
||||
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
@@ -2169,9 +2191,6 @@ impl Player {
|
||||
hardware_volume,
|
||||
)
|
||||
}
|
||||
_ => unreachable!(
|
||||
"non-rodio stream handled by platform-specific arms"
|
||||
),
|
||||
};
|
||||
|
||||
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
||||
|
||||
Reference in new issue
Block a user