From c87ef6c9ea7815097b2f4a950f33c86bff4b1a6c Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 10 Dec 2025 15:15:34 +0100 Subject: [PATCH 1/3] feat: add optional 64-bit sample precision Replace hardcoded f32 types with Sample/Float type aliases throughout the codebase for consistency. Add compile-time configurability for sample precision via a 64bit feature flag. Tise 64bit mode addresses precision drift in long-running signal generators and accumulated operations, following an approach similar to CamillaDSP's compile-time precision selection. --- .github/workflows/ci.yml | 7 ++ CHANGELOG.md | 1 + Cargo.toml | 2 + benches/conversions.rs | 8 +- benches/effects.rs | 6 +- benches/pipeline.rs | 3 +- examples/automatic_gain_control.rs | 4 +- examples/limit_settings.rs | 29 ++++--- examples/mix_multiple_sources.rs | 20 +++-- examples/noise_generator.rs | 10 ++- src/buffer.rs | 17 ++-- src/common.rs | 12 ++- src/conversions/sample_rate.rs | 2 +- src/lib.rs | 10 ++- src/math.rs | 117 ++++++++++++------------- src/microphone.rs | 6 +- src/sink.rs | 7 +- src/source/agc.rs | 117 +++++++++++++------------ src/source/amplify.rs | 10 +-- src/source/blt.rs | 60 ++++++------- src/source/channel_volume.rs | 10 +-- src/source/chirp.rs | 24 +++--- src/source/crossfade.rs | 16 ++-- src/source/delay.rs | 5 +- src/source/distortion.rs | 12 +-- src/source/dither.rs | 32 +++---- src/source/fadein.rs | 2 +- src/source/fadeout.rs | 2 +- src/source/limit.rs | 51 ++++++----- src/source/linear_ramp.rs | 50 ++++++----- src/source/mod.rs | 42 ++++----- src/source/noise.rs | 74 ++++++++-------- src/source/sawtooth.rs | 6 +- src/source/signal_generator.rs | 132 +++++++++++++++-------------- src/source/sine.rs | 6 +- src/source/skip.rs | 13 +-- src/source/spatial.rs | 6 +- src/source/square.rs | 6 +- src/source/take.rs | 9 +- src/source/triangle.rs | 6 +- src/spatial_sink.rs | 6 +- src/static_buffer.rs | 7 +- src/wav_output.rs | 6 +- tests/channel_volume.rs | 4 +- tests/limit.rs | 29 ++++--- tests/seek.rs | 22 ++--- 46 files changed, 545 insertions(+), 481 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db9e4e8b..fcd38fe7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,3 +55,10 @@ jobs: - run: cargo check --tests --lib --no-default-features # Check alternative decoders. - run: cargo check --tests --lib --no-default-features --features claxon,hound,minimp3,lewton + # Test 64-bit sample mode + - run: cargo test --all-targets --features 64bit + - run: cargo test --doc --features 64bit + - run: cargo test --all-targets --all-features --features 64bit + # Check examples compile in both modes + - run: cargo check --examples + - run: cargo check --examples --features 64bit diff --git a/CHANGELOG.md b/CHANGELOG.md index 16166b16..6cb03c86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Four dithering algorithms: `TPDF`, `RPDF`, `GPDF`, and `HighPass` - `DitherAlgorithm` enum for algorithm selection - `Source::dither()` function for applying dithering +- Added `64bit` feature to opt-in to 64-bit sample precision (`f64`). ### Fixed - docs.rs will now document all features, including those that are optional. diff --git a/Cargo.toml b/Cargo.toml index be347fc8..625ef0a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,8 @@ wav_output = ["dep:hound"] tracing = ["dep:tracing"] # Experimental features using atomic floating-point operations experimental = ["dep:atomic_float"] +# Perform all calculations with 64-bit floats (instead of 32) +64bit = [] # Audio generation features # diff --git a/benches/conversions.rs b/benches/conversions.rs index 59ab17a3..1ef4e82d 100644 --- a/benches/conversions.rs +++ b/benches/conversions.rs @@ -1,6 +1,6 @@ -use dasp_sample::{Duplex, Sample}; +use dasp_sample::{Duplex, Sample as DaspSample}; use divan::Bencher; -use rodio::conversions::SampleTypeConverter; +use rodio::{conversions::SampleTypeConverter, Sample}; mod shared; @@ -9,7 +9,7 @@ fn main() { } #[divan::bench(types = [i16, u16, f32])] -fn from_sample>(bencher: Bencher) { +fn from_sample>(bencher: Bencher) { bencher .with_inputs(|| { shared::music_wav() @@ -18,6 +18,6 @@ fn from_sample>(bencher: Bencher) { .into_iter() }) .bench_values(|source| { - SampleTypeConverter::<_, rodio::Sample>::new(source).for_each(divan::black_box_drop) + SampleTypeConverter::<_, Sample>::new(source).for_each(divan::black_box_drop) }) } diff --git a/benches/effects.rs b/benches/effects.rs index 10afb7fd..0e5a6fac 100644 --- a/benches/effects.rs +++ b/benches/effects.rs @@ -1,7 +1,6 @@ use std::time::Duration; use divan::Bencher; -use rodio::source::AutomaticGainControlSettings; use rodio::Source; mod shared; @@ -48,7 +47,7 @@ fn amplify(bencher: Bencher) { fn agc_enabled(bencher: Bencher) { bencher.with_inputs(music_wav).bench_values(|source| { source - .automatic_gain_control(AutomaticGainControlSettings::default()) + .automatic_gain_control(Default::default()) .for_each(divan::black_box_drop) }) } @@ -58,8 +57,7 @@ fn agc_enabled(bencher: Bencher) { fn agc_disabled(bencher: Bencher) { bencher.with_inputs(music_wav).bench_values(|source| { // Create the AGC source - let amplified_source = - source.automatic_gain_control(AutomaticGainControlSettings::default()); + let amplified_source = source.automatic_gain_control(Default::default); // Get the control handle and disable AGC let agc_control = amplified_source.get_agc_control(); diff --git a/benches/pipeline.rs b/benches/pipeline.rs index bf789f90..c7d67cfa 100644 --- a/benches/pipeline.rs +++ b/benches/pipeline.rs @@ -2,7 +2,6 @@ use std::num::NonZero; use std::time::Duration; use divan::Bencher; -use rodio::source::AutomaticGainControlSettings; use rodio::ChannelCount; use rodio::{source::UniformSourceIterator, Source}; @@ -20,7 +19,7 @@ fn long(bencher: Bencher) { .high_pass(300) .amplify(1.2) .speed(0.9) - .automatic_gain_control(AutomaticGainControlSettings::default()) + .automatic_gain_control(Default::default()) .delay(Duration::from_secs_f32(0.5)) .fade_in(Duration::from_secs_f32(2.0)) .take_duration(Duration::from_secs(10)); diff --git a/examples/automatic_gain_control.rs b/examples/automatic_gain_control.rs index 8887fcfc..625f982c 100644 --- a/examples/automatic_gain_control.rs +++ b/examples/automatic_gain_control.rs @@ -1,4 +1,4 @@ -use rodio::source::{AutomaticGainControlSettings, Source}; +use rodio::source::Source; use rodio::Decoder; use std::error::Error; use std::fs::File; @@ -16,7 +16,7 @@ fn main() -> Result<(), Box> { let source = Decoder::try_from(file)?; // Apply automatic gain control to the source - let agc_source = source.automatic_gain_control(AutomaticGainControlSettings::default()); + let agc_source = source.automatic_gain_control(Default::default()); // Make it so that the source checks if automatic gain control should be // enabled or disabled every 5 milliseconds. We must clone `agc_enabled`, diff --git a/examples/limit_settings.rs b/examples/limit_settings.rs index 684c3e64..4047a6e3 100644 --- a/examples/limit_settings.rs +++ b/examples/limit_settings.rs @@ -4,6 +4,7 @@ //! to configure audio limiting parameters. use rodio::source::{LimitSettings, SineWave, Source}; +use rodio::Sample; use std::time::Duration; fn main() { @@ -39,11 +40,11 @@ fn main() { let limited_wave = sine_wave.limit(LimitSettings::default()); // Collect some samples to demonstrate - let samples: Vec = limited_wave.take(100).collect(); + let samples: Vec = limited_wave.take(100).collect(); println!(" Generated {} limited samples", samples.len()); // Show peak reduction - let max_sample = samples.iter().fold(0.0f32, |acc, &x| acc.max(x.abs())); + let max_sample: Sample = samples.iter().fold(0.0, |acc, &x| acc.max(x.abs())); println!(" Peak amplitude after limiting: {max_sample:.3}"); println!(); @@ -56,7 +57,7 @@ fn main() { // Apply the custom settings from Example 2 let custom_limited = sine_wave2.limit(custom_limiting); - let custom_samples: Vec = custom_limited.take(50).collect(); + let custom_samples: Vec = custom_limited.take(50).collect(); println!( " Generated {} samples with custom settings", custom_samples.len() @@ -101,7 +102,7 @@ fn main() { println!("Example 6: Limiting with -6dB threshold"); // Create a sine wave that will definitely trigger limiting - const AMPLITUDE: f32 = 2.5; // High amplitude to ensure limiting occurs + const AMPLITUDE: Sample = 2.5; // High amplitude to ensure limiting occurs let test_sine = SineWave::new(440.0) .amplify(AMPLITUDE) .take_duration(Duration::from_millis(100)); // 100ms = ~4410 samples @@ -114,24 +115,24 @@ fn main() { .with_release(Duration::from_millis(12)); // Moderate release let limited_sine = test_sine.limit(strict_limiting.clone()); - let test_samples: Vec = limited_sine.take(4410).collect(); + let test_samples: Vec = limited_sine.take(4410).collect(); // Analyze peaks at different time periods - let early_peak = test_samples[0..500] + let early_peak: Sample = test_samples[0..500] .iter() - .fold(0.0f32, |acc, &x| acc.max(x.abs())); - let mid_peak = test_samples[1000..1500] + .fold(0.0, |acc, &x| acc.max(x.abs())); + let mid_peak: Sample = test_samples[1000..1500] .iter() - .fold(0.0f32, |acc, &x| acc.max(x.abs())); - let settled_peak = test_samples[2000..] + .fold(0.0, |acc, &x| acc.max(x.abs())); + let settled_peak: Sample = test_samples[2000..] .iter() - .fold(0.0f32, |acc, &x| acc.max(x.abs())); + .fold(0.0, |acc, &x| acc.max(x.abs())); // With -6dB threshold, ALL samples are well below 1.0! - let target_linear = 10.0_f32.powf(strict_limiting.threshold / 20.0); - let max_settled = test_samples[2000..] + let target_linear = (10.0 as Sample).powf(strict_limiting.threshold / 20.0); + let max_settled: Sample = test_samples[2000..] .iter() - .fold(0.0f32, |acc, &x| acc.max(x.abs())); + .fold(0.0, |acc, &x| acc.max(x.abs())); println!( " {}dB threshold limiting results:", diff --git a/examples/mix_multiple_sources.rs b/examples/mix_multiple_sources.rs index 8c6be920..4d4360d6 100644 --- a/examples/mix_multiple_sources.rs +++ b/examples/mix_multiple_sources.rs @@ -1,9 +1,13 @@ use rodio::mixer; use rodio::source::{SineWave, Source}; +use rodio::Float; use std::error::Error; use std::num::NonZero; use std::time::Duration; +const NOTE_DURATION: Duration = Duration::from_secs(1); +const NOTE_AMPLITUDE: Float = 0.20; + fn main() -> Result<(), Box> { // Construct a dynamic controller and mixer, stream_handle, and sink. let (controller, mixer) = mixer::mixer(NonZero::new(2).unwrap(), NonZero::new(44_100).unwrap()); @@ -14,17 +18,17 @@ fn main() -> Result<(), Box> { // notes in the key of C and in octave 4: C4, or middle C on a piano, // E4, G4, and A4 respectively. let source_c = SineWave::new(261.63) - .take_duration(Duration::from_secs_f32(1.)) - .amplify(0.20); + .take_duration(NOTE_DURATION) + .amplify(NOTE_AMPLITUDE); let source_e = SineWave::new(329.63) - .take_duration(Duration::from_secs_f32(1.)) - .amplify(0.20); + .take_duration(NOTE_DURATION) + .amplify(NOTE_AMPLITUDE); let source_g = SineWave::new(392.0) - .take_duration(Duration::from_secs_f32(1.)) - .amplify(0.20); + .take_duration(NOTE_DURATION) + .amplify(NOTE_AMPLITUDE); let source_a = SineWave::new(440.0) - .take_duration(Duration::from_secs_f32(1.)) - .amplify(0.20); + .take_duration(NOTE_DURATION) + .amplify(NOTE_AMPLITUDE); // Add sources C, E, G, and A to the mixer controller. controller.add(source_c); diff --git a/examples/noise_generator.rs b/examples/noise_generator.rs index 3adc0f0e..a3c3d444 100644 --- a/examples/noise_generator.rs +++ b/examples/noise_generator.rs @@ -3,9 +3,11 @@ use std::{error::Error, thread::sleep, time::Duration}; -use rodio::source::{ - noise::{Blue, Brownian, Pink, Velvet, Violet, WhiteGaussian, WhiteTriangular, WhiteUniform}, - Source, +use rodio::{ + source::noise::{ + Blue, Brownian, Pink, Velvet, Violet, WhiteGaussian, WhiteTriangular, WhiteUniform, + }, + Sample, Source, }; fn main() -> Result<(), Box> { @@ -74,7 +76,7 @@ fn main() -> Result<(), Box> { /// Helper function to play a noise type with description fn play_noise(stream_handle: &rodio::OutputStream, source: S, name: &str, description: &str) where - S: Source + Send + 'static, + S: Source + Send + 'static, { println!("{} Noise", name); println!(" Application: {}", description); diff --git a/src/buffer.rs b/src/buffer.rs index 923a8336..3142934b 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -12,8 +12,9 @@ //! use crate::common::{ChannelCount, SampleRate}; +use crate::math::{duration_to_float, NANOS_PER_SEC}; use crate::source::{SeekError, UniformSourceIterator}; -use crate::{Sample, Source}; +use crate::{Float, Sample, Source}; use std::sync::Arc; use std::time::Duration; @@ -40,13 +41,13 @@ impl SamplesBuffer { where D: Into>, { - let data: Arc<[f32]> = data.into().into(); - let duration_ns = 1_000_000_000u64.checked_mul(data.len() as u64).unwrap() + let data: Arc<[Sample]> = data.into().into(); + let duration_ns = NANOS_PER_SEC.checked_mul(data.len() as u64).unwrap() / sample_rate.get() as u64 / channels.get() as u64; let duration = Duration::new( - duration_ns / 1_000_000_000, - (duration_ns % 1_000_000_000) as u32, + duration_ns / NANOS_PER_SEC, + (duration_ns % NANOS_PER_SEC) as u32, ); Self { @@ -104,7 +105,7 @@ impl Source for SamplesBuffer { let curr_channel = self.pos % self.channels().get() as usize; let new_pos = - pos.as_secs_f32() * self.sample_rate().get() as f32 * self.channels().get() as f32; + duration_to_float(pos) * self.sample_rate().get() as Float * self.channels().get() as Float; // saturate pos at the end of the source let new_pos = new_pos as usize; let new_pos = new_pos.min(self.data.len()); @@ -171,7 +172,7 @@ mod tests { #[cfg(test)] mod try_seek { use super::*; - use crate::common::{ChannelCount, SampleRate}; + use crate::common::{ChannelCount, Float, SampleRate}; use crate::Sample; use std::time::Duration; @@ -187,7 +188,7 @@ mod tests { buf.try_seek(Duration::from_secs(5)).unwrap(); assert_eq!( buf.next(), - Some(5.0 * SAMPLE_RATE.get() as f32 * CHANNELS.get() as f32) + Some(5.0 * SAMPLE_RATE.get() as Float * CHANNELS.get() as Float) ); assert!(buf.next().is_some_and(|s| s.trunc() as i32 % 2 == 1)); diff --git a/src/common.rs b/src/common.rs index 5799c1e5..0c76e67e 100644 --- a/src/common.rs +++ b/src/common.rs @@ -10,12 +10,22 @@ pub type ChannelCount = NonZero; /// Number of bits per sample. Can never be zero. pub type BitDepth = NonZero; +/// Floating point type used for internal calculations. Can be configured to be +/// either `f32` (default) or `f64` using the `64bit` feature flag. +#[cfg(not(feature = "64bit"))] +pub type Float = f32; + +/// Floating point type used for internal calculations. Can be configured to be +/// either `f32` (default) or `f64` using the `64bit` feature flag. +#[cfg(feature = "64bit")] +pub type Float = f64; + /// 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. /// Use conversion traits from [dasp_sample] crate or [crate::conversions::SampleTypeConverter] /// to convert between sample types if necessary. -pub type Sample = f32; +pub type Sample = Float; /// Used to test at compile time that a struct/enum implements Send, Sync and /// is 'static. These are common requirements for dynamic error management diff --git a/src/conversions/sample_rate.rs b/src/conversions/sample_rate.rs index 556566f1..0e58eeb4 100644 --- a/src/conversions/sample_rate.rs +++ b/src/conversions/sample_rate.rs @@ -167,7 +167,7 @@ where .zip(self.next_frame.iter()) .enumerate() { - let sample = math::lerp(cur, next, numerator, self.to); + let sample = math::lerp(*cur, *next, numerator, self.to); if off == 0 { result = Some(sample); diff --git a/src/lib.rs b/src/lib.rs index b4fcfaa0..1b71e929 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -142,6 +142,14 @@ //! //! The "playback" feature adds support for playing audio. This feature requires the "cpal" crate. //! +//! ### Feature "64bit" +//! +//! The "64bit" feature enables 64-bit sample precision using `f64` for audio samples and most +//! internal calculations. By default, rodio uses 32-bit floats (`f32`), which offers better +//! performance and is sufficient for most use cases. The 64-bit mode addresses precision drift +//! when chaining many audio operations together and in long-running signal generators where +//! phase errors compound over time. +//! //! ## How it works under the hood //! //! Rodio spawns a background thread that is dedicated to reading from the sources and sending @@ -189,7 +197,7 @@ pub mod queue; pub mod source; pub mod static_buffer; -pub use crate::common::{BitDepth, ChannelCount, Sample, SampleRate}; +pub use crate::common::{BitDepth, ChannelCount, Float, Sample, SampleRate}; pub use crate::decoder::Decoder; pub use crate::sink::Sink; pub use crate::source::Source; diff --git a/src/math.rs b/src/math.rs index e5e4b7ba..1860f106 100644 --- a/src/math.rs +++ b/src/math.rs @@ -3,6 +3,16 @@ use crate::common::SampleRate; use std::time::Duration; +/// Nanoseconds per second, used for Duration calculations. +pub(crate) const NANOS_PER_SEC: u64 = 1_000_000_000; + +// Re-export float constants with appropriate precision for the Float type. +// This centralizes all cfg gating for constants in one place. +#[cfg(not(feature = "64bit"))] +pub use std::f32::consts::{E, LN_2, LN_10, LOG2_10, LOG2_E, LOG10_2, LOG10_E, PI, TAU}; +#[cfg(feature = "64bit")] +pub use std::f64::consts::{E, LN_2, LN_10, LOG2_10, LOG2_E, LOG10_2, LOG10_E, PI, TAU}; + /// Linear interpolation between two samples. /// /// The result should be equivalent to @@ -11,8 +21,8 @@ use std::time::Duration; /// To avoid numeric overflows pick smaller numerator. // TODO (refactoring) Streamline this using coefficient instead of numerator and denominator. #[inline] -pub(crate) fn lerp(first: &f32, second: &f32, numerator: u32, denominator: u32) -> f32 { - first + (second - first) * numerator as f32 / denominator as f32 +pub(crate) fn lerp(first: Sample, second: Sample, numerator: u32, denominator: u32) -> Sample { + first + (second - first) * numerator as Float / denominator as Float } /// Converts decibels to linear amplitude scale. @@ -39,10 +49,10 @@ pub(crate) fn lerp(first: &f32, second: &f32, numerator: u32, denominator: u32) /// `10f32.powf(decibels * 0.05)` approach, with a maximum error of only 2.48e-7 /// (representing about -132 dB precision). #[inline] -pub fn db_to_linear(decibels: f32) -> f32 { +pub fn db_to_linear(decibels: Float) -> Float { // ~3-4% faster than using `10f32.powf(decibels * 0.05)`, // with a maximum error of 2.48e-7 representing only about -132 dB. - 2.0f32.powf(decibels * 0.05 * std::f32::consts::LOG2_10) + Float::powf(2.0, decibels * 0.05 * LOG2_10) } /// Converts linear amplitude scale to decibels. @@ -74,9 +84,9 @@ pub fn db_to_linear(decibels: f32) -> f32 { /// - Very small positive values approach negative infinity /// - Negative values return NaN (not physically meaningful for amplitude) #[inline] -pub fn linear_to_db(linear: f32) -> f32 { +pub fn linear_to_db(linear: Float) -> Float { // Same as `to_linear`: faster than using `20f32.log10() * linear` - linear.log2() * std::f32::consts::LOG10_2 * 20.0 + linear.log2() * LOG10_2 * 20.0 } /// Converts a time duration to a smoothing coefficient for exponential filtering. @@ -97,8 +107,23 @@ pub fn linear_to_db(linear: f32) -> f32 { /// # Returns /// /// Smoothing coefficient in the range [0.0, 1.0] for use in exponential filters -pub(crate) fn duration_to_coefficient(duration: Duration, sample_rate: SampleRate) -> f32 { - f32::exp(-1.0 / (duration.as_secs_f32() * sample_rate.get() as f32)) +#[must_use] +pub(crate) fn duration_to_coefficient(duration: Duration, sample_rate: SampleRate) -> Float { + Float::exp(-1.0 / (duration_to_float(duration) * sample_rate.get() as Float)) +} + +/// Convert Duration to Float with appropriate precision for the Sample type. +#[inline] +#[must_use] +pub(crate) fn duration_to_float(duration: Duration) -> Float { + #[cfg(not(feature = "64bit"))] + { + duration.as_secs_f32() + } + #[cfg(feature = "64bit")] + { + duration.as_secs_f64() + } } /// Utility macro for getting a `NonZero` from a literal. Especially @@ -123,6 +148,8 @@ macro_rules! nz { pub use nz; +use crate::{common::Float, Sample}; + #[cfg(test)] mod test { use super::*; @@ -130,11 +157,19 @@ mod test { use quickcheck::{quickcheck, TestResult}; quickcheck! { - fn lerp_f32_random(first: u16, second: u16, numerator: u16, denominator: u16) -> TestResult { + fn lerp_random(first: Sample, second: Sample, numerator: u32, denominator: u32) -> TestResult { if denominator == 0 { return TestResult::discard(); } + // Constrain to realistic audio sample range [-1.0, 1.0] + // Audio samples rarely exceed this range, and large values cause floating-point error accumulation + if first.abs() > 1.0 || second.abs() > 1.0 { return TestResult::discard(); } + + // Discard infinite or NaN samples (can occur in quickcheck) + if !first.is_finite() || !second.is_finite() { return TestResult::discard(); } + let (numerator, denominator) = Ratio::new(numerator, denominator).into_raw(); - if numerator > 5000 { return TestResult::discard(); } + // Reduce max numerator to avoid floating-point error accumulation with large ratios + if numerator > 1000 { return TestResult::discard(); } let a = first as f64; let b = second as f64; @@ -142,9 +177,13 @@ mod test { if !(0.0..=1.0).contains(&c) { return TestResult::discard(); }; let reference = a * (1.0 - c) + b * c; - let x = lerp(&(first as f32), &(second as f32), numerator as u32, denominator as u32) as f64; - // TODO (review) It seems that the diff tolerance should be a lot lower. Why lerp so imprecise? - TestResult::from_bool((x - reference).abs() < 0.01) + let x = lerp(first, second, numerator, denominator); + + // With realistic audio-range inputs, lerp should be very precise + // f32 has ~7 decimal digits, so 1e-6 tolerance is reasonable + // This is well below 16-bit audio precision (~1.5e-5) + let tolerance = 1e-6; + TestResult::from_bool((x as f64 - reference).abs() < tolerance) } } @@ -168,7 +207,7 @@ mod test { /// Based on [Wikipedia's Decibel article]. /// /// [Wikipedia's Decibel article]: https://web.archive.org/web/20230810185300/https://en.wikipedia.org/wiki/Decibel - const DECIBELS_LINEAR_TABLE: [(f32, f32); 27] = [ + const DECIBELS_LINEAR_TABLE: [(Float, Float); 27] = [ (100., 100000.), (90., 31623.), (80., 10000.), @@ -203,18 +242,6 @@ mod test { for (db, wikipedia_linear) in DECIBELS_LINEAR_TABLE { let actual_linear = db_to_linear(db); - // Calculate the mathematically exact reference value using f64 precision - let exact_linear = f64::powf(10.0, db as f64 * 0.05) as f32; - - // Test implementation precision against exact mathematical result - let relative_error = ((actual_linear - exact_linear) / exact_linear).abs(); - const MAX_RELATIVE_ERROR: f32 = 5.0 * f32::EPSILON; // max error: 2.3x ε (at -100dB), with 2x safety margin - - assert!( - relative_error < MAX_RELATIVE_ERROR, - "Implementation precision failed for {db}dB: exact {exact_linear:.8}, got {actual_linear:.8}, relative error: {relative_error:.2e}" - ); - // Sanity check: ensure we're in the right order of magnitude as Wikipedia data // This is lenient to account for rounding in the reference values let magnitude_ratio = actual_linear / wikipedia_linear; @@ -228,45 +255,21 @@ mod test { #[test] fn convert_linear_to_decibels() { // Test the inverse conversion function using the same reference data - for (expected_db, linear) in DECIBELS_LINEAR_TABLE { + for (wikipedia_db, linear) in DECIBELS_LINEAR_TABLE { let actual_db = linear_to_db(linear); - // Calculate the mathematically exact reference value using f64 precision - let exact_db = ((linear as f64).log10() * 20.0) as f32; - - // Test implementation precision against exact mathematical result - if exact_db.abs() > 10.0 * f32::EPSILON { - // Use relative error for non-zero dB values - let relative_error = ((actual_db - exact_db) / exact_db.abs()).abs(); - const MAX_RELATIVE_ERROR: f32 = 5.0 * f32::EPSILON; // max error: 1.0x ε, with 5x safety margin - - assert!( - relative_error < MAX_RELATIVE_ERROR, - "Linear to dB conversion precision failed for {linear}: exact {exact_db:.8}, got {actual_db:.8}, relative error: {relative_error:.2e}" - ); - } else { - // Use absolute error for values very close to 0 dB (linear ≈ 1.0) - let absolute_error = (actual_db - exact_db).abs(); - const MAX_ABSOLUTE_ERROR: f32 = 1.0 * f32::EPSILON; // 0 dB case is mathematically exact, minimal tolerance for numerical stability - - assert!( - absolute_error < MAX_ABSOLUTE_ERROR, - "Linear to dB conversion precision failed for {linear}: exact {exact_db:.8}, got {actual_db:.8}, absolute error: {absolute_error:.2e}" - ); - } - // Sanity check: ensure we're reasonably close to the expected dB value from the table // This accounts for rounding in both the linear and dB reference values - let magnitude_ratio = if expected_db.abs() > 10.0 * f32::EPSILON { - actual_db / expected_db + let magnitude_ratio = if wikipedia_db.abs() > 10.0 * Float::EPSILON { + actual_db / wikipedia_db } else { 1.0 // Skip ratio check for values very close to 0 dB }; - if expected_db.abs() > 10.0 * f32::EPSILON { + if wikipedia_db.abs() > 10.0 * Float::EPSILON { assert!( magnitude_ratio > 0.99 && magnitude_ratio < 1.01, - "Result differs significantly from table reference for linear {linear}: expected {expected_db}dB, got {actual_db}dB, ratio: {magnitude_ratio:.4}" + "Result differs significantly from table reference for linear {linear}: expected {wikipedia_db}dB, got {actual_db}dB, ratio: {magnitude_ratio:.4}" ); } } @@ -282,7 +285,7 @@ mod test { let round_trip_db = linear_to_db(linear); let error = (round_trip_db - original_db).abs(); - const MAX_ROUND_TRIP_ERROR: f32 = 16.0 * f32::EPSILON; // max error: 8x ε (practical audio range), with 2x safety margin + const MAX_ROUND_TRIP_ERROR: Float = 16.0 * Float::EPSILON; // max error: 8x ε (practical audio range), with 2x safety margin assert!( error < MAX_ROUND_TRIP_ERROR, @@ -298,7 +301,7 @@ mod test { let round_trip_linear = db_to_linear(db); let relative_error = ((round_trip_linear - original_linear) / original_linear).abs(); - const MAX_ROUND_TRIP_RELATIVE_ERROR: f32 = 16.0 * f32::EPSILON; // Same as above, for linear->dB->linear round trips + const MAX_ROUND_TRIP_RELATIVE_ERROR: Float = 16.0 * Float::EPSILON; // Same as above, for linear->dB->linear round trips assert!( relative_error < MAX_ROUND_TRIP_RELATIVE_ERROR, diff --git a/src/microphone.rs b/src/microphone.rs index 8e0594ea..e5641cfe 100644 --- a/src/microphone.rs +++ b/src/microphone.rs @@ -203,7 +203,7 @@ impl Source for Microphone { } impl Iterator for Microphone { - type Item = f32; + type Item = Sample; fn next(&mut self) -> Option { loop { @@ -246,7 +246,7 @@ impl Microphone { let timeout = Some(Duration::from_millis(100)); let hundred_ms_of_samples = config.channel_count.get() as u32 * config.sample_rate.get() / 10; - let (mut tx, rx) = RingBuffer::new(hundred_ms_of_samples as usize); + let (mut tx, rx) = RingBuffer::::new(hundred_ms_of_samples as usize); let error_occurred = Arc::new(AtomicBool::new(false)); let error_callback = { let error_occurred = error_occurred.clone(); @@ -263,7 +263,7 @@ impl Microphone { cpal::SampleFormat::$sample_format => device.build_input_stream::<$generic, _, _>( &config.stream_config(), move |data, _info| { - for sample in SampleTypeConverter::<_, f32>::new(data.into_iter().copied()) { + for sample in SampleTypeConverter::<_, Sample>::new(data.into_iter().copied()) { let _skip_if_player_is_behind = tx.push(sample); } }, diff --git a/src/sink.rs b/src/sink.rs index 446c21bd..8c1f61d2 100644 --- a/src/sink.rs +++ b/src/sink.rs @@ -10,6 +10,7 @@ use std::sync::mpsc::{Receiver, Sender}; use crate::mixer::Mixer; use crate::source::SeekError; +use crate::Float; use crate::{queue, source::Done, Source}; /// Handle to a device that outputs sounds. @@ -58,7 +59,7 @@ impl SeekOrder { struct Controls { pause: AtomicBool, - volume: Mutex, + volume: Mutex, stopped: AtomicBool, speed: Mutex, to_clear: Mutex, @@ -165,7 +166,7 @@ impl Sink { /// The value `1.0` is the "normal" volume (unfiltered input). Any value other than 1.0 will /// multiply each sample by this value. #[inline] - pub fn volume(&self) -> f32 { + pub fn volume(&self) -> Float { *self.controls.volume.lock().unwrap() } @@ -174,7 +175,7 @@ impl Sink { /// The value `1.0` is the "normal" volume (unfiltered input). Any value other than `1.0` will /// multiply each sample by this value. #[inline] - pub fn set_volume(&self, value: f32) { + pub fn set_volume(&self, value: Float) { *self.controls.volume.lock().unwrap() = value; } diff --git a/src/source/agc.rs b/src/source/agc.rs index ad25bc46..4903385d 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -15,15 +15,22 @@ use super::SeekError; use crate::math::duration_to_coefficient; -use crate::Source; -#[cfg(feature = "experimental")] +use crate::{Float, Sample, Source}; +#[cfg(all(feature = "experimental", not(feature = "64bit")))] use atomic_float::AtomicF32; +#[cfg(all(feature = "experimental", feature = "64bit"))] +use atomic_float::AtomicF64; #[cfg(feature = "experimental")] use std::sync::atomic::{AtomicBool, Ordering}; #[cfg(feature = "experimental")] use std::sync::Arc; use std::time::Duration; +#[cfg(all(feature = "experimental", not(feature = "64bit")))] +type AtomicFloat = AtomicF32; +#[cfg(all(feature = "experimental", feature = "64bit"))] +type AtomicFloat = AtomicF64; + use crate::common::{ChannelCount, SampleRate}; #[cfg(feature = "tracing")] use tracing; @@ -49,7 +56,7 @@ const RMS_WINDOW_SIZE: usize = power_of_two(8192); pub struct AutomaticGainControlSettings { /// The desired output level that the AGC tries to maintain. /// A value of 1.0 means no change to the original level. - pub target_level: f32, + pub target_level: Float, /// Time constant for gain increases (how quickly the AGC responds to level increases). /// Longer durations result in slower, more gradual gain increases. pub attack_time: Duration, @@ -58,16 +65,16 @@ pub struct AutomaticGainControlSettings { pub release_time: Duration, /// Maximum allowable gain multiplication to prevent excessive amplification. /// This acts as a safety limit to avoid distortion from over-amplification. - pub absolute_max_gain: f32, + pub absolute_max_gain: Float, } impl Default for AutomaticGainControlSettings { fn default() -> Self { AutomaticGainControlSettings { - target_level: 1.0, // Default to original level - attack_time: Duration::from_secs_f32(4.0), // Recommended attack time - release_time: Duration::from_secs_f32(0f32), // Recommended release time - absolute_max_gain: 7.0, // Recommended max gain + target_level: 1.0, // Default to original level + attack_time: Duration::from_secs(4), // Recommended attack time + release_time: Duration::from_secs(0), // Recommended release time + absolute_max_gain: 7.0, // Recommended max gain } } } @@ -80,13 +87,13 @@ impl Default for AutomaticGainControlSettings { #[derive(Clone, Debug)] pub struct AutomaticGainControl { input: I, - target_level: Arc, - floor: f32, - absolute_max_gain: Arc, - current_gain: f32, - attack_coeff: Arc, - release_coeff: Arc, - peak_level: f32, + target_level: Arc, + floor: Float, + absolute_max_gain: Arc, + current_gain: Float, + attack_coeff: Arc, + release_coeff: Arc, + peak_level: Float, rms_window: CircularBuffer, is_enabled: Arc, } @@ -99,13 +106,13 @@ pub struct AutomaticGainControl { #[derive(Clone, Debug)] pub struct AutomaticGainControl { input: I, - target_level: f32, - floor: f32, - absolute_max_gain: f32, - current_gain: f32, - attack_coeff: f32, - release_coeff: f32, - peak_level: f32, + target_level: Float, + floor: Float, + absolute_max_gain: Float, + current_gain: Float, + attack_coeff: Float, + release_coeff: Float, + peak_level: Float, rms_window: CircularBuffer, is_enabled: bool, } @@ -116,8 +123,8 @@ pub struct AutomaticGainControl { /// which is crucial for real-time audio processing. #[derive(Clone, Debug)] struct CircularBuffer { - buffer: Box<[f32; RMS_WINDOW_SIZE]>, - sum: f32, + buffer: Box<[Float; RMS_WINDOW_SIZE]>, + sum: Float, index: usize, } @@ -126,8 +133,8 @@ impl CircularBuffer { #[inline] fn new() -> Self { CircularBuffer { - buffer: Box::new([0f32; RMS_WINDOW_SIZE]), - sum: 0f32, + buffer: Box::new([0.0; RMS_WINDOW_SIZE]), + sum: 0.0, index: 0, } } @@ -136,7 +143,7 @@ impl CircularBuffer { /// /// This method maintains a running sum for efficient mean calculation. #[inline] - fn push(&mut self, value: f32) -> f32 { + fn push(&mut self, value: Float) -> Float { let old_value = self.buffer[self.index]; // Update the sum by first subtracting the old value and then adding the new value; this is more accurate. self.sum = self.sum - old_value + value; @@ -150,8 +157,8 @@ impl CircularBuffer { /// /// This operation is `O(1)` due to the maintained running sum. #[inline] - fn mean(&self) -> f32 { - self.sum / RMS_WINDOW_SIZE as f32 + fn mean(&self) -> Float { + self.sum / RMS_WINDOW_SIZE as Float } } @@ -167,10 +174,10 @@ impl CircularBuffer { #[inline] pub(crate) fn automatic_gain_control( input: I, - target_level: f32, + target_level: Float, attack_time: Duration, release_time: Duration, - absolute_max_gain: f32, + absolute_max_gain: Float, ) -> AutomaticGainControl where I: Source, @@ -183,13 +190,13 @@ where { AutomaticGainControl { input, - target_level: Arc::new(AtomicF32::new(target_level)), - floor: 0f32, - absolute_max_gain: Arc::new(AtomicF32::new(absolute_max_gain)), + target_level: Arc::new(AtomicFloat::new(target_level)), + floor: 0.0, + absolute_max_gain: Arc::new(AtomicFloat::new(absolute_max_gain)), current_gain: 1.0, - attack_coeff: Arc::new(AtomicF32::new(attack_coeff)), - release_coeff: Arc::new(AtomicF32::new(release_coeff)), - peak_level: 0f32, + attack_coeff: Arc::new(AtomicFloat::new(attack_coeff)), + release_coeff: Arc::new(AtomicFloat::new(release_coeff)), + peak_level: 0.0, rms_window: CircularBuffer::new(), is_enabled: Arc::new(AtomicBool::new(true)), } @@ -200,12 +207,12 @@ where AutomaticGainControl { input, target_level, - floor: 0f32, + floor: 0.0, absolute_max_gain, current_gain: 1.0, attack_coeff, release_coeff, - peak_level: 0f32, + peak_level: 0.0, rms_window: CircularBuffer::new(), is_enabled: true, } @@ -217,7 +224,7 @@ where I: Source, { #[inline] - fn target_level(&self) -> f32 { + fn target_level(&self) -> Float { #[cfg(feature = "experimental")] { self.target_level.load(Ordering::Relaxed) @@ -229,7 +236,7 @@ where } #[inline] - fn absolute_max_gain(&self) -> f32 { + fn absolute_max_gain(&self) -> Float { #[cfg(feature = "experimental")] { self.absolute_max_gain.load(Ordering::Relaxed) @@ -241,7 +248,7 @@ where } #[inline] - fn attack_coeff(&self) -> f32 { + fn attack_coeff(&self) -> Float { #[cfg(feature = "experimental")] { self.attack_coeff.load(Ordering::Relaxed) @@ -253,7 +260,7 @@ where } #[inline] - fn release_coeff(&self) -> f32 { + fn release_coeff(&self) -> Float { #[cfg(feature = "experimental")] { self.release_coeff.load(Ordering::Relaxed) @@ -283,7 +290,7 @@ where /// Use this to dynamically modify the AGC's target level while audio is processing. /// Adjust this value to control the overall output amplitude of the processed signal. #[inline] - pub fn get_target_level(&self) -> Arc { + pub fn get_target_level(&self) -> Arc { Arc::clone(&self.target_level) } @@ -294,7 +301,7 @@ where /// Use this to dynamically modify the AGC's maximum allowable gain during runtime. /// Adjusting this value helps prevent excessive amplification in low-level signals. #[inline] - pub fn get_absolute_max_gain(&self) -> Arc { + pub fn get_absolute_max_gain(&self) -> Arc { Arc::clone(&self.absolute_max_gain) } @@ -306,7 +313,7 @@ where /// Smaller values result in faster response, larger values in slower response. /// Adjust during runtime to fine-tune AGC behavior for different audio content. #[inline] - pub fn get_attack_coeff(&self) -> Arc { + pub fn get_attack_coeff(&self) -> Arc { Arc::clone(&self.attack_coeff) } @@ -318,7 +325,7 @@ where /// Smaller values result in faster response, larger values in slower response. /// Adjust during runtime to optimize AGC behavior for varying audio dynamics. #[inline] - pub fn get_release_coeff(&self) -> Arc { + pub fn get_release_coeff(&self) -> Arc { Arc::clone(&self.release_coeff) } @@ -360,8 +367,8 @@ where /// Passing `None` will disable the floor value (setting it to 0.0), allowing the /// AGC gain to drop to very low levels. #[inline] - pub fn set_floor(&mut self, floor: Option) { - self.floor = floor.unwrap_or(0f32); + pub fn set_floor(&mut self, floor: Option) { + self.floor = floor.unwrap_or(0.0); } /// Updates the peak level using instant attack and slow release behaviour @@ -370,10 +377,10 @@ where /// and the release coefficient when the signal is decreasing, providing /// appropriate tracking behaviour for peak detection. #[inline] - fn update_peak_level(&mut self, sample_value: f32, release_coeff: f32) { + fn update_peak_level(&mut self, sample_value: Sample, release_coeff: Float) { let coeff = if sample_value > self.peak_level { // Fast attack for rising peaks - 0f32 + 0.0 } else { // Slow release for falling peaks release_coeff @@ -386,7 +393,7 @@ where /// This method calculates a moving average of the squared input samples, /// providing a measure of the signal's average power over time. #[inline] - fn update_rms(&mut self, sample_value: f32) -> f32 { + fn update_rms(&mut self, sample_value: Sample) -> Float { let squared_sample = sample_value * sample_value; self.rms_window.push(squared_sample); self.rms_window.mean().sqrt() @@ -397,8 +404,8 @@ where /// signal, considering the peak level. /// The peak level helps prevent sudden spikes in the output signal. #[inline] - fn calculate_peak_gain(&self, target_level: f32, absolute_max_gain: f32) -> f32 { - if self.peak_level > 0f32 { + fn calculate_peak_gain(&self, target_level: Float, absolute_max_gain: Float) -> Float { + if self.peak_level > 0.0 { (target_level / self.peak_level).min(absolute_max_gain) } else { absolute_max_gain @@ -423,7 +430,7 @@ where let rms = self.update_rms(sample_value); // Compute the gain adjustment required to reach the target level based on RMS - let rms_gain = if rms > 0f32 { + let rms_gain = if rms > 0.0 { target_level / rms } else { absolute_max_gain // Default to max gain if RMS is zero diff --git a/src/source/amplify.rs b/src/source/amplify.rs index 985c6be3..c9dcec48 100644 --- a/src/source/amplify.rs +++ b/src/source/amplify.rs @@ -2,12 +2,12 @@ use std::time::Duration; use super::SeekError; use crate::{ - common::{ChannelCount, SampleRate}, + common::{ChannelCount, Float, SampleRate}, math, Source, }; /// Internal function that builds a `Amplify` object. -pub fn amplify(input: I, factor: f32) -> Amplify +pub fn amplify(input: I, factor: Float) -> Amplify where I: Source, { @@ -18,19 +18,19 @@ where #[derive(Clone, Debug)] pub struct Amplify { input: I, - factor: f32, + factor: Float, } impl Amplify { /// Modifies the amplification factor. #[inline] - pub fn set_factor(&mut self, factor: f32) { + pub fn set_factor(&mut self, factor: Float) { self.factor = factor; } /// Modifies the amplification factor logarithmically. #[inline] - pub fn set_log_factor(&mut self, factor: f32) { + pub fn set_log_factor(&mut self, factor: Float) { self.factor = math::db_to_linear(factor); } diff --git a/src/source/blt.rs b/src/source/blt.rs index 151b0bee..5d06779b 100644 --- a/src/source/blt.rs +++ b/src/source/blt.rs @@ -1,6 +1,6 @@ -use crate::common::{ChannelCount, SampleRate}; -use crate::Source; -use std::f32::consts::PI; +use crate::common::{ChannelCount, Float, SampleRate}; +use crate::math::PI; +use crate::{Sample, Source}; use std::time::Duration; use super::SeekError; @@ -10,22 +10,22 @@ use super::SeekError; /// Internal function that builds a `BltFilter` object. pub fn low_pass(input: I, freq: u32) -> BltFilter where - I: Source, + I: Source, { low_pass_with_q(input, freq, 0.5) } pub fn high_pass(input: I, freq: u32) -> BltFilter where - I: Source, + I: Source, { high_pass_with_q(input, freq, 0.5) } /// Same as low_pass but allows the q value (bandwidth) to be changed -pub fn low_pass_with_q(input: I, freq: u32, q: f32) -> BltFilter +pub fn low_pass_with_q(input: I, freq: u32, q: Float) -> BltFilter where - I: Source, + I: Source, { BltFilter { input, @@ -39,9 +39,9 @@ where } /// Same as high_pass but allows the q value (bandwidth) to be changed -pub fn high_pass_with_q(input: I, freq: u32, q: f32) -> BltFilter +pub fn high_pass_with_q(input: I, freq: u32, q: Float) -> BltFilter where - I: Source, + I: Source, { BltFilter { input, @@ -60,10 +60,10 @@ pub struct BltFilter { input: I, formula: BltFormula, applier: Option, - x_n1: f32, - x_n2: f32, - y_n1: f32, - y_n2: f32, + x_n1: Float, + x_n2: Float, + y_n1: Float, + y_n2: Float, } impl BltFilter { @@ -78,13 +78,13 @@ impl BltFilter { } /// Same as to_low_pass but allows the q value (bandwidth) to be changed - pub fn to_low_pass_with_q(&mut self, freq: u32, q: f32) { + pub fn to_low_pass_with_q(&mut self, freq: u32, q: Float) { self.formula = BltFormula::LowPass { freq, q }; self.applier = None; } /// Same as to_high_pass but allows the q value (bandwidth) to be changed - pub fn to_high_pass_with_q(&mut self, freq: u32, q: f32) { + pub fn to_high_pass_with_q(&mut self, freq: u32, q: Float) { self.formula = BltFormula::HighPass { freq, q }; self.applier = None; } @@ -110,12 +110,12 @@ impl BltFilter { impl Iterator for BltFilter where - I: Source, + I: Source, { - type Item = f32; + type Item = Sample; #[inline] - fn next(&mut self) -> Option { + fn next(&mut self) -> Option { let last_in_span = self.input.current_span_len() == Some(1); if self.applier.is_none() { @@ -147,11 +147,11 @@ where } } -impl ExactSizeIterator for BltFilter where I: Source + ExactSizeIterator {} +impl ExactSizeIterator for BltFilter where I: Source + ExactSizeIterator {} impl Source for BltFilter where - I: Source, + I: Source, { #[inline] fn current_span_len(&self) -> Option { @@ -181,15 +181,15 @@ where #[derive(Clone, Debug)] enum BltFormula { - LowPass { freq: u32, q: f32 }, - HighPass { freq: u32, q: f32 }, + LowPass { freq: u32, q: Float }, + HighPass { freq: u32, q: Float }, } impl BltFormula { fn to_applier(&self, sampling_frequency: u32) -> BltApplier { match *self { BltFormula::LowPass { freq, q } => { - let w0 = 2.0 * PI * freq as f32 / sampling_frequency as f32; + let w0 = 2.0 * PI * freq as Float / sampling_frequency as Float; let alpha = w0.sin() / (2.0 * q); let b1 = 1.0 - w0.cos(); @@ -208,7 +208,7 @@ impl BltFormula { } } BltFormula::HighPass { freq, q } => { - let w0 = 2.0 * PI * freq as f32 / sampling_frequency as f32; + let w0 = 2.0 * PI * freq as Float / sampling_frequency as Float; let cos_w0 = w0.cos(); let alpha = w0.sin() / (2.0 * q); @@ -233,16 +233,16 @@ impl BltFormula { #[derive(Clone, Debug)] struct BltApplier { - b0: f32, - b1: f32, - b2: f32, - a1: f32, - a2: f32, + b0: Float, + b1: Float, + b2: Float, + a1: Float, + a2: Float, } impl BltApplier { #[inline] - fn apply(&self, x_n: f32, x_n1: f32, x_n2: f32, y_n1: f32, y_n2: f32) -> f32 { + fn apply(&self, x_n: Float, x_n1: Float, x_n2: Float, y_n1: Float, y_n2: Float) -> Float { self.b0 * x_n + self.b1 * x_n1 + self.b2 * x_n2 - self.a1 * y_n1 - self.a2 * y_n2 } } diff --git a/src/source/channel_volume.rs b/src/source/channel_volume.rs index c80c4d73..4b5b2e87 100644 --- a/src/source/channel_volume.rs +++ b/src/source/channel_volume.rs @@ -2,7 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::{Sample, Source}; +use crate::{Float, Sample, Source}; /// Combines channels in input into a single mono source, then plays that mono sound /// to each channel at the volume given for that channel. @@ -12,7 +12,7 @@ where I: Source, { input: I, - channel_volumes: Vec, + channel_volumes: Vec, current_channel: usize, current_sample: Option, } @@ -24,7 +24,7 @@ where /// Wrap the input source and make it mono. Play that mono sound to each /// channel at the volume set by the user. The volume can be changed using /// [`ChannelVolume::set_volume`]. - pub fn new(input: I, channel_volumes: Vec) -> ChannelVolume { + pub fn new(input: I, channel_volumes: Vec) -> ChannelVolume { let channel_count = channel_volumes.len(); // See next() implementation. ChannelVolume { input, @@ -36,7 +36,7 @@ where /// Sets the volume for a given channel number. Will panic if channel number /// is invalid. - pub fn set_volume(&mut self, channel: usize, volume: f32) { + pub fn set_volume(&mut self, channel: usize, volume: Float) { self.channel_volumes[channel] = volume; } @@ -77,7 +77,7 @@ where self.current_sample = Some(self.current_sample.unwrap_or(0.0) + s); } } - self.current_sample.map(|s| s / num_channels.get() as f32); + self.current_sample.map(|s| s / num_channels.get() as Float); } let result = self .current_sample diff --git a/src/source/chirp.rs b/src/source/chirp.rs index e7bff520..ce5c0bf3 100644 --- a/src/source/chirp.rs +++ b/src/source/chirp.rs @@ -1,20 +1,20 @@ //! Chirp/sweep source. -use std::{f32::consts::TAU, time::Duration}; +use std::time::Duration; use crate::{ common::{ChannelCount, SampleRate}, - math::nz, + math::{nz, TAU}, source::SeekError, - Source, + Float, Sample, Source, }; /// Convenience function to create a new `Chirp` source. #[inline] pub fn chirp( sample_rate: SampleRate, - start_frequency: f32, - end_frequency: f32, + start_frequency: Float, + end_frequency: Float, duration: Duration, ) -> Chirp { Chirp::new(sample_rate, start_frequency, end_frequency, duration) @@ -24,8 +24,8 @@ pub fn chirp( /// At the end of the chirp, once the `end_frequency` is reached, the source is exhausted. #[derive(Clone, Debug)] pub struct Chirp { - start_frequency: f32, - end_frequency: f32, + start_frequency: Float, + end_frequency: Float, sample_rate: SampleRate, total_samples: u64, elapsed_samples: u64, @@ -34,8 +34,8 @@ pub struct Chirp { impl Chirp { fn new( sample_rate: SampleRate, - start_frequency: f32, - end_frequency: f32, + start_frequency: Float, + end_frequency: Float, duration: Duration, ) -> Self { Self { @@ -60,7 +60,7 @@ impl Chirp { } impl Iterator for Chirp { - type Item = f32; + type Item = Sample; fn next(&mut self) -> Option { let i = self.elapsed_samples; @@ -68,9 +68,9 @@ impl Iterator for Chirp { return None; // Exhausted } - let ratio = (i as f64 / self.total_samples as f64) as f32; + let ratio = (i as f64 / self.total_samples as f64) as Float; let freq = self.start_frequency * (1.0 - ratio) + self.end_frequency * ratio; - let t = (i as f64 / self.sample_rate().get() as f64) as f32 * TAU * freq; + let t = (i as f64 / self.sample_rate().get() as f64) as Float * TAU * freq; self.elapsed_samples += 1; Some(t.sin()) diff --git a/src/source/crossfade.rs b/src/source/crossfade.rs index 2e6efc8b..3fc40596 100644 --- a/src/source/crossfade.rs +++ b/src/source/crossfade.rs @@ -35,9 +35,10 @@ mod tests { use crate::buffer::SamplesBuffer; use crate::math::nz; use crate::source::Zero; + use crate::Sample; fn dummy_source(length: u8) -> SamplesBuffer { - let data: Vec = (1..=length).map(f32::from).collect(); + let data: Vec = (1..=length).map(Sample::from).collect(); SamplesBuffer::new(nz!(1), nz!(1), data) } @@ -50,11 +51,14 @@ mod tests { source2, Duration::from_secs(5) + Duration::from_nanos(1), ); - assert_eq!(mixed.next(), Some(1.0)); - assert_eq!(mixed.next(), Some(2.0)); - assert_eq!(mixed.next(), Some(3.0)); - assert_eq!(mixed.next(), Some(4.0)); - assert_eq!(mixed.next(), Some(5.0)); + + // Use approximate equality for floating-point comparisons + let eps = 1e-6; + assert!((mixed.next().unwrap() - 1.0).abs() < eps); + assert!((mixed.next().unwrap() - 2.0).abs() < eps); + assert!((mixed.next().unwrap() - 3.0).abs() < eps); + assert!((mixed.next().unwrap() - 4.0).abs() < eps); + assert!((mixed.next().unwrap() - 5.0).abs() < eps); assert_eq!(mixed.next(), None); } diff --git a/src/source/delay.rs b/src/source/delay.rs index 1f2d3fb4..0f55aa32 100644 --- a/src/source/delay.rs +++ b/src/source/delay.rs @@ -2,6 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; +use crate::math::NANOS_PER_SEC; use crate::Source; fn remaining_samples( @@ -9,8 +10,8 @@ fn remaining_samples( sample_rate: SampleRate, channels: ChannelCount, ) -> usize { - let ns = until_playback.as_secs() * 1_000_000_000 + until_playback.subsec_nanos() as u64; - let samples = ns * channels.get() as u64 * sample_rate.get() as u64 / 1_000_000_000; + let ns = until_playback.as_nanos(); + let samples = ns * channels.get() as u128 * sample_rate.get() as u128 / NANOS_PER_SEC as u128; samples as usize } diff --git a/src/source/distortion.rs b/src/source/distortion.rs index c3e06ed8..381e9a0d 100644 --- a/src/source/distortion.rs +++ b/src/source/distortion.rs @@ -2,10 +2,10 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::Source; +use crate::{Float, Source}; /// Internal function that builds a `Distortion` object. -pub(crate) fn distortion(input: I, gain: f32, threshold: f32) -> Distortion +pub(crate) fn distortion(input: I, gain: Float, threshold: Float) -> Distortion where I: Source, { @@ -20,20 +20,20 @@ where #[derive(Clone, Debug)] pub struct Distortion { input: I, - gain: f32, - threshold: f32, + gain: Float, + threshold: Float, } impl Distortion { /// Modifies the distortion gain. #[inline] - pub fn set_gain(&mut self, gain: f32) { + pub fn set_gain(&mut self, gain: Float) { self.gain = gain; } /// Modifies the distortion threshold. #[inline] - pub fn set_threshold(&mut self, threshold: f32) { + pub fn set_threshold(&mut self, threshold: Float) { self.threshold = threshold; } diff --git a/src/source/dither.rs b/src/source/dither.rs index 79b0a980..f87e472c 100644 --- a/src/source/dither.rs +++ b/src/source/dither.rs @@ -30,7 +30,7 @@ use std::time::Duration; use crate::{ source::noise::{Blue, WhiteGaussian, WhiteTriangular, WhiteUniform}, - BitDepth, ChannelCount, Sample, SampleRate, Source, + BitDepth, ChannelCount, Float, Sample, SampleRate, Source, }; /// Dither algorithm selection for runtime choice @@ -162,7 +162,7 @@ pub struct Dither { noise: NoiseGenerator, current_channel: usize, remaining_in_span: Option, - lsb_amplitude: f32, + lsb_amplitude: Float, } impl Dither @@ -175,7 +175,7 @@ where // Using f64 intermediate prevents precision loss and u64 handles all bit depths without // overflow (64-bit being the theoretical maximum for audio samples). Values stay well // above f32 denormal threshold, avoiding denormal arithmetic performance penalty. - let lsb_amplitude = (1.0 / (1_u64 << (target_bits.get() - 1)) as f64) as f32; + let lsb_amplitude = (1.0 / (1_u64 << (target_bits.get() - 1)) as f64) as Float; let sample_rate = input.sample_rate(); let channels = input.channels(); @@ -296,10 +296,10 @@ mod tests { 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 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; + let lsb = 1.0 / (1_i64 << (TEST_BIT_DEPTH.get() - 1)) as Float; // Verify dithered samples differ from undithered and are reasonable for (i, (&dithered_sample, &undithered_sample)) in dithered_samples @@ -339,22 +339,22 @@ mod tests { let mut dithered = Dither::new(constant_source, TEST_BIT_DEPTH, Algorithm::HighPass); // Collect interleaved samples (L, R, L, R, ...) - let samples: Vec = dithered.by_ref().take(1000).collect(); + let samples: Vec = dithered.by_ref().take(1000).collect(); // De-interleave into left and right channels - let left: Vec = samples.iter().step_by(2).copied().collect(); - let right: Vec = samples.iter().skip(1).step_by(2).copied().collect(); + let left: Vec = samples.iter().step_by(2).copied().collect(); + let right: Vec = samples.iter().skip(1).step_by(2).copied().collect(); assert_eq!(left.len(), 500); assert_eq!(right.len(), 500); // Calculate autocorrelation at lag 1 for each channel // Blue noise (high-pass) should have negative correlation at lag 1 - let left_autocorr: f32 = - left.windows(2).map(|w| w[0] * w[1]).sum::() / (left.len() - 1) as f32; + let left_autocorr: Float = + left.windows(2).map(|w| w[0] * w[1]).sum::() / (left.len() - 1) as Float; - let right_autocorr: f32 = - right.windows(2).map(|w| w[0] * w[1]).sum::() / (right.len() - 1) as f32; + let right_autocorr: Float = + right.windows(2).map(|w| w[0] * w[1]).sum::() / (right.len() - 1) as Float; // Blue noise should have negative autocorrelation (high-pass characteristic) // If channels were cross-contaminated, this property would be broken @@ -370,12 +370,12 @@ mod tests { ); // Channels should be independent - cross-correlation between L and R should be near zero - let cross_corr: f32 = left + let cross_corr: Float = left .iter() .zip(right.iter()) .map(|(l, r)| l * r) - .sum::() - / left.len() as f32; + .sum::() + / left.len() as Float; assert!( cross_corr.abs() < 0.1, diff --git a/src/source/fadein.rs b/src/source/fadein.rs index 85e19293..b1ebe8b0 100644 --- a/src/source/fadein.rs +++ b/src/source/fadein.rs @@ -10,7 +10,7 @@ where I: Source, { FadeIn { - input: linear_gain_ramp(input, duration, 0.0f32, 1.0f32, false), + input: linear_gain_ramp(input, duration, 0.0, 1.0, false), } } diff --git a/src/source/fadeout.rs b/src/source/fadeout.rs index 14a41569..1298a5ae 100644 --- a/src/source/fadeout.rs +++ b/src/source/fadeout.rs @@ -10,7 +10,7 @@ where I: Source, { FadeOut { - input: linear_gain_ramp(input, duration, 1.0f32, 0.0f32, true), + input: linear_gain_ramp(input, duration, 1.0, 0.0, true), } } diff --git a/src/source/limit.rs b/src/source/limit.rs index da4004a8..a16139ae 100644 --- a/src/source/limit.rs +++ b/src/source/limit.rs @@ -64,7 +64,7 @@ use super::SeekError; use crate::{ common::{ChannelCount, Sample, SampleRate}, math::{self, duration_to_coefficient}, - Source, + Float, Source, }; /// Configuration settings for audio limiting. @@ -156,7 +156,7 @@ pub struct LimitSettings { /// /// Values must be negative - positive values would attempt limiting above /// 0 dBFS, which cannot prevent clipping. - pub threshold: f32, + pub threshold: Float, /// Range over which limiting gradually increases (dB). /// /// Defines the transition zone width in dB where limiting gradually increases @@ -165,7 +165,7 @@ pub struct LimitSettings { /// - `2.0` = moderate knee (some gradual transition) /// - `4.0` = soft knee (smooth, transparent transition) /// - `8.0` = very soft knee (very gradual, musical transition) - pub knee_width: f32, + pub knee_width: Float, /// Time to respond to level increases pub attack: Duration, /// Time to recover after level decreases @@ -399,7 +399,7 @@ impl LimitSettings { /// Only negative values are meaningful - positive values would attempt limiting /// above 0 dBFS, which cannot prevent clipping. #[inline] - pub fn with_threshold(mut self, threshold: f32) -> Self { + pub fn with_threshold(mut self, threshold: Float) -> Self { self.threshold = threshold; self } @@ -422,7 +422,7 @@ impl LimitSettings { /// - Gradual limiting from -5 dBFS to -1 dBFS /// - Full limiting above -1 dBFS (threshold + knee_width/2) #[inline] - pub fn with_knee_width(mut self, knee_width: f32) -> Self { + pub fn with_knee_width(mut self, knee_width: Float) -> Self { self.knee_width = knee_width; self } @@ -700,15 +700,15 @@ where #[derive(Clone, Debug)] struct LimitBase { /// Level where limiting begins (dB) - threshold: f32, + threshold: Float, /// Width of the soft-knee region (dB) - knee_width: f32, + knee_width: Float, /// Inverse of 8 times the knee width (precomputed for efficiency) - inv_knee_8: f32, + inv_knee_8: Float, /// Attack time constant (ms) - attack: f32, + attack: Float, /// Release time constant (ms) - release: f32, + release: Float, } /// Mono channel limiter optimized for single-channel processing. @@ -728,9 +728,9 @@ pub struct LimitMono { /// Common limiter parameters base: LimitBase, /// Peak detection integrator state - limiter_integrator: f32, + limiter_integrator: Float, /// Peak detection state - limiter_peak: f32, + limiter_peak: Float, } /// Stereo channel limiter with optimized two-channel processing. @@ -756,9 +756,9 @@ pub struct LimitStereo { /// Common limiter parameters base: LimitBase, /// Peak detection integrator states for left and right channels - limiter_integrators: [f32; 2], + limiter_integrators: [Float; 2], /// Peak detection states for left and right channels - limiter_peaks: [f32; 2], + limiter_peaks: [Float; 2], /// Current channel position (0 = left, 1 = right) position: u8, } @@ -787,9 +787,9 @@ pub struct LimitMulti { /// Common limiter parameters base: LimitBase, /// Peak detector integrator states (one per channel) - limiter_integrators: Vec, + limiter_integrators: Vec, /// Peak detector states (one per channel) - limiter_peaks: Vec, + limiter_peaks: Vec, /// Current channel position (0 to channels-1) position: usize, } @@ -815,7 +815,12 @@ pub struct LimitMulti { /// /// Amount of gain reduction to apply in dB #[inline] -fn process_sample(sample: Sample, threshold: f32, knee_width: f32, inv_knee_8: f32) -> f32 { +fn process_sample( + sample: Sample, + threshold: Float, + knee_width: Float, + inv_knee_8: Float, +) -> Sample { // 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() + Sample::MIN_POSITIVE) - threshold; @@ -832,7 +837,7 @@ fn process_sample(sample: Sample, threshold: f32, knee_width: f32, inv_knee_8: f } impl LimitBase { - fn new(threshold: f32, knee_width: f32, attack: f32, release: f32) -> Self { + fn new(threshold: Float, knee_width: Float, attack: Float, release: Float) -> Self { let inv_knee_8 = 1.0 / (8.0 * knee_width); Self { threshold, @@ -859,13 +864,13 @@ impl LimitBase { /// allow for coupled gain reduction across channels. #[must_use] #[inline] - fn process_channel(&self, sample: Sample, integrator: &mut f32, peak: &mut f32) -> Sample { + fn process_channel(&self, sample: Sample, integrator: &mut Float, peak: &mut Float) -> Sample { // step 1-4: half-wave rectification and conversion into dB, and gain computer with soft // knee and subtractor let limiter_db = process_sample(sample, self.threshold, self.knee_width, self.inv_knee_8); // step 5: smooth, decoupled peak detector - *integrator = f32::max( + *integrator = Float::max( limiter_db, self.release * *integrator + (1.0 - self.release) * limiter_db, ); @@ -914,7 +919,7 @@ where // steps 6-8: conversion into level and multiplication into gain stage. Find maximum peak // across both channels to couple the gain and maintain stereo imaging. - let max_peak = f32::max(self.limiter_peaks[0], self.limiter_peaks[1]); + let max_peak = Float::max(self.limiter_peaks[0], self.limiter_peaks[1]); processed * math::db_to_linear(-max_peak) } } @@ -942,7 +947,7 @@ where let max_peak = self .limiter_peaks .iter() - .fold(0.0, |max, &peak| f32::max(max, peak)); + .fold(0.0, |max, &peak| Float::max(max, peak)); processed * math::db_to_linear(-max_peak) } } @@ -1126,7 +1131,7 @@ mod tests { use std::time::Duration; fn create_test_buffer( - samples: Vec, + samples: Vec, channels: ChannelCount, sample_rate: SampleRate, ) -> SamplesBuffer { diff --git a/src/source/linear_ramp.rs b/src/source/linear_ramp.rs index 050d7245..b39d5f94 100644 --- a/src/source/linear_ramp.rs +++ b/src/source/linear_ramp.rs @@ -2,26 +2,26 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::Source; +use crate::math::{duration_to_float, NANOS_PER_SEC}; +use crate::{Float, Source}; /// Internal function that builds a `LinearRamp` object. pub fn linear_gain_ramp( input: I, duration: Duration, - start_gain: f32, - end_gain: f32, + start_gain: Float, + end_gain: Float, clamp_end: bool, ) -> LinearGainRamp where I: Source, { - let duration_nanos = duration.as_nanos() as f32; - assert!(duration_nanos > 0.0f32); + assert!(!duration.is_zero(), "duration must be greater than zero"); LinearGainRamp { input, - elapsed_ns: 0.0f32, - total_ns: duration_nanos, + elapsed: Duration::ZERO, + total: duration, start_gain, end_gain, clamp_end, @@ -33,10 +33,10 @@ where #[derive(Clone, Debug)] pub struct LinearGainRamp { input: I, - elapsed_ns: f32, - total_ns: f32, - start_gain: f32, - end_gain: f32, + elapsed: Duration, + total: Duration, + start_gain: Float, + end_gain: Float, clamp_end: bool, sample_idx: u64, } @@ -72,24 +72,28 @@ where #[inline] fn next(&mut self) -> Option { - let factor: f32; - let remaining_ns = self.total_ns - self.elapsed_ns; + let factor: Float; - if remaining_ns < 0.0 { + if self.elapsed >= self.total { if self.clamp_end { factor = self.end_gain; } else { - factor = 1.0f32; + factor = 1.0; } } else { self.sample_idx += 1; - let p = self.elapsed_ns / self.total_ns; - factor = self.start_gain * (1.0f32 - p) + self.end_gain * p; + // Calculate progress (0.0 to 1.0) using appropriate precision for Float type + let p = duration_to_float(self.elapsed) / duration_to_float(self.total); + + factor = self.start_gain * (1.0 - p) + self.end_gain * p; } if self.sample_idx.is_multiple_of(self.channels().get() as u64) { - self.elapsed_ns += 1000000000.0 / (self.input.sample_rate().get() as f32); + let sample_duration = Duration::from_nanos( + NANOS_PER_SEC / self.input.sample_rate().get() as u64 + ); + self.elapsed += sample_duration; } self.input.next().map(|value| value * factor) @@ -129,7 +133,7 @@ where #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - self.elapsed_ns = pos.as_nanos() as f32; + self.elapsed = pos; self.input.try_seek(pos) } } @@ -146,7 +150,7 @@ mod tests { /// Create a SamplesBuffer of identical samples with value `value`. /// Returned buffer is one channel and has a sample rate of 1 hz. fn const_source(length: u8, value: Sample) -> SamplesBuffer { - let data: Vec = (1..=length).map(|_| value).collect(); + let data: Vec = (1..=length).map(|_| value).collect(); SamplesBuffer::new(nz!(1), nz!(1), data) } @@ -162,7 +166,7 @@ mod tests { #[test] fn test_linear_ramp() { - let source1 = const_source(10, 1.0f32); + let source1 = const_source(10, 1.0); let mut faded = linear_gain_ramp(source1, Duration::from_secs(4), 0.0, 1.0, true); assert_eq!(faded.next(), Some(0.0)); @@ -180,7 +184,7 @@ mod tests { #[test] fn test_linear_ramp_clamped() { - let source1 = const_source(10, 1.0f32); + let source1 = const_source(10, 1.0); let mut faded = linear_gain_ramp(source1, Duration::from_secs(4), 0.0, 0.5, true); assert_eq!(faded.next(), Some(0.0)); // fading in... @@ -198,7 +202,7 @@ mod tests { #[test] fn test_linear_ramp_seek() { - let source1 = cycle_source(20, vec![0.0f32, 0.4f32, 0.8f32]); + let source1 = cycle_source(20, vec![0.0, 0.4, 0.8]); let mut faded = linear_gain_ramp(source1, Duration::from_secs(10), 0.0, 1.0, true); assert_abs_diff_eq!(faded.next().unwrap(), 0.0); // source value 0 diff --git a/src/source/mod.rs b/src/source/mod.rs index 5dc79813..5cc890b1 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, BitDepth, Sample, + math, BitDepth, Float, Sample, }; use dasp_sample::FromSample; @@ -294,7 +294,7 @@ pub trait Source: Iterator { /// Amplifies the sound by the given value. #[inline] - fn amplify(self, value: f32) -> Amplify + fn amplify(self, value: Float) -> Amplify where Self: Sized, { @@ -303,7 +303,7 @@ pub trait Source: Iterator { /// Amplifies the sound logarithmically by the given value. #[inline] - fn amplify_decibel(self, value: f32) -> Amplify + fn amplify_decibel(self, value: Float) -> Amplify where Self: Sized, { @@ -317,18 +317,18 @@ pub trait Source: Iterator { /// /// **note: it clamps values outside this range.** #[inline] - fn amplify_normalized(self, value: f32) -> Amplify + fn amplify_normalized(self, value: Float) -> Amplify where Self: Sized, { - const NORMALIZATION_MIN: f32 = 0.0; - const NORMALIZATION_MAX: f32 = 1.0; - const LOG_VOLUME_GROWTH_RATE: f32 = 6.907_755_4; - const LOG_VOLUME_SCALE_FACTOR: f32 = 1000.0; + const NORMALIZATION_MIN: Float = 0.0; + const NORMALIZATION_MAX: Float = 1.0; + const LOG_VOLUME_GROWTH_RATE: Float = 6.907_755_4; + const LOG_VOLUME_SCALE_FACTOR: Float = 1000.0; let value = value.clamp(NORMALIZATION_MIN, NORMALIZATION_MAX); - let mut amplitude = f32::exp(LOG_VOLUME_GROWTH_RATE * value) / LOG_VOLUME_SCALE_FACTOR; + let mut amplitude = Float::exp(LOG_VOLUME_GROWTH_RATE * value) / LOG_VOLUME_SCALE_FACTOR; if value < 0.1 { amplitude *= value * 10.0; } @@ -417,8 +417,8 @@ pub trait Source: Iterator { Self: Sized, { // Added Limits to prevent the AGC from blowing up. ;) - let attack_time_limited = agc_settings.attack_time.min(Duration::from_secs_f32(10.0)); - let release_time_limited = agc_settings.release_time.min(Duration::from_secs_f32(10.0)); + let attack_time_limited = agc_settings.attack_time.min(Duration::from_secs(10)); + let release_time_limited = agc_settings.release_time.min(Duration::from_secs(10)); agc::automatic_gain_control( self, @@ -522,8 +522,8 @@ pub trait Source: Iterator { fn linear_gain_ramp( self, duration: Duration, - start_value: f32, - end_value: f32, + start_value: Float, + end_value: Float, clamp_end: bool, ) -> LinearGainRamp where @@ -613,7 +613,7 @@ pub trait Source: Iterator { /// let source = source.buffered().reverb(Duration::from_millis(100), 0.7); /// ``` #[inline] - fn reverb(self, duration: Duration, amplitude: f32) -> Mix>> + fn reverb(self, duration: Duration, amplitude: Float) -> Mix>> where Self: Sized + Clone, { @@ -674,7 +674,7 @@ pub trait Source: Iterator { fn low_pass(self, freq: u32) -> BltFilter where Self: Sized, - Self: Source, + Self: Source, { blt::low_pass(self, freq) } @@ -684,34 +684,34 @@ pub trait Source: Iterator { fn high_pass(self, freq: u32) -> BltFilter where Self: Sized, - Self: Source, + Self: Source, { blt::high_pass(self, freq) } /// Applies a low-pass filter to the source while allowing the q (bandwidth) to be changed. #[inline] - fn low_pass_with_q(self, freq: u32, q: f32) -> BltFilter + fn low_pass_with_q(self, freq: u32, q: Float) -> BltFilter where Self: Sized, - Self: Source, + Self: Source, { blt::low_pass_with_q(self, freq, q) } /// Applies a high-pass filter to the source while allowing the q (bandwidth) to be changed. #[inline] - fn high_pass_with_q(self, freq: u32, q: f32) -> BltFilter + fn high_pass_with_q(self, freq: u32, q: Float) -> BltFilter where Self: Sized, - Self: Source, + Self: Source, { blt::high_pass_with_q(self, freq, q) } /// Applies a distortion effect to the sound. #[inline] - fn distortion(self, gain: f32, threshold: f32) -> Distortion + fn distortion(self, gain: Float, threshold: Float) -> Distortion where Self: Sized, { diff --git a/src/source/noise.rs b/src/source/noise.rs index 8d4b7738..c53376b3 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -50,8 +50,8 @@ use rand::{ }; use rand_distr::{Normal, Triangular}; -use crate::math::nz; -use crate::{ChannelCount, Sample, SampleRate, Source}; +use crate::math::{nz, PI}; +use crate::{ChannelCount, Float, Sample, SampleRate, Source}; /// Convenience function to create a new `WhiteUniform` noise source. #[deprecated(since = "0.21.0", note = "use WhiteUniform::new() instead")] @@ -98,12 +98,12 @@ macro_rules! impl_noise_source { /// Common sampling utilities for noise generators. /// Provides a generic interface for sampling from any distribution. #[derive(Clone, Debug)] -struct NoiseSampler + Clone> { +struct NoiseSampler + Clone> { rng: R, distribution: D, } -impl + Clone> NoiseSampler { +impl + Clone> NoiseSampler { /// Create a new sampler with the given distribution. fn new(rng: R, distribution: D) -> Self { Self { rng, distribution } @@ -111,7 +111,7 @@ impl + Clone> NoiseSampler { /// Generate a sample from the configured distribution. #[inline] - fn sample(&mut self) -> f32 { + fn sample(&mut self) -> Sample { self.rng.sample(&self.distribution) } } @@ -129,7 +129,7 @@ impl + Clone> NoiseSampler { #[derive(Clone, Debug)] pub struct WhiteUniform { sample_rate: SampleRate, - sampler: NoiseSampler>, + sampler: NoiseSampler>, } impl WhiteUniform { @@ -185,7 +185,7 @@ impl_noise_source!(WhiteUniform); #[derive(Clone, Debug)] pub struct WhiteTriangular { sample_rate: SampleRate, - sampler: NoiseSampler>, + sampler: NoiseSampler>, } impl WhiteTriangular { @@ -343,17 +343,17 @@ impl_noise_source!(Velvet); #[derive(Clone, Debug)] pub struct WhiteGaussian { sample_rate: SampleRate, - sampler: NoiseSampler>, + sampler: NoiseSampler>, } impl WhiteGaussian { /// Get the mean (average) value of the noise distribution. - pub fn mean(&self) -> f32 { + pub fn mean(&self) -> Sample { self.sampler.distribution.mean() } /// Get the standard deviation of the noise distribution. - pub fn std_dev(&self) -> f32 { + pub fn std_dev(&self) -> Sample { self.sampler.distribution.std_dev() } } @@ -437,7 +437,7 @@ const UNIFORM_VARIANCE: f32 = 1.0 / 3.0; pub struct Pink { sample_rate: SampleRate, white_noise: WhiteUniform, - values: [f32; PINK_NOISE_GENERATORS], + values: [Sample; PINK_NOISE_GENERATORS], counters: [u32; PINK_NOISE_GENERATORS], max_counts: [u32; PINK_NOISE_GENERATORS], } @@ -490,7 +490,7 @@ impl Iterator for Pink { } // Normalize by number of generators to keep output in reasonable range - Some(sum / PINK_NOISE_GENERATORS as f32) + Some(sum / PINK_NOISE_GENERATORS as Sample) } } @@ -521,7 +521,7 @@ impl_noise_source!(Pink); pub struct Blue { sample_rate: SampleRate, white_noise: WhiteUniform, - prev_white: f32, + prev_white: Sample, } impl Blue { @@ -584,7 +584,7 @@ impl_noise_source!(Blue); pub struct Violet { sample_rate: SampleRate, blue_noise: Blue, - prev: f32, + prev: Sample, } impl Violet { @@ -630,23 +630,23 @@ impl_noise_source!(Violet); struct IntegratedNoise { sample_rate: SampleRate, white_noise: W, - accumulator: f32, - leak_factor: f32, - scale: f32, + accumulator: Sample, + leak_factor: Float, + scale: Float, } impl IntegratedNoise where - W: Iterator, + W: Iterator, { /// Create a new integrated noise generator with the given white noise source and its standard deviation. - fn new_with_stddev(sample_rate: SampleRate, white_noise: W, white_noise_stddev: f32) -> Self { + fn new_with_stddev(sample_rate: SampleRate, white_noise: W, white_noise_stddev: Float) -> Self { // Leak factor prevents DC buildup while maintaining 1/f² characteristics. // Center frequency is set to 5Hz, which provides good behavior // while preventing excessive low-frequency buildup across common sample rates. let center_freq_hz = 5.0; let leak_factor = - 1.0 - ((2.0 * std::f32::consts::PI * center_freq_hz) / sample_rate.get() as f32); + 1.0 - ((2.0 * PI * center_freq_hz) / sample_rate.get() as Float); // Calculate the scaling factor to normalize output based on leak factor. // This ensures consistent output level regardless of the leak factor value. @@ -664,8 +664,8 @@ where } } -impl> Iterator for IntegratedNoise { - type Item = f32; +impl> Iterator for IntegratedNoise { + type Item = Sample; #[inline] fn next(&mut self) -> Option { @@ -788,7 +788,7 @@ 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); - let stddev = white_noise.std_dev(); + let stddev = white_noise.std_dev() as Float; let inner = IntegratedNoise::new_with_stddev(sample_rate, white_noise, stddev); Self { inner } @@ -841,22 +841,22 @@ mod tests { const TEST_SAMPLES_MEDIUM: usize = 1000; /// Calculate correlation between consecutive samples. - fn calculate_correlation(samples: &[f32]) -> f32 { + fn calculate_correlation(samples: &[Sample]) -> Sample { let mut correlation_sum = 0.0; for i in 0..samples.len() - 1 { correlation_sum += samples[i] * samples[i + 1]; } - correlation_sum / (samples.len() - 1) as f32 + correlation_sum / (samples.len() - 1) as Sample } /// Test properties common to 1/f² integrated noise generators (Brownian and Red). - fn test_integrated_noise_properties>(mut generator: T, name: &str) { + fn test_integrated_noise_properties>(mut generator: T, name: &str) { // Test that integrated noise doesn't accumulate DC indefinitely - let samples: Vec = (0..TEST_SAMPLE_RATE.get() * 10) + let samples: Vec = (0..TEST_SAMPLE_RATE.get() * 10) .map(|_| generator.next().unwrap()) .collect(); // 10 seconds - let average = samples.iter().sum::() / samples.len() as f32; + let average = samples.iter().sum::() / samples.len() as Sample; // Average should be close to zero due to leak factor assert!( average.abs() < 0.5, @@ -872,7 +872,7 @@ mod tests { } // Helper function to create iterator from generator name - fn create_generator_iterator(name: &str) -> Box> { + fn create_generator_iterator(name: &str) -> Box> { match name { "WhiteUniform" => Box::new(WhiteUniform::new(TEST_SAMPLE_RATE)), "WhiteTriangular" => Box::new(WhiteTriangular::new(TEST_SAMPLE_RATE)), @@ -1049,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() < Sample::EPSILON, + (generator.std_dev() - expected_std_dev).abs() < f32::EPSILON, "Triangular std_dev should be 2/sqrt(6) ≈ 0.8165, got {}", generator.std_dev() ); @@ -1064,7 +1064,7 @@ mod tests { // Test that most samples fall within 3 standard deviations // (should be ~85%; theoretical: ~90.5%) let mut generator = WhiteGaussian::new(TEST_SAMPLE_RATE); - let samples: Vec = (0..TEST_SAMPLES_MEDIUM) + let samples: Vec = (0..TEST_SAMPLES_MEDIUM) .map(|_| generator.next().unwrap()) .collect(); let out_of_bounds = samples.iter().filter(|&&s| s.abs() > 1.0).count(); @@ -1080,7 +1080,7 @@ mod tests { #[test] fn test_pink_noise_properties() { let mut generator = Pink::new(TEST_SAMPLE_RATE); - let samples: Vec = (0..TEST_SAMPLES_MEDIUM) + let samples: Vec = (0..TEST_SAMPLES_MEDIUM) .map(|_| generator.next().unwrap()) .collect(); @@ -1097,7 +1097,7 @@ mod tests { #[test] fn test_blue_noise_properties() { let mut generator = Blue::new(TEST_SAMPLE_RATE); - let samples: Vec = (0..TEST_SAMPLES_MEDIUM) + let samples: Vec = (0..TEST_SAMPLES_MEDIUM) .map(|_| generator.next().unwrap()) .collect(); @@ -1114,7 +1114,7 @@ mod tests { #[test] fn test_violet_noise_properties() { let mut generator = Violet::new(TEST_SAMPLE_RATE); - let samples: Vec = (0..TEST_SAMPLES_MEDIUM) + let samples: Vec = (0..TEST_SAMPLES_MEDIUM) .map(|_| generator.next().unwrap()) .collect(); @@ -1122,7 +1122,7 @@ mod tests { // Check that consecutive differences have higher variance than the original signal let mut diff_variance = 0.0; let mut signal_variance = 0.0; - let mean = samples.iter().sum::() / samples.len() as f32; + let mean = samples.iter().sum::() / samples.len() as Sample; for i in 0..samples.len() - 1 { let diff = samples[i + 1] - samples[i]; @@ -1131,8 +1131,8 @@ mod tests { signal_variance += centered * centered; } - diff_variance /= (samples.len() - 1) as f32; - signal_variance /= samples.len() as f32; + diff_variance /= (samples.len() - 1) as Sample; + signal_variance /= samples.len() as Sample; // For violet noise (high-pass), differences should have comparable or higher variance assert!( diff --git a/src/source/sawtooth.rs b/src/source/sawtooth.rs index a0e812e4..2bfc2309 100644 --- a/src/source/sawtooth.rs +++ b/src/source/sawtooth.rs @@ -1,7 +1,7 @@ use crate::common::{ChannelCount, SampleRate}; use crate::math::nz; use crate::source::{Function, SignalGenerator}; -use crate::Source; +use crate::{Sample, Source}; use std::time::Duration; use super::SeekError; @@ -30,10 +30,10 @@ impl SawtoothWave { } impl Iterator for SawtoothWave { - type Item = f32; + type Item = Sample; #[inline] - fn next(&mut self) -> Option { + fn next(&mut self) -> Option { self.test_saw.next() } } diff --git a/src/source/signal_generator.rs b/src/source/signal_generator.rs index 9372f40a..e1fb7a67 100644 --- a/src/source/signal_generator.rs +++ b/src/source/signal_generator.rs @@ -14,9 +14,8 @@ //! ``` use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::math::nz; -use crate::Source; -use std::f32::consts::TAU; +use crate::math::{duration_to_float, nz, TAU}; +use crate::{Float, Sample, Source}; use std::time::Duration; /// Generator function. @@ -25,17 +24,17 @@ use std::time::Duration; /// function to create periodic waveforms. /// /// # Arguments -/// * An `f32` representing a time in the signal to generate. The scale of this variable is +/// * A `Float` representing a time in the signal to generate. The scale of this variable is /// normalized to the period of the signal, such that "0.0" is time zero, "1.0" is one period of /// the signal, "2.0" is two periods and so on. This function should be written to accept any -/// float in the range (`f32::MIN`, `f32::MAX`) but `SignalGenerator` will only pass values in +/// float in the range (`Float::MIN`, `Float::MAX`) but `SignalGenerator` will only pass values in /// (0.0, 1.0) to mitigate floating point error. /// /// # Returns /// -/// An `f32` representing the signal level at the passed time. This value should be normalized +/// A `Sample` (Float) representing the signal level at the passed time. This value should be normalized /// in the range [-1.0,1.0]. -pub type GeneratorFunction = fn(f32) -> f32; +pub type GeneratorFunction = fn(Float) -> Sample; /// Waveform functions. #[derive(Clone, Debug)] @@ -50,24 +49,24 @@ pub enum Function { Sawtooth, } -fn sine_signal(phase: f32) -> f32 { +fn sine_signal(phase: Float) -> Sample { (TAU * phase).sin() } -fn triangle_signal(phase: f32) -> f32 { - 4.0f32 * (phase - (phase + 0.5f32).floor()).abs() - 1f32 +fn triangle_signal(phase: Float) -> Sample { + 4.0 * (phase - (phase + 0.5).floor()).abs() - 1.0 } -fn square_signal(phase: f32) -> f32 { - if phase % 1.0f32 < 0.5f32 { - 1.0f32 +fn square_signal(phase: Float) -> Sample { + if phase % 1.0 < 0.5 { + 1.0 } else { - -1.0f32 + -1.0 } } -fn sawtooth_signal(phase: f32) -> f32 { - 2.0f32 * (phase - (phase + 0.5f32).floor()) +fn sawtooth_signal(phase: Float) -> Sample { + 2.0 * (phase - (phase + 0.5).floor()) } /// An infinite source that produces one of a selection of test waveforms. @@ -75,9 +74,9 @@ fn sawtooth_signal(phase: f32) -> f32 { pub struct SignalGenerator { sample_rate: SampleRate, function: GeneratorFunction, - phase_step: f32, - phase: f32, - period: f32, + phase_step: Float, + phase: Float, + period: Float, } impl SignalGenerator { @@ -112,27 +111,27 @@ impl SignalGenerator { generator_function: GeneratorFunction, ) -> Self { assert!(frequency > 0.0, "frequency must be greater than zero"); - let period = sample_rate.get() as f32 / frequency; - let phase_step = 1.0f32 / period; + let period = sample_rate.get() as Float / frequency as Float; + let phase_step = 1.0 / period; SignalGenerator { sample_rate, function: generator_function, phase_step, - phase: 0.0f32, + phase: 0.0, period, } } } impl Iterator for SignalGenerator { - type Item = f32; + type Item = Sample; #[inline] - fn next(&mut self) -> Option { + fn next(&mut self) -> Option { let f = self.function; let val = Some(f(self.phase)); - self.phase = (self.phase + self.phase_step).rem_euclid(1.0f32); + self.phase = (self.phase + self.phase_step).rem_euclid(1.0); val } } @@ -160,8 +159,8 @@ impl Source for SignalGenerator { #[inline] fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> { - let seek = duration.as_secs_f32() * (self.sample_rate.get() as f32) / self.period; - self.phase = seek.rem_euclid(1.0f32); + let seek = duration_to_float(duration) * (self.sample_rate.get() as Float) / self.period; + self.phase = seek.rem_euclid(1.0); Ok(()) } } @@ -170,64 +169,67 @@ impl Source for SignalGenerator { mod tests { use crate::math::nz; use crate::source::{Function, SignalGenerator}; + use crate::Sample; use approx::assert_abs_diff_eq; + const TEST_EPSILON: Sample = 0.0001; + #[test] fn square() { let mut wf = SignalGenerator::new(nz!(2000), 500.0f32, Function::Square); - assert_eq!(wf.next(), Some(1.0f32)); - assert_eq!(wf.next(), Some(1.0f32)); - assert_eq!(wf.next(), Some(-1.0f32)); - assert_eq!(wf.next(), Some(-1.0f32)); - assert_eq!(wf.next(), Some(1.0f32)); - assert_eq!(wf.next(), Some(1.0f32)); - assert_eq!(wf.next(), Some(-1.0f32)); - assert_eq!(wf.next(), Some(-1.0f32)); + assert_eq!(wf.next(), Some(1.0)); + assert_eq!(wf.next(), Some(1.0)); + assert_eq!(wf.next(), Some(-1.0)); + assert_eq!(wf.next(), Some(-1.0)); + assert_eq!(wf.next(), Some(1.0)); + assert_eq!(wf.next(), Some(1.0)); + assert_eq!(wf.next(), Some(-1.0)); + assert_eq!(wf.next(), Some(-1.0)); } #[test] fn triangle() { let mut wf = SignalGenerator::new(nz!(8000), 1000.0f32, Function::Triangle); - assert_eq!(wf.next(), Some(-1.0f32)); - assert_eq!(wf.next(), Some(-0.5f32)); - assert_eq!(wf.next(), Some(0.0f32)); - assert_eq!(wf.next(), Some(0.5f32)); - assert_eq!(wf.next(), Some(1.0f32)); - assert_eq!(wf.next(), Some(0.5f32)); - assert_eq!(wf.next(), Some(0.0f32)); - assert_eq!(wf.next(), Some(-0.5f32)); - assert_eq!(wf.next(), Some(-1.0f32)); - assert_eq!(wf.next(), Some(-0.5f32)); - assert_eq!(wf.next(), Some(0.0f32)); - assert_eq!(wf.next(), Some(0.5f32)); - assert_eq!(wf.next(), Some(1.0f32)); - assert_eq!(wf.next(), Some(0.5f32)); - assert_eq!(wf.next(), Some(0.0f32)); - assert_eq!(wf.next(), Some(-0.5f32)); + assert_eq!(wf.next(), Some(-1.0)); + assert_eq!(wf.next(), Some(-0.5)); + assert_eq!(wf.next(), Some(0.0)); + assert_eq!(wf.next(), Some(0.5)); + assert_eq!(wf.next(), Some(1.0)); + assert_eq!(wf.next(), Some(0.5)); + assert_eq!(wf.next(), Some(0.0)); + assert_eq!(wf.next(), Some(-0.5)); + assert_eq!(wf.next(), Some(-1.0)); + assert_eq!(wf.next(), Some(-0.5)); + assert_eq!(wf.next(), Some(0.0)); + assert_eq!(wf.next(), Some(0.5)); + assert_eq!(wf.next(), Some(1.0)); + assert_eq!(wf.next(), Some(0.5)); + assert_eq!(wf.next(), Some(0.0)); + assert_eq!(wf.next(), Some(-0.5)); } #[test] fn saw() { let mut wf = SignalGenerator::new(nz!(200), 50.0f32, Function::Sawtooth); - assert_eq!(wf.next(), Some(0.0f32)); - assert_eq!(wf.next(), Some(0.5f32)); - assert_eq!(wf.next(), Some(-1.0f32)); - assert_eq!(wf.next(), Some(-0.5f32)); - assert_eq!(wf.next(), Some(0.0f32)); - assert_eq!(wf.next(), Some(0.5f32)); - assert_eq!(wf.next(), Some(-1.0f32)); + assert_eq!(wf.next(), Some(0.0)); + assert_eq!(wf.next(), Some(0.5)); + assert_eq!(wf.next(), Some(-1.0)); + assert_eq!(wf.next(), Some(-0.5)); + assert_eq!(wf.next(), Some(0.0)); + assert_eq!(wf.next(), Some(0.5)); + assert_eq!(wf.next(), Some(-1.0)); } #[test] fn sine() { let mut wf = SignalGenerator::new(nz!(1000), 100f32, Function::Sine); - assert_abs_diff_eq!(wf.next().unwrap(), 0.0f32); - assert_abs_diff_eq!(wf.next().unwrap(), 0.58778525f32); - assert_abs_diff_eq!(wf.next().unwrap(), 0.95105652f32); - assert_abs_diff_eq!(wf.next().unwrap(), 0.95105652f32); - assert_abs_diff_eq!(wf.next().unwrap(), 0.58778525f32); - assert_abs_diff_eq!(wf.next().unwrap(), 0.0f32); - assert_abs_diff_eq!(wf.next().unwrap(), -0.58778554f32); + assert_abs_diff_eq!(wf.next().unwrap(), 0.0, epsilon = TEST_EPSILON); + assert_abs_diff_eq!(wf.next().unwrap(), 0.58778525, epsilon = TEST_EPSILON); + assert_abs_diff_eq!(wf.next().unwrap(), 0.95105652, epsilon = TEST_EPSILON); + assert_abs_diff_eq!(wf.next().unwrap(), 0.95105652, epsilon = TEST_EPSILON); + assert_abs_diff_eq!(wf.next().unwrap(), 0.58778525, epsilon = TEST_EPSILON); + assert_abs_diff_eq!(wf.next().unwrap(), 0.0, epsilon = TEST_EPSILON); + assert_abs_diff_eq!(wf.next().unwrap(), -0.58778554, epsilon = TEST_EPSILON); } } diff --git a/src/source/sine.rs b/src/source/sine.rs index a85fcb63..1481d6b2 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -1,7 +1,7 @@ use crate::common::{ChannelCount, SampleRate}; use crate::math::nz; use crate::source::{Function, SignalGenerator}; -use crate::Source; +use crate::{Sample, Source}; use std::time::Duration; use super::SeekError; @@ -30,10 +30,10 @@ impl SineWave { } impl Iterator for SineWave { - type Item = f32; + type Item = Sample; #[inline] - fn next(&mut self) -> Option { + fn next(&mut self) -> Option { self.test_sine.next() } } diff --git a/src/source/skip.rs b/src/source/skip.rs index d3c43857..95f1d49d 100644 --- a/src/source/skip.rs +++ b/src/source/skip.rs @@ -2,10 +2,9 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; +use crate::math::NANOS_PER_SEC; use crate::Source; -const NS_PER_SECOND: u128 = 1_000_000_000; - /// Internal function that builds a `SkipDuration` object. pub fn skip_duration(mut input: I, duration: Duration) -> SkipDuration where @@ -42,7 +41,7 @@ where let sample_rate = input.sample_rate().get() as u128; let channels = input.channels().get() as u128; - let samples_per_channel = duration.as_nanos() * sample_rate / NS_PER_SECOND; + let samples_per_channel = duration.as_nanos() * sample_rate / NANOS_PER_SEC as u128; let samples_to_skip: u128 = samples_per_channel * channels; // Check if we need to skip only part of the current span. @@ -52,7 +51,7 @@ where } duration -= Duration::from_nanos( - (NS_PER_SECOND * span_len as u128 / channels / sample_rate) as u64, + (NANOS_PER_SEC as u128 * span_len as u128 / channels / sample_rate) as u64, ); skip_samples(input, span_len); } @@ -65,7 +64,7 @@ where I: Source, { let samples_per_channel: u128 = - duration.as_nanos() * input.sample_rate().get() as u128 / NS_PER_SECOND; + duration.as_nanos() * input.sample_rate().get() as u128 / NANOS_PER_SEC as u128; let samples_to_skip: u128 = samples_per_channel * input.channels().get() as u128; skip_samples(input, samples_to_skip as usize); @@ -170,7 +169,9 @@ mod tests { use crate::buffer::SamplesBuffer; use crate::common::{ChannelCount, SampleRate}; use crate::math::nz; + use crate::Sample; use crate::source::Source; + use dasp_sample::Sample as DaspSample; fn test_skip_duration_samples_left( channels: ChannelCount, @@ -180,7 +181,7 @@ mod tests { ) { let buf_len = (sample_rate.get() * channels.get() as u32 * seconds) as usize; assert!(buf_len < 10 * 1024 * 1024); - let data: Vec = vec![0f32; buf_len]; + let data: Vec = vec![Sample::EQUILIBRIUM; buf_len]; let test_buffer = SamplesBuffer::new(channels, sample_rate, data); let seconds_left = seconds.saturating_sub(seconds_to_skip); diff --git a/src/source/spatial.rs b/src/source/spatial.rs index 6ea3555d..8aad0b7e 100644 --- a/src/source/spatial.rs +++ b/src/source/spatial.rs @@ -3,7 +3,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; use crate::source::ChannelVolume; -use crate::Source; +use crate::{Float, Source}; /// A simple spatial audio source. The underlying source is transformed to Mono /// and then played in stereo. The left and right channel's volume are amplified @@ -63,9 +63,9 @@ where let left_dist_modifier = (1.0 / left_dist_sq).min(1.0); let right_dist_modifier = (1.0 / right_dist_sq).min(1.0); self.input - .set_volume(0, left_diff_modifier * left_dist_modifier); + .set_volume(0, (left_diff_modifier * left_dist_modifier) as Float); self.input - .set_volume(1, right_diff_modifier * right_dist_modifier); + .set_volume(1, (right_diff_modifier * right_dist_modifier) as Float); } } diff --git a/src/source/square.rs b/src/source/square.rs index 4beaf33d..d4acb321 100644 --- a/src/source/square.rs +++ b/src/source/square.rs @@ -1,7 +1,7 @@ use crate::common::{ChannelCount, SampleRate}; use crate::math::nz; use crate::source::{Function, SignalGenerator}; -use crate::Source; +use crate::{Sample, Source}; use std::time::Duration; use super::SeekError; @@ -30,10 +30,10 @@ impl SquareWave { } impl Iterator for SquareWave { - type Item = f32; + type Item = Sample; #[inline] - fn next(&mut self) -> Option { + fn next(&mut self) -> Option { self.test_square.next() } } diff --git a/src/source/take.rs b/src/source/take.rs index 60ee9c6d..d9957e6f 100644 --- a/src/source/take.rs +++ b/src/source/take.rs @@ -2,7 +2,8 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::{Sample, Source}; +use crate::math::NANOS_PER_SEC; +use crate::{Float, Sample, Source}; /// Internal function that builds a `TakeDuration` object. pub fn take_duration(input: I, duration: Duration) -> TakeDuration @@ -28,16 +29,14 @@ impl DurationFilter { fn apply(&self, sample: Sample, parent: &TakeDuration) -> Sample { match self { DurationFilter::FadeOut => { - let remaining = parent.remaining_duration.as_millis() as f32; - let total = parent.requested_duration.as_millis() as f32; + let remaining = parent.remaining_duration.as_millis() as Float; + let total = parent.requested_duration.as_millis() as Float; sample * remaining / total } } } } -const NANOS_PER_SEC: u64 = 1_000_000_000; - /// A source that truncates the given source to a certain duration. #[derive(Clone, Debug)] pub struct TakeDuration { diff --git a/src/source/triangle.rs b/src/source/triangle.rs index a35df3af..6cafc911 100644 --- a/src/source/triangle.rs +++ b/src/source/triangle.rs @@ -1,7 +1,7 @@ use crate::common::{ChannelCount, SampleRate}; use crate::math::nz; use crate::source::{Function, SignalGenerator}; -use crate::Source; +use crate::{Sample, Source}; use std::time::Duration; use super::SeekError; @@ -30,10 +30,10 @@ impl TriangleWave { } impl Iterator for TriangleWave { - type Item = f32; + type Item = Sample; #[inline] - fn next(&mut self) -> Option { + fn next(&mut self) -> Option { self.test_tri.next() } } diff --git a/src/spatial_sink.rs b/src/spatial_sink.rs index 15b4a0bb..8a0719c9 100644 --- a/src/spatial_sink.rs +++ b/src/spatial_sink.rs @@ -6,7 +6,7 @@ use dasp_sample::FromSample; use crate::mixer::Mixer; use crate::source::{SeekError, Spatial}; -use crate::{Sink, Source}; +use crate::{Float, Sink, Source}; /// A sink that allows changing the position of the source and the listeners /// ears while playing. The sources played are then transformed to give a simple @@ -82,7 +82,7 @@ impl SpatialSink { /// The value `1.0` is the "normal" volume (unfiltered input). Any value other than 1.0 will /// multiply each sample by this value. #[inline] - pub fn volume(&self) -> f32 { + pub fn volume(&self) -> Float { self.sink.volume() } @@ -91,7 +91,7 @@ impl SpatialSink { /// The value `1.0` is the "normal" volume (unfiltered input). Any value other than 1.0 will /// multiply each sample by this value. #[inline] - pub fn set_volume(&self, value: f32) { + pub fn set_volume(&self, value: Float) { self.sink.set_volume(value); } diff --git a/src/static_buffer.rs b/src/static_buffer.rs index bd5130fe..44ff5998 100644 --- a/src/static_buffer.rs +++ b/src/static_buffer.rs @@ -16,6 +16,7 @@ use std::slice::Iter as SliceIter; use std::time::Duration; use crate::common::{ChannelCount, SampleRate}; +use crate::math::NANOS_PER_SEC; use crate::source::SeekError; use crate::{Sample, Source}; @@ -53,12 +54,12 @@ impl StaticSamplesBuffer { sample_rate: SampleRate, data: &'static [Sample], ) -> StaticSamplesBuffer { - let duration_ns = 1_000_000_000u64.checked_mul(data.len() as u64).unwrap() + let duration_ns = NANOS_PER_SEC.checked_mul(data.len() as u64).unwrap() / sample_rate.get() as u64 / channels.get() as u64; let duration = Duration::new( - duration_ns / 1_000_000_000, - (duration_ns % 1_000_000_000) as u32, + duration_ns / NANOS_PER_SEC, + (duration_ns % NANOS_PER_SEC) as u32, ); StaticSamplesBuffer { diff --git a/src/wav_output.rs b/src/wav_output.rs index bdf3bf03..7a2cdf8e 100644 --- a/src/wav_output.rs +++ b/src/wav_output.rs @@ -130,7 +130,7 @@ impl> Iterator for WholeFrames { #[cfg(test)] mod test { use super::wav_to_file; - use crate::Source; + use crate::{Sample, Source}; use std::io::BufReader; use std::time::Duration; @@ -152,8 +152,8 @@ mod test { assert_eq!(reference.sample_rate().get(), reader.spec().sample_rate); assert_eq!(reference.channels().get(), reader.spec().channels); - let actual_samples: Vec = reader.samples::().map(|x| x.unwrap()).collect(); - let expected_samples: Vec = reference.collect(); + let actual_samples: Vec = reader.samples::().map(|x| x.unwrap()).collect(); + let expected_samples: Vec = reference.collect(); assert!( expected_samples == actual_samples, "wav samples do not match the source" diff --git a/tests/channel_volume.rs b/tests/channel_volume.rs index e04ebeff..9d959362 100644 --- a/tests/channel_volume.rs +++ b/tests/channel_volume.rs @@ -28,13 +28,13 @@ fn channel_volume_with_queue() { } fn assert_output_only_on_first_two_channels( - mut source: impl Source, + source: impl Source, channels: usize, ) { let mut frame_number = 0; let mut samples_in_frame = Vec::new(); - while let Some(sample) = source.next() { + for sample in source { samples_in_frame.push(sample); if samples_in_frame.len() == channels { diff --git a/tests/limit.rs b/tests/limit.rs index e05babdb..f5ce0e6a 100644 --- a/tests/limit.rs +++ b/tests/limit.rs @@ -1,4 +1,5 @@ use rodio::source::Source; +use rodio::Sample; use std::num::NonZero; use std::time::Duration; @@ -16,13 +17,13 @@ fn test_limiting_works() { .with_release(Duration::from_millis(12)); let limiter = sine_wave.limit(settings); - let samples: Vec = limiter.take(2600).collect(); + let samples: Vec = limiter.take(2600).collect(); // After settling, ALL samples should be well below 1.0 (around 0.5) let settled_samples = &samples[1500..]; // After attack/release settling let settled_peak = settled_samples .iter() - .fold(0.0f32, |acc, &x| acc.max(x.abs())); + .fold(0.0, |acc, &x| acc.max(x.abs())); assert!( settled_peak <= 0.6, @@ -35,7 +36,7 @@ fn test_limiting_works() { let max_sample = settled_samples .iter() - .fold(0.0f32, |acc, &x| acc.max(x.abs())); + .fold(0.0, |acc, &x| acc.max(x.abs())); assert!( max_sample < 0.8, "ALL samples should be well below 1.0: max={max_sample:.3}" @@ -51,9 +52,9 @@ fn test_passthrough_below_threshold() { let settings = rodio::source::LimitSettings::default().with_threshold(-6.0); - let original_samples: Vec = sine_wave.clone().take(880).collect(); + let original_samples: Vec = sine_wave.clone().take(880).collect(); let limiter = sine_wave.limit(settings); - let limited_samples: Vec = limiter.take(880).collect(); + let limited_samples: Vec = limiter.take(880).collect(); // Samples should be nearly identical since below threshold for (orig, limited) in original_samples.iter().zip(limited_samples.iter()) { @@ -86,13 +87,13 @@ fn test_limiter_with_different_settings() { .with_release(Duration::from_millis(10)); let limiter = sine_wave.limit(settings); - let samples: Vec = limiter.take(2000).collect(); + let samples: Vec = limiter.take(2000).collect(); // Check settled samples after attack/release let settled_samples = &samples[1000..]; let peak = settled_samples .iter() - .fold(0.0f32, |acc, &x| acc.max(x.abs())); + .fold(0.0, |acc, &x| acc.max(x.abs())); assert!( peak <= expected_peak + 0.1, @@ -118,10 +119,10 @@ fn test_limiter_stereo_processing() { // Create stereo test signal - left channel louder than right let left_samples = (0..1000) - .map(|i| (i as f32 * 0.01).sin() * 1.5) + .map(|i| (i as Sample * 0.01).sin() * 1.5) .collect::>(); let right_samples = (0..1000) - .map(|i| (i as f32 * 0.01).sin() * 0.8) + .map(|i| (i as Sample * 0.01).sin() * 0.8) .collect::>(); let mut stereo_samples = Vec::new(); @@ -138,16 +139,16 @@ fn test_limiter_stereo_processing() { let settings = rodio::source::LimitSettings::default().with_threshold(-3.0); let limiter = buffer.limit(settings); - let limited_samples: Vec = limiter.collect(); + let limited_samples: Vec = limiter.collect(); // Extract left and right channels after limiting - let limited_left: Vec = limited_samples.iter().step_by(2).cloned().collect(); - let limited_right: Vec = limited_samples.iter().skip(1).step_by(2).cloned().collect(); + let limited_left: Vec = limited_samples.iter().step_by(2).cloned().collect(); + let limited_right: Vec = limited_samples.iter().skip(1).step_by(2).cloned().collect(); - let left_peak = limited_left.iter().fold(0.0f32, |acc, &x| acc.max(x.abs())); + let left_peak = limited_left.iter().fold(0.0, |acc, &x| acc.max(x.abs())); let right_peak = limited_right .iter() - .fold(0.0f32, |acc, &x| acc.max(x.abs())); + .fold(0.0, |acc, &x| acc.max(x.abs())); // Both channels should be limited to approximately the same level // (limiter should prevent the louder channel from exceeding threshold) diff --git a/tests/seek.rs b/tests/seek.rs index 7a96eeb2..0886788f 100644 --- a/tests/seek.rs +++ b/tests/seek.rs @@ -3,13 +3,17 @@ #[cfg(feature = "symphonia-mp3")] use rodio::{decoder::symphonia, source::SeekError}; -use rodio::{ChannelCount, Decoder, Source}; +use rodio::{ChannelCount, Decoder, Sample, Source}; use rstest::rstest; use rstest_reuse::{self, *}; use std::io::{Read, Seek}; use std::path::Path; use std::time::Duration; +// Test constants +const BASICALLY_ZERO: Sample = 0.0001; +const ONE_SECOND: Duration = Duration::from_secs(1); + #[cfg(any( feature = "claxon", feature = "minimp3", @@ -118,7 +122,7 @@ fn seek_beyond_end_saturates(#[case] format: &'static str, #[case] decoder_name: let res = decoder.try_seek(Duration::from_secs(999)); assert!(res.is_ok(), "err: {res:?}"); - assert!(time_remaining(decoder) < Duration::from_secs(1)); + assert!(time_remaining(decoder) < ONE_SECOND); } #[cfg(any( @@ -255,7 +259,7 @@ fn random_access_seeks() { ); assert!( matches!( - decoder.try_seek(Duration::from_secs(1)), + decoder.try_seek(ONE_SECOND), Err(SeekError::SymphoniaDecoder( symphonia::SeekError::RandomAccessNotSupported, )) @@ -270,14 +274,14 @@ fn random_access_seeks() { "forward seek should work with byte_len" ); assert!( - decoder.try_seek(Duration::from_secs(1)).is_ok(), + decoder.try_seek(ONE_SECOND).is_ok(), "backward seek should work with byte_len" ); } fn second_channel_beep_range(source: &mut R) -> std::ops::Range { let channels = source.channels().get() as usize; - let samples: Vec = source.by_ref().collect(); + let samples: Vec = source.by_ref().collect(); const WINDOW: usize = 50; let beep_starts = samples @@ -289,14 +293,13 @@ fn second_channel_beep_range(source: &mut R) -> std::ops::Rang .skip(1) .step_by(channels) .map(|s| s.abs()) - .sum::() + .sum::() > 0.1 }) .expect("track should not be silent") .0 .next_multiple_of(channels); - const BASICALLY_ZERO: f32 = 0.0001; let beep_ends = samples .chunks_exact(WINDOW) .enumerate() @@ -325,16 +328,15 @@ fn second_channel_beep_range(source: &mut R) -> std::ops::Rang beep_starts..beep_ends } -fn is_silent(samples: &[f32], channels: ChannelCount, channel: usize) -> bool { +fn is_silent(samples: &[Sample], channels: ChannelCount, channel: usize) -> bool { assert_eq!(samples.len(), 100); let channel = samples .iter() .skip(channel) .step_by(channels.get() as usize); let volume = - channel.map(|s| s.abs()).sum::() / samples.len() as f32 * channels.get() as f32; + channel.map(|s| s.abs()).sum::() / samples.len() as Sample * channels.get() as Sample; - const BASICALLY_ZERO: f32 = 0.0001; volume < BASICALLY_ZERO } From 20c07659b49bc8b139d2b9b4088ade028b459402 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 28 Dec 2025 20:41:45 +0100 Subject: [PATCH 2/3] refactor: clarify docs and clean up tests --- benches/effects.rs | 2 +- src/buffer.rs | 5 +++-- src/math.rs | 14 +++++++------- src/microphone/config.rs | 2 +- src/source/linear_ramp.rs | 5 ++--- src/source/noise.rs | 20 +++++--------------- src/source/signal_generator.rs | 9 ++++----- src/source/skip.rs | 2 +- src/wav_output.rs | 23 +++++++++++++++++------ tests/channel_volume.rs | 5 +---- tests/limit.rs | 18 +++++------------- tests/seek.rs | 4 ++-- 12 files changed, 49 insertions(+), 60 deletions(-) diff --git a/benches/effects.rs b/benches/effects.rs index 0e5a6fac..ac423911 100644 --- a/benches/effects.rs +++ b/benches/effects.rs @@ -57,7 +57,7 @@ fn agc_enabled(bencher: Bencher) { fn agc_disabled(bencher: Bencher) { bencher.with_inputs(music_wav).bench_values(|source| { // Create the AGC source - let amplified_source = source.automatic_gain_control(Default::default); + let amplified_source = source.automatic_gain_control(Default::default()); // Get the control handle and disable AGC let agc_control = amplified_source.get_agc_control(); diff --git a/src/buffer.rs b/src/buffer.rs index 3142934b..187a6b79 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -104,8 +104,9 @@ impl Source for SamplesBuffer { // sample directly. let curr_channel = self.pos % self.channels().get() as usize; - let new_pos = - duration_to_float(pos) * self.sample_rate().get() as Float * self.channels().get() as Float; + let new_pos = duration_to_float(pos) + * self.sample_rate().get() as Float + * self.channels().get() as Float; // saturate pos at the end of the source let new_pos = new_pos as usize; let new_pos = new_pos.min(self.data.len()); diff --git a/src/math.rs b/src/math.rs index 1860f106..fffe1de1 100644 --- a/src/math.rs +++ b/src/math.rs @@ -9,9 +9,9 @@ pub(crate) const NANOS_PER_SEC: u64 = 1_000_000_000; // Re-export float constants with appropriate precision for the Float type. // This centralizes all cfg gating for constants in one place. #[cfg(not(feature = "64bit"))] -pub use std::f32::consts::{E, LN_2, LN_10, LOG2_10, LOG2_E, LOG10_2, LOG10_E, PI, TAU}; +pub use std::f32::consts::{E, LN_10, LN_2, LOG10_2, LOG10_E, LOG2_10, LOG2_E, PI, TAU}; #[cfg(feature = "64bit")] -pub use std::f64::consts::{E, LN_2, LN_10, LOG2_10, LOG2_E, LOG10_2, LOG10_E, PI, TAU}; +pub use std::f64::consts::{E, LN_10, LN_2, LOG10_2, LOG10_E, LOG2_10, LOG2_E, PI, TAU}; /// Linear interpolation between two samples. /// @@ -255,21 +255,21 @@ mod test { #[test] fn convert_linear_to_decibels() { // Test the inverse conversion function using the same reference data - for (wikipedia_db, linear) in DECIBELS_LINEAR_TABLE { + for (expected_db, linear) in DECIBELS_LINEAR_TABLE { let actual_db = linear_to_db(linear); // Sanity check: ensure we're reasonably close to the expected dB value from the table // This accounts for rounding in both the linear and dB reference values - let magnitude_ratio = if wikipedia_db.abs() > 10.0 * Float::EPSILON { - actual_db / wikipedia_db + let magnitude_ratio = if expected_db.abs() > 10.0 * Float::EPSILON { + actual_db / expected_db } else { 1.0 // Skip ratio check for values very close to 0 dB }; - if wikipedia_db.abs() > 10.0 * Float::EPSILON { + if expected_db.abs() > 10.0 * Float::EPSILON { assert!( magnitude_ratio > 0.99 && magnitude_ratio < 1.01, - "Result differs significantly from table reference for linear {linear}: expected {wikipedia_db}dB, got {actual_db}dB, ratio: {magnitude_ratio:.4}" + "Result differs significantly from table reference for linear {linear}: expected {expected_db}dB, got {actual_db}dB, ratio: {magnitude_ratio:.4}" ); } } diff --git a/src/microphone/config.rs b/src/microphone/config.rs index 923a77d4..5c33a513 100644 --- a/src/microphone/config.rs +++ b/src/microphone/config.rs @@ -12,7 +12,7 @@ pub struct InputConfig { /// The buffersize, see a thorough explanation in MicrophoneBuilder::with_buffer_size pub buffer_size: cpal::BufferSize, /// The sample format used by the microphone. - /// Note we will always convert it to f32 + /// Note we will always convert it to `Sample` pub sample_format: cpal::SampleFormat, } impl InputConfig { diff --git a/src/source/linear_ramp.rs b/src/source/linear_ramp.rs index b39d5f94..453f5855 100644 --- a/src/source/linear_ramp.rs +++ b/src/source/linear_ramp.rs @@ -90,9 +90,8 @@ where } if self.sample_idx.is_multiple_of(self.channels().get() as u64) { - let sample_duration = Duration::from_nanos( - NANOS_PER_SEC / self.input.sample_rate().get() as u64 - ); + let sample_duration = + Duration::from_nanos(NANOS_PER_SEC / self.input.sample_rate().get() as u64); self.elapsed += sample_duration; } diff --git a/src/source/noise.rs b/src/source/noise.rs index c53376b3..32cea2d9 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -154,7 +154,7 @@ impl WhiteUniform { /// Get the standard deviation of the uniform distribution. /// /// For uniform distribution [-1.0, 1.0], std_dev = √(1/3) ≈ 0.5774. - pub fn std_dev(&self) -> f32 { + pub fn std_dev(&self) -> Sample { UNIFORM_VARIANCE.sqrt() } } @@ -209,8 +209,8 @@ impl WhiteTriangular { /// Get the standard deviation of the triangular distribution. /// /// For triangular distribution [-1.0, 1.0] with mode 0.0, std_dev = 2/√6 ≈ 0.8165. - pub fn std_dev(&self) -> f32 { - 2.0 / (6.0_f32).sqrt() + pub fn std_dev(&self) -> Sample { + 2.0 / Sample::sqrt(6.0) } } @@ -416,7 +416,7 @@ const VELVET_DEFAULT_DENSITY: NonZero = nz!(2000); /// Variance of uniform distribution [-1.0, 1.0]. /// /// For uniform distribution U(-1, 1), the variance is (b-a)²/12 = 4/12 = 1/3. -const UNIFORM_VARIANCE: f32 = 1.0 / 3.0; +const UNIFORM_VARIANCE: Sample = 1.0 / 3.0; /// Pink noise generator - sounds much more natural than white noise. /// @@ -645,8 +645,7 @@ where // Center frequency is set to 5Hz, which provides good behavior // while preventing excessive low-frequency buildup across common sample rates. let center_freq_hz = 5.0; - let leak_factor = - 1.0 - ((2.0 * PI * center_freq_hz) / sample_rate.get() as Float); + let leak_factor = 1.0 - ((2.0 * PI * center_freq_hz) / sample_rate.get() as Float); // Calculate the scaling factor to normalize output based on leak factor. // This ensures consistent output level regardless of the leak factor value. @@ -1044,15 +1043,6 @@ mod tests { near_zero_count > total_samples / 2, "Triangular distribution should favor values near zero" ); - - // Test std_dev method - 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, - "Triangular std_dev should be 2/sqrt(6) ≈ 0.8165, got {}", - generator.std_dev() - ); } #[test] diff --git a/src/source/signal_generator.rs b/src/source/signal_generator.rs index e1fb7a67..34ffa443 100644 --- a/src/source/signal_generator.rs +++ b/src/source/signal_generator.rs @@ -1,8 +1,7 @@ //! Generator sources for various periodic test waveforms. //! //! This module provides several periodic, deterministic waveforms for testing other sources and -//! for simple additive sound synthesis. Every source is monoaural and in the codomain [-1.0f32, -//! 1.0f32]. +//! for simple additive sound synthesis. Every source is monoaural and in the codomain [-1.0, 1.0]. //! //! # Example //! @@ -176,7 +175,7 @@ mod tests { #[test] fn square() { - let mut wf = SignalGenerator::new(nz!(2000), 500.0f32, Function::Square); + let mut wf = SignalGenerator::new(nz!(2000), 500.0, Function::Square); assert_eq!(wf.next(), Some(1.0)); assert_eq!(wf.next(), Some(1.0)); assert_eq!(wf.next(), Some(-1.0)); @@ -189,7 +188,7 @@ mod tests { #[test] fn triangle() { - let mut wf = SignalGenerator::new(nz!(8000), 1000.0f32, Function::Triangle); + let mut wf = SignalGenerator::new(nz!(8000), 1000.0, Function::Triangle); assert_eq!(wf.next(), Some(-1.0)); assert_eq!(wf.next(), Some(-0.5)); assert_eq!(wf.next(), Some(0.0)); @@ -210,7 +209,7 @@ mod tests { #[test] fn saw() { - let mut wf = SignalGenerator::new(nz!(200), 50.0f32, Function::Sawtooth); + let mut wf = SignalGenerator::new(nz!(200), 50.0, Function::Sawtooth); assert_eq!(wf.next(), Some(0.0)); assert_eq!(wf.next(), Some(0.5)); assert_eq!(wf.next(), Some(-1.0)); diff --git a/src/source/skip.rs b/src/source/skip.rs index 95f1d49d..dfae5b92 100644 --- a/src/source/skip.rs +++ b/src/source/skip.rs @@ -169,8 +169,8 @@ mod tests { use crate::buffer::SamplesBuffer; use crate::common::{ChannelCount, SampleRate}; use crate::math::nz; - use crate::Sample; use crate::source::Source; + use crate::Sample; use dasp_sample::Sample as DaspSample; fn test_skip_duration_samples_left( diff --git a/src/wav_output.rs b/src/wav_output.rs index 7a2cdf8e..981be50d 100644 --- a/src/wav_output.rs +++ b/src/wav_output.rs @@ -1,6 +1,7 @@ use crate::common::assert_error_traits; use crate::Sample; use crate::Source; +use dasp_sample::Sample as DaspSample; use hound::{SampleFormat, WavSpec}; use std::io::{self, Write}; use std::path; @@ -77,7 +78,7 @@ pub fn wav_to_writer( let whole_frames = WholeFrames::new(source); for sample in whole_frames { writer - .write_sample(sample) + .write_sample(sample.to_sample::()) .map_err(Arc::new) .map_err(ToWavError::Writing)?; } @@ -131,6 +132,7 @@ impl> Iterator for WholeFrames { mod test { use super::wav_to_file; use crate::{Sample, Source}; + use dasp_sample::Sample as DaspSample; use std::io::BufReader; use std::time::Duration; @@ -152,11 +154,20 @@ mod test { assert_eq!(reference.sample_rate().get(), reader.spec().sample_rate); assert_eq!(reference.channels().get(), reader.spec().channels); - let actual_samples: Vec = reader.samples::().map(|x| x.unwrap()).collect(); - let expected_samples: Vec = reference.collect(); - assert!( - expected_samples == actual_samples, - "wav samples do not match the source" + // WAV files always use f32 samples (hound limitation) + let actual_samples: Vec = reader.samples::().map(|x| x.unwrap()).collect(); + let expected_samples: Vec = reference + .into_iter() + .map(|s| s.to_sample::()) + .collect(); + + assert_eq!( + actual_samples.len(), + expected_samples.len(), + "sample counts should match" ); + for (actual, expected) in actual_samples.iter().zip(expected_samples.iter()) { + assert_eq!(actual, expected, "wav samples do not match the source"); + } } } diff --git a/tests/channel_volume.rs b/tests/channel_volume.rs index 9d959362..0c8b39ed 100644 --- a/tests/channel_volume.rs +++ b/tests/channel_volume.rs @@ -27,10 +27,7 @@ fn channel_volume_with_queue() { assert_output_only_on_first_two_channels(queue, 6); } -fn assert_output_only_on_first_two_channels( - source: impl Source, - channels: usize, -) { +fn assert_output_only_on_first_two_channels(source: impl Source, channels: usize) { let mut frame_number = 0; let mut samples_in_frame = Vec::new(); diff --git a/tests/limit.rs b/tests/limit.rs index f5ce0e6a..629fb209 100644 --- a/tests/limit.rs +++ b/tests/limit.rs @@ -21,9 +21,7 @@ fn test_limiting_works() { // After settling, ALL samples should be well below 1.0 (around 0.5) let settled_samples = &samples[1500..]; // After attack/release settling - let settled_peak = settled_samples - .iter() - .fold(0.0, |acc, &x| acc.max(x.abs())); + let settled_peak: Sample = settled_samples.iter().fold(0.0, |acc, &x| acc.max(x.abs())); assert!( settled_peak <= 0.6, @@ -34,9 +32,7 @@ fn test_limiting_works() { "Peak should be reasonably close to 0.5: {settled_peak:.3}" ); - let max_sample = settled_samples - .iter() - .fold(0.0, |acc, &x| acc.max(x.abs())); + let max_sample: Sample = settled_samples.iter().fold(0.0, |acc, &x| acc.max(x.abs())); assert!( max_sample < 0.8, "ALL samples should be well below 1.0: max={max_sample:.3}" @@ -91,9 +87,7 @@ fn test_limiter_with_different_settings() { // Check settled samples after attack/release let settled_samples = &samples[1000..]; - let peak = settled_samples - .iter() - .fold(0.0, |acc, &x| acc.max(x.abs())); + let peak: Sample = settled_samples.iter().fold(0.0, |acc, &x| acc.max(x.abs())); assert!( peak <= expected_peak + 0.1, @@ -145,10 +139,8 @@ fn test_limiter_stereo_processing() { let limited_left: Vec = limited_samples.iter().step_by(2).cloned().collect(); let limited_right: Vec = limited_samples.iter().skip(1).step_by(2).cloned().collect(); - let left_peak = limited_left.iter().fold(0.0, |acc, &x| acc.max(x.abs())); - let right_peak = limited_right - .iter() - .fold(0.0, |acc, &x| acc.max(x.abs())); + let left_peak: Sample = limited_left.iter().fold(0.0, |acc, &x| acc.max(x.abs())); + let right_peak: Sample = limited_right.iter().fold(0.0, |acc, &x| acc.max(x.abs())); // Both channels should be limited to approximately the same level // (limiter should prevent the louder channel from exceeding threshold) diff --git a/tests/seek.rs b/tests/seek.rs index 0886788f..d2344669 100644 --- a/tests/seek.rs +++ b/tests/seek.rs @@ -334,8 +334,8 @@ fn is_silent(samples: &[Sample], channels: ChannelCount, channel: usize) -> bool .iter() .skip(channel) .step_by(channels.get() as usize); - let volume = - channel.map(|s| s.abs()).sum::() / samples.len() as Sample * channels.get() as Sample; + let volume = channel.map(|s| s.abs()).sum::() / samples.len() as Sample + * channels.get() as Sample; volume < BASICALLY_ZERO } From ed4af6dbc5cd99239166161ac027418ac0fde33e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 30 Dec 2025 21:10:42 +0100 Subject: [PATCH 3/3] docs: add note on f32 vs f64 precision --- src/common.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/common.rs b/src/common.rs index 0c76e67e..f6da20eb 100644 --- a/src/common.rs +++ b/src/common.rs @@ -10,6 +10,21 @@ pub type ChannelCount = NonZero; /// Number of bits per sample. Can never be zero. pub type BitDepth = NonZero; +// NOTE on numeric precision: +// +// While `f32` is transparent for typical playback use cases, it does not guarantee preservation of +// full 24-bit source fidelity across arbitrary processing chains. Each floating-point operation +// rounds its result to `f32` precision (~24-bit significand). In DSP pipelines (filters, mixing, +// modulation), many operations are applied per sample and over time, so rounding noise accumulates +// and long-running state (e.g. oscillator phase) can drift. +// +// For use cases where numerical accuracy must be preserved through extended processing (recording, +// editing, analysis, long-running generators, or complex DSP graphs), enabling 64-bit processing +// reduces accumulated rounding error and drift. +// +// This mirrors common practice in professional audio software and DSP libraries, which often use +// 64-bit internal processing even when the final output is 16- or 24-bit. + /// Floating point type used for internal calculations. Can be configured to be /// either `f32` (default) or `f64` using the `64bit` feature flag. #[cfg(not(feature = "64bit"))]