Add support for S32 output format

While at it, add a small tweak when converting "silent" samples
from float to integer. This ensures 0.0 converts to 0 and vice
versa.
This commit is contained in:
Roderick van Domburg 2021-03-13 23:43:24 +01:00
parent a4ef174fd0
commit 5f26a745d7
10 changed files with 81 additions and 18 deletions

View file

@ -37,6 +37,22 @@ pub enum AudioPacket {
OggData(Vec<u8>), OggData(Vec<u8>),
} }
// 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 { impl AudioPacket {
pub fn samples(&self) -> &[f32] { pub fn samples(&self) -> &[f32] {
match self { match self {
@ -59,11 +75,12 @@ impl AudioPacket {
} }
} }
pub fn f32_to_s32(samples: &[f32]) -> Vec<i32> {
convert_samples_to!(i32, samples)
}
pub fn f32_to_s16(samples: &[f32]) -> Vec<i16> { pub fn f32_to_s16(samples: &[f32]) -> Vec<i16> {
samples convert_samples_to!(i16, samples)
.iter()
.map(|sample| (*sample as f64 * (0x7FFF as f64 + 0.5) - 0.5) as i16)
.collect()
} }
} }

View file

@ -45,7 +45,13 @@ where
packet packet
.data .data
.iter() .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(), .collect(),
))); )));
} }

View file

