diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index fada25807..593724be4 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -9,34 +9,49 @@ 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; -const MIN_PERIOD_DIVISOR: Frames = 10; +const OPTIMAL_BUFFER_SIZE: Frames = SAMPLE_RATE as Frames / 2; +const OPTIMAL_PERIOD_SIZE: Frames = SAMPLE_RATE as Frames / 10; +const OPTIMAL_NUM_PERIODS: Frames = 5; +const MIN_NUM_PERIODS: Frames = 2; + +const COMMON_SAMPLE_RATES: [u32; 14] = [ + 8000, 11025, 16000, 22050, 44100, 48000, 88200, 96000, 176400, 192000, 352800, 384000, 705600, + 768000, +]; + +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), } } @@ -111,49 +122,49 @@ fn list_compatible_devices() -> SinkResult<()> { 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. - - 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 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:?}")); + // 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) { + // Only show devices that support + // 2 ch 44.1 Interleaved. + + 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 supported_formats: Vec = FORMATS + .iter() + .filter_map(|f| { + if hwp.test_format((*f).into()).is_ok() { + Some(format!("{f:?}")) + } else { + None + } + }) + .collect(); + + 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" + ); } } - - 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,237 +173,6 @@ 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 { let name = match device.as_deref() { @@ -401,7 +181,7 @@ impl Open for AlsaSink { exit(0); } Err(e) => { - error!("{}", e); + error!("{e}"); exit(1); } }, @@ -410,7 +190,7 @@ impl Open for AlsaSink { } .to_string(); - info!("Using AlsaSink with format: {:?}", format); + info!("Using AlsaSink with format: {format:?}"); Self { pcm: None, @@ -424,32 +204,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)?; } @@ -490,33 +257,191 @@ 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)?; - - 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) + fn set_period_and_buffer_size(hwp: &HwParams) -> 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 + } + }; + + 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 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(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: 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) { + 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(()) } }