fix(audio): default to shared System output and resolve Auto to a concrete backend (#470)
OOTB playback froze (seekbar advancing, no audio) on hosts where the configured backend was not usable — e.g. PipeWire as the Linux default but `pactl` absent (minimal Debian/Ubuntu). "Auto" (a null backend_type) fell through to a legacy CPAL path that forced the track rate onto the shared default device, producing a suspended/silent stream and leaving a process-wide stuck audio handle that survived Reset. - Add a cross-platform "System" backend (CpalDefaultBackend now on Linux too): opens the OS default output at its negotiated rate with rodio resampling and an explicit ~100ms CPAL buffer (MixerDeviceSink has zero internal buffering, so the bare default underran and stayed suspended). Shared, no exclusivity, no `pactl` — mixes with other apps like any normal player. - Make System the OOTB default and what Reset lands on; seed it on first run. Drop the per-startup #375 NULL->PipeWire backfill migration. - Resolve a null backend_type to System in the player so the legacy CPAL path is never reached (covers legacy installs and headless callers). - "Auto" in Settings is now a resolve-and-set action: it picks the best available backend (PipeWire if available, else System), stores that concrete value and updates the dropdown + device picker, so backend_type is never null. - Explicit PipeWire (DAC passthrough) and ALSA Direct (exclusive) configs are untouched and remain bit-perfect. cc
This commit is contained in:
10 files changed
+111
-75
No files matched your search
@@ -4,7 +4,8 @@
|
|||||||
//! allowing users to choose their preferred audio stack.
|
//! allowing users to choose their preferred audio stack.
|
||||||
|
|
||||||
use rodio::MixerDeviceSink;
|
use rodio::MixerDeviceSink;
|
||||||
#[cfg(not(target_os = "linux"))]
|
// CPAL traits + DeviceSinkBuilder are used by the cross-platform CpalDefaultBackend
|
||||||
|
// ("System") on every OS, including Linux where it opens the ALSA "default" PCM.
|
||||||
use rodio::{
|
use rodio::{
|
||||||
cpal::traits::{DeviceTrait, HostTrait},
|
cpal::traits::{DeviceTrait, HostTrait},
|
||||||
DeviceSinkBuilder,
|
DeviceSinkBuilder,
|
||||||
@@ -40,11 +41,11 @@ pub enum AudioBackendType {
|
|||||||
|
|
||||||
impl Default for AudioBackendType {
|
impl Default for AudioBackendType {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
if cfg!(target_os = "linux") {
|
// "System" everywhere: the OOTB default plays through the OS default
|
||||||
AudioBackendType::PipeWire
|
// output, shared with other apps (no bit-perfect, no `pactl`). Audiophile
|
||||||
} else {
|
// users opt into PipeWire / ALSA explicitly. This was PipeWire on Linux,
|
||||||
AudioBackendType::SystemDefault
|
// which hard-required `pactl` and froze OOTB playback without it (#470).
|
||||||
}
|
AudioBackendType::SystemDefault
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,6 +270,10 @@ impl BackendManager {
|
|||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
|
// System default (always available): shared OS default output via the
|
||||||
|
// ALSA "default" PCM. The app-like OOTB choice; listed first.
|
||||||
|
backends.push(AudioBackendType::SystemDefault);
|
||||||
|
|
||||||
// PipeWire (check if running)
|
// PipeWire (check if running)
|
||||||
if Self::is_pipewire_available() {
|
if Self::is_pipewire_available() {
|
||||||
backends.push(AudioBackendType::PipeWire);
|
backends.push(AudioBackendType::PipeWire);
|
||||||
@@ -314,14 +319,12 @@ impl BackendManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
AudioBackendType::SystemDefault => {
|
AudioBackendType::SystemDefault => {
|
||||||
#[cfg(not(target_os = "linux"))]
|
// "System": play through the OS default output via CPAL's default
|
||||||
{
|
// host — CoreAudio/WASAPI off-Linux, the ALSA "default" PCM on Linux
|
||||||
Ok(Box::new(CpalDefaultBackend::new()?))
|
// (routes to PipeWire/Pulse/dmix for shared mixing). Opens at the
|
||||||
}
|
// device's negotiated rate (rodio resamples); shared, no exclusivity,
|
||||||
#[cfg(target_os = "linux")]
|
// no `pactl`. Available on every platform.
|
||||||
{
|
Ok(Box::new(CpalDefaultBackend::new()?))
|
||||||
Err("SystemDefault backend is not available on Linux; use PipeWire, ALSA, or Pulse".to_string())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
AudioBackendType::Alsa => {
|
AudioBackendType::Alsa => {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -388,14 +391,15 @@ impl BackendManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CPAL default backend for non-Linux platforms (macOS CoreAudio, Windows WASAPI).
|
/// CPAL default backend ("System"): plays through the OS default output via
|
||||||
/// Uses the system default audio device via CPAL without any platform-specific commands.
|
/// CPAL's default host — CoreAudio (macOS), WASAPI (Windows), and the ALSA
|
||||||
#[cfg(not(target_os = "linux"))]
|
/// "default" PCM (Linux, which routes to PipeWire/Pulse/dmix for shared mixing).
|
||||||
|
/// No platform-specific commands (no `pactl`); opens at the device's negotiated
|
||||||
|
/// rate with rodio resampling, so it mixes with other apps like any normal player.
|
||||||
pub struct CpalDefaultBackend {
|
pub struct CpalDefaultBackend {
|
||||||
host: rodio::cpal::Host,
|
host: rodio::cpal::Host,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
impl CpalDefaultBackend {
|
impl CpalDefaultBackend {
|
||||||
pub fn new() -> BackendResult<Self> {
|
pub fn new() -> BackendResult<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -404,7 +408,6 @@ impl CpalDefaultBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
impl AudioBackend for CpalDefaultBackend {
|
impl AudioBackend for CpalDefaultBackend {
|
||||||
fn backend_type(&self) -> AudioBackendType {
|
fn backend_type(&self) -> AudioBackendType {
|
||||||
AudioBackendType::SystemDefault
|
AudioBackendType::SystemDefault
|
||||||
@@ -532,6 +535,22 @@ impl AudioBackend for CpalDefaultBackend {
|
|||||||
let builder = DeviceSinkBuilder::from_device(device)
|
let builder = DeviceSinkBuilder::from_device(device)
|
||||||
.map_err(|e| format!("Failed to create device sink builder: {}", e))?;
|
.map_err(|e| format!("Failed to create device sink builder: {}", e))?;
|
||||||
|
|
||||||
|
// MixerDeviceSink has zero internal buffering, so CPAL's buffer is the
|
||||||
|
// ONLY buffer between the mixer and the hardware. With the bare CPAL/ALSA
|
||||||
|
// default (no explicit size) the stream can underrun immediately on Linux:
|
||||||
|
// the node links to the audio server but stays suspended and never feeds
|
||||||
|
// audio (#470 — "System" shared output silent). Give it ~100ms, matching
|
||||||
|
// the PipeWire/ALSA backends. We deliberately do NOT pin the sample rate
|
||||||
|
// (no with_supported_config): the device keeps its negotiated rate and
|
||||||
|
// rodio resamples, which is the whole point of shared "System" output.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let mixer_sink = builder
|
||||||
|
.with_buffer_size(rodio::cpal::BufferSize::Fixed(
|
||||||
|
(config.sample_rate / 10).clamp(1024, 19200),
|
||||||
|
))
|
||||||
|
.open_stream()
|
||||||
|
.map_err(|e| format!("Failed to create output stream: {}", e))?;
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
let mixer_sink = builder
|
let mixer_sink = builder
|
||||||
.open_stream()
|
.open_stream()
|
||||||
.map_err(|e| format!("Failed to create output stream: {}", e))?;
|
.map_err(|e| format!("Failed to create output stream: {}", e))?;
|
||||||
|
|||||||
@@ -80,14 +80,17 @@ impl Default for AudioSettings {
|
|||||||
exclusive_mode: false,
|
exclusive_mode: false,
|
||||||
dac_passthrough: false,
|
dac_passthrough: false,
|
||||||
preferred_sample_rate: None,
|
preferred_sample_rate: None,
|
||||||
// Default to a per-OS-correct backend (PipeWire on Linux, SystemDefault
|
// OOTB default is "System" (Some(SystemDefault)): play through the OS
|
||||||
// elsewhere) instead of None. None used to mean "auto-detect" but it
|
// default output, shared with other apps like any normal player — no
|
||||||
// actually fell through to the legacy CPAL path in init_device, which
|
// bit-perfect, no `pactl`. See AudioBackendType::default(). "Auto"
|
||||||
// has a known race against rodio's DeviceSink drop on resume — visible
|
// (None) and explicit backends (PipeWire / ALSA) are honored as-is;
|
||||||
// as "audio doesn't come back after pause" (#375). Picking a concrete
|
// this only sets what a fresh install and the Reset action land on.
|
||||||
// backend here puts new installs on the proper backend init path; the
|
//
|
||||||
// runtime still degrades gracefully if the chosen backend isn't
|
// History: this defaulted to Some(PipeWire) on Linux to dodge a rodio
|
||||||
// available on the host.
|
// DeviceSink-drop-on-resume race on the CPAL path (#375), but that
|
||||||
|
// hard-required `pactl` and froze OOTB playback without it (#470). The
|
||||||
|
// #375 race is covered by the cpal 0.17.3 / alsa 0.11 stream-drop
|
||||||
|
// fixes, and "System" is the app-like default audiophiles override.
|
||||||
backend_type: Some(AudioBackendType::default()),
|
backend_type: Some(AudioBackendType::default()),
|
||||||
alsa_plugin: Some(AlsaPlugin::Hw), // Default to hw (bit-perfect)
|
alsa_plugin: Some(AlsaPlugin::Hw), // Default to hw (bit-perfect)
|
||||||
alsa_hardware_volume: false, // Disabled by default (maximum compatibility)
|
alsa_hardware_volume: false, // Disabled by default (maximum compatibility)
|
||||||
@@ -138,9 +141,7 @@ impl AudioSettingsStore {
|
|||||||
alsa_hardware_volume INTEGER NOT NULL DEFAULT 0,
|
alsa_hardware_volume INTEGER NOT NULL DEFAULT 0,
|
||||||
stream_first_track INTEGER NOT NULL DEFAULT 0,
|
stream_first_track INTEGER NOT NULL DEFAULT 0,
|
||||||
stream_buffer_seconds INTEGER NOT NULL DEFAULT 3
|
stream_buffer_seconds INTEGER NOT NULL DEFAULT 3
|
||||||
);
|
);",
|
||||||
INSERT OR IGNORE INTO audio_settings (id, exclusive_mode, dac_passthrough)
|
|
||||||
VALUES (1, 0, 0);",
|
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to create audio settings table: {}", e))?;
|
.map_err(|e| format!("Failed to create audio settings table: {}", e))?;
|
||||||
|
|
||||||
@@ -215,29 +216,23 @@ impl AudioSettingsStore {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Migration (#375): existing installs persisted backend_type = NULL when
|
// Seed the single settings row on first run with the OOTB default backend
|
||||||
// the user never explicitly picked a backend. NULL fell through to the
|
// ("System"). INSERT OR IGNORE is a one-time seed: it only fires when the
|
||||||
// legacy CPAL path in the player, which has a race against rodio's
|
// row does not exist yet, so existing installs are never rewritten on
|
||||||
// DeviceSink drop on resume that produces silent streams after pause.
|
// later launches (settings are only reset by the explicit Reset action,
|
||||||
// Backfill NULL rows with the per-OS default so those installs move onto
|
// never just by restarting).
|
||||||
// the proper backend init path. set_backend_type is the canonical writer
|
//
|
||||||
// (it serialises via serde_json), so we go through the same JSON shape
|
// There is deliberately NO backfill of existing NULL backend_type rows: a
|
||||||
// here for the row read path to round-trip cleanly.
|
// NULL backend_type means "Auto" and is preserved as-is. (An earlier #375
|
||||||
|
// workaround backfilled NULL -> PipeWire on every open, which hard-required
|
||||||
|
// `pactl` and froze OOTB playback without it, #470.)
|
||||||
let default_backend_json = serde_json::to_string(&AudioBackendType::default())
|
let default_backend_json = serde_json::to_string(&AudioBackendType::default())
|
||||||
.unwrap_or_else(|_| {
|
.map_err(|e| format!("Failed to serialize default backend: {}", e))?;
|
||||||
// Fallback only matters if serde_json::to_string fails for a
|
conn.execute(
|
||||||
// unit-variant enum, which it can't — but keep a sane string in
|
"INSERT OR IGNORE INTO audio_settings (id, exclusive_mode, dac_passthrough, backend_type) VALUES (1, 0, 0, ?1)",
|
||||||
// case the enum gains a non-trivial variant in the future.
|
|
||||||
if cfg!(target_os = "linux") {
|
|
||||||
"\"PipeWire\"".to_string()
|
|
||||||
} else {
|
|
||||||
"\"SystemDefault\"".to_string()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let _ = conn.execute(
|
|
||||||
"UPDATE audio_settings SET backend_type = ?1 WHERE id = 1 AND backend_type IS NULL",
|
|
||||||
params![default_backend_json],
|
params![default_backend_json],
|
||||||
);
|
)
|
||||||
|
.map_err(|e| format!("Failed to seed audio settings row: {}", e))?;
|
||||||
|
|
||||||
Ok(Self { conn })
|
Ok(Self { conn })
|
||||||
}
|
}
|
||||||
@@ -781,7 +776,8 @@ mod tests {
|
|||||||
fn audio_settings_default_values_are_stable() {
|
fn audio_settings_default_values_are_stable() {
|
||||||
let settings = AudioSettings::default();
|
let settings = AudioSettings::default();
|
||||||
|
|
||||||
assert_eq!(settings.backend_type, Some(AudioBackendType::default()));
|
// OOTB default is "System" — the OS default output (#470).
|
||||||
|
assert_eq!(settings.backend_type, Some(AudioBackendType::SystemDefault));
|
||||||
assert_eq!(settings.alsa_plugin, Some(AlsaPlugin::Hw));
|
assert_eq!(settings.alsa_plugin, Some(AlsaPlugin::Hw));
|
||||||
assert!(settings.gapless_enabled);
|
assert!(settings.gapless_enabled);
|
||||||
assert!(!settings.sync_audio_on_startup);
|
assert!(!settings.sync_audio_on_startup);
|
||||||
@@ -797,7 +793,8 @@ mod tests {
|
|||||||
|
|
||||||
let settings = store.get_settings().expect("get settings");
|
let settings = store.get_settings().expect("get settings");
|
||||||
|
|
||||||
assert_eq!(settings.backend_type, Some(AudioBackendType::default()));
|
// Fresh store is seeded with the OOTB default backend "System" (#470).
|
||||||
|
assert_eq!(settings.backend_type, Some(AudioBackendType::SystemDefault));
|
||||||
assert_eq!(settings.alsa_plugin, None);
|
assert_eq!(settings.alsa_plugin, None);
|
||||||
assert!(!settings.gapless_enabled);
|
assert!(!settings.gapless_enabled);
|
||||||
assert_eq!(settings.quality_fallback_behavior, "ask");
|
assert_eq!(settings.quality_fallback_behavior, "ask");
|
||||||
@@ -806,8 +803,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn backend_null_is_backfilled_to_default_backend() {
|
fn backend_null_stays_auto_on_reopen() {
|
||||||
let dir = unique_test_dir("backend-backfill");
|
// A NULL backend_type means "Auto" (system default output). It must be
|
||||||
|
// preserved across restarts — never backfilled to a concrete backend on
|
||||||
|
// store open (the old #375 backfill hard-coded PipeWire and froze OOTB
|
||||||
|
// playback on hosts without `pactl`, #470). Only the explicit Reset
|
||||||
|
// action rewrites settings.
|
||||||
|
let dir = unique_test_dir("backend-null-auto");
|
||||||
{
|
{
|
||||||
let store = AudioSettingsStore::new_at(&dir).expect("open store");
|
let store = AudioSettingsStore::new_at(&dir).expect("open store");
|
||||||
store
|
store
|
||||||
@@ -816,13 +818,13 @@ mod tests {
|
|||||||
"UPDATE audio_settings SET backend_type = NULL WHERE id = 1",
|
"UPDATE audio_settings SET backend_type = NULL WHERE id = 1",
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
.expect("force legacy null backend");
|
.expect("force null (Auto) backend");
|
||||||
}
|
}
|
||||||
|
|
||||||
let reopened = AudioSettingsStore::new_at(&dir).expect("reopen store");
|
let reopened = AudioSettingsStore::new_at(&dir).expect("reopen store");
|
||||||
let settings = reopened.get_settings().expect("get settings");
|
let settings = reopened.get_settings().expect("get settings");
|
||||||
|
|
||||||
assert_eq!(settings.backend_type, Some(AudioBackendType::default()));
|
assert_eq!(settings.backend_type, None);
|
||||||
let _ = std::fs::remove_dir_all(dir);
|
let _ = std::fs::remove_dir_all(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -759,16 +759,16 @@ fn try_init_stream_with_backend(
|
|||||||
channels: u16,
|
channels: u16,
|
||||||
state: &SharedState,
|
state: &SharedState,
|
||||||
) -> Option<Result<StreamType, String>> {
|
) -> Option<Result<StreamType, String>> {
|
||||||
// Check if backend system is configured.
|
// A None backend_type means "Auto" / unset. Resolve it to SystemDefault on
|
||||||
// On macOS, default to SystemDefault when not explicitly set,
|
// every platform instead of returning None — returning None made the caller
|
||||||
// so CoreAudio device probing and sample rate switching are active.
|
// fall through to the legacy CPAL path, which forced the track rate onto the
|
||||||
let backend_type = audio_settings.backend_type.or_else(|| {
|
// shared default device: that froze the seekbar with no audio AND left a
|
||||||
if cfg!(target_os = "macos") {
|
// process-wide stuck audio handle that survived Reset (#470). "Auto" is
|
||||||
Some(qbz_audio::AudioBackendType::SystemDefault)
|
// resolved to a concrete backend in the UI; this is the backend-side safety
|
||||||
} else {
|
// net for any remaining None (legacy installs, headless callers).
|
||||||
None
|
let backend_type = audio_settings
|
||||||
}
|
.backend_type
|
||||||
})?;
|
.unwrap_or(qbz_audio::AudioBackendType::SystemDefault);
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Using backend system: {:?} (device: {:?}, plugin: {:?})",
|
"Using backend system: {:?} (device: {:?}, plugin: {:?})",
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ pub fn v2_get_available_backends() -> Result<Vec<BackendInfo>, String> {
|
|||||||
AudioBackendType::PipeWire => "PipeWire",
|
AudioBackendType::PipeWire => "PipeWire",
|
||||||
AudioBackendType::Alsa => "ALSA Direct",
|
AudioBackendType::Alsa => "ALSA Direct",
|
||||||
AudioBackendType::Pulse => "PulseAudio",
|
AudioBackendType::Pulse => "PulseAudio",
|
||||||
AudioBackendType::SystemDefault => "System Audio",
|
AudioBackendType::SystemDefault => "System",
|
||||||
};
|
};
|
||||||
|
|
||||||
BackendInfo {
|
BackendInfo {
|
||||||
|
|||||||
@@ -3358,11 +3358,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleBackendChange(backendName: string) {
|
async function handleBackendChange(backendName: string) {
|
||||||
|
// "Auto" is a resolve-and-set action, not a persisted mode. Pick the best
|
||||||
|
// available backend — PipeWire if available (best quality + device routing),
|
||||||
|
// otherwise System (always works) — and continue exactly as if the user had
|
||||||
|
// selected that concrete backend. This keeps backend_type always concrete: it
|
||||||
|
// never stays null, which on Linux fell through to the legacy CPAL path that
|
||||||
|
// froze the seekbar with no audio and left a process-wide stuck audio handle
|
||||||
|
// surviving Reset (#470). The dropdown then shows what was chosen, and the
|
||||||
|
// device picker lets the user correct the DAC if the auto pick isn't theirs.
|
||||||
|
if (backendName === 'Auto') {
|
||||||
|
const pipewire = availableBackends.find(
|
||||||
|
b => b.backend_type === 'PipeWire' && b.is_available
|
||||||
|
);
|
||||||
|
const system = availableBackends.find(b => b.backend_type === 'SystemDefault');
|
||||||
|
backendName = (pipewire ?? system)?.name ?? 'System';
|
||||||
|
}
|
||||||
selectedBackend = backendName;
|
selectedBackend = backendName;
|
||||||
|
|
||||||
// Map UI name to backend type
|
// Map UI name to backend type (always concrete after the Auto resolution above)
|
||||||
const backend = availableBackends.find(b => b.name === backendName);
|
const backend = availableBackends.find(b => b.name === backendName);
|
||||||
const backendType = backendName === 'Auto' ? null : backend?.backend_type ?? null;
|
const backendType = backend?.backend_type ?? null;
|
||||||
|
|
||||||
// Auto-disable incompatible features
|
// Auto-disable incompatible features
|
||||||
// DAC Passthrough and PW force bit-perfect only work with PipeWire
|
// DAC Passthrough and PW force bit-perfect only work with PipeWire
|
||||||
|
|||||||
@@ -2145,7 +2145,7 @@
|
|||||||
"syncAudioOnStartup": "Audio-Einstellungen beim Start synchronisieren",
|
"syncAudioOnStartup": "Audio-Einstellungen beim Start synchronisieren",
|
||||||
"syncAudioOnStartupDesc": "Audiokonfiguration beim App-Start aus der Datenbank neu laden. Aktiviere dies, wenn dein Audiogerät oder deine Einstellungen nach dem Starten falsch erscheinen.",
|
"syncAudioOnStartupDesc": "Audiokonfiguration beim App-Start aus der Datenbank neu laden. Aktiviere dies, wenn dein Audiogerät oder deine Einstellungen nach dem Starten falsch erscheinen.",
|
||||||
"audioBackend": "Audio-Backend",
|
"audioBackend": "Audio-Backend",
|
||||||
"audioBackendDesc": "Wähle Audiosystem: Auto (empfohlen), PipeWire (modern), ALSA Direct (bitperfekt, exklusiv) oder PulseAudio (veraltet).",
|
"audioBackendDesc": "Wähle Audiosystem: System (Standard — geteilte Ausgabe wie jede App, kein Bitperfect), Auto (empfohlen — wählt das beste verfügbare Backend; ggf. musst du noch deinen DAC auswählen), PipeWire (modern), ALSA Direct (bitperfekt, exklusiv) oder PulseAudio (veraltet).",
|
||||||
"currentSampleRate": "Aktuelle Abtastrate",
|
"currentSampleRate": "Aktuelle Abtastrate",
|
||||||
"noActivePlayback": "Keine aktive Wiedergabe",
|
"noActivePlayback": "Keine aktive Wiedergabe",
|
||||||
"loadingDevices": "Geräte werden geladen...",
|
"loadingDevices": "Geräte werden geladen...",
|
||||||
|
|||||||
@@ -2145,7 +2145,7 @@
|
|||||||
"syncAudioOnStartup": "Sync audio settings on startup",
|
"syncAudioOnStartup": "Sync audio settings on startup",
|
||||||
"syncAudioOnStartupDesc": "Reload audio configuration from database when the app starts. Enable if your audio device or settings appear incorrect after launching.",
|
"syncAudioOnStartupDesc": "Reload audio configuration from database when the app starts. Enable if your audio device or settings appear incorrect after launching.",
|
||||||
"audioBackend": "Audio Backend",
|
"audioBackend": "Audio Backend",
|
||||||
"audioBackendDesc": "Choose audio system: Auto (recommended), PipeWire (modern), ALSA Direct (bit-perfect, exclusive), or PulseAudio (legacy).",
|
"audioBackendDesc": "Choose audio system: System (default — shared output like any app, no bit-perfect), Auto (recommended — picks the best available backend; you may still need to select your DAC), PipeWire (modern), ALSA Direct (bit-perfect, exclusive), or PulseAudio (legacy).",
|
||||||
"currentSampleRate": "Current Sample Rate",
|
"currentSampleRate": "Current Sample Rate",
|
||||||
"noActivePlayback": "No active playback",
|
"noActivePlayback": "No active playback",
|
||||||
"loadingDevices": "Loading devices...",
|
"loadingDevices": "Loading devices...",
|
||||||
|
|||||||
@@ -2145,7 +2145,7 @@
|
|||||||
"syncAudioOnStartup": "Sincronizar configuración de audio al iniciar",
|
"syncAudioOnStartup": "Sincronizar configuración de audio al iniciar",
|
||||||
"syncAudioOnStartupDesc": "Recarga la configuración de audio desde la base de datos al iniciar la app. Actívalo si tu dispositivo o configuración de audio aparecen incorrectos al abrir.",
|
"syncAudioOnStartupDesc": "Recarga la configuración de audio desde la base de datos al iniciar la app. Actívalo si tu dispositivo o configuración de audio aparecen incorrectos al abrir.",
|
||||||
"audioBackend": "Backend de Audio",
|
"audioBackend": "Backend de Audio",
|
||||||
"audioBackendDesc": "Elige el sistema de audio: Auto (recomendado), PipeWire (moderno), ALSA Direct (bit-perfect, exclusivo), o PulseAudio (legacy).",
|
"audioBackendDesc": "Elige el sistema de audio: System (predeterminado — salida compartida como cualquier app, sin bit-perfect), Auto (recomendado — elige el mejor backend disponible; quizá aún debas seleccionar tu DAC), PipeWire (moderno), ALSA Direct (bit-perfect, exclusivo), o PulseAudio (legacy).",
|
||||||
"currentSampleRate": "Tasa de Muestreo Actual",
|
"currentSampleRate": "Tasa de Muestreo Actual",
|
||||||
"noActivePlayback": "Sin reproducción activa",
|
"noActivePlayback": "Sin reproducción activa",
|
||||||
"loadingDevices": "Cargando dispositivos...",
|
"loadingDevices": "Cargando dispositivos...",
|
||||||
|
|||||||
@@ -2145,7 +2145,7 @@
|
|||||||
"syncAudioOnStartup": "Synchroniser la configuration audio au démarrage",
|
"syncAudioOnStartup": "Synchroniser la configuration audio au démarrage",
|
||||||
"syncAudioOnStartupDesc": "Recharge la configuration audio depuis la base de données au démarrage de l'application. Activez si votre appareil ou vos paramètres audio semblent incorrects après le lancement.",
|
"syncAudioOnStartupDesc": "Recharge la configuration audio depuis la base de données au démarrage de l'application. Activez si votre appareil ou vos paramètres audio semblent incorrects après le lancement.",
|
||||||
"audioBackend": "Backend audio",
|
"audioBackend": "Backend audio",
|
||||||
"audioBackendDesc": "Choisissez le système audio : Auto (recommandé), PipeWire (moderne), ALSA Direct (bit-perfect, exclusif) ou PulseAudio (ancien).",
|
"audioBackendDesc": "Choisissez le système audio : System (par défaut — sortie partagée comme toute app, pas de bit-perfect), Auto (recommandé — choisit le meilleur backend disponible ; vous devrez peut-être encore sélectionner votre DAC), PipeWire (moderne), ALSA Direct (bit-perfect, exclusif) ou PulseAudio (ancien).",
|
||||||
"currentSampleRate": "Taux d'échantillonnage actuel",
|
"currentSampleRate": "Taux d'échantillonnage actuel",
|
||||||
"noActivePlayback": "Pas de lecture en cours",
|
"noActivePlayback": "Pas de lecture en cours",
|
||||||
"loadingDevices": "Chargement des appareils...",
|
"loadingDevices": "Chargement des appareils...",
|
||||||
|
|||||||
@@ -2145,7 +2145,7 @@
|
|||||||
"syncAudioOnStartup": "Sincronizar configurações de áudio na inicialização",
|
"syncAudioOnStartup": "Sincronizar configurações de áudio na inicialização",
|
||||||
"syncAudioOnStartupDesc": "Recarregar configuração de áudio do banco de dados quando o app iniciar. Ative se o dispositivo ou configurações de áudio parecerem incorretos após o lançamento.",
|
"syncAudioOnStartupDesc": "Recarregar configuração de áudio do banco de dados quando o app iniciar. Ative se o dispositivo ou configurações de áudio parecerem incorretos após o lançamento.",
|
||||||
"audioBackend": "Backend de Áudio",
|
"audioBackend": "Backend de Áudio",
|
||||||
"audioBackendDesc": "Escolha o sistema de áudio: Auto (recomendado), PipeWire (moderno), ALSA Direct (bit-perfect, exclusivo) ou PulseAudio (legado).",
|
"audioBackendDesc": "Escolha o sistema de áudio: System (padrão — saída compartilhada como qualquer app, sem bit-perfect), Auto (recomendado — escolhe o melhor backend disponível; talvez ainda precise selecionar seu DAC), PipeWire (moderno), ALSA Direct (bit-perfect, exclusivo) ou PulseAudio (legado).",
|
||||||
"currentSampleRate": "Taxa de Amostragem Atual",
|
"currentSampleRate": "Taxa de Amostragem Atual",
|
||||||
"noActivePlayback": "Nenhuma reprodução ativa",
|
"noActivePlayback": "Nenhuma reprodução ativa",
|
||||||
"loadingDevices": "Carregando dispositivos...",
|
"loadingDevices": "Carregando dispositivos...",
|
||||||
|
|||||||
Reference in new issue
Block a user