Add command-line option to set F32 or S16 bit output

Usage: `--format {F32|S16}`. Default is F32.

 - Implemented for all backends, except for JACK audio which itself
 only supports 32-bit output at this time. Setting JACK audio to S16
 will panic and instruct the user to set output to F32.

 - The F32 default works fine for Rodio on macOS, but not on Raspian 10
 with Alsa as host. Therefore users on Linux systems are warned to set
 output to S16 in case of garbled sound with Rodio. This seems an issue
 with cpal incorrectly detecting the output stream format.

 - While at it, DRY up lots of code in the backends and by that virtue,
 also enable OggData passthrough on the subprocess backend.

 - I tested Rodio, ALSA, pipe and subprocess quite a bit, and call on
 others to join in and test the other backends.
This commit is contained in:
Roderick van Domburg 2021-03-12 23:05:38 +01:00
parent 1672eb87ab
commit 5257be7824
15 changed files with 504 additions and 314 deletions

View file

@ -58,6 +58,13 @@ impl AudioPacket {
AudioPacket::OggData(d) => d.is_empty(),
}
}
pub fn f32_to_s16(samples: &[f32]) -> Vec<i16> {
samples
.iter()
.map(|sample| (*sample as f64 * (0x7FFF as f64 + 0.5) - 0.5) as i16)
.collect()
}
}
#[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))]

View file

@ -35,7 +35,7 @@ sdl2 = { version = "0.34", optional = true }
gstreamer = { version = "0.16", optional = true }
gstreamer-app = { version = "0.16", optional = true }
glib = { version = "0.10", optional = true }
zerocopy = { version = "0.3", optional = true }
zerocopy = { version = "0.3" }
[features]
alsa-backend = ["alsa"]
@ -45,4 +45,4 @@ jackaudio-backend = ["jack"]
rodiojack-backend = ["rodio", "cpal/jack"]
rodio-backend = ["rodio", "cpal"]
sdl-backend = ["sdl2"]
gstreamer-backend = ["gstreamer", "gstreamer-app", "glib", "zerocopy"]
gstreamer-backend = ["gstreamer", "gstreamer-app", "glib" ]

View file

