diff --git a/audio/src/lib.rs b/audio/src/lib.rs index c4d862b3..8a9f88f5 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -58,6 +58,13 @@ impl AudioPacket { AudioPacket::OggData(d) => d.is_empty(), } } + + pub fn f32_to_s16(samples: &[f32]) -> Vec { + 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")))] diff --git a/playback/Cargo.toml b/playback/Cargo.toml index b8995a4b..67e06be7 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -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" ] diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 9bc17fe6..92b71f40 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -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, + format: AudioFormat, device: String, buffer: Vec, } @@ -34,25 +37,28 @@ fn list_outputs() { } } -fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box> { +fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box> { 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::()), + AudioFormat::S16 => (Format::s16(), mem::size_of::()), + }; + // 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> { } impl Open for AlsaSink { - fn open(device: Option) -> AlsaSink { - info!("Using alsa sink"); + fn open(device: Option, 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 = AudioPacket::f32_to_s16(&self.buffer); + io.writei(&buf_s16[..]) + } + }; + match io_result { + Ok(_) => (), + Err(err) => pcm.try_recover(err, false).unwrap(), + }; + + Ok(()) + } +} diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 1ad3631e..17ad86e6 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -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>, pipeline: gst::Pipeline, + format: AudioFormat, } impl Open for GstreamerSink { - fn open(device: Option) -> 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, 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::() - .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::() - .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::>(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(()) } diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index e95933fc..295941a4 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -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, @@ -39,8 +41,15 @@ impl ProcessHandler for JackData { } impl Open for JackSink { - fn open(client_name: Option) -> JackSink { - info!("Using jack sink!"); + fn open(client_name: Option, 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::(NUM_CHANNELS as usize * 1024 * mem::size_of::()); 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() { diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 3f5dae8d..550ebb84 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -1,8 +1,9 @@ use crate::audio::AudioPacket; +use crate::config::AudioFormat; use std::io; pub trait Open { - fn open(_: Option) -> Self; + fn open(_: Option, format: AudioFormat) -> Self; } pub trait Sink { @@ -11,8 +12,42 @@ pub trait Sink { fn write(&mut self, packet: &AudioPacket) -> io::Result<()>; } -fn mk_sink(device: Option) -> Box { - Box::new(S::open(device)) +pub trait SinkAsBytes { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()>; +} + +fn mk_sink(device: Option, format: AudioFormat) -> Box { + 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) -> Box)] = &[ +pub const BACKENDS: &'static [( + &'static str, + fn(Option, AudioFormat) -> Box, +)] = &[ #[cfg(feature = "alsa-backend")] ("alsa", mk_sink::), #[cfg(feature = "portaudio-backend")] @@ -92,7 +130,7 @@ pub const BACKENDS: &'static [(&'static str, fn(Option) -> Box ("subprocess", mk_sink::), ]; -pub fn find(name: Option) -> Option) -> Box> { +pub fn find(name: Option) -> Option, AudioFormat) -> Box> { if let Some(name) = name { BACKENDS .iter() diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 5516ee94..3a90d06f 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -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); +pub struct StdoutSink { + output: Box, + format: AudioFormat, +} impl Open for StdoutSink { - fn open(path: Option) -> 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, format: AudioFormat) -> StdoutSink { + info!("Using pipe sink with format: {:?}", format); + + let output: Box = 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::(), - ) - }, - 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(()) } } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 0b8eac0b..70caedd7 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -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>( - Option>, - StreamParameters, -); +pub enum PortAudioSink<'a> { + F32( + Option>, + StreamParameters, + ), + S16( + Option>, + StreamParameters, + ), +} fn output_devices() -> Box> { let count = portaudio_rs::device::get_count().unwrap(); @@ -40,8 +48,8 @@ fn find_output(device: &str) -> Option { } impl<'a> Open for PortAudioSink<'a> { - fn open(device: Option) -> PortAudioSink<'a> { - debug!("Using PortAudio sink"); + fn open(device: Option, 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,46 +69,87 @@ impl<'a> Open for PortAudioSink<'a> { None => Duration::new(0, 0), }; - let params = StreamParameters { - device: device_idx, - channel_count: 2, - suggested_latency: latency, - data: 0.0, - }; - - PortAudioSink(None, params) + macro_rules! open_sink { + ($sink: expr, $data: expr) => {{ + let params = StreamParameters { + device: device_idx, + channel_count: NUM_CHANNELS as u32, + suggested_latency: latency, + data: $data, + }; + $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( - Stream::open( - None, - Some(self.1), - 44100.0, - FRAMES_PER_BUFFER_UNSPECIFIED, - StreamFlags::empty(), - None, - ) - .unwrap(), - ); + macro_rules! start_sink { + ($stream: ident, $parameters: ident) => {{ + if $stream.is_none() { + *$stream = Some( + Stream::open( + None, + Some(*$parameters), + SAMPLE_RATE as f64, + FRAMES_PER_BUFFER_UNSPECIFIED, + StreamFlags::empty(), + None, + ) + .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 = 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(()) diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 4dca2108..8c1e8e83 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -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, ss: pulse::sample::Spec, device: Option, + format: AudioFormat, } impl Open for PulseAudioSink { - fn open(device: Option) -> PulseAudioSink { - debug!("Using PulseAudio sink"); + fn open(device: Option, 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::(), - ) - }; + 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", )) } } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 6c996e85..7571aa20 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -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 { - rodio_sink: rodio::Sink, - // We have to keep hold of this object, or the Sink can't play... - #[allow(dead_code)] - stream: rodio::OutputStream, +// 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 = 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, 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) -> 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) -> rodio::Device } impl Open for RodioSink { - fn open(device: Option) -> RodioSink { + fn open(device: Option, 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) -> JackRodioSink { + fn open(device: Option, 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) } } diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 727615d1..6e52b322 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -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, +pub enum SdlSink { + F32(AudioQueue), + S16(AudioQueue), } impl Open for SdlSink { - fn open(device: Option) -> SdlSink { - debug!("Using SDL sink"); + fn open(device: Option, 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) { - // sleep and wait for sdl thread to drain the queue a bit - thread::sleep(time::Duration::from_millis(10)); + 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::()); + queue.queue(packet.samples()) + } + SdlSink::S16(queue) => { + drain_sink!(queue, mem::size_of::()); + let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); + queue.queue(&samples_s16) + } + }; Ok(()) } } diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 123e0233..586bb75b 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -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, + format: AudioFormat, } impl Open for SubprocessSink { - fn open(shell_command: Option) -> SubprocessSink { + fn open(shell_command: Option, 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::(), - ) - }; + 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(()) } diff --git a/playback/src/config.rs b/playback/src/config.rs index be15b268..e1ed8dcf 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -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 { + 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, diff --git a/playback/src/player.rs b/playback/src/player.rs index b6b8ad5f..dbc09695 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -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>, @@ -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(mut file: T) -> Result { @@ -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); diff --git a/src/main.rs b/src/main.rs index 91a58659..a7cd8b30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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) -> Box, + format: AudioFormat, + backend: fn(Option, AudioFormat) -> Box, device: Option, mixer: fn(Option) -> Box, @@ -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) -> Box, + format: AudioFormat, + backend: fn(Option, AudioFormat) -> Box, device: Option, mixer: fn(Option) -> Box, 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 {