Skip to content

Commit 35296bf

Browse files
committed
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.
1 parent 68f409c commit 35296bf

File tree

7 files changed

+64
-433
lines changed

7 files changed

+64
-433
lines changed

playback/src/config.rs

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use super::player::NormalisationData;
22
use crate::convert::i24;
33
pub use crate::dither::{Ditherer, DithererBuilder};
4-
pub use crate::shape_noise::{NoiseShaper, NoiseShaperBuilder};
54

65
use std::convert::TryFrom;
76
use std::mem;
@@ -136,8 +135,7 @@ pub struct PlayerConfig {
136135

137136
// pass function pointers so they can be lazily instantiated *after* spawning a thread
138137
// (thereby circumventing Send bounds that they might not satisfy)
139-
pub ditherer: DithererBuilder,
140-
pub noise_shaper: NoiseShaperBuilder,
138+
pub ditherer: Option<DithererBuilder>,
141139
}
142140

143141
impl Default for PlayerConfig {
@@ -154,8 +152,7 @@ impl Default for PlayerConfig {
154152
normalisation_knee: 1.0,
155153
gapless: true,
156154
passthrough: false,
157-
ditherer: <dyn Ditherer>::default(),
158-
noise_shaper: <dyn NoiseShaper>::default(),
155+
ditherer: Some(<dyn Ditherer>::default()),
159156
}
160157
}
161158
}

playback/src/convert.rs

+17-19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use crate::dither::Ditherer;
2-
use crate::shape_noise::NoiseShaper;
1+
use crate::dither::{Ditherer, DithererBuilder};
32
use zerocopy::AsBytes;
43

54
#[derive(AsBytes, Copy, Clone, Debug)]
@@ -15,28 +14,32 @@ impl i24 {
1514
}
1615

1716
pub struct Converter {
18-
ditherer: Box<dyn Ditherer>,
19-
noise_shaper: Box<dyn NoiseShaper>,
17+
ditherer: Option<Box<dyn Ditherer>>,
2018
}
2119

