From c8ec26831c76cfe91697f758314d4257b8fe4be8 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 13 Apr 2021 23:04:53 +0200 Subject: [PATCH 01/23] Implement dithering and noise shaping Dithering lowers digital-to-analog conversion ("requantization") error, lowering distortion and replacing it with a constant, fixed noise level, which is more pleasant to the ear than the distortion. Doing so can with a noise-shaped dither can increase the dynamic range of 96 dB CD-quality audio to a perceived 120 dB. Guidance: experts can configure many different configurations of ditherers and noise shapers. For the rest of us, these are sane defaults depending on the output format: | Format | Ditherer | Noise shaper | +------------+------------------+--------------------------+ | S16 | Triangular (tri) | Wannamaker (fw9 or fw24) | | S24, S24_3 | High Pass (hp) | None | | S32 | None | None | | F32 | Not supported | Not supported | Notes: * Try swapping ditherers with Gaussian if you prefer a more analog sound. * Try swapping noise shapers with Fraction Saving if you are running on power-constrained hardware. In this case, disable dithering. --- Cargo.lock | 21 +- audio/Cargo.toml | 2 + audio/src/convert.rs | 59 ++++-- audio/src/dither.rs | 191 ++++++++++++++++++ audio/src/lib.rs | 5 + audio/src/shape_noise.rs | 240 +++++++++++++++++++++++ playback/src/audio_backend/alsa.rs | 6 +- playback/src/audio_backend/gstreamer.rs | 6 +- playback/src/audio_backend/jackaudio.rs | 4 +- playback/src/audio_backend/mod.rs | 27 +-- playback/src/audio_backend/pipe.rs | 11 +- playback/src/audio_backend/portaudio.rs | 31 +-- playback/src/audio_backend/pulseaudio.rs | 6 +- playback/src/audio_backend/rodio.rs | 28 ++- playback/src/audio_backend/sdl.rs | 34 ++-- playback/src/audio_backend/subprocess.rs | 6 +- playback/src/config.rs | 2 +- src/main.rs | 57 +++++- 18 files changed, 651 insertions(+), 85 deletions(-) create mode 100644 audio/src/dither.rs create mode 100644 audio/src/shape_noise.rs diff --git a/Cargo.lock b/Cargo.lock index 507785989..d45d973f4 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" @@ -1046,6 +1044,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.0" @@ -1149,6 +1153,8 @@ dependencies = [ "librespot-tremor", "log", "ogg", + "rand", + "rand_distr", "tempfile", "tokio", "vorbis", @@ -1501,6 +1507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1848,6 +1855,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/audio/Cargo.toml b/audio/Cargo.toml index a3dfbe7bb..8f77b643f 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -19,6 +19,8 @@ lewton = "0.10" log = "0.4" futures-util = { version = "0.3", default_features = false } ogg = "0.8" +rand = "0.8" +rand_distr = "0.4" tempfile = "3.1" tokio = { version = "1", features = ["sync", "macros"] } zerocopy = "0.3" diff --git a/audio/src/convert.rs b/audio/src/convert.rs index 450910b0b..e1eb90a1d 100644 --- a/audio/src/convert.rs +++ b/audio/src/convert.rs @@ -1,3 +1,5 @@ +use crate::dither::*; +use crate::shape_noise::*; use zerocopy::AsBytes; #[derive(AsBytes, Copy, Clone, Debug)] @@ -12,12 +14,30 @@ impl i24 { } } -// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity. +pub struct Requantizer { + ditherer: Box, + noise_shaper: Box, +} + +impl Requantizer { + pub fn new(ditherer: Box, noise_shaper: Box) -> Self { + Self { + ditherer, + noise_shaper, + } + } + + pub fn shaped_dither(&mut self, sample: f32) -> f32 { + let noise = self.ditherer.noise(sample); + self.noise_shaper.shape(sample, noise) + } +} + macro_rules! convert_samples_to { - ($type: ident, $samples: expr) => { - convert_samples_to!($type, $samples, 0) + ($type: ident, $samples: expr, $requantizer: expr) => { + convert_samples_to!($type, $samples, $requantizer, 0) }; - ($type: ident, $samples: expr, $drop_bits: expr) => { + ($type: ident, $samples: expr, $requantizer: expr, $drop_bits: expr) => { $samples .iter() .map(|sample| { @@ -25,32 +45,37 @@ macro_rules! convert_samples_to { // 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; + let mut int_value = *sample * (std::$type::MAX as f32 + 0.5) - 0.5; + int_value = $requantizer.shaped_dither(int_value); - // 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 + // https://doc.rust-lang.org/nomicon/casts.html: + // casting float to integer rounds towards zero, then saturates. + // ideally ties round to even, but since it is extremely + // unlikely that a float has *exactly* .5 as fraction, this + // should be more than precise enough + int_value as $type >> $drop_bits }) .collect() }; } -pub fn to_s32(samples: &[f32]) -> Vec { - convert_samples_to!(i32, samples) +pub fn to_s32(samples: &[f32], requantizer: &mut Requantizer) -> Vec { + convert_samples_to!(i32, samples, requantizer) } -pub fn to_s24(samples: &[f32]) -> Vec { - convert_samples_to!(i32, samples, 8) +// S24 is 24-bit PCM packed in an upper 32-bit word +pub fn to_s24(samples: &[f32], requantizer: &mut Requantizer) -> Vec { + convert_samples_to!(i32, samples, requantizer, 8) } -pub fn to_s24_3(samples: &[f32]) -> Vec { - to_s32(samples) +pub fn to_s24_3(samples: &[f32], requantizer: &mut Requantizer) -> Vec { + // TODO - can we improve performance by passing this as a closure? + to_s32(samples, requantizer) .iter() .map(|sample| i24::pcm_from_i32(*sample)) .collect() } -pub fn to_s16(samples: &[f32]) -> Vec { - convert_samples_to!(i16, samples) +pub fn to_s16(samples: &[f32], requantizer: &mut Requantizer) -> Vec { + convert_samples_to!(i16, samples, requantizer) } diff --git a/audio/src/dither.rs b/audio/src/dither.rs new file mode 100644 index 000000000..23e08c25d --- /dev/null +++ b/audio/src/dither.rs @@ -0,0 +1,191 @@ +use rand::rngs::ThreadRng; +use rand::Rng; +use rand_distr::{Distribution, StandardNormal, Triangular, Uniform}; + +const MIN_BOUND: f32 = -0.5; +const MAX_BOUND: f32 = 0.5; + +// Dithering lowers digital-to-analog conversion ("requantization") error, +// lowering distortion and replacing it with a constant, fixed noise level, +// which is more pleasant to the ear than the distortion. Doing so can with +// a noise-shaped dither can increase the dynamic range of 96 dB CD-quality +// audio to a perceived 120 dB. +// +// Guidance: experts can configure many different configurations of ditherers +// and noise shapers. For the rest of us, these are sane defaults depending +// on the output format: +// +// | Format | Recommended ditherer | Recommended noise shaper | +// +------------+----------------------+------------------------------------------+ +// | S16 | Triangular (tri) | Wannamaker9 (fw9) or Wannamaker24 (fw24) | +// | S24, S24_3 | High Pass (hp) | None | +// | S32 | None | None | +// | F32 | Not supported | Not supported | +// +// Notes: +// * Try swapping ditherers with Gaussian if you prefer a more analog sound. +// * Try swapping noise shapers with Fraction Saving if you are running on +// power-constrained hardware. In this case, disable dithering. +pub trait Ditherer { + fn new() -> Self + where + Self: Sized; + fn noise(&mut self, sample: f32) -> f32; +} + +pub struct NoDithering {} +impl Ditherer for NoDithering { + fn new() -> Self { + debug!("Ditherer: None"); + Self {} + } + + fn noise(&mut self, _sample: f32) -> f32 { + 0.0 + } +} + +// "True" white noise (refer to Gaussian for analog source hiss). Advantages: +// least CPU-intensive dither, lowest signal-to-noise ratio. Disadvantage: +// highest perceived loudness, suffers from intermodulation distortion unless +// you are using this for subtractive dithering, which you most likely are not +// and is not supported by any of the librespot backends. Guidance: use some +// other ditherer unless you know what you're doing. +pub struct RectangularDitherer { + cached_rng: ThreadRng, + distribution: Uniform, +} + +impl Ditherer for RectangularDitherer { + fn new() -> Self { + debug!("Ditherer: Rectangular"); + Self { + cached_rng: rand::thread_rng(), + distribution: Uniform::new_inclusive(MIN_BOUND, MAX_BOUND), + } + } + + fn noise(&mut self, _sample: f32) -> f32 { + self.distribution.sample(&mut self.cached_rng) + } +} + +// Like Rectangular, but with lower error and OK to use for the default case +// of non-subtractive dithering such as to the librespot backends. +pub struct StochasticDitherer { + cached_rng: ThreadRng, + distribution: Uniform, +} + +impl Ditherer for StochasticDitherer { + fn new() -> Self { + debug!("Ditherer: Stochastic"); + Self { + cached_rng: rand::thread_rng(), + distribution: Uniform::new(0.0, 1.0), + } + } + + fn noise(&mut self, sample: f32) -> f32 { + let fract = sample.fract(); + if self.distribution.sample(&mut self.cached_rng) <= fract { + 1.0 - fract + } else { + fract * -1.0 + } + } +} + +// Higher level than Rectangular. Advantages: superior to Rectangular as it +// does not suffer from modulation noise effects. Disadvantage: more CPU- +// expensive. Guidance: all-round recommendation to reduce quantization noise, +// even on 24-bit output. +pub struct TriangularDitherer { + cached_rng: ThreadRng, + distribution: Triangular, +} + +impl Ditherer for TriangularDitherer { + fn new() -> Self { + debug!("Ditherer: Triangular"); + Self { + cached_rng: rand::thread_rng(), + distribution: Triangular::new(MIN_BOUND, MAX_BOUND, MIN_BOUND + MAX_BOUND).unwrap(), + } + } + + fn noise(&mut self, _sample: f32) -> f32 { + self.distribution.sample(&mut self.cached_rng) + } +} + +// Like Triangular, but with higher noise power and more like phono hiss. +// Guidance: theoretically less optimal, but an alternative to Triangular +// if a more analog sound is sought after. +pub struct GaussianDitherer { + cached_rng: ThreadRng, +} + +impl Ditherer for GaussianDitherer { + fn new() -> Self { + debug!("Ditherer: Gaussian"); + Self { + cached_rng: rand::thread_rng(), + } + } + + fn noise(&mut self, _sample: f32) -> f32 { + (self.cached_rng.sample::(StandardNormal) - MAX_BOUND) / 2.0 // 1/2 LSB + } +} + +// Like Triangular, but with a high-pass filter. Advantages: comparably less +// perceptible noise, less CPU-intensive. Disadvantage: this acts like a FIR +// filter with weights [1.0, -1.0], and is superseded by noise shapers. +// Guidance: better than Triangular if not doing other noise shaping. +pub struct HighPassDitherer { + previous_noise: f32, + cached_rng: ThreadRng, + distribution: Uniform, +} + +impl Ditherer for HighPassDitherer { + fn new() -> Self { + debug!("Ditherer: High-Pass"); + Self { + previous_noise: 0.0, + cached_rng: rand::thread_rng(), + distribution: Uniform::new_inclusive(MIN_BOUND, MAX_BOUND), + } + } + + 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_noise; + self.previous_noise = new_noise; + high_passed_noise + } +} + +pub fn mk_ditherer() -> Box { + Box::new(D::new()) +} + +pub const DITHERERS: &'static [(&'static str, fn() -> Box)] = &[ + ("none", mk_ditherer::), + ("rect", mk_ditherer::), + ("sto", mk_ditherer::), + ("tri", mk_ditherer::), + ("gauss", mk_ditherer::), + ("hp", mk_ditherer::), +]; + +pub fn find_ditherer(name: Option) -> Option Box> { + match name { + Some(name) => DITHERERS + .iter() + .find(|ditherer| name == ditherer.0) + .map(|ditherer| ditherer.1), + _ => None, + } +} diff --git a/audio/src/lib.rs b/audio/src/lib.rs index b587f038c..c82cb93b5 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -5,7 +5,9 @@ extern crate log; pub mod convert; mod decrypt; +pub mod dither; mod fetch; +pub mod shape_noise; use cfg_if::cfg_if; @@ -24,12 +26,15 @@ pub use passthrough_decoder::{PassthroughDecoder, PassthroughError}; mod range_set; +pub use convert::Requantizer; pub use decrypt::AudioDecrypt; +pub use dither::{find_ditherer, Ditherer}; pub use fetch::{AudioFile, StreamLoaderController}; pub use fetch::{ READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_BEFORE_PLAYBACK_SECONDS, READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, }; +pub use shape_noise::{find_noise_shaper, NoiseShaper}; use std::fmt; pub enum AudioPacket { diff --git a/audio/src/shape_noise.rs b/audio/src/shape_noise.rs new file mode 100644 index 000000000..6876b7c51 --- /dev/null +++ b/audio/src/shape_noise.rs @@ -0,0 +1,240 @@ +const NUM_CHANNELS: usize = 2; + +// Noise shapers take the noise caused by errors in the digital-to-analog +// conversion process ("requantizing") plus any added dither, and change the +// frequency curve of that noise so that it is less perceptible to our ears. +// Like dithering, this process can generate more noise than there was in the +// first place ("noise power") but still make it more pleasant to listen to. +// Noise-shaped dithers can improve the dynamic range of 96 dB CD-quality audio +// to a perceived 120 dB. +// +// Guidance: take care to only use noise shaping when you are sure no further +// dithering and/or noise shaping is done further down the line -- like most +// delta-sigma DACs do. Exception to this rule is the Fraction Saver, which +// should be OK to use even on such DACs (but in this case, disable dithering +// in librespot -- you should do one or the other, not both). +// +// As for the more powerful noise shapers, it's a personal trade-off between +// absolute noise power and perceived noise. Co-incidentally, the lower the +// perceived noise, the higher the absolute power, and the higher the CPU- +// usage. All shapers have something going for them. +pub trait NoiseShaper { + fn new() -> Self + where + Self: Sized; + fn shape(&mut self, sample: f32, noise: f32) -> f32; +} + +pub struct NoShaping {} +impl NoiseShaper for NoShaping { + fn new() -> Self { + // Doing nothing here means that the requantizer will simply cast to + // integer. For Rust the default behavior then is to round towards + // zero. + debug!("Noise Shaper: None (rounding towards zero)"); + Self {} + } + + fn shape(&mut self, sample: f32, noise: f32) -> f32 { + sample + noise + } +} + +// First-order noise shaping. Advantages: negligible performance hit, infinite +// signal-to-noise ration at DC, lowered signal-to-noise ratio for low +// frequencies and slightly higher signal-to-noise ration for frequencies +// above Nyquist/2, kills certain limit cycles in subsequent IIR filters, +// safe to use on oversampling DACs. Disadvantage: higher perceived loudness +// than other options (but still less so than without shaping!). Guidance: +// there are very little reasons not to use this shaper, even on power- +// constrained hardware. If you do, best to set dithering to none. +pub struct FractionSaver { + active_channel: usize, + previous_fractions: [f32; NUM_CHANNELS], +} + +impl NoiseShaper for FractionSaver { + fn new() -> Self { + debug!("Noise Shaper: Fraction Saver"); + Self { + active_channel: 0, + previous_fractions: [0.0; NUM_CHANNELS], + } + } + + fn shape(&mut self, sample: f32, noise: f32) -> f32 { + let sample_with_fraction = sample + noise + self.previous_fractions[self.active_channel]; + self.previous_fractions[self.active_channel] = sample_with_fraction.fract(); + self.active_channel = self.active_channel ^ 1; + sample_with_fraction.floor() + } +} + +// Higher-order noise shapers. Advantage: lower perceived loudness than first- +// order noise-shaping. Disadvantage: higher CPU-usage, higher absolute noise +// level, not to be used when there will be dithering or noise shaping further +// down the chain, as is the case with most delta-sigma DACs. Guidance: use +// on non-oversampling DACs, or other DACs that do not do dithering or noise +// shaping themselves. +macro_rules! fir_shaper { + ($name: ident, $description: tt, $taps: expr, $weights: expr) => { + pub struct $name { + fir: FIR, + } + + impl NoiseShaper for $name { + fn new() -> Self { + debug!( + "Noise Shaper: {}, {} taps", + $description, + Self::WEIGHTS.len() + ); + Self { + fir: FIR::new(&Self::WEIGHTS), + } + } + + fn shape(&mut self, sample: f32, noise: f32) -> f32 { + self.fir.shape(sample, noise) + } + } + + impl $name { + const WEIGHTS: [f32; $taps] = $weights; + } + }; +} + +// 14.34 dB improvement in E-weighted noise at the expense of 12.19 dB higher +// noise power (unweighted) by pushing most noise into the spectrum above +// 15 kHz, meaning the noise is less audible. Guidance: this is a good +// cost/benefit trade-off. Widely used in other audio software. +fir_shaper!( + Lipshitz5, + "Lipshitz improved E-weighted", + 5, + [2.033, -2.165, 1.959, -1.590, 0.6149] +); + +// 18.32 dB improvement in E-weighted noise but at the expense of 23.1 dB +// higher noise power (unweighted). Approaches noise power levels of vinyl, +// but with lower perceived loudness. Guidance: certainly still acceptable +// noise levels, subject to personal preference. Arguably superseded by +// Wannamaker9. +fir_shaper!( + Lipshitz9, + "Lipshitz improved E-weighted", + 9, + [2.847, -4.685, 6.214, -7.184, 6.639, -5.032, 3.263, -1.632, 0.4191] +); + +// 10.47 dB improvement in F-weighted noise at the expense of 6.64 dB higher +// noise power (unweighted). Like Lipshitz, but with a refinement in psycho- +// acoustic levels. Spreads most noise into the spectrum above 10 kHz. +// Guidance: a less precise, but lower absolute noise alternative to Lipshitz5. +fir_shaper!( + Wannamaker3, + "Wannamaker F-weighted", + 3, + [1.623, -0.982, 0.109] +); + +// 16.8 dB improvement in F-weighted noise at the expense of 18.4 dB higher +// noise power (unweighted). Pushes most noise into the spectrum above 15 kHz. +// Guidance: refinement over Lipshitz9. This is what SoX uses. +fir_shaper!( + Wannamaker9, + "Wannamaker F-weighted", + 9, + [2.412, -3.370, 3.937, -4.174, 3.353, -2.205, 1.281, -0.569, 0.0847] +); + +// 16.7 dB improvement in F-weighted noise at the expense of 17.3 dB higher +// noise power (unweighted). This is close to the theoretical limit curve +// but with the highest CPU usage. Guidance: better than Wannamaker9 if you +// can suffer the performance hit. +fir_shaper!( + Wannamaker24, + "Wannamaker F-weighted", + 24, + [ + 2.391510, -3.284444, 3.679506, -3.635044, 2.524185, -1.146701, 0.115354, 0.513745, + -0.749277, 0.512386, -0.188997, -0.043705, 0.149843, -0.151186, 0.076302, -0.012070, + -0.021127, 0.025232, -0.016121, 0.004453, 0.000876, -0.001799, 0.000774, -0.000128 + ] +); + +struct FIR { + taps: usize, + weights: Vec, + active_channel: usize, + error_buffer: Vec, + buffer_index: usize, +} + +impl FIR { + fn new(weights: &[f32]) -> Self { + let taps = weights.len(); + Self { + taps, + weights: weights.to_vec(), + active_channel: 0, + error_buffer: vec![0.0; taps * NUM_CHANNELS], + buffer_index: 0, + } + } + + fn shape(&mut self, sample: f32, noise: f32) -> f32 { + // apply FIR filter + let mut sample_with_shaped_noise = sample as f64; + for index in 0..self.taps { + sample_with_shaped_noise = sample_with_shaped_noise + self.weighted_error(index); + } + + let dithered_sample = (sample_with_shaped_noise + noise as f64).round(); + + // store error and roll buffer -- this is a slight hack to increment + // buffer_index only if we are moving from channel 1 to channel 0, + // that is, just handled both the left and right channels + self.buffer_index = (self.buffer_index + self.active_channel) % self.taps; + let index = self.index_at_samples_ago(0); + self.error_buffer[index] = sample_with_shaped_noise - dithered_sample; + self.active_channel = self.active_channel ^ 1; + + dithered_sample as f32 + } +} + +impl FIR { + fn index_at_samples_ago(&self, errors_ago: usize) -> usize { + ((self.buffer_index + self.taps - errors_ago) % self.taps) + self.taps * self.active_channel + } + + fn weighted_error(&self, index: usize) -> f64 { + self.error_buffer[self.index_at_samples_ago(index)] * self.weights[index] as f64 + } +} + +pub fn mk_noise_shaper() -> Box { + Box::new(S::new()) +} + +pub const NOISE_SHAPERS: &'static [(&'static str, fn() -> Box)] = &[ + ("none", mk_noise_shaper::), + ("fract", mk_noise_shaper::), + ("iew5", mk_noise_shaper::), + ("iew9", mk_noise_shaper::), + ("fw3", mk_noise_shaper::), + ("fw9", mk_noise_shaper::), + ("fw24", mk_noise_shaper::), +]; + +pub fn find_noise_shaper(name: Option) -> Option Box> { + match name { + Some(name) => NOISE_SHAPERS + .iter() + .find(|noise_shaper| name == noise_shaper.0) + .map(|noise_shaper| noise_shaper.1), + _ => None, + } +} diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 54fed319d..2d9ff03e7 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -1,5 +1,5 @@ use super::{Open, Sink, SinkAsBytes}; -use crate::audio::AudioPacket; +use crate::audio::{AudioPacket, Requantizer}; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE}; use alsa::device_name::HintIter; @@ -18,6 +18,7 @@ pub struct AlsaSink { format: AudioFormat, device: String, buffer: Vec, + requantizer: Requantizer, } fn list_outputs() { @@ -71,7 +72,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box } impl Open for AlsaSink { - fn open(device: Option, format: AudioFormat) -> Self { + fn open(device: Option, format: AudioFormat, requantizer: Requantizer) -> Self { info!("Using Alsa sink with format: {:?}", format); let name = match device.as_ref().map(AsRef::as_ref) { @@ -90,6 +91,7 @@ impl Open for AlsaSink { format, device: name, buffer: vec![], + requantizer, } } } diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 93b718dcd..58f59396c 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,5 +1,5 @@ use super::{Open, Sink, SinkAsBytes}; -use crate::audio::AudioPacket; +use crate::audio::{AudioPacket, Requantizer}; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; @@ -17,10 +17,11 @@ pub struct GstreamerSink { tx: SyncSender>, pipeline: gst::Pipeline, format: AudioFormat, + requantizer: Requantizer, } impl Open for GstreamerSink { - fn open(device: Option, format: AudioFormat) -> Self { + fn open(device: Option, format: AudioFormat, requantizer: Requantizer) -> Self { info!("Using GStreamer sink with format: {:?}", format); gst::init().expect("failed to init GStreamer!"); @@ -115,6 +116,7 @@ impl Open for GstreamerSink { tx, pipeline, format, + requantizer, } } } diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index aca2edd2b..f72a88466 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -1,6 +1,6 @@ use super::{Open, Sink}; use crate::audio::AudioPacket; -use crate::config::AudioFormat; +use crate::config::{AudioFormat, Requantizer}; use crate::player::NUM_CHANNELS; use jack::{ AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, @@ -41,7 +41,7 @@ impl ProcessHandler for JackData { } impl Open for JackSink { - fn open(client_name: Option, format: AudioFormat) -> Self { + fn open(client_name: Option, format: AudioFormat, _requantizer: Requantizer) -> Self { if format != AudioFormat::F32 { warn!("JACK currently does not support {:?} output", format); } diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 72659f196..30b4981c1 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -1,9 +1,9 @@ -use crate::audio::AudioPacket; +use crate::audio::{AudioPacket, Requantizer}; use crate::config::AudioFormat; use std::io; pub trait Open { - fn open(_: Option, format: AudioFormat) -> Self; + fn open(_: Option, format: AudioFormat, requantizer: Requantizer) -> Self; } pub trait Sink { @@ -12,14 +12,18 @@ pub trait Sink { fn write(&mut self, packet: &AudioPacket) -> io::Result<()>; } -pub type SinkBuilder = fn(Option, AudioFormat) -> Box; +pub type SinkBuilder = fn(Option, AudioFormat, Requantizer) -> Box; pub trait SinkAsBytes { fn write_bytes(&mut self, data: &[u8]) -> io::Result<()>; } -fn mk_sink(device: Option, format: AudioFormat) -> Box { - Box::new(S::open(device, format)) +fn mk_sink( + device: Option, + format: AudioFormat, + requantizer: Requantizer, +) -> Box { + Box::new(S::open(device, format, requantizer)) } // reuse code for various backends @@ -32,19 +36,20 @@ macro_rules! sink_as_bytes { 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] = &convert::to_s32(samples, &mut self.requantizer); self.write_bytes(samples_s32.as_bytes()) } AudioFormat::S24 => { - let samples_s24: &[i32] = &convert::to_s24(samples); + let samples_s24: &[i32] = &convert::to_s24(samples, &mut self.requantizer); 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] = + &convert::to_s24_3(samples, &mut self.requantizer); self.write_bytes(samples_s24_3.as_bytes()) } AudioFormat::S16 => { - let samples_s16: &[i16] = &convert::to_s16(samples); + let samples_s16: &[i16] = &convert::to_s16(samples, &mut self.requantizer); self.write_bytes(samples_s16.as_bytes()) } }, @@ -105,6 +110,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 +122,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 4c6f27c11..68bba43f2 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -1,5 +1,5 @@ use super::{Open, Sink, SinkAsBytes}; -use crate::audio::AudioPacket; +use crate::audio::{AudioPacket, Requantizer}; use crate::config::AudioFormat; use std::fs::OpenOptions; use std::io::{self, Write}; @@ -7,10 +7,11 @@ use std::io::{self, Write}; pub struct StdoutSink { output: Box, format: AudioFormat, + requantizer: Requantizer, } impl Open for StdoutSink { - fn open(path: Option, format: AudioFormat) -> Self { + fn open(path: Option, format: AudioFormat, requantizer: Requantizer) -> Self { info!("Using pipe sink with format: {:?}", format); let output: Box = match path { @@ -18,7 +19,11 @@ impl Open for StdoutSink { _ => Box::new(io::stdout()), }; - Self { output, format } + Self { + output, + format, + requantizer, + } } } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 234a9af6a..2e4e7cfec 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -1,5 +1,5 @@ use super::{Open, Sink}; -use crate::audio::{convert, AudioPacket}; +use crate::audio::{convert, AudioPacket, Requantizer}; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; @@ -12,14 +12,17 @@ pub enum PortAudioSink<'a> { F32( Option>, StreamParameters, + Requantizer, ), S32( Option>, StreamParameters, + Requantizer, ), S16( Option>, StreamParameters, + Requantizer, ), } @@ -51,7 +54,7 @@ fn find_output(device: &str) -> Option { } impl<'a> Open for PortAudioSink<'a> { - fn open(device: Option, format: AudioFormat) -> PortAudioSink<'a> { + fn open(device: Option, format: AudioFormat, requantizer: Requantizer) -> PortAudioSink<'a> { info!("Using PortAudio sink with format: {:?}", format); warn!("This backend is known to panic on several platforms."); @@ -83,7 +86,7 @@ impl<'a> Open for PortAudioSink<'a> { suggested_latency: latency, data: 0.0 as $type, }; - $sink(None, params) + $sink(None, params, requantizer) }}; } match format { @@ -119,9 +122,9 @@ impl<'a> Sink for PortAudioSink<'a> { } match self { - Self::F32(stream, parameters) => start_sink!(ref mut stream, ref parameters), - Self::S32(stream, parameters) => start_sink!(ref mut stream, ref parameters), - Self::S16(stream, parameters) => start_sink!(ref mut stream, ref parameters), + Self::F32(stream, parameters, _) => start_sink!(ref mut stream, ref parameters), + Self::S32(stream, parameters, _) => start_sink!(ref mut stream, ref parameters), + Self::S16(stream, parameters, _) => start_sink!(ref mut stream, ref parameters), }; Ok(()) @@ -135,9 +138,9 @@ 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(()) @@ -152,15 +155,15 @@ impl<'a> Sink for PortAudioSink<'a> { let samples = packet.samples(); let result = match self { - Self::F32(stream, _parameters) => { + Self::F32(stream, _parameters, _) => { write_sink!(ref mut stream, samples) } - Self::S32(stream, _parameters) => { - let samples_s32: &[i32] = &convert::to_s32(samples); + Self::S32(stream, _parameters, ref mut requantizer) => { + let samples_s32: &[i32] = &convert::to_s32(samples, requantizer); write_sink!(ref mut stream, samples_s32) } - Self::S16(stream, _parameters) => { - let samples_s16: &[i16] = &convert::to_s16(samples); + Self::S16(stream, _parameters, ref mut requantizer) => { + let samples_s16: &[i16] = &convert::to_s16(samples, requantizer); write_sink!(ref mut stream, samples_s16) } }; diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index b165c0b2e..3f006b0ae 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -1,6 +1,6 @@ use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; -use crate::config::AudioFormat; +use crate::config::{AudioFormat, Requantizer}; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use libpulse_binding::{self as pulse, stream::Direction}; use libpulse_simple_binding::Simple; @@ -14,10 +14,11 @@ pub struct PulseAudioSink { ss: pulse::sample::Spec, device: Option, format: AudioFormat, + requantizer: Requantizer, } impl Open for PulseAudioSink { - fn open(device: Option, format: AudioFormat) -> Self { + fn open(device: Option, format: AudioFormat, requantizer: Requantizer) -> Self { info!("Using PulseAudio sink with format: {:?}", format); // PulseAudio calls S24 and S24_3 different from the rest of the world @@ -41,6 +42,7 @@ impl Open for PulseAudioSink { ss, device, format, + requantizer, } } } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 65436a326..6fb84bca8 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -5,7 +5,7 @@ use cpal::traits::{DeviceTrait, HostTrait}; use thiserror::Error; use super::Sink; -use crate::audio::{convert, AudioPacket}; +use crate::audio::{convert, AudioPacket, Requantizer}; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; @@ -16,16 +16,25 @@ use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; compile_error!("Rodio JACK backend is currently only supported on linux."); #[cfg(feature = "rodio-backend")] -pub fn mk_rodio(device: Option, format: AudioFormat) -> Box { - Box::new(open(cpal::default_host(), device, format)) +pub fn mk_rodio( + device: Option, + format: AudioFormat, + requantizer: Requantizer, +) -> Box { + Box::new(open(cpal::default_host(), device, format, requantizer)) } #[cfg(feature = "rodiojack-backend")] -pub fn mk_rodiojack(device: Option, format: AudioFormat) -> Box { +pub fn mk_rodiojack( + device: Option, + format: AudioFormat, + requantizer: Requantizer, +) -> Box { Box::new(open( cpal::host_from_id(cpal::HostId::Jack).unwrap(), device, format, + requantizer, )) } @@ -47,6 +56,7 @@ pub struct RodioSink { rodio_sink: rodio::Sink, format: AudioFormat, _stream: rodio::OutputStream, + requantizer: Requantizer, } fn list_formats(device: &rodio::Device) { @@ -151,7 +161,12 @@ fn create_sink( Ok((sink, stream)) } -pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> RodioSink { +pub fn open( + host: cpal::Host, + device: Option, + format: AudioFormat, + requantizer: Requantizer, +) -> RodioSink { debug!( "Using rodio sink with format {:?} and cpal host: {}", format, @@ -173,6 +188,7 @@ pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> Ro RodioSink { rodio_sink: sink, format, + requantizer, _stream: stream, } } @@ -189,7 +205,7 @@ impl Sink for RodioSink { self.rodio_sink.append(source); } AudioFormat::S16 => { - let samples_s16: &[i16] = &convert::to_s16(samples); + let samples_s16: &[i16] = &convert::to_s16(samples, &mut self.requantizer); 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 29566533b..6badd303f 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -1,18 +1,18 @@ use super::{Open, Sink}; -use crate::audio::{convert, AudioPacket}; +use crate::audio::{convert, AudioPacket, Requantizer}; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use sdl2::audio::{AudioQueue, AudioSpecDesired}; use std::{io, thread, time}; pub enum SdlSink { - F32(AudioQueue), - S32(AudioQueue), - S16(AudioQueue), + F32(AudioQueue, Requantizer), + S32(AudioQueue, Requantizer), + S16(AudioQueue, Requantizer), } impl Open for SdlSink { - fn open(device: Option, format: AudioFormat) -> Self { + fn open(device: Option, format: AudioFormat, requantizer: Requantizer) -> Self { info!("Using SDL sink with format: {:?}", format); if device.is_some() { @@ -35,7 +35,7 @@ impl Open for SdlSink { let queue: AudioQueue<$type> = audio .open_queue(None, &desired_spec) .expect("could not open SDL audio device"); - $sink(queue) + $sink(queue, requantizer) }}; } match format { @@ -58,9 +58,9 @@ impl Sink for SdlSink { }}; } match self { - Self::F32(queue) => start_sink!(queue), - Self::S32(queue) => start_sink!(queue), - Self::S16(queue) => start_sink!(queue), + Self::F32(queue, _) => start_sink!(queue), + Self::S32(queue, _) => start_sink!(queue), + Self::S16(queue, _) => start_sink!(queue), }; Ok(()) } @@ -73,9 +73,9 @@ impl Sink for SdlSink { }}; } match self { - Self::F32(queue) => stop_sink!(queue), - Self::S32(queue) => stop_sink!(queue), - Self::S16(queue) => stop_sink!(queue), + Self::F32(queue, _) => stop_sink!(queue), + Self::S32(queue, _) => stop_sink!(queue), + Self::S16(queue, _) => stop_sink!(queue), }; Ok(()) } @@ -92,17 +92,17 @@ impl Sink for SdlSink { let samples = packet.samples(); match self { - Self::F32(queue) => { + Self::F32(queue, _) => { drain_sink!(queue, AudioFormat::F32.size()); queue.queue(samples) } - Self::S32(queue) => { - let samples_s32: &[i32] = &convert::to_s32(samples); + Self::S32(queue, ref mut requantizer) => { + let samples_s32: &[i32] = &convert::to_s32(samples, requantizer); drain_sink!(queue, AudioFormat::S32.size()); queue.queue(samples_s32) } - Self::S16(queue) => { - let samples_s16: &[i16] = &convert::to_s16(samples); + Self::S16(queue, ref mut requantizer) => { + let samples_s16: &[i16] = &convert::to_s16(samples, requantizer); 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 b4af1b40f..959c74c0c 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -1,5 +1,5 @@ use super::{Open, Sink, SinkAsBytes}; -use crate::audio::AudioPacket; +use crate::audio::{AudioPacket, Requantizer}; use crate::config::AudioFormat; use shell_words::split; @@ -10,10 +10,11 @@ pub struct SubprocessSink { shell_command: String, child: Option, format: AudioFormat, + requantizer: Requantizer, } impl Open for SubprocessSink { - fn open(shell_command: Option, format: AudioFormat) -> Self { + fn open(shell_command: Option, format: AudioFormat, requantizer: Requantizer) -> Self { info!("Using subprocess sink with format: {:?}", format); if let Some(shell_command) = shell_command { @@ -21,6 +22,7 @@ impl Open for SubprocessSink { shell_command, child: None, format, + requantizer, } } else { panic!("subprocess sink requires specifying a shell command"); diff --git a/playback/src/config.rs b/playback/src/config.rs index f8f028933..312a4d903 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,4 +1,4 @@ -use crate::audio::convert::i24; +pub use crate::audio::convert::{i24, Requantizer}; use std::convert::TryFrom; use std::mem; use std::str::FromStr; diff --git a/src/main.rs b/src/main.rs index 78e7e2f96..18b743f4c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,11 @@ use sha1::{Digest, Sha1}; use tokio::sync::mpsc::UnboundedReceiver; use url::Url; +use librespot::audio::convert::Requantizer; +use librespot::audio::dither::{ + self, mk_ditherer, Ditherer, HighPassDitherer, NoDithering, TriangularDitherer, +}; +use librespot::audio::shape_noise::{self, mk_noise_shaper, NoShaping, NoiseShaper, Wannamaker9}; use librespot::connect::spirc::Spirc; use librespot::core::authentication::Credentials; use librespot::core::cache::Cache; @@ -108,10 +113,11 @@ fn print_version() { ); } -#[derive(Clone)] struct Setup { format: AudioFormat, - backend: fn(Option, AudioFormat) -> Box, + ditherer: fn() -> Box, + noise_shaper: fn() -> Box, + backend: fn(Option, AudioFormat, Requantizer) -> Box, device: Option, mixer: fn(Option) -> Box, @@ -181,6 +187,18 @@ fn get_setup(args: &[String]) -> Setup { "Output format (F32, S32, S24, S24_3 or S16). Defaults to S16", "FORMAT", ) + .optopt( + "", + "dither", + "Override the dither algorithm to use - [none, rect, sto, tri, hp, gauss].", + "DITHER", + ) + .optopt( + "", + "shape-noise", + "Override the noise shaping algorithm to use - [none, fract, iew5, iew9, fw3, fw9, fw24].", + "SHAPE_NOISE", + ) .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER") .optopt( "m", @@ -324,9 +342,36 @@ fn get_setup(args: &[String]) -> Setup { .map(|format| AudioFormat::try_from(format).expect("Invalid output format")) .unwrap_or_default(); + let ditherer_name = matches.opt_str("dither"); + let noise_shaper_name = matches.opt_str("shape-noise"); + + if format == AudioFormat::F32 && (ditherer_name.is_some() || noise_shaper_name.is_some()) { + unimplemented!( + "Dithering and noise shaping is not implemented for format {:?}", + format + ); + } + + let ditherer = match ditherer_name { + Some(_) => dither::find_ditherer(ditherer_name).expect("Invalid ditherer"), + _ => match format { + AudioFormat::S24 | AudioFormat::S24_3 => mk_ditherer::, + AudioFormat::S16 => mk_ditherer::, + _ => mk_ditherer::, + }, + }; + + let noise_shaper = match noise_shaper_name { + Some(_) => shape_noise::find_noise_shaper(noise_shaper_name).expect("Invalid noise shaper"), + _ => match format { + AudioFormat::S16 => mk_noise_shaper::, + _ => mk_noise_shaper::, + }, + }; + let device = matches.opt_str("device"); if device == Some("?".into()) { - backend(device, format); + backend(device, format, Requantizer::new(ditherer(), noise_shaper())); exit(0); } @@ -530,6 +575,8 @@ fn get_setup(args: &[String]) -> Setup { Setup { format, + ditherer, + noise_shaper, backend, cache, session_config, @@ -622,11 +669,13 @@ async fn main() { let audio_filter = mixer.get_audio_filter(); let format = setup.format; + let ditherer = setup.ditherer; + let noise_shaper = setup.noise_shaper; let backend = setup.backend; let device = setup.device.clone(); let (player, event_channel) = Player::new(player_config, session.clone(), audio_filter, move || { - (backend)(device, format) + (backend)(device, format, Requantizer::new(ditherer(), noise_shaper())) }); if setup.emit_sink_events { From f3553e12c911a18814a7052da93a0054f749c7ce Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 13 Apr 2021 23:12:34 +0200 Subject: [PATCH 02/23] cargo fmt --- playback/src/audio_backend/portaudio.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 2e4e7cfec..2d2a86f3b 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -54,7 +54,11 @@ fn find_output(device: &str) -> Option { } impl<'a> Open for PortAudioSink<'a> { - fn open(device: Option, format: AudioFormat, requantizer: Requantizer) -> PortAudioSink<'a> { + fn open( + device: Option, + format: AudioFormat, + requantizer: Requantizer, + ) -> PortAudioSink<'a> { info!("Using PortAudio sink with format: {:?}", format); warn!("This backend is known to panic on several platforms."); From fde697b7db4c671095f58cb49aacb269bf52ff1b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 13 Apr 2021 23:26:20 +0200 Subject: [PATCH 03/23] Fix example --- examples/play.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/examples/play.rs b/examples/play.rs index d6c7196df..b4b5fe945 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -1,5 +1,8 @@ use std::env; +use librespot::audio::convert::Requantizer; +use librespot::audio::dither::{self}; +use librespot::audio::shape_noise::{self}; use librespot::core::authentication::Credentials; use librespot::core::config::SessionConfig; use librespot::core::session::Session; @@ -24,6 +27,8 @@ async fn main() { let track = SpotifyId::from_base62(&args[3]).unwrap(); let backend = audio_backend::find(None).unwrap(); + let ditherer = dither::find_ditherer(None).unwrap(); + let noise_shaper = shape_noise::find_noise_shaper(None).unwrap(); println!("Connecting .."); let session = Session::connect(session_config, credentials, None) @@ -31,7 +36,11 @@ async fn main() { .unwrap(); let (mut player, _) = Player::new(player_config, session, None, move || { - backend(None, audio_format) + backend( + None, + audio_format, + Requantizer::new(ditherer(), noise_shaper()), + ) }); player.load(track, true, 0); From 977dbede1ae46c5ced49f4ed699409aa33e5cfae Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 14 Apr 2021 12:26:36 +0200 Subject: [PATCH 04/23] Correct dithering noise powers --- audio/src/dither.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/audio/src/dither.rs b/audio/src/dither.rs index 23e08c25d..430b15b60 100644 --- a/audio/src/dither.rs +++ b/audio/src/dither.rs @@ -1,9 +1,5 @@ use rand::rngs::ThreadRng; -use rand::Rng; -use rand_distr::{Distribution, StandardNormal, Triangular, Uniform}; - -const MIN_BOUND: f32 = -0.5; -const MAX_BOUND: f32 = 0.5; +use rand_distr::{Distribution, Normal, Triangular, Uniform}; // Dithering lowers digital-to-analog conversion ("requantization") error, // lowering distortion and replacing it with a constant, fixed noise level, @@ -61,7 +57,7 @@ impl Ditherer for RectangularDitherer { debug!("Ditherer: Rectangular"); Self { cached_rng: rand::thread_rng(), - distribution: Uniform::new_inclusive(MIN_BOUND, MAX_BOUND), + distribution: Uniform::new_inclusive(-0.5, 0.5), // 1 LSB } } @@ -110,7 +106,7 @@ impl Ditherer for TriangularDitherer { debug!("Ditherer: Triangular"); Self { cached_rng: rand::thread_rng(), - distribution: Triangular::new(MIN_BOUND, MAX_BOUND, MIN_BOUND + MAX_BOUND).unwrap(), + distribution: Triangular::new(-1.0, 1.0, 0.0).unwrap(), // 2 LSB } } @@ -124,6 +120,7 @@ impl Ditherer for TriangularDitherer { // if a more analog sound is sought after. pub struct GaussianDitherer { cached_rng: ThreadRng, + distribution: Normal, } impl Ditherer for GaussianDitherer { @@ -131,11 +128,12 @@ impl Ditherer for GaussianDitherer { debug!("Ditherer: Gaussian"); Self { cached_rng: rand::thread_rng(), + distribution: Normal::new(0.0, 0.25).unwrap(), // 1/2 LSB } } fn noise(&mut self, _sample: f32) -> f32 { - (self.cached_rng.sample::(StandardNormal) - MAX_BOUND) / 2.0 // 1/2 LSB + self.distribution.sample(&mut self.cached_rng) } } @@ -155,7 +153,7 @@ impl Ditherer for HighPassDitherer { Self { previous_noise: 0.0, cached_rng: rand::thread_rng(), - distribution: Uniform::new_inclusive(MIN_BOUND, MAX_BOUND), + distribution: Uniform::new_inclusive(-0.5, 0.5), // 1 LSB +/- 1 LSB (previous) = 2 LSB } } From 6ea089cb3323d46ae2ed80b81c13fc912aa4a49d Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 14 Apr 2021 12:42:08 +0200 Subject: [PATCH 05/23] Disable noise shaping by default --- audio/src/dither.rs | 21 +++++++++------------ audio/src/shape_noise.rs | 1 + src/main.rs | 20 +++++++------------- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/audio/src/dither.rs b/audio/src/dither.rs index 430b15b60..c53c07ce4 100644 --- a/audio/src/dither.rs +++ b/audio/src/dither.rs @@ -8,20 +8,17 @@ use rand_distr::{Distribution, Normal, Triangular, Uniform}; // audio to a perceived 120 dB. // // Guidance: experts can configure many different configurations of ditherers -// and noise shapers. For the rest of us, these are sane defaults depending -// on the output format: +// and noise shapers. For the rest of us: // -// | Format | Recommended ditherer | Recommended noise shaper | -// +------------+----------------------+------------------------------------------+ -// | S16 | Triangular (tri) | Wannamaker9 (fw9) or Wannamaker24 (fw24) | -// | S24, S24_3 | High Pass (hp) | None | -// | S32 | None | None | -// | F32 | Not supported | Not supported | +// * Don't dither or shape noise on S32 or F32 (not supported anyway). +// +// * Generally use high pass dithering (hp) without noise shaping. Depending +// on personal preference you may use Gaussian dithering (gauss) instead +// if you prefer a more analog sound. +// +// * On power-constrained hardware, use the fraction saving noise shaper +// instead of dithering. // -// Notes: -// * Try swapping ditherers with Gaussian if you prefer a more analog sound. -// * Try swapping noise shapers with Fraction Saving if you are running on -// power-constrained hardware. In this case, disable dithering. pub trait Ditherer { fn new() -> Self where diff --git a/audio/src/shape_noise.rs b/audio/src/shape_noise.rs index 6876b7c51..35c9e91c0 100644 --- a/audio/src/shape_noise.rs +++ b/audio/src/shape_noise.rs @@ -18,6 +18,7 @@ const NUM_CHANNELS: usize = 2; // absolute noise power and perceived noise. Co-incidentally, the lower the // perceived noise, the higher the absolute power, and the higher the CPU- // usage. All shapers have something going for them. +// pub trait NoiseShaper { fn new() -> Self where diff --git a/src/main.rs b/src/main.rs index 18b743f4c..40f5543af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,10 +6,8 @@ use tokio::sync::mpsc::UnboundedReceiver; use url::Url; use librespot::audio::convert::Requantizer; -use librespot::audio::dither::{ - self, mk_ditherer, Ditherer, HighPassDitherer, NoDithering, TriangularDitherer, -}; -use librespot::audio::shape_noise::{self, mk_noise_shaper, NoShaping, NoiseShaper, Wannamaker9}; +use librespot::audio::dither::{self, mk_ditherer, Ditherer, HighPassDitherer, NoDithering}; +use librespot::audio::shape_noise::{self, NoiseShaper}; use librespot::connect::spirc::Spirc; use librespot::core::authentication::Credentials; use librespot::core::cache::Cache; @@ -355,19 +353,15 @@ fn get_setup(args: &[String]) -> Setup { let ditherer = match ditherer_name { Some(_) => dither::find_ditherer(ditherer_name).expect("Invalid ditherer"), _ => match format { - AudioFormat::S24 | AudioFormat::S24_3 => mk_ditherer::, - AudioFormat::S16 => mk_ditherer::, + AudioFormat::S16 | AudioFormat::S24 | AudioFormat::S24_3 => { + mk_ditherer:: + } _ => mk_ditherer::, }, }; - let noise_shaper = match noise_shaper_name { - Some(_) => shape_noise::find_noise_shaper(noise_shaper_name).expect("Invalid noise shaper"), - _ => match format { - AudioFormat::S16 => mk_noise_shaper::, - _ => mk_noise_shaper::, - }, - }; + let noise_shaper = + shape_noise::find_noise_shaper(noise_shaper_name).expect("Invalid noise shaper"); let device = matches.opt_str("device"); if device == Some("?".into()) { From 00b36be390c5f30edda56334b771f2995c8bde0a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 14 Apr 2021 15:12:21 +0200 Subject: [PATCH 06/23] Document default ditherer and noise shaper --- src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 40f5543af..ca348c197 100644 --- a/src/main.rs +++ b/src/main.rs @@ -188,13 +188,13 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", "dither", - "Override the dither algorithm to use - [none, rect, sto, tri, hp, gauss].", + "Specify the dither algorithm to use - [none, rect, sto, tri, hp, gauss]. Defaults to 'hp' for formats S16, S24, S24_3 and 'none' for other formats.", "DITHER", ) .optopt( "", "shape-noise", - "Override the noise shaping algorithm to use - [none, fract, iew5, iew9, fw3, fw9, fw24].", + "Specify the noise shaping algorithm to use - [none, fract, iew5, iew9, fw3, fw9, fw24]. Defaults to 'none'.", "SHAPE_NOISE", ) .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER") From f7ac00106c2cad068f67fa15eb6c5ad412db93fa Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 14 Apr 2021 20:40:55 +0200 Subject: [PATCH 07/23] Fix panic when no noise shaper is specified --- audio/src/dither.rs | 2 +- audio/src/shape_noise.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/audio/src/dither.rs b/audio/src/dither.rs index c53c07ce4..51d20c0af 100644 --- a/audio/src/dither.rs +++ b/audio/src/dither.rs @@ -181,6 +181,6 @@ pub fn find_ditherer(name: Option) -> Option Box> .iter() .find(|ditherer| name == ditherer.0) .map(|ditherer| ditherer.1), - _ => None, + _ => Some(mk_ditherer::), } } diff --git a/audio/src/shape_noise.rs b/audio/src/shape_noise.rs index 35c9e91c0..215e36b50 100644 --- a/audio/src/shape_noise.rs +++ b/audio/src/shape_noise.rs @@ -236,6 +236,6 @@ pub fn find_noise_shaper(name: Option) -> Option Box None, + _ => Some(mk_noise_shaper::), } } From 636d1812fe7e7f04a09386c3cff815312aad9ee9 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 14 Apr 2021 21:59:46 +0200 Subject: [PATCH 08/23] Implement fmt::Display for Ditherer and NoiseShaper --- audio/src/convert.rs | 4 ++++ audio/src/dither.rs | 38 ++++++++++++++++++++++++++++++++------ audio/src/shape_noise.rs | 31 +++++++++++++++++++++---------- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/audio/src/convert.rs b/audio/src/convert.rs index e1eb90a1d..30f48d2a9 100644 --- a/audio/src/convert.rs +++ b/audio/src/convert.rs @@ -21,6 +21,10 @@ pub struct Requantizer { impl Requantizer { pub fn new(ditherer: Box, noise_shaper: Box) -> Self { + info!( + "Requantizing with ditherer: {} and noise shaper: {}", + ditherer, noise_shaper + ); Self { ditherer, noise_shaper, diff --git a/audio/src/dither.rs b/audio/src/dither.rs index 51d20c0af..4aa7fa05f 100644 --- a/audio/src/dither.rs +++ b/audio/src/dither.rs @@ -1,5 +1,6 @@ use rand::rngs::ThreadRng; use rand_distr::{Distribution, Normal, Triangular, Uniform}; +use std::fmt; // Dithering lowers digital-to-analog conversion ("requantization") error, // lowering distortion and replacing it with a constant, fixed noise level, @@ -23,16 +24,26 @@ pub trait Ditherer { fn new() -> Self where Self: Sized; + fn name(&self) -> String; 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()) + } +} + pub struct NoDithering {} impl Ditherer for NoDithering { fn new() -> Self { - debug!("Ditherer: None"); Self {} } + fn name(&self) -> String { + String::from("None") + } + fn noise(&mut self, _sample: f32) -> f32 { 0.0 } @@ -51,13 +62,16 @@ pub struct RectangularDitherer { impl Ditherer for RectangularDitherer { fn new() -> Self { - debug!("Ditherer: Rectangular"); Self { cached_rng: rand::thread_rng(), distribution: Uniform::new_inclusive(-0.5, 0.5), // 1 LSB } } + fn name(&self) -> String { + String::from("Rectangular") + } + fn noise(&mut self, _sample: f32) -> f32 { self.distribution.sample(&mut self.cached_rng) } @@ -72,13 +86,16 @@ pub struct StochasticDitherer { impl Ditherer for StochasticDitherer { fn new() -> Self { - debug!("Ditherer: Stochastic"); Self { cached_rng: rand::thread_rng(), distribution: Uniform::new(0.0, 1.0), } } + fn name(&self) -> String { + String::from("Stochastic") + } + fn noise(&mut self, sample: f32) -> f32 { let fract = sample.fract(); if self.distribution.sample(&mut self.cached_rng) <= fract { @@ -100,13 +117,16 @@ pub struct TriangularDitherer { impl Ditherer for TriangularDitherer { fn new() -> Self { - debug!("Ditherer: Triangular"); Self { cached_rng: rand::thread_rng(), distribution: Triangular::new(-1.0, 1.0, 0.0).unwrap(), // 2 LSB } } + fn name(&self) -> String { + String::from("Triangular") + } + fn noise(&mut self, _sample: f32) -> f32 { self.distribution.sample(&mut self.cached_rng) } @@ -122,13 +142,16 @@ pub struct GaussianDitherer { impl Ditherer for GaussianDitherer { fn new() -> Self { - debug!("Ditherer: Gaussian"); Self { cached_rng: rand::thread_rng(), distribution: Normal::new(0.0, 0.25).unwrap(), // 1/2 LSB } } + fn name(&self) -> String { + String::from("Gaussian") + } + fn noise(&mut self, _sample: f32) -> f32 { self.distribution.sample(&mut self.cached_rng) } @@ -146,7 +169,6 @@ pub struct HighPassDitherer { impl Ditherer for HighPassDitherer { fn new() -> Self { - debug!("Ditherer: High-Pass"); Self { previous_noise: 0.0, cached_rng: rand::thread_rng(), @@ -154,6 +176,10 @@ impl Ditherer for HighPassDitherer { } } + fn name(&self) -> String { + String::from("High Pass") + } + 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_noise; diff --git a/audio/src/shape_noise.rs b/audio/src/shape_noise.rs index 215e36b50..6a60010dc 100644 --- a/audio/src/shape_noise.rs +++ b/audio/src/shape_noise.rs @@ -1,3 +1,5 @@ +use std::fmt; + const NUM_CHANNELS: usize = 2; // Noise shapers take the noise caused by errors in the digital-to-analog @@ -23,19 +25,26 @@ pub trait NoiseShaper { fn new() -> Self where Self: Sized; + fn name(&self) -> String; fn shape(&mut self, sample: f32, noise: f32) -> f32; } +impl fmt::Display for dyn NoiseShaper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name()) + } +} + pub struct NoShaping {} impl NoiseShaper for NoShaping { fn new() -> Self { - // Doing nothing here means that the requantizer will simply cast to - // integer. For Rust the default behavior then is to round towards - // zero. - debug!("Noise Shaper: None (rounding towards zero)"); Self {} } + fn name(&self) -> String { + String::from("None") + } + fn shape(&mut self, sample: f32, noise: f32) -> f32 { sample + noise } @@ -56,13 +65,16 @@ pub struct FractionSaver { impl NoiseShaper for FractionSaver { fn new() -> Self { - debug!("Noise Shaper: Fraction Saver"); Self { active_channel: 0, previous_fractions: [0.0; NUM_CHANNELS], } } + fn name(&self) -> String { + String::from("Fraction Saver") + } + fn shape(&mut self, sample: f32, noise: f32) -> f32 { let sample_with_fraction = sample + noise + self.previous_fractions[self.active_channel]; self.previous_fractions[self.active_channel] = sample_with_fraction.fract(); @@ -85,16 +97,15 @@ macro_rules! fir_shaper { impl NoiseShaper for $name { fn new() -> Self { - debug!( - "Noise Shaper: {}, {} taps", - $description, - Self::WEIGHTS.len() - ); Self { fir: FIR::new(&Self::WEIGHTS), } } + fn name(&self) -> String { + format!("{}, {} taps", $description, Self::WEIGHTS.len()) + } + fn shape(&mut self, sample: f32, noise: f32) -> f32 { self.fir.shape(sample, noise) } From 2f11bbc3e57f79c52d299b9e7083600a414d1824 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 15 Apr 2021 21:53:23 +0200 Subject: [PATCH 09/23] Refactor name() into &'static str --- audio/src/dither.rs | 26 +++++++++++++------------- audio/src/shape_noise.rs | 14 +++++++------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/audio/src/dither.rs b/audio/src/dither.rs index 4aa7fa05f..c32ea1192 100644 --- a/audio/src/dither.rs +++ b/audio/src/dither.rs @@ -24,7 +24,7 @@ pub trait Ditherer { fn new() -> Self where Self: Sized; - fn name(&self) -> String; + fn name(&self) -> &'static str; fn noise(&mut self, sample: f32) -> f32; } @@ -40,8 +40,8 @@ impl Ditherer for NoDithering { Self {} } - fn name(&self) -> String { - String::from("None") + fn name(&self) -> &'static str { + "None" } fn noise(&mut self, _sample: f32) -> f32 { @@ -68,8 +68,8 @@ impl Ditherer for RectangularDitherer { } } - fn name(&self) -> String { - String::from("Rectangular") + fn name(&self) -> &'static str { + "Rectangular" } fn noise(&mut self, _sample: f32) -> f32 { @@ -92,8 +92,8 @@ impl Ditherer for StochasticDitherer { } } - fn name(&self) -> String { - String::from("Stochastic") + fn name(&self) -> &'static str { + "Stochastic" } fn noise(&mut self, sample: f32) -> f32 { @@ -123,8 +123,8 @@ impl Ditherer for TriangularDitherer { } } - fn name(&self) -> String { - String::from("Triangular") + fn name(&self) -> &'static str { + "Triangular" } fn noise(&mut self, _sample: f32) -> f32 { @@ -148,8 +148,8 @@ impl Ditherer for GaussianDitherer { } } - fn name(&self) -> String { - String::from("Gaussian") + fn name(&self) -> &'static str { + "Gaussian" } fn noise(&mut self, _sample: f32) -> f32 { @@ -176,8 +176,8 @@ impl Ditherer for HighPassDitherer { } } - fn name(&self) -> String { - String::from("High Pass") + fn name(&self) -> &'static str { + "High Pass" } fn noise(&mut self, _sample: f32) -> f32 { diff --git a/audio/src/shape_noise.rs b/audio/src/shape_noise.rs index 6a60010dc..4f437508b 100644 --- a/audio/src/shape_noise.rs +++ b/audio/src/shape_noise.rs @@ -25,7 +25,7 @@ pub trait NoiseShaper { fn new() -> Self where Self: Sized; - fn name(&self) -> String; + fn name(&self) -> &'static str; fn shape(&mut self, sample: f32, noise: f32) -> f32; } @@ -41,8 +41,8 @@ impl NoiseShaper for NoShaping { Self {} } - fn name(&self) -> String { - String::from("None") + fn name(&self) -> &'static str { + "None" } fn shape(&mut self, sample: f32, noise: f32) -> f32 { @@ -71,8 +71,8 @@ impl NoiseShaper for FractionSaver { } } - fn name(&self) -> String { - String::from("Fraction Saver") + fn name(&self) -> &'static str { + "Fraction Saver" } fn shape(&mut self, sample: f32, noise: f32) -> f32 { @@ -102,8 +102,8 @@ macro_rules! fir_shaper { } } - fn name(&self) -> String { - format!("{}, {} taps", $description, Self::WEIGHTS.len()) + fn name(&self) -> &'static str { + concat!($description, ", ", stringify!(Self::WEIGHTS.len()), " taps") } fn shape(&mut self, sample: f32, noise: f32) -> f32 { From 5dec737216765700714f10441b4f7125a3c6cd38 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 15 Apr 2021 22:10:57 +0200 Subject: [PATCH 10/23] Simplify name macro --- audio/src/shape_noise.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio/src/shape_noise.rs b/audio/src/shape_noise.rs index 4f437508b..833b8a8dc 100644 --- a/audio/src/shape_noise.rs +++ b/audio/src/shape_noise.rs @@ -103,7 +103,7 @@ macro_rules! fir_shaper { } fn name(&self) -> &'static str { - concat!($description, ", ", stringify!(Self::WEIGHTS.len()), " taps") + concat!($description, ", ", stringify!($taps), " taps") } fn shape(&mut self, sample: f32, noise: f32) -> f32 { From c995088eab2e035597f5206ea091b9b7961527da Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 19 Apr 2021 22:59:52 +0200 Subject: [PATCH 11/23] Move dithering and noise shaping to PlayerConfig Further improvements: * Restore Gaussian noise power level * Move `start_stop_noop` macro to default trait implementation * Improve readability and visibility * Fix clippy lints --- audio/src/convert.rs | 4 +- audio/src/dither.rs | 37 ++++-- audio/src/lib.rs | 2 - audio/src/shape_noise.rs | 32 +++-- playback/src/audio_backend/alsa.rs | 4 +- playback/src/audio_backend/gstreamer.rs | 5 +- playback/src/audio_backend/jackaudio.rs | 10 +- playback/src/audio_backend/mod.rs | 44 +++---- playback/src/audio_backend/pipe.rs | 10 +- playback/src/audio_backend/portaudio.rs | 31 ++--- playback/src/audio_backend/pulseaudio.rs | 8 +- playback/src/audio_backend/rodio.rs | 30 +---- playback/src/audio_backend/sdl.rs | 30 ++--- playback/src/audio_backend/subprocess.rs | 4 +- playback/src/config.rs | 13 +- playback/src/player.rs | 10 +- src/main.rs | 148 ++++++++++++----------- 17 files changed, 206 insertions(+), 216 deletions(-) diff --git a/audio/src/convert.rs b/audio/src/convert.rs index 30f48d2a9..54d978bde 100644 --- a/audio/src/convert.rs +++ b/audio/src/convert.rs @@ -1,5 +1,5 @@ -use crate::dither::*; -use crate::shape_noise::*; +use crate::dither::Ditherer; +use crate::shape_noise::NoiseShaper; use zerocopy::AsBytes; #[derive(AsBytes, Copy, Clone, Debug)] diff --git a/audio/src/dither.rs b/audio/src/dither.rs index c32ea1192..1fc1f97a5 100644 --- a/audio/src/dither.rs +++ b/audio/src/dither.rs @@ -11,14 +11,22 @@ use std::fmt; // Guidance: experts can configure many different configurations of ditherers // and noise shapers. For the rest of us: // -// * Don't dither or shape noise on S32 or F32 (not supported anyway). +// * Don't dither or shape noise on S32 or F32. On F32 it's not supported +// anyway (there are no rounding errors due to integer conversions) and on +// S32 the noise level is so far down that it is simply inaudible. // // * Generally use high pass dithering (hp) without noise shaping. Depending -// on personal preference you may use Gaussian dithering (gauss) instead -// if you prefer a more analog sound. +// on personal preference you may use Gaussian dithering (gauss) instead; +// it's not as good objectively, but it may be preferred subjectively if +// you are looking for a more "analog" sound. // // * On power-constrained hardware, use the fraction saving noise shaper -// instead of dithering. +// instead of dithering. Performance-wise, this is not necessary even on a +// Raspberry Pi Zero, but if you're on batteries... +// +// 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 trait Ditherer { fn new() -> Self @@ -28,6 +36,12 @@ pub trait Ditherer { fn noise(&mut self, sample: f32) -> f32; } +impl dyn Ditherer { + pub fn default() -> fn() -> Box { + mk_ditherer:: + } +} + impl fmt::Display for dyn Ditherer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.name()) @@ -52,7 +66,7 @@ impl Ditherer for NoDithering { // "True" white noise (refer to Gaussian for analog source hiss). Advantages: // least CPU-intensive dither, lowest signal-to-noise ratio. Disadvantage: // highest perceived loudness, suffers from intermodulation distortion unless -// you are using this for subtractive dithering, which you most likely are not +// you are using this for subtractive dithering, which you most likely are not, // and is not supported by any of the librespot backends. Guidance: use some // other ditherer unless you know what you're doing. pub struct RectangularDitherer { @@ -64,7 +78,8 @@ impl Ditherer for RectangularDitherer { fn new() -> Self { Self { cached_rng: rand::thread_rng(), - distribution: Uniform::new_inclusive(-0.5, 0.5), // 1 LSB + // 1 LSB peak-to-peak needed to linearize the response: + distribution: Uniform::new_inclusive(-0.5, 0.5), } } @@ -119,7 +134,8 @@ impl Ditherer for TriangularDitherer { fn new() -> Self { Self { cached_rng: rand::thread_rng(), - distribution: Triangular::new(-1.0, 1.0, 0.0).unwrap(), // 2 LSB + // 2 LSB peak-to-peak needed to linearize the response: + distribution: Triangular::new(-1.0, 1.0, 0.0).unwrap(), } } @@ -144,7 +160,8 @@ impl Ditherer for GaussianDitherer { fn new() -> Self { Self { cached_rng: rand::thread_rng(), - distribution: Normal::new(0.0, 0.25).unwrap(), // 1/2 LSB + // 1/2 LSB RMS needed to linearize the response: + distribution: Normal::new(0.0, 0.5).unwrap(), } } @@ -192,7 +209,9 @@ pub fn mk_ditherer() -> Box { Box::new(D::new()) } -pub const DITHERERS: &'static [(&'static str, fn() -> Box)] = &[ +pub type DithererBuilder = fn() -> Box; + +pub const DITHERERS: &[(&str, DithererBuilder)] = &[ ("none", mk_ditherer::), ("rect", mk_ditherer::), ("sto", mk_ditherer::), diff --git a/audio/src/lib.rs b/audio/src/lib.rs index c82cb93b5..c3340fe90 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -28,13 +28,11 @@ mod range_set; pub use convert::Requantizer; pub use decrypt::AudioDecrypt; -pub use dither::{find_ditherer, Ditherer}; pub use fetch::{AudioFile, StreamLoaderController}; pub use fetch::{ READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_BEFORE_PLAYBACK_SECONDS, READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, }; -pub use shape_noise::{find_noise_shaper, NoiseShaper}; use std::fmt; pub enum AudioPacket { diff --git a/audio/src/shape_noise.rs b/audio/src/shape_noise.rs index 833b8a8dc..a062b8de3 100644 --- a/audio/src/shape_noise.rs +++ b/audio/src/shape_noise.rs @@ -29,6 +29,12 @@ pub trait NoiseShaper { fn shape(&mut self, sample: f32, noise: f32) -> f32; } +impl dyn NoiseShaper { + pub fn default() -> fn() -> Box { + mk_noise_shaper:: + } +} + impl fmt::Display for dyn NoiseShaper { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.name()) @@ -78,7 +84,7 @@ impl NoiseShaper for FractionSaver { fn shape(&mut self, sample: f32, noise: f32) -> f32 { let sample_with_fraction = sample + noise + self.previous_fractions[self.active_channel]; self.previous_fractions[self.active_channel] = sample_with_fraction.fract(); - self.active_channel = self.active_channel ^ 1; + self.active_channel ^= 1; sample_with_fraction.floor() } } @@ -92,13 +98,13 @@ impl NoiseShaper for FractionSaver { macro_rules! fir_shaper { ($name: ident, $description: tt, $taps: expr, $weights: expr) => { pub struct $name { - fir: FIR, + filter: FirFilter, } impl NoiseShaper for $name { fn new() -> Self { Self { - fir: FIR::new(&Self::WEIGHTS), + filter: FirFilter::new(&Self::WEIGHTS), } } @@ -107,7 +113,7 @@ macro_rules! fir_shaper { } fn shape(&mut self, sample: f32, noise: f32) -> f32 { - self.fir.shape(sample, noise) + self.filter.shape(sample, noise) } } @@ -170,13 +176,13 @@ fir_shaper!( "Wannamaker F-weighted", 24, [ - 2.391510, -3.284444, 3.679506, -3.635044, 2.524185, -1.146701, 0.115354, 0.513745, - -0.749277, 0.512386, -0.188997, -0.043705, 0.149843, -0.151186, 0.076302, -0.012070, + 2.39151, -3.284444, 3.679506, -3.635044, 2.524185, -1.146701, 0.115354, 0.513745, + -0.749277, 0.512386, -0.188997, -0.043705, 0.149843, -0.151186, 0.076302, -0.01207, -0.021127, 0.025232, -0.016121, 0.004453, 0.000876, -0.001799, 0.000774, -0.000128 ] ); -struct FIR { +struct FirFilter { taps: usize, weights: Vec, active_channel: usize, @@ -184,7 +190,7 @@ struct FIR { buffer_index: usize, } -impl FIR { +impl FirFilter { fn new(weights: &[f32]) -> Self { let taps = weights.len(); Self { @@ -200,7 +206,7 @@ impl FIR { // apply FIR filter let mut sample_with_shaped_noise = sample as f64; for index in 0..self.taps { - sample_with_shaped_noise = sample_with_shaped_noise + self.weighted_error(index); + sample_with_shaped_noise += self.weighted_error(index); } let dithered_sample = (sample_with_shaped_noise + noise as f64).round(); @@ -211,13 +217,13 @@ impl FIR { self.buffer_index = (self.buffer_index + self.active_channel) % self.taps; let index = self.index_at_samples_ago(0); self.error_buffer[index] = sample_with_shaped_noise - dithered_sample; - self.active_channel = self.active_channel ^ 1; + self.active_channel ^= 1; dithered_sample as f32 } } -impl FIR { +impl FirFilter { fn index_at_samples_ago(&self, errors_ago: usize) -> usize { ((self.buffer_index + self.taps - errors_ago) % self.taps) + self.taps * self.active_channel } @@ -231,7 +237,9 @@ pub fn mk_noise_shaper() -> Box { Box::new(S::new()) } -pub const NOISE_SHAPERS: &'static [(&'static str, fn() -> Box)] = &[ +pub type NoiseShaperBuilder = fn() -> Box; + +pub const NOISE_SHAPERS: &[(&str, NoiseShaperBuilder)] = &[ ("none", mk_noise_shaper::), ("fract", mk_noise_shaper::), ("iew5", mk_noise_shaper::), diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 2d9ff03e7..2bd27437b 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -18,7 +18,6 @@ pub struct AlsaSink { format: AudioFormat, device: String, buffer: Vec, - requantizer: Requantizer, } fn list_outputs() { @@ -72,7 +71,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box } impl Open for AlsaSink { - fn open(device: Option, format: AudioFormat, requantizer: Requantizer) -> Self { + fn open(device: Option, format: AudioFormat) -> Self { info!("Using Alsa sink with format: {:?}", format); let name = match device.as_ref().map(AsRef::as_ref) { @@ -91,7 +90,6 @@ impl Open for AlsaSink { format, device: name, buffer: vec![], - requantizer, } } } diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 58f59396c..8dea5316a 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -17,11 +17,10 @@ pub struct GstreamerSink { tx: SyncSender>, pipeline: gst::Pipeline, format: AudioFormat, - requantizer: Requantizer, } impl Open for GstreamerSink { - fn open(device: Option, format: AudioFormat, requantizer: Requantizer) -> Self { + fn open(device: Option, format: AudioFormat) -> Self { info!("Using GStreamer sink with format: {:?}", format); gst::init().expect("failed to init GStreamer!"); @@ -116,13 +115,11 @@ impl Open for GstreamerSink { tx, pipeline, format, - requantizer, } } } 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 f72a88466..8681b72b0 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -1,6 +1,6 @@ use super::{Open, Sink}; -use crate::audio::AudioPacket; -use crate::config::{AudioFormat, Requantizer}; +use crate::audio::{AudioPacket, Requantizer}; +use crate::config::AudioFormat; use crate::player::NUM_CHANNELS; use jack::{ AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, @@ -41,7 +41,7 @@ impl ProcessHandler for JackData { } impl Open for JackSink { - fn open(client_name: Option, format: AudioFormat, _requantizer: Requantizer) -> Self { + fn open(client_name: Option, format: AudioFormat) -> Self { if format != AudioFormat::F32 { warn!("JACK currently does not support {:?} output", format); } @@ -69,9 +69,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, _requantizer: &mut Requantizer) -> 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 30b4981c1..c92b4f244 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -3,53 +3,52 @@ use crate::config::AudioFormat; use std::io; pub trait Open { - fn open(_: Option, format: AudioFormat, requantizer: Requantizer) -> Self; + fn open(_: Option, format: AudioFormat) -> Self; } 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, requantizer: &mut Requantizer) -> io::Result<()>; } -pub type SinkBuilder = fn(Option, AudioFormat, Requantizer) -> Box; +pub type SinkBuilder = fn(Option, AudioFormat) -> Box; pub trait SinkAsBytes { fn write_bytes(&mut self, data: &[u8]) -> io::Result<()>; } -fn mk_sink( - device: Option, - format: AudioFormat, - requantizer: Requantizer, -) -> Box { - Box::new(S::open(device, format, requantizer)) +fn mk_sink(device: Option, format: AudioFormat) -> Box { + Box::new(S::open(device, format)) } // reuse code for various backends macro_rules! sink_as_bytes { () => { - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, requantizer: &mut Requantizer) -> io::Result<()> { use crate::audio::convert::{self, 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, &mut self.requantizer); + let samples_s32: &[i32] = &convert::to_s32(samples, requantizer); self.write_bytes(samples_s32.as_bytes()) } AudioFormat::S24 => { - let samples_s24: &[i32] = &convert::to_s24(samples, &mut self.requantizer); + let samples_s24: &[i32] = &convert::to_s24(samples, requantizer); self.write_bytes(samples_s24.as_bytes()) } AudioFormat::S24_3 => { - let samples_s24_3: &[i24] = - &convert::to_s24_3(samples, &mut self.requantizer); + let samples_s24_3: &[i24] = &convert::to_s24_3(samples, requantizer); self.write_bytes(samples_s24_3.as_bytes()) } AudioFormat::S16 => { - let samples_s16: &[i16] = &convert::to_s16(samples, &mut self.requantizer); + let samples_s16: &[i16] = &convert::to_s16(samples, requantizer); self.write_bytes(samples_s16.as_bytes()) } }, @@ -59,17 +58,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")] diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 68bba43f2..5a0d991aa 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -7,11 +7,10 @@ use std::io::{self, Write}; pub struct StdoutSink { output: Box, format: AudioFormat, - requantizer: Requantizer, } impl Open for StdoutSink { - fn open(path: Option, format: AudioFormat, requantizer: Requantizer) -> Self { + fn open(path: Option, format: AudioFormat) -> Self { info!("Using pipe sink with format: {:?}", format); let output: Box = match path { @@ -19,16 +18,11 @@ impl Open for StdoutSink { _ => Box::new(io::stdout()), }; - Self { - output, - format, - requantizer, - } + Self { output, format } } } 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 2d2a86f3b..385d6e3f8 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -12,17 +12,14 @@ pub enum PortAudioSink<'a> { F32( Option>, StreamParameters, - Requantizer, ), S32( Option>, StreamParameters, - Requantizer, ), S16( Option>, StreamParameters, - Requantizer, ), } @@ -54,11 +51,7 @@ fn find_output(device: &str) -> Option { } impl<'a> Open for PortAudioSink<'a> { - fn open( - device: Option, - format: AudioFormat, - requantizer: Requantizer, - ) -> 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."); @@ -90,7 +83,7 @@ impl<'a> Open for PortAudioSink<'a> { suggested_latency: latency, data: 0.0 as $type, }; - $sink(None, params, requantizer) + $sink(None, params) }}; } match format { @@ -126,9 +119,9 @@ impl<'a> Sink for PortAudioSink<'a> { } match self { - Self::F32(stream, parameters, _) => start_sink!(ref mut stream, ref parameters), - Self::S32(stream, parameters, _) => start_sink!(ref mut stream, ref parameters), - Self::S16(stream, parameters, _) => start_sink!(ref mut stream, ref parameters), + Self::F32(stream, parameters) => start_sink!(ref mut stream, ref parameters), + Self::S32(stream, parameters) => start_sink!(ref mut stream, ref parameters), + Self::S16(stream, parameters) => start_sink!(ref mut stream, ref parameters), }; Ok(()) @@ -142,15 +135,15 @@ impl<'a> Sink for PortAudioSink<'a> { }}; } match self { - Self::F32(stream, _, _) => stop_sink!(ref mut stream), - Self::S32(stream, _, _) => stop_sink!(ref mut stream), - Self::S16(stream, _, _) => 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, requantizer: &mut Requantizer) -> io::Result<()> { macro_rules! write_sink { (ref mut $stream: expr, $samples: expr) => { $stream.as_mut().unwrap().write($samples) @@ -159,14 +152,14 @@ impl<'a> Sink for PortAudioSink<'a> { let samples = packet.samples(); let result = match self { - Self::F32(stream, _parameters, _) => { + Self::F32(stream, _parameters) => { write_sink!(ref mut stream, samples) } - Self::S32(stream, _parameters, ref mut requantizer) => { + Self::S32(stream, _parameters) => { let samples_s32: &[i32] = &convert::to_s32(samples, requantizer); write_sink!(ref mut stream, samples_s32) } - Self::S16(stream, _parameters, ref mut requantizer) => { + Self::S16(stream, _parameters) => { let samples_s16: &[i16] = &convert::to_s16(samples, requantizer); write_sink!(ref mut stream, samples_s16) } diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 3f006b0ae..1fdc99193 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -1,6 +1,6 @@ use super::{Open, Sink, SinkAsBytes}; -use crate::audio::AudioPacket; -use crate::config::{AudioFormat, Requantizer}; +use crate::audio::{AudioPacket, Requantizer}; +use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use libpulse_binding::{self as pulse, stream::Direction}; use libpulse_simple_binding::Simple; @@ -14,11 +14,10 @@ pub struct PulseAudioSink { ss: pulse::sample::Spec, device: Option, format: AudioFormat, - requantizer: Requantizer, } impl Open for PulseAudioSink { - fn open(device: Option, format: AudioFormat, requantizer: Requantizer) -> Self { + fn open(device: Option, format: AudioFormat) -> Self { info!("Using PulseAudio sink with format: {:?}", format); // PulseAudio calls S24 and S24_3 different from the rest of the world @@ -42,7 +41,6 @@ impl Open for PulseAudioSink { ss, device, format, - requantizer, } } } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 34930ffaa..d298d8c16 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -16,25 +16,16 @@ use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; compile_error!("Rodio JACK backend is currently only supported on linux."); #[cfg(feature = "rodio-backend")] -pub fn mk_rodio( - device: Option, - format: AudioFormat, - requantizer: Requantizer, -) -> Box { - Box::new(open(cpal::default_host(), device, format, requantizer)) +pub fn mk_rodio(device: Option, format: AudioFormat) -> Box { + Box::new(open(cpal::default_host(), device, format)) } #[cfg(feature = "rodiojack-backend")] -pub fn mk_rodiojack( - device: Option, - format: AudioFormat, - requantizer: Requantizer, -) -> Box { +pub fn mk_rodiojack(device: Option, format: AudioFormat) -> Box { Box::new(open( cpal::host_from_id(cpal::HostId::Jack).unwrap(), device, format, - requantizer, )) } @@ -56,7 +47,6 @@ pub struct RodioSink { rodio_sink: rodio::Sink, format: AudioFormat, _stream: rodio::OutputStream, - requantizer: Requantizer, } fn list_formats(device: &rodio::Device) { @@ -161,12 +151,7 @@ fn create_sink( Ok((sink, stream)) } -pub fn open( - host: cpal::Host, - device: Option, - format: AudioFormat, - requantizer: Requantizer, -) -> RodioSink { +pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> RodioSink { info!( "Using Rodio sink with format {:?} and cpal host: {}", format, @@ -183,15 +168,12 @@ pub fn open( RodioSink { rodio_sink: sink, format, - requantizer, _stream: stream, } } impl Sink for RodioSink { - start_stop_noop!(); - - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, requantizer: &mut Requantizer) -> io::Result<()> { let samples = packet.samples(); match self.format { AudioFormat::F32 => { @@ -200,7 +182,7 @@ impl Sink for RodioSink { self.rodio_sink.append(source); } AudioFormat::S16 => { - let samples_s16: &[i16] = &convert::to_s16(samples, &mut self.requantizer); + let samples_s16: &[i16] = &convert::to_s16(samples, requantizer); 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 6badd303f..f33cf61f9 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -6,13 +6,13 @@ use sdl2::audio::{AudioQueue, AudioSpecDesired}; use std::{io, thread, time}; pub enum SdlSink { - F32(AudioQueue, Requantizer), - S32(AudioQueue, Requantizer), - S16(AudioQueue, Requantizer), + F32(AudioQueue), + S32(AudioQueue), + S16(AudioQueue), } impl Open for SdlSink { - fn open(device: Option, format: AudioFormat, requantizer: Requantizer) -> Self { + fn open(device: Option, format: AudioFormat) -> Self { info!("Using SDL sink with format: {:?}", format); if device.is_some() { @@ -35,7 +35,7 @@ impl Open for SdlSink { let queue: AudioQueue<$type> = audio .open_queue(None, &desired_spec) .expect("could not open SDL audio device"); - $sink(queue, requantizer) + $sink(queue) }}; } match format { @@ -58,9 +58,9 @@ impl Sink for SdlSink { }}; } match self { - Self::F32(queue, _) => start_sink!(queue), - Self::S32(queue, _) => start_sink!(queue), - Self::S16(queue, _) => start_sink!(queue), + Self::F32(queue) => start_sink!(queue), + Self::S32(queue) => start_sink!(queue), + Self::S16(queue) => start_sink!(queue), }; Ok(()) } @@ -73,14 +73,14 @@ impl Sink for SdlSink { }}; } match self { - Self::F32(queue, _) => stop_sink!(queue), - Self::S32(queue, _) => stop_sink!(queue), - Self::S16(queue, _) => stop_sink!(queue), + Self::F32(queue) => stop_sink!(queue), + Self::S32(queue) => stop_sink!(queue), + Self::S16(queue) => stop_sink!(queue), }; Ok(()) } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, requantizer: &mut Requantizer) -> io::Result<()> { macro_rules! drain_sink { ($queue: expr, $size: expr) => {{ // sleep and wait for sdl thread to drain the queue a bit @@ -92,16 +92,16 @@ impl Sink for SdlSink { let samples = packet.samples(); match self { - Self::F32(queue, _) => { + Self::F32(queue) => { drain_sink!(queue, AudioFormat::F32.size()); queue.queue(samples) } - Self::S32(queue, ref mut requantizer) => { + Self::S32(queue) => { let samples_s32: &[i32] = &convert::to_s32(samples, requantizer); drain_sink!(queue, AudioFormat::S32.size()); queue.queue(samples_s32) } - Self::S16(queue, ref mut requantizer) => { + Self::S16(queue) => { let samples_s16: &[i16] = &convert::to_s16(samples, requantizer); 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 959c74c0c..3b4b0ebe3 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -10,11 +10,10 @@ pub struct SubprocessSink { shell_command: String, child: Option, format: AudioFormat, - requantizer: Requantizer, } impl Open for SubprocessSink { - fn open(shell_command: Option, format: AudioFormat, requantizer: Requantizer) -> Self { + fn open(shell_command: Option, format: AudioFormat) -> Self { info!("Using subprocess sink with format: {:?}", format); if let Some(shell_command) = shell_command { @@ -22,7 +21,6 @@ impl Open for SubprocessSink { shell_command, child: None, format, - requantizer, } } else { panic!("subprocess sink requires specifying a shell command"); diff --git a/playback/src/config.rs b/playback/src/config.rs index 312a4d903..2072de7b9 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,4 +1,6 @@ -pub use crate::audio::convert::{i24, Requantizer}; +pub use crate::audio::convert::i24; +pub use crate::audio::dither::{mk_ditherer, Ditherer}; +pub use crate::audio::shape_noise::{mk_noise_shaper, NoiseShaper}; use std::convert::TryFrom; use std::mem; use std::str::FromStr; @@ -115,7 +117,7 @@ impl Default for NormalisationMethod { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct PlayerConfig { pub bitrate: Bitrate, pub normalisation: bool, @@ -128,6 +130,11 @@ pub struct PlayerConfig { pub normalisation_knee: f32, pub gapless: bool, 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: fn() -> Box, + pub noise_shaper: fn() -> Box, } impl Default for PlayerConfig { @@ -144,6 +151,8 @@ impl Default for PlayerConfig { normalisation_knee: 1.0, gapless: true, passthrough: false, + ditherer: Ditherer::default(), + noise_shaper: NoiseShaper::default(), } } } diff --git a/playback/src/player.rs b/playback/src/player.rs index 3f0778f9a..f18742a6b 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -11,7 +11,9 @@ use futures_util::stream::futures_unordered::FuturesUnordered; use futures_util::{future, StreamExt, TryFutureExt}; use tokio::sync::{mpsc, oneshot}; -use crate::audio::{AudioDecoder, AudioError, AudioPacket, PassthroughDecoder, VorbisDecoder}; +use crate::audio::{ + AudioDecoder, AudioError, AudioPacket, PassthroughDecoder, Requantizer, VorbisDecoder, +}; use crate::audio::{AudioDecrypt, AudioFile, StreamLoaderController}; use crate::audio::{ READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_BEFORE_PLAYBACK_SECONDS, @@ -59,6 +61,7 @@ struct PlayerInternal { sink_event_callback: Option, audio_filter: Option>, event_senders: Vec>, + requantizer: Requantizer, limiter_active: bool, limiter_attack_counter: u32, @@ -293,6 +296,8 @@ impl Player { let handle = thread::spawn(move || { debug!("new Player[{}]", session.session_id()); + let requantizer = Requantizer::new((config.ditherer)(), (config.noise_shaper)()); + let internal = PlayerInternal { session, config, @@ -305,6 +310,7 @@ impl Player { sink_event_callback: None, audio_filter, event_senders: [event_sender].to_vec(), + requantizer, limiter_active: false, limiter_attack_counter: 0, @@ -1274,7 +1280,7 @@ impl PlayerInternal { } } - if let Err(err) = self.sink.write(&packet) { + if let Err(err) = self.sink.write(&packet, &mut self.requantizer) { error!("Could not write audio: {}", err); self.ensure_sink_stopped(false); } diff --git a/src/main.rs b/src/main.rs index ca348c197..2ac6625f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,9 +5,8 @@ use sha1::{Digest, Sha1}; use tokio::sync::mpsc::UnboundedReceiver; use url::Url; -use librespot::audio::convert::Requantizer; -use librespot::audio::dither::{self, mk_ditherer, Ditherer, HighPassDitherer, NoDithering}; -use librespot::audio::shape_noise::{self, NoiseShaper}; +use librespot::audio::dither::{self, mk_ditherer, HighPassDitherer, NoDithering}; +use librespot::audio::shape_noise::{self}; use librespot::connect::spirc::Spirc; use librespot::core::authentication::Credentials; use librespot::core::cache::Cache; @@ -113,9 +112,7 @@ fn print_version() { struct Setup { format: AudioFormat, - ditherer: fn() -> Box, - noise_shaper: fn() -> Box, - backend: fn(Option, AudioFormat, Requantizer) -> Box, + backend: fn(Option, AudioFormat) -> Box, device: Option, mixer: fn(Option) -> Box, @@ -340,32 +337,9 @@ fn get_setup(args: &[String]) -> Setup { .map(|format| AudioFormat::try_from(format).expect("Invalid output format")) .unwrap_or_default(); - let ditherer_name = matches.opt_str("dither"); - let noise_shaper_name = matches.opt_str("shape-noise"); - - if format == AudioFormat::F32 && (ditherer_name.is_some() || noise_shaper_name.is_some()) { - unimplemented!( - "Dithering and noise shaping is not implemented for format {:?}", - format - ); - } - - let ditherer = match ditherer_name { - Some(_) => dither::find_ditherer(ditherer_name).expect("Invalid ditherer"), - _ => match format { - AudioFormat::S16 | AudioFormat::S24 | AudioFormat::S24_3 => { - mk_ditherer:: - } - _ => mk_ditherer::, - }, - }; - - let noise_shaper = - shape_noise::find_noise_shaper(noise_shaper_name).expect("Invalid noise shaper"); - let device = matches.opt_str("device"); if device == Some("?".into()) { - backend(device, format, Requantizer::new(ditherer(), noise_shaper())); + backend(device, format); exit(0); } @@ -490,56 +464,90 @@ fn get_setup(args: &[String]) -> Setup { .as_ref() .map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate")) .unwrap_or_default(); - let gain_type = matches - .opt_str("normalisation-gain-type") + + let gapless = !matches.opt_present("disable-gapless"); + + let normalisation = matches.opt_present("enable-volume-normalisation"); + let normalisation_method = matches + .opt_str("normalisation-method") .as_ref() .map(|gain_type| { - NormalisationType::from_str(gain_type).expect("Invalid normalisation type") + NormalisationMethod::from_str(gain_type).expect("Invalid normalisation method") }) .unwrap_or_default(); - let normalisation_method = matches - .opt_str("normalisation-method") + let normalisation_type = matches + .opt_str("normalisation-gain-type") .as_ref() .map(|gain_type| { - NormalisationMethod::from_str(gain_type).expect("Invalid normalisation method") + NormalisationType::from_str(gain_type).expect("Invalid normalisation type") }) .unwrap_or_default(); + let normalisation_pregain = matches + .opt_str("normalisation-pregain") + .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) + .unwrap_or(PlayerConfig::default().normalisation_pregain); + let normalisation_threshold = NormalisationData::db_to_ratio( + matches + .opt_str("normalisation-threshold") + .map(|threshold| { + threshold + .parse::() + .expect("Invalid threshold float value") + }) + .unwrap_or(PlayerConfig::default().normalisation_threshold), + ); + let normalisation_attack = matches + .opt_str("normalisation-attack") + .map(|attack| attack.parse::().expect("Invalid attack float value")) + .unwrap_or(PlayerConfig::default().normalisation_attack * MILLIS) + / MILLIS; + let normalisation_release = matches + .opt_str("normalisation-release") + .map(|release| release.parse::().expect("Invalid release float value")) + .unwrap_or(PlayerConfig::default().normalisation_release * MILLIS) + / MILLIS; + let normalisation_knee = matches + .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 noise_shaper_name = matches.opt_str("shape-noise"); + + if format == AudioFormat::F32 && (ditherer_name.is_some() || noise_shaper_name.is_some()) { + unimplemented!( + "Dithering and noise shaping is not implemented for format {:?}", + format + ); + } + + let ditherer = match ditherer_name { + Some(_) => dither::find_ditherer(ditherer_name).expect("Invalid ditherer"), + _ => match format { + AudioFormat::S16 | AudioFormat::S24 | AudioFormat::S24_3 => { + mk_ditherer:: + } + _ => mk_ditherer::, + }, + }; + + let noise_shaper = + shape_noise::find_noise_shaper(noise_shaper_name).expect("Invalid noise shaper"); PlayerConfig { bitrate, - gapless: !matches.opt_present("disable-gapless"), - normalisation: matches.opt_present("enable-volume-normalisation"), + gapless, + normalisation, normalisation_method, - normalisation_type: gain_type, - normalisation_pregain: matches - .opt_str("normalisation-pregain") - .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) - .unwrap_or(PlayerConfig::default().normalisation_pregain), - normalisation_threshold: NormalisationData::db_to_ratio( - matches - .opt_str("normalisation-threshold") - .map(|threshold| { - threshold - .parse::() - .expect("Invalid threshold float value") - }) - .unwrap_or(PlayerConfig::default().normalisation_threshold), - ), - normalisation_attack: matches - .opt_str("normalisation-attack") - .map(|attack| attack.parse::().expect("Invalid attack float value")) - .unwrap_or(PlayerConfig::default().normalisation_attack * MILLIS) - / MILLIS, - normalisation_release: matches - .opt_str("normalisation-release") - .map(|release| release.parse::().expect("Invalid release float value")) - .unwrap_or(PlayerConfig::default().normalisation_release * MILLIS) - / MILLIS, - normalisation_knee: matches - .opt_str("normalisation-knee") - .map(|knee| knee.parse::().expect("Invalid knee float value")) - .unwrap_or(PlayerConfig::default().normalisation_knee), + normalisation_type, + normalisation_pregain, + normalisation_threshold, + normalisation_attack, + normalisation_release, + normalisation_knee, passthrough, + ditherer, + noise_shaper, } }; @@ -569,8 +577,6 @@ fn get_setup(args: &[String]) -> Setup { Setup { format, - ditherer, - noise_shaper, backend, cache, session_config, @@ -663,13 +669,11 @@ async fn main() { let audio_filter = mixer.get_audio_filter(); let format = setup.format; - let ditherer = setup.ditherer; - let noise_shaper = setup.noise_shaper; let backend = setup.backend; let device = setup.device.clone(); let (player, event_channel) = Player::new(player_config, session.clone(), audio_filter, move || { - (backend)(device, format, Requantizer::new(ditherer(), noise_shaper())) + (backend)(device, format) }); if setup.emit_sink_events { From 5ee2eddeb7a1cd65df2f4438a3e4a4bd0d4c6c89 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 19 Apr 2021 23:07:27 +0200 Subject: [PATCH 12/23] fix examples --- examples/play.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/examples/play.rs b/examples/play.rs index b4b5fe945..d6c7196df 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -1,8 +1,5 @@ use std::env; -use librespot::audio::convert::Requantizer; -use librespot::audio::dither::{self}; -use librespot::audio::shape_noise::{self}; use librespot::core::authentication::Credentials; use librespot::core::config::SessionConfig; use librespot::core::session::Session; @@ -27,8 +24,6 @@ async fn main() { let track = SpotifyId::from_base62(&args[3]).unwrap(); let backend = audio_backend::find(None).unwrap(); - let ditherer = dither::find_ditherer(None).unwrap(); - let noise_shaper = shape_noise::find_noise_shaper(None).unwrap(); println!("Connecting .."); let session = Session::connect(session_config, credentials, None) @@ -36,11 +31,7 @@ async fn main() { .unwrap(); let (mut player, _) = Player::new(player_config, session, None, move || { - backend( - None, - audio_format, - Requantizer::new(ditherer(), noise_shaper()), - ) + backend(None, audio_format) }); player.load(track, true, 0); From 2ab413682fda1fbf6fe569fa2f327dbc4392e307 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 28 Apr 2021 20:51:59 +0200 Subject: [PATCH 13/23] Fix high pass ditherer on interleaved samples --- audio/src/dither.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/audio/src/dither.rs b/audio/src/dither.rs index 1fc1f97a5..078468e7c 100644 --- a/audio/src/dither.rs +++ b/audio/src/dither.rs @@ -2,6 +2,8 @@ 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, // lowering distortion and replacing it with a constant, fixed noise level, // which is more pleasant to the ear than the distortion. Doing so can with @@ -179,7 +181,8 @@ impl Ditherer for GaussianDitherer { // filter with weights [1.0, -1.0], and is superseded by noise shapers. // Guidance: better than Triangular if not doing other noise shaping. pub struct HighPassDitherer { - previous_noise: f32, + active_channel: usize, + previous_noises: [f32; NUM_CHANNELS], cached_rng: ThreadRng, distribution: Uniform, } @@ -187,7 +190,8 @@ pub struct HighPassDitherer { impl Ditherer for HighPassDitherer { fn new() -> Self { Self { - previous_noise: 0.0, + 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 } @@ -199,8 +203,9 @@ impl Ditherer for HighPassDitherer { 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_noise; - self.previous_noise = new_noise; + 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 } } From b5ea6cc91f5fdc719e696f3a8b12c764448a470f Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 30 Apr 2021 21:24:21 +0200 Subject: [PATCH 14/23] Fix dithering and noise shaping on 24-bit formats --- audio/src/convert.rs | 46 +++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/audio/src/convert.rs b/audio/src/convert.rs index 54d978bde..a93aac753 100644 --- a/audio/src/convert.rs +++ b/audio/src/convert.rs @@ -7,9 +7,12 @@ 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(); + pub const MIN: i32 = -8388608; + pub const MAX: i32 = 8388607; + + 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]) } } @@ -39,9 +42,9 @@ impl Requantizer { macro_rules! convert_samples_to { ($type: ident, $samples: expr, $requantizer: expr) => { - convert_samples_to!($type, $samples, $requantizer, 0) + convert_samples_to!($type, $samples, $requantizer, $type) }; - ($type: ident, $samples: expr, $requantizer: expr, $drop_bits: expr) => { + ($type: ident, $samples: expr, $requantizer: expr, $scale: ident) => { $samples .iter() .map(|sample| { @@ -49,15 +52,32 @@ macro_rules! convert_samples_to { // 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 mut int_value = *sample * (std::$type::MAX as f32 + 0.5) - 0.5; + let mut int_value = *sample * ($scale::MAX as f32 + 0.5) - 0.5; int_value = $requantizer.shaped_dither(int_value); + // Special case for output formats with a range smaller than + // their primitive allows (i.e. S24: 24-bit samples in a 32-bit + // word): clamp between MIN and MAX to ensure that the most + // significant byte is zero. Otherwise, dithering may cause an + // overflow. Don't care in other cases as casting to primitives + // will saturate correctly (see below). + if ($scale::MAX as $type) < std::$type::MAX { + let min = $scale::MIN as f32; + let max = $scale::MAX as f32; + + if int_value < min { + int_value = min; + } else if int_value > max { + int_value = max; + } + } + // https://doc.rust-lang.org/nomicon/casts.html: // casting float to integer rounds towards zero, then saturates. - // ideally ties round to even, but since it is extremely - // unlikely that a float has *exactly* .5 as fraction, this - // should be more than precise enough - int_value as $type >> $drop_bits + // 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. + int_value as $type }) .collect() }; @@ -69,14 +89,14 @@ pub fn to_s32(samples: &[f32], requantizer: &mut Requantizer) -> Vec { // S24 is 24-bit PCM packed in an upper 32-bit word pub fn to_s24(samples: &[f32], requantizer: &mut Requantizer) -> Vec { - convert_samples_to!(i32, samples, requantizer, 8) + convert_samples_to!(i32, samples, requantizer, i24) } pub fn to_s24_3(samples: &[f32], requantizer: &mut Requantizer) -> Vec { // TODO - can we improve performance by passing this as a closure? - to_s32(samples, requantizer) + to_s24(samples, requantizer) .iter() - .map(|sample| i24::pcm_from_i32(*sample)) + .map(|sample| i24::from_s24(*sample)) .collect() } From 3c527e4db33bd0f1718e7baa83c924c2a695975a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 30 Apr 2021 23:14:38 +0200 Subject: [PATCH 15/23] Refactor sample conversion into `Requantizer` struct * Arguable nicer API with smaller code size * Slightly improved performance for S24_3 --- audio/src/convert.rs | 102 ++++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 49 deletions(-) diff --git a/audio/src/convert.rs b/audio/src/convert.rs index a93aac753..a315ae3e0 100644 --- a/audio/src/convert.rs +++ b/audio/src/convert.rs @@ -34,72 +34,76 @@ impl Requantizer { } } - pub fn shaped_dither(&mut self, sample: f32) -> f32 { - let noise = self.ditherer.noise(sample); - self.noise_shaper.shape(sample, noise) + // Denormalize, dither and shape noise + pub fn scale(&mut self, sample: f32, max: i32) -> f32 { + // Losslessly represent [-1.0..1.0] as some signed integer value 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. Taken from: + // http://blog.bjornroche.com/2009/12/int-float-int-its-jungle-out-there.html + let int_value = sample * (max as f32 + 0.5) - 0.5; + self.shaped_dither(int_value) } -} - -macro_rules! convert_samples_to { - ($type: ident, $samples: expr, $requantizer: expr) => { - convert_samples_to!($type, $samples, $requantizer, $type) - }; - ($type: ident, $samples: expr, $requantizer: expr, $scale: ident) => { - $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 mut int_value = *sample * ($scale::MAX as f32 + 0.5) - 0.5; - int_value = $requantizer.shaped_dither(int_value); - // Special case for output formats with a range smaller than - // their primitive allows (i.e. S24: 24-bit samples in a 32-bit - // word): clamp between MIN and MAX to ensure that the most - // significant byte is zero. Otherwise, dithering may cause an - // overflow. Don't care in other cases as casting to primitives - // will saturate correctly (see below). - if ($scale::MAX as $type) < std::$type::MAX { - let min = $scale::MIN as f32; - let max = $scale::MAX as f32; + // 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, min: i32, max: i32) -> f32 { + let int_value = self.scale(sample, max); - if int_value < min { - int_value = min; - } else if int_value > max { - int_value = max; - } - } + let min = min as f32; + let max = max 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. - int_value as $type - }) - .collect() - }; + fn shaped_dither(&mut self, sample: f32) -> f32 { + let noise = self.ditherer.noise(sample); + self.noise_shaper.shape(sample, noise) + } } +// 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 to_s32(samples: &[f32], requantizer: &mut Requantizer) -> Vec { - convert_samples_to!(i32, samples, requantizer) + samples + .iter() + .map(|sample| requantizer.scale(*sample, std::i32::MAX) as i32) + .collect() } // S24 is 24-bit PCM packed in an upper 32-bit word pub fn to_s24(samples: &[f32], requantizer: &mut Requantizer) -> Vec { - convert_samples_to!(i32, samples, requantizer, i24) + samples + .iter() + .map(|sample| requantizer.clamping_scale(*sample, i24::MIN, i24::MAX) as i32) + .collect() } +// S24_3 is 24-bit PCM in a 3-byte array pub fn to_s24_3(samples: &[f32], requantizer: &mut Requantizer) -> Vec { - // TODO - can we improve performance by passing this as a closure? - to_s24(samples, requantizer) + samples .iter() - .map(|sample| i24::from_s24(*sample)) + .map(|sample| { + // Not as DRY as calling to_s24 first, but this saves iterating + // over all samples twice. + let int_value = requantizer.clamping_scale(*sample, i24::MIN, i24::MAX) as i32; + i24::from_s24(int_value) + }) .collect() } pub fn to_s16(samples: &[f32], requantizer: &mut Requantizer) -> Vec { - convert_samples_to!(i16, samples, requantizer) + samples + .iter() + .map(|sample| requantizer.scale(*sample, std::i16::MAX as i32) as i16) + .collect() } From 6f0f9bc35b43321ed2911e43f5bdd0400f15a6a2 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 11 May 2021 23:24:05 +0200 Subject: [PATCH 16/23] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9bf71ec7..4a4c72132 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since v0.2.0. ## [Unreleased] +### Added +- [audio] Add support for dithering with `--dither` for lower requantization error (breaking) +- [audio] Add support for noise shaping with `--shape-noise` for lower perceived noise (breaking) ## [0.2.0] - 2021-05-04 From 418e44bb2153766ce009441eb94b9cf5d3ac39b1 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 11 May 2021 23:45:03 +0200 Subject: [PATCH 17/23] Fix some clippy lints --- playback/src/config.rs | 17 +++++++++-------- playback/src/player.rs | 4 ++-- src/main.rs | 4 ++-- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/playback/src/config.rs b/playback/src/config.rs index 2072de7b9..21058527a 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,6 +1,6 @@ pub use crate::audio::convert::i24; -pub use crate::audio::dither::{mk_ditherer, Ditherer}; -pub use crate::audio::shape_noise::{mk_noise_shaper, NoiseShaper}; +pub use crate::audio::dither::{Ditherer, DithererBuilder}; +pub use crate::audio::shape_noise::{NoiseShaper, NoiseShaperBuilder}; use std::convert::TryFrom; use std::mem; use std::str::FromStr; @@ -120,6 +120,9 @@ impl Default for NormalisationMethod { #[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, @@ -128,13 +131,11 @@ pub struct PlayerConfig { pub normalisation_attack: f32, pub normalisation_release: f32, pub normalisation_knee: f32, - pub gapless: bool, - 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: fn() -> Box, - pub noise_shaper: fn() -> Box, + pub ditherer: DithererBuilder, + pub noise_shaper: NoiseShaperBuilder, } impl Default for PlayerConfig { @@ -151,8 +152,8 @@ impl Default for PlayerConfig { normalisation_knee: 1.0, gapless: true, passthrough: false, - ditherer: Ditherer::default(), - noise_shaper: NoiseShaper::default(), + ditherer: ::default(), + noise_shaper: ::default(), } } } diff --git a/playback/src/player.rs b/playback/src/player.rs index cd13bfb07..95b15cde3 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -544,10 +544,10 @@ impl PlayerState { play_request_id, loaded_track: PlayerLoadedTrackData { decoder, - duration_ms, - bytes_per_second, normalisation_factor, stream_loader_controller, + bytes_per_second, + duration_ms, stream_position_pcm, }, }; diff --git a/src/main.rs b/src/main.rs index 3e2612db5..2a23bf93a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -618,15 +618,15 @@ fn get_setup(args: &[String]) -> Setup { PlayerConfig { bitrate, gapless, + passthrough, normalisation, - normalisation_method, normalisation_type, + normalisation_method, normalisation_pregain, normalisation_threshold, normalisation_attack, normalisation_release, normalisation_knee, - passthrough, ditherer, noise_shaper, } From f7bcb4917f26786b15072e9cd6b2839a52ecc743 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 19 May 2021 21:28:32 +0200 Subject: [PATCH 18/23] Clean up API --- playback/src/audio_backend/alsa.rs | 2 +- playback/src/audio_backend/gstreamer.rs | 2 +- playback/src/audio_backend/jackaudio.rs | 4 +- playback/src/audio_backend/mod.rs | 16 ++--- playback/src/audio_backend/pipe.rs | 2 +- playback/src/audio_backend/portaudio.rs | 8 +-- playback/src/audio_backend/pulseaudio.rs | 2 +- playback/src/audio_backend/rodio.rs | 6 +- playback/src/audio_backend/sdl.rs | 8 +-- playback/src/audio_backend/subprocess.rs | 2 +- playback/src/convert.rs | 76 ++++++++++++------------ playback/src/player.rs | 10 ++-- 12 files changed, 69 insertions(+), 69 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 260ba89dc..989396688 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -1,6 +1,6 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; -use crate::convert::Requantizer; +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 edc4e32bd..681ec0c29 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,6 +1,6 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; -use crate::convert::Requantizer; +use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index eee247bc0..ab75fff22 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -1,6 +1,6 @@ use super::{Open, Sink}; use crate::config::AudioFormat; -use crate::convert::Requantizer; +use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::player::NUM_CHANNELS; use jack::{ @@ -70,7 +70,7 @@ impl Open for JackSink { } impl Sink for JackSink { - fn write(&mut self, packet: &AudioPacket, _requantizer: &mut Requantizer) -> 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 20d310d2c..e4653f172 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -1,5 +1,5 @@ use crate::config::AudioFormat; -use crate::convert::Requantizer; +use crate::convert::Converter; use crate::decoder::AudioPacket; use std::io; @@ -14,7 +14,7 @@ pub trait Sink { fn stop(&mut self) -> io::Result<()> { Ok(()) } - fn write(&mut self, packet: &AudioPacket, requantizer: &mut Requantizer) -> io::Result<()>; + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()>; } pub type SinkBuilder = fn(Option, AudioFormat) -> Box; @@ -30,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, requantizer: &mut Requantizer) -> 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, requantizer); + 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, requantizer); + 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, requantizer); + 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, requantizer); + let samples_s16: &[i16] = &converter.f32_to_s16(samples); self.write_bytes(samples_s16.as_bytes()) } }, diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 25b287537..6ad2773b8 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -1,6 +1,6 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; -use crate::convert::Requantizer; +use crate::convert::Converter; use crate::decoder::AudioPacket; use std::fs::OpenOptions; use std::io::{self, Write}; diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 3c4159585..b289f47df 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::{self, Requantizer}; +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}; @@ -144,7 +144,7 @@ impl<'a> Sink for PortAudioSink<'a> { Ok(()) } - fn write(&mut self, packet: &AudioPacket, requantizer: &mut Requantizer) -> 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 +157,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, requantizer); + 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, requantizer); + 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 8d033e501..57c9b8bcb 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -1,6 +1,6 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; -use crate::convert::Requantizer; +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 b4d1efa44..52e9bc91a 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::{self, Requantizer}; +use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; @@ -174,7 +174,7 @@ pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> Ro } impl Sink for RodioSink { - fn write(&mut self, packet: &AudioPacket, requantizer: &mut Requantizer) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { let samples = packet.samples(); match self.format { AudioFormat::F32 => { @@ -183,7 +183,7 @@ impl Sink for RodioSink { self.rodio_sink.append(source); } AudioFormat::S16 => { - let samples_s16: &[i16] = &convert::to_s16(samples, requantizer); + 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 07eb7f9cb..ab7c7ecc4 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::{self, Requantizer}; +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, requantizer: &mut Requantizer) -> 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, requantizer); + 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, requantizer); + 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 ed0d33743..785fb3d23 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -1,6 +1,6 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; -use crate::convert::Requantizer; +use crate::convert::Converter; use crate::decoder::AudioPacket; use shell_words::split; diff --git a/playback/src/convert.rs b/playback/src/convert.rs index a315ae3e0..b7a09f1a0 100644 --- a/playback/src/convert.rs +++ b/playback/src/convert.rs @@ -17,15 +17,15 @@ impl i24 { } } -pub struct Requantizer { +pub struct Converter { ditherer: Box, noise_shaper: Box, } -impl Requantizer { +impl Converter { pub fn new(ditherer: Box, noise_shaper: Box) -> Self { info!( - "Requantizing with ditherer: {} and noise shaper: {}", + "Converting with ditherer: {} and noise shaper: {}", ditherer, noise_shaper ); Self { @@ -67,43 +67,43 @@ impl Requantizer { let noise = self.ditherer.noise(sample); self.noise_shaper.shape(sample, noise) } -} -// 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 to_s32(samples: &[f32], requantizer: &mut Requantizer) -> Vec { - samples - .iter() - .map(|sample| requantizer.scale(*sample, std::i32::MAX) as i32) - .collect() -} + // 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, std::i32::MAX) as i32) + .collect() + } -// S24 is 24-bit PCM packed in an upper 32-bit word -pub fn to_s24(samples: &[f32], requantizer: &mut Requantizer) -> Vec { - samples - .iter() - .map(|sample| requantizer.clamping_scale(*sample, i24::MIN, i24::MAX) 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, i24::MIN, i24::MAX) as i32) + .collect() + } -// S24_3 is 24-bit PCM in a 3-byte array -pub fn to_s24_3(samples: &[f32], requantizer: &mut Requantizer) -> Vec { - samples - .iter() - .map(|sample| { - // Not as DRY as calling to_s24 first, but this saves iterating - // over all samples twice. - let int_value = requantizer.clamping_scale(*sample, i24::MIN, i24::MAX) as i32; - i24::from_s24(int_value) - }) - .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| { + // Not as DRY as calling f32_to_s24 first, but this saves iterating + // over all samples twice. + let int_value = self.clamping_scale(*sample, i24::MIN, i24::MAX) as i32; + i24::from_s24(int_value) + }) + .collect() + } -pub fn to_s16(samples: &[f32], requantizer: &mut Requantizer) -> Vec { - samples - .iter() - .map(|sample| requantizer.scale(*sample, std::i16::MAX as i32) as i16) - .collect() + pub fn f32_to_s16(&mut self, samples: &[f32]) -> Vec { + samples + .iter() + .map(|sample| self.scale(*sample, std::i16::MAX as i32) as i16) + .collect() + } } diff --git a/playback/src/player.rs b/playback/src/player.rs index d2d2731ea..36248493d 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -18,7 +18,7 @@ use crate::audio::{ }; use crate::audio_backend::Sink; use crate::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}; -use crate::convert::Requantizer; +use crate::convert::Converter; use crate::core::session::Session; use crate::core::spotify_id::SpotifyId; use crate::core::util::SeqGenerator; @@ -60,7 +60,7 @@ struct PlayerInternal { sink_event_callback: Option, audio_filter: Option>, event_senders: Vec>, - requantizer: Requantizer, + converter: Converter, limiter_active: bool, limiter_attack_counter: u32, @@ -299,7 +299,7 @@ impl Player { let handle = thread::spawn(move || { debug!("new Player[{}]", session.session_id()); - let requantizer = Requantizer::new((config.ditherer)(), (config.noise_shaper)()); + let converter = Converter::new((config.ditherer)(), (config.noise_shaper)()); let internal = PlayerInternal { session, @@ -313,7 +313,7 @@ impl Player { sink_event_callback: None, audio_filter, event_senders: [event_sender].to_vec(), - requantizer, + converter, limiter_active: false, limiter_attack_counter: 0, @@ -1288,7 +1288,7 @@ impl PlayerInternal { } } - if let Err(err) = self.sink.write(&packet, &mut self.requantizer) { + if let Err(err) = self.sink.write(&packet, &mut self.converter) { error!("Could not write audio: {}", err); self.ensure_sink_stopped(false); } From 68f409c21714e9cfc768c2d0a2950b73d06f80fb Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 21 May 2021 23:28:16 +0200 Subject: [PATCH 19/23] Match reference Vorbis sample conversion technique See: https://github.com/librespot-org/librespot/pull/694#issuecomment-846231105 --- playback/src/convert.rs | 32 ++++++++++------------- playback/src/decoder/libvorbis_decoder.rs | 5 +--- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/playback/src/convert.rs b/playback/src/convert.rs index b7a09f1a0..5056bd097 100644 --- a/playback/src/convert.rs +++ b/playback/src/convert.rs @@ -7,9 +7,6 @@ use zerocopy::AsBytes; #[repr(transparent)] pub struct i24([u8; 3]); impl i24 { - pub const MIN: i32 = -8388608; - pub const MAX: i32 = 8388607; - fn from_s24(sample: i32) -> Self { // trim the padding in the most significant byte let [a, b, c, _d] = sample.to_le_bytes(); @@ -35,13 +32,10 @@ impl Converter { } // Denormalize, dither and shape noise - pub fn scale(&mut self, sample: f32, max: i32) -> f32 { - // Losslessly represent [-1.0..1.0] as some signed integer value 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. Taken from: - // http://blog.bjornroche.com/2009/12/int-float-int-its-jungle-out-there.html - let int_value = sample * (max as f32 + 0.5) - 0.5; + 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; self.shaped_dither(int_value) } @@ -50,11 +44,13 @@ impl Converter { // 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, min: i32, max: i32) -> f32 { - let int_value = self.scale(sample, max); + 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; - let min = min as f32; - let max = max as f32; if int_value < min { return min; } else if int_value > max { @@ -75,7 +71,7 @@ impl Converter { pub fn f32_to_s32(&mut self, samples: &[f32]) -> Vec { samples .iter() - .map(|sample| self.scale(*sample, std::i32::MAX) as i32) + .map(|sample| self.scale(*sample, 0x80000000) as i32) .collect() } @@ -83,7 +79,7 @@ impl Converter { pub fn f32_to_s24(&mut self, samples: &[f32]) -> Vec { samples .iter() - .map(|sample| self.clamping_scale(*sample, i24::MIN, i24::MAX) as i32) + .map(|sample| self.clamping_scale(*sample, 0x800000) as i32) .collect() } @@ -94,7 +90,7 @@ impl Converter { .map(|sample| { // Not as DRY as calling f32_to_s24 first, but this saves iterating // over all samples twice. - let int_value = self.clamping_scale(*sample, i24::MIN, i24::MAX) as i32; + let int_value = self.clamping_scale(*sample, 0x800000) as i32; i24::from_s24(int_value) }) .collect() @@ -103,7 +99,7 @@ impl Converter { pub fn f32_to_s16(&mut self, samples: &[f32]) -> Vec { samples .iter() - .map(|sample| self.scale(*sample, std::i16::MAX as i32) as i16) + .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 6f9a68a3a..23e66583e 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(), ))); } From 35296bfb428012404264eb6d09fa67ed6689880c Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 23 May 2021 22:11:17 +0200 Subject: [PATCH 20/23] Simplify and cut down on features - Remove separate noise shaping module and options. It does not cater to 99% users so does not justify the complexity. - Rename option values to `--dither` to better match what other audio libraries use. - Rework configuration so dithering is an `Option`. This saves a dynamic dispatch to a no-op `NoneDitherer` when dithering is set to none. - Change default ditherer to `tpdf` instead of `tpdf_hp` which should be even safer on all DACs. (`tpdf_hp` is the simplest form of FIR noise shaping) - Removed `rect` (`rpdf`) and `sto` ditherers, that are suboptimal to the others in all cases. Keeping them around is a bit academic. --- playback/src/config.rs | 7 +- playback/src/convert.rs | 36 +++-- playback/src/dither.rs | 148 ++++---------------- playback/src/lib.rs | 1 - playback/src/player.rs | 2 +- playback/src/shape_noise.rs | 260 ------------------------------------ src/main.rs | 43 +++--- 7 files changed, 64 insertions(+), 433 deletions(-) delete mode 100644 playback/src/shape_noise.rs diff --git a/playback/src/config.rs b/playback/src/config.rs index 696f26f84..5ae043bb1 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,7 +1,6 @@ use super::player::NormalisationData; use crate::convert::i24; pub use crate::dither::{Ditherer, DithererBuilder}; -pub use crate::shape_noise::{NoiseShaper, NoiseShaperBuilder}; use std::convert::TryFrom; use std::mem; @@ -136,8 +135,7 @@ pub struct PlayerConfig { // pass function pointers so they can be lazily instantiated *after* spawning a thread // (thereby circumventing Send bounds that they might not satisfy) - pub ditherer: DithererBuilder, - pub noise_shaper: NoiseShaperBuilder, + pub ditherer: Option, } impl Default for PlayerConfig { @@ -154,8 +152,7 @@ impl Default for PlayerConfig { normalisation_knee: 1.0, gapless: true, passthrough: false, - ditherer: ::default(), - noise_shaper: ::default(), + ditherer: Some(::default()), } } } diff --git a/playback/src/convert.rs b/playback/src/convert.rs index 5056bd097..c344d3e3c 100644 --- a/playback/src/convert.rs +++ b/playback/src/convert.rs @@ -1,5 +1,4 @@ -use crate::dither::Ditherer; -use crate::shape_noise::NoiseShaper; +use crate::dither::{Ditherer, DithererBuilder}; use zerocopy::AsBytes; #[derive(AsBytes, Copy, Clone, Debug)] @@ -15,28 +14,32 @@ impl i24 { } pub struct Converter { - ditherer: Box, - noise_shaper: Box, + ditherer: Option>, } impl Converter { - pub fn new(ditherer: Box, noise_shaper: Box) -> Self { - info!( - "Converting with ditherer: {} and noise shaper: {}", - ditherer, noise_shaper - ); - Self { - ditherer, - noise_shaper, + 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, dither and shape noise + // 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; - self.shaped_dither(int_value) + + 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. @@ -59,11 +62,6 @@ impl Converter { int_value } - fn shaped_dither(&mut self, sample: f32) -> f32 { - let noise = self.ditherer.noise(sample); - self.noise_shaper.shape(sample, noise) - } - // 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 diff --git a/playback/src/dither.rs b/playback/src/dither.rs index 078468e7c..f929fd64f 100644 --- a/playback/src/dither.rs +++ b/playback/src/dither.rs @@ -5,30 +5,27 @@ use std::fmt; const NUM_CHANNELS: usize = 2; // Dithering lowers digital-to-analog conversion ("requantization") error, -// lowering distortion and replacing it with a constant, fixed noise level, -// which is more pleasant to the ear than the distortion. Doing so can with -// a noise-shaped dither can increase the dynamic range of 96 dB CD-quality -// audio to a perceived 120 dB. +// linearizing output, lowering distortion and replacing it with a constant, +// fixed noise level, which is more pleasant to the ear than the distortion. // -// Guidance: experts can configure many different configurations of ditherers -// and noise shapers. For the rest of us: +// Guidance: // -// * Don't dither or shape noise on S32 or F32. On F32 it's not supported -// anyway (there are no rounding errors due to integer conversions) and on -// S32 the noise level is so far down that it is simply inaudible. -// -// * Generally use high pass dithering (hp) without noise shaping. Depending -// on personal preference you may use Gaussian dithering (gauss) instead; +// * 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. +// you are looking for a more "analog" sound akin to tape hiss. // -// * On power-constrained hardware, use the fraction saving noise shaper -// instead of dithering. Performance-wise, this is not necessary even on a -// Raspberry Pi Zero, but if you're on batteries... +// * 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. // -// 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. +// * 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 @@ -40,7 +37,7 @@ pub trait Ditherer { impl dyn Ditherer { pub fn default() -> fn() -> Box { - mk_ditherer:: + mk_ditherer:: } } @@ -50,83 +47,11 @@ impl fmt::Display for dyn Ditherer { } } -pub struct NoDithering {} -impl Ditherer for NoDithering { - fn new() -> Self { - Self {} - } - - fn name(&self) -> &'static str { - "None" - } - - fn noise(&mut self, _sample: f32) -> f32 { - 0.0 - } -} - -// "True" white noise (refer to Gaussian for analog source hiss). Advantages: -// least CPU-intensive dither, lowest signal-to-noise ratio. Disadvantage: -// highest perceived loudness, suffers from intermodulation distortion unless -// you are using this for subtractive dithering, which you most likely are not, -// and is not supported by any of the librespot backends. Guidance: use some -// other ditherer unless you know what you're doing. -pub struct RectangularDitherer { - cached_rng: ThreadRng, - distribution: Uniform, -} - -impl Ditherer for RectangularDitherer { - fn new() -> Self { - Self { - cached_rng: rand::thread_rng(), - // 1 LSB peak-to-peak needed to linearize the response: - distribution: Uniform::new_inclusive(-0.5, 0.5), - } - } - - fn name(&self) -> &'static str { - "Rectangular" - } - - fn noise(&mut self, _sample: f32) -> f32 { - self.distribution.sample(&mut self.cached_rng) - } -} - -// Like Rectangular, but with lower error and OK to use for the default case -// of non-subtractive dithering such as to the librespot backends. -pub struct StochasticDitherer { - cached_rng: ThreadRng, - distribution: Uniform, -} - -impl Ditherer for StochasticDitherer { - fn new() -> Self { - Self { - cached_rng: rand::thread_rng(), - distribution: Uniform::new(0.0, 1.0), - } - } - - fn name(&self) -> &'static str { - "Stochastic" - } - - fn noise(&mut self, sample: f32) -> f32 { - let fract = sample.fract(); - if self.distribution.sample(&mut self.cached_rng) <= fract { - 1.0 - fract - } else { - fract * -1.0 - } - } -} +// 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. +// -// Higher level than Rectangular. Advantages: superior to Rectangular as it -// does not suffer from modulation noise effects. Disadvantage: more CPU- -// expensive. Guidance: all-round recommendation to reduce quantization noise, -// even on 24-bit output. pub struct TriangularDitherer { cached_rng: ThreadRng, distribution: Triangular, @@ -150,9 +75,6 @@ impl Ditherer for TriangularDitherer { } } -// Like Triangular, but with higher noise power and more like phono hiss. -// Guidance: theoretically less optimal, but an alternative to Triangular -// if a more analog sound is sought after. pub struct GaussianDitherer { cached_rng: ThreadRng, distribution: Normal, @@ -176,10 +98,6 @@ impl Ditherer for GaussianDitherer { } } -// Like Triangular, but with a high-pass filter. Advantages: comparably less -// perceptible noise, less CPU-intensive. Disadvantage: this acts like a FIR -// filter with weights [1.0, -1.0], and is superseded by noise shapers. -// Guidance: better than Triangular if not doing other noise shaping. pub struct HighPassDitherer { active_channel: usize, previous_noises: [f32; NUM_CHANNELS], @@ -198,7 +116,7 @@ impl Ditherer for HighPassDitherer { } fn name(&self) -> &'static str { - "High Pass" + "Triangular, High Passed" } fn noise(&mut self, _sample: f32) -> f32 { @@ -216,21 +134,11 @@ pub fn mk_ditherer() -> Box { pub type DithererBuilder = fn() -> Box; -pub const DITHERERS: &[(&str, DithererBuilder)] = &[ - ("none", mk_ditherer::), - ("rect", mk_ditherer::), - ("sto", mk_ditherer::), - ("tri", mk_ditherer::), - ("gauss", mk_ditherer::), - ("hp", mk_ditherer::), -]; - -pub fn find_ditherer(name: Option) -> Option Box> { - match name { - Some(name) => DITHERERS - .iter() - .find(|ditherer| name == ditherer.0) - .map(|ditherer| ditherer.1), - _ => Some(mk_ditherer::), +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 3a8e2addd..31dadc44e 100644 --- a/playback/src/lib.rs +++ b/playback/src/lib.rs @@ -12,4 +12,3 @@ mod decoder; pub mod dither; pub mod mixer; pub mod player; -pub mod shape_noise; diff --git a/playback/src/player.rs b/playback/src/player.rs index 36248493d..3086d905f 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -299,7 +299,7 @@ impl Player { let handle = thread::spawn(move || { debug!("new Player[{}]", session.session_id()); - let converter = Converter::new((config.ditherer)(), (config.noise_shaper)()); + let converter = Converter::new(config.ditherer); let internal = PlayerInternal { session, diff --git a/playback/src/shape_noise.rs b/playback/src/shape_noise.rs deleted file mode 100644 index a062b8de3..000000000 --- a/playback/src/shape_noise.rs +++ /dev/null @@ -1,260 +0,0 @@ -use std::fmt; - -const NUM_CHANNELS: usize = 2; - -// Noise shapers take the noise caused by errors in the digital-to-analog -// conversion process ("requantizing") plus any added dither, and change the -// frequency curve of that noise so that it is less perceptible to our ears. -// Like dithering, this process can generate more noise than there was in the -// first place ("noise power") but still make it more pleasant to listen to. -// Noise-shaped dithers can improve the dynamic range of 96 dB CD-quality audio -// to a perceived 120 dB. -// -// Guidance: take care to only use noise shaping when you are sure no further -// dithering and/or noise shaping is done further down the line -- like most -// delta-sigma DACs do. Exception to this rule is the Fraction Saver, which -// should be OK to use even on such DACs (but in this case, disable dithering -// in librespot -- you should do one or the other, not both). -// -// As for the more powerful noise shapers, it's a personal trade-off between -// absolute noise power and perceived noise. Co-incidentally, the lower the -// perceived noise, the higher the absolute power, and the higher the CPU- -// usage. All shapers have something going for them. -// -pub trait NoiseShaper { - fn new() -> Self - where - Self: Sized; - fn name(&self) -> &'static str; - fn shape(&mut self, sample: f32, noise: f32) -> f32; -} - -impl dyn NoiseShaper { - pub fn default() -> fn() -> Box { - mk_noise_shaper:: - } -} - -impl fmt::Display for dyn NoiseShaper { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.name()) - } -} - -pub struct NoShaping {} -impl NoiseShaper for NoShaping { - fn new() -> Self { - Self {} - } - - fn name(&self) -> &'static str { - "None" - } - - fn shape(&mut self, sample: f32, noise: f32) -> f32 { - sample + noise - } -} - -// First-order noise shaping. Advantages: negligible performance hit, infinite -// signal-to-noise ration at DC, lowered signal-to-noise ratio for low -// frequencies and slightly higher signal-to-noise ration for frequencies -// above Nyquist/2, kills certain limit cycles in subsequent IIR filters, -// safe to use on oversampling DACs. Disadvantage: higher perceived loudness -// than other options (but still less so than without shaping!). Guidance: -// there are very little reasons not to use this shaper, even on power- -// constrained hardware. If you do, best to set dithering to none. -pub struct FractionSaver { - active_channel: usize, - previous_fractions: [f32; NUM_CHANNELS], -} - -impl NoiseShaper for FractionSaver { - fn new() -> Self { - Self { - active_channel: 0, - previous_fractions: [0.0; NUM_CHANNELS], - } - } - - fn name(&self) -> &'static str { - "Fraction Saver" - } - - fn shape(&mut self, sample: f32, noise: f32) -> f32 { - let sample_with_fraction = sample + noise + self.previous_fractions[self.active_channel]; - self.previous_fractions[self.active_channel] = sample_with_fraction.fract(); - self.active_channel ^= 1; - sample_with_fraction.floor() - } -} - -// Higher-order noise shapers. Advantage: lower perceived loudness than first- -// order noise-shaping. Disadvantage: higher CPU-usage, higher absolute noise -// level, not to be used when there will be dithering or noise shaping further -// down the chain, as is the case with most delta-sigma DACs. Guidance: use -// on non-oversampling DACs, or other DACs that do not do dithering or noise -// shaping themselves. -macro_rules! fir_shaper { - ($name: ident, $description: tt, $taps: expr, $weights: expr) => { - pub struct $name { - filter: FirFilter, - } - - impl NoiseShaper for $name { - fn new() -> Self { - Self { - filter: FirFilter::new(&Self::WEIGHTS), - } - } - - fn name(&self) -> &'static str { - concat!($description, ", ", stringify!($taps), " taps") - } - - fn shape(&mut self, sample: f32, noise: f32) -> f32 { - self.filter.shape(sample, noise) - } - } - - impl $name { - const WEIGHTS: [f32; $taps] = $weights; - } - }; -} - -// 14.34 dB improvement in E-weighted noise at the expense of 12.19 dB higher -// noise power (unweighted) by pushing most noise into the spectrum above -// 15 kHz, meaning the noise is less audible. Guidance: this is a good -// cost/benefit trade-off. Widely used in other audio software. -fir_shaper!( - Lipshitz5, - "Lipshitz improved E-weighted", - 5, - [2.033, -2.165, 1.959, -1.590, 0.6149] -); - -// 18.32 dB improvement in E-weighted noise but at the expense of 23.1 dB -// higher noise power (unweighted). Approaches noise power levels of vinyl, -// but with lower perceived loudness. Guidance: certainly still acceptable -// noise levels, subject to personal preference. Arguably superseded by -// Wannamaker9. -fir_shaper!( - Lipshitz9, - "Lipshitz improved E-weighted", - 9, - [2.847, -4.685, 6.214, -7.184, 6.639, -5.032, 3.263, -1.632, 0.4191] -); - -// 10.47 dB improvement in F-weighted noise at the expense of 6.64 dB higher -// noise power (unweighted). Like Lipshitz, but with a refinement in psycho- -// acoustic levels. Spreads most noise into the spectrum above 10 kHz. -// Guidance: a less precise, but lower absolute noise alternative to Lipshitz5. -fir_shaper!( - Wannamaker3, - "Wannamaker F-weighted", - 3, - [1.623, -0.982, 0.109] -); - -// 16.8 dB improvement in F-weighted noise at the expense of 18.4 dB higher -// noise power (unweighted). Pushes most noise into the spectrum above 15 kHz. -// Guidance: refinement over Lipshitz9. This is what SoX uses. -fir_shaper!( - Wannamaker9, - "Wannamaker F-weighted", - 9, - [2.412, -3.370, 3.937, -4.174, 3.353, -2.205, 1.281, -0.569, 0.0847] -); - -// 16.7 dB improvement in F-weighted noise at the expense of 17.3 dB higher -// noise power (unweighted). This is close to the theoretical limit curve -// but with the highest CPU usage. Guidance: better than Wannamaker9 if you -// can suffer the performance hit. -fir_shaper!( - Wannamaker24, - "Wannamaker F-weighted", - 24, - [ - 2.39151, -3.284444, 3.679506, -3.635044, 2.524185, -1.146701, 0.115354, 0.513745, - -0.749277, 0.512386, -0.188997, -0.043705, 0.149843, -0.151186, 0.076302, -0.01207, - -0.021127, 0.025232, -0.016121, 0.004453, 0.000876, -0.001799, 0.000774, -0.000128 - ] -); - -struct FirFilter { - taps: usize, - weights: Vec, - active_channel: usize, - error_buffer: Vec, - buffer_index: usize, -} - -impl FirFilter { - fn new(weights: &[f32]) -> Self { - let taps = weights.len(); - Self { - taps, - weights: weights.to_vec(), - active_channel: 0, - error_buffer: vec![0.0; taps * NUM_CHANNELS], - buffer_index: 0, - } - } - - fn shape(&mut self, sample: f32, noise: f32) -> f32 { - // apply FIR filter - let mut sample_with_shaped_noise = sample as f64; - for index in 0..self.taps { - sample_with_shaped_noise += self.weighted_error(index); - } - - let dithered_sample = (sample_with_shaped_noise + noise as f64).round(); - - // store error and roll buffer -- this is a slight hack to increment - // buffer_index only if we are moving from channel 1 to channel 0, - // that is, just handled both the left and right channels - self.buffer_index = (self.buffer_index + self.active_channel) % self.taps; - let index = self.index_at_samples_ago(0); - self.error_buffer[index] = sample_with_shaped_noise - dithered_sample; - self.active_channel ^= 1; - - dithered_sample as f32 - } -} - -impl FirFilter { - fn index_at_samples_ago(&self, errors_ago: usize) -> usize { - ((self.buffer_index + self.taps - errors_ago) % self.taps) + self.taps * self.active_channel - } - - fn weighted_error(&self, index: usize) -> f64 { - self.error_buffer[self.index_at_samples_ago(index)] * self.weights[index] as f64 - } -} - -pub fn mk_noise_shaper() -> Box { - Box::new(S::new()) -} - -pub type NoiseShaperBuilder = fn() -> Box; - -pub const NOISE_SHAPERS: &[(&str, NoiseShaperBuilder)] = &[ - ("none", mk_noise_shaper::), - ("fract", mk_noise_shaper::), - ("iew5", mk_noise_shaper::), - ("iew9", mk_noise_shaper::), - ("fw3", mk_noise_shaper::), - ("fw9", mk_noise_shaper::), - ("fw24", mk_noise_shaper::), -]; - -pub fn find_noise_shaper(name: Option) -> Option Box> { - match name { - Some(name) => NOISE_SHAPERS - .iter() - .find(|noise_shaper| name == noise_shaper.0) - .map(|noise_shaper| noise_shaper.1), - _ => Some(mk_noise_shaper::), - } -} diff --git a/src/main.rs b/src/main.rs index b1e546732..0b844a195 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,10 +16,9 @@ use librespot::playback::audio_backend::{self, Sink, BACKENDS}; use librespot::playback::config::{ AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, }; -use librespot::playback::dither::{self, mk_ditherer, HighPassDitherer, NoDithering}; +use librespot::playback::dither::{self, Ditherer}; use librespot::playback::mixer::{self, Mixer, MixerConfig}; use librespot::playback::player::{NormalisationData, Player}; -use librespot::playback::shape_noise::{self}; mod player_event_handler; use player_event_handler::{emit_sink_event, run_program_on_events}; @@ -251,15 +250,9 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", "dither", - "Specify the dither algorithm to use - [none, rect, sto, tri, hp, gauss]. Defaults to 'hp' for formats S16, S24, S24_3 and 'none' for other formats.", + "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( - "", - "shape-noise", - "Specify the noise shaping algorithm to use - [none, fract, iew5, iew9, fw3, fw9, fw24]. Defaults to 'none'.", - "SHAPE_NOISE", - ) .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER") .optopt( "m", @@ -591,28 +584,25 @@ fn get_setup(args: &[String]) -> Setup { .unwrap_or(PlayerConfig::default().normalisation_knee); let ditherer_name = matches.opt_str("dither"); - let noise_shaper_name = matches.opt_str("shape-noise"); - - if format == AudioFormat::F32 && (ditherer_name.is_some() || noise_shaper_name.is_some()) { - unimplemented!( - "Dithering and noise shaping is not implemented for format {:?}", - format - ); - } - - let ditherer = match ditherer_name { - Some(_) => dither::find_ditherer(ditherer_name).expect("Invalid ditherer"), - _ => match format { + 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 => { - mk_ditherer:: + Some(::default()) } - _ => mk_ditherer::, + _ => None, }, }; - let noise_shaper = - shape_noise::find_noise_shaper(noise_shaper_name).expect("Invalid noise shaper"); - PlayerConfig { bitrate, gapless, @@ -626,7 +616,6 @@ fn get_setup(args: &[String]) -> Setup { normalisation_release, normalisation_knee, ditherer, - noise_shaper, } }; From 3b5519d0806cccae5f9f8d346e4161eec9e6f061 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 23 May 2021 22:23:13 +0200 Subject: [PATCH 21/23] Update changelog --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64e8aae21..74bb8293b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - [playback] Add support for dithering with `--dither` for lower requantization error (breaking) -- [playback] Add support for noise shaping with `--shape-noise` for lower perceived noise (breaking) ### Changed * [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `AudioError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking) From 8454a2b40801ed694fbfe7989c2217fb409ca911 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 24 May 2021 21:16:39 +0200 Subject: [PATCH 22/23] Refactor getting default ditherer --- playback/src/config.rs | 8 ++++---- playback/src/dither.rs | 6 ------ src/main.rs | 4 ++-- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/playback/src/config.rs b/playback/src/config.rs index 5ae043bb1..76db6de33 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,6 +1,6 @@ use super::player::NormalisationData; use crate::convert::i24; -pub use crate::dither::{Ditherer, DithererBuilder}; +pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer}; use std::convert::TryFrom; use std::mem; @@ -139,8 +139,8 @@ pub struct PlayerConfig { } impl Default for PlayerConfig { - fn default() -> PlayerConfig { - PlayerConfig { + fn default() -> Self { + Self { bitrate: Bitrate::default(), normalisation: false, normalisation_type: NormalisationType::default(), @@ -152,7 +152,7 @@ impl Default for PlayerConfig { normalisation_knee: 1.0, gapless: true, passthrough: false, - ditherer: Some(::default()), + ditherer: Some(mk_ditherer::), } } } diff --git a/playback/src/dither.rs b/playback/src/dither.rs index f929fd64f..86aca6e27 100644 --- a/playback/src/dither.rs +++ b/playback/src/dither.rs @@ -35,12 +35,6 @@ pub trait Ditherer { fn noise(&mut self, sample: f32) -> f32; } -impl dyn Ditherer { - pub fn default() -> fn() -> Box { - mk_ditherer:: - } -} - impl fmt::Display for dyn Ditherer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.name()) diff --git a/src/main.rs b/src/main.rs index 0b844a195..1dbde3a42 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ use librespot::playback::audio_backend::{self, Sink, BACKENDS}; use librespot::playback::config::{ AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, }; -use librespot::playback::dither::{self, Ditherer}; +use librespot::playback::dither; use librespot::playback::mixer::{self, Mixer, MixerConfig}; use librespot::playback::player::{NormalisationData, Player}; @@ -597,7 +597,7 @@ fn get_setup(args: &[String]) -> Setup { // nothing set on command line => use default None => match format { AudioFormat::S16 | AudioFormat::S24 | AudioFormat::S24_3 => { - Some(::default()) + PlayerConfig::default().ditherer } _ => None, }, From deb171589170ab96420123541f87da493d5ad02a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 25 May 2021 23:00:48 +0200 Subject: [PATCH 23/23] Don't dither twice on PortAudio and GStreamer --- playback/src/audio_backend/gstreamer.rs | 3 ++- playback/src/audio_backend/portaudio.rs | 5 +---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 681ec0c29..b5273102a 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -38,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), diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index b289f47df..0bcd1aa59 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -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(),