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