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 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..2bff1dd56 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::SampleRateChangeNotSupported) + } + sink_as_bytes!(); } diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 84b13b6f0..53665ef7b 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::SampleRateChangeNotSupported) + } } impl JackSink { diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index f8f43e3fa..fa426892f 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: Not Supported")] + SampleRateChangeNotSupported, } 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..9ed6d5492 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::SampleRateChangeNotSupported) + } + sink_as_bytes!(); } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index f8b284f29..12a9f1d2d 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::SampleRateChangeNotSupported) + } } impl Drop for PortAudioSink<'_> { diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 0cc0850a8..7882ca9dd 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::SampleRateChangeNotSupported) + } + sink_as_bytes!(); } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index b6cd34617..dac0dfbf7 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::SampleRateChangeNotSupported) + } + 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..90c4d9a01 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::SampleRateChangeNotSupported) + } } impl SdlSink { diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index c624718b4..4ee75652e 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::SampleRateChangeNotSupported) + } + 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..ae7617ff9 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}, @@ -36,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; @@ -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,51 @@ 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 + ); + + 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; + } + } + } + self.send_event(PlayerEvent::Playing { track_id: track_id.clone(), play_request_id, @@ -1942,6 +2011,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 +2029,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 +2134,7 @@ impl PlayerInternal { duration_ms, normalisation_data, is_explicit, + sample_rate, .. } | PlayerState::Paused { @@ -2074,6 +2146,7 @@ impl PlayerInternal { duration_ms, normalisation_data, is_explicit, + sample_rate, .. } = old_state { @@ -2086,6 +2159,7 @@ impl PlayerInternal { duration_ms, stream_position_ms, is_explicit, + sample_rate, }; self.preload = PlayerPreload::None;