fix(audio): reset clock.force-rate on stop for all forced sessions (#263 leak)

QBZ forces the global PipeWire clock for any non-locked PipeWire stream, but the
stop/pause-suspend reset was gated on pw_force_bitperfect — which is only set in
DAC-passthrough mode. So a plain PipeWire user (no passthrough) forced the graph
clock on play and never reset it, leaving the whole graph force-clocked after
stopping (a stale clock.force-rate that resamples every other app).

Track whether QBZ actually wrote the force (CLOCK_FORCE_APPLIED) and make
reset_pipewire_clock self-gating: it only resets a force WE applied, never
clobbering another app's forced rate. The player's two reset call sites (Stop,
pause-suspend) now call it unconditionally.

Verified on hardware (Cambridge USB DAC, PipeWire + passthrough off): a 192kHz
track forces clock.force-rate=192000; on pause-suspend the clock returns to 0
(previously stayed stuck at 192000).

cc
This commit is contained in:
vicrodh committed 2026-05-31 09:57:56 -06:00
1 parent d19312fe17
commit b139392b9b
2 files changed
+31 -19

No files matched your search

+20
View File
@@ -15,6 +15,13 @@ use rodio::{
DeviceSinkBuilder, MixerDeviceSink,
};
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
/// Set true whenever QBZ writes a global `clock.force-rate` to the PipeWire
/// graph, so `reset_pipewire_clock` only resets a force WE applied and never
/// clobbers another app's intentional forced rate (issue #263, leak fix).
/// The force and the reset both run on the audio thread, so `Relaxed` is enough.
static CLOCK_FORCE_APPLIED: AtomicBool = AtomicBool::new(false);
pub struct PipeWireBackend {
#[allow(dead_code)]
@@ -32,6 +39,15 @@ impl PipeWireBackend {
/// Call this when playback stops so other apps aren't stuck at a forced rate.
/// Quantum reset is kept for safety even though we no longer force it.
pub fn reset_pipewire_clock() {
// Only reset a force WE applied — otherwise stopping QBZ would clobber
// another app's intentional clock.force-rate. This also makes the call
// safe to invoke unconditionally on every stop/suspend (issue #263 leak
// fix): previously the reset was gated on `pw_force_bitperfect`, but QBZ
// forces the clock for ANY non-locked PipeWire stream, so a plain
// PipeWire (no-passthrough) user left the graph force-clocked after stop.
if !CLOCK_FORCE_APPLIED.swap(false, Ordering::Relaxed) {
return;
}
log::info!("[PipeWire Backend] Resetting clock.force-rate and clock.force-quantum to 0");
let _ = Command::new("pw-metadata")
.args(["-n", "settings", "0", "clock.force-rate", "0"])
@@ -434,6 +450,10 @@ impl AudioBackend for PipeWireBackend {
log::warn!("[PipeWire Backend] Error executing pw-metadata: {}", e);
}
}
// Remember WE forced the clock so the stop/suspend reset undoes it
// (and only it). This runs for every non-locked PipeWire stream, so
// a plain no-passthrough user no longer leaks a forced rate on stop.
CLOCK_FORCE_APPLIED.store(true, Ordering::Relaxed);
}
// Note: clock.force-quantum is intentionally NOT set.