diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 8a9f88f5..cafadae9 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -37,6 +37,22 @@ pub enum AudioPacket { OggData(Vec), } +// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity. +macro_rules! convert_samples_to { + ($type: ident, $samples: 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 + } + }) + .collect() + }; +} + impl AudioPacket { pub fn samples(&self) -> &[f32] { match self { @@ -59,11 +75,12 @@ impl AudioPacket { } } + pub fn f32_to_s32(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples) + } + 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() + convert_samples_to!(i16, samples) } } diff --git a/audio/src/libvorbis_decoder.rs b/audio/src/libvorbis_decoder.rs index e7ccc984..449caaeb 100644 --- a/audio/src/libvorbis_decoder.rs +++ b/audio/src/libvorbis_decoder.rs @@ -45,7 +45,13 @@ where packet .data .iter() - .map(|sample| ((*sample as f64 + 0.5) / (0x7FFF as f64 + 0.5)) as f32) + .map(|sample| { + if *sample == 0 { + 0.0 + } else { + ((*sample as f64 + 0.5) / (0x7FFF as f64 + 0.5)) as f32 + } + }) .collect(), ))); } diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index ce758dcb..4d9f19ed 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -41,6 +41,7 @@ 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::()), }; @@ -157,6 +158,11 @@ impl AlsaSink { 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); diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 550ebb84..bc10e88a 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -28,6 +28,10 @@ macro_rules! sink_as_bytes { match packet { AudioPacket::Samples(samples) => match self.format { AudioFormat::F32 => self.write_bytes(samples.as_bytes()), + AudioFormat::S32 => { + let samples_s32 = AudioPacket::f32_to_s32(samples); + self.write_bytes(samples_s32.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/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 70caedd7..a7aa38cc 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -14,6 +14,10 @@ pub enum PortAudioSink<'a> { Option>, StreamParameters, ), + S32( + Option>, + StreamParameters, + ), S16( Option>, StreamParameters, @@ -70,19 +74,20 @@ impl<'a> Open for PortAudioSink<'a> { }; macro_rules! open_sink { - ($sink: expr, $data: expr) => {{ + ($sink: expr, $type: ty) => {{ let params = StreamParameters { device: device_idx, channel_count: NUM_CHANNELS as u32, suggested_latency: latency, - data: $data, + data: 0.0 as $type, }; $sink(None, params) }}; } match format { - AudioFormat::F32 => open_sink!(PortAudioSink::F32, 0.0), - AudioFormat::S16 => open_sink!(PortAudioSink::S16, 0), + AudioFormat::F32 => open_sink!(PortAudioSink::F32, f32), + AudioFormat::S32 => open_sink!(PortAudioSink::S32, i32), + AudioFormat::S16 => open_sink!(PortAudioSink::S16, i16), } } } @@ -109,6 +114,7 @@ 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), }; @@ -124,6 +130,7 @@ 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), }; @@ -141,6 +148,10 @@ impl<'a> Sink for PortAudioSink<'a> { let samples = packet.samples(); write_sink!(stream, &samples) } + PortAudioSink::S32(stream, _parameters) => { + let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); + write_sink!(stream, &samples_s32) + } PortAudioSink::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 8c1e8e83..a2d89f21 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -22,6 +22,7 @@ impl Open for PulseAudioSink { let pulse_format = match format { AudioFormat::F32 => pulse::sample::Format::F32le, + AudioFormat::S32 => pulse::sample::Format::S32le, AudioFormat::S16 => pulse::sample::Format::S16le, }; diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 7571aa20..97e03ec0 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -7,6 +7,8 @@ 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) => { @@ -27,12 +29,13 @@ macro_rules! rodio_sink { 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) - } + }, + _ => panic!(FORMAT_NOT_SUPPORTED), }; // Chunk sizes seem to be about 256 to 3000 ish items long. @@ -48,12 +51,16 @@ macro_rules! rodio_sink { 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); - } + match format { + AudioFormat::F32 => { + #[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); + } + }, + AudioFormat::S16 => {}, + _ => panic!(FORMAT_NOT_SUPPORTED), } let rodio_device = match_device(&host, device); diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 6e52b322..ef8c1836 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -7,6 +7,7 @@ use std::{io, mem, thread, time}; pub enum SdlSink { F32(AudioQueue), + S32(AudioQueue), S16(AudioQueue), } @@ -39,6 +40,7 @@ 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), } } @@ -54,6 +56,7 @@ impl Sink for SdlSink { } match self { SdlSink::F32(queue) => start_sink!(queue), + SdlSink::S32(queue) => start_sink!(queue), SdlSink::S16(queue) => start_sink!(queue), }; Ok(()) @@ -68,6 +71,7 @@ impl Sink for SdlSink { } match self { SdlSink::F32(queue) => stop_sink!(queue), + SdlSink::S32(queue) => stop_sink!(queue), SdlSink::S16(queue) => stop_sink!(queue), }; Ok(()) @@ -87,6 +91,11 @@ impl Sink for SdlSink { drain_sink!(queue, mem::size_of::()); queue.queue(packet.samples()) } + SdlSink::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) => { drain_sink!(queue, mem::size_of::()); let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); diff --git a/playback/src/config.rs b/playback/src/config.rs index e1ed8dcf..80771582 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -29,6 +29,7 @@ impl Default for Bitrate { #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum AudioFormat { F32, + S32, S16, } @@ -37,6 +38,7 @@ impl TryFrom<&String> for AudioFormat { 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!(), } diff --git a/src/main.rs b/src/main.rs index a7cd8b30..b4cfc437 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,7 +156,7 @@ fn setup(args: &[String]) -> Setup { .optopt( "", "format", - "Output format (F32 or S16). Defaults to F32", + "Output format (F32, S32 or S16). Defaults to F32", "FORMAT", ) .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER")