Skip to content
Merged
Show file tree
Hide file tree
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 Apr 13, 2021
f3553e1
cargo fmt
roderickvd Apr 13, 2021
fde697b
Fix example
roderickvd Apr 13, 2021
977dbed
Correct dithering noise powers
roderickvd Apr 14, 2021
6ea089c
Disable noise shaping by default
roderickvd Apr 14, 2021
00b36be
Document default ditherer and noise shaper
roderickvd Apr 14, 2021
f7ac001
Fix panic when no noise shaper is specified
roderickvd Apr 14, 2021
636d181
Implement fmt::Display for Ditherer and NoiseShaper
roderickvd Apr 14, 2021
2f11bbc
Refactor name() into &'static str
roderickvd Apr 15, 2021
34abd0d
Merge remote-tracking branch 'upstream/dev' into dithering-and-noise-…
roderickvd Apr 15, 2021
5dec737
Simplify name macro
roderickvd Apr 15, 2021
c995088
Move dithering and noise shaping to PlayerConfig
roderickvd Apr 19, 2021
5ee2edd
fix examples
roderickvd Apr 19, 2021
2ab4136
Fix high pass ditherer on interleaved samples
roderickvd Apr 28, 2021
b5ea6cc
Fix dithering and noise shaping on 24-bit formats
roderickvd Apr 30, 2021
3c527e4
Refactor sample conversion into `Requantizer` struct
roderickvd Apr 30, 2021
2f2c2ca
Merge remote-tracking branch 'upstream/dev' into dithering-and-noise-…
roderickvd Apr 30, 2021
cf8e8e3
Merge remote-tracking branch 'upstream/dev' into dithering-and-noise-…
roderickvd May 11, 2021
6f0f9bc
Update changelog
roderickvd May 11, 2021
418e44b
Fix some clippy lints
roderickvd May 11, 2021
de77487
Merge remote-tracking branch 'upstream/dev' into dithering-and-noise-…
roderickvd May 16, 2021
703b667
Merge remote-tracking branch 'upstream/dev' into dithering-and-noise-…
roderickvd May 18, 2021
f7bcb49
Clean up API
roderickvd May 19, 2021
68f409c
Match reference Vorbis sample conversion technique
roderickvd May 21, 2021
35296bf
Simplify and cut down on features
roderickvd May 23, 2021
3b5519d
Update changelog
roderickvd May 23, 2021
8454a2b
Refactor getting default ditherer
roderickvd May 24, 2021
deb1715
Don't dither twice on PortAudio and GStreamer
roderickvd May 25, 2021
1879499
Merge branch 'dev' into dithering-and-noise-shaping
roderickvd May 26, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions audio/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
63 changes: 46 additions & 17 deletions audio/src/convert.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::dither::*;
use crate::shape_noise::*;
use zerocopy::AsBytes;

#[derive(AsBytes, Copy, Clone, Debug)]
Expand All @@ -12,45 +14,72 @@ impl i24 {
}
}

// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity.
pub struct Requantizer {
ditherer: Box<dyn Ditherer>,
noise_shaper: Box<dyn NoiseShaper>,
}

impl Requantizer {
pub fn new(ditherer: Box<dyn Ditherer>, noise_shaper: Box<dyn NoiseShaper>) -> Self {
info!(
"Requantizing with ditherer: {} and noise shaper: {}",
ditherer, noise_shaper
);
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)
}
Comment thread
roderickvd marked this conversation as resolved.
Outdated
}

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| {
// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX]
// while maintaining DC linearity. There is nothing to be gained
// by doing this in f64, as the significand of a f32 is 24 bits,
// just like the maximum bit depth we are converting to.
let int_value = *sample * (std::$type::MAX as f32 + 0.5) - 0.5;
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<i32> {
convert_samples_to!(i32, samples)
pub fn to_s32(samples: &[f32], requantizer: &mut Requantizer) -> Vec<i32> {
convert_samples_to!(i32, samples, requantizer)
}

pub fn to_s24(samples: &[f32]) -> Vec<i32> {
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<i32> {
convert_samples_to!(i32, samples, requantizer, 8)
}

pub fn to_s24_3(samples: &[f32]) -> Vec<i24> {
to_s32(samples)
pub fn to_s24_3(samples: &[f32], requantizer: &mut Requantizer) -> Vec<i24> {
// 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<i16> {
convert_samples_to!(i16, samples)
pub fn to_s16(samples: &[f32], requantizer: &mut Requantizer) -> Vec<i16> {
convert_samples_to!(i16, samples, requantizer)
}
212 changes: 212 additions & 0 deletions audio/src/dither.rs
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;
Comment thread
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>),
}
}
5 changes: 5 additions & 0 deletions audio/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 {
Expand Down
Loading