diff --git a/src-tauri/src/audio.rs b/src-tauri/src/audio.rs index f121d508..500da89e 100644 --- a/src-tauri/src/audio.rs +++ b/src-tauri/src/audio.rs @@ -1530,6 +1530,20 @@ impl AudioCurrent { fn open_stream_for_device_and_rate(device_name: Option<&str>, desired_rate: u32) -> (rodio::OutputStream, rodio::OutputStreamHandle, u32) { use rodio::cpal::traits::{DeviceTrait, HostTrait}; + // Suppress ALSA stderr noise while enumerating devices on Unix. + #[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(); // Resolve the target device: named device first, fall back to system default. @@ -2993,6 +3007,12 @@ pub fn start_device_watcher(engine: &AudioEngine, app: tauri::AppHandle) { .and_then(|d| d.name().ok()) }).await.unwrap_or(None); + // How many consecutive checks the pinned device has been absent. + // ALSA can temporarily fail to enumerate a device that is busy (e.g. actively + // streaming), so we require 3 consecutive misses (~9 s) before treating it + // as truly unplugged. + let mut pinned_miss_count: u32 = 0; + loop { tokio::time::sleep(Duration::from_secs(3)).await; @@ -3026,7 +3046,14 @@ pub fn start_device_watcher(engine: &AudioEngine, app: tauri::AppHandle) { if let Some(ref dev_name) = pinned { // ── Case 2: pinned device disappeared ──────────────────────── if !available.iter().any(|d| d == dev_name) { + pinned_miss_count += 1; + // Only act after 3 consecutive misses (~9 s) to avoid false + // positives when ALSA temporarily hides a busy device. + if pinned_miss_count < 3 { + continue; + } eprintln!("[psysonic] device-watcher: pinned device '{dev_name}' disconnected, falling back to system default"); + pinned_miss_count = 0; *selected_device.lock().unwrap() = None; // Debounce so the OS finishes reconfiguring. @@ -3053,8 +3080,11 @@ pub fn start_device_watcher(engine: &AudioEngine, app: tauri::AppHandle) { } last_default = current_default; + } else { + // Device is present — reset miss counter. + pinned_miss_count = 0; } - // Pinned device still present — nothing to do. + // Pinned device still present (or not yet confirmed gone) — nothing to do. continue; } diff --git a/src/App.tsx b/src/App.tsx index 67ec901b..419ce0b0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -421,13 +421,10 @@ function TauriEventBridge() { useEffect(() => { let unlisten: (() => void) | undefined; listen('audio:device-changed', () => { - const { currentTrack, currentTime, isPlaying, playTrack, resetAudioPause } = usePlayerStore.getState(); + const { currentTrack, isPlaying, playTrack, resetAudioPause } = usePlayerStore.getState(); if (!currentTrack) return; if (isPlaying) { - const pos = currentTime; - const dur = currentTrack.duration || 1; playTrack(currentTrack); - setTimeout(() => usePlayerStore.getState().seek(pos / dur), 600); } else { // Paused: clear warm-pause flag so the next resume uses the cold path // (audio_play + seek) which creates a new Sink on the new device. @@ -446,10 +443,7 @@ function TauriEventBridge() { const { currentTrack, currentTime, isPlaying, playTrack, resetAudioPause } = usePlayerStore.getState(); if (!currentTrack) return; if (isPlaying) { - const pos = currentTime; - const dur = currentTrack.duration || 1; playTrack(currentTrack); - setTimeout(() => usePlayerStore.getState().seek(pos / dur), 600); } else { resetAudioPause(); } diff --git a/src/locales/de.ts b/src/locales/de.ts index 7d8534a1..51a6fb9e 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -502,6 +502,7 @@ export const deTranslation = { audioOutputDevice: 'Audio-Ausgabegerät', audioOutputDeviceDesc: 'Wähle das Audiogerät, über das Psysonic spielt. Änderungen werden sofort übernommen und starten den aktuellen Track neu.', audioOutputDeviceDefault: 'Systemstandard', + audioOutputDeviceRefresh: 'Geräteliste aktualisieren', hiResTitle: 'Native Hi-Res-Wiedergabe', hiResEnabled: 'Native Hi-Res-Wiedergabe aktivieren', hiResDesc: "Standardmäßig wird auf 44,1 kHz begrenzt (maximale Stabilität). Nur aktivieren, wenn Hardware und Netzwerk zuverlässig hohe Abtastraten (88,2 kHz+) unterstützen.", diff --git a/src/locales/en.ts b/src/locales/en.ts index dcf9a94c..9722a8b4 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -504,6 +504,7 @@ export const enTranslation = { audioOutputDevice: 'Audio Output Device', audioOutputDeviceDesc: 'Select which audio device Psysonic plays through. Changes take effect immediately and restart the current track.', audioOutputDeviceDefault: 'System Default', + audioOutputDeviceRefresh: 'Refresh device list', hiResTitle: 'Native Hi-Res Playback', hiResEnabled: 'Enable native hi-res playback', hiResDesc: "Forces 44.1 kHz output by default for maximum stability. Enable only if your hardware and network reliably support high sample rates (88.2 kHz+).", diff --git a/src/locales/es.ts b/src/locales/es.ts index 99ce3de6..497661a3 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -505,6 +505,7 @@ export const esTranslation = { audioOutputDevice: 'Dispositivo de salida de audio', audioOutputDeviceDesc: 'Selecciona el dispositivo de audio que usa Psysonic. Los cambios surten efecto de inmediato y reinician la pista actual.', audioOutputDeviceDefault: 'Predeterminado del sistema', + audioOutputDeviceRefresh: 'Actualizar lista de dispositivos', hiResTitle: 'Reproducción Nativa Hi-Res', hiResEnabled: 'Habilitar reproducción nativa hi-res', hiResDesc: "Fuerza 44.1 kHz de salida por defecto para máxima estabilidad. Habilita solo si tu hardware y red soportan confiablemente altas tasas de muestreo (88.2 kHz+).", diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 9eb55284..5f4c7d28 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -502,6 +502,7 @@ export const frTranslation = { audioOutputDevice: 'Périphérique de sortie audio', audioOutputDeviceDesc: 'Choisissez le périphérique audio utilisé par Psysonic. Les changements sont immédiats et relancent la piste en cours.', audioOutputDeviceDefault: 'Défaut système', + audioOutputDeviceRefresh: 'Actualiser la liste', hiResTitle: 'Lecture haute résolution native', hiResEnabled: 'Activer la lecture haute résolution native', hiResDesc: "Force une sortie à 44,1 kHz par défaut pour une stabilité maximale. N'activer que si le matériel et le réseau prennent en charge les hautes fréquences d'échantillonnage (88,2 kHz+).", diff --git a/src/locales/nb.ts b/src/locales/nb.ts index eb206888..445645dd 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -503,6 +503,7 @@ export const nbTranslation = { audioOutputDevice: 'Lydutgangsenhet', audioOutputDeviceDesc: 'Velg hvilken lydenhet Psysonic spiller gjennom. Endringer trer i kraft umiddelbart og starter gjeldende spor på nytt.', audioOutputDeviceDefault: 'Systemstandard', + audioOutputDeviceRefresh: 'Oppdater enhetsliste', hiResTitle: 'Innebygd hi-res-avspilling', hiResEnabled: 'Aktiver innebygd hi-res-avspilling', hiResDesc: "Begrenser utdata til 44,1 kHz som standard for maksimal stabilitet. Aktiver kun hvis maskinvare og nettverk støtter høye samplingsrater (88,2 kHz+) pålitelig.", diff --git a/src/locales/nl.ts b/src/locales/nl.ts index bf51d6ba..53001b8e 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -501,6 +501,7 @@ export const nlTranslation = { audioOutputDevice: 'Audio-uitvoerapparaat', audioOutputDeviceDesc: 'Kies via welk audioapparaat Psysonic afspeelt. Wijzigingen worden direct toegepast en starten het huidige nummer opnieuw.', audioOutputDeviceDefault: 'Systeemstandaard', + audioOutputDeviceRefresh: 'Apparatenlijst vernieuwen', hiResTitle: 'Natieve hi-res-weergave', hiResEnabled: 'Natieve hi-res-weergave inschakelen', hiResDesc: "Beperkt de uitvoer standaard tot 44,1 kHz voor maximale stabiliteit. Alleen inschakelen als hardware en netwerk hoge samplerates (88,2 kHz+) ondersteunen.", diff --git a/src/locales/ru.ts b/src/locales/ru.ts index 87309508..dc1bad43 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -519,6 +519,7 @@ export const ruTranslation = { audioOutputDevice: 'Устройство вывода звука', audioOutputDeviceDesc: 'Выберите аудиоустройство для воспроизведения. Изменения применяются немедленно и перезапускают текущий трек.', audioOutputDeviceDefault: 'Системное по умолчанию', + audioOutputDeviceRefresh: 'Обновить список устройств', hiResTitle: 'Нативное воспроизведение Hi-Res', hiResEnabled: 'Включить нативное Hi-Res воспроизведение', hiResDesc: "По умолчанию вывод ограничен до 44,1 кГц для максимальной стабильности. Включайте только если оборудование и сеть надёжно поддерживают высокие частоты (88,2 кГц+).", diff --git a/src/locales/zh.ts b/src/locales/zh.ts index ccfc10fe..57dbc527 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -497,6 +497,7 @@ export const zhTranslation = { audioOutputDevice: '音频输出设备', audioOutputDeviceDesc: '选择 Psysonic 播放音频的设备。更改立即生效并重新开始当前曲目。', audioOutputDeviceDefault: '系统默认', + audioOutputDeviceRefresh: '刷新设备列表', hiResTitle: '原生高清晰度播放', hiResEnabled: '启用原生高清晰度播放', hiResDesc: "默认强制 44.1 kHz 输出以获得最大稳定性。仅在硬件和网络可靠支持高采样率(88.2 kHz+)时启用。", diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 0e0c14cd..16af5266 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -559,7 +559,7 @@ export default function Settings() { className="icon-btn" onClick={refreshAudioDevices} disabled={devicesLoading || deviceSwitching} - data-tooltip={t('common.refresh')} + data-tooltip={t('settings.audioOutputDeviceRefresh')} >