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:
2 files changed
+31
-19
No files matched your search
@@ -15,6 +15,13 @@ use rodio::{
|
|||||||
DeviceSinkBuilder, MixerDeviceSink,
|
DeviceSinkBuilder, MixerDeviceSink,
|
||||||
};
|
};
|
||||||
use std::process::Command;
|
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 {
|
pub struct PipeWireBackend {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -32,6 +39,15 @@ impl PipeWireBackend {
|
|||||||
/// Call this when playback stops so other apps aren't stuck at a forced rate.
|
/// 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.
|
/// Quantum reset is kept for safety even though we no longer force it.
|
||||||
pub fn reset_pipewire_clock() {
|
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");
|
log::info!("[PipeWire Backend] Resetting clock.force-rate and clock.force-quantum to 0");
|
||||||
let _ = Command::new("pw-metadata")
|
let _ = Command::new("pw-metadata")
|
||||||
.args(["-n", "settings", "0", "clock.force-rate", "0"])
|
.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);
|
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.
|
// Note: clock.force-quantum is intentionally NOT set.
|
||||||
|
|||||||
@@ -2591,17 +2591,13 @@ impl Player {
|
|||||||
*pause_suspend_deadline = Some(
|
*pause_suspend_deadline = Some(
|
||||||
Instant::now() + Duration::from_millis(PAUSE_SUSPEND_DELAY_MS),
|
Instant::now() + Duration::from_millis(PAUSE_SUSPEND_DELAY_MS),
|
||||||
);
|
);
|
||||||
// Reset PipeWire clock if bit-perfect was active
|
// Reset the PipeWire clock if WE forced it. The call is
|
||||||
|
// self-gating (reset_pipewire_clock no-ops unless QBZ set
|
||||||
|
// the force), so it is now unconditional: the previous
|
||||||
|
// pw_force_bitperfect gate missed plain (no-passthrough)
|
||||||
|
// PipeWire users and leaked a forced rate after stop (#263).
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
if thread_settings
|
qbz_audio::pipewire_backend::PipeWireBackend::reset_pipewire_clock();
|
||||||
.lock()
|
|
||||||
.ok()
|
|
||||||
.map(|s| s.pw_force_bitperfect)
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
qbz_audio::pipewire_backend::PipeWireBackend::reset_pipewire_clock(
|
|
||||||
);
|
|
||||||
}
|
|
||||||
log::info!("Audio thread: stopped");
|
log::info!("Audio thread: stopped");
|
||||||
}
|
}
|
||||||
AudioCommand::SetVolume(volume) => {
|
AudioCommand::SetVolume(volume) => {
|
||||||
@@ -3228,16 +3224,12 @@ impl Player {
|
|||||||
}
|
}
|
||||||
drop(stream_opt.take());
|
drop(stream_opt.take());
|
||||||
pause_suspend_deadline = None;
|
pause_suspend_deadline = None;
|
||||||
// Reset PipeWire clock if bit-perfect was active
|
// Reset the PipeWire clock if WE forced it (self-gating;
|
||||||
|
// see reset_pipewire_clock). Unconditional now — the
|
||||||
|
// previous pw_force_bitperfect gate leaked a forced rate
|
||||||
|
// for plain (no-passthrough) PipeWire users (#263).
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
if thread_settings
|
qbz_audio::pipewire_backend::PipeWireBackend::reset_pipewire_clock();
|
||||||
.lock()
|
|
||||||
.ok()
|
|
||||||
.map(|s| s.pw_force_bitperfect)
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
qbz_audio::pipewire_backend::PipeWireBackend::reset_pipewire_clock();
|
|
||||||
}
|
|
||||||
log::info!("Audio thread: suspended stream after pause");
|
log::info!("Audio thread: suspended stream after pause");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user