fix(audio): suppress ALSA stderr noise during device enumeration

Device watcher (3 s loop) and audio_list_devices both called
cpal output_devices(), triggering ALSA to probe unavailable backends
(JACK, OSS, dmix) and spam stderr with error messages.

Fix: redirect fd 2 to /dev/null for the duration of each enumeration
on Unix via libc dup/dup2 + RAII guard. Also add a module-level
cache in Settings.tsx so audio_list_devices is only invoked once
per app session instead of on every Audio tab activation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-13 18:30:24 +02:00
parent 16cb4f5a6f
commit 1cf5faff21
4 changed files with 50 additions and 2 deletions
+1
View File
@@ -3564,6 +3564,7 @@ dependencies = [
"discord-rich-presence",
"futures-util",
"id3",
"libc",
"lofty",
"md5",
"reqwest 0.12.28",
+3
View File
@@ -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",
+35
View File
@@ -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<String> {
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<String> = host