From a9240e04c661b9982b6fdeedaec357ffe7054c6a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 12 Sep 2025 22:38:43 +0200 Subject: [PATCH 01/11] feat: add audio dithering support with dither source and feature flag --- CHANGELOG.md | 4 + Cargo.toml | 15 +- src/source/dither.rs | 379 +++++++++++++++++++++++++++++++++++++++++++ src/source/mod.rs | 20 +++ src/source/noise.rs | 1 + 5 files changed, 417 insertions(+), 2 deletions(-) create mode 100644 src/source/dither.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d859f26..8e44d5bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 SamplesBuffer. - Adds `wav_to_writer` which writes a `Source` to a writer. - Added supported for `I24` output (24-bit samples on 4 bytes storage). +- Added audio dithering support with `dither` feature (enabled by default): + - Four dithering algorithms: `TPDF`, `RPDF`, `GPDF`, and `HighPass` + - `DitherAlgorithm` enum for algorithm selection + - `dither()` function for applying quantization noise shaping ### Fixed - docs.rs will now document all features, including those that are optional. diff --git a/Cargo.toml b/Cargo.toml index d43c0676..bc53c68b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,14 +11,23 @@ edition = "2021" [features] # Default feature set provides audio playback and common format support -default = ["playback", "recording", "flac", "mp3", "mp4", "vorbis", "wav"] +default = [ + "playback", + "recording", + "flac", + "mp3", + "mp4", + "vorbis", + "wav", + "dither", +] # Core functionality features # # Enable audio playback playback = ["dep:cpal"] # Enable audio recording -recording = ["dep:cpal", "rtrb"] +recording = ["dep:cpal", "dep:rtrb"] # Enable writing audio to WAV files wav_output = ["dep:hound"] # Enable structured observability and instrumentation @@ -28,6 +37,8 @@ experimental = ["dep:atomic_float"] # Audio generation features # +# Enable audio dithering +dither = ["noise"] # Enable noise generation (white noise, pink noise, etc.) noise = ["rand", "rand_distr"] diff --git a/src/source/dither.rs b/src/source/dither.rs new file mode 100644 index 00000000..52712ef0 --- /dev/null +++ b/src/source/dither.rs @@ -0,0 +1,379 @@ +//! Dithering for audio quantization and requantization. +//! +//! Dithering is a technique in digital audio processing that eliminates quantization +//! artifacts during various stages of audio processing. This module provides tools for +//! adding appropriate dither noise to maintain audio quality during quantization +//! operations. +//! +//! ## Example +//! +//! ```rust +//! use rodio::source::{dither, SineWave}; +//! use rodio::source::DitherAlgorithm; +//! use rodio::BitDepth; +//! +//! let source = SineWave::new(440.0); +//! let dithered = dither(source, BitDepth::new(16).unwrap(), DitherAlgorithm::TPDF); +//! ``` +//! +//! ## Guidelines +//! +//! - **Apply dithering before volume changes** for optimal results +//! - **Dither once** - Apply only at the final output stage to avoid noise accumulation +//! - **Choose TPDF** for most professional audio applications (it's the default) +//! - **Use HighPass** for material with audible low-frequency dither artifacts +//! - **Use target output bit depth** - Not the source bit depth! +//! +//! When you later change volume (e.g., with `Sink::set_volume()`), both the signal +//! and dither noise scale together, maintaining proper dithering behavior. + +use std::time::Duration; + +use rand::{rngs::SmallRng, Rng, SeedableRng}; + +use crate::{ + source::noise::{Blue, WhiteGaussian, WhiteTriangular, WhiteUniform}, + BitDepth, ChannelCount, Sample, SampleRate, Source, +}; + +/// Dither algorithm selection - chooses the probability density function (PDF). +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum Algorithm { + /// GPDF (Gaussian PDF) - normal/bell curve distribution. + /// + /// Uses Gaussian white noise which more closely mimics natural processes and + /// analog circuits. Higher noise floor than TPDF. + GPDF, + + /// High-pass dithering - reduces low-frequency artifacts. + /// + /// Uses blue noise (high-pass filtered white noise) to push dither energy + /// toward higher frequencies. Particularly effective for reducing audible + /// low-frequency modulation artifacts. Best for material with significant + /// low-frequency content where traditional white dither might be audible. + HighPass, + + /// RPDF (Rectangular PDF) - uniform distribution. + /// + /// Uses uniform white noise for basic decorrelation. Simpler than TPDF but + /// allows some correlation between signal and quantization error at low levels. + /// Slightly lower noise floor than TPDF. + RPDF, + + /// TPDF (Triangular PDF) - triangular distribution. + /// + /// The gold standard for audio dithering. Provides mathematically optimal + /// decorrelation by completely eliminating correlation between the original + /// signal and quantization error. + #[default] + TPDF, +} + +/// Internal dithering implementation with a specific noise generator type. +#[derive(Clone, Debug)] +pub struct DitherImpl { + input: I, + noise: N, + target_bits: BitDepth, + lsb_amplitude: f32, +} + +impl DitherImpl +where + I: Source, + N: Iterator, +{ + /// Creates a new dither source with a custom noise generator. + /// + /// This low-level internal constructor allows providing a custom noise generator. + /// The noise generator should produce samples with appropriate amplitude + /// for the chosen dither type. + #[inline] + pub(crate) fn new_with_noise(input: I, noise: N, target_bits: BitDepth) -> Self { + // LSB amplitude for signed audio: 1.0 / (2^(bits-1)) + // This represents the amplitude of one quantization level + // Use i64 bit shifting to avoid overflow (supports up to 63 bits) + let lsb_amplitude = 1.0 / (1_i64 << (target_bits.get() - 1)) as f32; + + Self { + input, + noise, + target_bits, + lsb_amplitude, + } + } +} + +impl Iterator for DitherImpl +where + I: Source, + N: Iterator, +{ + type Item = Sample; + + #[inline] + fn next(&mut self) -> Option { + let input_sample = self.input.next()?; + let noise_sample = self.noise.next().unwrap_or(0.0); + + // Add dither noise at the target quantization level + let dithered = input_sample + noise_sample * self.lsb_amplitude; + + Some(dithered) + } +} + +impl Source for DitherImpl +where + I: Source, + N: Iterator, +{ + #[inline] + fn current_span_len(&self) -> Option { + self.input.current_span_len() + } + + #[inline] + fn channels(&self) -> ChannelCount { + self.input.channels() + } + + #[inline] + fn sample_rate(&self) -> SampleRate { + self.input.sample_rate() + } + + #[inline] + fn total_duration(&self) -> Option { + self.input.total_duration() + } + + #[inline] + fn bits_per_sample(&self) -> Option { + Some(self.target_bits) + } + + #[inline] + fn try_seek(&mut self, pos: Duration) -> Result<(), crate::source::SeekError> { + self.input.try_seek(pos) + } +} + +/// Dithering interface delegating to the supported dithering algorithms. +#[derive(Clone)] +pub enum Dither +where + R: Rng + SeedableRng + Clone, +{ + /// GPDF dithering with Gaussian white noise + GPDF(DitherImpl>), + + /// High-pass dithering with blue noise + HighPass(DitherImpl>), + + /// RPDF dithering with uniform white noise + RPDF(DitherImpl>), + + /// TPDF dithering with triangular white noise + TPDF(DitherImpl>), +} + +impl Dither +where + I: Source, + R: Rng + SeedableRng + Clone, +{ +} + +impl Dither +where + I: Source, +{ + /// Creates a new dithered source using the specified algorithm. + /// + /// This is the main constructor for dithering. Choose the algorithm based on your needs: + /// - `GPDF`: Natural/analog-like characteristics + /// - `HighPass`: Reduces low-frequency dither artifacts + /// - `RPDF`: Lower noise floor but some correlation + /// - `TPDF` (default): Optimal decorrelation + #[inline] + pub fn new(input: I, target_bits: BitDepth, algorithm: Algorithm) -> Self { + let sample_rate = input.sample_rate(); + match algorithm { + Algorithm::GPDF => { + let noise = WhiteGaussian::new(sample_rate); + Self::GPDF(DitherImpl::new_with_noise(input, noise, target_bits)) + } + Algorithm::HighPass => { + let noise = Blue::new(sample_rate); + Self::HighPass(DitherImpl::new_with_noise(input, noise, target_bits)) + } + Algorithm::RPDF => { + let noise = WhiteUniform::new(sample_rate); + Self::RPDF(DitherImpl::new_with_noise(input, noise, target_bits)) + } + Algorithm::TPDF => { + let noise = WhiteTriangular::new(sample_rate); + Self::TPDF(DitherImpl::new_with_noise(input, noise, target_bits)) + } + } + } +} + +impl Iterator for Dither +where + I: Source, + R: Rng + SeedableRng + Clone, +{ + type Item = Sample; + + #[inline] + fn next(&mut self) -> Option { + match self { + Dither::GPDF(d) => d.next(), + Dither::HighPass(d) => d.next(), + Dither::RPDF(d) => d.next(), + Dither::TPDF(d) => d.next(), + } + } +} + +impl Source for Dither +where + I: Source, + R: Rng + SeedableRng + Clone, +{ + #[inline] + fn current_span_len(&self) -> Option { + match self { + Dither::GPDF(d) => d.current_span_len(), + Dither::HighPass(d) => d.current_span_len(), + Dither::RPDF(d) => d.current_span_len(), + Dither::TPDF(d) => d.current_span_len(), + } + } + + #[inline] + fn channels(&self) -> ChannelCount { + match self { + Dither::GPDF(d) => d.channels(), + Dither::HighPass(d) => d.channels(), + Dither::RPDF(d) => d.channels(), + Dither::TPDF(d) => d.channels(), + } + } + + #[inline] + fn sample_rate(&self) -> SampleRate { + match self { + Dither::GPDF(d) => d.sample_rate(), + Dither::HighPass(d) => d.sample_rate(), + Dither::RPDF(d) => d.sample_rate(), + Dither::TPDF(d) => d.sample_rate(), + } + } + + #[inline] + fn total_duration(&self) -> Option { + match self { + Dither::GPDF(d) => d.total_duration(), + Dither::HighPass(d) => d.total_duration(), + Dither::RPDF(d) => d.total_duration(), + Dither::TPDF(d) => d.total_duration(), + } + } + + #[inline] + fn bits_per_sample(&self) -> Option { + match self { + Dither::GPDF(d) => d.bits_per_sample(), + Dither::HighPass(d) => d.bits_per_sample(), + Dither::RPDF(d) => d.bits_per_sample(), + Dither::TPDF(d) => d.bits_per_sample(), + } + } + + #[inline] + fn try_seek(&mut self, pos: Duration) -> Result<(), crate::source::SeekError> { + match self { + Dither::GPDF(d) => d.try_seek(pos), + Dither::HighPass(d) => d.try_seek(pos), + Dither::RPDF(d) => d.try_seek(pos), + Dither::TPDF(d) => d.try_seek(pos), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::source::{SineWave, Source}; + use crate::{nz, BitDepth, SampleRate}; + + const TEST_SAMPLE_RATE: SampleRate = nz!(44100); + const TEST_BIT_DEPTH: BitDepth = nz!(16); + + #[test] + fn test_dither_algorithms() { + let source = SineWave::new(440.0).take_duration(std::time::Duration::from_millis(10)); + + // Test all four algorithms + let mut gpdf = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::GPDF); + let mut highpass = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::HighPass); + let mut rpdf = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::RPDF); + let mut tpdf = Dither::new(source, TEST_BIT_DEPTH, Algorithm::TPDF); + + for _ in 0..10 { + let gpdf_sample = gpdf.next().unwrap(); + let highpass_sample = highpass.next().unwrap(); + let rpdf_sample = rpdf.next().unwrap(); + let tpdf_sample = tpdf.next().unwrap(); + + // RPDF and TPDF should be bounded + assert!((-1.0..=1.0).contains(&rpdf_sample)); + assert!((-1.0..=1.0).contains(&tpdf_sample)); + + // Note: GPDF (Gaussian) and HighPass (Blue) may occasionally exceed [-1,1] bounds + assert!(gpdf_sample.is_normal()); + assert!(highpass_sample.is_normal()); + } + } + + #[test] + fn test_dither_adds_noise() { + let source = SineWave::new(440.0).take_duration(std::time::Duration::from_millis(10)); + let mut dithered = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::TPDF); + let mut undithered = source; + + // Collect samples from both sources + let dithered_samples: Vec = (0..10).filter_map(|_| dithered.next()).collect(); + let undithered_samples: Vec = (0..10).filter_map(|_| undithered.next()).collect(); + + let lsb = 1.0 / (1_i64 << (TEST_BIT_DEPTH.get() - 1)) as f32; + + // Verify dithered samples differ from undithered and are reasonable + for (i, (&dithered_sample, &undithered_sample)) in dithered_samples + .iter() + .zip(undithered_samples.iter()) + .enumerate() + { + // Should be finite + assert!( + dithered_sample.is_finite(), + "Dithered sample {} should be finite", + i + ); + + // The difference should be small (just dither noise) + let diff = (dithered_sample - undithered_sample).abs(); + let max_expected_diff = lsb * 2.0; // Max triangular dither amplitude + assert!( + diff <= max_expected_diff, + "Dither noise too large: sample {}, diff {}, max expected {}", + i, + diff, + max_expected_diff + ); + } + } +} diff --git a/src/source/mod.rs b/src/source/mod.rs index 52dcc2f0..6916d84d 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -85,6 +85,26 @@ mod triangle; mod uniform; mod zero; +#[cfg(feature = "dither")] +#[cfg_attr(docsrs, doc(cfg(feature = "dither")))] +mod dither; +#[cfg(feature = "dither")] +#[cfg_attr(docsrs, doc(cfg(feature = "dither")))] +pub use self::dither::{Algorithm as DitherAlgorithm, Dither}; + +/// Creates a dithered source using the specified algorithm. +/// +/// Dithering eliminates quantization artifacts when converting from high-precision +/// audio to lower bit depths. Apply at the target output bit depth. +#[cfg(feature = "dither")] +#[cfg_attr(docsrs, doc(cfg(feature = "dither")))] +pub fn dither(input: I, target_bits: BitDepth, algorithm: DitherAlgorithm) -> Dither +where + I: Source, +{ + Dither::new(input, target_bits, algorithm) +} + #[cfg(feature = "noise")] #[cfg_attr(docsrs, doc(cfg(feature = "noise")))] pub mod noise; diff --git a/src/source/noise.rs b/src/source/noise.rs index b9987a8d..9271e70e 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -19,6 +19,7 @@ //! ```rust //! use std::num::NonZero; //! use rodio::source::noise::{WhiteUniform, Pink, WhiteTriangular, Blue, Red}; +//! use rodio::SampleRate; //! //! let sample_rate = NonZero::new(44100).unwrap(); //! From 923d24f0ff973a539adf094a7a235ad2a2aa1005 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 17 Sep 2025 22:21:06 +0200 Subject: [PATCH 02/11] fix: handle LSB amplitude for high bit depths in dither source --- src/source/dither.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/source/dither.rs b/src/source/dither.rs index 52712ef0..95aae270 100644 --- a/src/source/dither.rs +++ b/src/source/dither.rs @@ -92,8 +92,13 @@ where pub(crate) fn new_with_noise(input: I, noise: N, target_bits: BitDepth) -> Self { // LSB amplitude for signed audio: 1.0 / (2^(bits-1)) // This represents the amplitude of one quantization level - // Use i64 bit shifting to avoid overflow (supports up to 63 bits) - let lsb_amplitude = 1.0 / (1_i64 << (target_bits.get() - 1)) as f32; + let lsb_amplitude = if target_bits.get() >= Sample::MANTISSA_DIGITS { + // For bit depths at or beyond the floating point precision limit, + // the LSB amplitude calculation becomes meaningless + Sample::MIN_POSITIVE + } else { + 1.0 / (1_i64 << (target_bits.get() - 1)) as f32 + }; Self { input, From f77724415acbbcf72d00fc472db10da16a953b0a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 17 Sep 2025 22:22:39 +0200 Subject: [PATCH 03/11] refactor: Velvet noise to use usize for density and positions Also replace f32 with Sample where appropriate for consistency --- CHANGELOG.md | 1 + src/source/limit.rs | 2 +- src/source/noise.rs | 40 ++++++++++++++++++++++++---------------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e44d5bd..851d0ded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `output_to_wav` renamed to `wav_to_file` and now takes ownership of the `Source`. - `Blue` noise generator uses uniform instead of Gaussian noise for better performance. - `Gaussian` noise generator has standard deviation of 0.6 for perceptual equivalence. +- `Velvet` noise generator takes density in Hz as `usize` instead of `f32`. ## Version [0.21.1] (2025-07-14) diff --git a/src/source/limit.rs b/src/source/limit.rs index 37ca282a..85b9a4b4 100644 --- a/src/source/limit.rs +++ b/src/source/limit.rs @@ -817,7 +817,7 @@ pub struct LimitMulti { fn process_sample(sample: Sample, threshold: f32, knee_width: f32, inv_knee_8: f32) -> f32 { // Add slight DC offset. Some samples are silence, which is -inf dB and gets the limiter stuck. // Adding a small positive offset prevents this. - let bias_db = math::linear_to_db(sample.abs() + f32::MIN_POSITIVE) - threshold; + let bias_db = math::linear_to_db(sample.abs() + Sample::MIN_POSITIVE) - threshold; let knee_boundary_db = bias_db * 2.0; if knee_boundary_db < -knee_width { 0.0 diff --git a/src/source/noise.rs b/src/source/noise.rs index 9271e70e..f71704a8 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -244,9 +244,9 @@ impl_noise_source!(WhiteTriangular); pub struct Velvet { sample_rate: SampleRate, rng: R, - grid_size: f32, // samples per grid cell - grid_pos: f32, // current position in grid cell - impulse_pos: f32, // where impulse occurs in current grid + grid_size: usize, // samples per grid cell + grid_pos: usize, // current position in grid cell + impulse_pos: usize, // where impulse occurs in current grid } impl Velvet { @@ -260,14 +260,14 @@ impl Velvet { /// Create a new velvet noise generator with a custom RNG. pub fn new_with_rng(sample_rate: SampleRate, mut rng: R) -> Self { let density = VELVET_DEFAULT_DENSITY; - let grid_size = sample_rate.get() as f32 / density; - let impulse_pos = rng.random::() * grid_size; + let grid_size = (sample_rate.get() as f32 / density).ceil() as usize; + let impulse_pos = rng.random_range(0..grid_size); Self { sample_rate, rng, grid_size, - grid_pos: 0.0, + grid_pos: 0, impulse_pos, } } @@ -284,14 +284,18 @@ impl Velvet { pub fn new_with_density(sample_rate: SampleRate, density: f32) -> Self { let mut rng = R::from_os_rng(); let density = density.max(f32::MIN_POSITIVE); - let grid_size = sample_rate.get() as f32 / density; - let impulse_pos = rng.random::() * grid_size; + let grid_size = (sample_rate.get() as f32 / density).ceil() as usize; + let impulse_pos = if grid_size > 0 { + rng.random_range(0..grid_size) + } else { + 0 + }; Self { sample_rate, rng, grid_size, - grid_pos: 0.0, + grid_pos: 0, impulse_pos, } } @@ -302,7 +306,7 @@ impl Iterator for Velvet { #[inline] fn next(&mut self) -> Option { - let output = if self.grid_pos as usize == self.impulse_pos as usize { + let output = if self.grid_pos == self.impulse_pos { // Generate impulse with random polarity if self.rng.random::() { 1.0 @@ -313,12 +317,16 @@ impl Iterator for Velvet { 0.0 }; - self.grid_pos += 1.0; + self.grid_pos = self.grid_pos.wrapping_add(1); // Start new grid cell when we reach the end if self.grid_pos >= self.grid_size { - self.grid_pos = 0.0; - self.impulse_pos = self.rng.random::() * self.grid_size; + self.grid_pos = 0; + self.impulse_pos = if self.grid_size > 0 { + self.rng.random_range(0..self.grid_size) + } else { + 0 + }; } Some(output) @@ -1015,8 +1023,8 @@ mod tests { #[test] fn test_white_uniform_distribution() { let mut generator = WhiteUniform::new(TEST_SAMPLE_RATE); - let mut min = f32::INFINITY; - let mut max = f32::NEG_INFINITY; + let mut min = Sample::INFINITY; + let mut max = Sample::NEG_INFINITY; for _ in 0..TEST_SAMPLES_MEDIUM { let sample = generator.next().unwrap(); @@ -1054,7 +1062,7 @@ mod tests { let generator = WhiteTriangular::new(TEST_SAMPLE_RATE); let expected_std_dev = 2.0 / (6.0_f32).sqrt(); assert!( - (generator.std_dev() - expected_std_dev).abs() < f32::EPSILON, + (generator.std_dev() - expected_std_dev).abs() < Sample::EPSILON, "Triangular std_dev should be 2/sqrt(6) ≈ 0.8165, got {}", generator.std_dev() ); From eccbdbfefd243f08f810119e8725e3b56f3cdb5b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 26 Sep 2025 23:02:14 +0200 Subject: [PATCH 04/11] refactor: rebase on master --- src/common.rs | 3 +++ src/lib.rs | 2 +- src/source/dither.rs | 17 ----------------- src/source/mod.rs | 4 +++- 4 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/common.rs b/src/common.rs index 6c713a07..5799c1e5 100644 --- a/src/common.rs +++ b/src/common.rs @@ -7,6 +7,9 @@ pub type SampleRate = NonZero; /// Number of channels in a stream. Can never be Zero pub type ChannelCount = NonZero; +/// Number of bits per sample. Can never be zero. +pub type BitDepth = NonZero; + /// Represents value of a single sample. /// Silence corresponds to the value `0.0`. The expected amplitude range is -1.0...1.0. /// Values below and above this range are clipped in conversion to other sample types. diff --git a/src/lib.rs b/src/lib.rs index e4b8ba43..b4fcfaa0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -189,7 +189,7 @@ pub mod queue; pub mod source; pub mod static_buffer; -pub use crate::common::{ChannelCount, Sample, SampleRate}; +pub use crate::common::{BitDepth, ChannelCount, Sample, SampleRate}; pub use crate::decoder::Decoder; pub use crate::sink::Sink; pub use crate::source::Source; diff --git a/src/source/dither.rs b/src/source/dither.rs index 95aae270..a5fd41a4 100644 --- a/src/source/dither.rs +++ b/src/source/dither.rs @@ -74,7 +74,6 @@ pub enum Algorithm { pub struct DitherImpl { input: I, noise: N, - target_bits: BitDepth, lsb_amplitude: f32, } @@ -103,7 +102,6 @@ where Self { input, noise, - target_bits, lsb_amplitude, } } @@ -153,11 +151,6 @@ where self.input.total_duration() } - #[inline] - fn bits_per_sample(&self) -> Option { - Some(self.target_bits) - } - #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), crate::source::SeekError> { self.input.try_seek(pos) @@ -288,16 +281,6 @@ where } } - #[inline] - fn bits_per_sample(&self) -> Option { - match self { - Dither::GPDF(d) => d.bits_per_sample(), - Dither::HighPass(d) => d.bits_per_sample(), - Dither::RPDF(d) => d.bits_per_sample(), - Dither::TPDF(d) => d.bits_per_sample(), - } - } - #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), crate::source::SeekError> { match self { diff --git a/src/source/mod.rs b/src/source/mod.rs index 6916d84d..c33498b5 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -1,6 +1,8 @@ //! Sources of sound and various filters. use core::time::Duration; +#[cfg(feature = "dither")] +use std::num::NonZero; use std::sync::Arc; use crate::{ @@ -98,7 +100,7 @@ pub use self::dither::{Algorithm as DitherAlgorithm, Dither}; /// audio to lower bit depths. Apply at the target output bit depth. #[cfg(feature = "dither")] #[cfg_attr(docsrs, doc(cfg(feature = "dither")))] -pub fn dither(input: I, target_bits: BitDepth, algorithm: DitherAlgorithm) -> Dither +pub fn dither(input: I, target_bits: NonZero, algorithm: DitherAlgorithm) -> Dither where I: Source, { From d4be9df486d18522c1116084df14d5dcd660fd2d Mon Sep 17 00:00:00 2001 From: dvdsk Date: Sat, 27 Sep 2025 13:34:52 +0200 Subject: [PATCH 05/11] messy example of builder API approach --- src/source/dither.rs | 483 +++++++++++++++++++++---------------------- src/source/mod.rs | 28 +-- 2 files changed, 244 insertions(+), 267 deletions(-) diff --git a/src/source/dither.rs b/src/source/dither.rs index a5fd41a4..59e85436 100644 --- a/src/source/dither.rs +++ b/src/source/dither.rs @@ -36,76 +36,32 @@ use crate::{ BitDepth, ChannelCount, Sample, SampleRate, Source, }; -/// Dither algorithm selection - chooses the probability density function (PDF). -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub enum Algorithm { - /// GPDF (Gaussian PDF) - normal/bell curve distribution. - /// - /// Uses Gaussian white noise which more closely mimics natural processes and - /// analog circuits. Higher noise floor than TPDF. - GPDF, - - /// High-pass dithering - reduces low-frequency artifacts. - /// - /// Uses blue noise (high-pass filtered white noise) to push dither energy - /// toward higher frequencies. Particularly effective for reducing audible - /// low-frequency modulation artifacts. Best for material with significant - /// low-frequency content where traditional white dither might be audible. - HighPass, - - /// RPDF (Rectangular PDF) - uniform distribution. - /// - /// Uses uniform white noise for basic decorrelation. Simpler than TPDF but - /// allows some correlation between signal and quantization error at low levels. - /// Slightly lower noise floor than TPDF. - RPDF, - - /// TPDF (Triangular PDF) - triangular distribution. - /// - /// The gold standard for audio dithering. Provides mathematically optimal - /// decorrelation by completely eliminating correlation between the original - /// signal and quantization error. - #[default] - TPDF, -} - -/// Internal dithering implementation with a specific noise generator type. -#[derive(Clone, Debug)] -pub struct DitherImpl { - input: I, - noise: N, - lsb_amplitude: f32, -} - -impl DitherImpl -where - I: Source, - N: Iterator, -{ - /// Creates a new dither source with a custom noise generator. - /// - /// This low-level internal constructor allows providing a custom noise generator. - /// The noise generator should produce samples with appropriate amplitude - /// for the chosen dither type. - #[inline] - pub(crate) fn new_with_noise(input: I, noise: N, target_bits: BitDepth) -> Self { - // LSB amplitude for signed audio: 1.0 / (2^(bits-1)) - // This represents the amplitude of one quantization level - let lsb_amplitude = if target_bits.get() >= Sample::MANTISSA_DIGITS { - // For bit depths at or beyond the floating point precision limit, - // the LSB amplitude calculation becomes meaningless - Sample::MIN_POSITIVE - } else { - 1.0 / (1_i64 << (target_bits.get() - 1)) as f32 - }; - - Self { - input, - noise, - lsb_amplitude, - } - } -} +// /// Dither algorithm selection - chooses the probability density function (PDF). +// #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +// pub enum Algorithm { +// /// High-pass dithering - reduces low-frequency artifacts. +// /// +// /// Uses blue noise (high-pass filtered white noise) to push dither energy +// /// toward higher frequencies. Particularly effective for reducing audible +// /// low-frequency modulation artifacts. Best for material with significant +// /// low-frequency content where traditional white dither might be audible. +// HighPass, +// +// /// RPDF (Rectangular PDF) - uniform distribution. +// /// +// /// Uses uniform white noise for basic decorrelation. Simpler than TPDF but +// /// allows some correlation between signal and quantization error at low levels. +// /// Slightly lower noise floor than TPDF. +// RPDF, +// +// /// TPDF (Triangular PDF) - triangular distribution. +// /// +// /// The gold standard for audio dithering. Provides mathematically optimal +// /// decorrelation by completely eliminating correlation between the original +// /// signal and quantization error. +// #[default] +// TPDF, +// } impl Iterator for DitherImpl where @@ -157,33 +113,30 @@ where } } -/// Dithering interface delegating to the supported dithering algorithms. -#[derive(Clone)] -pub enum Dither -where - R: Rng + SeedableRng + Clone, -{ - /// GPDF dithering with Gaussian white noise - GPDF(DitherImpl>), - - /// High-pass dithering with blue noise - HighPass(DitherImpl>), - - /// RPDF dithering with uniform white noise - RPDF(DitherImpl>), +// /// Dithering interface delegating to the supported dithering algorithms. +// #[derive(Clone)] +// pub enum Dither +// where +// R: Rng + SeedableRng + Clone, +// { +// /// High-pass dithering with blue noise +// HighPass(DitherImpl>), +// +// /// RPDF dithering with uniform white noise +// RPDF(DitherImpl>), +// +// /// TPDF dithering with triangular white noise +// TPDF(DitherImpl>), +// } - /// TPDF dithering with triangular white noise - TPDF(DitherImpl>), -} - -impl Dither -where - I: Source, - R: Rng + SeedableRng + Clone, -{ +#[derive(Clone, Debug)] +pub struct DitherImpl { + input: I, + noise: N, + lsb_amplitude: f32, } -impl Dither +impl DitherImpl where I: Source, { @@ -195,173 +148,197 @@ where /// - `RPDF`: Lower noise floor but some correlation /// - `TPDF` (default): Optimal decorrelation #[inline] - pub fn new(input: I, target_bits: BitDepth, algorithm: Algorithm) -> Self { - let sample_rate = input.sample_rate(); - match algorithm { - Algorithm::GPDF => { - let noise = WhiteGaussian::new(sample_rate); - Self::GPDF(DitherImpl::new_with_noise(input, noise, target_bits)) - } - Algorithm::HighPass => { - let noise = Blue::new(sample_rate); - Self::HighPass(DitherImpl::new_with_noise(input, noise, target_bits)) - } - Algorithm::RPDF => { - let noise = WhiteUniform::new(sample_rate); - Self::RPDF(DitherImpl::new_with_noise(input, noise, target_bits)) - } - Algorithm::TPDF => { - let noise = WhiteTriangular::new(sample_rate); - Self::TPDF(DitherImpl::new_with_noise(input, noise, target_bits)) - } - } + pub fn builder(input: I, target_bits: BitDepth) -> DitherBuilder { + DitherBuilder { input, target_bits } } } -impl Iterator for Dither -where - I: Source, - R: Rng + SeedableRng + Clone, -{ - type Item = Sample; - - #[inline] - fn next(&mut self) -> Option { - match self { - Dither::GPDF(d) => d.next(), - Dither::HighPass(d) => d.next(), - Dither::RPDF(d) => d.next(), - Dither::TPDF(d) => d.next(), - } - } +struct DitherBuilder { + input: I, + target_bits: BitDepth, } -impl Source for Dither -where - I: Source, - R: Rng + SeedableRng + Clone, -{ - #[inline] - fn current_span_len(&self) -> Option { - match self { - Dither::GPDF(d) => d.current_span_len(), - Dither::HighPass(d) => d.current_span_len(), - Dither::RPDF(d) => d.current_span_len(), - Dither::TPDF(d) => d.current_span_len(), - } - } - - #[inline] - fn channels(&self) -> ChannelCount { - match self { - Dither::GPDF(d) => d.channels(), - Dither::HighPass(d) => d.channels(), - Dither::RPDF(d) => d.channels(), - Dither::TPDF(d) => d.channels(), - } - } - - #[inline] - fn sample_rate(&self) -> SampleRate { - match self { - Dither::GPDF(d) => d.sample_rate(), - Dither::HighPass(d) => d.sample_rate(), - Dither::RPDF(d) => d.sample_rate(), - Dither::TPDF(d) => d.sample_rate(), - } - } - - #[inline] - fn total_duration(&self) -> Option { - match self { - Dither::GPDF(d) => d.total_duration(), - Dither::HighPass(d) => d.total_duration(), - Dither::RPDF(d) => d.total_duration(), - Dither::TPDF(d) => d.total_duration(), +impl DitherBuilder { + fn lsb_amplitude(&self) -> f32 { + // LSB amplitude for signed audio: 1.0 / (2^(bits-1)) + // This represents the amplitude of one quantization level + if self.target_bits.get() >= Sample::MANTISSA_DIGITS { + // For bit depths at or beyond the floating point precision limit, + // the LSB amplitude calculation becomes meaningless + Sample::MIN_POSITIVE + } else { + 1.0 / (1_i64 << (self.target_bits.get() - 1)) as f32 } } - - #[inline] - fn try_seek(&mut self, pos: Duration) -> Result<(), crate::source::SeekError> { - match self { - Dither::GPDF(d) => d.try_seek(pos), - Dither::HighPass(d) => d.try_seek(pos), - Dither::RPDF(d) => d.try_seek(pos), - Dither::TPDF(d) => d.try_seek(pos), + /// GPDF (Gaussian PDF) - normal/bell curve distribution. + /// + /// Uses Gaussian white noise which more closely mimics natural processes and + /// analog circuits. Higher noise floor than TPDF. + fn gpdf(self) -> DitherImpl { + DitherImpl { + lsb_amplitude: self.lsb_amplitude(), + noise: WhiteGaussian::new(self.input.sample_rate()), + input: self.input, } } + // Algorithm::HighPass => { + // let noise = Blue::new(sample_rate); + // Self::HighPass(DitherImpl::new_with_noise(input, noise, target_bits)) + // } + // Algorithm::RPDF => { + // let noise = WhiteUniform::new(sample_rate); + // Self::RPDF(DitherImpl::new_with_noise(input, noise, target_bits)) + // } + // Algorithm::TPDF => { + // let noise = WhiteTriangular::new(sample_rate); + // Self::TPDF(DitherImpl::new_with_noise(input, noise, target_bits)) + // } } -#[cfg(test)] -mod tests { - use super::*; - use crate::source::{SineWave, Source}; - use crate::{nz, BitDepth, SampleRate}; - - const TEST_SAMPLE_RATE: SampleRate = nz!(44100); - const TEST_BIT_DEPTH: BitDepth = nz!(16); - - #[test] - fn test_dither_algorithms() { - let source = SineWave::new(440.0).take_duration(std::time::Duration::from_millis(10)); - - // Test all four algorithms - let mut gpdf = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::GPDF); - let mut highpass = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::HighPass); - let mut rpdf = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::RPDF); - let mut tpdf = Dither::new(source, TEST_BIT_DEPTH, Algorithm::TPDF); - - for _ in 0..10 { - let gpdf_sample = gpdf.next().unwrap(); - let highpass_sample = highpass.next().unwrap(); - let rpdf_sample = rpdf.next().unwrap(); - let tpdf_sample = tpdf.next().unwrap(); - - // RPDF and TPDF should be bounded - assert!((-1.0..=1.0).contains(&rpdf_sample)); - assert!((-1.0..=1.0).contains(&tpdf_sample)); - - // Note: GPDF (Gaussian) and HighPass (Blue) may occasionally exceed [-1,1] bounds - assert!(gpdf_sample.is_normal()); - assert!(highpass_sample.is_normal()); - } - } - - #[test] - fn test_dither_adds_noise() { - let source = SineWave::new(440.0).take_duration(std::time::Duration::from_millis(10)); - let mut dithered = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::TPDF); - let mut undithered = source; - - // Collect samples from both sources - let dithered_samples: Vec = (0..10).filter_map(|_| dithered.next()).collect(); - let undithered_samples: Vec = (0..10).filter_map(|_| undithered.next()).collect(); - - let lsb = 1.0 / (1_i64 << (TEST_BIT_DEPTH.get() - 1)) as f32; - - // Verify dithered samples differ from undithered and are reasonable - for (i, (&dithered_sample, &undithered_sample)) in dithered_samples - .iter() - .zip(undithered_samples.iter()) - .enumerate() - { - // Should be finite - assert!( - dithered_sample.is_finite(), - "Dithered sample {} should be finite", - i - ); - - // The difference should be small (just dither noise) - let diff = (dithered_sample - undithered_sample).abs(); - let max_expected_diff = lsb * 2.0; // Max triangular dither amplitude - assert!( - diff <= max_expected_diff, - "Dither noise too large: sample {}, diff {}, max expected {}", - i, - diff, - max_expected_diff - ); - } - } -} +// impl Iterator for Dither +// where +// I: Source, +// R: Rng + SeedableRng + Clone, +// { +// type Item = Sample; +// +// #[inline] +// fn next(&mut self) -> Option { +// match self { +// Dither::GPDF(d) => d.next(), +// Dither::HighPass(d) => d.next(), +// Dither::RPDF(d) => d.next(), +// Dither::TPDF(d) => d.next(), +// } +// } +// } + +// impl Source for Dither +// where +// I: Source, +// R: Rng + SeedableRng + Clone, +// { +// #[inline] +// fn current_span_len(&self) -> Option { +// match self { +// Dither::GPDF(d) => d.current_span_len(), +// Dither::HighPass(d) => d.current_span_len(), +// Dither::RPDF(d) => d.current_span_len(), +// Dither::TPDF(d) => d.current_span_len(), +// } +// } +// +// #[inline] +// fn channels(&self) -> ChannelCount { +// match self { +// Dither::GPDF(d) => d.channels(), +// Dither::HighPass(d) => d.channels(), +// Dither::RPDF(d) => d.channels(), +// Dither::TPDF(d) => d.channels(), +// } +// } +// +// #[inline] +// fn sample_rate(&self) -> SampleRate { +// match self { +// Dither::GPDF(d) => d.sample_rate(), +// Dither::HighPass(d) => d.sample_rate(), +// Dither::RPDF(d) => d.sample_rate(), +// Dither::TPDF(d) => d.sample_rate(), +// } +// } +// +// #[inline] +// fn total_duration(&self) -> Option { +// match self { +// Dither::GPDF(d) => d.total_duration(), +// Dither::HighPass(d) => d.total_duration(), +// Dither::RPDF(d) => d.total_duration(), +// Dither::TPDF(d) => d.total_duration(), +// } +// } +// +// #[inline] +// fn try_seek(&mut self, pos: Duration) -> Result<(), crate::source::SeekError> { +// match self { +// Dither::GPDF(d) => d.try_seek(pos), +// Dither::HighPass(d) => d.try_seek(pos), +// Dither::RPDF(d) => d.try_seek(pos), +// Dither::TPDF(d) => d.try_seek(pos), +// } +// } +// } + +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::source::{SineWave, Source}; +// use crate::{nz, BitDepth, SampleRate}; +// +// const TEST_SAMPLE_RATE: SampleRate = nz!(44100); +// const TEST_BIT_DEPTH: BitDepth = nz!(16); +// +// #[test] +// fn test_dither_algorithms() { +// let source = SineWave::new(440.0).take_duration(std::time::Duration::from_millis(10)); +// +// // Test all four algorithms +// let mut gpdf = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::GPDF); +// let mut highpass = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::HighPass); +// let mut rpdf = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::RPDF); +// let mut tpdf = Dither::new(source, TEST_BIT_DEPTH, Algorithm::TPDF); +// +// for _ in 0..10 { +// let gpdf_sample = gpdf.next().unwrap(); +// let highpass_sample = highpass.next().unwrap(); +// let rpdf_sample = rpdf.next().unwrap(); +// let tpdf_sample = tpdf.next().unwrap(); +// +// // RPDF and TPDF should be bounded +// assert!((-1.0..=1.0).contains(&rpdf_sample)); +// assert!((-1.0..=1.0).contains(&tpdf_sample)); +// +// // Note: GPDF (Gaussian) and HighPass (Blue) may occasionally exceed [-1,1] bounds +// assert!(gpdf_sample.is_normal()); +// assert!(highpass_sample.is_normal()); +// } +// } +// +// #[test] +// fn test_dither_adds_noise() { +// let source = SineWave::new(440.0).take_duration(std::time::Duration::from_millis(10)); +// let mut dithered = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::TPDF); +// let mut undithered = source; +// +// // Collect samples from both sources +// let dithered_samples: Vec = (0..10).filter_map(|_| dithered.next()).collect(); +// let undithered_samples: Vec = (0..10).filter_map(|_| undithered.next()).collect(); +// +// let lsb = 1.0 / (1_i64 << (TEST_BIT_DEPTH.get() - 1)) as f32; +// +// // Verify dithered samples differ from undithered and are reasonable +// for (i, (&dithered_sample, &undithered_sample)) in dithered_samples +// .iter() +// .zip(undithered_samples.iter()) +// .enumerate() +// { +// // Should be finite +// assert!( +// dithered_sample.is_finite(), +// "Dithered sample {} should be finite", +// i +// ); +// +// // The difference should be small (just dither noise) +// let diff = (dithered_sample - undithered_sample).abs(); +// let max_expected_diff = lsb * 2.0; // Max triangular dither amplitude +// assert!( +// diff <= max_expected_diff, +// "Dither noise too large: sample {}, diff {}, max expected {}", +// i, +// diff, +// max_expected_diff +// ); +// } +// } +// } diff --git a/src/source/mod.rs b/src/source/mod.rs index c33498b5..1fea576b 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -1,8 +1,8 @@ //! Sources of sound and various filters. use core::time::Duration; -#[cfg(feature = "dither")] -use std::num::NonZero; +// #[cfg(feature = "dither")] +// use std::num::NonZero; use std::sync::Arc; use crate::{ @@ -94,18 +94,18 @@ mod dither; #[cfg_attr(docsrs, doc(cfg(feature = "dither")))] pub use self::dither::{Algorithm as DitherAlgorithm, Dither}; -/// Creates a dithered source using the specified algorithm. -/// -/// Dithering eliminates quantization artifacts when converting from high-precision -/// audio to lower bit depths. Apply at the target output bit depth. -#[cfg(feature = "dither")] -#[cfg_attr(docsrs, doc(cfg(feature = "dither")))] -pub fn dither(input: I, target_bits: NonZero, algorithm: DitherAlgorithm) -> Dither -where - I: Source, -{ - Dither::new(input, target_bits, algorithm) -} +// /// Creates a dithered source using the specified algorithm. +// /// +// /// Dithering eliminates quantization artifacts when converting from high-precision +// /// audio to lower bit depths. Apply at the target output bit depth. +// #[cfg(feature = "dither")] +// #[cfg_attr(docsrs, doc(cfg(feature = "dither")))] +// pub fn dither(input: I, target_bits: NonZero, algorithm: DitherAlgorithm) -> Dither +// where +// I: Source, +// { +// Dither::new(input, target_bits, algorithm) +// } #[cfg(feature = "noise")] #[cfg_attr(docsrs, doc(cfg(feature = "noise")))] From f3b93316fb87580b67b7eb374f9c8ec62a8f2adc Mon Sep 17 00:00:00 2001 From: dvdsk Date: Sat, 27 Sep 2025 15:50:48 +0200 Subject: [PATCH 06/11] nice and simple :) --- src/source/dither.rs | 391 ++++++++++++++++--------------------------- 1 file changed, 143 insertions(+), 248 deletions(-) diff --git a/src/source/dither.rs b/src/source/dither.rs index 59e85436..b958086a 100644 --- a/src/source/dither.rs +++ b/src/source/dither.rs @@ -27,43 +27,10 @@ //! When you later change volume (e.g., with `Sink::set_volume()`), both the signal //! and dither noise scale together, maintaining proper dithering behavior. +use crate::{BitDepth, ChannelCount, Sample, SampleRate, Source}; use std::time::Duration; -use rand::{rngs::SmallRng, Rng, SeedableRng}; - -use crate::{ - source::noise::{Blue, WhiteGaussian, WhiteTriangular, WhiteUniform}, - BitDepth, ChannelCount, Sample, SampleRate, Source, -}; - -// /// Dither algorithm selection - chooses the probability density function (PDF). -// #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -// pub enum Algorithm { -// /// High-pass dithering - reduces low-frequency artifacts. -// /// -// /// Uses blue noise (high-pass filtered white noise) to push dither energy -// /// toward higher frequencies. Particularly effective for reducing audible -// /// low-frequency modulation artifacts. Best for material with significant -// /// low-frequency content where traditional white dither might be audible. -// HighPass, -// -// /// RPDF (Rectangular PDF) - uniform distribution. -// /// -// /// Uses uniform white noise for basic decorrelation. Simpler than TPDF but -// /// allows some correlation between signal and quantization error at low levels. -// /// Slightly lower noise floor than TPDF. -// RPDF, -// -// /// TPDF (Triangular PDF) - triangular distribution. -// /// -// /// The gold standard for audio dithering. Provides mathematically optimal -// /// decorrelation by completely eliminating correlation between the original -// /// signal and quantization error. -// #[default] -// TPDF, -// } - -impl Iterator for DitherImpl +impl Iterator for Dither where I: Source, N: Iterator, @@ -82,7 +49,7 @@ where } } -impl Source for DitherImpl +impl Source for Dither where I: Source, N: Iterator, @@ -113,232 +80,160 @@ where } } -// /// Dithering interface delegating to the supported dithering algorithms. -// #[derive(Clone)] -// pub enum Dither -// where -// R: Rng + SeedableRng + Clone, -// { -// /// High-pass dithering with blue noise -// HighPass(DitherImpl>), -// -// /// RPDF dithering with uniform white noise -// RPDF(DitherImpl>), -// -// /// TPDF dithering with triangular white noise -// TPDF(DitherImpl>), -// } - #[derive(Clone, Debug)] -pub struct DitherImpl { +pub struct Dither { input: I, noise: N, lsb_amplitude: f32, } -impl DitherImpl -where - I: Source, -{ - /// Creates a new dithered source using the specified algorithm. - /// - /// This is the main constructor for dithering. Choose the algorithm based on your needs: - /// - `GPDF`: Natural/analog-like characteristics - /// - `HighPass`: Reduces low-frequency dither artifacts - /// - `RPDF`: Lower noise floor but some correlation - /// - `TPDF` (default): Optimal decorrelation - #[inline] - pub fn builder(input: I, target_bits: BitDepth) -> DitherBuilder { - DitherBuilder { input, target_bits } - } +trait DitherAlgorithm { + type Noise; + fn build_noise(self, sample_rate: SampleRate) -> Self::Noise; } -struct DitherBuilder { - input: I, - target_bits: BitDepth, +macro_rules! dither_algos { + ($($(#[$outer:meta])* $name:ident, $noise:ident);+) => { + $( + $(#[$outer])* + struct $name; + + impl DitherAlgorithm for $name { + type Noise = crate::source::noise::$noise; + fn build_noise(self, sample_rate: SampleRate) -> Self::Noise { + crate::source::noise::$noise::new(sample_rate) + } + } + )+ + }; } - -impl DitherBuilder { - fn lsb_amplitude(&self) -> f32 { - // LSB amplitude for signed audio: 1.0 / (2^(bits-1)) - // This represents the amplitude of one quantization level - if self.target_bits.get() >= Sample::MANTISSA_DIGITS { - // For bit depths at or beyond the floating point precision limit, - // the LSB amplitude calculation becomes meaningless - Sample::MIN_POSITIVE - } else { - 1.0 / (1_i64 << (self.target_bits.get() - 1)) as f32 - } - } +dither_algos! { /// GPDF (Gaussian PDF) - normal/bell curve distribution. /// /// Uses Gaussian white noise which more closely mimics natural processes and /// analog circuits. Higher noise floor than TPDF. - fn gpdf(self) -> DitherImpl { - DitherImpl { - lsb_amplitude: self.lsb_amplitude(), - noise: WhiteGaussian::new(self.input.sample_rate()), - input: self.input, - } - } - // Algorithm::HighPass => { - // let noise = Blue::new(sample_rate); - // Self::HighPass(DitherImpl::new_with_noise(input, noise, target_bits)) - // } - // Algorithm::RPDF => { - // let noise = WhiteUniform::new(sample_rate); - // Self::RPDF(DitherImpl::new_with_noise(input, noise, target_bits)) - // } - // Algorithm::TPDF => { - // let noise = WhiteTriangular::new(sample_rate); - // Self::TPDF(DitherImpl::new_with_noise(input, noise, target_bits)) - // } + GPDF, WhiteGaussian; + /// High-pass dithering - reduces low-frequency artifacts. + /// + /// Uses blue noise (high-pass filtered white noise) to push dither energy + /// toward higher frequencies. Particularly effective for reducing audible + /// low-frequency modulation artifacts. Best for material with significant + /// low-frequency content where traditional white dither might be audible. + HighPass, Blue; + /// RPDF (Rectangular PDF) - uniform distribution. + /// + /// Uses uniform white noise for basic decorrelation. Simpler than TPDF but + /// allows some correlation between signal and quantization error at low levels. + /// Slightly lower noise floor than TPDF. + RPDF, WhiteUniform; + /// TPDF (Triangular PDF) - triangular distribution. + /// + /// The gold standard for audio dithering. Provides mathematically optimal + /// decorrelation by completely eliminating correlation between the original + /// signal and quantization error. + TPDF, WhiteTriangular } -// impl Iterator for Dither -// where -// I: Source, -// R: Rng + SeedableRng + Clone, -// { -// type Item = Sample; -// -// #[inline] -// fn next(&mut self) -> Option { -// match self { -// Dither::GPDF(d) => d.next(), -// Dither::HighPass(d) => d.next(), -// Dither::RPDF(d) => d.next(), -// Dither::TPDF(d) => d.next(), -// } -// } -// } +fn dither( + input: I, + algo: A, + target_bits: BitDepth, +) -> Dither::Noise> +where + I: Source, + A: DitherAlgorithm, +{ + // LSB amplitude for signed audio: 1.0 / (2^(bits-1)) + // This represents the amplitude of one quantization level + let lsb_amplitude = if target_bits.get() >= Sample::MANTISSA_DIGITS { + // For bit depths at or beyond the floating point precision limit, + // the LSB amplitude calculation becomes meaningless + Sample::MIN_POSITIVE + } else { + 1.0 / (1_i64 << (target_bits.get() - 1)) as f32 + }; + + Dither { + noise: algo.build_noise(input.sample_rate()), + input, + lsb_amplitude, + } +} -// impl Source for Dither -// where -// I: Source, -// R: Rng + SeedableRng + Clone, -// { -// #[inline] -// fn current_span_len(&self) -> Option { -// match self { -// Dither::GPDF(d) => d.current_span_len(), -// Dither::HighPass(d) => d.current_span_len(), -// Dither::RPDF(d) => d.current_span_len(), -// Dither::TPDF(d) => d.current_span_len(), -// } -// } -// -// #[inline] -// fn channels(&self) -> ChannelCount { -// match self { -// Dither::GPDF(d) => d.channels(), -// Dither::HighPass(d) => d.channels(), -// Dither::RPDF(d) => d.channels(), -// Dither::TPDF(d) => d.channels(), -// } -// } -// -// #[inline] -// fn sample_rate(&self) -> SampleRate { -// match self { -// Dither::GPDF(d) => d.sample_rate(), -// Dither::HighPass(d) => d.sample_rate(), -// Dither::RPDF(d) => d.sample_rate(), -// Dither::TPDF(d) => d.sample_rate(), -// } -// } -// -// #[inline] -// fn total_duration(&self) -> Option { -// match self { -// Dither::GPDF(d) => d.total_duration(), -// Dither::HighPass(d) => d.total_duration(), -// Dither::RPDF(d) => d.total_duration(), -// Dither::TPDF(d) => d.total_duration(), -// } -// } -// -// #[inline] -// fn try_seek(&mut self, pos: Duration) -> Result<(), crate::source::SeekError> { -// match self { -// Dither::GPDF(d) => d.try_seek(pos), -// Dither::HighPass(d) => d.try_seek(pos), -// Dither::RPDF(d) => d.try_seek(pos), -// Dither::TPDF(d) => d.try_seek(pos), -// } -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use crate::source::{SineWave, Source}; + use crate::{nz, BitDepth, SampleRate}; -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::source::{SineWave, Source}; -// use crate::{nz, BitDepth, SampleRate}; -// -// const TEST_SAMPLE_RATE: SampleRate = nz!(44100); -// const TEST_BIT_DEPTH: BitDepth = nz!(16); -// -// #[test] -// fn test_dither_algorithms() { -// let source = SineWave::new(440.0).take_duration(std::time::Duration::from_millis(10)); -// -// // Test all four algorithms -// let mut gpdf = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::GPDF); -// let mut highpass = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::HighPass); -// let mut rpdf = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::RPDF); -// let mut tpdf = Dither::new(source, TEST_BIT_DEPTH, Algorithm::TPDF); -// -// for _ in 0..10 { -// let gpdf_sample = gpdf.next().unwrap(); -// let highpass_sample = highpass.next().unwrap(); -// let rpdf_sample = rpdf.next().unwrap(); -// let tpdf_sample = tpdf.next().unwrap(); -// -// // RPDF and TPDF should be bounded -// assert!((-1.0..=1.0).contains(&rpdf_sample)); -// assert!((-1.0..=1.0).contains(&tpdf_sample)); -// -// // Note: GPDF (Gaussian) and HighPass (Blue) may occasionally exceed [-1,1] bounds -// assert!(gpdf_sample.is_normal()); -// assert!(highpass_sample.is_normal()); -// } -// } -// -// #[test] -// fn test_dither_adds_noise() { -// let source = SineWave::new(440.0).take_duration(std::time::Duration::from_millis(10)); -// let mut dithered = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::TPDF); -// let mut undithered = source; -// -// // Collect samples from both sources -// let dithered_samples: Vec = (0..10).filter_map(|_| dithered.next()).collect(); -// let undithered_samples: Vec = (0..10).filter_map(|_| undithered.next()).collect(); -// -// let lsb = 1.0 / (1_i64 << (TEST_BIT_DEPTH.get() - 1)) as f32; -// -// // Verify dithered samples differ from undithered and are reasonable -// for (i, (&dithered_sample, &undithered_sample)) in dithered_samples -// .iter() -// .zip(undithered_samples.iter()) -// .enumerate() -// { -// // Should be finite -// assert!( -// dithered_sample.is_finite(), -// "Dithered sample {} should be finite", -// i -// ); -// -// // The difference should be small (just dither noise) -// let diff = (dithered_sample - undithered_sample).abs(); -// let max_expected_diff = lsb * 2.0; // Max triangular dither amplitude -// assert!( -// diff <= max_expected_diff, -// "Dither noise too large: sample {}, diff {}, max expected {}", -// i, -// diff, -// max_expected_diff -// ); -// } -// } -// } + fn show_api() { + let source = SineWave::new(440.0).take_duration(std::time::Duration::from_millis(10)); + let source = dither(source, GPDF, nz!(16)); + } + // const TEST_SAMPLE_RATE: SampleRate = nz!(44100); + // const TEST_BIT_DEPTH: BitDepth = nz!(16); + // + // #[test] + // fn test_dither_algorithms() { + // let source = SineWave::new(440.0).take_duration(std::time::Duration::from_millis(10)); + // + // // Test all four algorithms + // let mut gpdf = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::GPDF); + // let mut highpass = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::HighPass); + // let mut rpdf = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::RPDF); + // let mut tpdf = Dither::new(source, TEST_BIT_DEPTH, Algorithm::TPDF); + // + // for _ in 0..10 { + // let gpdf_sample = gpdf.next().unwrap(); + // let highpass_sample = highpass.next().unwrap(); + // let rpdf_sample = rpdf.next().unwrap(); + // let tpdf_sample = tpdf.next().unwrap(); + // + // // RPDF and TPDF should be bounded + // assert!((-1.0..=1.0).contains(&rpdf_sample)); + // assert!((-1.0..=1.0).contains(&tpdf_sample)); + // + // // Note: GPDF (Gaussian) and HighPass (Blue) may occasionally exceed [-1,1] bounds + // assert!(gpdf_sample.is_normal()); + // assert!(highpass_sample.is_normal()); + // } + // } + // + // #[test] + // fn test_dither_adds_noise() { + // let source = SineWave::new(440.0).take_duration(std::time::Duration::from_millis(10)); + // let mut dithered = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::TPDF); + // let mut undithered = source; + // + // // Collect samples from both sources + // let dithered_samples: Vec = (0..10).filter_map(|_| dithered.next()).collect(); + // let undithered_samples: Vec = (0..10).filter_map(|_| undithered.next()).collect(); + // + // let lsb = 1.0 / (1_i64 << (TEST_BIT_DEPTH.get() - 1)) as f32; + // + // // Verify dithered samples differ from undithered and are reasonable + // for (i, (&dithered_sample, &undithered_sample)) in dithered_samples + // .iter() + // .zip(undithered_samples.iter()) + // .enumerate() + // { + // // Should be finite + // assert!( + // dithered_sample.is_finite(), + // "Dithered sample {} should be finite", + // i + // ); + // + // // The difference should be small (just dither noise) + // let diff = (dithered_sample - undithered_sample).abs(); + // let max_expected_diff = lsb * 2.0; // Max triangular dither amplitude + // assert!( + // diff <= max_expected_diff, + // "Dither noise too large: sample {}, diff {}, max expected {}", + // i, + // diff, + // max_expected_diff + // ); + // } + // } +} From f54393d7ce92309ff2d79cb7d8fb5b9b6f929325 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 27 Sep 2025 21:27:42 +0200 Subject: [PATCH 07/11] refactor: remove explicit SmallRng from noise type impl blocks --- src/source/noise.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/source/noise.rs b/src/source/noise.rs index f71704a8..808f3ea6 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -188,7 +188,7 @@ pub struct WhiteTriangular { sampler: NoiseSampler>, } -impl WhiteTriangular { +impl WhiteTriangular { /// Create a new triangular white noise generator with SmallRng seeded from system entropy. pub fn new(sample_rate: SampleRate) -> Self { Self::new_with_rng(sample_rate, SmallRng::from_os_rng()) @@ -249,7 +249,7 @@ pub struct Velvet { impulse_pos: usize, // where impulse occurs in current grid } -impl Velvet { +impl Velvet { /// Create a new velvet noise generator with SmallRng seeded from system entropy. pub fn new(sample_rate: SampleRate) -> Self { Self::new_with_rng(sample_rate, SmallRng::from_os_rng()) @@ -370,7 +370,7 @@ impl WhiteGaussian { } } -impl WhiteGaussian { +impl WhiteGaussian { /// Create a new Gaussian white noise generator with `SmallRng` seeded from system entropy. pub fn new(sample_rate: SampleRate) -> Self { Self::new_with_rng(sample_rate, SmallRng::from_os_rng()) @@ -454,7 +454,7 @@ pub struct Pink { max_counts: [u32; PINK_NOISE_GENERATORS], } -impl Pink { +impl Pink { /// Create a new pink noise generator with `SmallRng` seeded from system entropy. pub fn new(sample_rate: SampleRate) -> Self { Self::new_with_rng(sample_rate, SmallRng::from_os_rng()) @@ -536,7 +536,7 @@ pub struct Blue { prev_white: f32, } -impl Blue { +impl Blue { /// Create a new blue noise generator with `SmallRng` seeded from system entropy. pub fn new(sample_rate: SampleRate) -> Self { Self::new_with_rng(sample_rate, SmallRng::from_os_rng()) @@ -599,7 +599,7 @@ pub struct Violet { prev: f32, } -impl Violet { +impl Violet { /// Create a new violet noise generator with `SmallRng` seeded from system entropy. pub fn new(sample_rate: SampleRate) -> Self { Self::new_with_rng(sample_rate, SmallRng::from_os_rng()) @@ -712,7 +712,7 @@ pub struct Brownian { inner: IntegratedNoise>, } -impl Brownian { +impl Brownian { /// Create a new brownian noise generator with `SmallRng` seeded from system entropy. pub fn new(sample_rate: SampleRate) -> Self { Self::new_with_rng(sample_rate, SmallRng::from_os_rng()) @@ -789,7 +789,7 @@ pub struct Red { inner: IntegratedNoise>, } -impl Red { +impl Red { /// Create a new red noise generator with `SmallRng` seeded from system entropy. pub fn new(sample_rate: SampleRate) -> Self { Self::new_with_rng(sample_rate, SmallRng::from_os_rng()) From 629a1e3906f89a3c95f035b9338d01c72a95fb35 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 27 Sep 2025 21:52:14 +0200 Subject: [PATCH 08/11] refactor: remove SeedableRng bound from noise generators and change Velvet density to NonZero --- src/source/noise.rs | 65 ++++++++++++++++++--------------------------- 1 file changed, 26 insertions(+), 39 deletions(-) diff --git a/src/source/noise.rs b/src/source/noise.rs index 808f3ea6..8d4b7738 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -41,7 +41,7 @@ //! let white_custom = WhiteUniform::::new_with_rng(sample_rate, StdRng::seed_from_u64(12345)); //! ``` -use std::time::Duration; +use std::{num::NonZero, time::Duration}; use rand::{ distr::{Distribution, Uniform}, @@ -139,7 +139,7 @@ impl WhiteUniform { } } -impl WhiteUniform { +impl WhiteUniform { /// Create a new white noise generator with a custom RNG. pub fn new_with_rng(sample_rate: SampleRate, rng: R) -> Self { let distribution = @@ -195,7 +195,7 @@ impl WhiteTriangular { } } -impl WhiteTriangular { +impl WhiteTriangular { /// Create a new triangular white noise generator with a custom RNG. pub fn new_with_rng(sample_rate: SampleRate, rng: R) -> Self { let distribution = Triangular::new(-1.0, 1.0, 0.0).expect("Valid triangular distribution"); @@ -256,35 +256,21 @@ impl Velvet { } } -impl Velvet { +impl Velvet { /// Create a new velvet noise generator with a custom RNG. - pub fn new_with_rng(sample_rate: SampleRate, mut rng: R) -> Self { - let density = VELVET_DEFAULT_DENSITY; - let grid_size = (sample_rate.get() as f32 / density).ceil() as usize; - let impulse_pos = rng.random_range(0..grid_size); - - Self { - sample_rate, - rng, - grid_size, - grid_pos: 0, - impulse_pos, - } + pub fn new_with_rng(sample_rate: SampleRate, rng: R) -> Self { + Self::new_with_density(sample_rate, VELVET_DEFAULT_DENSITY, rng) } -} -impl Velvet { - /// Create a new velvet noise generator with custom density (impulses per second). + /// Create a new velvet noise generator with custom density (impulses per second) and RNG. /// /// **Density guidelines:** /// - 500-1000 Hz: Sparse, distant reverb effects /// - 1000-2000 Hz: Balanced reverb simulation (default: 2000 Hz) /// - 2000-4000 Hz: Dense, close reverb effects /// - >4000 Hz: Very dense, approaching continuous noise - pub fn new_with_density(sample_rate: SampleRate, density: f32) -> Self { - let mut rng = R::from_os_rng(); - let density = density.max(f32::MIN_POSITIVE); - let grid_size = (sample_rate.get() as f32 / density).ceil() as usize; + pub fn new_with_density(sample_rate: SampleRate, density: NonZero, mut rng: R) -> Self { + let grid_size = (sample_rate.get() as f32 / density.get() as f32).ceil() as usize; let impulse_pos = if grid_size > 0 { rng.random_range(0..grid_size) } else { @@ -301,6 +287,8 @@ impl Velvet { } } +impl Velvet {} + impl Iterator for Velvet { type Item = Sample; @@ -358,7 +346,7 @@ pub struct WhiteGaussian { sampler: NoiseSampler>, } -impl WhiteGaussian { +impl WhiteGaussian { /// Get the mean (average) value of the noise distribution. pub fn mean(&self) -> f32 { self.sampler.distribution.mean() @@ -377,7 +365,7 @@ impl WhiteGaussian { } } -impl WhiteGaussian { +impl WhiteGaussian { /// Create a new Gaussian white noise generator with a custom RNG. pub fn new_with_rng(sample_rate: SampleRate, rng: R) -> Self { // For Gaussian to achieve equivalent decorrelation to triangular dithering, it needs @@ -423,7 +411,7 @@ const PINK_NOISE_GENERATORS: usize = 16; /// This provides a good balance between realistic reverb characteristics and computational /// efficiency. Lower values create sparser, more distant reverb effects, while higher values /// create denser, closer reverb simulation. -const VELVET_DEFAULT_DENSITY: f32 = 2000.0; +const VELVET_DEFAULT_DENSITY: NonZero = nz!(2000); /// Variance of uniform distribution [-1.0, 1.0]. /// @@ -461,7 +449,7 @@ impl Pink { } } -impl Pink { +impl Pink { /// Create a new pink noise generator with a custom RNG. pub fn new_with_rng(sample_rate: SampleRate, rng: R) -> Self { let mut max_counts = [1u32; PINK_NOISE_GENERATORS]; @@ -543,7 +531,7 @@ impl Blue { } } -impl Blue { +impl Blue { /// Create a new blue noise generator with a custom RNG. pub fn new_with_rng(sample_rate: SampleRate, rng: R) -> Self { Self { @@ -606,7 +594,7 @@ impl Violet { } } -impl Violet { +impl Violet { /// Create a new violet noise generator with a custom RNG. pub fn new_with_rng(sample_rate: SampleRate, rng: R) -> Self { Self { @@ -719,7 +707,7 @@ impl Brownian { } } -impl Brownian { +impl Brownian { /// Create a new brownian noise generator with a custom RNG. pub fn new_with_rng(sample_rate: SampleRate, rng: R) -> Self { let white_noise = WhiteGaussian::new_with_rng(sample_rate, rng); @@ -796,7 +784,7 @@ impl Red { } } -impl Red { +impl Red { /// Create a new red noise generator with a custom RNG. pub fn new_with_rng(sample_rate: SampleRate, rng: R) -> Self { let white_noise = WhiteUniform::new_with_rng(sample_rate, rng); @@ -844,7 +832,6 @@ impl Source for Red { mod tests { use super::*; use rand::rngs::SmallRng; - use rand::SeedableRng; use rstest::rstest; use rstest_reuse::{self, *}; @@ -1181,16 +1168,17 @@ mod tests { } assert!( - impulse_count > (VELVET_DEFAULT_DENSITY * 0.75) as usize - && impulse_count < (VELVET_DEFAULT_DENSITY * 1.25) as usize, + impulse_count > (VELVET_DEFAULT_DENSITY.get() as f32 * 0.75) as usize + && impulse_count < (VELVET_DEFAULT_DENSITY.get() as f32 * 1.25) as usize, "Impulse count out of range: expected ~{VELVET_DEFAULT_DENSITY}, got {impulse_count}" ); } #[test] fn test_velvet_custom_density() { - let density = 1000.0; // impulses per second for testing - let mut generator = Velvet::::new_with_density(TEST_SAMPLE_RATE, density); + let density = nz!(1000); // impulses per second for testing + let mut generator = + Velvet::new_with_density(TEST_SAMPLE_RATE, density, SmallRng::from_os_rng()); let mut impulse_count = 0; for _ in 0..TEST_SAMPLE_RATE.get() { @@ -1200,10 +1188,9 @@ mod tests { } // Should be approximately the requested density - let actual_density = impulse_count as f32; assert!( - (actual_density - density).abs() < 200.0, - "Custom density not achieved: expected ~{density}, got {actual_density}" + density.get() - impulse_count < 200, + "Custom density not achieved: expected ~{density}, got {impulse_count}" ); } } From 829381303575b26e7c02ef11093e4cef1d56e1a8 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 27 Sep 2025 22:04:12 +0200 Subject: [PATCH 09/11] refactor: allow runtime dither algorithm selection - Replace trait-based DitherAlgorithm with runtime enum Algorithm - Implement Dither as a single struct supporting all algorithms - Add NoiseGenerator enum for dynamic noise generation - Expose new dither() function using Algorithm and BitDepth - Update docs and tests for new API --- src/source/dither.rs | 342 +++++++++++++++++++++++-------------------- src/source/mod.rs | 28 ++-- 2 files changed, 195 insertions(+), 175 deletions(-) diff --git a/src/source/dither.rs b/src/source/dither.rs index b958086a..409a3a0a 100644 --- a/src/source/dither.rs +++ b/src/source/dither.rs @@ -8,8 +8,7 @@ //! ## Example //! //! ```rust -//! use rodio::source::{dither, SineWave}; -//! use rodio::source::DitherAlgorithm; +//! use rodio::source::{dither, SineWave, DitherAlgorithm, Source}; //! use rodio::BitDepth; //! //! let source = SineWave::new(440.0); @@ -21,19 +20,155 @@ //! - **Apply dithering before volume changes** for optimal results //! - **Dither once** - Apply only at the final output stage to avoid noise accumulation //! - **Choose TPDF** for most professional audio applications (it's the default) -//! - **Use HighPass** for material with audible low-frequency dither artifacts //! - **Use target output bit depth** - Not the source bit depth! //! //! When you later change volume (e.g., with `Sink::set_volume()`), both the signal //! and dither noise scale together, maintaining proper dithering behavior. -use crate::{BitDepth, ChannelCount, Sample, SampleRate, Source}; +use rand::{rngs::SmallRng, Rng}; use std::time::Duration; -impl Iterator for Dither +use crate::{ + source::noise::{Blue, WhiteGaussian, WhiteTriangular, WhiteUniform}, + BitDepth, ChannelCount, Sample, SampleRate, Source, +}; + +/// Dither algorithm selection for runtime choice +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum Algorithm { + /// GPDF (Gaussian PDF) - normal/bell curve distribution. + /// + /// Uses Gaussian white noise which more closely mimics natural processes and + /// analog circuits. Higher noise floor than TPDF. + GPDF, + + /// High-pass dithering - reduces low-frequency artifacts. + /// + /// Uses blue noise (high-pass filtered white noise) to push dither energy + /// toward higher frequencies. Particularly effective for reducing audible + /// low-frequency modulation artifacts. + HighPass, + + /// RPDF (Rectangular PDF) - uniform distribution. + /// + /// Uses uniform white noise for basic decorrelation. Simpler than TPDF but + /// allows some correlation between signal and quantization error at low levels. + /// Slightly lower noise floor than TPDF. + RPDF, + + /// TPDF (Triangular PDF) - triangular distribution. + /// + /// The gold standard for audio dithering. Provides mathematically optimal + /// decorrelation by completely eliminating correlation between the original + /// signal and quantization error. + #[default] + TPDF, +} + +#[derive(Clone, Debug)] +#[allow(clippy::upper_case_acronyms)] +enum NoiseGenerator { + TPDF(WhiteTriangular), + RPDF(WhiteUniform), + GPDF(WhiteGaussian), + HighPass(Blue), +} + +impl NoiseGenerator { + fn new(algorithm: Algorithm, sample_rate: SampleRate) -> Self { + match algorithm { + Algorithm::TPDF => Self::TPDF(WhiteTriangular::new(sample_rate)), + Algorithm::RPDF => Self::RPDF(WhiteUniform::new(sample_rate)), + Algorithm::GPDF => Self::GPDF(WhiteGaussian::new(sample_rate)), + Algorithm::HighPass => Self::HighPass(Blue::new(sample_rate)), + } + } + + #[inline] + fn next(&mut self) -> Option { + match self { + Self::TPDF(gen) => gen.next(), + Self::RPDF(gen) => gen.next(), + Self::GPDF(gen) => gen.next(), + Self::HighPass(gen) => gen.next(), + } + } + + fn algorithm(&self) -> Algorithm { + match self { + Self::TPDF(_) => Algorithm::TPDF, + Self::RPDF(_) => Algorithm::RPDF, + Self::GPDF(_) => Algorithm::GPDF, + Self::HighPass(_) => Algorithm::HighPass, + } + } +} + +/// A dithered audio source that applies quantization noise to reduce artifacts. +/// +/// This struct wraps any audio source and applies dithering noise according to the +/// selected algorithm. Dithering is essential for digital audio playback and when +/// converting audio to different bit depths to prevent audible distortion. +/// +/// # Example +/// +/// ```rust +/// use rodio::source::{SineWave, dither, DitherAlgorithm}; +/// use rodio::BitDepth; +/// +/// let source = SineWave::new(440.0); +/// let dithered = dither(source, BitDepth::new(16).unwrap(), DitherAlgorithm::TPDF); +/// ``` +#[derive(Clone, Debug)] +pub struct Dither { + input: I, + noise: NoiseGenerator, + lsb_amplitude: f32, +} + +impl Dither +where + I: Source, +{ + /// Creates a new dithered source with the specified algorithm + pub fn new(input: I, target_bits: BitDepth, algorithm: Algorithm) -> Self { + // LSB amplitude for signed audio: 1.0 / (2^(bits-1)) + // For high bit depths (> mantissa precision), we're limited by the sample type's + // mantissa bits. Instead of dithering to a level that would be truncated, + // we dither at the actual LSB level representable by the sample format. + let lsb_amplitude = if target_bits.get() >= Sample::MANTISSA_DIGITS { + Sample::MIN_POSITIVE + } else { + 1.0 / (1_i64 << (target_bits.get() - 1)) as f32 + }; + + let sample_rate = input.sample_rate(); + Self { + input, + noise: NoiseGenerator::new(algorithm, sample_rate), + lsb_amplitude, + } + } + + /// Change the dithering algorithm at runtime + /// This recreates the noise generator with the new algorithm + pub fn set_algorithm(&mut self, algorithm: Algorithm) { + if self.noise.algorithm() != algorithm { + let sample_rate = self.input.sample_rate(); + self.noise = NoiseGenerator::new(algorithm, sample_rate); + } + } + + /// Get the current dithering algorithm + #[inline] + pub fn algorithm(&self) -> Algorithm { + self.noise.algorithm() + } +} + +impl Iterator for Dither where I: Source, - N: Iterator, { type Item = Sample; @@ -42,17 +177,14 @@ where let input_sample = self.input.next()?; let noise_sample = self.noise.next().unwrap_or(0.0); - // Add dither noise at the target quantization level - let dithered = input_sample + noise_sample * self.lsb_amplitude; - - Some(dithered) + // Apply subtractive dithering at the target quantization level + Some(input_sample - noise_sample * self.lsb_amplitude) } } -impl Source for Dither +impl Source for Dither where I: Source, - N: Iterator, { #[inline] fn current_span_len(&self) -> Option { @@ -80,160 +212,50 @@ where } } -#[derive(Clone, Debug)] -pub struct Dither { - input: I, - noise: N, - lsb_amplitude: f32, -} - -trait DitherAlgorithm { - type Noise; - fn build_noise(self, sample_rate: SampleRate) -> Self::Noise; -} - -macro_rules! dither_algos { - ($($(#[$outer:meta])* $name:ident, $noise:ident);+) => { - $( - $(#[$outer])* - struct $name; - - impl DitherAlgorithm for $name { - type Noise = crate::source::noise::$noise; - fn build_noise(self, sample_rate: SampleRate) -> Self::Noise { - crate::source::noise::$noise::new(sample_rate) - } - } - )+ - }; -} -dither_algos! { - /// GPDF (Gaussian PDF) - normal/bell curve distribution. - /// - /// Uses Gaussian white noise which more closely mimics natural processes and - /// analog circuits. Higher noise floor than TPDF. - GPDF, WhiteGaussian; - /// High-pass dithering - reduces low-frequency artifacts. - /// - /// Uses blue noise (high-pass filtered white noise) to push dither energy - /// toward higher frequencies. Particularly effective for reducing audible - /// low-frequency modulation artifacts. Best for material with significant - /// low-frequency content where traditional white dither might be audible. - HighPass, Blue; - /// RPDF (Rectangular PDF) - uniform distribution. - /// - /// Uses uniform white noise for basic decorrelation. Simpler than TPDF but - /// allows some correlation between signal and quantization error at low levels. - /// Slightly lower noise floor than TPDF. - RPDF, WhiteUniform; - /// TPDF (Triangular PDF) - triangular distribution. - /// - /// The gold standard for audio dithering. Provides mathematically optimal - /// decorrelation by completely eliminating correlation between the original - /// signal and quantization error. - TPDF, WhiteTriangular -} - -fn dither( - input: I, - algo: A, - target_bits: BitDepth, -) -> Dither::Noise> -where - I: Source, - A: DitherAlgorithm, -{ - // LSB amplitude for signed audio: 1.0 / (2^(bits-1)) - // This represents the amplitude of one quantization level - let lsb_amplitude = if target_bits.get() >= Sample::MANTISSA_DIGITS { - // For bit depths at or beyond the floating point precision limit, - // the LSB amplitude calculation becomes meaningless - Sample::MIN_POSITIVE - } else { - 1.0 / (1_i64 << (target_bits.get() - 1)) as f32 - }; - - Dither { - noise: algo.build_noise(input.sample_rate()), - input, - lsb_amplitude, - } -} - #[cfg(test)] mod tests { use super::*; use crate::source::{SineWave, Source}; use crate::{nz, BitDepth, SampleRate}; - fn show_api() { + const TEST_SAMPLE_RATE: SampleRate = nz!(44100); + const TEST_BIT_DEPTH: BitDepth = nz!(16); + + #[test] + fn test_dither_adds_noise() { let source = SineWave::new(440.0).take_duration(std::time::Duration::from_millis(10)); - let source = dither(source, GPDF, nz!(16)); + let mut dithered = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::TPDF); + let mut undithered = source; + + // Collect samples from both sources + let dithered_samples: Vec = (0..10).filter_map(|_| dithered.next()).collect(); + let undithered_samples: Vec = (0..10).filter_map(|_| undithered.next()).collect(); + + let lsb = 1.0 / (1_i64 << (TEST_BIT_DEPTH.get() - 1)) as f32; + + // Verify dithered samples differ from undithered and are reasonable + for (i, (&dithered_sample, &undithered_sample)) in dithered_samples + .iter() + .zip(undithered_samples.iter()) + .enumerate() + { + // Should be finite + assert!( + dithered_sample.is_finite(), + "Dithered sample {} should be finite", + i + ); + + // The difference should be small (just dither noise) + let diff = (dithered_sample - undithered_sample).abs(); + let max_expected_diff = lsb * 2.0; // Max triangular dither amplitude + assert!( + diff <= max_expected_diff, + "Dither noise too large: sample {}, diff {}, max expected {}", + i, + diff, + max_expected_diff + ); + } } - // const TEST_SAMPLE_RATE: SampleRate = nz!(44100); - // const TEST_BIT_DEPTH: BitDepth = nz!(16); - // - // #[test] - // fn test_dither_algorithms() { - // let source = SineWave::new(440.0).take_duration(std::time::Duration::from_millis(10)); - // - // // Test all four algorithms - // let mut gpdf = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::GPDF); - // let mut highpass = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::HighPass); - // let mut rpdf = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::RPDF); - // let mut tpdf = Dither::new(source, TEST_BIT_DEPTH, Algorithm::TPDF); - // - // for _ in 0..10 { - // let gpdf_sample = gpdf.next().unwrap(); - // let highpass_sample = highpass.next().unwrap(); - // let rpdf_sample = rpdf.next().unwrap(); - // let tpdf_sample = tpdf.next().unwrap(); - // - // // RPDF and TPDF should be bounded - // assert!((-1.0..=1.0).contains(&rpdf_sample)); - // assert!((-1.0..=1.0).contains(&tpdf_sample)); - // - // // Note: GPDF (Gaussian) and HighPass (Blue) may occasionally exceed [-1,1] bounds - // assert!(gpdf_sample.is_normal()); - // assert!(highpass_sample.is_normal()); - // } - // } - // - // #[test] - // fn test_dither_adds_noise() { - // let source = SineWave::new(440.0).take_duration(std::time::Duration::from_millis(10)); - // let mut dithered = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::TPDF); - // let mut undithered = source; - // - // // Collect samples from both sources - // let dithered_samples: Vec = (0..10).filter_map(|_| dithered.next()).collect(); - // let undithered_samples: Vec = (0..10).filter_map(|_| undithered.next()).collect(); - // - // let lsb = 1.0 / (1_i64 << (TEST_BIT_DEPTH.get() - 1)) as f32; - // - // // Verify dithered samples differ from undithered and are reasonable - // for (i, (&dithered_sample, &undithered_sample)) in dithered_samples - // .iter() - // .zip(undithered_samples.iter()) - // .enumerate() - // { - // // Should be finite - // assert!( - // dithered_sample.is_finite(), - // "Dithered sample {} should be finite", - // i - // ); - // - // // The difference should be small (just dither noise) - // let diff = (dithered_sample - undithered_sample).abs(); - // let max_expected_diff = lsb * 2.0; // Max triangular dither amplitude - // assert!( - // diff <= max_expected_diff, - // "Dither noise too large: sample {}, diff {}, max expected {}", - // i, - // diff, - // max_expected_diff - // ); - // } - // } } diff --git a/src/source/mod.rs b/src/source/mod.rs index 1fea576b..58d1613f 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -1,14 +1,12 @@ //! Sources of sound and various filters. use core::time::Duration; -// #[cfg(feature = "dither")] -// use std::num::NonZero; use std::sync::Arc; use crate::{ buffer::SamplesBuffer, common::{assert_error_traits, ChannelCount, SampleRate}, - math, Sample, + math, BitDepth, Sample, }; use dasp_sample::FromSample; @@ -94,18 +92,18 @@ mod dither; #[cfg_attr(docsrs, doc(cfg(feature = "dither")))] pub use self::dither::{Algorithm as DitherAlgorithm, Dither}; -// /// Creates a dithered source using the specified algorithm. -// /// -// /// Dithering eliminates quantization artifacts when converting from high-precision -// /// audio to lower bit depths. Apply at the target output bit depth. -// #[cfg(feature = "dither")] -// #[cfg_attr(docsrs, doc(cfg(feature = "dither")))] -// pub fn dither(input: I, target_bits: NonZero, algorithm: DitherAlgorithm) -> Dither -// where -// I: Source, -// { -// Dither::new(input, target_bits, algorithm) -// } +/// Creates a dithered source using the specified algorithm. +/// +/// Dithering eliminates quantization artifacts during digital audio playback +/// and when converting between bit depths. Apply at the target output bit depth. +#[cfg(feature = "dither")] +#[cfg_attr(docsrs, doc(cfg(feature = "dither")))] +pub fn dither(input: I, target_bits: BitDepth, algorithm: DitherAlgorithm) -> Dither +where + I: Source, +{ + Dither::new(input, target_bits, algorithm) +} #[cfg(feature = "noise")] #[cfg_attr(docsrs, doc(cfg(feature = "noise")))] From 9a9e262ed421d8ff858ed21f91f482b69c89b72d Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 28 Sep 2025 00:10:58 +0200 Subject: [PATCH 10/11] refactor: move dither to Source trait --- src/source/dither.rs | 12 ++++++------ src/source/mod.rs | 40 ++++++++++++++++++++++++++-------------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/source/dither.rs b/src/source/dither.rs index 409a3a0a..2f08395a 100644 --- a/src/source/dither.rs +++ b/src/source/dither.rs @@ -8,11 +8,11 @@ //! ## Example //! //! ```rust -//! use rodio::source::{dither, SineWave, DitherAlgorithm, Source}; -//! use rodio::BitDepth; +//! use rodio::source::{DitherAlgorithm, SineWave}; +//! use rodio::{BitDepth, Source}; //! //! let source = SineWave::new(440.0); -//! let dithered = dither(source, BitDepth::new(16).unwrap(), DitherAlgorithm::TPDF); +//! let dithered = source.dither(BitDepth::new(16).unwrap(), DitherAlgorithm::TPDF); //! ``` //! //! ## Guidelines @@ -113,11 +113,11 @@ impl NoiseGenerator { /// # Example /// /// ```rust -/// use rodio::source::{SineWave, dither, DitherAlgorithm}; -/// use rodio::BitDepth; +/// use rodio::source::{DitherAlgorithm, SineWave}; +/// use rodio::{BitDepth, Source}; /// /// let source = SineWave::new(440.0); -/// let dithered = dither(source, BitDepth::new(16).unwrap(), DitherAlgorithm::TPDF); +/// let dithered = source.dither(BitDepth::new(16).unwrap(), DitherAlgorithm::TPDF); /// ``` #[derive(Clone, Debug)] pub struct Dither { diff --git a/src/source/mod.rs b/src/source/mod.rs index 58d1613f..ec6c3313 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -87,24 +87,11 @@ mod zero; #[cfg(feature = "dither")] #[cfg_attr(docsrs, doc(cfg(feature = "dither")))] -mod dither; +pub mod dither; #[cfg(feature = "dither")] #[cfg_attr(docsrs, doc(cfg(feature = "dither")))] pub use self::dither::{Algorithm as DitherAlgorithm, Dither}; -/// Creates a dithered source using the specified algorithm. -/// -/// Dithering eliminates quantization artifacts during digital audio playback -/// and when converting between bit depths. Apply at the target output bit depth. -#[cfg(feature = "dither")] -#[cfg_attr(docsrs, doc(cfg(feature = "dither")))] -pub fn dither(input: I, target_bits: BitDepth, algorithm: DitherAlgorithm) -> Dither -where - I: Source, -{ - Dither::new(input, target_bits, algorithm) -} - #[cfg(feature = "noise")] #[cfg_attr(docsrs, doc(cfg(feature = "noise")))] pub mod noise; @@ -210,6 +197,31 @@ pub trait Source: Iterator { buffered::buffered(self) } + /// Applies dithering to the source at the specified bit depth. + /// + /// Dithering eliminates quantization artifacts during digital audio playback + /// and when converting between bit depths. Apply at the target output bit depth. + /// + /// # Example + /// + /// ``` + /// use rodio::source::{SineWave, Source, DitherAlgorithm}; + /// use rodio::BitDepth; + /// + /// let source = SineWave::new(440.0) + /// .amplify(0.5) + /// .dither(BitDepth::new(16).unwrap(), DitherAlgorithm::default()); + /// ``` + #[cfg(feature = "dither")] + #[cfg_attr(docsrs, doc(cfg(feature = "dither")))] + #[inline] + fn dither(self, target_bits: BitDepth, algorithm: DitherAlgorithm) -> Dither + where + Self: Sized, + { + Dither::new(self, target_bits, algorithm) + } + /// Mixes this source with another one. #[inline] fn mix(self, other: S) -> Mix From 3d72cdfe17eba69c9ce1170be0451d3c9f605372 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 28 Sep 2025 00:12:26 +0200 Subject: [PATCH 11/11] docs: clarify dither function as Source::dither --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 851d0ded..0c9df654 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added audio dithering support with `dither` feature (enabled by default): - Four dithering algorithms: `TPDF`, `RPDF`, `GPDF`, and `HighPass` - `DitherAlgorithm` enum for algorithm selection - - `dither()` function for applying quantization noise shaping + - `Source::dither()` function for applying dithering ### Fixed - docs.rs will now document all features, including those that are optional.