-
Notifications
You must be signed in to change notification settings - Fork 828
Implement dithering #694
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
roderickvd
merged 29 commits into
librespot-org:dev
from
roderickvd:dithering-and-noise-shaping
May 26, 2021
Merged
Implement dithering #694
Changes from 8 commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
c8ec268
Implement dithering and noise shaping
roderickvd f3553e1
cargo fmt
roderickvd fde697b
Fix example
roderickvd 977dbed
Correct dithering noise powers
roderickvd 6ea089c
Disable noise shaping by default
roderickvd 00b36be
Document default ditherer and noise shaper
roderickvd f7ac001
Fix panic when no noise shaper is specified
roderickvd 636d181
Implement fmt::Display for Ditherer and NoiseShaper
roderickvd 2f11bbc
Refactor name() into &'static str
roderickvd 34abd0d
Merge remote-tracking branch 'upstream/dev' into dithering-and-noise-…
roderickvd 5dec737
Simplify name macro
roderickvd c995088
Move dithering and noise shaping to PlayerConfig
roderickvd 5ee2edd
fix examples
roderickvd 2ab4136
Fix high pass ditherer on interleaved samples
roderickvd b5ea6cc
Fix dithering and noise shaping on 24-bit formats
roderickvd 3c527e4
Refactor sample conversion into `Requantizer` struct
roderickvd 2f2c2ca
Merge remote-tracking branch 'upstream/dev' into dithering-and-noise-…
roderickvd cf8e8e3
Merge remote-tracking branch 'upstream/dev' into dithering-and-noise-…
roderickvd 6f0f9bc
Update changelog
roderickvd 418e44b
Fix some clippy lints
roderickvd de77487
Merge remote-tracking branch 'upstream/dev' into dithering-and-noise-…
roderickvd 703b667
Merge remote-tracking branch 'upstream/dev' into dithering-and-noise-…
roderickvd f7bcb49
Clean up API
roderickvd 68f409c
Match reference Vorbis sample conversion technique
roderickvd 35296bf
Simplify and cut down on features
roderickvd 3b5519d
Update changelog
roderickvd 8454a2b
Refactor getting default ditherer
roderickvd deb1715
Don't dither twice on PortAudio and GStreamer
roderickvd 1879499
Merge branch 'dev' into dithering-and-noise-shaping
roderickvd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| 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, | ||
| // 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: | ||
| // | ||
| // * 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. | ||
| // | ||
| pub trait Ditherer { | ||
| fn new() -> Self | ||
| where | ||
| Self: Sized; | ||
| fn name(&self) -> String; | ||
|
roderickvd marked this conversation as resolved.
Outdated
|
||
| 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 { | ||
| Self {} | ||
| } | ||
|
|
||
| fn name(&self) -> String { | ||
| String::from("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<f32>, | ||
| } | ||
|
|
||
| impl Ditherer for RectangularDitherer { | ||
| fn new() -> Self { | ||
| 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) | ||
| } | ||
| } | ||
|
|
||
| // 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<f32>, | ||
| } | ||
|
|
||
| impl Ditherer for StochasticDitherer { | ||
| fn new() -> Self { | ||
| 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 { | ||
| 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<f32>, | ||
| } | ||
|
|
||
| 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 | ||
| } | ||
| } | ||
|
|
||
| fn name(&self) -> String { | ||
| String::from("Triangular") | ||
| } | ||
|
|
||
| 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, | ||
| distribution: Normal<f32>, | ||
| } | ||
|
|
||
| impl Ditherer for GaussianDitherer { | ||
| fn new() -> Self { | ||
| 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) | ||
| } | ||
| } | ||
|
|
||
| // 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<f32>, | ||
| } | ||
|
|
||
| impl Ditherer for HighPassDitherer { | ||
| fn new() -> Self { | ||
| Self { | ||
| previous_noise: 0.0, | ||
| cached_rng: rand::thread_rng(), | ||
| distribution: Uniform::new_inclusive(-0.5, 0.5), // 1 LSB +/- 1 LSB (previous) = 2 LSB | ||
| } | ||
| } | ||
|
|
||
| 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; | ||
| self.previous_noise = new_noise; | ||
| high_passed_noise | ||
| } | ||
| } | ||
|
|
||
| pub fn mk_ditherer<D: Ditherer + 'static>() -> Box<dyn Ditherer> { | ||
| Box::new(D::new()) | ||
| } | ||
|
|
||
| pub const DITHERERS: &'static [(&'static str, fn() -> Box<dyn Ditherer>)] = &[ | ||
| ("none", mk_ditherer::<NoDithering>), | ||
| ("rect", mk_ditherer::<RectangularDitherer>), | ||
| ("sto", mk_ditherer::<StochasticDitherer>), | ||
| ("tri", mk_ditherer::<TriangularDitherer>), | ||
| ("gauss", mk_ditherer::<GaussianDitherer>), | ||
| ("hp", mk_ditherer::<HighPassDitherer>), | ||
| ]; | ||
|
|
||
| pub fn find_ditherer(name: Option<String>) -> Option<fn() -> Box<dyn Ditherer>> { | ||
| match name { | ||
| Some(name) => DITHERERS | ||
| .iter() | ||
| .find(|ditherer| name == ditherer.0) | ||
| .map(|ditherer| ditherer.1), | ||
| _ => Some(mk_ditherer::<NoDithering>), | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.