diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index b647b1e4..9a8721e0 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -2,41 +2,56 @@ use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::{NUM_CHANNELS, SAMPLE_RATE as DECODER_SAMPLE_RATE}; use alsa::device_name::HintIter; use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; 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 OPTIMAL_NUM_PERIODS: Frames = 5; +const MIN_NUM_PERIODS: Frames = 2; -const MAX_PERIOD_DIVISOR: Frames = 4; -const MIN_PERIOD_DIVISOR: Frames = 10; +const COMMON_SAMPLE_RATES: [u32; 14] = [ + 8000, 11025, 16000, 22050, 44100, 48000, 88200, 96000, 176400, 192000, 352800, 384000, 705600, + 768000, +]; + +const SUPPORTED_SAMPLE_RATES: [u32; 4] = [44100, 48000, 88200, 96000]; + +const FORMATS: [AudioFormat; 6] = [ + AudioFormat::S16, + AudioFormat::S24, + AudioFormat::S24_3, + AudioFormat::S32, + AudioFormat::F32, + AudioFormat::F64, +]; #[derive(Debug, Error)] enum AlsaError { - #[error(" Device {device} Unsupported Format {alsa_format:?} ({format:?}), {e}")] + #[error(" Device {device} Unsupported Format {alsa_format} ({format:?}), {e}, Supported Format(s): {supported_formats:?}")] UnsupportedFormat { device: String, alsa_format: Format, format: AudioFormat, + supported_formats: Vec, e: alsa::Error, }, - #[error(" Device {device} Unsupported Channel Count {channel_count}, {e}")] + #[error(" Device {device} Unsupported Channel Count {channel_count}, {e}, Supported Channel Count(s): {supported_channel_counts:?}")] UnsupportedChannelCount { device: String, channel_count: u8, + supported_channel_counts: Vec, e: alsa::Error, }, - #[error(" Device {device} Unsupported Sample Rate {samplerate}, {e}")] + #[error(" Device {device} Unsupported Sample Rate {samplerate}, {e}, Supported Sample Rate(s): {supported_rates:?}")] UnsupportedSampleRate { device: String, samplerate: u32, + supported_rates: Vec, e: alsa::Error, }, @@ -63,9 +78,6 @@ enum AlsaError { #[error(" Could Not Parse Output Name(s) and/or Description(s), {0}")] Parsing(alsa::Error), - - #[error("")] - NotConnected, } impl From for SinkError { @@ -75,7 +87,6 @@ impl From for SinkError { match e { DrainFailure(_) | OnWrite(_) => SinkError::OnWrite(es), PcmSetUp { .. } => SinkError::ConnectionRefused(es), - NotConnected => SinkError::NotConnected(es), _ => SinkError::InvalidParams(es), } } @@ -98,6 +109,8 @@ impl From for Format { pub struct AlsaSink { pcm: Option, format: AudioFormat, + sample_rate: u32, + latency_scale_factor: f64, device: String, period_buffer: Vec, } @@ -106,54 +119,83 @@ fn list_compatible_devices() -> SinkResult<()> { let i = HintIter::new_str(None, "pcm").map_err(AlsaError::Parsing)?; println!("\n\n\tCompatible alsa device(s):\n"); - println!("\t------------------------------------------------------\n"); + println!("\t--------------------------------------------------------------------\n"); for a in i { if let Some(Direction::Playback) = a.direction { if let Some(name) = a.name { - if let Ok(pcm) = PCM::new(&name, Direction::Playback, false) { - if let Ok(hwp) = HwParams::any(&pcm) { - // Only show devices that support - // 2 ch 44.1 Interleaved. + // surround* outputs throw: + // ALSA lib pcm_route.c:877:(find_matching_chmap) Found no matching channel map + if name.contains(':') && !name.starts_with("surround") { + if let Ok(pcm) = PCM::new(&name, Direction::Playback, false) { + if let Ok(hwp) = HwParams::any(&pcm) { + if hwp.set_access(Access::RWInterleaved).is_ok() + && hwp.set_channels(NUM_CHANNELS as u32).is_ok() + { + let mut supported_formats_and_samplerates = String::new(); - if hwp.set_access(Access::RWInterleaved).is_ok() - && hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).is_ok() - && hwp.set_channels(NUM_CHANNELS as u32).is_ok() - { - let mut supported_formats = vec![]; + for format in FORMATS.iter() { + let hwp = hwp.clone(); - for f in &[ - AudioFormat::S16, - AudioFormat::S24, - AudioFormat::S24_3, - AudioFormat::S32, - AudioFormat::F32, - AudioFormat::F64, - ] { - if hwp.test_format(Format::from(*f)).is_ok() { - supported_formats.push(format!("{f:?}")); + if hwp.set_format((*format).into()).is_ok() { + let sample_rates: Vec = SUPPORTED_SAMPLE_RATES + .iter() + .filter_map(|sample_rate| { + let hwp = hwp.clone(); + if hwp + .set_rate(*sample_rate, ValueOr::Nearest) + .is_ok() + { + match *sample_rate { + 44100 => Some("44.1kHz".to_string()), + 48000 => Some("48kHz".to_string()), + 88200 => Some("88.2kHz".to_string()), + 96000 => Some("96kHz".to_string()), + _ => None, + } + } else { + None + } + }) + .collect(); + + if !sample_rates.is_empty() { + let format_and_sample_rates = + if *format == AudioFormat::S24_3 { + format!( + "\n\t\tFormat: {format:?} Sample Rate(s): {}", + sample_rates.join(", ") + ) + } else { + format!( + "\n\t\tFormat: {format:?} Sample Rate(s): {}", + sample_rates.join(", ") + ) + }; + + supported_formats_and_samplerates + .push_str(&format_and_sample_rates); + } + } + } + + if !supported_formats_and_samplerates.is_empty() { + println!("\tDevice:\n\n\t\t{name}\n"); + + println!( + "\tDescription:\n\n\t\t{}\n", + a.desc.unwrap_or_default().replace('\n', "\n\t\t") + ); + + println!("\tSupported Format & Sample Rate Combinations:\n{supported_formats_and_samplerates}\n"); + + println!( + "\t--------------------------------------------------------------------\n" + ); } } - - if !supported_formats.is_empty() { - println!("\tDevice:\n\n\t\t{name}\n"); - - println!( - "\tDescription:\n\n\t\t{}\n", - a.desc.unwrap_or_default().replace('\n', "\n\t\t") - ); - - println!( - "\tSupported Format(s):\n\n\t\t{}\n", - supported_formats.join(" ") - ); - - println!( - "\t------------------------------------------------------\n" - ); - } - } - }; + }; + } } } } @@ -162,246 +204,15 @@ fn list_compatible_devices() -> SinkResult<()> { Ok(()) } -fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> { - let pcm = PCM::new(dev_name, Direction::Playback, false).map_err(|e| AlsaError::PcmSetUp { - device: dev_name.to_string(), - e, - })?; - - let bytes_per_period = { - let hwp = HwParams::any(&pcm).map_err(AlsaError::HwParams)?; - - hwp.set_access(Access::RWInterleaved) - .map_err(|e| AlsaError::UnsupportedAccessType { - device: dev_name.to_string(), - e, - })?; - - let alsa_format = Format::from(format); - - hwp.set_format(alsa_format) - .map_err(|e| AlsaError::UnsupportedFormat { - device: dev_name.to_string(), - alsa_format, - format, - e, - })?; - - hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).map_err(|e| { - AlsaError::UnsupportedSampleRate { - device: dev_name.to_string(), - samplerate: SAMPLE_RATE, - e, - } - })?; - - hwp.set_channels(NUM_CHANNELS as u32) - .map_err(|e| AlsaError::UnsupportedChannelCount { - device: dev_name.to_string(), - channel_count: NUM_CHANNELS, - e, - })?; - - // Clone the hwp while it's in - // a good working state so that - // in the event of an error setting - // the buffer and period sizes - // we can use the good working clone - // instead of the hwp that's in an - // error state. - let hwp_clone = hwp.clone(); - - // At a sampling rate of 44100: - // The largest buffer is 22050 Frames (500ms) with 5512 Frame periods (125ms). - // The smallest buffer is 4410 Frames (100ms) with 441 Frame periods (10ms). - // Actual values may vary. - // - // Larger buffer and period sizes are preferred as extremely small values - // will cause high CPU useage. - // - // If no buffer or period size is in those ranges or an error happens - // trying to set the buffer or period size use the device's defaults - // which may not be ideal but are *hopefully* serviceable. - - let buffer_size = { - let max = match hwp.get_buffer_size_max() { - Err(e) => { - trace!("Error getting the device's max Buffer size: {}", e); - ZERO_FRAMES - } - Ok(s) => s, - }; - - let min = match hwp.get_buffer_size_min() { - Err(e) => { - trace!("Error getting the device's min Buffer size: {}", e); - ZERO_FRAMES - } - Ok(s) => s, - }; - - let buffer_size = if min < max { - match (MIN_BUFFER..=MAX_BUFFER) - .rev() - .find(|f| (min..=max).contains(f)) - { - Some(size) => { - trace!("Desired Frames per Buffer: {:?}", size); - - match hwp.set_buffer_size_near(size) { - Err(e) => { - trace!("Error setting the device's Buffer size: {}", e); - ZERO_FRAMES - } - Ok(s) => s, - } - } - None => { - trace!("No Desired Buffer size in range reported by the device."); - ZERO_FRAMES - } - } - } else { - trace!("The device's min reported Buffer size was greater than or equal to it's max reported Buffer size."); - ZERO_FRAMES - }; - - if buffer_size == ZERO_FRAMES { - trace!( - "Desired Buffer Frame range: {:?} - {:?}", - MIN_BUFFER, - MAX_BUFFER - ); - - trace!( - "Actual Buffer Frame range as reported by the device: {:?} - {:?}", - min, - max - ); - } - - buffer_size - }; - - let period_size = { - if buffer_size == ZERO_FRAMES { - ZERO_FRAMES - } else { - let max = match hwp.get_period_size_max() { - Err(e) => { - trace!("Error getting the device's max Period size: {}", e); - ZERO_FRAMES - } - Ok(s) => s, - }; - - let min = match hwp.get_period_size_min() { - Err(e) => { - trace!("Error getting the device's min Period size: {}", e); - ZERO_FRAMES - } - Ok(s) => s, - }; - - let max_period = buffer_size / MAX_PERIOD_DIVISOR; - let min_period = buffer_size / MIN_PERIOD_DIVISOR; - - let period_size = if min < max && min_period < max_period { - match (min_period..=max_period) - .rev() - .find(|f| (min..=max).contains(f)) - { - Some(size) => { - trace!("Desired Frames per Period: {:?}", size); - - match hwp.set_period_size_near(size, ValueOr::Nearest) { - Err(e) => { - trace!("Error setting the device's Period size: {}", e); - ZERO_FRAMES - } - Ok(s) => s, - } - } - None => { - trace!("No Desired Period size in range reported by the device."); - ZERO_FRAMES - } - } - } else { - trace!("The device's min reported Period size was greater than or equal to it's max reported Period size,"); - trace!("or the desired min Period size was greater than or equal to the desired max Period size."); - ZERO_FRAMES - }; - - if period_size == ZERO_FRAMES { - trace!("Buffer size: {:?}", buffer_size); - - trace!( - "Desired Period Frame range: {:?} (Buffer size / {:?}) - {:?} (Buffer size / {:?})", - min_period, - MIN_PERIOD_DIVISOR, - max_period, - MAX_PERIOD_DIVISOR, - ); - - trace!( - "Actual Period Frame range as reported by the device: {:?} - {:?}", - min, - max - ); - } - - period_size - } - }; - - if buffer_size == ZERO_FRAMES || period_size == ZERO_FRAMES { - trace!( - "Failed to set Buffer and/or Period size, falling back to the device's defaults." - ); - - trace!("You may experience higher than normal CPU usage and/or audio issues."); - - pcm.hw_params(&hwp_clone).map_err(AlsaError::Pcm)?; - } else { - pcm.hw_params(&hwp).map_err(AlsaError::Pcm)?; - } - - let hwp = pcm.hw_params_current().map_err(AlsaError::Pcm)?; - - // Don't assume we got what we wanted. Ask to make sure. - let frames_per_period = hwp.get_period_size().map_err(AlsaError::HwParams)?; - - let frames_per_buffer = hwp.get_buffer_size().map_err(AlsaError::HwParams)?; - - let swp = pcm.sw_params_current().map_err(AlsaError::Pcm)?; - - swp.set_start_threshold(frames_per_buffer - frames_per_period) - .map_err(AlsaError::SwParams)?; - - pcm.sw_params(&swp).map_err(AlsaError::Pcm)?; - - trace!("Actual Frames per Buffer: {:?}", frames_per_buffer); - trace!("Actual Frames per Period: {:?}", frames_per_period); - - // Let ALSA do the math for us. - pcm.frames_to_bytes(frames_per_period) as usize - }; - - trace!("Period Buffer size in bytes: {:?}", bytes_per_period); - - Ok((pcm, bytes_per_period)) -} - impl Open for AlsaSink { - fn open(device: Option, format: AudioFormat) -> Self { + fn open(device: Option, format: AudioFormat, sample_rate: u32) -> Self { let name = match device.as_deref() { Some("?") => match list_compatible_devices() { Ok(_) => { exit(0); } Err(e) => { - error!("{}", e); + error!("{e}"); exit(1); } }, @@ -410,11 +221,15 @@ impl Open for AlsaSink { } .to_string(); - info!("Using AlsaSink with format: {:?}", format); + let latency_scale_factor = DECODER_SAMPLE_RATE as f64 / sample_rate as f64; + + info!("Using AlsaSink with format: {format:?}, sample rate: {sample_rate}"); Self { pcm: None, format, + sample_rate, + latency_scale_factor, device: name, period_buffer: vec![], } @@ -424,32 +239,19 @@ 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.open_device()?; } Ok(()) } fn stop(&mut self) -> SinkResult<()> { - if self.pcm.is_some() { - // Zero fill the remainder of the period buffer and - // write any leftover data before draining the actual PCM buffer. - self.period_buffer.resize(self.period_buffer.capacity(), 0); - self.write_buf()?; - - let pcm = self.pcm.take().ok_or(AlsaError::NotConnected)?; + // Zero fill the remainder of the period buffer and + // write any leftover data before draining the actual PCM buffer. + self.period_buffer.resize(self.period_buffer.capacity(), 0); + self.write_buf()?; + if let Some(pcm) = self.pcm.take() { pcm.drain().map_err(AlsaError::DrainFailure)?; } @@ -458,6 +260,7 @@ impl Sink for AlsaSink { fn get_latency_pcm(&mut self) -> u64 { let buffer_len = self.period_buffer.len(); + let latency_scale_factor = self.latency_scale_factor; self.pcm .as_mut() @@ -467,7 +270,9 @@ impl Sink for AlsaSink { let frames_in_buffer = pcm.bytes_to_frames(buffer_len as isize); - (delay_frames + frames_in_buffer) as u64 + let total_frames = (delay_frames + frames_in_buffer) as f64; + + (total_frames * latency_scale_factor) as u64 }) }) .unwrap_or(0) @@ -507,33 +312,203 @@ impl SinkAsBytes for AlsaSink { impl AlsaSink { pub const NAME: &'static str = "alsa"; - fn write_buf(&mut self) -> SinkResult<()> { - if self.pcm.is_some() { - let write_result = { - let pcm = self.pcm.as_mut().ok_or(AlsaError::NotConnected)?; + fn set_period_and_buffer_size( + hwp: &HwParams, + optimal_buffer_size: Frames, + optimal_period_size: Frames, + ) -> bool { + let period_size = match hwp.set_period_size_near(optimal_period_size, ValueOr::Nearest) { + Ok(period_size) => { + if period_size > 0 { + trace!("Closest Supported Period Size to Optimal ({optimal_period_size}): {period_size}"); + period_size + } else { + trace!("Error getting Period Size, Period Size must be greater than 0, falling back to the device's default Buffer parameters"); + 0 + } + } + Err(e) => { + trace!("Error getting Period Size: {e}, falling back to the device's default Buffer parameters"); + 0 + } + }; - match pcm.io_bytes().writei(&self.period_buffer) { - Ok(_) => Ok(()), - Err(e) => { - // Capture and log the original error as a warning, and then try to recover. - // If recovery fails then forward that error back to player. - warn!( - "Error writing from AlsaSink buffer to PCM, trying to recover, {}", - e - ); - - pcm.try_recover(e, false).map_err(AlsaError::OnWrite) + if period_size > 0 { + let buffer_size = match hwp + .set_buffer_size_near((period_size * OPTIMAL_NUM_PERIODS).max(optimal_buffer_size)) + { + Ok(buffer_size) => { + if buffer_size >= period_size * MIN_NUM_PERIODS { + trace!("Closest Supported Buffer Size to Optimal ({optimal_buffer_size}): {buffer_size}"); + buffer_size + } else { + trace!("Error getting Buffer Size, Buffer Size must be at least {period_size} * {MIN_NUM_PERIODS}, falling back to the device's default Buffer parameters"); + 0 } } + Err(e) => { + trace!("Error getting Buffer Size: {e}, falling back to the device's default Buffer parameters"); + 0 + } }; - if let Err(e) = write_result { - self.pcm = None; - return Err(e.into()); + return buffer_size > 0; + } + + false + } + + fn open_device(&mut self) -> SinkResult<()> { + let optimal_buffer_size: Frames = self.sample_rate as Frames / 2; + let optimal_period_size: Frames = self.sample_rate as Frames / 10; + + let pcm = PCM::new(&self.device, Direction::Playback, false).map_err(|e| { + AlsaError::PcmSetUp { + device: self.device.clone(), + e, + } + })?; + + { + let hwp = HwParams::any(&pcm).map_err(AlsaError::HwParams)?; + + hwp.set_access(Access::RWInterleaved).map_err(|e| { + AlsaError::UnsupportedAccessType { + device: self.device.clone(), + e, + } + })?; + + let alsa_format = self.format.into(); + + hwp.set_format(alsa_format).map_err(|e| { + let supported_formats = FORMATS + .iter() + .filter_map(|f| { + if hwp.test_format((*f).into()).is_ok() { + Some(format!("{f:?}")) + } else { + None + } + }) + .collect(); + + AlsaError::UnsupportedFormat { + device: self.device.clone(), + alsa_format, + format: self.format, + supported_formats, + e, + } + })?; + + hwp.set_rate(self.sample_rate, ValueOr::Nearest) + .map_err(|e| { + let supported_rates = (hwp.get_rate_min().unwrap_or_default() + ..=hwp.get_rate_max().unwrap_or_default()) + .filter(|r| COMMON_SAMPLE_RATES.contains(r) && hwp.test_rate(*r).is_ok()) + .collect(); + + AlsaError::UnsupportedSampleRate { + device: self.device.clone(), + samplerate: self.sample_rate, + supported_rates, + e, + } + })?; + + hwp.set_channels(NUM_CHANNELS as u32).map_err(|e| { + let supported_channel_counts = (hwp.get_channels_min().unwrap_or_default() + ..=hwp.get_channels_max().unwrap_or_default()) + .filter(|c| hwp.test_channels(*c).is_ok()) + .collect(); + + AlsaError::UnsupportedChannelCount { + device: self.device.clone(), + channel_count: NUM_CHANNELS, + supported_channel_counts, + e, + } + })?; + + // Calculate a buffer and period size as close + // to optimal as possible. + + // hwp continuity is very important. + let hwp_clone = hwp.clone(); + + if Self::set_period_and_buffer_size( + &hwp_clone, + optimal_buffer_size, + optimal_period_size, + ) { + pcm.hw_params(&hwp_clone).map_err(AlsaError::Pcm)?; + } else { + pcm.hw_params(&hwp).map_err(AlsaError::Pcm)?; + } + + let hwp = pcm.hw_params_current().map_err(AlsaError::Pcm)?; + + // Don't assume we got what we wanted. Ask to make sure. + let buffer_size = hwp.get_buffer_size().map_err(AlsaError::HwParams)?; + + let period_size = hwp.get_period_size().map_err(AlsaError::HwParams)?; + + let swp = pcm.sw_params_current().map_err(AlsaError::Pcm)?; + + swp.set_start_threshold(buffer_size - period_size) + .map_err(AlsaError::SwParams)?; + + pcm.sw_params(&swp).map_err(AlsaError::Pcm)?; + + if buffer_size != optimal_buffer_size { + trace!("A Buffer Size of {buffer_size} Frames is Suboptimal"); + + if buffer_size < optimal_buffer_size { + trace!("A smaller than necessary Buffer Size can lead to Buffer underruns (audio glitches) and high CPU usage."); + } else { + trace!("A larger than necessary Buffer Size can lead to perceivable latency (lag)."); + } + } + + let optimal_period_size = buffer_size / OPTIMAL_NUM_PERIODS; + + if period_size != optimal_period_size { + trace!("A Period Size of {period_size} Frames is Suboptimal"); + + if period_size < optimal_period_size { + trace!("A smaller than necessary Period Size relative to Buffer Size can lead to high CPU usage."); + } else { + trace!("A larger than necessary Period Size relative to Buffer Size can lessen Buffer underrun (audio glitch) protection."); + } + } + + // Let ALSA do the math for us. + let bytes_per_period = pcm.frames_to_bytes(period_size) as usize; + + trace!("Period Buffer size in bytes: {bytes_per_period}"); + + self.period_buffer = Vec::with_capacity(bytes_per_period); + } + + self.pcm = Some(pcm); + + Ok(()) + } + + fn write_buf(&mut self) -> SinkResult<()> { + if let Some(pcm) = self.pcm.as_mut() { + if let Err(e) = pcm.io_bytes().writei(&self.period_buffer) { + // Capture and log the original error as a warning, and then try to recover. + // If recovery fails then forward that error back to player. + warn!("Error writing from AlsaSink Buffer to PCM, trying to recover, {e}"); + + pcm.try_recover(e, false).map_err(AlsaError::OnWrite)?; } } self.period_buffer.clear(); + Ok(()) } } diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index e3cc78cf..5651211e 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -14,7 +14,7 @@ use std::sync::Arc; use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; use crate::{ - config::AudioFormat, convert::Converter, decoder::AudioPacket, NUM_CHANNELS, SAMPLE_RATE, + config::AudioFormat, convert::Converter, decoder::AudioPacket, NUM_CHANNELS, }; pub struct GstreamerSink { @@ -26,8 +26,8 @@ pub struct GstreamerSink { } impl Open for GstreamerSink { - fn open(device: Option, format: AudioFormat) -> Self { - info!("Using GStreamer sink with format: {format:?}"); + fn open(device: Option, format: AudioFormat, sample_rate: u32) -> Self { + info!("Using GStreamer sink with format: {format:?}, sample rate: {sample_rate}"); gst::init().expect("failed to init GStreamer!"); let gst_format = match format { @@ -39,7 +39,7 @@ impl Open for GstreamerSink { AudioFormat::S16 => gst_audio::AUDIO_FORMAT_S16, }; - let gst_info = gst_audio::AudioInfo::builder(gst_format, SAMPLE_RATE, NUM_CHANNELS as u32) + let gst_info = gst_audio::AudioInfo::builder(gst_format, sample_rate, NUM_CHANNELS as u32) .build() .expect("Failed to create GStreamer audio format"); let gst_caps = gst_info.to_caps().expect("Failed to create GStreamer caps"); diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 9d40ee82..180040e6 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -38,11 +38,11 @@ impl ProcessHandler for JackData { } impl Open for JackSink { - fn open(client_name: Option, format: AudioFormat) -> Self { + fn open(client_name: Option, format: AudioFormat, sample_rate: u32) -> Self { if format != AudioFormat::F32 { warn!("JACK currently does not support {format:?} output"); } - info!("Using JACK sink with format {:?}", AudioFormat::F32); + info!("Using JACK sink with format {:?}, sample rate: {sample_rate}", AudioFormat::F32); let client_name = client_name.unwrap_or_else(|| "librespot".to_string()); let (client, _status) = diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 05916fa6..fc39162f 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -20,7 +20,7 @@ pub enum SinkError { pub type SinkResult = Result; pub trait Open { - fn open(_: Option, format: AudioFormat) -> Self; + fn open(_: Option, format: AudioFormat, sample_rate: u32) -> Self; } pub trait Sink { @@ -36,14 +36,18 @@ pub trait Sink { fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()>; } -pub type SinkBuilder = fn(Option, AudioFormat) -> Box; +pub type SinkBuilder = fn(Option, AudioFormat, u32) -> Box; pub trait SinkAsBytes { fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()>; } -fn mk_sink(device: Option, format: AudioFormat) -> Box { - Box::new(S::open(device, format)) +fn mk_sink( + device: Option, + format: AudioFormat, + sample_rate: u32, +) -> Box { + Box::new(S::open(device, format, sample_rate)) } // reuse code for various backends diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index e0e8a77c..05db4953 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -42,13 +42,13 @@ pub struct StdoutSink { } impl Open for StdoutSink { - fn open(file: Option, format: AudioFormat) -> Self { + fn open(file: Option, format: AudioFormat, sample_rate: u32) -> Self { if let Some("?") = file.as_deref() { println!("\nUsage:\n\nOutput to stdout:\n\n\t--backend pipe\n\nOutput to file:\n\n\t--backend pipe --device {{filename}}\n"); exit(0); } - info!("Using StdoutSink (pipe) with format: {:?}", format); + info!("Using StdoutSink (pipe) with format: {format:?}, sample rate: {sample_rate}"); Self { output: None, diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index c44245cf..eb455b0f 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -12,14 +12,17 @@ pub enum PortAudioSink<'a> { F32( Option>, StreamParameters, + f64, ), S32( Option>, StreamParameters, + f64, ), S16( Option>, StreamParameters, + f64, ), } @@ -51,8 +54,8 @@ fn find_output(device: &str) -> Option { } impl<'a> Open for PortAudioSink<'a> { - fn open(device: Option, format: AudioFormat) -> PortAudioSink<'a> { - info!("Using PortAudio sink with format: {format:?}"); + fn open(device: Option, format: AudioFormat, sample_rate: u32) -> PortAudioSink<'a> { + info!("Using PortAudio sink with format: {format:?}, sample rate: {sample_rate}"); portaudio_rs::initialize().unwrap(); @@ -80,13 +83,13 @@ impl<'a> Open for PortAudioSink<'a> { suggested_latency: latency, data: 0.0 as $type, }; - $sink(None, params) + $sink(None, params, sample_rate) }}; } match format { - AudioFormat::F32 => open_sink!(Self::F32, f32), - AudioFormat::S32 => open_sink!(Self::S32, i32), - AudioFormat::S16 => open_sink!(Self::S16, i16), + AudioFormat::F32 => open_sink!(Self::F32, f32, sample_rate as f64), + AudioFormat::S32 => open_sink!(Self::S32, i32, sample_rate as f64), + AudioFormat::S16 => open_sink!(Self::S16, i16, sample_rate as f64), _ => { unimplemented!("PortAudio currently does not support {format:?} output") } @@ -97,13 +100,13 @@ impl<'a> Open for PortAudioSink<'a> { impl<'a> Sink for PortAudioSink<'a> { fn start(&mut self) -> SinkResult<()> { macro_rules! start_sink { - (ref mut $stream: ident, ref $parameters: ident) => {{ + (ref mut $stream: ident, ref $parameters: ident, ref $sample_rate: ident ) => {{ if $stream.is_none() { *$stream = Some( Stream::open( None, Some(*$parameters), - SAMPLE_RATE as f64, + *$sample_rate, FRAMES_PER_BUFFER_UNSPECIFIED, StreamFlags::DITHER_OFF, // no need to dither twice; use librespot dithering instead None, @@ -116,9 +119,9 @@ impl<'a> Sink for PortAudioSink<'a> { } match self { - Self::F32(stream, parameters) => start_sink!(ref mut stream, ref parameters), - Self::S32(stream, parameters) => start_sink!(ref mut stream, ref parameters), - Self::S16(stream, parameters) => start_sink!(ref mut stream, ref parameters), + Self::F32(stream, parameters, sample_rate) => start_sink!(ref mut stream, ref parameters, ref sample_rate), + Self::S32(stream, parameters, sample_rate) => start_sink!(ref mut stream, ref parameters, ref sample_rate), + Self::S16(stream, parameters, sample_rate) => start_sink!(ref mut stream, ref parameters, ref sample_rate), }; Ok(()) diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 35d25f38..d60351e9 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -2,7 +2,7 @@ use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::{NUM_CHANNELS, SAMPLE_RATE as DECODER_SAMPLE_RATE}; use libpulse_binding::{self as pulse, error::PAErr, stream::Direction}; use libpulse_simple_binding::Simple; use std::env; @@ -24,9 +24,6 @@ enum PulseError { #[error(" Failed to Drain Pulseaudio Buffer, {0}")] DrainFailure(PAErr), - #[error("")] - NotConnected, - #[error(" {0}")] OnWrite(PAErr), } @@ -38,40 +35,63 @@ impl From for SinkError { match e { DrainFailure(_) | OnWrite(_) => SinkError::OnWrite(es), ConnectionRefused(_) => SinkError::ConnectionRefused(es), - NotConnected => SinkError::NotConnected(es), InvalidSampleSpec { .. } => SinkError::InvalidParams(es), } } } +impl From for pulse::sample::Format { + fn from(f: AudioFormat) -> pulse::sample::Format { + use AudioFormat::*; + match f { + F64 | F32 => pulse::sample::Format::FLOAT32NE, + S32 => pulse::sample::Format::S32NE, + S24 => pulse::sample::Format::S24_32NE, + S24_3 => pulse::sample::Format::S24NE, + S16 => pulse::sample::Format::S16NE, + } + } +} + pub struct PulseAudioSink { sink: Option, device: Option, app_name: String, stream_desc: String, format: AudioFormat, + sample_rate: u32, + + sample_spec: pulse::sample::Spec, } impl Open for PulseAudioSink { - fn open(device: Option, format: AudioFormat) -> Self { + fn open(device: Option, format: AudioFormat, sample_rate: u32) -> Self { let app_name = env::var("PULSE_PROP_application.name").unwrap_or_default(); let stream_desc = env::var("PULSE_PROP_stream.description").unwrap_or_default(); - let mut actual_format = format; - - if actual_format == AudioFormat::F64 { + let format = if format == AudioFormat::F64 { warn!("PulseAudio currently does not support F64 output"); - actual_format = AudioFormat::F32; - } + AudioFormat::F32 + } else { + format + }; - info!("Using PulseAudioSink with format: {actual_format:?}"); + info!("Using PulseAudioSink with format: {format:?}, sample rate: {sample_rate}"); + + let sample_spec = pulse::sample::Spec { + format: format.into(), + channels: NUM_CHANNELS, + rate: sample_rate, + }; Self { sink: None, device, app_name, stream_desc, - format: actual_format, + format, + sample_rate, + sample_spec, } } } @@ -79,31 +99,15 @@ impl Open for PulseAudioSink { impl Sink for PulseAudioSink { fn start(&mut self) -> SinkResult<()> { if self.sink.is_none() { - // PulseAudio calls S24 and S24_3 different from the rest of the world - let pulse_format = match self.format { - AudioFormat::F32 => pulse::sample::Format::FLOAT32NE, - AudioFormat::S32 => pulse::sample::Format::S32NE, - AudioFormat::S24 => pulse::sample::Format::S24_32NE, - AudioFormat::S24_3 => pulse::sample::Format::S24NE, - AudioFormat::S16 => pulse::sample::Format::S16NE, - _ => unreachable!(), - }; - - let sample_spec = pulse::sample::Spec { - format: pulse_format, - channels: NUM_CHANNELS, - rate: SAMPLE_RATE, - }; - - if !sample_spec.is_valid() { + if !self.sample_spec.is_valid() { let pulse_error = PulseError::InvalidSampleSpec { - pulse_format, + pulse_format: self.sample_spec.format, format: self.format, channels: NUM_CHANNELS, - rate: SAMPLE_RATE, + rate: self.sample_rate, }; - return Err(SinkError::from(pulse_error)); + return Err(pulse_error.into()); } let sink = Simple::new( @@ -112,7 +116,7 @@ impl Sink for PulseAudioSink { Direction::Playback, // Direction. self.device.as_deref(), // Our device (sink) name. &self.stream_desc, // Description of our stream. - &sample_spec, // Our sample format. + &self.sample_spec, // Our sample format. None, // Use default channel map. None, // Use default buffering attributes. ) @@ -125,9 +129,10 @@ impl Sink for PulseAudioSink { } fn stop(&mut self) -> SinkResult<()> { - let sink = self.sink.take().ok_or(PulseError::NotConnected)?; + if let Some(sink) = self.sink.take() { + sink.drain().map_err(PulseError::DrainFailure)?; + } - sink.drain().map_err(PulseError::DrainFailure)?; Ok(()) } @@ -135,9 +140,9 @@ impl Sink for PulseAudioSink { self.sink .as_mut() .and_then(|sink| { - sink.get_latency() - .ok() - .map(|micro_sec| (micro_sec.as_secs_f64() * SAMPLE_RATE as f64) as u64) + sink.get_latency().ok().map(|micro_sec| { + (micro_sec.as_secs_f64() * DECODER_SAMPLE_RATE as f64).round() as u64 + }) }) .unwrap_or(0) } @@ -147,9 +152,9 @@ impl Sink for PulseAudioSink { impl SinkAsBytes for PulseAudioSink { fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { - let sink = self.sink.as_mut().ok_or(PulseError::NotConnected)?; - - sink.write(data).map_err(PulseError::OnWrite)?; + if let Some(sink) = self.sink.as_mut() { + sink.write(data).map_err(PulseError::OnWrite)?; + } Ok(()) } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 2632f54a..4b63d352 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -9,7 +9,7 @@ use super::{Sink, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::NUM_CHANNELS; #[cfg(all( feature = "rodiojack-backend", @@ -18,16 +18,17 @@ use crate::{NUM_CHANNELS, SAMPLE_RATE}; compile_error!("Rodio JACK backend is currently only supported on linux."); #[cfg(feature = "rodio-backend")] -pub fn mk_rodio(device: Option, format: AudioFormat) -> Box { - Box::new(open(cpal::default_host(), device, format)) +pub fn mk_rodio(device: Option, format: AudioFormat, sample_rate: u32) -> Box { + Box::new(open(cpal::default_host(), device, format, sample_rate)) } #[cfg(feature = "rodiojack-backend")] -pub fn mk_rodiojack(device: Option, format: AudioFormat) -> Box { +pub fn mk_rodiojack(device: Option, format: AudioFormat, sample_rate: u32) -> Box { Box::new(open( cpal::host_from_id(cpal::HostId::Jack).unwrap(), device, format, + sample_rate, )) } @@ -62,6 +63,7 @@ impl From for SinkError { pub struct RodioSink { rodio_sink: rodio::Sink, format: AudioFormat, + sample_rate: u32, _stream: rodio::OutputStream, } @@ -164,7 +166,7 @@ fn create_sink( Ok((sink, stream)) } -pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> RodioSink { +pub fn open(host: cpal::Host, device: Option, format: AudioFormat, sample_rate: u32) -> RodioSink { info!( "Using Rodio sink with format {format:?} and cpal host: {}", host.id().name() @@ -180,6 +182,7 @@ pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> Ro RodioSink { rodio_sink: sink, format, + sample_rate, _stream: stream, } } @@ -205,7 +208,7 @@ impl Sink for RodioSink { let samples_f32: &[f32] = &converter.f64_to_f32(samples); let source = rodio::buffer::SamplesBuffer::new( NUM_CHANNELS as u16, - SAMPLE_RATE, + self.sample_rate, samples_f32, ); self.rodio_sink.append(source); @@ -214,7 +217,7 @@ impl Sink for RodioSink { let samples_s16: &[i16] = &converter.f64_to_s16(samples); let source = rodio::buffer::SamplesBuffer::new( NUM_CHANNELS as u16, - SAMPLE_RATE, + self.sample_rate, samples_s16, ); self.rodio_sink.append(source); diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 0d220928..ce7c146b 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -14,8 +14,8 @@ pub enum SdlSink { } impl Open for SdlSink { - fn open(device: Option, format: AudioFormat) -> Self { - info!("Using SDL sink with format: {:?}", format); + fn open(device: Option, format: AudioFormat, sample_rate: u32) -> Self { + info!("Using SDL sink with format: {format:?}, sample rate: {sample_rate}"); if device.is_some() { warn!("SDL sink does not support specifying a device name"); @@ -27,7 +27,7 @@ impl Open for SdlSink { .expect("could not initialize SDL audio subsystem"); let desired_spec = AudioSpecDesired { - freq: Some(SAMPLE_RATE as i32), + freq: Some(sample_rate as i32), channels: Some(NUM_CHANNELS), samples: None, }; diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 6ce545da..15778dbe 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -66,13 +66,13 @@ pub struct SubprocessSink { } impl Open for SubprocessSink { - fn open(shell_command: Option, format: AudioFormat) -> Self { + fn open(shell_command: Option, format: AudioFormat, sample_rate: u32) -> Self { if let Some("?") = shell_command.as_deref() { println!("\nUsage:\n\nOutput to a Subprocess:\n\n\t--backend subprocess --device {{shell_command}}\n"); exit(0); } - info!("Using SubprocessSink with format: {:?}", format); + info!("Using SubprocessSink with format: {format:?}, sample rate: {sample_rate}"); Self { shell_command,