Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep samples in 64 bit #773

Merged
merged 7 commits into from
May 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [playback] Add support for dithering with `--dither` for lower requantization error (breaking)
- [playback] Add `--volume-range` option to set dB range and control `log` and `cubic` volume control curves
- [playback] `alsamixer`: support for querying dB range from Alsa softvol
- [playback] Add `--format F64` (supported by Alsa and GStreamer only)

### Changed
- [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `AudioError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking)
- [connect, playback] Moved volume controls from `librespot-connect` to `librespot-playback` crate
- [connect] Synchronize player volume with mixer volume on playback
- [playback] Store and pass samples in 64-bit floating point
- [playback] Make cubic volume control available to all mixers with `--volume-ctrl cubic`
- [playback] Normalize volumes to `[0.0..1.0]` instead of `[0..65535]` for greater precision and performance (breaking)
- [playback] `alsamixer`: complete rewrite (breaking)
Expand Down
1 change: 1 addition & 0 deletions playback/src/audio_backend/alsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ fn list_outputs() {
fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box<Error>> {
let pcm = PCM::new(dev_name, Direction::Playback, false)?;
let alsa_format = match format {
AudioFormat::F64 => Format::float64(),
AudioFormat::F32 => Format::float(),
AudioFormat::S32 => Format::s32(),
AudioFormat::S24 => Format::s24(),
Expand Down
7 changes: 4 additions & 3 deletions playback/src/audio_backend/jackaudio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ impl Open for JackSink {
}

impl Sink for JackSink {
fn write(&mut self, packet: &AudioPacket, _: &mut Converter) -> io::Result<()> {
for s in packet.samples().iter() {
let res = self.send.send(*s);
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> {
let samples_f32: &[f32] = &converter.f64_to_f32(packet.samples());
for sample in samples_f32.iter() {
let res = self.send.send(*sample);
if res.is_err() {
error!("cannot write to channel");
}
Expand Down
14 changes: 9 additions & 5 deletions playback/src/audio_backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,25 @@ macro_rules! sink_as_bytes {
use zerocopy::AsBytes;
match packet {
AudioPacket::Samples(samples) => match self.format {
AudioFormat::F32 => self.write_bytes(samples.as_bytes()),
AudioFormat::F64 => self.write_bytes(samples.as_bytes()),
AudioFormat::F32 => {
let samples_f32: &[f32] = &converter.f64_to_f32(samples);
self.write_bytes(samples_f32.as_bytes())
}
AudioFormat::S32 => {
let samples_s32: &[i32] = &converter.f32_to_s32(samples);
let samples_s32: &[i32] = &converter.f64_to_s32(samples);
self.write_bytes(samples_s32.as_bytes())
}
AudioFormat::S24 => {
let samples_s24: &[i32] = &converter.f32_to_s24(samples);
let samples_s24: &[i32] = &converter.f64_to_s24(samples);
self.write_bytes(samples_s24.as_bytes())
}
AudioFormat::S24_3 => {
let samples_s24_3: &[i24] = &converter.f32_to_s24_3(samples);
let samples_s24_3: &[i24] = &converter.f64_to_s24_3(samples);
self.write_bytes(samples_s24_3.as_bytes())
}
AudioFormat::S16 => {
let samples_s16: &[i16] = &converter.f32_to_s16(samples);
let samples_s16: &[i16] = &converter.f64_to_s16(samples);
self.write_bytes(samples_s16.as_bytes())
}
},
Expand Down
7 changes: 4 additions & 3 deletions playback/src/audio_backend/portaudio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,15 @@ impl<'a> Sink for PortAudioSink<'a> {
let samples = packet.samples();
let result = match self {
Self::F32(stream, _parameters) => {
write_sink!(ref mut stream, samples)
let samples_f32: &[f32] = &converter.f64_to_f32(samples);
write_sink!(ref mut stream, samples_f32)
}
Self::S32(stream, _parameters) => {
let samples_s32: &[i32] = &converter.f32_to_s32(samples);
let samples_s32: &[i32] = &converter.f64_to_s32(samples);
write_sink!(ref mut stream, samples_s32)
}
Self::S16(stream, _parameters) => {
let samples_s16: &[i16] = &converter.f32_to_s16(samples);
let samples_s16: &[i16] = &converter.f64_to_s16(samples);
write_sink!(ref mut stream, samples_s16)
}
};
Expand Down
3 changes: 3 additions & 0 deletions playback/src/audio_backend/pulseaudio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ impl Open for PulseAudioSink {
AudioFormat::S24 => pulse::sample::Format::S24_32le,
AudioFormat::S24_3 => pulse::sample::Format::S24le,
AudioFormat::S16 => pulse::sample::Format::S16le,
_ => {
unimplemented!("PulseAudio currently does not support {:?} output", format)
}
};

let ss = pulse::sample::Spec {
Expand Down
10 changes: 7 additions & 3 deletions playback/src/audio_backend/rodio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,16 @@ impl Sink for RodioSink {
let samples = packet.samples();
match self.format {
AudioFormat::F32 => {
let source =
rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples);
let samples_f32: &[f32] = &converter.f64_to_f32(samples);
let source = rodio::buffer::SamplesBuffer::new(
NUM_CHANNELS as u16,
SAMPLE_RATE,
samples_f32,
);
self.rodio_sink.append(source);
}
AudioFormat::S16 => {
let samples_s16: &[i16] = &converter.f32_to_s16(samples);
let samples_s16: &[i16] = &converter.f64_to_s16(samples);
let source = rodio::buffer::SamplesBuffer::new(
NUM_CHANNELS as u16,
SAMPLE_RATE,
Expand Down
7 changes: 4 additions & 3 deletions playback/src/audio_backend/sdl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,17 @@ impl Sink for SdlSink {
let samples = packet.samples();
match self {
Self::F32(queue) => {
let samples_f32: &[f32] = &converter.f64_to_f32(samples);
drain_sink!(queue, AudioFormat::F32.size());
queue.queue(samples)
queue.queue(samples_f32)
}
Self::S32(queue) => {
let samples_s32: &[i32] = &converter.f32_to_s32(samples);
let samples_s32: &[i32] = &converter.f64_to_s32(samples);
drain_sink!(queue, AudioFormat::S32.size());
queue.queue(samples_s32)
}
Self::S16(queue) => {
let samples_s16: &[i16] = &converter.f32_to_s16(samples);
let samples_s16: &[i16] = &converter.f64_to_s16(samples);
drain_sink!(queue, AudioFormat::S16.size());
queue.queue(samples_s16)
}
Expand Down
22 changes: 13 additions & 9 deletions playback/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ impl Default for Bitrate {

#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)]
pub enum AudioFormat {
F64,
F32,
S32,
S24,
Expand All @@ -44,6 +45,7 @@ impl TryFrom<&String> for AudioFormat {
type Error = ();
fn try_from(s: &String) -> Result<Self, Self::Error> {
match s.to_uppercase().as_str() {
"F64" => Ok(Self::F64),
"F32" => Ok(Self::F32),
"S32" => Ok(Self::S32),
"S24" => Ok(Self::S24),
Expand All @@ -65,6 +67,8 @@ impl AudioFormat {
#[allow(dead_code)]
pub fn size(&self) -> usize {
match self {
Self::F64 => mem::size_of::<f64>(),
Self::F32 => mem::size_of::<f32>(),
Self::S24_3 => mem::size_of::<i24>(),
Self::S16 => mem::size_of::<i16>(),
_ => mem::size_of::<i32>(), // S32 and S24 are both stored in i32
Expand Down Expand Up @@ -127,11 +131,11 @@ pub struct PlayerConfig {
pub normalisation: bool,
pub normalisation_type: NormalisationType,
pub normalisation_method: NormalisationMethod,
pub normalisation_pregain: f32,
pub normalisation_threshold: f32,
pub normalisation_attack: f32,
pub normalisation_release: f32,
pub normalisation_knee: f32,
pub normalisation_pregain: f64,
pub normalisation_threshold: f64,
pub normalisation_attack: f64,
pub normalisation_release: f64,
pub normalisation_knee: f64,

// pass function pointers so they can be lazily instantiated *after* spawning a thread
// (thereby circumventing Send bounds that they might not satisfy)
Expand Down Expand Up @@ -160,10 +164,10 @@ impl Default for PlayerConfig {
// fields are intended for volume control range in dB
#[derive(Clone, Copy, Debug)]
pub enum VolumeCtrl {
Cubic(f32),
Cubic(f64),
Fixed,
Linear,
Log(f32),
Log(f64),
}

impl FromStr for VolumeCtrl {
Expand All @@ -183,9 +187,9 @@ impl VolumeCtrl {
pub const MAX_VOLUME: u16 = std::u16::MAX;

// Taken from: https://www.dr-lex.be/info-stuff/volumecontrols.html
pub const DEFAULT_DB_RANGE: f32 = 60.0;
pub const DEFAULT_DB_RANGE: f64 = 60.0;

pub fn from_str_with_range(s: &str, db_range: f32) -> Result<Self, <Self as FromStr>::Err> {
pub fn from_str_with_range(s: &str, db_range: f64) -> Result<Self, <Self as FromStr>::Err> {
use self::VolumeCtrl::*;
match s.to_lowercase().as_ref() {
"cubic" => Ok(Cubic(db_range)),
Expand Down
37 changes: 22 additions & 15 deletions playback/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,34 +30,37 @@ impl Converter {
}
}

// Denormalize and dither
pub fn scale(&mut self, sample: f32, factor: i64) -> f32 {
const SCALE_S32: f64 = 2147483648.;
const SCALE_S24: f64 = 8388608.;
const SCALE_S16: f64 = 32768.;

pub fn scale(&mut self, sample: f64, factor: f64) -> f64 {
let dither = match self.ditherer {
Some(ref mut d) => d.noise(),
None => 0.0,
};

// From the many float to int conversion methods available, match what
// the reference Vorbis implementation uses: sample * 32768 (for 16 bit)
let int_value = sample * factor as f32 + dither;
let int_value = sample * factor + dither;

// Casting float to integer rounds towards zero by default, i.e. it
// truncates, and that generates larger error than rounding to nearest.
// Absolute lowest error is gained from rounding ties to even.
math::round::half_to_even(int_value.into(), 0) as f32
math::round::half_to_even(int_value, 0)
}

// Special case for samples packed in a word of greater bit depth (e.g.
// S24): clamp between min and max to ensure that the most significant
// byte is zero. Otherwise, dithering may cause an overflow. This is not
// necessary for other formats, because casting to integer will saturate
// to the bounds of the primitive.
pub fn clamping_scale(&mut self, sample: f32, factor: i64) -> f32 {
pub fn clamping_scale(&mut self, sample: f64, factor: f64) -> f64 {
let int_value = self.scale(sample, factor);

// In two's complement, there are more negative than positive values.
let min = -factor as f32;
let max = (factor - 1) as f32;
let min = -factor;
let max = factor - 1.0;

if int_value < min {
return min;
Expand All @@ -67,38 +70,42 @@ impl Converter {
int_value
}

pub fn f32_to_s32(&mut self, samples: &[f32]) -> Vec<i32> {
pub fn f64_to_f32(&mut self, samples: &[f64]) -> Vec<f32> {
samples.iter().map(|sample| *sample as f32).collect()
}

pub fn f64_to_s32(&mut self, samples: &[f64]) -> Vec<i32> {
samples
.iter()
.map(|sample| self.scale(*sample, 0x80000000) as i32)
.map(|sample| self.scale(*sample, Self::SCALE_S32) as i32)
.collect()
}

// S24 is 24-bit PCM packed in an upper 32-bit word
pub fn f32_to_s24(&mut self, samples: &[f32]) -> Vec<i32> {
pub fn f64_to_s24(&mut self, samples: &[f64]) -> Vec<i32> {
samples
.iter()
.map(|sample| self.clamping_scale(*sample, 0x800000) as i32)
.map(|sample| self.clamping_scale(*sample, Self::SCALE_S24) as i32)
.collect()
}

// S24_3 is 24-bit PCM in a 3-byte array
pub fn f32_to_s24_3(&mut self, samples: &[f32]) -> Vec<i24> {
pub fn f64_to_s24_3(&mut self, samples: &[f64]) -> Vec<i24> {
samples
.iter()
.map(|sample| {
// Not as DRY as calling f32_to_s24 first, but this saves iterating
// over all samples twice.
let int_value = self.clamping_scale(*sample, 0x800000) as i32;
let int_value = self.clamping_scale(*sample, Self::SCALE_S24) as i32;
i24::from_s24(int_value)
})
.collect()
}

pub fn f32_to_s16(&mut self, samples: &[f32]) -> Vec<i16> {
pub fn f64_to_s16(&mut self, samples: &[f64]) -> Vec<i16> {
samples
.iter()
.map(|sample| self.scale(*sample, 0x8000) as i16)
.map(|sample| self.scale(*sample, Self::SCALE_S16) as i16)
.collect()
}
}
8 changes: 3 additions & 5 deletions playback/src/decoder/lewton_decoder.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::{AudioDecoder, AudioError, AudioPacket};

use lewton::inside_ogg::OggStreamReader;
use lewton::samples::InterleavedSamples;

use std::error;
use std::fmt;
Expand Down Expand Up @@ -35,11 +36,8 @@ where
use lewton::OggReadError::NoCapturePatternFound;
use lewton::VorbisError::{BadAudio, OggError};
loop {
match self
.0
.read_dec_packet_generic::<lewton::samples::InterleavedSamples<f32>>()
{
Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet.samples))),
match self.0.read_dec_packet_generic::<InterleavedSamples<f32>>() {
Ok(Some(packet)) => return Ok(Some(AudioPacket::samples_from_f32(packet.samples))),
Ok(None) => return Ok(None),

Err(BadAudio(AudioIsHeader)) => (),
Expand Down
9 changes: 7 additions & 2 deletions playback/src/decoder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ mod passthrough_decoder;
pub use passthrough_decoder::{PassthroughDecoder, PassthroughError};

pub enum AudioPacket {
Samples(Vec<f32>),
Samples(Vec<f64>),
OggData(Vec<u8>),
}

impl AudioPacket {
pub fn samples(&self) -> &[f32] {
pub fn samples_from_f32(f32_samples: Vec<f32>) -> Self {
let f64_samples = f32_samples.iter().map(|sample| *sample as f64).collect();
AudioPacket::Samples(f64_samples)
}

pub fn samples(&self) -> &[f64] {
match self {
AudioPacket::Samples(s) => s,
AudioPacket::OggData(_) => panic!("can't return OggData on samples"),
Expand Down
Loading