From 8782b79be13066aeadf7ed47180461e287d23393 Mon Sep 17 00:00:00 2001 From: Jay Malhotra Date: Thu, 18 Dec 2025 23:44:34 +0000 Subject: [PATCH 1/3] chore: Add ability to change player sample rate Adds the ability for the player to change its sample rate, to be able to accurately play back local files that aren't sampled at 44.1 kHz. The functionality to change sample rate must be implemented individually by each sink. In this commit, support for this has only been added to the alsa sink. Testing this by swapping between a 48.0 kHz local file and a 44.1 kHz Spotify song appears to play back both without any distortion of sound. --- playback/src/audio_backend/alsa.rs | 52 ++++++++++++++--------- playback/src/audio_backend/gstreamer.rs | 4 ++ playback/src/audio_backend/jackaudio.rs | 4 ++ playback/src/audio_backend/mod.rs | 5 +++ playback/src/audio_backend/pipe.rs | 4 ++ playback/src/audio_backend/portaudio.rs | 4 ++ playback/src/audio_backend/pulseaudio.rs | 4 ++ playback/src/audio_backend/rodio.rs | 4 ++ playback/src/audio_backend/sdl.rs | 4 ++ playback/src/audio_backend/subprocess.rs | 4 ++ playback/src/decoder/symphonia_decoder.rs | 25 +++++------ playback/src/player.rs | 45 ++++++++++++++++++++ 12 files changed, 125 insertions(+), 34 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index bd2b4bf5c..b29a0914d 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -9,8 +9,6 @@ use alsa::{Direction, ValueOr}; use std::process::exit; use thiserror::Error; -const MAX_BUFFER: Frames = (SAMPLE_RATE / 2) as Frames; -const MIN_BUFFER: Frames = (SAMPLE_RATE / 10) as Frames; const ZERO_FRAMES: Frames = 0; const MAX_PERIOD_DIVISOR: Frames = 4; @@ -162,7 +160,7 @@ fn list_compatible_devices() -> SinkResult<()> { Ok(()) } -fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> { +fn open_device(dev_name: &str, format: AudioFormat, sample_rate: u32) -> SinkResult<(PCM, usize)> { let pcm = PCM::new(dev_name, Direction::Playback, false).map_err(|e| AlsaError::PcmSetUp { device: dev_name.to_string(), e, @@ -187,10 +185,10 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> e, })?; - hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).map_err(|e| { + hwp.set_rate(sample_rate, ValueOr::Nearest).map_err(|e| { AlsaError::UnsupportedSampleRate { device: dev_name.to_string(), - samplerate: SAMPLE_RATE, + samplerate: sample_rate, e, } })?; @@ -240,8 +238,11 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> Ok(s) => s, }; + let max_buffer: Frames = (sample_rate / 2) as Frames; + let min_buffer: Frames = (sample_rate / 10) as Frames; + let buffer_size = if min < max { - match (MIN_BUFFER..=MAX_BUFFER) + match (min_buffer..=max_buffer) .rev() .find(|f| (min..=max).contains(f)) { @@ -269,7 +270,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> }; if buffer_size == ZERO_FRAMES { - trace!("Desired Buffer Frame range: {MIN_BUFFER:?} - {MAX_BUFFER:?}",); + trace!("Desired Buffer Frame range: {min_buffer:?} - {max_buffer:?}",); trace!("Actual Buffer Frame range as reported by the device: {min:?} - {max:?}",); } @@ -416,18 +417,7 @@ impl Open for AlsaSink { impl Sink for AlsaSink { fn start(&mut self) -> SinkResult<()> { if self.pcm.is_none() { - let (pcm, bytes_per_period) = open_device(&self.device, self.format)?; - self.pcm = Some(pcm); - - if self.period_buffer.capacity() != bytes_per_period { - self.period_buffer = Vec::with_capacity(bytes_per_period); - } - - // Should always match the "Period Buffer size in bytes: " trace! message. - trace!( - "Period Buffer capacity: {:?}", - self.period_buffer.capacity() - ); + self.start_internal(SAMPLE_RATE)?; } Ok(()) @@ -448,6 +438,13 @@ impl Sink for AlsaSink { Ok(()) } + fn update_sample_rate(&mut self, new_sample_rate: u32) -> SinkResult<()> { + self.stop()?; + self.start_internal(new_sample_rate)?; + + Ok(()) + } + sink_as_bytes!(); } @@ -483,6 +480,23 @@ impl SinkAsBytes for AlsaSink { impl AlsaSink { pub const NAME: &'static str = "alsa"; + fn start_internal(&mut self, sample_rate: u32) -> SinkResult<()> { + let (pcm, bytes_per_period) = open_device(&self.device, self.format, sample_rate)?; + self.pcm = Some(pcm); + + if self.period_buffer.capacity() != bytes_per_period { + self.period_buffer = Vec::with_capacity(bytes_per_period); + } + + // Should always match the "Period Buffer size in bytes: " trace! message. + trace!( + "Period Buffer capacity: {:?}", + self.period_buffer.capacity() + ); + + Ok(()) + } + fn write_buf(&mut self) -> SinkResult<()> { if self.pcm.is_some() { let write_result = { diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index f41d43339..190afe13d 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -172,6 +172,10 @@ impl Sink for GstreamerSink { Ok(()) } + fn update_sample_rate(&mut self, _new_sample_rate: u32) -> SinkResult<()> { + Err(SinkError::SampleRateChange("not implemented".into())) + } + sink_as_bytes!(); } diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 84b13b6f0..886ca114c 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -80,6 +80,10 @@ impl Sink for JackSink { } Ok(()) } + + fn update_sample_rate(&mut self, _new_sample_rate: u32) -> SinkResult<()> { + Err(SinkError::SampleRateChange("not implemented".into())) + } } impl JackSink { diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index f8f43e3fa..705fc6e81 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -15,6 +15,8 @@ pub enum SinkError { InvalidParams(String), #[error("Audio Sink Error Changing State: {0}")] StateChange(String), + #[error("Audio Sink Error Updating Sample Rate: {0}")] + SampleRateChange(String), } pub type SinkResult = Result; @@ -30,6 +32,9 @@ pub trait Sink { fn stop(&mut self) -> SinkResult<()> { Ok(()) } + + fn update_sample_rate(&mut self, new_sample_rate: u32) -> SinkResult<()>; + fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()>; } diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 8dfd21ea4..c07eea597 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -92,6 +92,10 @@ impl Sink for StdoutSink { Ok(()) } + fn update_sample_rate(&mut self, _new_sample_rate: u32) -> SinkResult<()> { + Err(SinkError::SampleRateChange("not implemented".into())) + } + sink_as_bytes!(); } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index f8b284f29..4be0b7d1b 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -173,6 +173,10 @@ impl Sink for PortAudioSink<'_> { Ok(()) } + + fn update_sample_rate(&mut self, _new_sample_rate: u32) -> SinkResult<()> { + Err(SinkError::SampleRateChange("not implemented".into())) + } } impl Drop for PortAudioSink<'_> { diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 0cc0850a8..ddef9a1cb 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -133,6 +133,10 @@ impl Sink for PulseAudioSink { Ok(()) } + fn update_sample_rate(&mut self, _new_sample_rate: u32) -> SinkResult<()> { + Err(SinkError::SampleRateChange("not implemented".into())) + } + sink_as_bytes!(); } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index b6cd34617..d8647e2a3 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -240,6 +240,10 @@ impl Sink for RodioSink { Ok(()) } + fn update_sample_rate(&mut self, _new_sample_rate: u32) -> SinkResult<()> { + Err(SinkError::SampleRateChange("not implemented".into())) + } + fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> { let samples = packet .samples() diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 0d2209282..56399d7ca 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -114,6 +114,10 @@ impl Sink for SdlSink { }; result.map_err(SinkError::OnWrite) } + + fn update_sample_rate(&mut self, _new_sample_rate: u32) -> SinkResult<()> { + Err(SinkError::SampleRateChange("not implemented".into())) + } } impl SdlSink { diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index c624718b4..6ce3f02bd 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -135,6 +135,10 @@ impl Sink for SubprocessSink { } } + fn update_sample_rate(&mut self, _new_sample_rate: u32) -> SinkResult<()> { + Err(SinkError::SampleRateChange("not implemented".into())) + } + sink_as_bytes!(); } diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index 56561bb66..71c786903 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -12,7 +12,7 @@ use symphonia::core::{ use super::{AudioDecoder, AudioPacket, AudioPacketPosition, DecoderError, DecoderResult}; -use crate::{NUM_CHANNELS, PAGES_PER_MS, SAMPLE_RATE, player::NormalisationData, symphonia_util}; +use crate::{NUM_CHANNELS, PAGES_PER_MS, player::NormalisationData, symphonia_util}; pub struct SymphoniaDecoder { probe_result: ProbeResult, @@ -60,20 +60,6 @@ impl SymphoniaDecoder { let decoder = symphonia::default::get_codecs().make(&track.codec_params, &decoder_opts)?; - let rate = decoder.codec_params().sample_rate.ok_or_else(|| { - DecoderError::SymphoniaDecoder("Could not retrieve sample rate".into()) - })?; - - // TODO: The official client supports local files with sample rates other than 44,100 kHz. - // To play these accurately, we need to either resample the input audio, or introduce a way - // to change the player's current sample rate (likely by closing and re-opening the sink - // with new parameters). - if rate != SAMPLE_RATE { - return Err(DecoderError::SymphoniaDecoder(format!( - "Unsupported sample rate: {rate}" - ))); - } - let channels = decoder.codec_params().channels.ok_or_else(|| { DecoderError::SymphoniaDecoder("Could not retrieve channel configuration".into()) })?; @@ -176,6 +162,15 @@ impl SymphoniaDecoder { Some(metadata) } + pub fn sample_rate(&self) -> DecoderResult { + self.decoder + .codec_params() + .sample_rate + .ok_or(DecoderError::SymphoniaDecoder( + "Could not retrieve sample rate".into(), + )) + } + #[inline] fn ts_to_ms(&self, ts: u64) -> u32 { match self.decoder.codec_params().time_base { diff --git a/playback/src/player.rs b/playback/src/player.rs index f3d803d51..a27f82f1b 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -20,6 +20,7 @@ use std::{ #[cfg(feature = "passthrough-decoder")] use crate::decoder::PassthroughDecoder; use crate::{ + SAMPLE_RATE, audio::{AudioDecrypt, AudioFetchParams, AudioFile, StreamLoaderController}, audio_backend::Sink, config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}, @@ -95,6 +96,8 @@ struct PlayerInternal { last_progress_update: Instant, local_file_lookup: Arc, + + sink_sample_rate: u32, } static PLAYER_COUNTER: AtomicUsize = AtomicUsize::new(0); @@ -530,6 +533,8 @@ impl Player { last_progress_update: Instant::now(), local_file_lookup: Arc::new(local_file_lookup), + + sink_sample_rate: SAMPLE_RATE, }; // While PlayerInternal is written as a future, it still contains blocking code. @@ -705,6 +710,7 @@ struct PlayerLoadedTrackData { duration_ms: u32, stream_position_ms: u32, is_explicit: bool, + sample_rate: u32, } enum PlayerPreload { @@ -742,6 +748,7 @@ enum PlayerState { stream_position_ms: u32, suggested_to_preload_next_track: bool, is_explicit: bool, + sample_rate: u32, }, Playing { track_id: SpotifyUri, @@ -757,6 +764,7 @@ enum PlayerState { reported_nominal_start_time: Option, suggested_to_preload_next_track: bool, is_explicit: bool, + sample_rate: u32, }, EndOfTrack { track_id: SpotifyUri, @@ -823,6 +831,7 @@ impl PlayerState { stream_position_ms, is_explicit, audio_item, + sample_rate, .. } => { *self = EndOfTrack { @@ -837,6 +846,7 @@ impl PlayerState { duration_ms, stream_position_ms, is_explicit, + sample_rate, }, }; } @@ -864,6 +874,7 @@ impl PlayerState { stream_position_ms, suggested_to_preload_next_track, is_explicit, + sample_rate, } => { *self = Playing { track_id, @@ -880,6 +891,7 @@ impl PlayerState { .checked_sub(Duration::from_millis(stream_position_ms as u64)), suggested_to_preload_next_track, is_explicit, + sample_rate, }; } _ => { @@ -906,6 +918,7 @@ impl PlayerState { stream_position_ms, suggested_to_preload_next_track, is_explicit, + sample_rate, .. } => { *self = Paused { @@ -921,6 +934,7 @@ impl PlayerState { stream_position_ms, suggested_to_preload_next_track, is_explicit, + sample_rate, }; } _ => { @@ -1241,6 +1255,7 @@ impl PlayerTrackLoader { duration_ms, stream_position_ms, is_explicit, + sample_rate: SAMPLE_RATE, }); } } @@ -1303,6 +1318,14 @@ impl PlayerTrackLoader { } }; + let sample_rate = match decoder.sample_rate() { + Ok(sample_rate) => sample_rate, + Err(e) => { + error!("Unable to determine track sample rate: {}", e); + return None; + } + }; + let file_size = fs::metadata(path).ok()?.len(); let bytes_per_second = (file_size / duration.as_secs()) as usize; @@ -1320,6 +1343,7 @@ impl PlayerTrackLoader { duration_ms: duration.as_millis() as u32, stream_position_ms, is_explicit: false, + sample_rate, audio_item: AudioItem { duration_ms: duration.as_millis() as u32, uri: track_uri.to_uri(), @@ -1921,6 +1945,22 @@ impl PlayerInternal { if start_playback { self.ensure_sink_running(); + + if self.sink_sample_rate != loaded_track.sample_rate { + debug!( + "Updating sink sample rate from {} Hz to {} Hz", + self.sink_sample_rate, loaded_track.sample_rate + ); + + if let Err(e) = self.sink.update_sample_rate(loaded_track.sample_rate) { + error!( + "Failed to update sink sample rate: {e}. Track playback may be distorted." + ); + } else { + self.sink_sample_rate = loaded_track.sample_rate; + } + } + self.send_event(PlayerEvent::Playing { track_id: track_id.clone(), play_request_id, @@ -1942,6 +1982,7 @@ impl PlayerInternal { .checked_sub(Duration::from_millis(position_ms as u64)), suggested_to_preload_next_track: false, is_explicit: loaded_track.is_explicit, + sample_rate: loaded_track.sample_rate, }; } else { self.ensure_sink_stopped(false); @@ -1959,6 +2000,7 @@ impl PlayerInternal { stream_position_ms: loaded_track.stream_position_ms, suggested_to_preload_next_track: false, is_explicit: loaded_track.is_explicit, + sample_rate: loaded_track.sample_rate, }; self.send_event(PlayerEvent::Paused { @@ -2063,6 +2105,7 @@ impl PlayerInternal { duration_ms, normalisation_data, is_explicit, + sample_rate, .. } | PlayerState::Paused { @@ -2074,6 +2117,7 @@ impl PlayerInternal { duration_ms, normalisation_data, is_explicit, + sample_rate, .. } = old_state { @@ -2086,6 +2130,7 @@ impl PlayerInternal { duration_ms, stream_position_ms, is_explicit, + sample_rate, }; self.preload = PlayerPreload::None; From 892522e7f21cec5c884c1bf8417f0bf99e450261 Mon Sep 17 00:00:00 2001 From: Jay Malhotra Date: Sat, 24 Jan 2026 23:48:08 +0000 Subject: [PATCH 2/3] feat: Skip track on failed sample rate changes Adjust the error handling for failed changes of sample rate, such that we skip the track. In addition to skipping the track, if the error is due to sample rate changes not being supported by the current track, then mark the track as unavailable as it will not become playable (at least for the lifetime of the current program run). --- playback/src/audio_backend/gstreamer.rs | 2 +- playback/src/audio_backend/jackaudio.rs | 2 +- playback/src/audio_backend/mod.rs | 4 +-- playback/src/audio_backend/pipe.rs | 2 +- playback/src/audio_backend/portaudio.rs | 2 +- playback/src/audio_backend/pulseaudio.rs | 2 +- playback/src/audio_backend/rodio.rs | 2 +- playback/src/audio_backend/sdl.rs | 2 +- playback/src/audio_backend/subprocess.rs | 2 +- playback/src/player.rs | 45 +++++++++++++++++++----- 10 files changed, 47 insertions(+), 18 deletions(-) diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 190afe13d..2bff1dd56 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -173,7 +173,7 @@ impl Sink for GstreamerSink { } fn update_sample_rate(&mut self, _new_sample_rate: u32) -> SinkResult<()> { - Err(SinkError::SampleRateChange("not implemented".into())) + Err(SinkError::SampleRateChangeNotSupported) } sink_as_bytes!(); diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 886ca114c..53665ef7b 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -82,7 +82,7 @@ impl Sink for JackSink { } fn update_sample_rate(&mut self, _new_sample_rate: u32) -> SinkResult<()> { - Err(SinkError::SampleRateChange("not implemented".into())) + Err(SinkError::SampleRateChangeNotSupported) } } diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 705fc6e81..fa426892f 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -15,8 +15,8 @@ pub enum SinkError { InvalidParams(String), #[error("Audio Sink Error Changing State: {0}")] StateChange(String), - #[error("Audio Sink Error Updating Sample Rate: {0}")] - SampleRateChange(String), + #[error("Audio Sink Error Updating Sample Rate: Not Supported")] + SampleRateChangeNotSupported, } pub type SinkResult = Result; diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index c07eea597..9ed6d5492 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -93,7 +93,7 @@ impl Sink for StdoutSink { } fn update_sample_rate(&mut self, _new_sample_rate: u32) -> SinkResult<()> { - Err(SinkError::SampleRateChange("not implemented".into())) + Err(SinkError::SampleRateChangeNotSupported) } sink_as_bytes!(); diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 4be0b7d1b..12a9f1d2d 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -175,7 +175,7 @@ impl Sink for PortAudioSink<'_> { } fn update_sample_rate(&mut self, _new_sample_rate: u32) -> SinkResult<()> { - Err(SinkError::SampleRateChange("not implemented".into())) + Err(SinkError::SampleRateChangeNotSupported) } } diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index ddef9a1cb..7882ca9dd 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -134,7 +134,7 @@ impl Sink for PulseAudioSink { } fn update_sample_rate(&mut self, _new_sample_rate: u32) -> SinkResult<()> { - Err(SinkError::SampleRateChange("not implemented".into())) + Err(SinkError::SampleRateChangeNotSupported) } sink_as_bytes!(); diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index d8647e2a3..dac0dfbf7 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -241,7 +241,7 @@ impl Sink for RodioSink { } fn update_sample_rate(&mut self, _new_sample_rate: u32) -> SinkResult<()> { - Err(SinkError::SampleRateChange("not implemented".into())) + Err(SinkError::SampleRateChangeNotSupported) } fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> { diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 56399d7ca..90c4d9a01 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -116,7 +116,7 @@ impl Sink for SdlSink { } fn update_sample_rate(&mut self, _new_sample_rate: u32) -> SinkResult<()> { - Err(SinkError::SampleRateChange("not implemented".into())) + Err(SinkError::SampleRateChangeNotSupported) } } diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 6ce3f02bd..4ee75652e 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -136,7 +136,7 @@ impl Sink for SubprocessSink { } fn update_sample_rate(&mut self, _new_sample_rate: u32) -> SinkResult<()> { - Err(SinkError::SampleRateChange("not implemented".into())) + Err(SinkError::SampleRateChangeNotSupported) } sink_as_bytes!(); diff --git a/playback/src/player.rs b/playback/src/player.rs index a27f82f1b..ae7617ff9 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -37,12 +37,12 @@ use futures_util::{ }; use librespot_metadata::{audio::UniqueFields, track::Tracks}; +use crate::SAMPLES_PER_SECOND; +use crate::audio_backend::SinkError; use symphonia::core::io::MediaSource; use symphonia::core::probe::Hint; use tokio::sync::{mpsc, oneshot}; -use crate::SAMPLES_PER_SECOND; - const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; pub const DB_VOLTAGE_RATIO: f64 = 20.0; pub const PCM_AT_0DBFS: f64 = 1.0; @@ -1952,12 +1952,41 @@ impl PlayerInternal { self.sink_sample_rate, loaded_track.sample_rate ); - if let Err(e) = self.sink.update_sample_rate(loaded_track.sample_rate) { - error!( - "Failed to update sink sample rate: {e}. Track playback may be distorted." - ); - } else { - self.sink_sample_rate = loaded_track.sample_rate; + match self.sink.update_sample_rate(loaded_track.sample_rate) { + Ok(()) => { + self.sink_sample_rate = loaded_track.sample_rate; + } + Err(e) => { + error!("{e}"); + + let reaction_event = match e { + SinkError::SampleRateChangeNotSupported => { + // The sink does not support changing its sample rate; this track + // will never be playable. + PlayerEvent::Unavailable { + track_id: track_id.clone(), + play_request_id, + } + } + _ => { + // Could be a transient error; try the next track + PlayerEvent::EndOfTrack { + track_id: track_id.clone(), + play_request_id, + } + } + }; + + self.send_event(reaction_event); + + self.state = PlayerState::EndOfTrack { + track_id, + play_request_id, + loaded_track, + }; + + return; + } } } From 1480a2214bf35ac0c85cc58cd949940a0e2c7a59 Mon Sep 17 00:00:00 2001 From: Jay Malhotra Date: Sat, 24 Jan 2026 23:53:07 +0000 Subject: [PATCH 3/3] docs: Add changelog entry # Conflicts: # CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0c418b6c..169b5eec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [connect] Add method `add_to_queue` to `Spirc` to add tracks, episodes, albums and playlists to the queue - [playback] Add `SetQueue` player event, emitting when the queue changes (context loaded, track added to queue, or queue set via Spotify Connect). Gated behind `ConnectConfig::emit_set_queue_events` +- [playback] Add support in the `alsa` backend for playing local files at sample rates other than 44,100 Hz ### Changed