librespot/playback/src/audio_backend/rodio.rs

233 lines
6.9 KiB
Rust
Raw Normal View History

2019-03-20 13:24:03 +00:00
use std::process::exit;
use std::thread;
use std::time::Duration;
2019-03-20 13:24:03 +00:00
use cpal::traits::{DeviceTrait, HostTrait};
use thiserror::Error;
use super::{Sink, SinkError, SinkResult};
use crate::config::AudioFormat;
Implement dithering (#694) Dithering lowers digital-to-analog conversion ("requantization") error, linearizing output, lowering distortion and replacing it with a constant, fixed noise level, which is more pleasant to the ear than the distortion. Guidance: - On S24, S24_3 and S24, the default is to use triangular dithering. Depending on personal preference you may use Gaussian dithering instead; it's not as good objectively, but it may be preferred subjectively if you are looking for a more "analog" sound akin to tape hiss. - Advanced users who know that they have a DAC without noise shaping have a third option: high-passed dithering, which is like triangular dithering except that it moves dithering noise up in frequency where it is less audible. Note: 99% of DACs are of delta-sigma design with noise shaping, so unless you have a multibit / R2R DAC, or otherwise know what you are doing, this is not for you. - Don't dither or shape noise on S32 or F32. On F32 it's not supported anyway (there are no integer conversions and so no rounding errors) and on S32 the noise level is so far down that it is simply inaudible even after volume normalisation and control. New command line option: --dither DITHER Specify the dither algorithm to use - [none, gpdf, tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16 S24, S24_3 and 'none' for other formats. Notes: This PR also features some opportunistic improvements. Worthy of mention are: - matching reference Vorbis sample conversion techniques for lower noise - a cleanup of the convert API
2021-05-26 19:19:17 +00:00
use crate::convert::Converter;
2021-04-13 08:29:34 +00:00
use crate::decoder::AudioPacket;
use crate::{NUM_CHANNELS, SAMPLE_RATE};
#[cfg(all(
feature = "rodiojack-backend",
not(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))
))]
compile_error!("Rodio JACK backend is currently only supported on linux.");
#[cfg(feature = "rodio-backend")]
pub fn mk_rodio(device: Option<String>, format: AudioFormat) -> Box<dyn Sink> {
Box::new(open(cpal::default_host(), device, format))
}
#[cfg(feature = "rodiojack-backend")]
pub fn mk_rodiojack(device: Option<String>, format: AudioFormat) -> Box<dyn Sink> {
Box::new(open(
cpal::host_from_id(cpal::HostId::Jack).unwrap(),
device,
format,
))
}
#[derive(Debug, Error)]
pub enum RodioError {
#[error("<RodioSink> No Device Available")]
NoDeviceAvailable,
#[error("<RodioSink> device \"{0}\" is Not Available")]
DeviceNotAvailable(String),
#[error("<RodioSink> Play Error: {0}")]
PlayError(#[from] rodio::PlayError),
#[error("<RodioSink> Stream Error: {0}")]
StreamError(#[from] rodio::StreamError),
#[error("<RodioSink> Cannot Get Audio Devices: {0}")]
DevicesError(#[from] cpal::DevicesError),
#[error("<RodioSink> {0}")]
Samples(String),
}
impl From<RodioError> for SinkError {
fn from(e: RodioError) -> SinkError {
use RodioError::*;
let es = e.to_string();
match e {
StreamError(_) | PlayError(_) | Samples(_) => SinkError::OnWrite(es),
NoDeviceAvailable | DeviceNotAvailable(_) => SinkError::ConnectionRefused(es),
DevicesError(_) => SinkError::InvalidParams(es),
}
}
}
2019-03-20 13:24:03 +00:00
pub struct RodioSink {
rodio_sink: rodio::Sink,
format: AudioFormat,
_stream: rodio::OutputStream,
2019-03-20 13:24:03 +00:00
}
fn list_formats(device: &rodio::Device) {
match device.default_output_config() {
Ok(cfg) => {
debug!(" Default config:");
debug!(" {:?}", cfg);
}
Err(e) => {
// Use loglevel debug, since even the output is only debug
debug!("Error getting default rodio::Sink config: {}", e);
}
};
match device.supported_output_configs() {
Ok(mut cfgs) => {
if let Some(first) = cfgs.next() {
debug!(" Available configs:");
debug!(" {:?}", first);
} else {
return;
}
for cfg in cfgs {
debug!(" {:?}", cfg);
}
}
Err(e) => {
debug!("Error getting supported rodio::Sink configs: {}", e);
}
}
}
fn list_outputs(host: &cpal::Host) -> Result<(), cpal::DevicesError> {
let mut default_device_name = None;
if let Some(default_device) = host.default_output_device() {
default_device_name = default_device.name().ok();
println!(
"Default Audio Device:\n {}",
default_device_name.as_deref().unwrap_or("[unknown name]")
);
list_formats(&default_device);
println!("Other Available Audio Devices:");
} else {
warn!("No default device was found");
}
for device in host.output_devices()? {
match device.name() {
Ok(name) if Some(&name) == default_device_name.as_ref() => (),
Ok(name) => {
println!(" {}", name);
list_formats(&device);
}
Err(e) => {
warn!("Cannot get device name: {}", e);
println!(" [unknown name]");
list_formats(&device);
}
2019-03-20 13:24:03 +00:00
}
}
Ok(())
2019-03-20 13:24:03 +00:00
}
fn create_sink(
host: &cpal::Host,
device: Option<String>,
) -> Result<(rodio::Sink, rodio::OutputStream), RodioError> {
let rodio_device = match device {
Some(ask) if &ask == "?" => {
let exit_code = match list_outputs(host) {
Ok(()) => 0,
Err(e) => {
error!("{}", e);
1
}
};
exit(exit_code)
}
Some(device_name) => {
host.output_devices()?
.find(|d| d.name().ok().map_or(false, |name| name == device_name)) // Ignore devices for which getting name fails
.ok_or(RodioError::DeviceNotAvailable(device_name))?
2019-03-20 13:24:03 +00:00
}
None => host
.default_output_device()
.ok_or(RodioError::NoDeviceAvailable)?,
};
let name = rodio_device.name().ok();
info!(
"Using audio device: {}",
name.as_deref().unwrap_or("[unknown name]")
);
let (stream, handle) = rodio::OutputStream::try_from_device(&rodio_device)?;
let sink = rodio::Sink::try_new(&handle)?;
Ok((sink, stream))
}
pub fn open(host: cpal::Host, device: Option<String>, format: AudioFormat) -> RodioSink {
info!(
"Using Rodio sink with format {:?} and cpal host: {}",
format,
host.id().name()
);
if format != AudioFormat::S16 && format != AudioFormat::F32 {
unimplemented!("Rodio currently only supports F32 and S16 formats");
}
let (sink, stream) = create_sink(&host, device).unwrap();
2019-03-20 13:24:03 +00:00
debug!("Rodio sink was created");
RodioSink {
rodio_sink: sink,
format,
_stream: stream,
2019-03-20 13:24:03 +00:00
}
}
impl Sink for RodioSink {
fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> {
let samples = packet
.samples()
.map_err(|e| RodioError::Samples(e.to_string()))?;
match self.format {
AudioFormat::F32 => {
let samples_f32: &[f32] = &converter.f64_to_f32(samples);
let source = rodio::buffer::SamplesBuffer::new(
NUM_CHANNELS as u16,
SAMPLE_RATE,
samples_f32,
);
self.rodio_sink.append(source);
}
AudioFormat::S16 => {
let samples_s16: &[i16] = &converter.f64_to_s16(samples);
let source = rodio::buffer::SamplesBuffer::new(
NUM_CHANNELS as u16,
SAMPLE_RATE,
samples_s16,
);
self.rodio_sink.append(source);
}
_ => unreachable!(),
};
// Chunk sizes seem to be about 256 to 3000 ish items long.
// Assuming they're on average 1628 then a half second buffer is:
// 44100 elements --> about 27 chunks
while self.rodio_sink.len() > 26 {
// sleep and wait for rodio to drain a bit
thread::sleep(Duration::from_millis(10));
}
2019-03-20 13:24:03 +00:00
Ok(())
}
}
impl RodioSink {
#[allow(dead_code)]
pub const NAME: &'static str = "rodio";
}