2220
impl Converter {
23-
pub fn new(ditherer: Box<dyn Ditherer>, noise_shaper: Box<dyn NoiseShaper>) -> Self {
24-
info!(
25-
"Converting with ditherer: {} and noise shaper: {}",
26-
ditherer, noise_shaper
27-
);
28-
Self {
29-
ditherer,
30-
noise_shaper,
21+
pub fn new(dither_config: Option<DithererBuilder>) -> Self {
22+
if let Some(ref ditherer_builder) = dither_config {
23+
let ditherer = (ditherer_builder)();
24+
info!("Converting with ditherer: {}", ditherer.name());
25+
Self {
26+
ditherer: Some(ditherer),
27+
}
28+
} else {
29+
Self { ditherer: None }
3130
}
3231
}
3332

34-
// Denormalize, dither and shape noise
33+
// Denormalize and dither
3534
pub fn scale(&mut self, sample: f32, factor: i64) -> f32 {
3635
// From the many float to int conversion methods available, match what
3736
// the reference Vorbis implementation uses: sample * 32768 (for 16 bit)
3837
let int_value = sample * factor as f32;
39-
self.shaped_dither(int_value)
38+
39+
match self.ditherer {
40+
Some(ref mut d) => int_value + d.noise(int_value),
41+
None => int_value,
42+
}
4043
}
4144

4245
// Special case for samples packed in a word of greater bit depth (e.g.
@@ -59,11 +62,6 @@ impl Converter {
5962
int_value
6063
}
6164

62-
fn shaped_dither(&mut self, sample: f32) -> f32 {
63-
let noise = self.ditherer.noise(sample);
64-
self.noise_shaper.shape(sample, noise)
65-
}
66-
6765
// https://doc.rust-lang.org/nomicon/casts.html: casting float to integer
6866
// rounds towards zero, then saturates. Ideally halves should round to even to
6967
// prevent any bias, but since it is extremely unlikely that a float has

playback/src/dither.rs

+28-120
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,27 @@ use std::fmt;
55
const NUM_CHANNELS: usize = 2;
66

77
// Dithering lowers digital-to-analog conversion ("requantization") error,
8-
// lowering distortion and replacing it with a constant, fixed noise level,
9-
// which is more pleasant to the ear than the distortion. Doing so can with
10-
// a noise-shaped dither can increase the dynamic range of 96 dB CD-quality
11-
// audio to a perceived 120 dB.
8+
// linearizing output, lowering distortion and replacing it with a constant,
9+
// fixed noise level, which is more pleasant to the ear than the distortion.
1210
//
13-
// Guidance: experts can configure many different configurations of ditherers
14-
// and noise shapers. For the rest of us:
11+
// Guidance:
1512
//
16-
// * Don't dither or shape noise on S32 or F32. On F32 it's not supported
17-
// anyway (there are no rounding errors due to integer conversions) and on
18-
// S32 the noise level is so far down that it is simply inaudible.
19-
//
20-
// * Generally use high pass dithering (hp) without noise shaping. Depending
21-
// on personal preference you may use Gaussian dithering (gauss) instead;
13+
// * On S24, S24_3 and S24, the default is to use triangular dithering.
14+
// Depending on personal preference you may use Gaussian dithering instead;
2215
// it's not as good objectively, but it may be preferred subjectively if
23-
// you are looking for a more "analog" sound.
16+
// you are looking for a more "analog" sound akin to tape hiss.
2417
//
25-
// * On power-constrained hardware, use the fraction saving noise shaper
26-
// instead of dithering. Performance-wise, this is not necessary even on a
27-
// Raspberry Pi Zero, but if you're on batteries...
18+
// * Advanced users who know that they have a DAC without noise shaping have
19+
// a third option: high-passed dithering, which is like triangular dithering
20+
// except that it moves dithering noise up in frequency where it is less
21+
// audible. Note: 99% of DACs are of delta-sigma design with noise shaping,
22+
// so unless you have a multibit / R2R DAC, or otherwise know what you are
23+
// doing, this is not for you.
2824
//
29-
// Implementation note: we save the handle to ThreadRng so it doesn't require
30-
// a lookup on each call (which is on each sample!). This is ~2.5x as fast.
31-
// Downside is that it is not Send so we cannot move it around player threads.
25+
// * Don't dither or shape noise on S32 or F32. On F32 it's not supported
26+
// anyway (there are no integer conversions and so no rounding errors) and
27+
// on S32 the noise level is so far down that it is simply inaudible even
28+
// after volume normalisation and control.
3229
//
3330
pub trait Ditherer {
3431
fn new() -> Self
@@ -40,7 +37,7 @@ pub trait Ditherer {
4037

4138
impl dyn Ditherer {
4239
pub fn default() -> fn() -> Box<Self> {
43-
mk_ditherer::<HighPassDitherer>
40+
mk_ditherer::<TriangularDitherer>
4441
}
4542
}
4643

@@ -50,83 +47,11 @@ impl fmt::Display for dyn Ditherer {
5047
}
5148
}
5249

53-
pub struct NoDithering {}
54-
impl Ditherer for NoDithering {
55-
fn new() -> Self {
56-
Self {}
57-
}
58-
59-
fn name(&self) -> &'static str {
60-
"None"
61-
}
62-
63-
fn noise(&mut self, _sample: f32) -> f32 {
64-
0.0
65-
}
66-
}
67-
68-
// "True" white noise (refer to Gaussian for analog source hiss). Advantages:
69-
// least CPU-intensive dither, lowest signal-to-noise ratio. Disadvantage:
70-
// highest perceived loudness, suffers from intermodulation distortion unless
71-
// you are using this for subtractive dithering, which you most likely are not,
72-
// and is not supported by any of the librespot backends. Guidance: use some
73-
// other ditherer unless you know what you're doing.
74-
pub struct RectangularDitherer {
75-
cached_rng: ThreadRng,
76-
distribution: Uniform<f32>,
77-
}
78-
79-
impl Ditherer for RectangularDitherer {
80-
fn new() -> Self {
81-
Self {
82-
cached_rng: rand::thread_rng(),
83-
// 1 LSB peak-to-peak needed to linearize the response:
84-
distribution: Uniform::new_inclusive(-0.5, 0.5),
85-
}
86-
}
87-
88-
fn name(&self) -> &'static str {
89-
"Rectangular"
90-
}
91-
92-
fn noise(&mut self, _sample: f32) -> f32 {
93-
self.distribution.sample(&mut self.cached_rng)
94-
}
95-
}
96-
97-
// Like Rectangular, but with lower error and OK to use for the default case
98-
// of non-subtractive dithering such as to the librespot backends.
99-
pub struct StochasticDitherer {
100-
cached_rng: ThreadRng,
101-
distribution: Uniform<f32>,
102-
}
103-
104-
impl Ditherer for StochasticDitherer {
105-
fn new() -> Self {
106-
Self {
107-
cached_rng: rand::thread_rng(),
108-
distribution: Uniform::new(0.0, 1.0),
109-
}
110-
}
111-
112-
fn name(&self) -> &'static str {
113-
"Stochastic"
114-
}
115-
116-
fn noise(&mut self, sample: f32) -> f32 {
117-
let fract = sample.fract();
118-
if self.distribution.sample(&mut self.cached_rng) <= fract {
119-
1.0 - fract
120-
} else {
121-
fract * -1.0
122-
}
123-
}
124-
}
50+
// Implementation note: we save the handle to ThreadRng so it doesn't require
51+
// a lookup on each call (which is on each sample!). This is ~2.5x as fast.
52+
// Downside is that it is not Send so we cannot move it around player threads.
53+
//
12554

126-
// Higher level than Rectangular. Advantages: superior to Rectangular as it
127-
// does not suffer from modulation noise effects. Disadvantage: more CPU-
128-
// expensive. Guidance: all-round recommendation to reduce quantization noise,
129-
// even on 24-bit output.
13055
pub struct TriangularDitherer {
13156
cached_rng: ThreadRng,
13257
distribution: Triangular<f32>,
@@ -150,9 +75,6 @@ impl Ditherer for TriangularDitherer {
15075
}
15176
}
15277

153-
// Like Triangular, but with higher noise power and more like phono hiss.
154-
// Guidance: theoretically less optimal, but an alternative to Triangular
155-
// if a more analog sound is sought after.
15678
pub struct GaussianDitherer {
15779
cached_rng: ThreadRng,
15880
distribution: Normal<f32>,
@@ -176,10 +98,6 @@ impl Ditherer for GaussianDitherer {
17698
}
17799
}
178100

179-
// Like Triangular, but with a high-pass filter. Advantages: comparably less
180-
// perceptible noise, less CPU-intensive. Disadvantage: this acts like a FIR
181-
// filter with weights [1.0, -1.0], and is superseded by noise shapers.
182-
// Guidance: better than Triangular if not doing other noise shaping.
183101
pub struct HighPassDitherer {
184102
active_channel: usize,
185103
previous_noises: [f32; NUM_CHANNELS],
@@ -198,7 +116,7 @@ impl Ditherer for HighPassDitherer {
198116
}
199117

200118
fn name(&self) -> &'static str {
201-
"High Pass"
119+
"Triangular, High Passed"
202120
}
203121

204122
fn noise(&mut self, _sample: f32) -> f32 {
@@ -216,21 +134,11 @@ pub fn mk_ditherer<D: Ditherer + 'static>() -> Box<dyn Ditherer> {
216134

217135
pub type DithererBuilder = fn() -> Box<dyn Ditherer>;
218136

219-
pub const DITHERERS: &[(&str, DithererBuilder)] = &[
220-
("none", mk_ditherer::<NoDithering>),
221-
("rect", mk_ditherer::<RectangularDitherer>),
222-
("sto", mk_ditherer::<StochasticDitherer>),
223-
("tri", mk_ditherer::<TriangularDitherer>),
224-
("gauss", mk_ditherer::<GaussianDitherer>),
225-
("hp", mk_ditherer::<HighPassDitherer>),
226-
];
227-
228-
pub fn find_ditherer(name: Option<String>) -> Option<fn() -> Box<dyn Ditherer>> {
229-
match name {
230-
Some(name) => DITHERERS
231-
.iter()
232-
.find(|ditherer| name == ditherer.0)
233-
.map(|ditherer| ditherer.1),
234-
_ => Some(mk_ditherer::<NoDithering>),
137+
pub fn find_ditherer(name: Option<String>) -> Option<DithererBuilder> {
138+
match name.as_deref() {
139+
Some("tpdf") => Some(mk_ditherer::<TriangularDitherer>),
140+
Some("gpdf") => Some(mk_ditherer::<GaussianDitherer>),
141+
Some("tpdf_hp") => Some(mk_ditherer::<HighPassDitherer>),
142+
_ => None,
235143
}
236144
}

playback/src/lib.rs

-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,3 @@ mod decoder;
1212
pub mod dither;
1313
pub mod mixer;
1414
pub mod player;
15-
pub mod shape_noise;

playback/src/player.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ impl Player {
299299
let handle = thread::spawn(move || {
300300
debug!("new Player[{}]", session.session_id());
301301

302-
let converter = Converter::new((config.ditherer)(), (config.noise_shaper)());
302+
let converter = Converter::new(config.ditherer);
303303

304304
let internal = PlayerInternal {
305305
session,

0 commit comments

Comments
 (0)