1
0
Fork 0
mirror of https://github.com/librespot-org/librespot.git synced 2025-10-03 01:39:28 +02:00
librespot/playback/src/dither.rs
Roderick van Domburg f59766af7e
perf(playback): optimize audio conversion with 16-bit dithering and bit shifts
Since Spotify audio is always 16-bit depth, optimize the conversion pipeline:
- Always dither at 16-bit level regardless of output format
- Preserve fractional precision until final rounding for better requantization
- Replace floating-point multiplication with compile-time bit shifts
- Add comprehensive inlining to eliminate function call overhead
- Specialize 24-bit clamping to remove runtime branching

This maintains proper dithering of the original 16-bit quantization artifacts
while maximizing performance through bit-shift operations and eliminating
unnecessary runtime calculations.
2025-08-14 00:31:59 +02:00

162 lines
4.8 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use rand::SeedableRng;
use rand::rngs::SmallRng;
use rand_distr::{Distribution, Normal, Triangular, Uniform};
use std::fmt;
use crate::NUM_CHANNELS;
// Dithering lowers digital-to-analog conversion ("requantization") error,
// linearizing output, lowering distortion and replacing it with a constant,
// fixed noise level, which is more pleasant to the ear than the distortion.
//
// Guidance:
//
// * 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 akin to tape hiss.
//
// * 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.
//
// * 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
where
Self: Sized;
fn name(&self) -> &'static str;
fn noise(&mut self) -> f64;
}
impl fmt::Display for dyn Ditherer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
fn create_rng() -> SmallRng {
SmallRng::from_os_rng()
}
pub struct TriangularDitherer {
cached_rng: SmallRng,
distribution: Triangular<f64>,
}
impl Ditherer for TriangularDitherer {
fn new() -> Self {
Self {
cached_rng: create_rng(),
// 2 LSB peak-to-peak needed to linearize the response:
distribution: Triangular::new(-1.0, 1.0, 0.0).unwrap(),
}
}
fn name(&self) -> &'static str {
Self::NAME
}
#[inline]
fn noise(&mut self) -> f64 {
self.distribution.sample(&mut self.cached_rng)
}
}
impl TriangularDitherer {
pub const NAME: &'static str = "tpdf";
}
pub struct GaussianDitherer {
cached_rng: SmallRng,
distribution: Normal<f64>,
}
impl Ditherer for GaussianDitherer {
fn new() -> Self {
Self {
cached_rng: create_rng(),
// For Gaussian to achieve equivalent decorrelation to triangular dithering, it needs
// 3-4 dB higher amplitude than TPDF's optimal 0.408 LSB. If optimizing:
// - minimum correlation: σ ≈ 0.58
// - perceptual equivalence: σ ≈ 0.65
// - worst-case performance: σ ≈ 0.70
//
// σ = 0.6 LSB is a reasonable compromise that balances mathematical theory with
// empirical performance across various signal types.
distribution: Normal::new(0.0, 0.6).unwrap(),
}
}
fn name(&self) -> &'static str {
Self::NAME
}
#[inline]
fn noise(&mut self) -> f64 {
self.distribution.sample(&mut self.cached_rng)
}
}
impl GaussianDitherer {
pub const NAME: &'static str = "gpdf";
}
pub struct HighPassDitherer {
active_channel: usize,
previous_noises: [f64; NUM_CHANNELS as usize],
cached_rng: SmallRng,
distribution: Uniform<f64>,
}
impl Ditherer for HighPassDitherer {
fn new() -> Self {
Self {
active_channel: 0,
previous_noises: [0.0; NUM_CHANNELS as usize],
cached_rng: create_rng(),
// 1 LSB +/- 1 LSB (previous) = 2 LSB
distribution: Uniform::new_inclusive(-0.5, 0.5)
.expect("Failed to create uniform distribution"),
}
}
fn name(&self) -> &'static str {
Self::NAME
}
#[inline]
fn noise(&mut self) -> f64 {
let new_noise = self.distribution.sample(&mut self.cached_rng);
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
}
}
impl HighPassDitherer {
pub const NAME: &'static str = "tpdf_hp";
}
pub fn mk_ditherer<D: Ditherer + 'static>() -> Box<dyn Ditherer> {
Box::new(D::new())
}
pub type DithererBuilder = fn() -> Box<dyn Ditherer>;
pub fn find_ditherer(name: Option<String>) -> Option<DithererBuilder> {
match name.as_deref() {
Some(TriangularDitherer::NAME) => Some(mk_ditherer::<TriangularDitherer>),
Some(GaussianDitherer::NAME) => Some(mk_ditherer::<GaussianDitherer>),
Some(HighPassDitherer::NAME) => Some(mk_ditherer::<HighPassDitherer>),
_ => None,
}
}