Implement dithering (#694)

Dithering lowers digital-to-analog conversion ("requantization") error, linearizing output, lowering distortion and replacing it with a constant, fixed noise level, which is more pleasant to the ear than the distortion.

Guidance:

- On S24, S24_3 and S24, the default is to use triangular dithering. Depending on personal preference you may use Gaussian dithering instead; it's not as good objectively, but it may be preferred subjectively if you are looking for a more "analog" sound akin to tape hiss.

- Advanced users who know that they have a DAC without noise shaping have a third option: high-passed dithering, which is like triangular dithering except that it moves dithering noise up in frequency where it is less audible. Note: 99% of DACs are of delta-sigma design with noise shaping, so unless you have a multibit / R2R DAC, or otherwise know what you are doing, this is not for you.

- Don't dither or shape noise on S32 or F32. On F32 it's not supported anyway (there are no integer conversions and so no rounding errors) and on S32 the noise level is so far down that it is simply inaudible even after volume normalisation and control.

New command line option:

--dither DITHER Specify the dither algorithm to use - [none, gpdf,
                tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16
                S24, S24_3 and 'none' for other formats.

Notes:

This PR also features some opportunistic improvements. Worthy of mention are:
- matching reference Vorbis sample conversion techniques for lower noise
- a cleanup of the convert API
This commit is contained in:
Roderick van Domburg 2021-05-26 21:19:17 +02:00 committed by GitHub
parent 7f113b37c3
commit bb3dd64c87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 339 additions and 100 deletions

View file

@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
- [discovery] The crate `librespot-discovery` for discovery in LAN was created. Its functionality was previously part of `librespot-connect`. - [discovery] The crate `librespot-discovery` for discovery in LAN was created. Its functionality was previously part of `librespot-connect`.
- [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] 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] `alsamixer`: support for querying dB range from Alsa softvol

21
Cargo.lock generated
View file

@ -1,7 +1,5 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3
[[package]] [[package]]
name = "aes" name = "aes"
version = "0.6.0" version = "0.6.0"
@ -1075,6 +1073,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "libm"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
[[package]] [[package]]
name = "libmdns" name = "libmdns"
version = "0.6.1" version = "0.6.1"
@ -1300,6 +1304,8 @@ dependencies = [
"log", "log",
"ogg", "ogg",
"portaudio-rs", "portaudio-rs",
"rand",
"rand_distr",
"rodio", "rodio",
"sdl2", "sdl2",
"shell-words", "shell-words",
@ -1545,6 +1551,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"libm",
] ]
[[package]] [[package]]
@ -1896,6 +1903,16 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "rand_distr"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da9e8f32ad24fb80d07d2323a9a2ce8b30d68a62b8cb4df88119ff49a698f038"
dependencies = [
"num-traits",
"rand",
]
[[package]] [[package]]
name = "rand_hc" name = "rand_hc"
version = "0.3.0" version = "0.3.0"

View file

@ -48,6 +48,10 @@ librespot-tremor = { version = "0.2", optional = true }
ogg = "0.8" ogg = "0.8"
vorbis = { version ="0.0", optional = true } vorbis = { version ="0.0", optional = true }
# Dithering
rand = "0.8"
rand_distr = "0.4"
[features] [features]
alsa-backend = ["alsa"] alsa-backend = ["alsa"]
portaudio-backend = ["portaudio-rs"] portaudio-backend = ["portaudio-rs"]

View file

@ -1,5 +1,6 @@
use super::{Open, Sink, SinkAsBytes}; use super::{Open, Sink, SinkAsBytes};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use crate::player::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE}; use crate::player::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE};
use alsa::device_name::HintIter; use alsa::device_name::HintIter;

View file

