mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
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:
parent
a4ef174fd0
commit
5f26a745d7
10 changed files with 81 additions and 18 deletions
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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!(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in a new issue