diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4bc9fb46..b52ee90e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3564,6 +3564,7 @@ dependencies = [ "discord-rich-presence", "futures-util", "id3", + "libc", "lofty", "md5", "reqwest 0.12.28", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 040a9985..8ba6694f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -47,6 +47,9 @@ thread-priority = "1" lofty = "0.22" id3 = "1.16.4" +[target.'cfg(unix)'.dependencies] +libc = "0.2" + [target.'cfg(windows)'.dependencies] windows = { version = "0.58", features = [ "Win32_Foundation", diff --git a/src-tauri/src/audio.rs b/src-tauri/src/audio.rs index 17135f3a..f121d508 100644 --- a/src-tauri/src/audio.rs +++ b/src-tauri/src/audio.rs @@ -2,6 +2,8 @@ use std::io::{Cursor, Read, Seek, SeekFrom}; use std::sync::{Arc, Mutex}; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; use std::time::{Duration, Instant}; +#[cfg(unix)] +use libc; use ringbuf::{HeapConsumer, HeapProducer, HeapRb}; @@ -2896,9 +2898,29 @@ pub async fn audio_play_radio( } /// Returns the names of all available audio output devices on the current host. +/// On Linux, ALSA probes unavailable backends (JACK, OSS, dmix) and prints errors to +/// stderr. We suppress fd 2 for the duration of enumeration to keep the terminal clean. #[tauri::command] pub fn audio_list_devices() -> Vec { use rodio::cpal::traits::{DeviceTrait, HostTrait}; + + #[cfg(unix)] + let _guard = { + struct StderrGuard(i32); + impl Drop for StderrGuard { + fn drop(&mut self) { + unsafe { libc::dup2(self.0, 2); libc::close(self.0); } + } + } + unsafe { + let saved = libc::dup(2); + let devnull = libc::open(b"/dev/null\0".as_ptr() as *const libc::c_char, libc::O_WRONLY); + libc::dup2(devnull, 2); + libc::close(devnull); + StderrGuard(saved) + } + }; + let host = rodio::cpal::default_host(); host.output_devices() .map(|iter| iter.filter_map(|d| d.name().ok()).collect()) @@ -2975,8 +2997,21 @@ pub fn start_device_watcher(engine: &AudioEngine, app: tauri::AppHandle) { tokio::time::sleep(Duration::from_secs(3)).await; // Enumerate all available output devices and the current default. + // Suppress stderr on Unix to avoid ALSA probing noise (JACK, OSS, dmix). let (current_default, available) = tauri::async_runtime::spawn_blocking(|| { use rodio::cpal::traits::{DeviceTrait, HostTrait}; + #[cfg(unix)] + let _guard = unsafe { + struct StderrGuard(i32); + impl Drop for StderrGuard { + fn drop(&mut self) { unsafe { libc::dup2(self.0, 2); libc::close(self.0); } } + } + let saved = libc::dup(2); + let devnull = libc::open(b"/dev/null\0".as_ptr() as *const libc::c_char, libc::O_WRONLY); + libc::dup2(devnull, 2); + libc::close(devnull); + StderrGuard(saved) + }; let host = rodio::cpal::default_host(); let default = host.default_output_device().and_then(|d| d.name().ok()); let available: Vec = host diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 514cba70..475ffc24 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -221,6 +221,10 @@ function snapHotCacheMb(v: number): number { return Math.round((x - 32) / 32) * 32 + 32; } +// Module-level cache — device enumeration triggers ALSA probing on Linux (stderr noise), +// so we only enumerate once per app session. +let _audioDevicesCache: string[] | null = null; + export default function Settings() { const auth = useAuthStore(); const theme = useThemeStore(); @@ -278,10 +282,15 @@ export default function Settings() { invoke('get_hot_cache_size', { customDir: auth.hotCacheDownloadDir || null }).then(setHotCacheBytes).catch(() => setHotCacheBytes(0)); }, [activeTab, auth.offlineDownloadDir, auth.hotCacheDownloadDir]); - // Load available audio output devices when Audio tab is open. + // Load available audio output devices when Audio tab is first opened. + // Uses a module-level cache so ALSA enumeration (noisy on Linux) only happens once per session. useEffect(() => { if (activeTab !== 'audio') return; - invoke('audio_list_devices').then(setAudioDevices).catch(() => {}); + if (_audioDevicesCache) { setAudioDevices(_audioDevicesCache); return; } + invoke('audio_list_devices').then(devices => { + _audioDevicesCache = devices; + setAudioDevices(devices); + }).catch(() => {}); }, [activeTab]); /** Live disk usage for hot cache while Audio settings are open (interval + refresh when index changes). */