mirror of
https://github.com/librespot-org/librespot.git
synced 2025-01-07 17:24:04 +00:00
Change the backends so that they support the diffrent sample rates
This commit is contained in:
parent
3bcf5498d2
commit
e1ea400220
10 changed files with 392 additions and 402 deletions
|
@ -2,41 +2,56 @@ use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};
|
||||||
use crate::config::AudioFormat;
|
use crate::config::AudioFormat;
|
||||||
use crate::convert::Converter;
|
use crate::convert::Converter;
|
||||||
use crate::decoder::AudioPacket;
|
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::device_name::HintIter;
|
||||||
use alsa::pcm::{Access, Format, Frames, HwParams, PCM};
|
use alsa::pcm::{Access, Format, Frames, HwParams, PCM};
|
||||||
use alsa::{Direction, ValueOr};
|
use alsa::{Direction, ValueOr};
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
const MAX_BUFFER: Frames = (SAMPLE_RATE / 2) as Frames;
|
const OPTIMAL_NUM_PERIODS: Frames = 5;
|
||||||
const MIN_BUFFER: Frames = (SAMPLE_RATE / 10) as Frames;
|
const MIN_NUM_PERIODS: Frames = 2;
|
||||||
const ZERO_FRAMES: Frames = 0;
|
|
||||||
|
|
||||||
const MAX_PERIOD_DIVISOR: Frames = 4;
|
const COMMON_SAMPLE_RATES: [u32; 14] = [
|
||||||
const MIN_PERIOD_DIVISOR: Frames = 10;
|
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)]
|
#[derive(Debug, Error)]
|
||||||
enum AlsaError {
|
enum AlsaError {
|
||||||
#[error("<AlsaSink> Device {device} Unsupported Format {alsa_format:?} ({format:?}), {e}")]
|
#[error("<AlsaSink> Device {device} Unsupported Format {alsa_format} ({format:?}), {e}, Supported Format(s): {supported_formats:?}")]
|
||||||
UnsupportedFormat {
|
UnsupportedFormat {
|
||||||
device: String,
|
device: String,
|
||||||
alsa_format: Format,
|
alsa_format: Format,
|
||||||
format: AudioFormat,
|
format: AudioFormat,
|
||||||
|
supported_formats: Vec<String>,
|
||||||
e: alsa::Error,
|
e: alsa::Error,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("<AlsaSink> Device {device} Unsupported Channel Count {channel_count}, {e}")]
|
#[error("<AlsaSink> Device {device} Unsupported Channel Count {channel_count}, {e}, Supported Channel Count(s): {supported_channel_counts:?}")]
|
||||||
UnsupportedChannelCount {
|
UnsupportedChannelCount {
|
||||||
device: String,
|
device: String,
|
||||||
channel_count: u8,
|
channel_count: u8,
|
||||||
|
supported_channel_counts: Vec<u32>,
|
||||||
e: alsa::Error,
|
e: alsa::Error,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("<AlsaSink> Device {device} Unsupported Sample Rate {samplerate}, {e}")]
|
#[error("<AlsaSink> Device {device} Unsupported Sample Rate {samplerate}, {e}, Supported Sample Rate(s): {supported_rates:?}")]
|
||||||
UnsupportedSampleRate {
|
UnsupportedSampleRate {
|
||||||
device: String,
|
device: String,
|
||||||
samplerate: u32,
|
samplerate: u32,
|
||||||
|
supported_rates: Vec<u32>,
|
||||||
e: alsa::Error,
|
e: alsa::Error,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -63,9 +78,6 @@ enum AlsaError {
|
||||||
|
|
||||||
#[error("<AlsaSink> Could Not Parse Output Name(s) and/or Description(s), {0}")]
|
#[error("<AlsaSink> Could Not Parse Output Name(s) and/or Description(s), {0}")]
|
||||||
Parsing(alsa::Error),
|
Parsing(alsa::Error),
|
||||||
|
|
||||||
#[error("<AlsaSink>")]
|
|
||||||
NotConnected,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<AlsaError> for SinkError {
|
impl From<AlsaError> for SinkError {
|
||||||
|
@ -75,7 +87,6 @@ impl From<AlsaError> for SinkError {
|
||||||
match e {
|
match e {
|
||||||
DrainFailure(_) | OnWrite(_) => SinkError::OnWrite(es),
|
DrainFailure(_) | OnWrite(_) => SinkError::OnWrite(es),
|
||||||
PcmSetUp { .. } => SinkError::ConnectionRefused(es),
|
PcmSetUp { .. } => SinkError::ConnectionRefused(es),
|
||||||
NotConnected => SinkError::NotConnected(es),
|
|
||||||
_ => SinkError::InvalidParams(es),
|
_ => SinkError::InvalidParams(es),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,6 +109,8 @@ impl From<AudioFormat> for Format {
|
||||||
pub struct AlsaSink {
|
pub struct AlsaSink {
|
||||||
pcm: Option<PCM>,
|
pcm: Option<PCM>,
|
||||||
format: AudioFormat,
|
format: AudioFormat,
|
||||||
|
sample_rate: u32,
|
||||||
|
latency_scale_factor: f64,
|
||||||
device: String,
|
device: String,
|
||||||
period_buffer: Vec<u8>,
|
period_buffer: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
@ -106,36 +119,67 @@ fn list_compatible_devices() -> SinkResult<()> {
|
||||||
let i = HintIter::new_str(None, "pcm").map_err(AlsaError::Parsing)?;
|
let i = HintIter::new_str(None, "pcm").map_err(AlsaError::Parsing)?;
|
||||||
|
|
||||||
println!("\n\n\tCompatible alsa device(s):\n");
|
println!("\n\n\tCompatible alsa device(s):\n");
|
||||||
println!("\t------------------------------------------------------\n");
|
println!("\t--------------------------------------------------------------------\n");
|
||||||
|
|
||||||
for a in i {
|
for a in i {
|
||||||
if let Some(Direction::Playback) = a.direction {
|
if let Some(Direction::Playback) = a.direction {
|
||||||
if let Some(name) = a.name {
|
if let Some(name) = a.name {
|
||||||
|
// 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(pcm) = PCM::new(&name, Direction::Playback, false) {
|
||||||
if let Ok(hwp) = HwParams::any(&pcm) {
|
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()
|
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()
|
&& hwp.set_channels(NUM_CHANNELS as u32).is_ok()
|
||||||
{
|
{
|
||||||
let mut supported_formats = vec![];
|
let mut supported_formats_and_samplerates = String::new();
|
||||||
|
|
||||||
for f in &[
|
for format in FORMATS.iter() {
|
||||||
AudioFormat::S16,
|
let hwp = hwp.clone();
|
||||||
AudioFormat::S24,
|
|
||||||
AudioFormat::S24_3,
|
if hwp.set_format((*format).into()).is_ok() {
|
||||||
AudioFormat::S32,
|
let sample_rates: Vec<String> = SUPPORTED_SAMPLE_RATES
|
||||||
AudioFormat::F32,
|
.iter()
|
||||||
AudioFormat::F64,
|
.filter_map(|sample_rate| {
|
||||||
] {
|
let hwp = hwp.clone();
|
||||||
if hwp.test_format(Format::from(*f)).is_ok() {
|
if hwp
|
||||||
supported_formats.push(format!("{f:?}"));
|
.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.is_empty() {
|
if !supported_formats_and_samplerates.is_empty() {
|
||||||
println!("\tDevice:\n\n\t\t{name}\n");
|
println!("\tDevice:\n\n\t\t{name}\n");
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
|
@ -143,13 +187,10 @@ fn list_compatible_devices() -> SinkResult<()> {
|
||||||
a.desc.unwrap_or_default().replace('\n', "\n\t\t")
|
a.desc.unwrap_or_default().replace('\n', "\n\t\t")
|
||||||
);
|
);
|
||||||
|
|
||||||
println!(
|
println!("\tSupported Format & Sample Rate Combinations:\n{supported_formats_and_samplerates}\n");
|
||||||
"\tSupported Format(s):\n\n\t\t{}\n",
|
|
||||||
supported_formats.join(" ")
|
|
||||||
);
|
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"\t------------------------------------------------------\n"
|
"\t--------------------------------------------------------------------\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,250 +199,20 @@ fn list_compatible_devices() -> SinkResult<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
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 {
|
impl Open for AlsaSink {
|
||||||
fn open(device: Option<String>, format: AudioFormat) -> Self {
|
fn open(device: Option<String>, format: AudioFormat, sample_rate: u32) -> Self {
|
||||||
let name = match device.as_deref() {
|
let name = match device.as_deref() {
|
||||||
Some("?") => match list_compatible_devices() {
|
Some("?") => match list_compatible_devices() {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{}", e);
|
error!("{e}");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -410,11 +221,15 @@ impl Open for AlsaSink {
|
||||||
}
|
}
|
||||||
.to_string();
|
.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 {
|
Self {
|
||||||
pcm: None,
|
pcm: None,
|
||||||
format,
|
format,
|
||||||
|
sample_rate,
|
||||||
|
latency_scale_factor,
|
||||||
device: name,
|
device: name,
|
||||||
period_buffer: vec![],
|
period_buffer: vec![],
|
||||||
}
|
}
|
||||||
|
@ -424,32 +239,19 @@ impl Open for AlsaSink {
|
||||||
impl Sink for AlsaSink {
|
impl Sink for AlsaSink {
|
||||||
fn start(&mut self) -> SinkResult<()> {
|
fn start(&mut self) -> SinkResult<()> {
|
||||||
if self.pcm.is_none() {
|
if self.pcm.is_none() {
|
||||||
let (pcm, bytes_per_period) = open_device(&self.device, self.format)?;
|
self.open_device()?;
|
||||||
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop(&mut self) -> SinkResult<()> {
|
fn stop(&mut self) -> SinkResult<()> {
|
||||||
if self.pcm.is_some() {
|
|
||||||
// Zero fill the remainder of the period buffer and
|
// Zero fill the remainder of the period buffer and
|
||||||
// write any leftover data before draining the actual PCM buffer.
|
// write any leftover data before draining the actual PCM buffer.
|
||||||
self.period_buffer.resize(self.period_buffer.capacity(), 0);
|
self.period_buffer.resize(self.period_buffer.capacity(), 0);
|
||||||
self.write_buf()?;
|
self.write_buf()?;
|
||||||
|
|
||||||
let pcm = self.pcm.take().ok_or(AlsaError::NotConnected)?;
|
if let Some(pcm) = self.pcm.take() {
|
||||||
|
|
||||||
pcm.drain().map_err(AlsaError::DrainFailure)?;
|
pcm.drain().map_err(AlsaError::DrainFailure)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -458,6 +260,7 @@ impl Sink for AlsaSink {
|
||||||
|
|
||||||
fn get_latency_pcm(&mut self) -> u64 {
|
fn get_latency_pcm(&mut self) -> u64 {
|
||||||
let buffer_len = self.period_buffer.len();
|
let buffer_len = self.period_buffer.len();
|
||||||
|
let latency_scale_factor = self.latency_scale_factor;
|
||||||
|
|
||||||
self.pcm
|
self.pcm
|
||||||
.as_mut()
|
.as_mut()
|
||||||
|
@ -467,7 +270,9 @@ impl Sink for AlsaSink {
|
||||||
|
|
||||||
let frames_in_buffer = pcm.bytes_to_frames(buffer_len as isize);
|
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)
|
.unwrap_or(0)
|
||||||
|
@ -507,33 +312,203 @@ impl SinkAsBytes for AlsaSink {
|
||||||
impl AlsaSink {
|
impl AlsaSink {
|
||||||
pub const NAME: &'static str = "alsa";
|
pub const NAME: &'static str = "alsa";
|
||||||
|
|
||||||
fn write_buf(&mut self) -> SinkResult<()> {
|
fn set_period_and_buffer_size(
|
||||||
if self.pcm.is_some() {
|
hwp: &HwParams,
|
||||||
let write_result = {
|
optimal_buffer_size: Frames,
|
||||||
let pcm = self.pcm.as_mut().ok_or(AlsaError::NotConnected)?;
|
optimal_period_size: Frames,
|
||||||
|
) -> bool {
|
||||||
match pcm.io_bytes().writei(&self.period_buffer) {
|
let period_size = match hwp.set_period_size_near(optimal_period_size, ValueOr::Nearest) {
|
||||||
Ok(_) => Ok(()),
|
Ok(period_size) => {
|
||||||
Err(e) => {
|
if period_size > 0 {
|
||||||
// Capture and log the original error as a warning, and then try to recover.
|
trace!("Closest Supported Period Size to Optimal ({optimal_period_size}): {period_size}");
|
||||||
// If recovery fails then forward that error back to player.
|
period_size
|
||||||
warn!(
|
} else {
|
||||||
"Error writing from AlsaSink buffer to PCM, trying to recover, {}",
|
trace!("Error getting Period Size, Period Size must be greater than 0, falling back to the device's default Buffer parameters");
|
||||||
e
|
0
|
||||||
);
|
|
||||||
|
|
||||||
pcm.try_recover(e, false).map_err(AlsaError::OnWrite)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
trace!("Error getting Period Size: {e}, falling back to the device's default Buffer parameters");
|
||||||
|
0
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = write_result {
|
if period_size > 0 {
|
||||||
self.pcm = None;
|
let buffer_size = match hwp
|
||||||
return Err(e.into());
|
.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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
self.period_buffer.clear();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ use std::sync::Arc;
|
||||||
use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};
|
use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::AudioFormat, convert::Converter, decoder::AudioPacket, NUM_CHANNELS, SAMPLE_RATE,
|
config::AudioFormat, convert::Converter, decoder::AudioPacket, NUM_CHANNELS,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct GstreamerSink {
|
pub struct GstreamerSink {
|
||||||
|
@ -26,8 +26,8 @@ pub struct GstreamerSink {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Open for GstreamerSink {
|
impl Open for GstreamerSink {
|
||||||
fn open(device: Option<String>, format: AudioFormat) -> Self {
|
fn open(device: Option<String>, format: AudioFormat, sample_rate: u32) -> Self {
|
||||||
info!("Using GStreamer sink with format: {format:?}");
|
info!("Using GStreamer sink with format: {format:?}, sample rate: {sample_rate}");
|
||||||
gst::init().expect("failed to init GStreamer!");
|
gst::init().expect("failed to init GStreamer!");
|
||||||
|
|
||||||
let gst_format = match format {
|
let gst_format = match format {
|
||||||
|
@ -39,7 +39,7 @@ impl Open for GstreamerSink {
|
||||||
AudioFormat::S16 => gst_audio::AUDIO_FORMAT_S16,
|
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()
|
.build()
|
||||||
.expect("Failed to create GStreamer audio format");
|
.expect("Failed to create GStreamer audio format");
|
||||||
let gst_caps = gst_info.to_caps().expect("Failed to create GStreamer caps");
|
let gst_caps = gst_info.to_caps().expect("Failed to create GStreamer caps");
|
||||||
|
|
|
@ -38,11 +38,11 @@ impl ProcessHandler for JackData {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Open for JackSink {
|
impl Open for JackSink {
|
||||||
fn open(client_name: Option<String>, format: AudioFormat) -> Self {
|
fn open(client_name: Option<String>, format: AudioFormat, sample_rate: u32) -> Self {
|
||||||
if format != AudioFormat::F32 {
|
if format != AudioFormat::F32 {
|
||||||
warn!("JACK currently does not support {format:?} output");
|
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_name = client_name.unwrap_or_else(|| "librespot".to_string());
|
||||||
let (client, _status) =
|
let (client, _status) =
|
||||||
|
|
|
@ -20,7 +20,7 @@ pub enum SinkError {
|
||||||
pub type SinkResult<T> = Result<T, SinkError>;
|
pub type SinkResult<T> = Result<T, SinkError>;
|
||||||
|
|
||||||
pub trait Open {
|
pub trait Open {
|
||||||
fn open(_: Option<String>, format: AudioFormat) -> Self;
|
fn open(_: Option<String>, format: AudioFormat, sample_rate: u32) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Sink {
|
pub trait Sink {
|
||||||
|
@ -36,14 +36,18 @@ pub trait Sink {
|
||||||
fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()>;
|
fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type SinkBuilder = fn(Option<String>, AudioFormat) -> Box<dyn Sink>;
|
pub type SinkBuilder = fn(Option<String>, AudioFormat, u32) -> Box<dyn Sink>;
|
||||||
|
|
||||||
pub trait SinkAsBytes {
|
pub trait SinkAsBytes {
|
||||||
fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()>;
|
fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mk_sink<S: Sink + Open + 'static>(device: Option<String>, format: AudioFormat) -> Box<dyn Sink> {
|
fn mk_sink<S: Sink + Open + 'static>(
|
||||||
Box::new(S::open(device, format))
|
device: Option<String>,
|
||||||
|
format: AudioFormat,
|
||||||
|
sample_rate: u32,
|
||||||
|
) -> Box<dyn Sink> {
|
||||||
|
Box::new(S::open(device, format, sample_rate))
|
||||||
}
|
}
|
||||||
|
|
||||||
// reuse code for various backends
|
// reuse code for various backends
|
||||||
|
|
|
@ -42,13 +42,13 @@ pub struct StdoutSink {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Open for StdoutSink {
|
impl Open for StdoutSink {
|
||||||
fn open(file: Option<String>, format: AudioFormat) -> Self {
|
fn open(file: Option<String>, format: AudioFormat, sample_rate: u32) -> Self {
|
||||||
if let Some("?") = file.as_deref() {
|
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");
|
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);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Using StdoutSink (pipe) with format: {:?}", format);
|
info!("Using StdoutSink (pipe) with format: {format:?}, sample rate: {sample_rate}");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
output: None,
|
output: None,
|
||||||
|
|
|
@ -12,14 +12,17 @@ pub enum PortAudioSink<'a> {
|
||||||
F32(
|
F32(
|
||||||
Option<portaudio_rs::stream::Stream<'a, f32, f32>>,
|
Option<portaudio_rs::stream::Stream<'a, f32, f32>>,
|
||||||
StreamParameters<f32>,
|
StreamParameters<f32>,
|
||||||
|
f64,
|
||||||
),
|
),
|
||||||
S32(
|
S32(
|
||||||
Option<portaudio_rs::stream::Stream<'a, i32, i32>>,
|
Option<portaudio_rs::stream::Stream<'a, i32, i32>>,
|
||||||
StreamParameters<i32>,
|
StreamParameters<i32>,
|
||||||
|
f64,
|
||||||
),
|
),
|
||||||
S16(
|
S16(
|
||||||
Option<portaudio_rs::stream::Stream<'a, i16, i16>>,
|
Option<portaudio_rs::stream::Stream<'a, i16, i16>>,
|
||||||
StreamParameters<i16>,
|
StreamParameters<i16>,
|
||||||
|
f64,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,8 +54,8 @@ fn find_output(device: &str) -> Option<DeviceIndex> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Open for PortAudioSink<'a> {
|
impl<'a> Open for PortAudioSink<'a> {
|
||||||
fn open(device: Option<String>, format: AudioFormat) -> PortAudioSink<'a> {
|
fn open(device: Option<String>, format: AudioFormat, sample_rate: u32) -> PortAudioSink<'a> {
|
||||||
info!("Using PortAudio sink with format: {format:?}");
|
info!("Using PortAudio sink with format: {format:?}, sample rate: {sample_rate}");
|
||||||
|
|
||||||
portaudio_rs::initialize().unwrap();
|
portaudio_rs::initialize().unwrap();
|
||||||
|
|
||||||
|
@ -80,13 +83,13 @@ impl<'a> Open for PortAudioSink<'a> {
|
||||||
suggested_latency: latency,
|
suggested_latency: latency,
|
||||||
data: 0.0 as $type,
|
data: 0.0 as $type,
|
||||||
};
|
};
|
||||||
$sink(None, params)
|
$sink(None, params, sample_rate)
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
match format {
|
match format {
|
||||||
AudioFormat::F32 => open_sink!(Self::F32, f32),
|
AudioFormat::F32 => open_sink!(Self::F32, f32, sample_rate as f64),
|
||||||
AudioFormat::S32 => open_sink!(Self::S32, i32),
|
AudioFormat::S32 => open_sink!(Self::S32, i32, sample_rate as f64),
|
||||||
AudioFormat::S16 => open_sink!(Self::S16, i16),
|
AudioFormat::S16 => open_sink!(Self::S16, i16, sample_rate as f64),
|
||||||
_ => {
|
_ => {
|
||||||
unimplemented!("PortAudio currently does not support {format:?} output")
|
unimplemented!("PortAudio currently does not support {format:?} output")
|
||||||
}
|
}
|
||||||
|
@ -97,13 +100,13 @@ impl<'a> Open for PortAudioSink<'a> {
|
||||||
impl<'a> Sink for PortAudioSink<'a> {
|
impl<'a> Sink for PortAudioSink<'a> {
|
||||||
fn start(&mut self) -> SinkResult<()> {
|
fn start(&mut self) -> SinkResult<()> {
|
||||||
macro_rules! start_sink {
|
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() {
|
if $stream.is_none() {
|
||||||
*$stream = Some(
|
*$stream = Some(
|
||||||
Stream::open(
|
Stream::open(
|
||||||
None,
|
None,
|
||||||
Some(*$parameters),
|
Some(*$parameters),
|
||||||
SAMPLE_RATE as f64,
|
*$sample_rate,
|
||||||
FRAMES_PER_BUFFER_UNSPECIFIED,
|
FRAMES_PER_BUFFER_UNSPECIFIED,
|
||||||
StreamFlags::DITHER_OFF, // no need to dither twice; use librespot dithering instead
|
StreamFlags::DITHER_OFF, // no need to dither twice; use librespot dithering instead
|
||||||
None,
|
None,
|
||||||
|
@ -116,9 +119,9 @@ impl<'a> Sink for PortAudioSink<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Self::F32(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) => start_sink!(ref mut stream, ref parameters),
|
Self::S32(stream, parameters, sample_rate) => start_sink!(ref mut stream, ref parameters, ref sample_rate),
|
||||||
Self::S16(stream, parameters) => start_sink!(ref mut stream, ref parameters),
|
Self::S16(stream, parameters, sample_rate) => start_sink!(ref mut stream, ref parameters, ref sample_rate),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -2,7 +2,7 @@ use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};
|
||||||
use crate::config::AudioFormat;
|
use crate::config::AudioFormat;
|
||||||
use crate::convert::Converter;
|
use crate::convert::Converter;
|
||||||
use crate::decoder::AudioPacket;
|
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_binding::{self as pulse, error::PAErr, stream::Direction};
|
||||||
use libpulse_simple_binding::Simple;
|
use libpulse_simple_binding::Simple;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
@ -24,9 +24,6 @@ enum PulseError {
|
||||||
#[error("<PulseAudioSink> Failed to Drain Pulseaudio Buffer, {0}")]
|
#[error("<PulseAudioSink> Failed to Drain Pulseaudio Buffer, {0}")]
|
||||||
DrainFailure(PAErr),
|
DrainFailure(PAErr),
|
||||||
|
|
||||||
#[error("<PulseAudioSink>")]
|
|
||||||
NotConnected,
|
|
||||||
|
|
||||||
#[error("<PulseAudioSink> {0}")]
|
#[error("<PulseAudioSink> {0}")]
|
||||||
OnWrite(PAErr),
|
OnWrite(PAErr),
|
||||||
}
|
}
|
||||||
|
@ -38,40 +35,63 @@ impl From<PulseError> for SinkError {
|
||||||
match e {
|
match e {
|
||||||
DrainFailure(_) | OnWrite(_) => SinkError::OnWrite(es),
|
DrainFailure(_) | OnWrite(_) => SinkError::OnWrite(es),
|
||||||
ConnectionRefused(_) => SinkError::ConnectionRefused(es),
|
ConnectionRefused(_) => SinkError::ConnectionRefused(es),
|
||||||
NotConnected => SinkError::NotConnected(es),
|
|
||||||
InvalidSampleSpec { .. } => SinkError::InvalidParams(es),
|
InvalidSampleSpec { .. } => SinkError::InvalidParams(es),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<AudioFormat> 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 {
|
pub struct PulseAudioSink {
|
||||||
sink: Option<Simple>,
|
sink: Option<Simple>,
|
||||||
device: Option<String>,
|
device: Option<String>,
|
||||||
app_name: String,
|
app_name: String,
|
||||||
stream_desc: String,
|
stream_desc: String,
|
||||||
format: AudioFormat,
|
format: AudioFormat,
|
||||||
|
sample_rate: u32,
|
||||||
|
|
||||||
|
sample_spec: pulse::sample::Spec,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Open for PulseAudioSink {
|
impl Open for PulseAudioSink {
|
||||||
fn open(device: Option<String>, format: AudioFormat) -> Self {
|
fn open(device: Option<String>, format: AudioFormat, sample_rate: u32) -> Self {
|
||||||
let app_name = env::var("PULSE_PROP_application.name").unwrap_or_default();
|
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 stream_desc = env::var("PULSE_PROP_stream.description").unwrap_or_default();
|
||||||
|
|
||||||
let mut actual_format = format;
|
let format = if format == AudioFormat::F64 {
|
||||||
|
|
||||||
if actual_format == AudioFormat::F64 {
|
|
||||||
warn!("PulseAudio currently does not support F64 output");
|
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 {
|
Self {
|
||||||
sink: None,
|
sink: None,
|
||||||
device,
|
device,
|
||||||
app_name,
|
app_name,
|
||||||
stream_desc,
|
stream_desc,
|
||||||
format: actual_format,
|
format,
|
||||||
|
sample_rate,
|
||||||
|
sample_spec,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,31 +99,15 @@ impl Open for PulseAudioSink {
|
||||||
impl Sink for PulseAudioSink {
|
impl Sink for PulseAudioSink {
|
||||||
fn start(&mut self) -> SinkResult<()> {
|
fn start(&mut self) -> SinkResult<()> {
|
||||||
if self.sink.is_none() {
|
if self.sink.is_none() {
|
||||||
// PulseAudio calls S24 and S24_3 different from the rest of the world
|
if !self.sample_spec.is_valid() {
|
||||||
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() {
|
|
||||||
let pulse_error = PulseError::InvalidSampleSpec {
|
let pulse_error = PulseError::InvalidSampleSpec {
|
||||||
pulse_format,
|
pulse_format: self.sample_spec.format,
|
||||||
format: self.format,
|
format: self.format,
|
||||||
channels: NUM_CHANNELS,
|
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(
|
let sink = Simple::new(
|
||||||
|
@ -112,7 +116,7 @@ impl Sink for PulseAudioSink {
|
||||||
Direction::Playback, // Direction.
|
Direction::Playback, // Direction.
|
||||||
self.device.as_deref(), // Our device (sink) name.
|
self.device.as_deref(), // Our device (sink) name.
|
||||||
&self.stream_desc, // Description of our stream.
|
&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 channel map.
|
||||||
None, // Use default buffering attributes.
|
None, // Use default buffering attributes.
|
||||||
)
|
)
|
||||||
|
@ -125,9 +129,10 @@ impl Sink for PulseAudioSink {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop(&mut self) -> SinkResult<()> {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,9 +140,9 @@ impl Sink for PulseAudioSink {
|
||||||
self.sink
|
self.sink
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.and_then(|sink| {
|
.and_then(|sink| {
|
||||||
sink.get_latency()
|
sink.get_latency().ok().map(|micro_sec| {
|
||||||
.ok()
|
(micro_sec.as_secs_f64() * DECODER_SAMPLE_RATE as f64).round() as u64
|
||||||
.map(|micro_sec| (micro_sec.as_secs_f64() * SAMPLE_RATE as f64) as u64)
|
})
|
||||||
})
|
})
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
@ -147,9 +152,9 @@ impl Sink for PulseAudioSink {
|
||||||
|
|
||||||
impl SinkAsBytes for PulseAudioSink {
|
impl SinkAsBytes for PulseAudioSink {
|
||||||
fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
|
fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
|
||||||
let sink = self.sink.as_mut().ok_or(PulseError::NotConnected)?;
|
if let Some(sink) = self.sink.as_mut() {
|
||||||
|
|
||||||
sink.write(data).map_err(PulseError::OnWrite)?;
|
sink.write(data).map_err(PulseError::OnWrite)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ use super::{Sink, SinkError, SinkResult};
|
||||||
use crate::config::AudioFormat;
|
use crate::config::AudioFormat;
|
||||||
use crate::convert::Converter;
|
use crate::convert::Converter;
|
||||||
use crate::decoder::AudioPacket;
|
use crate::decoder::AudioPacket;
|
||||||
use crate::{NUM_CHANNELS, SAMPLE_RATE};
|
use crate::NUM_CHANNELS;
|
||||||
|
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
feature = "rodiojack-backend",
|
feature = "rodiojack-backend",
|
||||||
|
@ -18,16 +18,17 @@ use crate::{NUM_CHANNELS, SAMPLE_RATE};
|
||||||
compile_error!("Rodio JACK backend is currently only supported on linux.");
|
compile_error!("Rodio JACK backend is currently only supported on linux.");
|
||||||
|
|
||||||
#[cfg(feature = "rodio-backend")]
|
#[cfg(feature = "rodio-backend")]
|
||||||
pub fn mk_rodio(device: Option<String>, format: AudioFormat) -> Box<dyn Sink> {
|
pub fn mk_rodio(device: Option<String>, format: AudioFormat, sample_rate: u32) -> Box<dyn Sink> {
|
||||||
Box::new(open(cpal::default_host(), device, format))
|
Box::new(open(cpal::default_host(), device, format, sample_rate))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "rodiojack-backend")]
|
#[cfg(feature = "rodiojack-backend")]
|
||||||
pub fn mk_rodiojack(device: Option<String>, format: AudioFormat) -> Box<dyn Sink> {
|
pub fn mk_rodiojack(device: Option<String>, format: AudioFormat, sample_rate: u32) -> Box<dyn Sink> {
|
||||||
Box::new(open(
|
Box::new(open(
|
||||||
cpal::host_from_id(cpal::HostId::Jack).unwrap(),
|
cpal::host_from_id(cpal::HostId::Jack).unwrap(),
|
||||||
device,
|
device,
|
||||||
format,
|
format,
|
||||||
|
sample_rate,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +63,7 @@ impl From<RodioError> for SinkError {
|
||||||
pub struct RodioSink {
|
pub struct RodioSink {
|
||||||
rodio_sink: rodio::Sink,
|
rodio_sink: rodio::Sink,
|
||||||
format: AudioFormat,
|
format: AudioFormat,
|
||||||
|
sample_rate: u32,
|
||||||
_stream: rodio::OutputStream,
|
_stream: rodio::OutputStream,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,7 +166,7 @@ fn create_sink(
|
||||||
Ok((sink, stream))
|
Ok((sink, stream))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open(host: cpal::Host, device: Option<String>, format: AudioFormat) -> RodioSink {
|
pub fn open(host: cpal::Host, device: Option<String>, format: AudioFormat, sample_rate: u32) -> RodioSink {
|
||||||
info!(
|
info!(
|
||||||
"Using Rodio sink with format {format:?} and cpal host: {}",
|
"Using Rodio sink with format {format:?} and cpal host: {}",
|
||||||
host.id().name()
|
host.id().name()
|
||||||
|
@ -180,6 +182,7 @@ pub fn open(host: cpal::Host, device: Option<String>, format: AudioFormat) -> Ro
|
||||||
RodioSink {
|
RodioSink {
|
||||||
rodio_sink: sink,
|
rodio_sink: sink,
|
||||||
format,
|
format,
|
||||||
|
sample_rate,
|
||||||
_stream: stream,
|
_stream: stream,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -205,7 +208,7 @@ impl Sink for RodioSink {
|
||||||
let samples_f32: &[f32] = &converter.f64_to_f32(samples);
|
let samples_f32: &[f32] = &converter.f64_to_f32(samples);
|
||||||
let source = rodio::buffer::SamplesBuffer::new(
|
let source = rodio::buffer::SamplesBuffer::new(
|
||||||
NUM_CHANNELS as u16,
|
NUM_CHANNELS as u16,
|
||||||
SAMPLE_RATE,
|
self.sample_rate,
|
||||||
samples_f32,
|
samples_f32,
|
||||||
);
|
);
|
||||||
self.rodio_sink.append(source);
|
self.rodio_sink.append(source);
|
||||||
|
@ -214,7 +217,7 @@ impl Sink for RodioSink {
|
||||||
let samples_s16: &[i16] = &converter.f64_to_s16(samples);
|
let samples_s16: &[i16] = &converter.f64_to_s16(samples);
|
||||||
let source = rodio::buffer::SamplesBuffer::new(
|
let source = rodio::buffer::SamplesBuffer::new(
|
||||||
NUM_CHANNELS as u16,
|
NUM_CHANNELS as u16,
|
||||||
SAMPLE_RATE,
|
self.sample_rate,
|
||||||
samples_s16,
|
samples_s16,
|
||||||
);
|
);
|
||||||
self.rodio_sink.append(source);
|
self.rodio_sink.append(source);
|
||||||
|
|
|
@ -14,8 +14,8 @@ pub enum SdlSink {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Open for SdlSink {
|
impl Open for SdlSink {
|
||||||
fn open(device: Option<String>, format: AudioFormat) -> Self {
|
fn open(device: Option<String>, format: AudioFormat, sample_rate: u32) -> Self {
|
||||||
info!("Using SDL sink with format: {:?}", format);
|
info!("Using SDL sink with format: {format:?}, sample rate: {sample_rate}");
|
||||||
|
|
||||||
if device.is_some() {
|
if device.is_some() {
|
||||||
warn!("SDL sink does not support specifying a device name");
|
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");
|
.expect("could not initialize SDL audio subsystem");
|
||||||
|
|
||||||
let desired_spec = AudioSpecDesired {
|
let desired_spec = AudioSpecDesired {
|
||||||
freq: Some(SAMPLE_RATE as i32),
|
freq: Some(sample_rate as i32),
|
||||||
channels: Some(NUM_CHANNELS),
|
channels: Some(NUM_CHANNELS),
|
||||||
samples: None,
|
samples: None,
|
||||||
};
|
};
|
||||||
|
|
|
@ -66,13 +66,13 @@ pub struct SubprocessSink {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Open for SubprocessSink {
|
impl Open for SubprocessSink {
|
||||||
fn open(shell_command: Option<String>, format: AudioFormat) -> Self {
|
fn open(shell_command: Option<String>, format: AudioFormat, sample_rate: u32) -> Self {
|
||||||
if let Some("?") = shell_command.as_deref() {
|
if let Some("?") = shell_command.as_deref() {
|
||||||
println!("\nUsage:\n\nOutput to a Subprocess:\n\n\t--backend subprocess --device {{shell_command}}\n");
|
println!("\nUsage:\n\nOutput to a Subprocess:\n\n\t--backend subprocess --device {{shell_command}}\n");
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Using SubprocessSink with format: {:?}", format);
|
info!("Using SubprocessSink with format: {format:?}, sample rate: {sample_rate}");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
shell_command,
|
shell_command,
|
||||||
|
|
Loading…
Reference in a new issue