mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
fix(audio): device-reset false positive + stderr noise + i18n tooltip
- Device watcher requires 3 consecutive misses (~9 s) before triggering audio:device-reset, preventing false positives when ALSA temporarily hides a busy device from output_devices() enumeration - Add stderr suppression to open_stream_for_device_and_rate (last source of ALSA terminal noise on Linux) - Fix tooltip showing raw key 'common.refresh' — replaced with settings.audioOutputDeviceRefresh (added to all 8 locales) - Device switch restarts track from beginning (no seek-back) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+31
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+1
-7
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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+).",
|
||||
|
||||
@@ -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+).",
|
||||
|
||||
@@ -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+).",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -519,6 +519,7 @@ export const ruTranslation = {
|
||||
audioOutputDevice: 'Устройство вывода звука',
|
||||
audioOutputDeviceDesc: 'Выберите аудиоустройство для воспроизведения. Изменения применяются немедленно и перезапускают текущий трек.',
|
||||
audioOutputDeviceDefault: 'Системное по умолчанию',
|
||||
audioOutputDeviceRefresh: 'Обновить список устройств',
|
||||
hiResTitle: 'Нативное воспроизведение Hi-Res',
|
||||
hiResEnabled: 'Включить нативное Hi-Res воспроизведение',
|
||||
hiResDesc: "По умолчанию вывод ограничен до 44,1 кГц для максимальной стабильности. Включайте только если оборудование и сеть надёжно поддерживают высокие частоты (88,2 кГц+).",
|
||||
|
||||
@@ -497,6 +497,7 @@ export const zhTranslation = {
|
||||
audioOutputDevice: '音频输出设备',
|
||||
audioOutputDeviceDesc: '选择 Psysonic 播放音频的设备。更改立即生效并重新开始当前曲目。',
|
||||
audioOutputDeviceDefault: '系统默认',
|
||||
audioOutputDeviceRefresh: '刷新设备列表',
|
||||
hiResTitle: '原生高清晰度播放',
|
||||
hiResEnabled: '启用原生高清晰度播放',
|
||||
hiResDesc: "默认强制 44.1 kHz 输出以获得最大稳定性。仅在硬件和网络可靠支持高采样率(88.2 kHz+)时启用。",
|
||||
|
||||
@@ -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')}
|
||||
>
|
||||
<RotateCcw size={15} className={devicesLoading ? 'spin' : ''} />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user