feat(audio): release/refresh output-device button (#263)

After bit-perfect ALSA Direct grabs a DAC exclusively (hw: + D-Bus
reservation), the device stays sequestered and is invisible to PipeWire,
so switching back to the PipeWire backend can't see it without restarting
the app. Tauri had a refresh affordance for hot-plugged DACs; this is the
refresh plus an explicit release, in one button.

Backend: Player::release_device() / AudioCommand::ReleaseDevice tears the
stream down immediately (cancelling the deferred drop), freeing the hw:
grab + its reservation, then un-suspends the parked sink and resets any
forced graph clock (both self-gating). It does NOT reopen, unlike
reinit_device which would re-grab the device.

UI: a refresh-cw icon button beside the Output device select, propagated
through the settings callback chain to handle_release_device, which
releases, waits for PipeWire to reclaim, then re-enumerates so a freed or
hot-plugged DAC reappears without an app restart.

cc
This commit is contained in:
vicrodh committed 2026-05-31 17:41:01 -06:00
1 parent bdb9cadad6
commit d1ebf8ea1c
7 files changed
+150 -15

No files matched your search

+30
View File
@@ -937,6 +937,36 @@ pub async fn handle_reset(
apply_audio(&ctx, &runtime, Apply::Reinit);
}
/// Release the held output device, then re-enumerate. Frees a device QBZ is
/// holding exclusively (ALSA Direct, which leaves the DAC invisible to
/// PipeWire/other apps) and rebuilds the snapshot so a freed or hot-plugged
/// DAC shows up in the list without restarting the app — the Tauri
/// "refresh" affordance plus an explicit release, in one action.
pub async fn handle_release_device(
ctx: Arc<SettingsCtx>,
runtime: Arc<AppRuntime<SlintAdapter>>,
weak: slint::Weak<AppWindow>,
) {
if let Err(e) = runtime.core().player().release_device() {
log::error!("[qbz-slint] player.release_device failed: {e}");
}
// Let PipeWire/WirePlumber reclaim the just-freed device before we list.
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
let snap = {
let ctx = ctx.clone();
match tokio::task::spawn_blocking(move || load_snapshot(&ctx)).await {
Ok(s) => s,
Err(e) => {
log::error!("[qbz-slint] release-device rebuild task failed: {e}");
return;
}
}
};
let _ = weak.upgrade_in_event_loop(move |w| {
apply_snapshot(&w, snap);
});
}
#[cfg(test)]
mod tests {
use super::*;