feat(audio): surface "JACK is not bit-perfect" in Audio settings

JACK routes through the JACK graph (resamples to the graph rate), so it
can't be bit-perfect — but nothing told the user. Add a `backend-is-jack`
flag (settings.rs computes it beside alsa/pipewire at both set-sites;
state.slint property) and a WarningBanner in AudioSettings, gated on it,
pointing at ALSA-direct / PipeWire-passthrough for bit-perfect. UI-only;
backend behavior unchanged.

Blind change (no build this session) — validates on the next qbz_ui build.

cc
This commit is contained in:
vicrodh committed 2026-06-30 09:27:07 -06:00
1 parent 67f5569af4
commit 6f7ff9956c
3 files changed
+15

No files matched your search

@@ -10,6 +10,7 @@ import { SettingsState, DacWizardActions } from "../state.slint";
import { QbzToggle } from "../primitives/QbzToggle.slint"; import { QbzToggle } from "../primitives/QbzToggle.slint";
import { QbzSelect } from "../primitives/QbzSelect.slint"; import { QbzSelect } from "../primitives/QbzSelect.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint"; import { QbzIcon } from "../primitives/QbzIcon.slint";
import { WarningBanner } from "../primitives/WarningBanner.slint";
import { SettingRow } from "SettingRow.slint"; import { SettingRow } from "SettingRow.slint";
component GroupHeader inherits Text { component GroupHeader inherits Text {
@@ -95,6 +96,13 @@ export component AudioSettings inherits VerticalLayout {
} }
} }
} }
// JACK routes through the JACK graph (resamples to the graph rate), so it
// can't be bit-perfect — surface that so the user isn't misled.
if SettingsState.backend-is-jack: WarningBanner {
variant: "warning";
title: @tr("JACK is not bit-perfect");
body: @tr("The JACK backend routes audio through the JACK graph, which resamples to the graph's sample rate. For bit-perfect playback use ALSA (direct) or PipeWire with passthrough.");
}
SettingRow { SettingRow {
label: @tr("Output device"); label: @tr("Output device");
description: @tr("The DAC or sound device that receives audio."); description: @tr("The DAC or sound device that receives audio.");
+1
View File
@@ -2804,6 +2804,7 @@ export global SettingsState {
// Audio — Rust-computed conditional flags driving `if`-gated rows. // Audio — Rust-computed conditional flags driving `if`-gated rows.
in-out property <bool> backend-is-alsa: false; in-out property <bool> backend-is-alsa: false;
in-out property <bool> backend-is-pipewire: false; in-out property <bool> backend-is-pipewire: false;
in-out property <bool> backend-is-jack: false;
in-out property <bool> alsa-plugin-is-hw: false; in-out property <bool> alsa-plugin-is-hw: false;
// --- PLAYBACK ---------------------------------------------------------- // --- PLAYBACK ----------------------------------------------------------
+6
View File
@@ -147,6 +147,7 @@ pub struct SettingsSnapshot {
// Audio — conditional flags. // Audio — conditional flags.
backend_is_alsa: bool, backend_is_alsa: bool,
backend_is_pipewire: bool, backend_is_pipewire: bool,
backend_is_jack: bool,
alsa_plugin_is_hw: bool, alsa_plugin_is_hw: bool,
// Playback. // Playback.
continue_playback: bool, continue_playback: bool,
@@ -481,6 +482,7 @@ fn build_snapshot(
let backend_is_alsa = active_backend == AudioBackendType::Alsa; let backend_is_alsa = active_backend == AudioBackendType::Alsa;
let backend_is_pipewire = active_backend == AudioBackendType::PipeWire; let backend_is_pipewire = active_backend == AudioBackendType::PipeWire;
let backend_is_jack = active_backend == AudioBackendType::Jack;
let alsa_plugin_is_hw = alsa_plugin == AlsaPlugin::Hw; let alsa_plugin_is_hw = alsa_plugin == AlsaPlugin::Hw;
let (out_backend_label, out_mode_label, out_backend_active, out_mode_active) = let (out_backend_label, out_mode_label, out_backend_active, out_mode_active) =
output_labels(&audio); output_labels(&audio);
@@ -525,6 +527,7 @@ fn build_snapshot(
skip_sink_switch: audio.skip_sink_switch, skip_sink_switch: audio.skip_sink_switch,
backend_is_alsa, backend_is_alsa,
backend_is_pipewire, backend_is_pipewire,
backend_is_jack,
alsa_plugin_is_hw, alsa_plugin_is_hw,
continue_playback, continue_playback,
show_context_icon: prefs.show_context_icon, show_context_icon: prefs.show_context_icon,
@@ -596,6 +599,7 @@ pub fn apply_snapshot(window: &AppWindow, snap: SettingsSnapshot) {
// Audio — conditional flags. // Audio — conditional flags.
st.set_backend_is_alsa(snap.backend_is_alsa); st.set_backend_is_alsa(snap.backend_is_alsa);
st.set_backend_is_pipewire(snap.backend_is_pipewire); st.set_backend_is_pipewire(snap.backend_is_pipewire);
st.set_backend_is_jack(snap.backend_is_jack);
st.set_alsa_plugin_is_hw(snap.alsa_plugin_is_hw); st.set_alsa_plugin_is_hw(snap.alsa_plugin_is_hw);
// Playback. // Playback.
st.set_continue_playback(snap.continue_playback); st.set_continue_playback(snap.continue_playback);
@@ -721,6 +725,7 @@ fn push_conditional_flags(ctx: &SettingsCtx, weak: &slint::Weak<AppWindow>) {
let plugin = audio.alsa_plugin.unwrap_or(AlsaPlugin::Hw); let plugin = audio.alsa_plugin.unwrap_or(AlsaPlugin::Hw);
let is_alsa = backend == AudioBackendType::Alsa; let is_alsa = backend == AudioBackendType::Alsa;
let is_pipewire = backend == AudioBackendType::PipeWire; let is_pipewire = backend == AudioBackendType::PipeWire;
let is_jack = backend == AudioBackendType::Jack;
let plugin_is_hw = plugin == AlsaPlugin::Hw; let plugin_is_hw = plugin == AlsaPlugin::Hw;
let plugin_index = ALSA_PLUGINS let plugin_index = ALSA_PLUGINS
.iter() .iter()
@@ -730,6 +735,7 @@ fn push_conditional_flags(ctx: &SettingsCtx, weak: &slint::Weak<AppWindow>) {
let st = w.global::<SettingsState>(); let st = w.global::<SettingsState>();
st.set_backend_is_alsa(is_alsa); st.set_backend_is_alsa(is_alsa);
st.set_backend_is_pipewire(is_pipewire); st.set_backend_is_pipewire(is_pipewire);
st.set_backend_is_jack(is_jack);
st.set_alsa_plugin_is_hw(plugin_is_hw); st.set_alsa_plugin_is_hw(plugin_is_hw);
st.set_alsa_plugin_index(plugin_index); st.set_alsa_plugin_index(plugin_index);
}); });