librespot_playback/
dither.rs

1use rand::SeedableRng;
2use rand::rngs::SmallRng;
3use rand_distr::{Distribution, Normal, Triangular, Uniform};
4use std::fmt;
5
6use crate::NUM_CHANNELS;
7
8// Dithering lowers digital-to-analog conversion ("requantization") error,
9// linearizing output, lowering distortion and replacing it with a constant,
10// fixed noise level, which is more pleasant to the ear than the distortion.
11//
12// Guidance:
13//
14//  * On S24, S24_3 and S24, the default is to use triangular dithering.
15//    Depending on personal preference you may use Gaussian dithering instead;
16//    it's not as good objectively, but it may be preferred subjectively if
17//    you are looking for a more "analog" sound akin to tape hiss.
18//
19//  * Advanced users who know that they have a DAC without noise shaping have
20//    a third option: high-passed dithering, which is like triangular dithering
21//    except that it moves dithering noise up in frequency where it is less
22//    audible. Note: 99% of DACs are of delta-sigma design with noise shaping,
23//    so unless you have a multibit / R2R DAC, or otherwise know what you are
24//    doing, this is not for you.
25//
26//  * Don't dither or shape noise on S32 or F32. On F32 it's not supported
27//    anyway (there are no integer conversions and so no rounding errors) and
28//    on S32 the noise level is so far down that it is simply inaudible even
29//    after volume normalisation and control.
30//
31pub trait Ditherer {
32    fn new() -> Self
33    where
34        Self: Sized;
35    fn name(&self) -> &'static str;
36    fn noise(&mut self) -> f64;
37}
38
39impl fmt::Display for dyn Ditherer {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        write!(f, "{}", self.name())
42    }
43}
44
45fn create_rng() -> SmallRng {
46    SmallRng::from_os_rng()
47}
48
49pub struct TriangularDitherer {
50    cached_rng: SmallRng,
51    distribution: Triangular<f64>,
52}
53
54impl Ditherer for TriangularDitherer {
55    fn new() -> Self {
56        Self {
57            cached_rng: create_rng(),
58            // 2 LSB peak-to-peak needed to linearize the response:
59            distribution: Triangular::new(-1.0, 1.0, 0.0).unwrap(),
60        }
61    }
62
63    fn name(&self) -> &'static str {
64        Self::NAME
65    }
66
67    #[inline]
68    fn noise(&mut self) -> f64 {
69        self.distribution.sample(&mut self.cached_rng)
70    }
71}
72
73impl TriangularDitherer {
74    pub const NAME: &'static str = "tpdf";
75}
76
77pub struct GaussianDitherer {
78    cached_rng: SmallRng,
79    distribution: Normal<f64>,
80}
81
82impl Ditherer for GaussianDitherer {
83    fn new() -> Self {
84        Self {
85            cached_rng: create_rng(),
86            // For Gaussian to achieve equivalent decorrelation to triangular dithering, it needs
87            // 3-4 dB higher amplitude than TPDF's optimal 0.408 LSB. If optimizing:
88            // - minimum correlation: σ ≈ 0.58
89            // - perceptual equivalence: σ ≈ 0.65
90            // - worst-case performance: σ ≈ 0.70
91            //
92            // σ = 0.6 LSB is a reasonable compromise that balances mathematical theory with
93            // empirical performance across various signal types.
94            distribution: Normal::new(0.0, 0.6).unwrap(),
95        }
96    }
97
98    fn name(&self) -> &'static str {
99        Self::NAME
100    }
101
102    #[inline]
103    fn noise(&mut self) -> f64 {
104        self.distribution.sample(&mut self.cached_rng)
105    }
106}
107
108impl GaussianDitherer {
109    pub const NAME: &'static str = "gpdf";
110}
111
112pub struct HighPassDitherer {
113    active_channel: usize,
114    previous_noises: [f64; NUM_CHANNELS as usize],
115    cached_rng: SmallRng,
116    distribution: Uniform<f64>,
117}
118
119impl Ditherer for HighPassDitherer {
120    fn new() -> Self {
121        Self {
122            active_channel: 0,
123            previous_noises: [0.0; NUM_CHANNELS as usize],
124            cached_rng: create_rng(),
125            // 1 LSB +/- 1 LSB (previous) = 2 LSB
126            distribution: Uniform::new_inclusive(-0.5, 0.5)
127                .expect("Failed to create uniform distribution"),
128        }
129    }
130
131    fn name(&self) -> &'static str {
132        Self::NAME
133    }
134
135    #[inline]
136    fn noise(&mut self) -> f64 {
137        let new_noise = self.distribution.sample(&mut self.cached_rng);
138        let high_passed_noise = new_noise - self.previous_noises[self.active_channel];
139        self.previous_noises[self.active_channel] = new_noise;
140        self.active_channel ^= 1;
141        high_passed_noise
142    }
143}
144
145impl HighPassDitherer {
146    pub const NAME: &'static str = "tpdf_hp";
147}
148
149pub fn mk_ditherer<D: Ditherer + 'static>() -> Box<dyn Ditherer> {
150    Box::new(D::new())
151}
152
153pub type DithererBuilder = fn() -> Box<dyn Ditherer>;
154
155pub fn find_ditherer(name: Option<String>) -> Option<DithererBuilder> {
156    match name.as_deref() {
157        Some(TriangularDitherer::NAME) => Some(mk_ditherer::<TriangularDitherer>),
158        Some(GaussianDitherer::NAME) => Some(mk_ditherer::<GaussianDitherer>),
159        Some(HighPassDitherer::NAME) => Some(mk_ditherer::<HighPassDitherer>),
160        _ => None,
161    }
162}