@ -1,18 +1,21 @@
use super::{Open, Sink};
use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use crate::player::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE};
use alsa::device_name::HintIter;
use alsa::pcm::{Access, Format, Frames, HwParams, PCM};
use alsa::{Direction, Error, ValueOr};
use std::cmp::min;
use std::ffi::CString;
use std::io;
use std::process::exit;
use std::{io, mem};
const PREFERRED_PERIOD_SIZE: Frames = 11025; // Period of roughly 125ms
const BUFFERED_PERIODS: Frames = 4;
const BUFFERED_LATENCY: f32 = 0.125; // seconds
const BUFFERED_PERIODS: u8 = 4;
pub struct AlsaSink {
pcm: Option<PCM>,
format: AudioFormat,
device: String,
buffer: Vec<f32>,
}
@ -34,25 +37,28 @@ fn list_outputs() {
}
}
fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box<Error>> {
fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box<Error>> {
let pcm = PCM::new(dev_name, Direction::Playback, false)?;
let mut period_size = PREFERRED_PERIOD_SIZE;
let (alsa_format, sample_size) = match format {
AudioFormat::F32 => (Format::float(), mem::size_of::<f32>()),
AudioFormat::S16 => (Format::s16(), mem::size_of::<i16>()),
};
// http://www.linuxjournal.com/article/6735?page=0,1#N0x19ab2890.0x19ba78d8
// latency = period_size * periods / (rate * bytes_per_frame)
// For stereo samples encoded as 32-bit floats, one frame has a length of eight bytes.
// 500ms = buffer_size / (44100 * 8)
// buffer_size_bytes = 0.5 * 44100 / 8
// buffer_size_frames = 0.5 * 44100 = 22050
{
// Set hardware parameters: 44100 Hz / Stereo / 32-bit float
let hwp = HwParams::any(&pcm)?;
// For stereo samples encoded as 32-bit float, one frame has a length of eight bytes.
let mut period_size = ((SAMPLES_PER_SECOND * sample_size as u32) as f32
* (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as i32;
// Set hardware parameters: 44100 Hz / stereo / 32-bit float or 16-bit signed integer
{
let hwp = HwParams::any(&pcm)?;
hwp.set_access(Access::RWInterleaved)?;
hwp.set_format(Format::float())?;
hwp.set_rate(44100, ValueOr::Nearest)?;
hwp.set_channels(2)?;
hwp.set_format(alsa_format)?;
hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest)?;
hwp.set_channels(NUM_CHANNELS as u32)?;
period_size = hwp.set_period_size_near(period_size, ValueOr::Greater)?;
hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS)?;
hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS as i32)?;
pcm.hw_params(&hwp)?;
let swp = pcm.sw_params_current()?;
@ -64,12 +70,12 @@ fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box<Error>> {
}
impl Open for AlsaSink {
fn open(device: Option<String>) -> AlsaSink {
info!("Using alsa sink");
fn open(device: Option<String>, format: AudioFormat) -> AlsaSink {
info!("Using Alsa sink with format: {:?}", format);
let name = match device.as_ref().map(AsRef::as_ref) {
Some("?") => {
println!("Listing available alsa outputs");
println!("Listing available Alsa outputs:");
list_outputs();
exit(0)
}
@ -80,6 +86,7 @@ impl Open for AlsaSink {
AlsaSink {
pcm: None,
format: format,
device: name,
buffer: vec![],
}
@ -89,12 +96,13 @@ impl Open for AlsaSink {
impl Sink for AlsaSink {
fn start(&mut self) -> io::Result<()> {
if self.pcm.is_none() {
let pcm = open_device(&self.device);
let pcm = open_device(&self.device, self.format);
match pcm {
Ok((p, period_size)) => {
self.pcm = Some(p);
// Create a buffer for all samples for a full period
self.buffer = Vec::with_capacity((period_size * 2) as usize);
self.buffer =
Vec::with_capacity((period_size * BUFFERED_PERIODS as i32) as usize);
}
Err(e) => {
error!("Alsa error PCM open {}", e);
@ -111,14 +119,10 @@ impl Sink for AlsaSink {
fn stop(&mut self) -> io::Result<()> {
{
let pcm = self.pcm.as_mut().unwrap();
// Write any leftover data in the period buffer
// before draining the actual buffer
let io = pcm.io_f32().unwrap();
match io.writei(&self.buffer[..]) {
Ok(_) => (),
Err(err) => pcm.try_recover(err, false).unwrap(),
}
self.write_buf().expect("could not flush buffer");
let pcm = self.pcm.as_mut().unwrap();
pcm.drain().unwrap();
}
self.pcm = None;
@ -137,12 +141,7 @@ impl Sink for AlsaSink {
.extend_from_slice(&data[processed_data..processed_data + data_to_buffer]);
processed_data += data_to_buffer;
if self.buffer.len() == self.buffer.capacity() {
let pcm = self.pcm.as_mut().unwrap();
let io = pcm.io_f32().unwrap();
match io.writei(&self.buffer) {
Ok(_) => (),
Err(err) => pcm.try_recover(err, false).unwrap(),
}
self.write_buf().expect("could not append to buffer");
self.buffer.clear();
}
}
@ -150,3 +149,26 @@ impl Sink for AlsaSink {
Ok(())
}
}
impl AlsaSink {
fn write_buf(&mut self) -> io::Result<()> {
let pcm = self.pcm.as_mut().unwrap();
let io_result = match self.format {
AudioFormat::F32 => {
let io = pcm.io_f32().unwrap();
io.writei(&self.buffer)
}
AudioFormat::S16 => {
let io = pcm.io_i16().unwrap();
let buf_s16: Vec<i16> = AudioPacket::f32_to_s16(&self.buffer);
io.writei(&buf_s16[..])
}
};
match io_result {
Ok(_) => (),
Err(err) => pcm.try_recover(err, false).unwrap(),
};
Ok(())
}
}

View file

@ -1,21 +1,29 @@
use super::{Open, Sink};
use super::{Open, Sink, SinkAsBytes};
use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
use gst::prelude::*;
use gst::*;
use std::sync::mpsc::{sync_channel, SyncSender};
use std::{io, thread};
use zerocopy::*;
use zerocopy::AsBytes;
#[allow(dead_code)]
pub struct GstreamerSink {
tx: SyncSender<Vec<u8>>,
pipeline: gst::Pipeline,
format: AudioFormat,
}
impl Open for GstreamerSink {
fn open(device: Option<String>) -> GstreamerSink {
gst::init().expect("Failed to init gstreamer!");
let pipeline_str_preamble = r#"appsrc caps="audio/x-raw,format=F32,layout=interleaved,channels=2,rate=44100" block=true max-bytes=4096 name=appsrc0 "#;
fn open(device: Option<String>, format: AudioFormat) -> GstreamerSink {
info!("Using GStreamer sink with format: {:?}", format);
gst::init().expect("failed to init GStreamer!");
let pipeline_str_preamble = format!(
r#"appsrc caps="audio/x-raw,format={:?},layout=interleaved,channels={},rate={}" block=true max-bytes=4096 name=appsrc0 "#,
format, NUM_CHANNELS, SAMPLE_RATE
);
let pipeline_str_rest = r#" ! audioconvert ! autoaudiosink"#;
let pipeline_str: String = match device {
Some(x) => format!("{}{}", pipeline_str_preamble, x),
@ -27,25 +35,25 @@ impl Open for GstreamerSink {
let pipelinee = gst::parse_launch(&*pipeline_str).expect("Couldn't launch pipeline; likely a GStreamer issue or an error in the pipeline string you specified in the 'device' argument to librespot.");
let pipeline = pipelinee
.dynamic_cast::<gst::Pipeline>()
.expect("Couldn't cast pipeline element at runtime!");
let bus = pipeline.get_bus().expect("Couldn't get bus from pipeline");
.expect("couldn't cast pipeline element at runtime!");
let bus = pipeline.get_bus().expect("couldn't get bus from pipeline");
let mainloop = glib::MainLoop::new(None, false);
let appsrce: gst::Element = pipeline
.get_by_name("appsrc0")
.expect("Couldn't get appsrc from pipeline");
.expect("couldn't get appsrc from pipeline");
let appsrc: gst_app::AppSrc = appsrce
.dynamic_cast::<gst_app::AppSrc>()
.expect("Couldn't cast AppSrc element at runtime!");
.expect("couldn't cast AppSrc element at runtime!");
let bufferpool = gst::BufferPool::new();
let appsrc_caps = appsrc.get_caps().expect("Couldn't get appsrc caps");
let appsrc_caps = appsrc.get_caps().expect("couldn't get appsrc caps");
let mut conf = bufferpool.get_config();
conf.set_params(Some(&appsrc_caps), 8192, 0, 0);
bufferpool
.set_config(conf)
.expect("Couldn't configure the buffer pool");
.expect("couldn't configure the buffer pool");
bufferpool
.set_active(true)
.expect("Couldn't activate buffer pool");
.expect("couldn't activate buffer pool");
let (tx, rx) = sync_channel::<Vec<u8>>(128);
thread::spawn(move || {
@ -57,7 +65,7 @@ impl Open for GstreamerSink {
mutbuf.set_size(data.len());
mutbuf
.copy_from_slice(0, data.as_bytes())
.expect("Failed to copy from slice");
.expect("failed to copy from slice");
let _eat = appsrc.push_buffer(okbuffer);
}
}
@ -83,33 +91,32 @@ impl Open for GstreamerSink {
glib::Continue(true)
})
.expect("Failed to add bus watch");
.expect("failed to add bus watch");
thread_mainloop.run();
});
pipeline
.set_state(gst::State::Playing)
.expect("Unable to set the pipeline to the `Playing` state");
.expect("unable to set the pipeline to the `Playing` state");
GstreamerSink {
tx: tx,
pipeline: pipeline,
format: format,
}
}
}
impl Sink for GstreamerSink {
fn start(&mut self) -> io::Result<()> {
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
Ok(())
}
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
start_stop_noop!();
sink_as_bytes!();
}
impl SinkAsBytes for GstreamerSink {
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> {
// Copy expensively (in to_vec()) to avoid thread synchronization
let deighta: &[u8] = packet.samples().as_bytes();
self.tx
.send(deighta.to_vec())
.send(data.to_vec())
.expect("tx send failed in write function");
Ok(())
}

View file

@ -1,10 +1,12 @@
use super::{Open, Sink};
use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use crate::player::NUM_CHANNELS;
use jack::{
AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope,
};
use std::io;
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
use std::{io, mem};
pub struct JackSink {
send: SyncSender<f32>,
@ -39,8 +41,15 @@ impl ProcessHandler for JackData {
}
impl Open for JackSink {
fn open(client_name: Option<String>) -> JackSink {
info!("Using jack sink!");
fn open(client_name: Option<String>, format: AudioFormat) -> JackSink {
info!("Using JACK sink with format {:?}", format);
if format != AudioFormat::F32 {
panic!(
"JACK sink only supports 32-bit floating point output. Use `--format {:?}`",
AudioFormat::F32
);
}
let client_name = client_name.unwrap_or("librespot".to_string());
let (client, _status) =
@ -48,7 +57,7 @@ impl Open for JackSink {
let ch_r = client.register_port("out_0", AudioOut::default()).unwrap();
let ch_l = client.register_port("out_1", AudioOut::default()).unwrap();
// buffer for samples from librespot (~10ms)
let (tx, rx) = sync_channel(2 * 1024 * 4);
let (tx, rx) = sync_channel::<f32>(NUM_CHANNELS as usize * 1024 * mem::size_of::<f32>());
let jack_data = JackData {
rec: rx,
port_l: ch_l,
@ -64,13 +73,7 @@ impl Open for JackSink {
}
impl Sink for JackSink {
fn start(&mut self) -> io::Result<()> {
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
Ok(())
}
start_stop_noop!();
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
for s in packet.samples().iter() {

View file

@ -1,8 +1,9 @@
use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use std::io;
pub trait Open {
fn open(_: Option<String>) -> Self;
fn open(_: Option<String>, format: AudioFormat) -> Self;
}
pub trait Sink {
@ -11,8 +12,42 @@ pub trait Sink {
fn write(&mut self, packet: &AudioPacket) -> io::Result<()>;
}
fn mk_sink<S: Sink + Open + 'static>(device: Option<String>) -> Box<dyn Sink> {
Box::new(S::open(device))
pub trait SinkAsBytes {
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()>;
}
fn mk_sink<S: Sink + Open + 'static>(device: Option<String>, format: AudioFormat) -> Box<dyn Sink> {
Box::new(S::open(device, format))
}
// reuse code for various backends
macro_rules! sink_as_bytes {
() => {
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
use zerocopy::AsBytes;
match packet {
AudioPacket::Samples(samples) => match self.format {
AudioFormat::F32 => self.write_bytes(samples.as_bytes()),
AudioFormat::S16 => {
let samples_s16 = AudioPacket::f32_to_s16(samples);
self.write_bytes(samples_s16.as_bytes())
}
},
AudioPacket::OggData(samples) => self.write_bytes(samples),
}
}
};
}
macro_rules! start_stop_noop {
() => {
fn start(&mut self) -> io::Result<()> {
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
Ok(())
}
};
}
#[cfg(feature = "alsa-backend")]
@ -68,7 +103,10 @@ use self::pipe::StdoutSink;
mod subprocess;
use self::subprocess::SubprocessSink;
pub const BACKENDS: &'static [(&'static str, fn(Option<String>) -> Box<dyn Sink>)] = &[
pub const BACKENDS: &'static [(
&'static str,
fn(Option<String>, AudioFormat) -> Box<dyn Sink>,
)] = &[
#[cfg(feature = "alsa-backend")]
("alsa", mk_sink::<AlsaSink>),
#[cfg(feature = "portaudio-backend")]
@ -92,7 +130,7 @@ pub const BACKENDS: &'static [(&'static str, fn(Option<String>) -> Box<dyn Sink>
("subprocess", mk_sink::<SubprocessSink>),
];
pub fn find(name: Option<String>) -> Option<fn(Option<String>) -> Box<dyn Sink>> {
pub fn find(name: Option<String>) -> Option<fn(Option<String>, AudioFormat) -> Box<dyn Sink>> {
if let Some(name) = name {
BACKENDS
.iter()

View file

@ -1,46 +1,39 @@
use super::{Open, Sink};
use super::{Open, Sink, SinkAsBytes};
use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use std::fs::OpenOptions;
use std::io::{self, Write};
use std::mem;
use std::slice;
pub struct StdoutSink(Box<dyn Write>);
pub struct StdoutSink {
output: Box<dyn Write>,
format: AudioFormat,
}
impl Open for StdoutSink {
fn open(path: Option<String>) -> StdoutSink {
if let Some(path) = path {
let file = OpenOptions::new().write(true).open(path).unwrap();
StdoutSink(Box::new(file))
} else {
StdoutSink(Box::new(io::stdout()))
fn open(path: Option<String>, format: AudioFormat) -> StdoutSink {
info!("Using pipe sink with format: {:?}", format);
let output: Box<dyn Write> = match path {
Some(path) => Box::new(OpenOptions::new().write(true).open(path).unwrap()),
_ => Box::new(io::stdout()),
};
StdoutSink {
output: output,
format: format,
}
}
}
impl Sink for StdoutSink {
fn start(&mut self) -> io::Result<()> {
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
Ok(())
}
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
let data: &[u8] = match packet {
AudioPacket::Samples(data) => unsafe {
slice::from_raw_parts(
data.as_ptr() as *const u8,
data.len() * mem::size_of::<f32>(),
)
},
AudioPacket::OggData(data) => data,
};
self.0.write_all(data)?;
self.0.flush()?;
start_stop_noop!();
sink_as_bytes!();
}
impl SinkAsBytes for StdoutSink {
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> {
self.output.write_all(data)?;
self.output.flush()?;
Ok(())
}
}

View file

@ -1,5 +1,7 @@
use super::{Open, Sink};
use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
use portaudio_rs;
use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo};
use portaudio_rs::stream::*;
@ -7,10 +9,16 @@ use std::io;
use std::process::exit;
use std::time::Duration;
pub struct PortAudioSink<'a>(
pub enum PortAudioSink<'a> {
F32(
Option<portaudio_rs::stream::Stream<'a, f32, f32>>,
StreamParameters<f32>,
);
),
S16(
Option<portaudio_rs::stream::Stream<'a, i16, i16>>,
StreamParameters<i16>,
),
}
fn output_devices() -> Box<dyn Iterator<Item = (DeviceIndex, DeviceInfo)>> {
let count = portaudio_rs::device::get_count().unwrap();
@ -40,8 +48,8 @@ fn find_output(device: &str) -> Option<DeviceIndex> {
}
impl<'a> Open for PortAudioSink<'a> {
fn open(device: Option<String>) -> PortAudioSink<'a> {
debug!("Using PortAudio sink");
fn open(device: Option<String>, format: AudioFormat) -> PortAudioSink<'a> {
info!("Using PortAudio sink with format: {:?}", format);
portaudio_rs::initialize().unwrap();
@ -53,7 +61,7 @@ impl<'a> Open for PortAudioSink<'a> {
Some(device) => find_output(device),
None => get_default_output_index(),
}
.expect("Could not find device");
.expect("could not find device");
let info = portaudio_rs::device::get_info(device_idx);
let latency = match info {
@ -61,25 +69,34 @@ impl<'a> Open for PortAudioSink<'a> {
None => Duration::new(0, 0),
};
macro_rules! open_sink {
($sink: expr, $data: expr) => {{
let params = StreamParameters {
device: device_idx,
channel_count: 2,
channel_count: NUM_CHANNELS as u32,
suggested_latency: latency,
data: 0.0,
data: $data,
};
PortAudioSink(None, params)
$sink(None, params)
}};
}
match format {
AudioFormat::F32 => open_sink!(PortAudioSink::F32, 0.0),
AudioFormat::S16 => open_sink!(PortAudioSink::S16, 0),
}
}
}
impl<'a> Sink for PortAudioSink<'a> {
fn start(&mut self) -> io::Result<()> {
if self.0.is_none() {
self.0 = Some(
macro_rules! start_sink {
($stream: ident, $parameters: ident) => {{
if $stream.is_none() {
*$stream = Some(
Stream::open(
None,
Some(self.1),
44100.0,
Some(*$parameters),
SAMPLE_RATE as f64,
FRAMES_PER_BUFFER_UNSPECIFIED,
StreamFlags::empty(),
None,
@ -87,20 +104,52 @@ impl<'a> Sink for PortAudioSink<'a> {
.unwrap(),
);
}
$stream.as_mut().unwrap().start().unwrap()
}};
}
match self {
PortAudioSink::F32(stream, parameters) => start_sink!(stream, parameters),
PortAudioSink::S16(stream, parameters) => start_sink!(stream, parameters),
};
self.0.as_mut().unwrap().start().unwrap();
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
self.0.as_mut().unwrap().stop().unwrap();
self.0 = None;
macro_rules! stop_sink {
($stream: expr) => {{
$stream.as_mut().unwrap().stop().unwrap();
*$stream = None;
}};
}
match self {
PortAudioSink::F32(stream, _parameters) => stop_sink!(stream),
PortAudioSink::S16(stream, _parameters) => stop_sink!(stream),
};
Ok(())
}
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
match self.0.as_mut().unwrap().write(packet.samples()) {
macro_rules! write_sink {
($stream: expr, $samples: expr) => {
$stream.as_mut().unwrap().write($samples)
};
}
let result = match self {
PortAudioSink::F32(stream, _parameters) => {
let samples = packet.samples();
write_sink!(stream, &samples)
}
PortAudioSink::S16(stream, _parameters) => {
let samples_s16: Vec<i16> = AudioPacket::f32_to_s16(packet.samples());
write_sink!(stream, &samples_s16)
}
};
match result {
Ok(_) => (),
Err(portaudio_rs::PaError::OutputUnderflowed) => error!("PortAudio write underflow"),
Err(e) => panic!("PA Error {}", e),
Err(e) => panic!("PortAudio error {}", e),
};
Ok(())

View file

@ -1,9 +1,10 @@
use super::{Open, Sink};
use super::{Open, Sink, SinkAsBytes};
use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
use libpulse_binding::{self as pulse, stream::Direction};
use libpulse_simple_binding::Simple;
use std::io;
use std::mem;
const APP_NAME: &str = "librespot";
const STREAM_NAME: &str = "Spotify endpoint";
@ -12,16 +13,22 @@ pub struct PulseAudioSink {
s: Option<Simple>,
ss: pulse::sample::Spec,
device: Option<String>,
format: AudioFormat,
}
impl Open for PulseAudioSink {
fn open(device: Option<String>) -> PulseAudioSink {
debug!("Using PulseAudio sink");
fn open(device: Option<String>, format: AudioFormat) -> PulseAudioSink {
info!("Using PulseAudio sink with format: {:?}", format);
let pulse_format = match format {
AudioFormat::F32 => pulse::sample::Format::F32le,
AudioFormat::S16 => pulse::sample::Format::S16le,
};
let ss = pulse::sample::Spec {
format: pulse::sample::Format::F32le,
channels: 2, // stereo
rate: 44100,
format: pulse_format,
channels: NUM_CHANNELS,
rate: SAMPLE_RATE,
};
debug_assert!(ss.is_valid());
@ -29,6 +36,7 @@ impl Open for PulseAudioSink {
s: None,
ss: ss,
device: device,
format: format,
}
}
}
@ -67,19 +75,13 @@ impl Sink for PulseAudioSink {
Ok(())
}
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
if let Some(s) = &self.s {
// SAFETY: An f32 consists of four bytes, so that the given slice can be interpreted
// as a byte array of four. Each byte pointer is validly aligned, and so is the newly
// created slice.
let d: &[u8] = unsafe {
std::slice::from_raw_parts(
packet.samples().as_ptr() as *const u8,
packet.samples().len() * mem::size_of::<f32>(),
)
};
sink_as_bytes!();
}
match s.write(d) {
impl SinkAsBytes for PulseAudioSink {
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> {
if let Some(s) = &self.s {
match s.write(data) {
Ok(_) => Ok(()),
Err(e) => Err(io::Error::new(
io::ErrorKind::BrokenPipe,
@ -89,7 +91,7 @@ impl Sink for PulseAudioSink {
} else {
Err(io::Error::new(
io::ErrorKind::NotConnected,
"Not connected to pulseaudio",
"Not connected to PulseAudio",
))
}
}

View file

@ -2,33 +2,90 @@ use super::{Open, Sink};
extern crate cpal;
extern crate rodio;
use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use cpal::traits::{DeviceTrait, HostTrait};
use std::process::exit;
use std::{io, thread, time};
pub struct RodioSink {
// most code is shared between RodioSink and JackRodioSink
macro_rules! rodio_sink {
($name: ident) => {
pub struct $name {
rodio_sink: rodio::Sink,
// We have to keep hold of this object, or the Sink can't play...
#[allow(dead_code)]
stream: rodio::OutputStream,
format: AudioFormat,
}
impl Sink for $name {
start_stop_noop!();
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
let samples = packet.samples();
match self.format {
AudioFormat::F32 => {
let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples);
self.rodio_sink.append(source)
}
AudioFormat::S16 => {
let samples_s16: Vec<i16> = AudioPacket::f32_to_s16(samples);
let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples_s16);
self.rodio_sink.append(source)
}
};
// 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(time::Duration::from_millis(10));
}
Ok(())
}
}
impl $name {
fn open_sink(host: &cpal::Host, device: Option<String>, format: AudioFormat) -> $name {
if format != AudioFormat::S16 {
#[cfg(target_os = "linux")]
{
warn!("Rodio output to Alsa is known to cause garbled sound on output formats other than 16-bit signed integer.");
warn!("Consider using `--backend alsa` OR `--format {:?}`", AudioFormat::S16);
}
}
let rodio_device = match_device(&host, device);
debug!("Using cpal device");
let stream = rodio::OutputStream::try_from_device(&rodio_device)
.expect("couldn't open output stream.");
debug!("Using Rodio stream");
let sink = rodio::Sink::try_new(&stream.1).expect("couldn't create output sink.");
debug!("Using Rodio sink");
$name {
rodio_sink: sink,
stream: stream.0,
format: format,
}
}
}
};
}
rodio_sink!(RodioSink);
#[cfg(all(
feature = "rodiojack-backend",
any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")
))]
pub struct JackRodioSink {
jackrodio_sink: rodio::Sink,
// We have to keep hold of this object, or the Sink can't play...
#[allow(dead_code)]
stream: rodio::OutputStream,
}
rodio_sink!(JackRodioSink);
fn list_formats(ref device: &rodio::Device) {
let default_fmt = match device.default_output_config() {
Ok(fmt) => cpal::SupportedStreamConfig::from(fmt),
Err(e) => {
warn!("Error getting default rodio::Sink config: {}", e);
warn!("Error getting default Rodio output config: {}", e);
return;
}
};
@ -38,13 +95,13 @@ fn list_formats(ref device: &rodio::Device) {
let mut output_configs = match device.supported_output_configs() {
Ok(f) => f.peekable(),
Err(e) => {
warn!("Error getting supported rodio::Sink configs: {}", e);
warn!("Error getting supported Rodio output configs: {}", e);
return;
}
};
if output_configs.peek().is_some() {
debug!(" Available configs:");
debug!(" Available output configs:");
for format in output_configs {
debug!(" {:?}", format);
}
@ -54,13 +111,13 @@ fn list_formats(ref device: &rodio::Device) {
fn list_outputs(ref host: &cpal::Host) {
let default_device = get_default_device(host);
let default_device_name = default_device.name().expect("cannot get output name");
println!("Default Audio Device:\n {}", default_device_name);
println!("Default audio device:\n {}", default_device_name);
list_formats(&default_device);
println!("Other Available Audio Devices:");
println!("Other available audio devices:");
let found_devices = host.output_devices().expect(&format!(
"Cannot get list of output devices of Host: {:?}",
"Cannot get list of output devices of host: {:?}",
host.id()
));
for device in found_devices {
@ -86,7 +143,7 @@ fn match_device(ref host: &cpal::Host, device: Option<String>) -> rodio::Device
}
let found_devices = host.output_devices().expect(&format!(
"Cannot get list of output devices of Host: {:?}",
"cannot get list of output devices of host: {:?}",
host.id()
));
for d in found_devices {
@ -102,22 +159,14 @@ fn match_device(ref host: &cpal::Host, device: Option<String>) -> rodio::Device
}
impl Open for RodioSink {
fn open(device: Option<String>) -> RodioSink {
fn open(device: Option<String>, format: AudioFormat) -> RodioSink {
let host = cpal::default_host();
debug!("Using rodio sink with cpal host: {:?}", host.id());
let rodio_device = match_device(&host, device);
debug!("Using cpal device");
let stream = rodio::OutputStream::try_from_device(&rodio_device)
.expect("Couldn't open output stream.");
debug!("Using rodio stream");
let sink = rodio::Sink::try_new(&stream.1).expect("Couldn't create output sink.");
debug!("Using rodio sink");
RodioSink {
rodio_sink: sink,
stream: stream.0,
}
info!(
"Using Rodio sink with format {:?} and cpal host: {:?}",
format,
host.id()
);
Self::open_sink(&host, device, format)
}
}
@ -126,89 +175,18 @@ impl Open for RodioSink {
any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")
))]
impl Open for JackRodioSink {
fn open(device: Option<String>) -> JackRodioSink {
fn open(device: Option<String>, format: AudioFormat) -> JackRodioSink {
let host = cpal::host_from_id(
cpal::available_hosts()
.into_iter()
.find(|id| *id == cpal::HostId::Jack)
.expect("Jack Host not found"),
.expect("JACK host not found"),
)
.expect("Jack Host not found");
debug!("Using jack rodio sink with cpal Jack host");
let rodio_device = match_device(&host, device);
debug!("Using cpal device");
let stream = rodio::OutputStream::try_from_device(&rodio_device)
.expect("Couldn't open output stream.");
debug!("Using jack rodio stream");
let sink = rodio::Sink::try_new(&stream.1).expect("Couldn't create output sink.");
debug!("Using jack rodio sink");
JackRodioSink {
jackrodio_sink: sink,
stream: stream.0,
}
}
}
impl Sink for RodioSink {
fn start(&mut self) -> io::Result<()> {
// More similar to an "unpause" than "play". Doesn't undo "stop".
// self.rodio_sink.play();
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
// This will immediately stop playback, but the sink is then unusable.
// We just have to let the current buffer play till the end.
// self.rodio_sink.stop();
Ok(())
}
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
let source = rodio::buffer::SamplesBuffer::new(2, 44100, packet.samples());
self.rodio_sink.append(source);
// 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(time::Duration::from_millis(10));
}
Ok(())
}
}
#[cfg(all(
feature = "rodiojack-backend",
any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")
))]
impl Sink for JackRodioSink {
fn start(&mut self) -> io::Result<()> {
// More similar to an "unpause" than "play". Doesn't undo "stop".
// self.rodio_sink.play();
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
// This will immediately stop playback, but the sink is then unusable.
// We just have to let the current buffer play till the end.
// self.rodio_sink.stop();
Ok(())
}
fn write(&mut self, data: &[f32]) -> io::Result<()> {
let source = rodio::buffer::SamplesBuffer::new(2, 44100, data);
self.jackrodio_sink.append(source);
// 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.jackrodio_sink.len() > 26 {
// sleep and wait for rodio to drain a bit
thread::sleep(time::Duration::from_millis(10));
}
Ok(())
.expect("JACK host not found");
info!(
"Using JACK Rodio sink with format {:?} and cpal JACK host",
format
);
Self::open_sink(&host, device, format)
}
}

View file

@ -1,57 +1,98 @@
use super::{Open, Sink};
use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
use sdl2::audio::{AudioQueue, AudioSpecDesired};
use std::{io, thread, time};
use std::{io, mem, thread, time};
type Channel = f32;
pub struct SdlSink {
queue: AudioQueue<Channel>,
pub enum SdlSink {
F32(AudioQueue<f32>),
S16(AudioQueue<i16>),
}
impl Open for SdlSink {
fn open(device: Option<String>) -> SdlSink {
debug!("Using SDL sink");
fn open(device: Option<String>, format: AudioFormat) -> SdlSink {
info!("Using SDL sink with format: {:?}", format);
if device.is_some() {
panic!("SDL sink does not support specifying a device name");
}
let ctx = sdl2::init().expect("Could not init SDL");
let audio = ctx.audio().expect("Could not init SDL audio subsystem");
let ctx = sdl2::init().expect("could not initialize SDL");
let audio = ctx
.audio()
.expect("could not initialize SDL audio subsystem");
let desired_spec = AudioSpecDesired {
freq: Some(44_100),
channels: Some(2),
freq: Some(SAMPLE_RATE as i32),
channels: Some(NUM_CHANNELS),
samples: None,
};
let queue = audio
.open_queue(None, &desired_spec)
.expect("Could not open SDL audio device");
SdlSink { queue: queue }
macro_rules! open_sink {
($sink: expr, $type: ty) => {{
let queue: AudioQueue<$type> = audio
.open_queue(None, &desired_spec)
.expect("could not open SDL audio device");
$sink(queue)
}};
}
match format {
AudioFormat::F32 => open_sink!(SdlSink::F32, f32),
AudioFormat::S16 => open_sink!(SdlSink::S16, i16),
}
}
}
impl Sink for SdlSink {
fn start(&mut self) -> io::Result<()> {
self.queue.clear();
self.queue.resume();
macro_rules! start_sink {
($queue: expr) => {{
$queue.clear();
$queue.resume();
}};
}
match self {
SdlSink::F32(queue) => start_sink!(queue),
SdlSink::S16(queue) => start_sink!(queue),
};
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
self.queue.pause();
self.queue.clear();
macro_rules! stop_sink {
($queue: expr) => {{
$queue.pause();
$queue.clear();
}};
}
match self {
SdlSink::F32(queue) => stop_sink!(queue),
SdlSink::S16(queue) => stop_sink!(queue),
};
Ok(())
}
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
while self.queue.size() > (2 * 4 * 44_100) {
macro_rules! drain_sink {
($queue: expr, $size: expr) => {{
// sleep and wait for sdl thread to drain the queue a bit
while $queue.size() > (NUM_CHANNELS as u32 * $size as u32 * SAMPLE_RATE) {
thread::sleep(time::Duration::from_millis(10));
}
self.queue.queue(packet.samples());
}};
}
match self {
SdlSink::F32(queue) => {
drain_sink!(queue, mem::size_of::<f32>());
queue.queue(packet.samples())
}
SdlSink::S16(queue) => {
drain_sink!(queue, mem::size_of::<i16>());
let samples_s16: Vec<i16> = AudioPacket::f32_to_s16(packet.samples());
queue.queue(&samples_s16)
}
};
Ok(())
}
}

View file

@ -1,22 +1,25 @@
use super::{Open, Sink};
use super::{Open, Sink, SinkAsBytes};
use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use shell_words::split;
use std::io::{self, Write};
use std::mem;
use std::process::{Child, Command, Stdio};
use std::slice;
pub struct SubprocessSink {
shell_command: String,
child: Option<Child>,
format: AudioFormat,
}
impl Open for SubprocessSink {
fn open(shell_command: Option<String>) -> SubprocessSink {
fn open(shell_command: Option<String>, format: AudioFormat) -> SubprocessSink {
info!("Using subprocess sink with format: {:?}", format);
if let Some(shell_command) = shell_command {
SubprocessSink {
shell_command: shell_command,
child: None,
format: format,
}
} else {
panic!("subprocess sink requires specifying a shell command");
@ -44,16 +47,15 @@ impl Sink for SubprocessSink {
Ok(())
}
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
let data: &[u8] = unsafe {
slice::from_raw_parts(
packet.samples().as_ptr() as *const u8,
packet.samples().len() * mem::size_of::<f32>(),
)
};
sink_as_bytes!();
}
impl SinkAsBytes for SubprocessSink {
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> {
if let Some(child) = &mut self.child {
let child_stdin = child.stdin.as_mut().unwrap();
child_stdin.write_all(data)?;
child_stdin.flush()?;
}
Ok(())
}

View file

@ -1,3 +1,4 @@
use std::convert::TryFrom;
use std::str::FromStr;
#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)]
@ -25,6 +26,29 @@ impl Default for Bitrate {
}
}
#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)]
pub enum AudioFormat {
F32,
S16,
}
impl TryFrom<&String> for AudioFormat {
type Error = ();
fn try_from(s: &String) -> Result<Self, Self::Error> {
match s.to_uppercase().as_str() {
"F32" => Ok(AudioFormat::F32),
"S16" => Ok(AudioFormat::S16),
_ => unimplemented!(),
}
}
}
impl Default for AudioFormat {
fn default() -> AudioFormat {
AudioFormat::F32
}
}
#[derive(Clone, Debug)]
pub enum NormalisationType {
Album,

View file

@ -25,8 +25,12 @@ use crate::audio_backend::Sink;
use crate::metadata::{AudioItem, FileFormat};
use crate::mixer::AudioFilter;
pub const SAMPLE_RATE: u32 = 44100;
pub const NUM_CHANNELS: u8 = 2;
pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE as u32 * NUM_CHANNELS as u32;
const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000;
const SAMPLES_PER_SECOND: u32 = 44100 * 2;
const DB_VOLTAGE_RATIO: f32 = 20.0;
pub struct Player {
commands: Option<futures::sync::mpsc::UnboundedSender<PlayerCommand>>,
@ -202,11 +206,11 @@ pub struct NormalisationData {
impl NormalisationData {
pub fn db_to_ratio(db: f32) -> f32 {
return f32::powf(10.0, db / 20.0);
return f32::powf(10.0, db / DB_VOLTAGE_RATIO);
}
pub fn ratio_to_db(ratio: f32) -> f32 {
return ratio.log10() * 20.0;
return ratio.log10() * DB_VOLTAGE_RATIO;
}
fn parse_from_file<T: Read + Seek>(mut file: T) -> Result<NormalisationData> {
@ -937,8 +941,8 @@ impl Future for PlayerInternal {
if !self.config.passthrough {
if let Some(ref packet) = packet {
*stream_position_pcm =
*stream_position_pcm + (packet.samples().len() / 2) as u64;
*stream_position_pcm = *stream_position_pcm
+ (packet.samples().len() / NUM_CHANNELS as usize) as u64;
let stream_position_millis =
Self::position_pcm_to_ms(*stream_position_pcm);

View file

@ -2,6 +2,7 @@ use futures::sync::mpsc::UnboundedReceiver;
use futures::{Async, Future, Poll, Stream};
use log::{error, info, trace, warn};
use sha1::{Digest, Sha1};
use std::convert::TryFrom;
use std::env;
use std::io::{stderr, Write};
use std::mem;
@ -22,7 +23,9 @@ use librespot::core::version;
use librespot::connect::discovery::{discovery, DiscoveryStream};
use librespot::connect::spirc::{Spirc, SpircTask};
use librespot::playback::audio_backend::{self, Sink, BACKENDS};
use librespot::playback::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig};
use librespot::playback::config::{
AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig,
};
use librespot::playback::mixer::{self, Mixer, MixerConfig};
use librespot::playback::player::{NormalisationData, Player, PlayerEvent};
@ -85,7 +88,8 @@ fn print_version() {
#[derive(Clone)]
struct Setup {
backend: fn(Option<String>) -> Box<dyn Sink>,
format: AudioFormat,
backend: fn(Option<String>, AudioFormat) -> Box<dyn Sink>,
device: Option<String>,
mixer: fn(Option<MixerConfig>) -> Box<dyn Mixer>,
@ -149,6 +153,12 @@ fn setup(args: &[String]) -> Setup {
"Audio device to use. Use '?' to list options if using portaudio or alsa",
"DEVICE",
)
.optopt(
"",
"format",
"Output format (F32 or S16). Defaults to F32",
"FORMAT",
)
.optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER")
.optopt(
"m",
@ -292,9 +302,15 @@ fn setup(args: &[String]) -> Setup {
let backend = audio_backend::find(backend_name).expect("Invalid backend");
let format = matches
.opt_str("format")
.as_ref()
.map(|format| AudioFormat::try_from(format).expect("Invalid output format"))
.unwrap_or(AudioFormat::default());
let device = matches.opt_str("device");
if device == Some("?".into()) {
backend(device);
backend(device, format);
exit(0);
}
@ -496,6 +512,7 @@ fn setup(args: &[String]) -> Setup {
let enable_discovery = !matches.opt_present("disable-discovery");
Setup {
format: format,
backend: backend,
cache: cache,
session_config: session_config,
@ -517,7 +534,8 @@ struct Main {
player_config: PlayerConfig,
session_config: SessionConfig,
connect_config: ConnectConfig,
backend: fn(Option<String>) -> Box<dyn Sink>,
format: AudioFormat,
backend: fn(Option<String>, AudioFormat) -> Box<dyn Sink>,
device: Option<String>,
mixer: fn(Option<MixerConfig>) -> Box<dyn Mixer>,
mixer_config: MixerConfig,
@ -547,6 +565,7 @@ impl Main {
session_config: setup.session_config,
player_config: setup.player_config,
connect_config: setup.connect_config,
format: setup.format,
backend: setup.backend,
device: setup.device,
mixer: setup.mixer,
@ -626,11 +645,12 @@ impl Future for Main {
let connect_config = self.connect_config.clone();
let audio_filter = mixer.get_audio_filter();
let format = self.format;
let backend = self.backend;
let device = self.device.clone();
let (player, event_channel) =
Player::new(player_config, session.clone(), audio_filter, move || {
(backend)(device)
(backend)(device, format)
});
if self.emit_sink_events {