diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d859f26..0c9df654 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 + - `Source::dither()` function for applying dithering ### Fixed - docs.rs will now document all features, including those that are optional. @@ -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) 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/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 new file mode 100644 index 00000000..2f08395a --- /dev/null +++ b/src/source/dither.rs @@ -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 { + 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::{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 { + 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, +{ + 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); + + // Apply subtractive dithering at the target quantization level + Some(input_sample - noise_sample * self.lsb_amplitude) + } +} + +impl Source for Dither +where + I: Source, +{ + #[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 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 = (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/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/mod.rs b/src/source/mod.rs index 52dcc2f0..ec6c3313 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -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; @@ -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; @@ -190,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 diff --git a/src/source/noise.rs b/src/source/noise.rs index b9987a8d..8d4b7738 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(); //! @@ -40,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}, @@ -138,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 = @@ -187,14 +188,14 @@ 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()) } } -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"); @@ -243,65 +244,57 @@ 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 { +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()) } } -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; - let impulse_pos = rng.random::() * grid_size; - - Self { - sample_rate, - rng, - grid_size, - grid_pos: 0.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; - let impulse_pos = rng.random::() * grid_size; + 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 { + 0 + }; Self { sample_rate, rng, grid_size, - grid_pos: 0.0, + grid_pos: 0, impulse_pos, } } } +impl Velvet {} + impl Iterator for Velvet { type Item = Sample; #[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 @@ -312,12 +305,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) @@ -349,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() @@ -361,14 +358,14 @@ 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()) } } -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 @@ -414,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]. /// @@ -445,14 +442,14 @@ 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()) } } -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]; @@ -527,14 +524,14 @@ 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()) } } -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 { @@ -590,14 +587,14 @@ 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()) } } -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 { @@ -703,14 +700,14 @@ 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()) } } -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); @@ -780,14 +777,14 @@ 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()) } } -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); @@ -835,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, *}; @@ -1014,8 +1010,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(); @@ -1053,7 +1049,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() ); @@ -1172,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() { @@ -1191,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}" ); } }