From 6e62ee4057b8ea3144f2f4df85e5d56c37c8fd72 Mon Sep 17 00:00:00 2001 From: salmonik Date: Mon, 2 Nov 2026 13:28:23 +0100 Subject: audio: open the output at the track's rate instead of pw-alsa's 44100 cpal asks the ALSA "default" PCM for its default config and pipewire-alsa answers 44.1 kHz no matter what the graph runs at, so 48 kHz material was resampled down to 44.1 in our linear resampler and then back up to 48 by PipeWire. The stream now opens at the track's own rate (when the device supports it), which keeps our resampler out of the path and leaves the conversion to PipeWire's polyphase one. The new `sample_rate` setting forces a fixed rate instead ("auto" by default), for anyone who would rather match the graph exactly. --- src/config.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) (limited to 'src/config.rs') diff --git a/src/config.rs b/src/config.rs index a08f01f..52afd92 100644 --- a/src/config.rs +++ b/src/config.rs @@ -25,6 +25,8 @@ pub struct Config { pub volume: Option, pub shuffle: Option, pub display_format: Option, + /// Output sample rate: `None` follows the track, `Some` forces a rate. + pub sample_rate: Option, } /// Identity of the config file on disk, used to tell whether a cached copy is @@ -64,6 +66,7 @@ impl Config { volume: Some(50), shuffle: None, display_format: Some(String::from("$ARTIST - $TRACK")), + sample_rate: None, }; default.save()?; return Ok(default); @@ -192,6 +195,8 @@ impl Config { output.push_str(&format!("shuffle = \"{}\"\n", shuffle)); let fmt = self.display_format.clone().unwrap_or_default(); output.push_str(&format!("display_format = \"{}\"\n", fmt)); + let sample_rate = self.sample_rate.as_deref().unwrap_or("auto"); + output.push_str(&format!("sample_rate = \"{}\"\n", sample_rate)); if let Some(parent) = self.config_path.parent() { fs::create_dir_all(parent)?; } @@ -207,6 +212,11 @@ impl Config { Ok(()) } + /// Forced output rate, or `None` to follow each track. + pub fn sample_rate_hz(&self) -> Option { + self.sample_rate.as_deref().and_then(|s| s.parse().ok()) + } + pub fn get(&self, key: &str) -> Result> { match key { "music_paths" => Ok(self @@ -252,6 +262,10 @@ impl Config { .display_format .clone() .unwrap_or_else(|| "not set".to_string())), + "sample_rate" => Ok(self + .sample_rate + .clone() + .unwrap_or_else(|| "auto".to_string())), _ => Err(format!("Unknown setting: {}", key).into()), } } @@ -291,6 +305,20 @@ impl Config { "display_format" => { self.display_format = Some(value.to_string()); } + "sample_rate" => { + let value = value.trim(); + if value.eq_ignore_ascii_case("auto") || value == "off" { + self.sample_rate = None; + } else { + let rate: u32 = value + .parse() + .map_err(|_| format!("Invalid sample rate: {}", value))?; + if !(8_000..=384_000).contains(&rate) { + return Err(format!("Sample rate must be 8000-384000 Hz: {}", value).into()); + } + self.sample_rate = Some(rate.to_string()); + } + } _ => return Err(format!("Unknown setting: {}", key).into()), } Ok(()) @@ -357,6 +385,12 @@ impl Config { .clone() .unwrap_or_else(|| "not set".to_string()) ); + println!( + "sample_rate = {}", + self.sample_rate + .clone() + .unwrap_or_else(|| "auto".to_string()) + ); } pub fn unset(&mut self, key: &str) -> Result<(), Box> { @@ -370,6 +404,7 @@ impl Config { "volume" => self.volume = None, "shuffle" => self.shuffle = None, "display_format" => self.display_format = None, + "sample_rate" => self.sample_rate = None, _ => return Err(format!("Unknown setting: {}", key).into()), } Ok(()) -- cgit v1.2.3