feat(audio): add PipeWire force bit-perfect mode

Add experimental toggle to force PipeWire clock.force-quantum alongside
clock.force-rate for true bit-perfect playback. Quantum is matched to
sample rate (1024 for <=48kHz, 2048 for <=96kHz, etc). Both values
reset to 0 on stop and pause-suspend to avoid affecting other apps.

Requires DAC Passthrough to be enabled, PipeWire backend only.
This commit is contained in:
vicrodh committed 2026-02-19 18:01:42 -06:00
1 parent 98ffd3dff2
commit cbc2ff0b26
13 files changed
+181 -5

No files matched your search

+3
View File
@@ -112,6 +112,9 @@ pub struct BackendConfig {
/// Exclusive mode flag
pub exclusive_mode: bool,
/// When true, force PipeWire clock.force-quantum for bit-perfect playback
pub pw_force_bitperfect: bool,
}
/// Result type for backend operations
+45
View File
@@ -21,6 +21,17 @@ pub struct PipeWireBackend {
host: rodio::cpal::Host,
}
/// Calculate the optimal PipeWire quantum for a given sample rate.
/// Returns a power-of-2 value matching common audio interface expectations.
fn quantum_for_sample_rate(sample_rate: u32) -> u32 {
match sample_rate {
r if r <= 48000 => 1024,
r if r <= 96000 => 2048,
r if r <= 192000 => 4096,
_ => 8192,
}
}
impl PipeWireBackend {
pub fn new() -> BackendResult<Self> {
Ok(Self {
@@ -28,6 +39,18 @@ impl PipeWireBackend {
})
}
/// Reset PipeWire clock.force-rate and clock.force-quantum to 0.
/// Call this when playback stops so other apps aren't stuck at a forced rate.
pub fn reset_pipewire_clock() {
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"])
.output();
let _ = Command::new("pw-metadata")
.args(["-n", "settings", "0", "clock.force-quantum", "0"])
.output();
}
/// Parse pactl output to get device list with pretty names
fn enumerate_pipewire_sinks(&self) -> BackendResult<Vec<AudioDevice>> {
// Get default sink
@@ -194,6 +217,28 @@ impl AudioBackend for PipeWireBackend {
}
}
// Force quantum if bit-perfect mode is enabled
if config.pw_force_bitperfect {
let quantum = quantum_for_sample_rate(config.sample_rate);
log::info!("[PipeWire Backend] Forcing quantum to {} via pw-metadata (bit-perfect)", quantum);
let quantum_result = Command::new("pw-metadata")
.args(["-n", "settings", "0", "clock.force-quantum", &quantum.to_string()])
.output();
match quantum_result {
Ok(output) if output.status.success() => {
log::info!("[PipeWire Backend] Quantum forced to {}", quantum);
}
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
log::warn!("[PipeWire Backend] Failed to force quantum: {}", stderr);
}
Err(e) => {
log::warn!("[PipeWire Backend] Error executing pw-metadata for quantum: {}", e);
}
}
}
// Wait for PipeWire to apply the sample rate change
std::thread::sleep(std::time::Duration::from_millis(300));
+23 -2
View File
@@ -47,6 +47,9 @@ pub struct AudioSettings {
/// When true, tracks with the same format are cross-faded seamlessly via Rodio Sink queueing.
/// Only works with cached tracks on Rodio backend (not ALSA Direct or streaming).
pub gapless_enabled: bool,
/// When true, force PipeWire clock.force-quantum alongside clock.force-rate for bit-perfect.
/// Reset both to 0 on stop. PipeWire-only, requires dac_passthrough.
pub pw_force_bitperfect: bool,
}
impl Default for AudioSettings {
@@ -68,6 +71,7 @@ impl Default for AudioSettings {
normalization_enabled: false, // Off by default — preserves bit-perfect pipeline
normalization_target_lufs: -14.0, // Spotify/YouTube standard
gapless_enabled: false, // Off by default — user opts in
pw_force_bitperfect: false, // Off by default — experimental PipeWire feature
}
}
}
@@ -152,6 +156,10 @@ impl AudioSettingsStore {
"ALTER TABLE audio_settings ADD COLUMN device_sample_rate_limits TEXT",
[],
);
let _ = conn.execute(
"ALTER TABLE audio_settings ADD COLUMN pw_force_bitperfect INTEGER DEFAULT 0",
[],
);
Ok(Self { conn })
}
@@ -170,7 +178,7 @@ impl AudioSettingsStore {
pub fn get_settings(&self) -> Result<AudioSettings, String> {
self.conn
.query_row(
"SELECT output_device, exclusive_mode, dac_passthrough, preferred_sample_rate, backend_type, alsa_plugin, alsa_hardware_volume, stream_first_track, stream_buffer_seconds, streaming_only, limit_quality_to_device, device_max_sample_rate, normalization_enabled, normalization_target_lufs, gapless_enabled, device_sample_rate_limits FROM audio_settings WHERE id = 1",
"SELECT output_device, exclusive_mode, dac_passthrough, preferred_sample_rate, backend_type, alsa_plugin, alsa_hardware_volume, stream_first_track, stream_buffer_seconds, streaming_only, limit_quality_to_device, device_max_sample_rate, normalization_enabled, normalization_target_lufs, gapless_enabled, device_sample_rate_limits, pw_force_bitperfect FROM audio_settings WHERE id = 1",
[],
|row| {
// Parse backend_type from JSON string
@@ -206,6 +214,7 @@ impl AudioSettingsStore {
normalization_enabled: row.get::<_, Option<i64>>(12)?.unwrap_or(0) != 0,
normalization_target_lufs: row.get::<_, Option<f64>>(13)?.unwrap_or(-14.0) as f32,
gapless_enabled: row.get::<_, Option<i64>>(14)?.unwrap_or(0) != 0,
pw_force_bitperfect: row.get::<_, Option<i64>>(16)?.unwrap_or(0) != 0,
})
},
)
@@ -422,6 +431,16 @@ impl AudioSettingsStore {
Ok(())
}
pub fn set_pw_force_bitperfect(&self, enabled: bool) -> Result<(), String> {
self.conn
.execute(
"UPDATE audio_settings SET pw_force_bitperfect = ?1 WHERE id = 1",
params![enabled as i64],
)
.map_err(|e| format!("Failed to set pw_force_bitperfect: {}", e))?;
Ok(())
}
pub fn set_normalization_target_lufs(&self, target: f32) -> Result<(), String> {
self.conn
.execute(
@@ -468,7 +487,8 @@ impl AudioSettingsStore {
normalization_enabled = ?13,
normalization_target_lufs = ?14,
gapless_enabled = ?15,
device_sample_rate_limits = ?16
device_sample_rate_limits = ?16,
pw_force_bitperfect = ?17
WHERE id = 1",
params![
defaults.output_device,
@@ -487,6 +507,7 @@ impl AudioSettingsStore {
defaults.normalization_target_lufs as f64,
defaults.gapless_enabled as i64,
limits_json,
defaults.pw_force_bitperfect as i64,
],
)
.map_err(|e| format!("Failed to reset audio settings: {}", e))?;