|
| 1 | +//! Dithering for audio quantization and requantization. |
| 2 | +//! |
| 3 | +//! Dithering is a technique in digital audio processing that eliminates quantization |
| 4 | +//! artifacts during various stages of audio processing. This module provides tools for |
| 5 | +//! adding appropriate dither noise to maintain audio quality during quantization |
| 6 | +//! operations. |
| 7 | +//! |
| 8 | +//! ## Example |
| 9 | +//! |
| 10 | +//! ```rust |
| 11 | +//! use rodio::source::{DitherAlgorithm, SineWave}; |
| 12 | +//! use rodio::{BitDepth, Source}; |
| 13 | +//! |
| 14 | +//! let source = SineWave::new(440.0); |
| 15 | +//! let dithered = source.dither(BitDepth::new(16).unwrap(), DitherAlgorithm::TPDF); |
| 16 | +//! ``` |
| 17 | +//! |
| 18 | +//! ## Guidelines |
| 19 | +//! |
| 20 | +//! - **Apply dithering before volume changes** for optimal results |
| 21 | +//! - **Dither once** - Apply only at the final output stage to avoid noise accumulation |
| 22 | +//! - **Choose TPDF** for most professional audio applications (it's the default) |
| 23 | +//! - **Use target output bit depth** - Not the source bit depth! |
| 24 | +//! |
| 25 | +//! When you later change volume (e.g., with `Sink::set_volume()`), both the signal |
| 26 | +//! and dither noise scale together, maintaining proper dithering behavior. |
| 27 | +
|
| 28 | +use rand::{rngs::SmallRng, Rng}; |
| 29 | +use std::time::Duration; |
| 30 | + |
| 31 | +use crate::{ |
| 32 | + source::noise::{Blue, WhiteGaussian, WhiteTriangular, WhiteUniform}, |
| 33 | + BitDepth, ChannelCount, Sample, SampleRate, Source, |
| 34 | +}; |
| 35 | + |
| 36 | +/// Dither algorithm selection for runtime choice |
| 37 | +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] |
| 38 | +pub enum Algorithm { |
| 39 | + /// GPDF (Gaussian PDF) - normal/bell curve distribution. |
| 40 | + /// |
| 41 | + /// Uses Gaussian white noise which more closely mimics natural processes and |
| 42 | + /// analog circuits. Higher noise floor than TPDF. |
| 43 | + GPDF, |
| 44 | + |
| 45 | + /// High-pass dithering - reduces low-frequency artifacts. |
| 46 | + /// |
| 47 | + /// Uses blue noise (high-pass filtered white noise) to push dither energy |
| 48 | + /// toward higher frequencies. Particularly effective for reducing audible |
| 49 | + /// low-frequency modulation artifacts. |
| 50 | + HighPass, |
| 51 | + |
| 52 | + /// RPDF (Rectangular PDF) - uniform distribution. |
| 53 | + /// |
| 54 | + /// Uses uniform white noise for basic decorrelation. Simpler than TPDF but |
| 55 | + /// allows some correlation between signal and quantization error at low levels. |
| 56 | + /// Slightly lower noise floor than TPDF. |
| 57 | + RPDF, |
| 58 | + |
| 59 | + /// TPDF (Triangular PDF) - triangular distribution. |
| 60 | + /// |
| 61 | + /// The gold standard for audio dithering. Provides mathematically optimal |
| 62 | + /// decorrelation by completely eliminating correlation between the original |
| 63 | + /// signal and quantization error. |
| 64 | + #[default] |
| 65 | + TPDF, |
| 66 | +} |
| 67 | + |
| 68 | +#[derive(Clone, Debug)] |
| 69 | +#[allow(clippy::upper_case_acronyms)] |
| 70 | +enum NoiseGenerator<R: Rng = SmallRng> { |
| 71 | + TPDF(WhiteTriangular<R>), |
| 72 | + RPDF(WhiteUniform<R>), |
| 73 | + GPDF(WhiteGaussian<R>), |
| 74 | + HighPass(Blue<R>), |
| 75 | +} |
| 76 | + |
| 77 | +impl NoiseGenerator { |
| 78 | + fn new(algorithm: Algorithm, sample_rate: SampleRate) -> Self { |
| 79 | + match algorithm { |
| 80 | + Algorithm::TPDF => Self::TPDF(WhiteTriangular::new(sample_rate)), |
| 81 | + Algorithm::RPDF => Self::RPDF(WhiteUniform::new(sample_rate)), |
| 82 | + Algorithm::GPDF => Self::GPDF(WhiteGaussian::new(sample_rate)), |
| 83 | + Algorithm::HighPass => Self::HighPass(Blue::new(sample_rate)), |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + #[inline] |
| 88 | + fn next(&mut self) -> Option<Sample> { |
| 89 | + match self { |
| 90 | + Self::TPDF(gen) => gen.next(), |
| 91 | + Self::RPDF(gen) => gen.next(), |
| 92 | + Self::GPDF(gen) => gen.next(), |
| 93 | + Self::HighPass(gen) => gen.next(), |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + fn algorithm(&self) -> Algorithm { |
| 98 | + match self { |
| 99 | + Self::TPDF(_) => Algorithm::TPDF, |
| 100 | + Self::RPDF(_) => Algorithm::RPDF, |
| 101 | + Self::GPDF(_) => Algorithm::GPDF, |
| 102 | + Self::HighPass(_) => Algorithm::HighPass, |
| 103 | + } |
| 104 | + } |
| 105 | +} |
| 106 | + |
| 107 | +/// A dithered audio source that applies quantization noise to reduce artifacts. |
| 108 | +/// |
| 109 | +/// This struct wraps any audio source and applies dithering noise according to the |
| 110 | +/// selected algorithm. Dithering is essential for digital audio playback and when |
| 111 | +/// converting audio to different bit depths to prevent audible distortion. |
| 112 | +/// |
| 113 | +/// # Example |
| 114 | +/// |
| 115 | +/// ```rust |
| 116 | +/// use rodio::source::{DitherAlgorithm, SineWave}; |
| 117 | +/// use rodio::{BitDepth, Source}; |
| 118 | +/// |
| 119 | +/// let source = SineWave::new(440.0); |
| 120 | +/// let dithered = source.dither(BitDepth::new(16).unwrap(), DitherAlgorithm::TPDF); |
| 121 | +/// ``` |
| 122 | +#[derive(Clone, Debug)] |
| 123 | +pub struct Dither<I> { |
| 124 | + input: I, |
| 125 | + noise: NoiseGenerator, |
| 126 | + lsb_amplitude: f32, |
| 127 | +} |
| 128 | + |
| 129 | +impl<I> Dither<I> |
| 130 | +where |
| 131 | + I: Source, |
| 132 | +{ |
| 133 | + /// Creates a new dithered source with the specified algorithm |
| 134 | + pub fn new(input: I, target_bits: BitDepth, algorithm: Algorithm) -> Self { |
| 135 | + // LSB amplitude for signed audio: 1.0 / (2^(bits-1)) |
| 136 | + // For high bit depths (> mantissa precision), we're limited by the sample type's |
| 137 | + // mantissa bits. Instead of dithering to a level that would be truncated, |
| 138 | + // we dither at the actual LSB level representable by the sample format. |
| 139 | + let lsb_amplitude = if target_bits.get() >= Sample::MANTISSA_DIGITS { |
| 140 | + Sample::MIN_POSITIVE |
| 141 | + } else { |
| 142 | + 1.0 / (1_i64 << (target_bits.get() - 1)) as f32 |
| 143 | + }; |
| 144 | + |
| 145 | + let sample_rate = input.sample_rate(); |
| 146 | + Self { |
| 147 | + input, |
| 148 | + noise: NoiseGenerator::new(algorithm, sample_rate), |
| 149 | + lsb_amplitude, |
| 150 | + } |
| 151 | + } |
| 152 | + |
| 153 | + /// Change the dithering algorithm at runtime |
| 154 | + /// This recreates the noise generator with the new algorithm |
| 155 | + pub fn set_algorithm(&mut self, algorithm: Algorithm) { |
| 156 | + if self.noise.algorithm() != algorithm { |
| 157 | + let sample_rate = self.input.sample_rate(); |
| 158 | + self.noise = NoiseGenerator::new(algorithm, sample_rate); |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + /// Get the current dithering algorithm |
| 163 | + #[inline] |
| 164 | + pub fn algorithm(&self) -> Algorithm { |
| 165 | + self.noise.algorithm() |
| 166 | + } |
| 167 | +} |
| 168 | + |
| 169 | +impl<I> Iterator for Dither<I> |
| 170 | +where |
| 171 | + I: Source, |
| 172 | +{ |
| 173 | + type Item = Sample; |
| 174 | + |
| 175 | + #[inline] |
| 176 | + fn next(&mut self) -> Option<Self::Item> { |
| 177 | + let input_sample = self.input.next()?; |
| 178 | + let noise_sample = self.noise.next().unwrap_or(0.0); |
| 179 | + |
| 180 | + // Apply subtractive dithering at the target quantization level |
| 181 | + Some(input_sample - noise_sample * self.lsb_amplitude) |
| 182 | + } |
| 183 | +} |
| 184 | + |
| 185 | +impl<I> Source for Dither<I> |
| 186 | +where |
| 187 | + I: Source, |
| 188 | +{ |
| 189 | + #[inline] |
| 190 | + fn current_span_len(&self) -> Option<usize> { |
| 191 | + self.input.current_span_len() |
| 192 | + } |
| 193 | + |
| 194 | + #[inline] |
| 195 | + fn channels(&self) -> ChannelCount { |
| 196 | + self.input.channels() |
| 197 | + } |
| 198 | + |
| 199 | + #[inline] |
| 200 | + fn sample_rate(&self) -> SampleRate { |
| 201 | + self.input.sample_rate() |
| 202 | + } |
| 203 | + |
| 204 | + #[inline] |
| 205 | + fn total_duration(&self) -> Option<Duration> { |
| 206 | + self.input.total_duration() |
| 207 | + } |
| 208 | + |
| 209 | + #[inline] |
| 210 | + fn try_seek(&mut self, pos: Duration) -> Result<(), crate::source::SeekError> { |
| 211 | + self.input.try_seek(pos) |
| 212 | + } |
| 213 | +} |
| 214 | + |
| 215 | +#[cfg(test)] |
| 216 | +mod tests { |
| 217 | + use super::*; |
| 218 | + use crate::source::{SineWave, Source}; |
| 219 | + use crate::{nz, BitDepth, SampleRate}; |
| 220 | + |
| 221 | + const TEST_SAMPLE_RATE: SampleRate = nz!(44100); |
| 222 | + const TEST_BIT_DEPTH: BitDepth = nz!(16); |
| 223 | + |
| 224 | + #[test] |
| 225 | + fn test_dither_adds_noise() { |
| 226 | + let source = SineWave::new(440.0).take_duration(std::time::Duration::from_millis(10)); |
| 227 | + let mut dithered = Dither::new(source.clone(), TEST_BIT_DEPTH, Algorithm::TPDF); |
| 228 | + let mut undithered = source; |
| 229 | + |
| 230 | + // Collect samples from both sources |
| 231 | + let dithered_samples: Vec<f32> = (0..10).filter_map(|_| dithered.next()).collect(); |
| 232 | + let undithered_samples: Vec<f32> = (0..10).filter_map(|_| undithered.next()).collect(); |
| 233 | + |
| 234 | + let lsb = 1.0 / (1_i64 << (TEST_BIT_DEPTH.get() - 1)) as f32; |
| 235 | + |
| 236 | + // Verify dithered samples differ from undithered and are reasonable |
| 237 | + for (i, (&dithered_sample, &undithered_sample)) in dithered_samples |
| 238 | + .iter() |
| 239 | + .zip(undithered_samples.iter()) |
| 240 | + .enumerate() |
| 241 | + { |
| 242 | + // Should be finite |
| 243 | + assert!( |
| 244 | + dithered_sample.is_finite(), |
| 245 | + "Dithered sample {} should be finite", |
| 246 | + i |
| 247 | + ); |
| 248 | + |
| 249 | + // The difference should be small (just dither noise) |
| 250 | + let diff = (dithered_sample - undithered_sample).abs(); |
| 251 | + let max_expected_diff = lsb * 2.0; // Max triangular dither amplitude |
| 252 | + assert!( |
| 253 | + diff <= max_expected_diff, |
| 254 | + "Dither noise too large: sample {}, diff {}, max expected {}", |
| 255 | + i, |
| 256 | + diff, |
| 257 | + max_expected_diff |
| 258 | + ); |
| 259 | + } |
| 260 | + } |
| 261 | +} |
0 commit comments