@ -1,5 +1,6 @@
use super::{Open, Sink, SinkAsBytes}; use super::{Open, Sink, SinkAsBytes};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
@ -37,7 +38,8 @@ impl Open for GstreamerSink {
"appsrc caps=\"audio/x-raw,format={}LE,layout=interleaved,channels={},rate={}\" block=true max-bytes={} name=appsrc0 ", "appsrc caps=\"audio/x-raw,format={}LE,layout=interleaved,channels={},rate={}\" block=true max-bytes={} name=appsrc0 ",
gst_format, NUM_CHANNELS, SAMPLE_RATE, gst_bytes gst_format, NUM_CHANNELS, SAMPLE_RATE, gst_bytes
); );
let pipeline_str_rest = r#" ! audioconvert ! autoaudiosink"#; // no need to dither twice; use librespot dithering instead
let pipeline_str_rest = r#" ! audioconvert dithering=none ! autoaudiosink"#;
let pipeline_str: String = match device { let pipeline_str: String = match device {
Some(x) => format!("{}{}", pipeline_str_preamble, x), Some(x) => format!("{}{}", pipeline_str_preamble, x),
None => format!("{}{}", pipeline_str_preamble, pipeline_str_rest), None => format!("{}{}", pipeline_str_preamble, pipeline_str_rest),
@ -120,7 +122,6 @@ impl Open for GstreamerSink {
} }
impl Sink for GstreamerSink { impl Sink for GstreamerSink {
start_stop_noop!();
sink_as_bytes!(); sink_as_bytes!();
} }

View file

@ -1,5 +1,6 @@
use super::{Open, Sink}; use super::{Open, Sink};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use crate::player::NUM_CHANNELS; use crate::player::NUM_CHANNELS;
use jack::{ use jack::{
@ -69,9 +70,7 @@ impl Open for JackSink {
} }
impl Sink for JackSink { impl Sink for JackSink {
start_stop_noop!(); fn write(&mut self, packet: &AudioPacket, _: &mut Converter) -> io::Result<()> {
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
for s in packet.samples().iter() { for s in packet.samples().iter() {
let res = self.send.send(*s); let res = self.send.send(*s);
if res.is_err() { if res.is_err() {

View file

@ -1,4 +1,5 @@
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use std::io; use std::io;
@ -7,9 +8,13 @@ pub trait Open {
} }
pub trait Sink { pub trait Sink {
fn start(&mut self) -> io::Result<()>; fn start(&mut self) -> io::Result<()> {
fn stop(&mut self) -> io::Result<()>; Ok(())
fn write(&mut self, packet: &AudioPacket) -> io::Result<()>; }
fn stop(&mut self) -> io::Result<()> {
Ok(())
}
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()>;
} }
pub type SinkBuilder = fn(Option<String>, AudioFormat) -> Box<dyn Sink>; pub type SinkBuilder = fn(Option<String>, AudioFormat) -> Box<dyn Sink>;
@ -25,26 +30,26 @@ fn mk_sink<S: Sink + Open + 'static>(device: Option<String>, format: AudioFormat
// reuse code for various backends // reuse code for various backends
macro_rules! sink_as_bytes { macro_rules! sink_as_bytes {
() => { () => {
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> {
use crate::convert::{self, i24}; use crate::convert::i24;
use zerocopy::AsBytes; use zerocopy::AsBytes;
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 => { AudioFormat::S32 => {
let samples_s32: &[i32] = &convert::to_s32(samples); let samples_s32: &[i32] = &converter.f32_to_s32(samples);
self.write_bytes(samples_s32.as_bytes()) self.write_bytes(samples_s32.as_bytes())
} }
AudioFormat::S24 => { AudioFormat::S24 => {
let samples_s24: &[i32] = &convert::to_s24(samples); let samples_s24: &[i32] = &converter.f32_to_s24(samples);
self.write_bytes(samples_s24.as_bytes()) self.write_bytes(samples_s24.as_bytes())
} }
AudioFormat::S24_3 => { AudioFormat::S24_3 => {
let samples_s24_3: &[i24] = &convert::to_s24_3(samples); let samples_s24_3: &[i24] = &converter.f32_to_s24_3(samples);
self.write_bytes(samples_s24_3.as_bytes()) self.write_bytes(samples_s24_3.as_bytes())
} }
AudioFormat::S16 => { AudioFormat::S16 => {
let samples_s16: &[i16] = &convert::to_s16(samples); let samples_s16: &[i16] = &converter.f32_to_s16(samples);
self.write_bytes(samples_s16.as_bytes()) self.write_bytes(samples_s16.as_bytes())
} }
}, },
@ -54,17 +59,6 @@ macro_rules! sink_as_bytes {
}; };
} }
macro_rules! start_stop_noop {
() => {
fn start(&mut self) -> io::Result<()> {
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
Ok(())
}
};
}
#[cfg(feature = "alsa-backend")] #[cfg(feature = "alsa-backend")]
mod alsa; mod alsa;
#[cfg(feature = "alsa-backend")] #[cfg(feature = "alsa-backend")]
@ -105,6 +99,8 @@ mod subprocess;
use self::subprocess::SubprocessSink; use self::subprocess::SubprocessSink;
pub const BACKENDS: &[(&str, SinkBuilder)] = &[ pub const BACKENDS: &[(&str, SinkBuilder)] = &[
#[cfg(feature = "rodio-backend")]
("rodio", rodio::mk_rodio), // default goes first
#[cfg(feature = "alsa-backend")] #[cfg(feature = "alsa-backend")]
("alsa", mk_sink::<AlsaSink>), ("alsa", mk_sink::<AlsaSink>),
#[cfg(feature = "portaudio-backend")] #[cfg(feature = "portaudio-backend")]
@ -115,8 +111,6 @@ pub const BACKENDS: &[(&str, SinkBuilder)] = &[
("jackaudio", mk_sink::<JackSink>), ("jackaudio", mk_sink::<JackSink>),
#[cfg(feature = "gstreamer-backend")] #[cfg(feature = "gstreamer-backend")]
("gstreamer", mk_sink::<GstreamerSink>), ("gstreamer", mk_sink::<GstreamerSink>),
#[cfg(feature = "rodio-backend")]
("rodio", rodio::mk_rodio),
#[cfg(feature = "rodiojack-backend")] #[cfg(feature = "rodiojack-backend")]
("rodiojack", rodio::mk_rodiojack), ("rodiojack", rodio::mk_rodiojack),
#[cfg(feature = "sdl-backend")] #[cfg(feature = "sdl-backend")]

View file

@ -1,5 +1,6 @@
use super::{Open, Sink, SinkAsBytes}; use super::{Open, Sink, SinkAsBytes};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::{self, Write}; use std::io::{self, Write};
@ -23,7 +24,6 @@ impl Open for StdoutSink {
} }
impl Sink for StdoutSink { impl Sink for StdoutSink {
start_stop_noop!();
sink_as_bytes!(); sink_as_bytes!();
} }

View file

@ -1,6 +1,6 @@
use super::{Open, Sink}; use super::{Open, Sink};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert; use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo};
@ -55,9 +55,6 @@ impl<'a> Open for PortAudioSink<'a> {
fn open(device: Option<String>, format: AudioFormat) -> PortAudioSink<'a> { fn open(device: Option<String>, format: AudioFormat) -> PortAudioSink<'a> {
info!("Using PortAudio sink with format: {:?}", format); info!("Using PortAudio sink with format: {:?}", format);
warn!("This backend is known to panic on several platforms.");
warn!("Consider using some other backend, or better yet, contributing a fix.");
portaudio_rs::initialize().unwrap(); portaudio_rs::initialize().unwrap();
let device_idx = match device.as_ref().map(AsRef::as_ref) { let device_idx = match device.as_ref().map(AsRef::as_ref) {
@ -109,7 +106,7 @@ impl<'a> Sink for PortAudioSink<'a> {
Some(*$parameters), Some(*$parameters),
SAMPLE_RATE as f64, SAMPLE_RATE as f64,
FRAMES_PER_BUFFER_UNSPECIFIED, FRAMES_PER_BUFFER_UNSPECIFIED,
StreamFlags::empty(), StreamFlags::DITHER_OFF, // no need to dither twice; use librespot dithering instead
None, None,
) )
.unwrap(), .unwrap(),
@ -136,15 +133,15 @@ impl<'a> Sink for PortAudioSink<'a> {
}}; }};
} }
match self { match self {
Self::F32(stream, _parameters) => stop_sink!(ref mut stream), Self::F32(stream, _) => stop_sink!(ref mut stream),
Self::S32(stream, _parameters) => stop_sink!(ref mut stream), Self::S32(stream, _) => stop_sink!(ref mut stream),
Self::S16(stream, _parameters) => stop_sink!(ref mut stream), Self::S16(stream, _) => stop_sink!(ref mut stream),
}; };
Ok(()) Ok(())
} }
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> {
macro_rules! write_sink { macro_rules! write_sink {
(ref mut $stream: expr, $samples: expr) => { (ref mut $stream: expr, $samples: expr) => {
$stream.as_mut().unwrap().write($samples) $stream.as_mut().unwrap().write($samples)
@ -157,11 +154,11 @@ impl<'a> Sink for PortAudioSink<'a> {
write_sink!(ref mut stream, samples) write_sink!(ref mut stream, samples)
} }
Self::S32(stream, _parameters) => { Self::S32(stream, _parameters) => {
let samples_s32: &[i32] = &convert::to_s32(samples); let samples_s32: &[i32] = &converter.f32_to_s32(samples);
write_sink!(ref mut stream, samples_s32) write_sink!(ref mut stream, samples_s32)
} }
Self::S16(stream, _parameters) => { Self::S16(stream, _parameters) => {
let samples_s16: &[i16] = &convert::to_s16(samples); let samples_s16: &[i16] = &converter.f32_to_s16(samples);
write_sink!(ref mut stream, samples_s16) write_sink!(ref mut stream, samples_s16)
} }
}; };

View file

@ -1,5 +1,6 @@
use super::{Open, Sink, SinkAsBytes}; use super::{Open, Sink, SinkAsBytes};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
use libpulse_binding::{self as pulse, stream::Direction}; use libpulse_binding::{self as pulse, stream::Direction};

View file

@ -6,7 +6,7 @@ use thiserror::Error;
use super::Sink; use super::Sink;
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert; use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
@ -174,9 +174,7 @@ pub fn open(host: cpal::Host, device: Option<String>, format: AudioFormat) -> Ro
} }
impl Sink for RodioSink { impl Sink for RodioSink {
start_stop_noop!(); fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> {
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
let samples = packet.samples(); let samples = packet.samples();
match self.format { match self.format {
AudioFormat::F32 => { AudioFormat::F32 => {
@ -185,7 +183,7 @@ impl Sink for RodioSink {
self.rodio_sink.append(source); self.rodio_sink.append(source);
} }
AudioFormat::S16 => { AudioFormat::S16 => {
let samples_s16: &[i16] = &convert::to_s16(samples); let samples_s16: &[i16] = &converter.f32_to_s16(samples);
let source = rodio::buffer::SamplesBuffer::new( let source = rodio::buffer::SamplesBuffer::new(
NUM_CHANNELS as u16, NUM_CHANNELS as u16,
SAMPLE_RATE, SAMPLE_RATE,

View file

@ -1,6 +1,6 @@
use super::{Open, Sink}; use super::{Open, Sink};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert; use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
use sdl2::audio::{AudioQueue, AudioSpecDesired}; use sdl2::audio::{AudioQueue, AudioSpecDesired};
@ -81,7 +81,7 @@ impl Sink for SdlSink {
Ok(()) Ok(())
} }
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> {
macro_rules! drain_sink { macro_rules! drain_sink {
($queue: expr, $size: expr) => {{ ($queue: expr, $size: expr) => {{
// sleep and wait for sdl thread to drain the queue a bit // sleep and wait for sdl thread to drain the queue a bit
@ -98,12 +98,12 @@ impl Sink for SdlSink {
queue.queue(samples) queue.queue(samples)
} }
Self::S32(queue) => { Self::S32(queue) => {
let samples_s32: &[i32] = &convert::to_s32(samples); let samples_s32: &[i32] = &converter.f32_to_s32(samples);
drain_sink!(queue, AudioFormat::S32.size()); drain_sink!(queue, AudioFormat::S32.size());
queue.queue(samples_s32) queue.queue(samples_s32)
} }
Self::S16(queue) => { Self::S16(queue) => {
let samples_s16: &[i16] = &convert::to_s16(samples); let samples_s16: &[i16] = &converter.f32_to_s16(samples);
drain_sink!(queue, AudioFormat::S16.size()); drain_sink!(queue, AudioFormat::S16.size());
queue.queue(samples_s16) queue.queue(samples_s16)
} }

View file

@ -1,5 +1,6 @@
use super::{Open, Sink, SinkAsBytes}; use super::{Open, Sink, SinkAsBytes};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use shell_words::split; use shell_words::split;

View file

@ -1,5 +1,6 @@
use super::player::db_to_ratio; use super::player::db_to_ratio;
use crate::convert::i24; use crate::convert::i24;
pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer};
use std::convert::TryFrom; use std::convert::TryFrom;
use std::mem; use std::mem;
@ -117,10 +118,12 @@ impl Default for NormalisationMethod {
} }
} }
#[derive(Clone, Debug)] #[derive(Clone)]
pub struct PlayerConfig { pub struct PlayerConfig {
pub bitrate: Bitrate, pub bitrate: Bitrate,
pub gapless: bool, pub gapless: bool,
pub passthrough: bool,
pub normalisation: bool, pub normalisation: bool,
pub normalisation_type: NormalisationType, pub normalisation_type: NormalisationType,
pub normalisation_method: NormalisationMethod, pub normalisation_method: NormalisationMethod,
@ -129,12 +132,15 @@ pub struct PlayerConfig {
pub normalisation_attack: f32, pub normalisation_attack: f32,
pub normalisation_release: f32, pub normalisation_release: f32,
pub normalisation_knee: f32, pub normalisation_knee: f32,
pub passthrough: bool,
// pass function pointers so they can be lazily instantiated *after* spawning a thread
// (thereby circumventing Send bounds that they might not satisfy)
pub ditherer: Option<DithererBuilder>,
} }
impl Default for PlayerConfig { impl Default for PlayerConfig {
fn default() -> PlayerConfig { fn default() -> Self {
PlayerConfig { Self {
bitrate: Bitrate::default(), bitrate: Bitrate::default(),
gapless: true, gapless: true,
normalisation: false, normalisation: false,
@ -146,6 +152,7 @@ impl Default for PlayerConfig {
normalisation_release: 0.1, normalisation_release: 0.1,
normalisation_knee: 1.0, normalisation_knee: 1.0,
passthrough: false, passthrough: false,
ditherer: Some(mk_ditherer::<TriangularDitherer>),
} }
} }
} }

View file

@ -1,3 +1,4 @@
use crate::dither::{Ditherer, DithererBuilder};
use zerocopy::AsBytes; use zerocopy::AsBytes;
#[derive(AsBytes, Copy, Clone, Debug)] #[derive(AsBytes, Copy, Clone, Debug)]
@ -5,52 +6,98 @@ use zerocopy::AsBytes;
#[repr(transparent)] #[repr(transparent)]
pub struct i24([u8; 3]); pub struct i24([u8; 3]);
impl i24 { impl i24 {
fn pcm_from_i32(sample: i32) -> Self { fn from_s24(sample: i32) -> Self {
// drop the least significant byte // trim the padding in the most significant byte
let [a, b, c, _d] = (sample >> 8).to_le_bytes(); let [a, b, c, _d] = sample.to_le_bytes();
i24([a, b, c]) i24([a, b, c])
} }
} }
// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity. pub struct Converter {
macro_rules! convert_samples_to { ditherer: Option<Box<dyn Ditherer>>,
($type: ident, $samples: expr) => { }
convert_samples_to!($type, $samples, 0)
}; impl Converter {
($type: ident, $samples: expr, $drop_bits: expr) => { pub fn new(dither_config: Option<DithererBuilder>) -> Self {
$samples if let Some(ref ditherer_builder) = dither_config {
let ditherer = (ditherer_builder)();
info!("Converting with ditherer: {}", ditherer.name());
Self {
ditherer: Some(ditherer),
}
} else {
Self { ditherer: None }
}
}
// Denormalize and dither
pub fn scale(&mut self, sample: f32, factor: i64) -> f32 {
// 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;
match self.ditherer {
Some(ref mut d) => int_value + d.noise(int_value),
None => int_value,
}
}
// 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 {
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;
if int_value < min {
return min;
} else if int_value > max {
return max;
}
int_value
}
// https://doc.rust-lang.org/nomicon/casts.html: casting float to integer
// rounds towards zero, then saturates. Ideally halves should round to even to
// prevent any bias, but since it is extremely unlikely that a float has
// *exactly* .5 as fraction, this should be more than precise enough.
pub fn f32_to_s32(&mut self, samples: &[f32]) -> Vec<i32> {
samples
.iter()
.map(|sample| self.scale(*sample, 0x80000000) 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> {
samples
.iter()
.map(|sample| self.clamping_scale(*sample, 0x800000) 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> {
samples
.iter() .iter()
.map(|sample| { .map(|sample| {
// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] // Not as DRY as calling f32_to_s24 first, but this saves iterating
// while maintaining DC linearity. There is nothing to be gained // over all samples twice.
// by doing this in f64, as the significand of a f32 is 24 bits, let int_value = self.clamping_scale(*sample, 0x800000) as i32;
// just like the maximum bit depth we are converting to. i24::from_s24(int_value)
let int_value = *sample * (std::$type::MAX as f32 + 0.5) - 0.5;
// Casting floats to ints truncates by default, which results
// in larger quantization error than rounding arithmetically.
// Flooring is faster, but again with larger error.
int_value.round() as $type >> $drop_bits
}) })
.collect() .collect()
}; }
}
pub fn to_s32(samples: &[f32]) -> Vec<i32> { pub fn f32_to_s16(&mut self, samples: &[f32]) -> Vec<i16> {
convert_samples_to!(i32, samples) samples
}
pub fn to_s24(samples: &[f32]) -> Vec<i32> {
convert_samples_to!(i32, samples, 8)
}
pub fn to_s24_3(samples: &[f32]) -> Vec<i24> {
to_s32(samples)
.iter() .iter()
.map(|sample| i24::pcm_from_i32(*sample)) .map(|sample| self.scale(*sample, 0x8000) as i16)
.collect() .collect()
} }
pub fn to_s16(samples: &[f32]) -> Vec<i16> {
convert_samples_to!(i16, samples)
} }

View file

@ -38,14 +38,11 @@ where
loop { loop {
match self.0.packets().next() { match self.0.packets().next() {
Some(Ok(packet)) => { Some(Ok(packet)) => {
// Losslessly represent [-32768, 32767] to [-1.0, 1.0] while maintaining DC linearity.
return Ok(Some(AudioPacket::Samples( return Ok(Some(AudioPacket::Samples(
packet packet
.data .data
.iter() .iter()
.map(|sample| { .map(|sample| (*sample as f64 / 0x8000 as f64) as f32)
((*sample as f64 + 0.5) / (std::i16::MAX as f64 + 0.5)) as f32
})
.collect(), .collect(),
))); )));
} }

138
playback/src/dither.rs Normal file
View file

@ -0,0 +1,138 @@
use rand::rngs::ThreadRng;
use rand_distr::{Distribution, Normal, Triangular, Uniform};
use std::fmt;
const NUM_CHANNELS: usize = 2;
// Dithering lowers digital-to-analog conversion ("requantization") error,
// linearizing output, lowering distortion and replacing it with a constant,
// fixed noise level, which is more pleasant to the ear than the distortion.
//
// Guidance:
//
// * On S24, S24_3 and S24, the default is to use triangular dithering.
// Depending on personal preference you may use Gaussian dithering instead;
// it's not as good objectively, but it may be preferred subjectively if
// you are looking for a more "analog" sound akin to tape hiss.
//
// * Advanced users who know that they have a DAC without noise shaping have
// a third option: high-passed dithering, which is like triangular dithering
// except that it moves dithering noise up in frequency where it is less
// audible. Note: 99% of DACs are of delta-sigma design with noise shaping,
// so unless you have a multibit / R2R DAC, or otherwise know what you are
// doing, this is not for you.
//
// * Don't dither or shape noise on S32 or F32. On F32 it's not supported
// anyway (there are no integer conversions and so no rounding errors) and
// on S32 the noise level is so far down that it is simply inaudible even
// after volume normalisation and control.
//
pub trait Ditherer {
fn new() -> Self
where
Self: Sized;
fn name(&self) -> &'static str;
fn noise(&mut self, sample: f32) -> f32;
}
impl fmt::Display for dyn Ditherer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
// Implementation note: we save the handle to ThreadRng so it doesn't require
// a lookup on each call (which is on each sample!). This is ~2.5x as fast.
// Downside is that it is not Send so we cannot move it around player threads.
//
pub struct TriangularDitherer {
cached_rng: ThreadRng,
distribution: Triangular<f32>,
}
impl Ditherer for TriangularDitherer {
fn new() -> Self {
Self {
cached_rng: rand::thread_rng(),
// 2 LSB peak-to-peak needed to linearize the response:
distribution: Triangular::new(-1.0, 1.0, 0.0).unwrap(),
}
}
fn name(&self) -> &'static str {
"Triangular"
}
fn noise(&mut self, _sample: f32) -> f32 {
self.distribution.sample(&mut self.cached_rng)
}
}
pub struct GaussianDitherer {
cached_rng: ThreadRng,
distribution: Normal<f32>,
}
impl Ditherer for GaussianDitherer {
fn new() -> Self {
Self {
cached_rng: rand::thread_rng(),
// 1/2 LSB RMS needed to linearize the response:
distribution: Normal::new(0.0, 0.5).unwrap(),
}
}
fn name(&self) -> &'static str {
"Gaussian"
}
fn noise(&mut self, _sample: f32) -> f32 {
self.distribution.sample(&mut self.cached_rng)
}
}
pub struct HighPassDitherer {
active_channel: usize,
previous_noises: [f32; NUM_CHANNELS],
cached_rng: ThreadRng,
distribution: Uniform<f32>,
}
impl Ditherer for HighPassDitherer {
fn new() -> Self {
Self {
active_channel: 0,
previous_noises: [0.0; NUM_CHANNELS],
cached_rng: rand::thread_rng(),
distribution: Uniform::new_inclusive(-0.5, 0.5), // 1 LSB +/- 1 LSB (previous) = 2 LSB
}
}
fn name(&self) -> &'static str {
"Triangular, High Passed"
}
fn noise(&mut self, _sample: f32) -> f32 {
let new_noise = self.distribution.sample(&mut self.cached_rng);
let high_passed_noise = new_noise - self.previous_noises[self.active_channel];
self.previous_noises[self.active_channel] = new_noise;
self.active_channel ^= 1;
high_passed_noise
}
}
pub fn mk_ditherer<D: Ditherer + 'static>() -> Box<dyn Ditherer> {
Box::new(D::new())
}
pub type DithererBuilder = fn() -> Box<dyn Ditherer>;
pub fn find_ditherer(name: Option<String>) -> Option<DithererBuilder> {
match name.as_deref() {
Some("tpdf") => Some(mk_ditherer::<TriangularDitherer>),
Some("gpdf") => Some(mk_ditherer::<GaussianDitherer>),
Some("tpdf_hp") => Some(mk_ditherer::<HighPassDitherer>),
_ => None,
}
}

View file

@ -9,5 +9,6 @@ pub mod audio_backend;
pub mod config; pub mod config;
mod convert; mod convert;
mod decoder; mod decoder;
pub mod dither;
pub mod mixer; pub mod mixer;
pub mod player; pub mod player;

View file

@ -18,6 +18,7 @@ use crate::audio::{
}; };
use crate::audio_backend::Sink; use crate::audio_backend::Sink;
use crate::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}; use crate::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig};
use crate::convert::Converter;
use crate::core::session::Session; use crate::core::session::Session;
use crate::core::spotify_id::SpotifyId; use crate::core::spotify_id::SpotifyId;
use crate::core::util::SeqGenerator; use crate::core::util::SeqGenerator;
@ -59,6 +60,7 @@ struct PlayerInternal {
sink_event_callback: Option<SinkEventCallback>, sink_event_callback: Option<SinkEventCallback>,
audio_filter: Option<Box<dyn AudioFilter + Send>>, audio_filter: Option<Box<dyn AudioFilter + Send>>,
event_senders: Vec<mpsc::UnboundedSender<PlayerEvent>>, event_senders: Vec<mpsc::UnboundedSender<PlayerEvent>>,
converter: Converter,
limiter_active: bool, limiter_active: bool,
limiter_attack_counter: u32, limiter_attack_counter: u32,
@ -297,6 +299,8 @@ impl Player {
let handle = thread::spawn(move || { let handle = thread::spawn(move || {
debug!("new Player[{}]", session.session_id()); debug!("new Player[{}]", session.session_id());
let converter = Converter::new(config.ditherer);
let internal = PlayerInternal { let internal = PlayerInternal {
session, session,
config, config,
@ -309,6 +313,7 @@ impl Player {
sink_event_callback: None, sink_event_callback: None,
audio_filter, audio_filter,
event_senders: [event_sender].to_vec(), event_senders: [event_sender].to_vec(),
converter,
limiter_active: false, limiter_active: false,
limiter_attack_counter: 0, limiter_attack_counter: 0,
@ -1283,7 +1288,7 @@ impl PlayerInternal {
} }
} }
if let Err(err) = self.sink.write(&packet) { if let Err(err) = self.sink.write(&packet, &mut self.converter) {
error!("Could not write audio: {}", err); error!("Could not write audio: {}", err);
self.ensure_sink_stopped(false); self.ensure_sink_stopped(false);
} }

View file

@ -16,6 +16,7 @@ use librespot::playback::audio_backend::{self, SinkBuilder, BACKENDS};
use librespot::playback::config::{ use librespot::playback::config::{
AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl, AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl,
}; };
use librespot::playback::dither;
use librespot::playback::mixer::mappings::MappedCtrl; use librespot::playback::mixer::mappings::MappedCtrl;
use librespot::playback::mixer::{self, MixerConfig, MixerFn}; use librespot::playback::mixer::{self, MixerConfig, MixerFn};
use librespot::playback::player::{db_to_ratio, Player}; use librespot::playback::player::{db_to_ratio, Player};
@ -170,7 +171,6 @@ fn print_version() {
); );
} }
#[derive(Clone)]
struct Setup { struct Setup {
format: AudioFormat, format: AudioFormat,
backend: SinkBuilder, backend: SinkBuilder,
@ -246,6 +246,12 @@ fn get_setup(args: &[String]) -> Setup {
"Output format (F32, S32, S24, S24_3 or S16). Defaults to S16", "Output format (F32, S32, S24, S24_3 or S16). Defaults to S16",
"FORMAT", "FORMAT",
) )
.optopt(
"",
"dither",
"Specify the dither algorithm to use - [none, gpdf, tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16, S24, S24_3 and 'none' for other formats.",
"DITHER",
)
.optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER") .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER")
.optopt( .optopt(
"m", "m",
@ -570,7 +576,9 @@ fn get_setup(args: &[String]) -> Setup {
.as_ref() .as_ref()
.map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate")) .map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate"))
.unwrap_or_default(); .unwrap_or_default();
let gapless = !matches.opt_present("disable-gapless"); let gapless = !matches.opt_present("disable-gapless");
let normalisation = matches.opt_present("enable-volume-normalisation"); let normalisation = matches.opt_present("enable-volume-normalisation");
let normalisation_method = matches let normalisation_method = matches
.opt_str("normalisation-method") .opt_str("normalisation-method")
@ -612,11 +620,33 @@ fn get_setup(args: &[String]) -> Setup {
.opt_str("normalisation-knee") .opt_str("normalisation-knee")
.map(|knee| knee.parse::<f32>().expect("Invalid knee float value")) .map(|knee| knee.parse::<f32>().expect("Invalid knee float value"))
.unwrap_or(PlayerConfig::default().normalisation_knee); .unwrap_or(PlayerConfig::default().normalisation_knee);
let ditherer_name = matches.opt_str("dither");
let ditherer = match ditherer_name.as_deref() {
// explicitly disabled on command line
Some("none") => None,
// explicitly set on command line
Some(_) => {
if format == AudioFormat::F32 {
unimplemented!("Dithering is not available on format {:?}", format);
}
Some(dither::find_ditherer(ditherer_name).expect("Invalid ditherer"))
}
// nothing set on command line => use default
None => match format {
AudioFormat::S16 | AudioFormat::S24 | AudioFormat::S24_3 => {
PlayerConfig::default().ditherer
}
_ => None,
},
};
let passthrough = matches.opt_present("passthrough"); let passthrough = matches.opt_present("passthrough");
PlayerConfig { PlayerConfig {
bitrate, bitrate,
gapless, gapless,
passthrough,
normalisation, normalisation,
normalisation_type, normalisation_type,
normalisation_method, normalisation_method,
@ -625,7 +655,7 @@ fn get_setup(args: &[String]) -> Setup {
normalisation_attack, normalisation_attack,
normalisation_release, normalisation_release,
normalisation_knee, normalisation_knee,
passthrough, ditherer,
} }
}; };