mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +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",
|
"discord-rich-presence",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"id3",
|
"id3",
|
||||||
|
"libc",
|
||||||
"lofty",
|
"lofty",
|
||||||
"md5",
|
"md5",
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ thread-priority = "1"
|
|||||||
lofty = "0.22"
|
lofty = "0.22"
|
||||||
id3 = "1.16.4"
|
id3 = "1.16.4"
|
||||||
|
|
||||||
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows = { version = "0.58", features = [
|
windows = { version = "0.58", features = [
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ use std::io::{Cursor, Read, Seek, SeekFrom};
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
#[cfg(unix)]
|
||||||
|
use libc;
|
||||||
|
|
||||||
use ringbuf::{HeapConsumer, HeapProducer, HeapRb};
|
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.
|
/// 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]
|
#[tauri::command]
|
||||||
pub fn audio_list_devices() -> Vec<String> {
|
pub fn audio_list_devices() -> Vec<String> {
|
||||||
use rodio::cpal::traits::{DeviceTrait, HostTrait};
|
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();
|
let host = rodio::cpal::default_host();
|
||||||
host.output_devices()
|
host.output_devices()
|
||||||
.map(|iter| iter.filter_map(|d| d.name().ok()).collect())
|
.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;
|
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||||
|
|
||||||
// Enumerate all available output devices and the current default.
|
// 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(|| {
|
let (current_default, available) = tauri::async_runtime::spawn_blocking(|| {
|
||||||
use rodio::cpal::traits::{DeviceTrait, HostTrait};
|
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 host = rodio::cpal::default_host();
|
||||||
let default = host.default_output_device().and_then(|d| d.name().ok());
|
let default = host.default_output_device().and_then(|d| d.name().ok());
|
||||||
let available: Vec<String> = host
|
let available: Vec<String> = host
|
||||||
|
|||||||
+11
-2
@@ -221,6 +221,10 @@ function snapHotCacheMb(v: number): number {
|
|||||||
return Math.round((x - 32) / 32) * 32 + 32;
|
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() {
|
export default function Settings() {
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const theme = useThemeStore();
|
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));
|
invoke<number>('get_hot_cache_size', { customDir: auth.hotCacheDownloadDir || null }).then(setHotCacheBytes).catch(() => setHotCacheBytes(0));
|
||||||
}, [activeTab, auth.offlineDownloadDir, auth.hotCacheDownloadDir]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (activeTab !== 'audio') return;
|
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]);
|
}, [activeTab]);
|
||||||
|
|
||||||
/** Live disk usage for hot cache while Audio settings are open (interval + refresh when index changes). */
|
/** Live disk usage for hot cache while Audio settings are open (interval + refresh when index changes). */
|
||||||
|
|||||||
Reference in New Issue
Block a user