From ce8d3890702c94e82cc220d6ba65b3444fb3b5ec Mon Sep 17 00:00:00 2001 From: Frank Stellmacher <171614930+Psychotoxical@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:38:05 +0200 Subject: [PATCH] fix(linux): prefer pipewire/pulse over raw ALSA default for audio output (#288) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On PipeWire-based distros (Debian 13, Ubuntu 22+, Gnome-on-PipeWire, and similar), cpal's default_output_device() resolves to the raw ALSA `default` alias. During early app-start that alias sometimes routes to a null sink: the stream opens without error, progress ticks run, but nothing reaches the user. Closes #234. Prefer the pipewire-alsa / pulse-alsa aliases explicitly before falling back to cpal's default, but only on Linux — macOS / Windows paths are untouched. Systems that don't expose either alias (pure ALSA, no PipeWire) fall through to the original default-device path. Co-authored-by: Claude Opus 4.7 (1M context) --- src-tauri/src/audio.rs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/audio.rs b/src-tauri/src/audio.rs index 2046d898..3be25d25 100644 --- a/src-tauri/src/audio.rs +++ b/src-tauri/src/audio.rs @@ -2076,10 +2076,28 @@ fn open_stream_for_device_and_rate(device_name: Option<&str>, desired_rate: u32) let host = rodio::cpal::default_host(); - // Resolve the target device: named device first, fall back to system default. - let device = device_name.and_then(|name| { + // Resolve the target device: explicit name first, then (on Linux) prefer + // a "pipewire" or "pulse" ALSA alias before falling back to cpal's system + // default. On PipeWire-based distros the raw ALSA `default` alias can + // route to a null sink at app-start (issue #234 on Debian 13): the stream + // opens cleanly, progress ticks run, no audio reaches the user. The + // named-alias path goes through pipewire-alsa's real sink and just works. + // On systems where neither alias exists (pure ALSA, macOS, Windows), + // `find_by_name` returns None and we drop through to `default_output_device` + // unchanged — no regression. + let find_by_name = |name: &str| -> Option<_> { host.output_devices().ok()?.find(|d| d.name().ok().as_deref() == Some(name)) - }).or_else(|| host.default_output_device()); + }; + + let device = device_name + .and_then(find_by_name) + .or_else(|| { + #[cfg(target_os = "linux")] + { find_by_name("pipewire").or_else(|| find_by_name("pulse")) } + #[cfg(not(target_os = "linux"))] + { None } + }) + .or_else(|| host.default_output_device()); if let Some(device) = device { if desired_rate > 0 {