From bb3dd64c87c3e1cd8286029f00fc94a64c6ef719 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 26 May 2021 21:19:17 +0200 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + Cargo.lock | 21 +++- playback/Cargo.toml | 4 + playback/src/audio_backend/alsa.rs | 1 + playback/src/audio_backend/gstreamer.rs | 5 +- playback/src/audio_backend/jackaudio.rs | 5 +- playback/src/audio_backend/mod.rs | 38 +++--- playback/src/audio_backend/pipe.rs | 2 +- playback/src/audio_backend/portaudio.rs | 19 ++- playback/src/audio_backend/pulseaudio.rs | 1 + playback/src/audio_backend/rodio.rs | 8 +- playback/src/audio_backend/sdl.rs | 8 +- playback/src/audio_backend/subprocess.rs | 1 + playback/src/config.rs | 15 ++- playback/src/convert.rs | 125 ++++++++++++++------ playback/src/decoder/libvorbis_decoder.rs | 5 +- playback/src/dither.rs | 138 ++++++++++++++++++++++ playback/src/lib.rs | 1 + playback/src/player.rs | 7 +- src/main.rs | 34 +++++- 20 files changed, 339 insertions(+), 100 deletions(-) create mode 100644 playback/src/dither.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 57d50727..4ff1b8ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - [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] `alsamixer`: support for querying dB range from Alsa softvol diff --git a/Cargo.lock b/Cargo.lock index 5dc36ce2..d22a7e36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,7 +1,5 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 - [[package]] name = "aes" version = "0.6.0" @@ -1075,6 +1073,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" + [[package]] name = "libmdns" version = "0.6.1" @@ -1300,6 +1304,8 @@ dependencies = [ "log", "ogg", "portaudio-rs", + "rand", + "rand_distr", "rodio", "sdl2", "shell-words", @@ -1545,6 +1551,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1896,6 +1903,16 @@ dependencies = [ "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]] name = "rand_hc" version = "0.3.0" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 37806062..6970e7a8 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -48,6 +48,10 @@ librespot-tremor = { version = "0.2", optional = true } ogg = "0.8" vorbis = { version ="0.0", optional = true } +# Dithering +rand = "0.8" +rand_distr = "0.4" + [features] alsa-backend = ["alsa"] portaudio-backend = ["portaudio-rs"] diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index c7bc4e55..98939668 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -1,5 +1,6 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; +use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::player::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE}; use alsa::device_name::HintIter; diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index e31c66ae..b5273102 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,5 +1,6 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; +use crate::convert::Converter; use crate::decoder::AudioPacket; 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 ", 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 { Some(x) => format!("{}{}", pipeline_str_preamble, x), None => format!("{}{}", pipeline_str_preamble, pipeline_str_rest), @@ -120,7 +122,6 @@ impl Open for GstreamerSink { } impl Sink for GstreamerSink { - start_stop_noop!(); sink_as_bytes!(); } diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 816147ff..ab75fff2 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -1,5 +1,6 @@ use super::{Open, Sink}; use crate::config::AudioFormat; +use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::player::NUM_CHANNELS; use jack::{ @@ -69,9 +70,7 @@ impl Open for JackSink { } impl Sink for JackSink { - start_stop_noop!(); - - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, _: &mut Converter) -> io::Result<()> { for s in packet.samples().iter() { let res = self.send.send(*s); if res.is_err() { diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 84e35634..e4653f17 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -1,4 +1,5 @@ use crate::config::AudioFormat; +use crate::convert::Converter; use crate::decoder::AudioPacket; use std::io; @@ -7,9 +8,13 @@ pub trait Open { } pub trait Sink { - fn start(&mut self) -> io::Result<()>; - fn stop(&mut self) -> io::Result<()>; - fn write(&mut self, packet: &AudioPacket) -> io::Result<()>; + fn start(&mut self) -> io::Result<()> { + Ok(()) + } + fn stop(&mut self) -> io::Result<()> { + Ok(()) + } + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()>; } pub type SinkBuilder = fn(Option, AudioFormat) -> Box; @@ -25,26 +30,26 @@ fn mk_sink(device: Option, format: AudioFormat // reuse code for various backends macro_rules! sink_as_bytes { () => { - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - use crate::convert::{self, i24}; + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { + use crate::convert::i24; use zerocopy::AsBytes; match packet { AudioPacket::Samples(samples) => match self.format { AudioFormat::F32 => self.write_bytes(samples.as_bytes()), 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()) } 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()) } 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()) } 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()) } }, @@ -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")] mod alsa; #[cfg(feature = "alsa-backend")] @@ -105,6 +99,8 @@ mod subprocess; use self::subprocess::SubprocessSink; pub const BACKENDS: &[(&str, SinkBuilder)] = &[ + #[cfg(feature = "rodio-backend")] + ("rodio", rodio::mk_rodio), // default goes first #[cfg(feature = "alsa-backend")] ("alsa", mk_sink::), #[cfg(feature = "portaudio-backend")] @@ -115,8 +111,6 @@ pub const BACKENDS: &[(&str, SinkBuilder)] = &[ ("jackaudio", mk_sink::), #[cfg(feature = "gstreamer-backend")] ("gstreamer", mk_sink::), - #[cfg(feature = "rodio-backend")] - ("rodio", rodio::mk_rodio), #[cfg(feature = "rodiojack-backend")] ("rodiojack", rodio::mk_rodiojack), #[cfg(feature = "sdl-backend")] diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index df3e6c0f..6ad2773b 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -1,5 +1,6 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; +use crate::convert::Converter; use crate::decoder::AudioPacket; use std::fs::OpenOptions; use std::io::{self, Write}; @@ -23,7 +24,6 @@ impl Open for StdoutSink { } impl Sink for StdoutSink { - start_stop_noop!(); sink_as_bytes!(); } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 4fe471a9..0bcd1aa5 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -1,6 +1,6 @@ use super::{Open, Sink}; use crate::config::AudioFormat; -use crate::convert; +use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; @@ -55,9 +55,6 @@ impl<'a> Open for PortAudioSink<'a> { fn open(device: Option, format: AudioFormat) -> PortAudioSink<'a> { 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(); let device_idx = match device.as_ref().map(AsRef::as_ref) { @@ -109,7 +106,7 @@ impl<'a> Sink for PortAudioSink<'a> { Some(*$parameters), SAMPLE_RATE as f64, FRAMES_PER_BUFFER_UNSPECIFIED, - StreamFlags::empty(), + StreamFlags::DITHER_OFF, // no need to dither twice; use librespot dithering instead None, ) .unwrap(), @@ -136,15 +133,15 @@ impl<'a> Sink for PortAudioSink<'a> { }}; } match self { - Self::F32(stream, _parameters) => stop_sink!(ref mut stream), - Self::S32(stream, _parameters) => stop_sink!(ref mut stream), - Self::S16(stream, _parameters) => stop_sink!(ref mut stream), + Self::F32(stream, _) => stop_sink!(ref mut stream), + Self::S32(stream, _) => stop_sink!(ref mut stream), + Self::S16(stream, _) => stop_sink!(ref mut stream), }; Ok(()) } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { macro_rules! write_sink { (ref mut $stream: expr, $samples: expr) => { $stream.as_mut().unwrap().write($samples) @@ -157,11 +154,11 @@ impl<'a> Sink for PortAudioSink<'a> { write_sink!(ref mut stream, samples) } 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) } 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) } }; diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 90a4a67a..57c9b8bc 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -1,5 +1,6 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; +use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use libpulse_binding::{self as pulse, stream::Direction}; diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 9399a309..52e9bc91 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -6,7 +6,7 @@ use thiserror::Error; use super::Sink; use crate::config::AudioFormat; -use crate::convert; +use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; @@ -174,9 +174,7 @@ pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> Ro } impl Sink for RodioSink { - start_stop_noop!(); - - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { let samples = packet.samples(); match self.format { AudioFormat::F32 => { @@ -185,7 +183,7 @@ impl Sink for RodioSink { self.rodio_sink.append(source); } 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( NUM_CHANNELS as u16, SAMPLE_RATE, diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index a3a608d9..ab7c7ecc 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -1,6 +1,6 @@ use super::{Open, Sink}; use crate::config::AudioFormat; -use crate::convert; +use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use sdl2::audio::{AudioQueue, AudioSpecDesired}; @@ -81,7 +81,7 @@ impl Sink for SdlSink { Ok(()) } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { macro_rules! drain_sink { ($queue: expr, $size: expr) => {{ // sleep and wait for sdl thread to drain the queue a bit @@ -98,12 +98,12 @@ impl Sink for SdlSink { queue.queue(samples) } 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()); queue.queue(samples_s32) } 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()); queue.queue(samples_s16) } diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index f493e7a7..785fb3d2 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -1,5 +1,6 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; +use crate::convert::Converter; use crate::decoder::AudioPacket; use shell_words::split; diff --git a/playback/src/config.rs b/playback/src/config.rs index 9f8d97e1..4b9a74f0 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,5 +1,6 @@ use super::player::db_to_ratio; use crate::convert::i24; +pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer}; use std::convert::TryFrom; use std::mem; @@ -117,10 +118,12 @@ impl Default for NormalisationMethod { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct PlayerConfig { pub bitrate: Bitrate, pub gapless: bool, + pub passthrough: bool, + pub normalisation: bool, pub normalisation_type: NormalisationType, pub normalisation_method: NormalisationMethod, @@ -129,12 +132,15 @@ pub struct PlayerConfig { pub normalisation_attack: f32, pub normalisation_release: 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, } impl Default for PlayerConfig { - fn default() -> PlayerConfig { - PlayerConfig { + fn default() -> Self { + Self { bitrate: Bitrate::default(), gapless: true, normalisation: false, @@ -146,6 +152,7 @@ impl Default for PlayerConfig { normalisation_release: 0.1, normalisation_knee: 1.0, passthrough: false, + ditherer: Some(mk_ditherer::), } } } diff --git a/playback/src/convert.rs b/playback/src/convert.rs index 450910b0..c344d3e3 100644 --- a/playback/src/convert.rs +++ b/playback/src/convert.rs @@ -1,3 +1,4 @@ +use crate::dither::{Ditherer, DithererBuilder}; use zerocopy::AsBytes; #[derive(AsBytes, Copy, Clone, Debug)] @@ -5,52 +6,98 @@ use zerocopy::AsBytes; #[repr(transparent)] pub struct i24([u8; 3]); impl i24 { - fn pcm_from_i32(sample: i32) -> Self { - // drop the least significant byte - let [a, b, c, _d] = (sample >> 8).to_le_bytes(); + fn from_s24(sample: i32) -> Self { + // trim the padding in the most significant byte + let [a, b, c, _d] = sample.to_le_bytes(); i24([a, b, c]) } } -// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity. -macro_rules! convert_samples_to { - ($type: ident, $samples: expr) => { - convert_samples_to!($type, $samples, 0) - }; - ($type: ident, $samples: expr, $drop_bits: expr) => { - $samples +pub struct Converter { + ditherer: Option>, +} + +impl Converter { + pub fn new(dither_config: Option) -> Self { + 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 { + 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 { + 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 { + samples .iter() .map(|sample| { - // Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] - // while maintaining DC linearity. There is nothing to be gained - // by doing this in f64, as the significand of a f32 is 24 bits, - // just like the maximum bit depth we are converting to. - 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 + // 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; + i24::from_s24(int_value) }) .collect() - }; -} + } -pub fn to_s32(samples: &[f32]) -> Vec { - convert_samples_to!(i32, samples) -} - -pub fn to_s24(samples: &[f32]) -> Vec { - convert_samples_to!(i32, samples, 8) -} - -pub fn to_s24_3(samples: &[f32]) -> Vec { - to_s32(samples) - .iter() - .map(|sample| i24::pcm_from_i32(*sample)) - .collect() -} - -pub fn to_s16(samples: &[f32]) -> Vec { - convert_samples_to!(i16, samples) + pub fn f32_to_s16(&mut self, samples: &[f32]) -> Vec { + samples + .iter() + .map(|sample| self.scale(*sample, 0x8000) as i16) + .collect() + } } diff --git a/playback/src/decoder/libvorbis_decoder.rs b/playback/src/decoder/libvorbis_decoder.rs index 6f9a68a3..23e66583 100644 --- a/playback/src/decoder/libvorbis_decoder.rs +++ b/playback/src/decoder/libvorbis_decoder.rs @@ -38,14 +38,11 @@ where loop { match self.0.packets().next() { Some(Ok(packet)) => { - // Losslessly represent [-32768, 32767] to [-1.0, 1.0] while maintaining DC linearity. return Ok(Some(AudioPacket::Samples( packet .data .iter() - .map(|sample| { - ((*sample as f64 + 0.5) / (std::i16::MAX as f64 + 0.5)) as f32 - }) + .map(|sample| (*sample as f64 / 0x8000 as f64) as f32) .collect(), ))); } diff --git a/playback/src/dither.rs b/playback/src/dither.rs new file mode 100644 index 00000000..86aca6e2 --- /dev/null +++ b/playback/src/dither.rs @@ -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, +} + +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, +} + +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, +} + +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() -> Box { + Box::new(D::new()) +} + +pub type DithererBuilder = fn() -> Box; + +pub fn find_ditherer(name: Option) -> Option { + match name.as_deref() { + Some("tpdf") => Some(mk_ditherer::), + Some("gpdf") => Some(mk_ditherer::), + Some("tpdf_hp") => Some(mk_ditherer::), + _ => None, + } +} diff --git a/playback/src/lib.rs b/playback/src/lib.rs index 58423380..31dadc44 100644 --- a/playback/src/lib.rs +++ b/playback/src/lib.rs @@ -9,5 +9,6 @@ pub mod audio_backend; pub mod config; mod convert; mod decoder; +pub mod dither; pub mod mixer; pub mod player; diff --git a/playback/src/player.rs b/playback/src/player.rs index 659804f8..ba159946 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -18,6 +18,7 @@ use crate::audio::{ }; use crate::audio_backend::Sink; use crate::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}; +use crate::convert::Converter; use crate::core::session::Session; use crate::core::spotify_id::SpotifyId; use crate::core::util::SeqGenerator; @@ -59,6 +60,7 @@ struct PlayerInternal { sink_event_callback: Option, audio_filter: Option>, event_senders: Vec>, + converter: Converter, limiter_active: bool, limiter_attack_counter: u32, @@ -297,6 +299,8 @@ impl Player { let handle = thread::spawn(move || { debug!("new Player[{}]", session.session_id()); + let converter = Converter::new(config.ditherer); + let internal = PlayerInternal { session, config, @@ -309,6 +313,7 @@ impl Player { sink_event_callback: None, audio_filter, event_senders: [event_sender].to_vec(), + converter, limiter_active: false, 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); self.ensure_sink_stopped(false); } diff --git a/src/main.rs b/src/main.rs index 739336bb..ab47605e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ use librespot::playback::audio_backend::{self, SinkBuilder, BACKENDS}; use librespot::playback::config::{ AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl, }; +use librespot::playback::dither; use librespot::playback::mixer::mappings::MappedCtrl; use librespot::playback::mixer::{self, MixerConfig, MixerFn}; use librespot::playback::player::{db_to_ratio, Player}; @@ -170,7 +171,6 @@ fn print_version() { ); } -#[derive(Clone)] struct Setup { format: AudioFormat, backend: SinkBuilder, @@ -246,6 +246,12 @@ fn get_setup(args: &[String]) -> Setup { "Output format (F32, S32, S24, S24_3 or S16). Defaults to S16", "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( "m", @@ -570,7 +576,9 @@ fn get_setup(args: &[String]) -> Setup { .as_ref() .map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate")) .unwrap_or_default(); + let gapless = !matches.opt_present("disable-gapless"); + let normalisation = matches.opt_present("enable-volume-normalisation"); let normalisation_method = matches .opt_str("normalisation-method") @@ -612,11 +620,33 @@ fn get_setup(args: &[String]) -> Setup { .opt_str("normalisation-knee") .map(|knee| knee.parse::().expect("Invalid knee float value")) .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"); PlayerConfig { bitrate, gapless, + passthrough, normalisation, normalisation_type, normalisation_method, @@ -625,7 +655,7 @@ fn get_setup(args: &[String]) -> Setup { normalisation_attack, normalisation_release, normalisation_knee, - passthrough, + ditherer, } };