Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
52 changes: 33 additions & 19 deletions playback/src/audio_backend/alsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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,
}
})?;
Expand Down Expand Up @@ -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))
{
Expand Down Expand Up @@ -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:?}",);
}
Expand Down Expand Up @@ -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(())
Expand All @@ -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!();
}

Expand Down Expand Up @@ -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 = {
Expand Down
4 changes: 4 additions & 0 deletions playback/src/audio_backend/gstreamer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!();
}

Expand Down
4 changes: 4 additions & 0 deletions playback/src/audio_backend/jackaudio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions playback/src/audio_backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = Result<T, SinkError>;
Expand All @@ -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<()>;
}

Expand Down
4 changes: 4 additions & 0 deletions playback/src/audio_backend/pipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!();
}

Expand Down
4 changes: 4 additions & 0 deletions playback/src/audio_backend/portaudio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<'_> {
Expand Down
4 changes: 4 additions & 0 deletions playback/src/audio_backend/pulseaudio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!();
}

Expand Down
4 changes: 4 additions & 0 deletions playback/src/audio_backend/rodio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions playback/src/audio_backend/sdl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions playback/src/audio_backend/subprocess.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!();
}

Expand Down
25 changes: 10 additions & 15 deletions playback/src/decoder/symphonia_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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())
})?;
Expand Down Expand Up @@ -176,6 +162,15 @@ impl SymphoniaDecoder {
Some(metadata)
}

pub fn sample_rate(&self) -> DecoderResult<u32> {
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 {
Expand Down
Loading