mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
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:
Generated
+1
@@ -3564,6 +3564,7 @@ dependencies = [
|
||||
"discord-rich-presence",
|
||||
"futures-util",
|
||||
"id3",
|
||||
"libc",
|
||||
"lofty",
|
||||
"md5",
|
||||
"reqwest 0.12.28",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
+11
-2
@@ -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<number>('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<string[]>('audio_list_devices').then(setAudioDevices).catch(() => {});
|
||||
if (_audioDevicesCache) { setAudioDevices(_audioDevicesCache); return; }
|
||||
invoke<string[]>('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). */
|
||||
|
||||
Reference in New Issue
Block a user