@ -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 pcm = PCM::new(dev_name, Direction::Playback, false)?;
let (alsa_format, sample_size) = match format { let (alsa_format, sample_size) = match format {
AudioFormat::F32 => (Format::float(), mem::size_of::<f32>()), AudioFormat::F32 => (Format::float(), mem::size_of::<f32>()),
AudioFormat::S32 => (Format::s32(), mem::size_of::<i32>()),
AudioFormat::S16 => (Format::s16(), mem::size_of::<i16>()), AudioFormat::S16 => (Format::s16(), mem::size_of::<i16>()),
}; };
@ -157,6 +158,11 @@ impl AlsaSink {
let io = pcm.io_f32().unwrap(); let io = pcm.io_f32().unwrap();
io.writei(&self.buffer) io.writei(&self.buffer)
} }
AudioFormat::S32 => {
let io = pcm.io_i32().unwrap();
let buf_s32: Vec<i32> = AudioPacket::f32_to_s32(&self.buffer);
io.writei(&buf_s32[..])
}
AudioFormat::S16 => { AudioFormat::S16 => {
let io = pcm.io_i16().unwrap(); let io = pcm.io_i16().unwrap();
let buf_s16: Vec<i16> = AudioPacket::f32_to_s16(&self.buffer); let buf_s16: Vec<i16> = AudioPacket::f32_to_s16(&self.buffer);

View file

@ -28,6 +28,10 @@ macro_rules! sink_as_bytes {
match packet { match packet {
AudioPacket::Samples(samples) => match self.format { AudioPacket::Samples(samples) => match self.format {
AudioFormat::F32 => self.write_bytes(samples.as_bytes()), 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 => { AudioFormat::S16 => {
let samples_s16 = AudioPacket::f32_to_s16(samples); let samples_s16 = AudioPacket::f32_to_s16(samples);
self.write_bytes(samples_s16.as_bytes()) self.write_bytes(samples_s16.as_bytes())

View file

@ -14,6 +14,10 @@ pub enum PortAudioSink<'a> {
Option<portaudio_rs::stream::Stream<'a, f32, f32>>, Option<portaudio_rs::stream::Stream<'a, f32, f32>>,
StreamParameters<f32>, StreamParameters<f32>,
), ),
S32(
Option<portaudio_rs::stream::Stream<'a, i32, i32>>,
StreamParameters<i32>,
),
S16( S16(
Option<portaudio_rs::stream::Stream<'a, i16, i16>>, Option<portaudio_rs::stream::Stream<'a, i16, i16>>,
StreamParameters<i16>, StreamParameters<i16>,
@ -70,19 +74,20 @@ impl<'a> Open for PortAudioSink<'a> {
}; };
macro_rules! open_sink { macro_rules! open_sink {
($sink: expr, $data: expr) => {{ ($sink: expr, $type: ty) => {{
let params = StreamParameters { let params = StreamParameters {
device: device_idx, device: device_idx,
channel_count: NUM_CHANNELS as u32, channel_count: NUM_CHANNELS as u32,
suggested_latency: latency, suggested_latency: latency,
data: $data, data: 0.0 as $type,
}; };
$sink(None, params) $sink(None, params)
}}; }};
} }
match format { match format {
AudioFormat::F32 => open_sink!(PortAudioSink::F32, 0.0), AudioFormat::F32 => open_sink!(PortAudioSink::F32, f32),
AudioFormat::S16 => open_sink!(PortAudioSink::S16, 0), 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 { match self {
PortAudioSink::F32(stream, parameters) => start_sink!(stream, parameters), PortAudioSink::F32(stream, parameters) => start_sink!(stream, parameters),
PortAudioSink::S32(stream, parameters) => start_sink!(stream, parameters),
PortAudioSink::S16(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 { match self {
PortAudioSink::F32(stream, _parameters) => stop_sink!(stream), PortAudioSink::F32(stream, _parameters) => stop_sink!(stream),
PortAudioSink::S32(stream, _parameters) => stop_sink!(stream),
PortAudioSink::S16(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(); let samples = packet.samples();
write_sink!(stream, &samples) write_sink!(stream, &samples)
} }
PortAudioSink::S32(stream, _parameters) => {
let samples_s32: Vec<i32> = AudioPacket::f32_to_s32(packet.samples());
write_sink!(stream, &samples_s32)
}
PortAudioSink::S16(stream, _parameters) => { PortAudioSink::S16(stream, _parameters) => {
let samples_s16: Vec<i16> = AudioPacket::f32_to_s16(packet.samples()); let samples_s16: Vec<i16> = AudioPacket::f32_to_s16(packet.samples());
write_sink!(stream, &samples_s16) write_sink!(stream, &samples_s16)

View file

@ -22,6 +22,7 @@ impl Open for PulseAudioSink {
let pulse_format = match format { let pulse_format = match format {
AudioFormat::F32 => pulse::sample::Format::F32le, AudioFormat::F32 => pulse::sample::Format::F32le,
AudioFormat::S32 => pulse::sample::Format::S32le,
AudioFormat::S16 => pulse::sample::Format::S16le, AudioFormat::S16 => pulse::sample::Format::S16le,
}; };

View file

@ -7,6 +7,8 @@ use cpal::traits::{DeviceTrait, HostTrait};
use std::process::exit; use std::process::exit;
use std::{io, thread, time}; 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 // most code is shared between RodioSink and JackRodioSink
macro_rules! rodio_sink { macro_rules! rodio_sink {
($name: ident) => { ($name: ident) => {
@ -27,12 +29,13 @@ macro_rules! rodio_sink {
AudioFormat::F32 => { AudioFormat::F32 => {
let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples); let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples);
self.rodio_sink.append(source) self.rodio_sink.append(source)
} },
AudioFormat::S16 => { AudioFormat::S16 => {
let samples_s16: Vec<i16> = AudioPacket::f32_to_s16(samples); let samples_s16: Vec<i16> = AudioPacket::f32_to_s16(samples);
let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples_s16); let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples_s16);
self.rodio_sink.append(source) self.rodio_sink.append(source)
} },
_ => panic!(FORMAT_NOT_SUPPORTED),
}; };
// Chunk sizes seem to be about 256 to 3000 ish items long. // Chunk sizes seem to be about 256 to 3000 ish items long.
@ -48,12 +51,16 @@ macro_rules! rodio_sink {
impl $name { impl $name {
fn open_sink(host: &cpal::Host, device: Option<String>, format: AudioFormat) -> $name { fn open_sink(host: &cpal::Host, device: Option<String>, format: AudioFormat) -> $name {
if format != AudioFormat::S16 { match format {
#[cfg(target_os = "linux")] 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); 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); let rodio_device = match_device(&host, device);

View file

@ -7,6 +7,7 @@ use std::{io, mem, thread, time};
pub enum SdlSink { pub enum SdlSink {
F32(AudioQueue<f32>), F32(AudioQueue<f32>),
S32(AudioQueue<i32>),
S16(AudioQueue<i16>), S16(AudioQueue<i16>),
} }
@ -39,6 +40,7 @@ impl Open for SdlSink {
} }
match format { match format {
AudioFormat::F32 => open_sink!(SdlSink::F32, f32), AudioFormat::F32 => open_sink!(SdlSink::F32, f32),
AudioFormat::S32 => open_sink!(SdlSink::S32, i32),
AudioFormat::S16 => open_sink!(SdlSink::S16, i16), AudioFormat::S16 => open_sink!(SdlSink::S16, i16),
} }
} }
@ -54,6 +56,7 @@ impl Sink for SdlSink {
} }
match self { match self {
SdlSink::F32(queue) => start_sink!(queue), SdlSink::F32(queue) => start_sink!(queue),
SdlSink::S32(queue) => start_sink!(queue),
SdlSink::S16(queue) => start_sink!(queue), SdlSink::S16(queue) => start_sink!(queue),
}; };
Ok(()) Ok(())
@ -68,6 +71,7 @@ impl Sink for SdlSink {
} }
match self { match self {
SdlSink::F32(queue) => stop_sink!(queue), SdlSink::F32(queue) => stop_sink!(queue),
SdlSink::S32(queue) => stop_sink!(queue),
SdlSink::S16(queue) => stop_sink!(queue), SdlSink::S16(queue) => stop_sink!(queue),
}; };
Ok(()) Ok(())
@ -87,6 +91,11 @@ impl Sink for SdlSink {
drain_sink!(queue, mem::size_of::<f32>()); drain_sink!(queue, mem::size_of::<f32>());
queue.queue(packet.samples()) queue.queue(packet.samples())
} }
SdlSink::S32(queue) => {
drain_sink!(queue, mem::size_of::<i32>());
let samples_s32: Vec<i32> = AudioPacket::f32_to_s32(packet.samples());
queue.queue(&samples_s32)
}
SdlSink::S16(queue) => { SdlSink::S16(queue) => {
drain_sink!(queue, mem::size_of::<i16>()); drain_sink!(queue, mem::size_of::<i16>());
let samples_s16: Vec<i16> = AudioPacket::f32_to_s16(packet.samples()); let samples_s16: Vec<i16> = AudioPacket::f32_to_s16(packet.samples());

View file

@ -29,6 +29,7 @@ impl Default for Bitrate {
#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)]
pub enum AudioFormat { pub enum AudioFormat {
F32, F32,
S32,
S16, S16,
} }
@ -37,6 +38,7 @@ impl TryFrom<&String> for AudioFormat {
fn try_from(s: &String) -> Result<Self, Self::Error> { fn try_from(s: &String) -> Result<Self, Self::Error> {
match s.to_uppercase().as_str() { match s.to_uppercase().as_str() {
"F32" => Ok(AudioFormat::F32), "F32" => Ok(AudioFormat::F32),
"S32" => Ok(AudioFormat::S32),
"S16" => Ok(AudioFormat::S16), "S16" => Ok(AudioFormat::S16),
_ => unimplemented!(), _ => unimplemented!(),
} }

View file

@ -156,7 +156,7 @@ fn setup(args: &[String]) -> Setup {
.optopt( .optopt(
"", "",
"format", "format",
"Output format (F32 or S16). Defaults to F32", "Output format (F32, S32 or S16). Defaults to F32",
"FORMAT", "FORMAT",
) )
.optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER") .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER")