Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- `Source::dither()` function for applying dithering

### Fixed
- docs.rs will now document all features, including those that are optional.
Expand All @@ -32,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)

Expand Down
15 changes: 13 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"]

Expand Down
3 changes: 3 additions & 0 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ pub type SampleRate = NonZero<u32>;
/// Number of channels in a stream. Can never be Zero
pub type ChannelCount = NonZero<u16>;

/// Number of bits per sample. Can never be zero.
pub type BitDepth = NonZero<u32>;

/// 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.
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
261 changes: 261 additions & 0 deletions src/source/dither.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
//! 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::{DitherAlgorithm, SineWave};
//! use rodio::{BitDepth, Source};
//!
//! let source = SineWave::new(440.0);
//! let dithered = source.dither(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 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 rand::{rngs::SmallRng, Rng};
use std::time::Duration;

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<R: Rng = SmallRng> {
TPDF(WhiteTriangular<R>),
RPDF(WhiteUniform<R>),
GPDF(WhiteGaussian<R>),
HighPass(Blue<R>),
}

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<Sample> {
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::{DitherAlgorithm, SineWave};
/// use rodio::{BitDepth, Source};
///
/// let source = SineWave::new(440.0);
/// let dithered = source.dither(BitDepth::new(16).unwrap(), DitherAlgorithm::TPDF);
/// ```
#[derive(Clone, Debug)]
pub struct Dither<I> {
input: I,
noise: NoiseGenerator,
lsb_amplitude: f32,
}

impl<I> Dither<I>
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<I> Iterator for Dither<I>
where
I: Source,
{
type Item = Sample;

#[inline]
fn next(&mut self) -> Option<Self::Item> {
let input_sample = self.input.next()?;
let noise_sample = self.noise.next().unwrap_or(0.0);

// Apply subtractive dithering at the target quantization level
Some(input_sample - noise_sample * self.lsb_amplitude)
}
}

impl<I> Source for Dither<I>
where
I: Source,
{
#[inline]
fn current_span_len(&self) -> Option<usize> {
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<Duration> {
self.input.total_duration()
}

#[inline]
fn try_seek(&mut self, pos: Duration) -> Result<(), crate::source::SeekError> {
self.input.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_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<f32> = (0..10).filter_map(|_| dithered.next()).collect();
let undithered_samples: Vec<f32> = (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
);
}
}
}
2 changes: 1 addition & 1 deletion src/source/limit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -817,7 +817,7 @@ pub struct LimitMulti<I> {
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
Expand Down
34 changes: 33 additions & 1 deletion src/source/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::sync::Arc;
use crate::{
buffer::SamplesBuffer,
common::{assert_error_traits, ChannelCount, SampleRate},
math, Sample,
math, BitDepth, Sample,
};

use dasp_sample::FromSample;
Expand Down Expand Up @@ -85,6 +85,13 @@ mod triangle;
mod uniform;
mod zero;

#[cfg(feature = "dither")]
#[cfg_attr(docsrs, doc(cfg(feature = "dither")))]
pub mod dither;
#[cfg(feature = "dither")]
#[cfg_attr(docsrs, doc(cfg(feature = "dither")))]
pub use self::dither::{Algorithm as DitherAlgorithm, Dither};

#[cfg(feature = "noise")]
#[cfg_attr(docsrs, doc(cfg(feature = "noise")))]
pub mod noise;
Expand Down Expand Up @@ -190,6 +197,31 @@ pub trait Source: Iterator<Item = Sample> {
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<Self>
where
Self: Sized,
{
Dither::new(self, target_bits, algorithm)
}

/// Mixes this source with another one.
#[inline]
fn mix<S>(self, other: S) -> Mix<Self, S>
Expand Down
Loading
Loading