From 770ea15498a0f1cfc7b9986f0954f4150258c29f Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 17 Mar 2021 00:00:27 +0100 Subject: [PATCH] Add support for S24 and S24_3 output formats --- Cargo.lock | 1 + audio/Cargo.toml | 1 + audio/src/lib.rs | 33 ++++++++++++--- playback/src/audio_backend/alsa.rs | 52 ++++++++++-------------- playback/src/audio_backend/gstreamer.rs | 19 ++++++--- playback/src/audio_backend/jackaudio.rs | 2 +- playback/src/audio_backend/mod.rs | 8 ++++ playback/src/audio_backend/pipe.rs | 2 +- playback/src/audio_backend/portaudio.rs | 38 +++++++++++------ playback/src/audio_backend/pulseaudio.rs | 5 ++- playback/src/audio_backend/rodio.rs | 8 ++-- playback/src/audio_backend/sdl.rs | 36 ++++++++++------ playback/src/config.rs | 28 ++++++++++--- src/main.rs | 2 +- 14 files changed, 155 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a2e42ea..2296cfed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1473,6 +1473,7 @@ dependencies = [ "ogg", "tempfile", "vorbis", + "zerocopy", ] [[package]] diff --git a/audio/Cargo.toml b/audio/Cargo.toml index b7e6e35f..06f1dda6 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -22,6 +22,7 @@ log = "0.4" num-bigint = "0.3" num-traits = "0.2" tempfile = "3.1" +zerocopy = "0.3" librespot-tremor = { version = "0.2.0", optional = true } vorbis = { version ="0.0.14", optional = true } diff --git a/audio/src/lib.rs b/audio/src/lib.rs index cafadae9..86c5b4ae 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -31,23 +31,35 @@ pub use fetch::{ READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, }; use std::fmt; +use zerocopy::AsBytes; pub enum AudioPacket { Samples(Vec), OggData(Vec), } +#[derive(AsBytes, Copy, Clone, Debug)] +#[allow(non_camel_case_types)] +#[repr(transparent)] +pub struct i24([u8; 3]); +impl i24 { + fn pcm_from_i32(sample: i32) -> Self { + // drop the least significant byte + let [a, b, c, _d] = (sample >> 8).to_le_bytes(); + i24([a, b, c]) + } +} + // Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity. macro_rules! convert_samples_to { ($type: ident, $samples: expr) => { + convert_samples_to!($type, $samples, 0) + }; + ($type: ident, $samples: expr, $shift: expr) => { $samples .iter() .map(|sample| { - if *sample == 0.0 { - 0 as $type - } else { - (*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type - } + (*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type >> $shift }) .collect() }; @@ -79,6 +91,17 @@ impl AudioPacket { convert_samples_to!(i32, samples) } + pub fn f32_to_s24(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples, 8) + } + + pub fn f32_to_s24_3(samples: &[f32]) -> Vec { + Self::f32_to_s32(samples) + .iter() + .map(|sample| i24::pcm_from_i32(*sample)) + .collect() + } + pub fn f32_to_s16(samples: &[f32]) -> Vec { convert_samples_to!(i16, samples) } diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 4d9f19ed..35d0ab11 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -1,4 +1,4 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE}; @@ -7,8 +7,8 @@ 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 BUFFERED_LATENCY: f32 = 0.125; // seconds const BUFFERED_PERIODS: Frames = 4; @@ -17,7 +17,7 @@ pub struct AlsaSink { pcm: Option, format: AudioFormat, device: String, - buffer: Vec, + buffer: Vec, } fn list_outputs() { @@ -39,16 +39,18 @@ fn list_outputs() { fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box> { let pcm = PCM::new(dev_name, Direction::Playback, false)?; - let (alsa_format, sample_size) = match format { - AudioFormat::F32 => (Format::float(), mem::size_of::()), - AudioFormat::S32 => (Format::s32(), mem::size_of::()), - AudioFormat::S16 => (Format::s16(), mem::size_of::()), + let alsa_format = match format { + AudioFormat::F32 => Format::float(), + AudioFormat::S32 => Format::s32(), + AudioFormat::S24 => Format::s24(), + AudioFormat::S24_3 => Format::S243LE, + AudioFormat::S16 => Format::s16(), }; // 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 float, one frame has a length of eight bytes. - let mut period_size = ((SAMPLES_PER_SECOND * sample_size as u32) as f32 + let mut period_size = ((SAMPLES_PER_SECOND * format.size() as u32) as f32 * (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as Frames; // Set hardware parameters: 44100 Hz / stereo / 32-bit float or 16-bit signed integer @@ -85,7 +87,7 @@ impl Open for AlsaSink { } .to_string(); - AlsaSink { + Self { pcm: None, format: format, device: name, @@ -102,7 +104,9 @@ impl Sink for AlsaSink { 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 * BUFFERED_PERIODS) as usize); + self.buffer = Vec::with_capacity( + period_size as usize * BUFFERED_PERIODS as usize * self.format.size(), + ); } Err(e) => { error!("Alsa error PCM open {}", e); @@ -121,7 +125,7 @@ impl Sink for AlsaSink { { // Write any leftover data in the period buffer // before draining the actual buffer - self.write_buf().expect("could not flush buffer"); + self.write_bytes(&[]).expect("could not flush buffer"); let pcm = self.pcm.as_mut().unwrap(); pcm.drain().unwrap(); } @@ -129,9 +133,12 @@ impl Sink for AlsaSink { Ok(()) } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + sink_as_bytes!(); +} + +impl SinkAsBytes for AlsaSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { let mut processed_data = 0; - let data = packet.samples(); while processed_data < data.len() { let data_to_buffer = min( self.buffer.capacity() - self.buffer.len(), @@ -153,23 +160,8 @@ impl Sink for AlsaSink { 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::S32 => { - let io = pcm.io_i32().unwrap(); - let buf_s32: Vec = AudioPacket::f32_to_s32(&self.buffer); - io.writei(&buf_s32[..]) - } - 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 { + let io = pcm.io_bytes(); + match io.writei(&self.buffer) { Ok(_) => (), Err(err) => pcm.try_recover(err, false).unwrap(), }; diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 17ad86e6..3695857e 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -18,11 +18,18 @@ pub struct GstreamerSink { impl Open for GstreamerSink { fn open(device: Option, format: AudioFormat) -> GstreamerSink { info!("Using GStreamer sink with format: {:?}", format); - gst::init().expect("failed to init GStreamer!"); + + // GStreamer calls S24 and S24_3 different from the rest of the world + let gst_format = match format { + AudioFormat::S24 => "S24_32".to_string(), + AudioFormat::S24_3 => "S24".to_string(), + _ => format!("{:?}", format), + }; + 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 + "appsrc caps=\"audio/x-raw,format={}LE,layout=interleaved,channels={},rate={}\" block=true max-bytes=4096 name=appsrc0 ", + gst_format, NUM_CHANNELS, SAMPLE_RATE ); let pipeline_str_rest = r#" ! audioconvert ! autoaudiosink"#; let pipeline_str: String = match device { @@ -47,7 +54,7 @@ impl Open for GstreamerSink { let bufferpool = gst::BufferPool::new(); 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); + conf.set_params(Some(&appsrc_caps), 2048 * format.size() as u32, 0, 0); bufferpool .set_config(conf) .expect("couldn't configure the buffer pool"); @@ -55,7 +62,7 @@ impl Open for GstreamerSink { .set_active(true) .expect("couldn't activate buffer pool"); - let (tx, rx) = sync_channel::>(128); + let (tx, rx) = sync_channel::>(64 * format.size()); thread::spawn(move || { for data in rx { let buffer = bufferpool.acquire_buffer(None); @@ -99,7 +106,7 @@ impl Open for GstreamerSink { .set_state(gst::State::Playing) .expect("unable to set the pipeline to the `Playing` state"); - GstreamerSink { + Self { tx: tx, pipeline: pipeline, format: format, diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 2412d07c..05c6c317 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -61,7 +61,7 @@ impl Open for JackSink { }; let active_client = AsyncClient::new(client, (), jack_data).unwrap(); - JackSink { + Self { send: tx, active_client: active_client, } diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index bc10e88a..9c46dbe4 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -32,6 +32,14 @@ macro_rules! sink_as_bytes { let samples_s32 = AudioPacket::f32_to_s32(samples); self.write_bytes(samples_s32.as_bytes()) } + AudioFormat::S24 => { + let samples_s24 = AudioPacket::f32_to_s24(samples); + self.write_bytes(samples_s24.as_bytes()) + } + AudioFormat::S24_3 => { + let samples_s24_3 = AudioPacket::f32_to_s24_3(samples); + self.write_bytes(samples_s24_3.as_bytes()) + } AudioFormat::S16 => { let samples_s16 = AudioPacket::f32_to_s16(samples); self.write_bytes(samples_s16.as_bytes()) diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 3a90d06f..ae77e320 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -18,7 +18,7 @@ impl Open for StdoutSink { _ => Box::new(io::stdout()), }; - StdoutSink { + Self { output: output, format: format, } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index a7aa38cc..213f2d02 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -18,6 +18,10 @@ pub enum PortAudioSink<'a> { Option>, StreamParameters, ), + S24( + Option>, + StreamParameters, + ), S16( Option>, StreamParameters, @@ -85,9 +89,13 @@ impl<'a> Open for PortAudioSink<'a> { }}; } match format { - AudioFormat::F32 => open_sink!(PortAudioSink::F32, f32), - AudioFormat::S32 => open_sink!(PortAudioSink::S32, i32), - AudioFormat::S16 => open_sink!(PortAudioSink::S16, i16), + AudioFormat::F32 => open_sink!(Self::F32, f32), + AudioFormat::S32 => open_sink!(Self::S32, i32), + AudioFormat::S24 => open_sink!(Self::S24, i32), + AudioFormat::S24_3 => { + unimplemented!("PortAudio currently does not support S24_3 output") + } + AudioFormat::S16 => open_sink!(Self::S16, i16), } } } @@ -113,9 +121,10 @@ impl<'a> Sink for PortAudioSink<'a> { }}; } match self { - PortAudioSink::F32(stream, parameters) => start_sink!(stream, parameters), - PortAudioSink::S32(stream, parameters) => start_sink!(stream, parameters), - PortAudioSink::S16(stream, parameters) => start_sink!(stream, parameters), + Self::F32(stream, parameters) => start_sink!(stream, parameters), + Self::S32(stream, parameters) => start_sink!(stream, parameters), + Self::S24(stream, parameters) => start_sink!(stream, parameters), + Self::S16(stream, parameters) => start_sink!(stream, parameters), }; Ok(()) @@ -129,9 +138,10 @@ impl<'a> Sink for PortAudioSink<'a> { }}; } match self { - PortAudioSink::F32(stream, _parameters) => stop_sink!(stream), - PortAudioSink::S32(stream, _parameters) => stop_sink!(stream), - PortAudioSink::S16(stream, _parameters) => stop_sink!(stream), + Self::F32(stream, _parameters) => stop_sink!(stream), + Self::S32(stream, _parameters) => stop_sink!(stream), + Self::S24(stream, _parameters) => stop_sink!(stream), + Self::S16(stream, _parameters) => stop_sink!(stream), }; Ok(()) @@ -144,15 +154,19 @@ impl<'a> Sink for PortAudioSink<'a> { }; } let result = match self { - PortAudioSink::F32(stream, _parameters) => { + Self::F32(stream, _parameters) => { let samples = packet.samples(); write_sink!(stream, &samples) } - PortAudioSink::S32(stream, _parameters) => { + Self::S32(stream, _parameters) => { let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); write_sink!(stream, &samples_s32) } - PortAudioSink::S16(stream, _parameters) => { + Self::S24(stream, _parameters) => { + let samples_s24: Vec = AudioPacket::f32_to_s24(packet.samples()); + write_sink!(stream, &samples_s24) + } + Self::S16(stream, _parameters) => { let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); write_sink!(stream, &samples_s16) } diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index a2d89f21..16800eb0 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -20,9 +20,12 @@ impl Open for PulseAudioSink { fn open(device: Option, format: AudioFormat) -> PulseAudioSink { info!("Using PulseAudio sink with format: {:?}", format); + // PulseAudio calls S24 and S24_3 different from the rest of the world let pulse_format = match format { AudioFormat::F32 => pulse::sample::Format::F32le, AudioFormat::S32 => pulse::sample::Format::S32le, + AudioFormat::S24 => pulse::sample::Format::S24_32le, + AudioFormat::S24_3 => pulse::sample::Format::S24le, AudioFormat::S16 => pulse::sample::Format::S16le, }; @@ -33,7 +36,7 @@ impl Open for PulseAudioSink { }; debug_assert!(ss.is_valid()); - PulseAudioSink { + Self { s: None, ss: ss, device: device, diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 97e03ec0..5262a9cc 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -7,8 +7,6 @@ use cpal::traits::{DeviceTrait, HostTrait}; use std::process::exit; use std::{io, thread, time}; -const FORMAT_NOT_SUPPORTED: &'static str = "Rodio currently does not support that output format"; - // most code is shared between RodioSink and JackRodioSink macro_rules! rodio_sink { ($name: ident) => { @@ -35,7 +33,7 @@ macro_rules! rodio_sink { let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples_s16); self.rodio_sink.append(source) }, - _ => panic!(FORMAT_NOT_SUPPORTED), + _ => unimplemented!(), }; // Chunk sizes seem to be about 256 to 3000 ish items long. @@ -60,7 +58,7 @@ macro_rules! rodio_sink { } }, AudioFormat::S16 => {}, - _ => panic!(FORMAT_NOT_SUPPORTED), + _ => unimplemented!("Rodio currently only supports F32 and S16 formats"), } let rodio_device = match_device(&host, device); @@ -71,7 +69,7 @@ macro_rules! rodio_sink { let sink = rodio::Sink::try_new(&stream.1).expect("couldn't create output sink."); debug!("Using Rodio sink"); - $name { + Self { rodio_sink: sink, stream: stream.0, format: format, diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index ef8c1836..32d710f8 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -8,6 +8,7 @@ use std::{io, mem, thread, time}; pub enum SdlSink { F32(AudioQueue), S32(AudioQueue), + S24(AudioQueue), S16(AudioQueue), } @@ -16,7 +17,7 @@ impl Open for SdlSink { info!("Using SDL sink with format: {:?}", format); if device.is_some() { - panic!("SDL sink does not support specifying a device name"); + warn!("SDL sink does not support specifying a device name"); } let ctx = sdl2::init().expect("could not initialize SDL"); @@ -39,9 +40,11 @@ impl Open for SdlSink { }}; } match format { - AudioFormat::F32 => open_sink!(SdlSink::F32, f32), - AudioFormat::S32 => open_sink!(SdlSink::S32, i32), - AudioFormat::S16 => open_sink!(SdlSink::S16, i16), + AudioFormat::F32 => open_sink!(Self::F32, f32), + AudioFormat::S32 => open_sink!(Self::S32, i32), + AudioFormat::S24 => open_sink!(Self::S24, i32), + AudioFormat::S24_3 => unimplemented!("SDL currently does not support S24_3 output"), + AudioFormat::S16 => open_sink!(Self::S16, i16), } } } @@ -55,9 +58,10 @@ impl Sink for SdlSink { }}; } match self { - SdlSink::F32(queue) => start_sink!(queue), - SdlSink::S32(queue) => start_sink!(queue), - SdlSink::S16(queue) => start_sink!(queue), + Self::F32(queue) => start_sink!(queue), + Self::S32(queue) => start_sink!(queue), + Self::S24(queue) => start_sink!(queue), + Self::S16(queue) => start_sink!(queue), }; Ok(()) } @@ -70,9 +74,10 @@ impl Sink for SdlSink { }}; } match self { - SdlSink::F32(queue) => stop_sink!(queue), - SdlSink::S32(queue) => stop_sink!(queue), - SdlSink::S16(queue) => stop_sink!(queue), + Self::F32(queue) => stop_sink!(queue), + Self::S32(queue) => stop_sink!(queue), + Self::S24(queue) => stop_sink!(queue), + Self::S16(queue) => stop_sink!(queue), }; Ok(()) } @@ -87,16 +92,21 @@ impl Sink for SdlSink { }}; } match self { - SdlSink::F32(queue) => { + Self::F32(queue) => { drain_sink!(queue, mem::size_of::()); queue.queue(packet.samples()) } - SdlSink::S32(queue) => { + Self::S32(queue) => { drain_sink!(queue, mem::size_of::()); let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); queue.queue(&samples_s32) } - SdlSink::S16(queue) => { + Self::S24(queue) => { + drain_sink!(queue, mem::size_of::()); + let samples_s24: Vec = AudioPacket::f32_to_s24(packet.samples()); + queue.queue(&samples_s24) + } + Self::S16(queue) => { drain_sink!(queue, mem::size_of::()); let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); queue.queue(&samples_s16) diff --git a/playback/src/config.rs b/playback/src/config.rs index 7348b7bf..630c1406 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,4 +1,6 @@ +use crate::audio::i24; use std::convert::TryFrom; +use std::mem; use std::str::FromStr; #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] @@ -30,6 +32,8 @@ impl Default for Bitrate { pub enum AudioFormat { F32, S32, + S24, + S24_3, S16, } @@ -37,17 +41,31 @@ impl TryFrom<&String> for AudioFormat { type Error = (); fn try_from(s: &String) -> Result { match s.to_uppercase().as_str() { - "F32" => Ok(AudioFormat::F32), - "S32" => Ok(AudioFormat::S32), - "S16" => Ok(AudioFormat::S16), - _ => unimplemented!(), + "F32" => Ok(Self::F32), + "S32" => Ok(Self::S32), + "S24" => Ok(Self::S24), + "S24_3" => Ok(Self::S24_3), + "S16" => Ok(Self::S16), + _ => Err(()), } } } impl Default for AudioFormat { fn default() -> AudioFormat { - AudioFormat::S16 + Self::S16 + } +} + +impl AudioFormat { + // not used by all backends + #[allow(dead_code)] + pub fn size(&self) -> usize { + match self { + Self::S24_3 => mem::size_of::(), + Self::S16 => mem::size_of::(), + _ => mem::size_of::(), + } } } diff --git a/src/main.rs b/src/main.rs index 07b85b30..7426e2c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,7 +156,7 @@ fn setup(args: &[String]) -> Setup { .optopt( "", "format", - "Output format (F32, S32 or S16). Defaults to S16", + "Output format (F32, S32, S24, S24_3 or S16). Defaults to S16", "FORMAT", ) .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER")