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>),
}
// 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<i32> {
convert_samples_to!(i32, samples)
}
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()
convert_samples_to!(i16, samples)
}
}

View file

@ -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(),
)));
}

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 (alsa_format, sample_size) = match format {
AudioFormat::F32 => (Format::float(), mem::size_of::<f32>()),
AudioFormat::S32 => (Format::s32(), mem::size_of::<i32>()),
AudioFormat::S16 => (Format::s16(), mem::size_of::<i16>()),
};
@ -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<i32> = AudioPacket::f32_to_s32(&self.buffer);
io.writei(&buf_s32[..])
}
AudioFormat::S16 => {
let io = pcm.io_i16().unwrap();
let buf_s16: Vec<i16> = AudioPacket::f32_to_s16(&self.buffer);

View file

@ -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())

View file

@ -14,6 +14,10 @@ pub enum PortAudioSink<'a> {
Option<portaudio_rs::stream::Stream<'a, f32, f32>>,
StreamParameters<f32>,
),
S32(
Option<portaudio_rs::stream::Stream<'a, i32, i32>>,
StreamParameters<i32>,
),
S16(
Option<portaudio_rs::stream::Stream<'a, i16, i16>>,
StreamParameters<i16>,
@ -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<i32> = AudioPacket::f32_to_s32(packet.samples());
write_sink!(stream, &samples_s32)
}
PortAudioSink::S16(stream, _parameters) => {
let samples_s16: Vec<i16> = AudioPacket::f32_to_s16(packet.samples());
write_sink!(stream, &samples_s16)

View file

@ -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,
};

View file

@ -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<i16> = 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<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);
}
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);

View file

@ -7,6 +7,7 @@ use std::{io, mem, thread, time};
pub enum SdlSink {
F32(AudioQueue<f32>),
S32(AudioQueue<i32>),
S16(AudioQueue<i16>),
}
@ -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::<f32>());
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) => {
drain_sink!(queue, mem::size_of::<i16>());
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)]
pub enum AudioFormat {
F32,
S32,
S16,
}
@ -37,6 +38,7 @@ impl TryFrom<&String> for AudioFormat {
fn try_from(s: &String) -> Result<Self, Self::Error> {
match s.to_uppercase().as_str() {
"F32" => Ok(AudioFormat::F32),
"S32" => Ok(AudioFormat::S32),
"S16" => Ok(AudioFormat::S16),
_ => unimplemented!(),
}

View file

@ -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")