mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
fix(audio): Linux output device selection and watcher (#176)
* fix(audio): stabilize Linux output device picker and watcher Keep pinned ALSA/cpal device ids stable when enumeration omits the active sink or returns an equivalent name. Stop Linux device-watcher from clearing the pin based on missing list entries; macOS and Windows still treat repeated absence as unplugged. Settings refresh flow calls canonicalize and refetches the list; add i18n for the out-of-list device label. * fix(settings): sort audio output devices by label cpal enumeration order is arbitrary; order the dropdown by readable label and place the current OS default device first among concrete outputs.
This commit is contained in:
@@ -505,6 +505,7 @@ export const deTranslation = {
|
||||
audioOutputDeviceRefresh: 'Geräteliste aktualisieren',
|
||||
audioOutputDeviceOsDefaultNow: 'aktuelle Systemausgabe',
|
||||
audioOutputDeviceListError: 'Audiogeräteliste konnte nicht geladen werden.',
|
||||
audioOutputDeviceNotInCurrentList: 'nicht in der aktuellen Liste',
|
||||
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.",
|
||||
|
||||
@@ -507,6 +507,7 @@ export const enTranslation = {
|
||||
audioOutputDeviceRefresh: 'Refresh device list',
|
||||
audioOutputDeviceOsDefaultNow: 'current system output',
|
||||
audioOutputDeviceListError: 'Could not load the audio device list.',
|
||||
audioOutputDeviceNotInCurrentList: 'not in current 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+).",
|
||||
|
||||
@@ -508,6 +508,7 @@ export const esTranslation = {
|
||||
audioOutputDeviceRefresh: 'Actualizar lista de dispositivos',
|
||||
audioOutputDeviceOsDefaultNow: 'salida actual del sistema',
|
||||
audioOutputDeviceListError: 'No se pudo cargar la lista de dispositivos de audio.',
|
||||
audioOutputDeviceNotInCurrentList: 'no está en la lista actual',
|
||||
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+).",
|
||||
|
||||
@@ -505,6 +505,7 @@ export const frTranslation = {
|
||||
audioOutputDeviceRefresh: 'Actualiser la liste',
|
||||
audioOutputDeviceOsDefaultNow: 'sortie système actuelle',
|
||||
audioOutputDeviceListError: 'Impossible de charger la liste des périphériques audio.',
|
||||
audioOutputDeviceNotInCurrentList: 'absent de la liste actuelle',
|
||||
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+).",
|
||||
|
||||
@@ -506,6 +506,7 @@ export const nbTranslation = {
|
||||
audioOutputDeviceRefresh: 'Oppdater enhetsliste',
|
||||
audioOutputDeviceOsDefaultNow: 'gjeldende systemutgang',
|
||||
audioOutputDeviceListError: 'Kunne ikke laste listen over lydenheter.',
|
||||
audioOutputDeviceNotInCurrentList: 'ikke i gjeldende liste',
|
||||
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.",
|
||||
|
||||
@@ -504,6 +504,7 @@ export const nlTranslation = {
|
||||
audioOutputDeviceRefresh: 'Apparatenlijst vernieuwen',
|
||||
audioOutputDeviceOsDefaultNow: 'huidige systeemuitvoer',
|
||||
audioOutputDeviceListError: 'De lijst met audio-apparaten kon niet worden geladen.',
|
||||
audioOutputDeviceNotInCurrentList: 'staat niet in de huidige lijst',
|
||||
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.",
|
||||
|
||||
@@ -522,6 +522,7 @@ export const ruTranslation = {
|
||||
audioOutputDeviceRefresh: 'Обновить список устройств',
|
||||
audioOutputDeviceOsDefaultNow: 'текущий системный вывод',
|
||||
audioOutputDeviceListError: 'Не удалось загрузить список аудиоустройств.',
|
||||
audioOutputDeviceNotInCurrentList: 'нет в текущем списке',
|
||||
hiResTitle: 'Нативное воспроизведение Hi-Res',
|
||||
hiResEnabled: 'Включить нативное Hi-Res воспроизведение',
|
||||
hiResDesc: "По умолчанию вывод ограничен до 44,1 кГц для максимальной стабильности. Включайте только если оборудование и сеть надёжно поддерживают высокие частоты (88,2 кГц+).",
|
||||
|
||||
@@ -500,6 +500,7 @@ export const zhTranslation = {
|
||||
audioOutputDeviceRefresh: '刷新设备列表',
|
||||
audioOutputDeviceOsDefaultNow: '当前系统输出',
|
||||
audioOutputDeviceListError: '无法加载音频设备列表。',
|
||||
audioOutputDeviceNotInCurrentList: '不在当前列表中',
|
||||
hiResTitle: '原生高清晰度播放',
|
||||
hiResEnabled: '启用原生高清晰度播放',
|
||||
hiResDesc: "默认强制 44.1 kHz 输出以获得最大稳定性。仅在硬件和网络可靠支持高采样率(88.2 kHz+)时启用。",
|
||||
|
||||
+44
-4
@@ -300,17 +300,44 @@ function disambiguatedAudioDeviceLabel(raw: string, baseLabel: string, duplicate
|
||||
return `${baseLabel} · ${audioDeviceDuplicateHint(raw)}`;
|
||||
}
|
||||
|
||||
/** cpal order is arbitrary; sort by readable label, current OS default first. */
|
||||
function sortAudioDeviceIds(devices: string[], osDefaultDeviceId: string | null): string[] {
|
||||
return [...devices].sort((a, b) => {
|
||||
const aDef = osDefaultDeviceId && a === osDefaultDeviceId;
|
||||
const bDef = osDefaultDeviceId && b === osDefaultDeviceId;
|
||||
if (aDef !== bDef) return aDef ? -1 : 1;
|
||||
const la = formatAudioDeviceLabel(a);
|
||||
const lb = formatAudioDeviceLabel(b);
|
||||
const byLabel = la.localeCompare(lb, undefined, { sensitivity: 'base' });
|
||||
if (byLabel !== 0) return byLabel;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
}
|
||||
|
||||
function buildAudioDeviceSelectOptions(
|
||||
devices: string[],
|
||||
defaultLabel: string,
|
||||
osDefaultDeviceId: string | null,
|
||||
osDefaultMark: string,
|
||||
pinnedDevice: string | null,
|
||||
notInListSuffix: string,
|
||||
): { value: string; label: string }[] {
|
||||
const baseLabels = devices.map(formatAudioDeviceLabel);
|
||||
const countByBase = new Map<string, number>();
|
||||
for (const b of baseLabels) countByBase.set(b, (countByBase.get(b) ?? 0) + 1);
|
||||
const pinned = pinnedDevice?.trim() || null;
|
||||
const pinnedNotListed = !!(pinned && !devices.includes(pinned));
|
||||
const ghost: { value: string; label: string }[] = pinnedNotListed
|
||||
? (() => {
|
||||
const base = formatAudioDeviceLabel(pinned);
|
||||
let label = `${base} · ${notInListSuffix}`;
|
||||
if (osDefaultDeviceId && pinned === osDefaultDeviceId) label = `${label} · ${osDefaultMark}`;
|
||||
return [{ value: pinned, label }];
|
||||
})()
|
||||
: [];
|
||||
return [
|
||||
{ value: '', label: defaultLabel },
|
||||
...ghost,
|
||||
...devices.map((d, i) => {
|
||||
const base = baseLabels[i];
|
||||
const dup = (countByBase.get(base) ?? 0) > 1;
|
||||
@@ -390,9 +417,20 @@ export default function Settings() {
|
||||
});
|
||||
const defP = invoke<string | null>('audio_default_output_device_name').catch(() => null);
|
||||
Promise.all([listP, defP])
|
||||
.then(([devices, osDefault]) => {
|
||||
setAudioDevices(devices);
|
||||
setOsDefaultAudioDeviceId(osDefault ?? null);
|
||||
.then(async ([devices, osDefault]) => {
|
||||
let canon: string | null = null;
|
||||
try {
|
||||
canon = await invoke<string | null>('audio_canonicalize_selected_device');
|
||||
if (canon) useAuthStore.getState().setAudioOutputDevice(canon);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const finalList = canon
|
||||
? await invoke<string[]>('audio_list_devices').catch(() => devices)
|
||||
: devices;
|
||||
const defId = osDefault ?? null;
|
||||
setAudioDevices(sortAudioDeviceIds(finalList, defId));
|
||||
setOsDefaultAudioDeviceId(defId);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!silent) setDevicesLoading(false);
|
||||
@@ -671,11 +709,13 @@ export default function Settings() {
|
||||
t('settings.audioOutputDeviceDefault'),
|
||||
osDefaultAudioDeviceId,
|
||||
t('settings.audioOutputDeviceOsDefaultNow'),
|
||||
auth.audioOutputDevice,
|
||||
t('settings.audioOutputDeviceNotInCurrentList'),
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
className="icon-btn"
|
||||
onClick={refreshAudioDevices}
|
||||
onClick={() => refreshAudioDevices()}
|
||||
disabled={devicesLoading || deviceSwitching}
|
||||
data-tooltip={t('settings.audioOutputDeviceRefresh')}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user