fix(player): recreate CoreAudio exclusive streams
This commit is contained in:
2 files changed
+254
-89
No files matched your search
@@ -454,10 +454,27 @@ fn create_output_stream_with_config(
|
|||||||
/// Output stream type - either rodio or ALSA Direct
|
/// Output stream type - either rodio or ALSA Direct
|
||||||
enum StreamType {
|
enum StreamType {
|
||||||
Rodio(MixerDeviceSink),
|
Rodio(MixerDeviceSink),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
CoreAudio {
|
||||||
|
sink: MixerDeviceSink,
|
||||||
|
_exclusive_guard: Option<qbz_audio::CoreAudioExclusiveGuard>,
|
||||||
|
},
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
AlsaDirect(Arc<qbz_audio::AlsaDirectStream>),
|
AlsaDirect(Arc<qbz_audio::AlsaDirectStream>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StreamType {
|
||||||
|
fn rodio_sink(&self) -> Option<&MixerDeviceSink> {
|
||||||
|
match self {
|
||||||
|
StreamType::Rodio(sink) => Some(sink),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
StreamType::CoreAudio { sink, .. } => Some(sink),
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
StreamType::AlsaDirect(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Try to create output stream using the backend system (if configured)
|
/// Try to create output stream using the backend system (if configured)
|
||||||
/// Returns None if backend system is not configured (backend_type = None)
|
/// Returns None if backend system is not configured (backend_type = None)
|
||||||
///
|
///
|
||||||
@@ -469,10 +486,10 @@ fn try_init_stream_with_backend(
|
|||||||
state: &SharedState,
|
state: &SharedState,
|
||||||
) -> Option<Result<StreamType, String>> {
|
) -> Option<Result<StreamType, String>> {
|
||||||
// Check if backend system is configured.
|
// Check if backend system is configured.
|
||||||
// On non-Linux, default to SystemDefault when not explicitly set,
|
// On macOS, default to SystemDefault when not explicitly set,
|
||||||
// so macOS gets CoreAudio device probing and sample rate switching.
|
// so CoreAudio device probing and sample rate switching are active.
|
||||||
let backend_type = audio_settings.backend_type.or_else(|| {
|
let backend_type = audio_settings.backend_type.or_else(|| {
|
||||||
if cfg!(not(target_os = "linux")) {
|
if cfg!(target_os = "macos") {
|
||||||
Some(qbz_audio::AudioBackendType::SystemDefault)
|
Some(qbz_audio::AudioBackendType::SystemDefault)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -540,15 +557,26 @@ fn try_init_stream_with_backend(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to regular rodio stream (PipeWire, Pulse, ALSA via CPAL)
|
// Fallback to regular rodio stream (PipeWire, Pulse, ALSA via CPAL)
|
||||||
match backend.create_output_stream(&config) {
|
match backend.create_output_stream_with_exclusive_guard(&config) {
|
||||||
Ok(mixer_sink) => {
|
Ok((mixer_sink, exclusive_guard)) => {
|
||||||
log::info!(
|
log::info!(
|
||||||
"Stream created via {:?} backend at {}Hz",
|
"Stream created via {:?} backend at {}Hz",
|
||||||
backend_type,
|
backend_type,
|
||||||
sample_rate
|
sample_rate
|
||||||
);
|
);
|
||||||
state.set_bit_perfect_mode(Some(BitPerfectMode::Disabled));
|
state.set_bit_perfect_mode(Some(BitPerfectMode::Disabled));
|
||||||
Some(Ok(StreamType::Rodio(mixer_sink)))
|
#[cfg(target_os = "macos")]
|
||||||
|
let stream = if backend_type == AudioBackendType::SystemDefault {
|
||||||
|
StreamType::CoreAudio {
|
||||||
|
sink: mixer_sink,
|
||||||
|
_exclusive_guard: exclusive_guard,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
StreamType::Rodio(mixer_sink)
|
||||||
|
};
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let stream = StreamType::Rodio(mixer_sink);
|
||||||
|
Some(Ok(stream))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Backend stream creation failed: {}", e);
|
log::error!("Backend stream creation failed: {}", e);
|
||||||
@@ -970,7 +998,7 @@ impl Player {
|
|||||||
-> Option<StreamType> {
|
-> Option<StreamType> {
|
||||||
// Try backend system if configured
|
// Try backend system if configured
|
||||||
if let Ok(settings) = thread_settings.lock() {
|
if let Ok(settings) = thread_settings.lock() {
|
||||||
if settings.backend_type.is_some() {
|
if settings.backend_type.is_some() || cfg!(target_os = "macos") {
|
||||||
// Use provided sample rate/channels to maintain DAC passthrough
|
// Use provided sample rate/channels to maintain DAC passthrough
|
||||||
log::info!(
|
log::info!(
|
||||||
"Initializing backend system with {}Hz/{}ch",
|
"Initializing backend system with {}Hz/{}ch",
|
||||||
@@ -1147,7 +1175,7 @@ impl Player {
|
|||||||
let dac_passthrough = thread_settings
|
let dac_passthrough = thread_settings
|
||||||
.lock()
|
.lock()
|
||||||
.ok()
|
.ok()
|
||||||
.map(|s| s.dac_passthrough)
|
.map(|s| cfg!(target_os = "linux") && s.dac_passthrough)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
// Check if we need to recreate the stream
|
// Check if we need to recreate the stream
|
||||||
@@ -1162,15 +1190,32 @@ impl Player {
|
|||||||
.and_then(|s| s.backend_type)
|
.and_then(|s| s.backend_type)
|
||||||
.map(|b| b == AudioBackendType::Alsa)
|
.map(|b| b == AudioBackendType::Alsa)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
let using_coreaudio_exclusive = thread_settings
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.map(|s| {
|
||||||
|
cfg!(target_os = "macos")
|
||||||
|
&& s.backend_type.unwrap_or(AudioBackendType::SystemDefault)
|
||||||
|
== AudioBackendType::SystemDefault
|
||||||
|
&& s.exclusive_mode
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
let needs_new_stream = stream_opt.is_none()
|
let needs_new_stream = stream_opt.is_none()
|
||||||
|| (dac_passthrough && format_changed)
|
|| (dac_passthrough && format_changed)
|
||||||
|| (using_alsa_direct && format_changed);
|
|| (using_alsa_direct && format_changed)
|
||||||
|
|| (using_coreaudio_exclusive && format_changed);
|
||||||
|
|
||||||
if needs_new_stream {
|
if needs_new_stream {
|
||||||
if stream_opt.is_some() {
|
if stream_opt.is_some() {
|
||||||
if (dac_passthrough || using_alsa_direct) && format_changed {
|
if (dac_passthrough
|
||||||
let mode = if using_alsa_direct {
|
|| using_alsa_direct
|
||||||
|
|| using_coreaudio_exclusive)
|
||||||
|
&& format_changed
|
||||||
|
{
|
||||||
|
let mode = if using_coreaudio_exclusive {
|
||||||
|
"CoreAudio exclusive"
|
||||||
|
} else if using_alsa_direct {
|
||||||
"ALSA Direct"
|
"ALSA Direct"
|
||||||
} else {
|
} else {
|
||||||
"DAC passthrough"
|
"DAC passthrough"
|
||||||
@@ -1201,9 +1246,10 @@ impl Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"DAC passthrough: {}, ALSA Direct: {}",
|
"DAC passthrough: {}, ALSA Direct: {}, CoreAudio exclusive: {}",
|
||||||
dac_passthrough,
|
dac_passthrough,
|
||||||
using_alsa_direct
|
using_alsa_direct,
|
||||||
|
using_coreaudio_exclusive
|
||||||
);
|
);
|
||||||
|
|
||||||
// Try backend system first (if configured), then fall back to legacy CPAL
|
// Try backend system first (if configured), then fall back to legacy CPAL
|
||||||
@@ -1421,7 +1467,8 @@ impl Player {
|
|||||||
|
|
||||||
// Create PlaybackEngine from StreamType
|
// Create PlaybackEngine from StreamType
|
||||||
let mut engine = match stream {
|
let mut engine = match stream {
|
||||||
StreamType::Rodio(ref mixer_sink) => {
|
stream if stream.rodio_sink().is_some() => {
|
||||||
|
let mixer_sink = stream.rodio_sink().expect("rodio sink");
|
||||||
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
||||||
Ok(e) => {
|
Ok(e) => {
|
||||||
*consecutive_sink_failures = 0;
|
*consecutive_sink_failures = 0;
|
||||||
@@ -1485,6 +1532,9 @@ impl Player {
|
|||||||
hardware_volume,
|
hardware_volume,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
_ => unreachable!(
|
||||||
|
"non-rodio stream handled by platform-specific arms"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
||||||
@@ -1607,7 +1657,7 @@ impl Player {
|
|||||||
let dac_passthrough = thread_settings
|
let dac_passthrough = thread_settings
|
||||||
.lock()
|
.lock()
|
||||||
.ok()
|
.ok()
|
||||||
.map(|s| s.dac_passthrough)
|
.map(|s| cfg!(target_os = "linux") && s.dac_passthrough)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
// Check if we need to recreate the stream
|
// Check if we need to recreate the stream
|
||||||
@@ -1620,15 +1670,32 @@ impl Player {
|
|||||||
.and_then(|s| s.backend_type)
|
.and_then(|s| s.backend_type)
|
||||||
.map(|b| b == AudioBackendType::Alsa)
|
.map(|b| b == AudioBackendType::Alsa)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
let using_coreaudio_exclusive = thread_settings
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.map(|s| {
|
||||||
|
cfg!(target_os = "macos")
|
||||||
|
&& s.backend_type.unwrap_or(AudioBackendType::SystemDefault)
|
||||||
|
== AudioBackendType::SystemDefault
|
||||||
|
&& s.exclusive_mode
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
let needs_new_stream = stream_opt.is_none()
|
let needs_new_stream = stream_opt.is_none()
|
||||||
|| (dac_passthrough && format_changed)
|
|| (dac_passthrough && format_changed)
|
||||||
|| (using_alsa_direct && format_changed);
|
|| (using_alsa_direct && format_changed)
|
||||||
|
|| (using_coreaudio_exclusive && format_changed);
|
||||||
|
|
||||||
if needs_new_stream {
|
if needs_new_stream {
|
||||||
if stream_opt.is_some() {
|
if stream_opt.is_some() {
|
||||||
if (dac_passthrough || using_alsa_direct) && format_changed {
|
if (dac_passthrough
|
||||||
let mode = if using_alsa_direct {
|
|| using_alsa_direct
|
||||||
|
|| using_coreaudio_exclusive)
|
||||||
|
&& format_changed
|
||||||
|
{
|
||||||
|
let mode = if using_coreaudio_exclusive {
|
||||||
|
"CoreAudio exclusive"
|
||||||
|
} else if using_alsa_direct {
|
||||||
"ALSA Direct"
|
"ALSA Direct"
|
||||||
} else {
|
} else {
|
||||||
"DAC passthrough"
|
"DAC passthrough"
|
||||||
@@ -1774,7 +1841,8 @@ impl Player {
|
|||||||
|
|
||||||
// Create PlaybackEngine
|
// Create PlaybackEngine
|
||||||
let mut engine = match stream {
|
let mut engine = match stream {
|
||||||
StreamType::Rodio(ref mixer_sink) => {
|
stream if stream.rodio_sink().is_some() => {
|
||||||
|
let mixer_sink = stream.rodio_sink().expect("rodio sink");
|
||||||
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
||||||
Ok(e) => {
|
Ok(e) => {
|
||||||
*consecutive_sink_failures = 0;
|
*consecutive_sink_failures = 0;
|
||||||
@@ -1802,6 +1870,9 @@ impl Player {
|
|||||||
hardware_volume,
|
hardware_volume,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
_ => unreachable!(
|
||||||
|
"non-rodio stream handled by platform-specific arms"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
||||||
@@ -2000,7 +2071,8 @@ impl Player {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut engine = match stream {
|
let mut engine = match stream {
|
||||||
StreamType::Rodio(ref mixer_sink) => {
|
stream if stream.rodio_sink().is_some() => {
|
||||||
|
let mixer_sink = stream.rodio_sink().expect("rodio sink");
|
||||||
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -2024,6 +2096,9 @@ impl Player {
|
|||||||
hardware_volume,
|
hardware_volume,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
_ => unreachable!(
|
||||||
|
"non-rodio stream handled by platform-specific arms"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let volume =
|
let volume =
|
||||||
@@ -2152,12 +2227,8 @@ impl Player {
|
|||||||
// playback reach this handler with
|
// playback reach this handler with
|
||||||
// current_audio_data Some and skip the
|
// current_audio_data Some and skip the
|
||||||
// streaming branch entirely (issue #335).
|
// streaming branch entirely (issue #335).
|
||||||
if current_audio_data.is_none()
|
if current_audio_data.is_none() && current_streaming_source.is_none() {
|
||||||
&& current_streaming_source.is_none()
|
log::warn!("Audio thread: cannot seek - no audio data available");
|
||||||
{
|
|
||||||
log::warn!(
|
|
||||||
"Audio thread: cannot seek - no audio data available"
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(ref stream_src) = *current_streaming_source {
|
if let Some(ref stream_src) = *current_streaming_source {
|
||||||
@@ -2210,7 +2281,8 @@ impl Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut engine = match stream {
|
let mut engine = match stream {
|
||||||
StreamType::Rodio(ref mixer_sink) => {
|
stream if stream.rodio_sink().is_some() => {
|
||||||
|
let mixer_sink = stream.rodio_sink().expect("rodio sink");
|
||||||
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -2234,6 +2306,9 @@ impl Player {
|
|||||||
hardware_volume,
|
hardware_volume,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
_ => unreachable!(
|
||||||
|
"non-rodio stream handled by platform-specific arms"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
||||||
@@ -2251,56 +2326,42 @@ impl Player {
|
|||||||
// probe the format (e.g., rodio-only MP4/AAC),
|
// probe the format (e.g., rodio-only MP4/AAC),
|
||||||
// preserving existing behavior for those cases.
|
// preserving existing behavior for those cases.
|
||||||
let skip_duration = Duration::from_secs(position_secs);
|
let skip_duration = Duration::from_secs(position_secs);
|
||||||
let skipped_source: Box<dyn Source<Item = f32> + Send> =
|
let skipped_source: Box<dyn Source<Item = f32> + Send> = if let Some(
|
||||||
if let Some(ref stream_src) = *current_streaming_source {
|
ref stream_src,
|
||||||
match IncrementalStreamingSource::new(stream_src.clone()) {
|
) =
|
||||||
Ok(mut s) => {
|
*current_streaming_source
|
||||||
if let Err(e) = s.seek_to(skip_duration) {
|
{
|
||||||
log::error!(
|
match IncrementalStreamingSource::new(stream_src.clone()) {
|
||||||
"Failed to native-seek streaming source: {}",
|
Ok(mut s) => {
|
||||||
e
|
if let Err(e) = s.seek_to(skip_duration) {
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Box::new(s)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!(
|
log::error!(
|
||||||
"Failed to create streaming source for seek: {}",
|
"Failed to native-seek streaming source: {}",
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Box::new(s)
|
||||||
}
|
}
|
||||||
} else {
|
Err(e) => {
|
||||||
let audio_data = current_audio_data
|
log::error!(
|
||||||
.as_ref()
|
"Failed to create streaming source for seek: {}",
|
||||||
.expect("current_audio_data was checked Some above");
|
e
|
||||||
match InMemorySource::new(audio_data.clone()) {
|
);
|
||||||
Ok(mut s) => match s.seek_to(skip_duration) {
|
return;
|
||||||
Ok(()) => Box::new(s),
|
}
|
||||||
Err(e) => {
|
}
|
||||||
log::warn!(
|
} else {
|
||||||
|
let audio_data = current_audio_data
|
||||||
|
.as_ref()
|
||||||
|
.expect("current_audio_data was checked Some above");
|
||||||
|
match InMemorySource::new(audio_data.clone()) {
|
||||||
|
Ok(mut s) => match s.seek_to(skip_duration) {
|
||||||
|
Ok(()) => Box::new(s),
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!(
|
||||||
"Native seek on cached source failed ({}); falling back to skip_duration",
|
"Native seek on cached source failed ({}); falling back to skip_duration",
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
match decode_with_fallback(audio_data) {
|
|
||||||
Ok(fb) => Box::new(fb.skip_duration(skip_duration)),
|
|
||||||
Err(e) => {
|
|
||||||
log::error!(
|
|
||||||
"Failed to decode audio for seek: {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!(
|
|
||||||
"InMemorySource probe failed ({}); falling back to skip_duration",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
match decode_with_fallback(audio_data) {
|
match decode_with_fallback(audio_data) {
|
||||||
Ok(fb) => Box::new(fb.skip_duration(skip_duration)),
|
Ok(fb) => Box::new(fb.skip_duration(skip_duration)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -2312,8 +2373,25 @@ impl Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!(
|
||||||
|
"InMemorySource probe failed ({}); falling back to skip_duration",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
match decode_with_fallback(audio_data) {
|
||||||
|
Ok(fb) => Box::new(fb.skip_duration(skip_duration)),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to decode audio for seek: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Send Reset to analyzer (seek invalidates accumulated samples)
|
// Send Reset to analyzer (seek invalidates accumulated samples)
|
||||||
let _ = analyzer_tx.try_send(AnalyzerMessage::Reset);
|
let _ = analyzer_tx.try_send(AnalyzerMessage::Reset);
|
||||||
|
|||||||
+107
-20
@@ -465,10 +465,27 @@ fn create_output_stream_with_config(
|
|||||||
/// Output stream type - either rodio or ALSA Direct
|
/// Output stream type - either rodio or ALSA Direct
|
||||||
enum StreamType {
|
enum StreamType {
|
||||||
Rodio(MixerDeviceSink),
|
Rodio(MixerDeviceSink),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
CoreAudio {
|
||||||
|
sink: MixerDeviceSink,
|
||||||
|
_exclusive_guard: Option<crate::audio::CoreAudioExclusiveGuard>,
|
||||||
|
},
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
AlsaDirect(Arc<crate::audio::AlsaDirectStream>),
|
AlsaDirect(Arc<crate::audio::AlsaDirectStream>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StreamType {
|
||||||
|
fn rodio_sink(&self) -> Option<&MixerDeviceSink> {
|
||||||
|
match self {
|
||||||
|
StreamType::Rodio(sink) => Some(sink),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
StreamType::CoreAudio { sink, .. } => Some(sink),
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
StreamType::AlsaDirect(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Try to create output stream using the backend system (if configured)
|
/// Try to create output stream using the backend system (if configured)
|
||||||
/// Returns None if backend system is not configured (backend_type = None)
|
/// Returns None if backend system is not configured (backend_type = None)
|
||||||
///
|
///
|
||||||
@@ -478,8 +495,16 @@ fn try_init_stream_with_backend(
|
|||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
channels: u16,
|
channels: u16,
|
||||||
) -> Option<Result<StreamType, String>> {
|
) -> Option<Result<StreamType, String>> {
|
||||||
// Check if backend system is configured
|
// Check if backend system is configured. On macOS, default Auto to
|
||||||
let backend_type = audio_settings.backend_type?;
|
// SystemDefault so CoreAudio device probing, rate switching, and Hog Mode
|
||||||
|
// are available to existing installs with no saved backend.
|
||||||
|
let backend_type = audio_settings.backend_type.or_else(|| {
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
Some(AudioBackendType::SystemDefault)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Using backend system: {:?} (device: {:?}, plugin: {:?})",
|
"Using backend system: {:?} (device: {:?}, plugin: {:?})",
|
||||||
@@ -541,14 +566,25 @@ fn try_init_stream_with_backend(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to regular rodio stream (PipeWire, Pulse, ALSA via CPAL)
|
// Fallback to regular rodio stream (PipeWire, Pulse, ALSA via CPAL)
|
||||||
match backend.create_output_stream(&config) {
|
match backend.create_output_stream_with_exclusive_guard(&config) {
|
||||||
Ok(mixer_sink) => {
|
Ok((mixer_sink, exclusive_guard)) => {
|
||||||
log::info!(
|
log::info!(
|
||||||
"Stream created via {:?} backend at {}Hz",
|
"Stream created via {:?} backend at {}Hz",
|
||||||
backend_type,
|
backend_type,
|
||||||
sample_rate
|
sample_rate
|
||||||
);
|
);
|
||||||
Some(Ok(StreamType::Rodio(mixer_sink)))
|
#[cfg(target_os = "macos")]
|
||||||
|
let stream = if backend_type == AudioBackendType::SystemDefault {
|
||||||
|
StreamType::CoreAudio {
|
||||||
|
sink: mixer_sink,
|
||||||
|
_exclusive_guard: exclusive_guard,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
StreamType::Rodio(mixer_sink)
|
||||||
|
};
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let stream = StreamType::Rodio(mixer_sink);
|
||||||
|
Some(Ok(stream))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Backend stream creation failed: {}", e);
|
log::error!("Backend stream creation failed: {}", e);
|
||||||
@@ -907,7 +943,7 @@ impl Player {
|
|||||||
-> Option<StreamType> {
|
-> Option<StreamType> {
|
||||||
// Try backend system if configured
|
// Try backend system if configured
|
||||||
if let Ok(settings) = thread_settings.lock() {
|
if let Ok(settings) = thread_settings.lock() {
|
||||||
if settings.backend_type.is_some() {
|
if settings.backend_type.is_some() || cfg!(target_os = "macos") {
|
||||||
// Use provided sample rate/channels to maintain DAC passthrough
|
// Use provided sample rate/channels to maintain DAC passthrough
|
||||||
log::info!(
|
log::info!(
|
||||||
"Initializing backend system with {}Hz/{}ch",
|
"Initializing backend system with {}Hz/{}ch",
|
||||||
@@ -1078,7 +1114,7 @@ impl Player {
|
|||||||
let dac_passthrough = thread_settings
|
let dac_passthrough = thread_settings
|
||||||
.lock()
|
.lock()
|
||||||
.ok()
|
.ok()
|
||||||
.map(|s| s.dac_passthrough)
|
.map(|s| cfg!(target_os = "linux") && s.dac_passthrough)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
// Check if we need to recreate the stream
|
// Check if we need to recreate the stream
|
||||||
@@ -1093,15 +1129,32 @@ impl Player {
|
|||||||
.and_then(|s| s.backend_type)
|
.and_then(|s| s.backend_type)
|
||||||
.map(|b| b == AudioBackendType::Alsa)
|
.map(|b| b == AudioBackendType::Alsa)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
let using_coreaudio_exclusive = thread_settings
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.map(|s| {
|
||||||
|
cfg!(target_os = "macos")
|
||||||
|
&& s.backend_type.unwrap_or(AudioBackendType::SystemDefault)
|
||||||
|
== AudioBackendType::SystemDefault
|
||||||
|
&& s.exclusive_mode
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
let needs_new_stream = stream_opt.is_none()
|
let needs_new_stream = stream_opt.is_none()
|
||||||
|| (dac_passthrough && format_changed)
|
|| (dac_passthrough && format_changed)
|
||||||
|| (using_alsa_direct && format_changed);
|
|| (using_alsa_direct && format_changed)
|
||||||
|
|| (using_coreaudio_exclusive && format_changed);
|
||||||
|
|
||||||
if needs_new_stream {
|
if needs_new_stream {
|
||||||
if stream_opt.is_some() {
|
if stream_opt.is_some() {
|
||||||
if (dac_passthrough || using_alsa_direct) && format_changed {
|
if (dac_passthrough
|
||||||
let mode = if using_alsa_direct {
|
|| using_alsa_direct
|
||||||
|
|| using_coreaudio_exclusive)
|
||||||
|
&& format_changed
|
||||||
|
{
|
||||||
|
let mode = if using_coreaudio_exclusive {
|
||||||
|
"CoreAudio exclusive"
|
||||||
|
} else if using_alsa_direct {
|
||||||
"ALSA Direct"
|
"ALSA Direct"
|
||||||
} else {
|
} else {
|
||||||
"DAC passthrough"
|
"DAC passthrough"
|
||||||
@@ -1122,9 +1175,10 @@ impl Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"DAC passthrough: {}, ALSA Direct: {}",
|
"DAC passthrough: {}, ALSA Direct: {}, CoreAudio exclusive: {}",
|
||||||
dac_passthrough,
|
dac_passthrough,
|
||||||
using_alsa_direct
|
using_alsa_direct,
|
||||||
|
using_coreaudio_exclusive
|
||||||
);
|
);
|
||||||
|
|
||||||
// Try backend system first (if configured), then fall back to legacy CPAL
|
// Try backend system first (if configured), then fall back to legacy CPAL
|
||||||
@@ -1334,7 +1388,8 @@ impl Player {
|
|||||||
|
|
||||||
// Create PlaybackEngine from StreamType
|
// Create PlaybackEngine from StreamType
|
||||||
let mut engine = match stream {
|
let mut engine = match stream {
|
||||||
StreamType::Rodio(ref mixer_sink) => {
|
stream if stream.rodio_sink().is_some() => {
|
||||||
|
let mixer_sink = stream.rodio_sink().expect("rodio sink");
|
||||||
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
||||||
Ok(e) => {
|
Ok(e) => {
|
||||||
*consecutive_sink_failures = 0;
|
*consecutive_sink_failures = 0;
|
||||||
@@ -1398,6 +1453,9 @@ impl Player {
|
|||||||
hardware_volume,
|
hardware_volume,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
_ => unreachable!(
|
||||||
|
"non-rodio stream handled by platform-specific arms"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
||||||
@@ -1519,7 +1577,7 @@ impl Player {
|
|||||||
let dac_passthrough = thread_settings
|
let dac_passthrough = thread_settings
|
||||||
.lock()
|
.lock()
|
||||||
.ok()
|
.ok()
|
||||||
.map(|s| s.dac_passthrough)
|
.map(|s| cfg!(target_os = "linux") && s.dac_passthrough)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
// Check if we need to recreate the stream
|
// Check if we need to recreate the stream
|
||||||
@@ -1532,15 +1590,32 @@ impl Player {
|
|||||||
.and_then(|s| s.backend_type)
|
.and_then(|s| s.backend_type)
|
||||||
.map(|b| b == AudioBackendType::Alsa)
|
.map(|b| b == AudioBackendType::Alsa)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
let using_coreaudio_exclusive = thread_settings
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.map(|s| {
|
||||||
|
cfg!(target_os = "macos")
|
||||||
|
&& s.backend_type.unwrap_or(AudioBackendType::SystemDefault)
|
||||||
|
== AudioBackendType::SystemDefault
|
||||||
|
&& s.exclusive_mode
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
let needs_new_stream = stream_opt.is_none()
|
let needs_new_stream = stream_opt.is_none()
|
||||||
|| (dac_passthrough && format_changed)
|
|| (dac_passthrough && format_changed)
|
||||||
|| (using_alsa_direct && format_changed);
|
|| (using_alsa_direct && format_changed)
|
||||||
|
|| (using_coreaudio_exclusive && format_changed);
|
||||||
|
|
||||||
if needs_new_stream {
|
if needs_new_stream {
|
||||||
if stream_opt.is_some() {
|
if stream_opt.is_some() {
|
||||||
if (dac_passthrough || using_alsa_direct) && format_changed {
|
if (dac_passthrough
|
||||||
let mode = if using_alsa_direct {
|
|| using_alsa_direct
|
||||||
|
|| using_coreaudio_exclusive)
|
||||||
|
&& format_changed
|
||||||
|
{
|
||||||
|
let mode = if using_coreaudio_exclusive {
|
||||||
|
"CoreAudio exclusive"
|
||||||
|
} else if using_alsa_direct {
|
||||||
"ALSA Direct"
|
"ALSA Direct"
|
||||||
} else {
|
} else {
|
||||||
"DAC passthrough"
|
"DAC passthrough"
|
||||||
@@ -1670,7 +1745,8 @@ impl Player {
|
|||||||
|
|
||||||
// Create PlaybackEngine
|
// Create PlaybackEngine
|
||||||
let mut engine = match stream {
|
let mut engine = match stream {
|
||||||
StreamType::Rodio(ref mixer_sink) => {
|
stream if stream.rodio_sink().is_some() => {
|
||||||
|
let mixer_sink = stream.rodio_sink().expect("rodio sink");
|
||||||
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
||||||
Ok(e) => {
|
Ok(e) => {
|
||||||
*consecutive_sink_failures = 0;
|
*consecutive_sink_failures = 0;
|
||||||
@@ -1698,6 +1774,9 @@ impl Player {
|
|||||||
hardware_volume,
|
hardware_volume,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
_ => unreachable!(
|
||||||
|
"non-rodio stream handled by platform-specific arms"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
||||||
@@ -1896,7 +1975,8 @@ impl Player {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut engine = match stream {
|
let mut engine = match stream {
|
||||||
StreamType::Rodio(ref mixer_sink) => {
|
stream if stream.rodio_sink().is_some() => {
|
||||||
|
let mixer_sink = stream.rodio_sink().expect("rodio sink");
|
||||||
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -1920,6 +2000,9 @@ impl Player {
|
|||||||
hardware_volume,
|
hardware_volume,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
_ => unreachable!(
|
||||||
|
"non-rodio stream handled by platform-specific arms"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let volume =
|
let volume =
|
||||||
@@ -2031,7 +2114,8 @@ impl Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut engine = match stream {
|
let mut engine = match stream {
|
||||||
StreamType::Rodio(ref mixer_sink) => {
|
stream if stream.rodio_sink().is_some() => {
|
||||||
|
let mixer_sink = stream.rodio_sink().expect("rodio sink");
|
||||||
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -2055,6 +2139,9 @@ impl Player {
|
|||||||
hardware_volume,
|
hardware_volume,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
_ => unreachable!(
|
||||||
|
"non-rodio stream handled by platform-specific arms"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
let volume = thread_state.volume.load(Ordering::SeqCst) as f32 / 100.0;
|
||||||
|
|||||||
Reference in new issue
Block a user