diff --git a/CHANGELOG.md b/CHANGELOG.md index 80a71d44..bbd6843a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `OutputStreamConfig::channel_count()`, `OutputStreamConfig::sample_rate()`, `OutputStreamConfig::buffer_size()` and `OutputStreamConfig::sample_format()` getters to access an `OutputStreamConfig`'s channel count, sample rate, buffer size and sample format values. +- Added `Source::limit()` method for limiting the maximum amplitude of a source. ### Changed - Breaking: `OutputStreamBuilder` should now be used to initialize an audio output stream. diff --git a/Cargo.toml b/Cargo.toml index 35034392..c3bef7df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -132,6 +132,10 @@ required-features = ["playback"] name = "into_file" required-features = ["mp3", "wav"] +[[example]] +name = "limit_wav" +required-features = ["playback", "wav"] + [[example]] name = "low_pass" required-features = ["playback", "wav"] diff --git a/examples/limit_settings.rs b/examples/limit_settings.rs new file mode 100644 index 00000000..9e79642b --- /dev/null +++ b/examples/limit_settings.rs @@ -0,0 +1,149 @@ +//! Example demonstrating the new LimitSettings API for audio limiting. +//! +//! This example shows how to use the LimitSettings struct with the builder +//! to configure audio limiting parameters. + +use rodio::source::{LimitSettings, SineWave, Source}; +use std::time::Duration; + +fn main() { + println!("Example 1: Default LimitSettings"); + let default_limiting = LimitSettings::default(); + println!(" Threshold: {} dB", default_limiting.threshold); + println!(" Knee width: {} dB", default_limiting.knee_width); + println!(" Attack: {:?}", default_limiting.attack); + println!(" Release: {:?}", default_limiting.release); + println!(); + + println!("Example 2: Custom LimitSettings with builder pattern"); + let custom_limiting = LimitSettings::new() + .with_threshold(-3.0) + .with_knee_width(2.0) + .with_attack(Duration::from_millis(10)) + .with_release(Duration::from_millis(50)); + + println!(" Threshold: {} dB", custom_limiting.threshold); + println!(" Knee width: {} dB", custom_limiting.knee_width); + println!(" Attack: {:?}", custom_limiting.attack); + println!(" Release: {:?}", custom_limiting.release); + println!(); + + println!("Example 3: Applying limiter to a sine wave with default settings"); + + // Create a sine wave at 440 Hz + let sine_wave = SineWave::new(440.0) + .amplify(2.0) // Amplify to cause limiting + .take_duration(Duration::from_millis(100)); + + // Apply limiting with default settings (simplest usage) + let limited_wave = sine_wave.limit(LimitSettings::default()); + + // Collect some samples to demonstrate + 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())); + println!(" Peak amplitude after limiting: {:.3}", max_sample); + println!(); + + println!("Example 4: Custom settings with builder pattern"); + + // Create another sine wave for custom limiting + let sine_wave2 = SineWave::new(880.0) + .amplify(1.8) + .take_duration(Duration::from_millis(50)); + + // Apply the custom settings from Example 2 + let custom_limited = sine_wave2.limit(custom_limiting); + let custom_samples: Vec = custom_limited.take(50).collect(); + println!( + " Generated {} samples with custom settings", + custom_samples.len() + ); + println!(); + + println!("Example 5: Comparing different limiting scenarios"); + + let gentle_limiting = LimitSettings::default() + .with_threshold(-6.0) // Higher threshold (less limiting) + .with_knee_width(8.0) // Wide knee (softer) + .with_attack(Duration::from_millis(20)) // Slower attack + .with_release(Duration::from_millis(200)); // Slower release + + let aggressive_limiting = LimitSettings::default() + .with_threshold(-1.0) // Lower threshold (more limiting) + .with_knee_width(1.0) // Narrow knee (harder) + .with_attack(Duration::from_millis(2)) // Fast attack + .with_release(Duration::from_millis(20)); // Fast release + + println!(" Gentle limiting:"); + println!( + " Threshold: {} dB, Knee: {} dB", + gentle_limiting.threshold, gentle_limiting.knee_width + ); + println!( + " Attack: {:?}, Release: {:?}", + gentle_limiting.attack, gentle_limiting.release + ); + + println!(" Aggressive limiting:"); + println!( + " Threshold: {} dB, Knee: {} dB", + aggressive_limiting.threshold, aggressive_limiting.knee_width + ); + println!( + " Attack: {:?}, Release: {:?}", + aggressive_limiting.attack, aggressive_limiting.release + ); + println!(); + + 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 + let test_sine = SineWave::new(440.0) + .amplify(AMPLITUDE) + .take_duration(Duration::from_millis(100)); // 100ms = ~4410 samples + + // Apply limiting with -6dB threshold (should limit to ~0.5) + let strict_limiting = LimitSettings::default() + .with_threshold(-6.0) + .with_knee_width(0.5) // Narrow knee for precise limiting + .with_attack(Duration::from_millis(3)) // Fast attack + .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(); + + // Analyze peaks at different time periods + let early_peak = test_samples[0..500] + .iter() + .fold(0.0f32, |acc, &x| acc.max(x.abs())); + let mid_peak = test_samples[1000..1500] + .iter() + .fold(0.0f32, |acc, &x| acc.max(x.abs())); + let settled_peak = test_samples[2000..] + .iter() + .fold(0.0f32, |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..] + .iter() + .fold(0.0f32, |acc, &x| acc.max(x.abs())); + + println!( + " {}dB threshold limiting results:", + strict_limiting.threshold + ); + println!(" Original max amplitude: {AMPLITUDE}"); + println!(" Target threshold: {:.3}", target_linear); + println!(" Early peak (0-500 samples): {:.3}", early_peak); + println!(" Mid peak (1000-1500 samples): {:.3}", mid_peak); + println!(" Settled peak (2000+ samples): {:.3}", settled_peak); + println!( + " ALL samples now well below 1.0: max = {:.3}", + max_settled + ); +} diff --git a/examples/limit_wav.rs b/examples/limit_wav.rs new file mode 100644 index 00000000..b104d3ff --- /dev/null +++ b/examples/limit_wav.rs @@ -0,0 +1,20 @@ +use rodio::{source::LimitSettings, Source}; +use std::error::Error; + +fn main() -> Result<(), Box> { + let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; + let sink = rodio::Sink::connect_new(stream_handle.mixer()); + + let file = std::fs::File::open("assets/music.wav")?; + let source = rodio::Decoder::try_from(file)? + .amplify(3.0) + .limit(LimitSettings::default()); + + sink.append(source); + + println!("Playing music.wav with limiting until finished..."); + sink.sleep_until_end(); + println!("Done."); + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 87eb043b..4cab8f00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -167,7 +167,6 @@ pub use cpal::{ }; mod common; -mod math; mod sink; mod spatial_sink; #[cfg(feature = "playback")] @@ -178,6 +177,7 @@ mod wav_output; pub mod buffer; pub mod conversions; pub mod decoder; +pub mod math; pub mod mixer; pub mod queue; pub mod source; diff --git a/src/math.rs b/src/math.rs index 981ae7dd..7227ef80 100644 --- a/src/math.rs +++ b/src/math.rs @@ -1,3 +1,5 @@ +//! Math utilities for audio processing. + /// Linear interpolation between two samples. /// /// The result should be equivalent to @@ -6,10 +8,74 @@ /// To avoid numeric overflows pick smaller numerator. // TODO (refactoring) Streamline this using coefficient instead of numerator and denominator. #[inline] -pub fn lerp(first: &f32, second: &f32, numerator: u32, denominator: u32) -> f32 { +pub(crate) fn lerp(first: &f32, second: &f32, numerator: u32, denominator: u32) -> f32 { first + (second - first) * numerator as f32 / denominator as f32 } +/// Converts decibels to linear amplitude scale. +/// +/// This function converts a decibel value to its corresponding linear amplitude value +/// using the formula: `linear = 10^(decibels/20)` for amplitude. +/// +/// # Arguments +/// +/// * `decibels` - The decibel value to convert. Common ranges: +/// - 0 dB = linear value of 1.0 (no change) +/// - Positive dB values represent amplification (> 1.0) +/// - Negative dB values represent attenuation (< 1.0) +/// - -60 dB ≈ 0.001 (barely audible) +/// - +20 dB = 10.0 (10x amplification) +/// +/// # Returns +/// +/// The linear amplitude value corresponding to the input decibels. +/// +/// # Performance +/// +/// This implementation is optimized for speed, being ~3-4% faster than the standard +/// `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 { + // ~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) +} + +/// Converts linear amplitude scale to decibels. +/// +/// This function converts a linear amplitude value to its corresponding decibel value +/// using the formula: `decibels = 20 * log10(linear)` for amplitude. +/// +/// # Arguments +/// +/// * `linear` - The linear amplitude value to convert. Must be positive for meaningful results: +/// - 1.0 = 0 dB (no change) +/// - Values > 1.0 represent amplification (positive dB) +/// - Values < 1.0 represent attenuation (negative dB) +/// - 0.0 results in negative infinity +/// - Negative values are not physically meaningful for amplitude +/// +/// # Returns +/// +/// The decibel value corresponding to the input linear amplitude. +/// +/// # Performance +/// +/// This implementation is optimized for speed, being faster than the standard +/// `20.0 * linear.log10()` approach while maintaining high precision. +/// +/// # Special Cases +/// +/// - `linear_to_db(0.0)` returns negative infinity +/// - 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 { + // Same as `to_linear`: faster than using `20f32.log10() * linear` + linear.log2() * std::f32::consts::LOG10_2 * 20.0 +} + #[cfg(test)] mod test { use super::*; @@ -34,4 +100,174 @@ mod test { TestResult::from_bool((x - reference).abs() < 0.01) } } + + /// Tolerance values for precision tests, derived from empirical measurement + /// of actual implementation errors across the full ±100dB range. + /// + /// Methodology: + /// 1. Calculated relative errors against mathematically exact `f64` calculations + /// 2. Found maximum errors: dB->linear = 2.3x ε, linear->dB = 1.0x ε, round-trip = 8x ε + /// 3. Applied 2x safety margins for cross-platform robustness + /// 4. All tolerances are much stricter than audio precision requirements: + /// - 16-bit audio: ~6e-6 precision needed + /// - 24-bit audio: ~6e-8 precision needed + /// - Our tolerances: ~6e-7 to 2e-6 (10-1000x better than audio needs) + /// + /// Range context: + /// - Practical audio range (-60dB to +40dB): max errors ~1x ε + /// - Extended range (-100dB to +100dB): max errors ~2.3x ε + /// - Extreme edge cases beyond ±100dB have larger errors but are rarely used + + /// 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] = [ + (100., 100000.), + (90., 31623.), + (80., 10000.), + (70., 3162.), + (60., 1000.), + (50., 316.2), + (40., 100.), + (30., 31.62), + (20., 10.), + (10., 3.162), + (5.998, 1.995), + (3.003, 1.413), + (1.002, 1.122), + (0., 1.), + (-1.002, 0.891), + (-3.003, 0.708), + (-5.998, 0.501), + (-10., 0.3162), + (-20., 0.1), + (-30., 0.03162), + (-40., 0.01), + (-50., 0.003162), + (-60., 0.001), + (-70., 0.0003162), + (-80., 0.0001), + (-90., 0.00003162), + (-100., 0.00001), + ]; + + #[test] + fn convert_decibels_to_linear() { + 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: exact {:.8}, got {:.8}, relative error: {:.2e}", + db, exact_linear, actual_linear, relative_error + ); + + // 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; + assert!( + magnitude_ratio > 0.99 && magnitude_ratio < 1.01, + "Result magnitude differs significantly from Wikipedia reference for {}dB: Wikipedia {}, got {}, ratio: {:.4}", + db, wikipedia_linear, actual_linear, magnitude_ratio + ); + } + } + + #[test] + fn convert_linear_to_decibels() { + // Test the inverse conversion function using the same reference data + for (expected_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 {}: exact {:.8}, got {:.8}, relative error: {:.2e}", + linear, exact_db, actual_db, relative_error + ); + } 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 {}: exact {:.8}, got {:.8}, absolute error: {:.2e}", + linear, exact_db, actual_db, absolute_error + ); + } + + // 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 + } else { + 1.0 // Skip ratio check for values very close to 0 dB + }; + + if expected_db.abs() > 10.0 * f32::EPSILON { + assert!( + magnitude_ratio > 0.99 && magnitude_ratio < 1.01, + "Result differs significantly from table reference for linear {}: expected {}dB, got {}dB, ratio: {:.4}", + linear, expected_db, actual_db, magnitude_ratio + ); + } + } + } + + #[test] + fn round_trip_conversion_accuracy() { + // Test that converting dB -> linear -> dB gives back the original value + let test_db_values = [-60.0, -20.0, -6.0, 0.0, 6.0, 20.0, 40.0]; + + for &original_db in &test_db_values { + let linear = db_to_linear(original_db); + 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 + + assert!( + error < MAX_ROUND_TRIP_ERROR, + "Round-trip conversion failed for {}dB: got {:.8}dB, error: {:.2e}", + original_db, + round_trip_db, + error + ); + } + + // Test that converting linear -> dB -> linear gives back the original value + let test_linear_values = [0.001, 0.1, 1.0, 10.0, 100.0]; + + for &original_linear in &test_linear_values { + let db = linear_to_db(original_linear); + 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 + + assert!( + relative_error < MAX_ROUND_TRIP_RELATIVE_ERROR, + "Round-trip conversion failed for {}: got {:.8}, relative error: {:.2e}", + original_linear, + round_trip_linear, + relative_error + ); + } + } } diff --git a/src/source/amplify.rs b/src/source/amplify.rs index 9c3556f1..985c6be3 100644 --- a/src/source/amplify.rs +++ b/src/source/amplify.rs @@ -1,8 +1,10 @@ use std::time::Duration; use super::SeekError; -use crate::common::{ChannelCount, SampleRate}; -use crate::Source; +use crate::{ + common::{ChannelCount, SampleRate}, + math, Source, +}; /// Internal function that builds a `Amplify` object. pub fn amplify(input: I, factor: f32) -> Amplify @@ -12,11 +14,6 @@ where Amplify { input, factor } } -/// Internal function that converts decibels to linear -pub(super) fn to_linear(decibels: f32) -> f32 { - f32::powf(10f32, decibels * 0.05) -} - /// Filter that modifies each sample by a given value. #[derive(Clone, Debug)] pub struct Amplify { @@ -34,7 +31,7 @@ impl Amplify { /// Modifies the amplification factor logarithmically. #[inline] pub fn set_log_factor(&mut self, factor: f32) { - self.factor = to_linear(factor); + self.factor = math::db_to_linear(factor); } /// Returns a reference to the inner source. @@ -104,53 +101,3 @@ where self.input.try_seek(pos) } } - -#[cfg(test)] -mod test { - use super::*; - /// 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] = [ - (100., 100000.), - (90., 31623.), - (80., 10000.), - (70., 3162.), - (60., 1000.), - (50., 316.2), - (40., 100.), - (30., 31.62), - (20., 10.), - (10., 3.162), - (5.998, 1.995), - (3.003, 1.413), - (1.002, 1.122), - (0., 1.), - (-1.002, 0.891), - (-3.003, 0.708), - (-5.998, 0.501), - (-10., 0.3162), - (-20., 0.1), - (-30., 0.03162), - (-40., 0.01), - (-50., 0.003162), - (-60., 0.001), - (-70., 0.0003162), - (-80., 0.0001), - (-90., 0.00003162), - (-100., 0.00001), - ]; - - #[test] - fn convert_decibels_to_linear() { - for (db, linear) in DECIBELS_LINEAR_TABLE { - const PRECISION: f32 = 5.066e3; - let to_linear = to_linear(db); - - assert!( - 2.0 * (to_linear - linear).abs() - < PRECISION * f32::EPSILON * (to_linear.abs() + linear.abs()) - ); - } - } -} diff --git a/src/source/limit.rs b/src/source/limit.rs new file mode 100644 index 00000000..1952c1a2 --- /dev/null +++ b/src/source/limit.rs @@ -0,0 +1,1174 @@ +//! Audio peak limiting for dynamic range control. +//! +//! This module implements a feedforward limiter that prevents audio peaks from exceeding +//! a specified threshold while maintaining audio quality. The limiter is based on: +//! Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic Range Compressor Design, +//! A Tutorial and Analysis. Journal of The Audio Engineering Society, 60, 399-408. +//! +//! # What is Limiting? +//! +//! A limiter reduces the amplitude of audio signals that exceed a threshold level. +//! For example, with a -6dB threshold, peaks above that level are reduced to stay near the +//! threshold, preventing clipping and maintaining consistent output levels. +//! +//! # Features +//! +//! * **Soft-knee limiting** - Gradual transition into limiting for natural sound +//! * **Per-channel detection** - Decoupled peak detection per channel +//! * **Coupled gain reduction** - Uniform gain reduction across channels preserves stereo imaging +//! * **Configurable timing** - Adjustable attack/release times for different use cases +//! * **Efficient processing** - Optimized implementations for mono, stereo, and multi-channel audio +//! +//! # Usage +//! +//! Use [`LimitSettings`] to configure the limiter, then apply it to any audio source: +//! +//! ```rust +//! use rodio::source::{SineWave, Source, LimitSettings}; +//! use std::time::Duration; +//! +//! // Create a loud sine wave +//! let source = SineWave::new(440.0).amplify(2.0); +//! +//! // Apply limiting with -6dB threshold +//! let settings = LimitSettings::default().with_threshold(-6.0); +//! let limited = source.limit(settings); +//! ``` +//! +//! # Presets +//! +//! [`LimitSettings`] provides optimized presets for common use cases: +//! +//! * [`LimitSettings::default()`] - General-purpose limiting (-1 dBFS, balanced) +//! * [`LimitSettings::dynamic_content()`] - Music and sound effects (-3 dBFS, transparent) +//! * [`LimitSettings::broadcast()`] - Streaming and voice chat (fast response, consistent) +//! * [`LimitSettings::mastering()`] - Final production stage (-0.5 dBFS, tight peak control) +//! * [`LimitSettings::gaming()`] - Interactive audio (-3 dBFS, responsive dynamics) +//! * [`LimitSettings::live_performance()`] - Real-time applications (ultra-fast protection) +//! +//! ```rust +//! use rodio::source::{SineWave, Source, LimitSettings}; +//! +//! // Use preset optimized for music +//! let music = SineWave::new(440.0).amplify(1.5); +//! let limited_music = music.limit(LimitSettings::dynamic_content()); +//! +//! // Use preset optimized for streaming +//! let stream = SineWave::new(440.0).amplify(2.0); +//! let limited_stream = stream.limit(LimitSettings::broadcast()); +//! ``` + +use std::time::Duration; + +use super::SeekError; +use crate::{ + common::{ChannelCount, Sample, SampleRate}, + math, Source, +}; + +/// Configuration settings for audio limiting. +/// +/// This struct defines how the limiter behaves, including when to start limiting +/// (threshold), how gradually to apply it (knee width), and how quickly to respond +/// to level changes (attack/release times). +/// +/// # Parameters +/// +/// * **Threshold** - Level in dB where limiting begins (must be negative, typically -1 to -6 dB) +/// * **Knee Width** - Range in dB over which limiting gradually increases (wider = smoother) +/// * **Attack** - Time to respond to level increases (shorter = faster but may distort) +/// * **Release** - Time to recover after level decreases (longer = smoother) +/// +/// # Examples +/// +/// ## Basic Usage +/// +/// ```rust +/// use rodio::source::{SineWave, Source, LimitSettings}; +/// use std::time::Duration; +/// +/// // Use default settings (-1 dB threshold, 4 dB knee, 5ms attack, 100ms release) +/// let source = SineWave::new(440.0).amplify(2.0); +/// let limited = source.limit(LimitSettings::default()); +/// ``` +/// +/// ## Custom Settings with Builder Pattern +/// +/// ```rust +/// use rodio::source::{SineWave, Source, LimitSettings}; +/// use std::time::Duration; +/// +/// let source = SineWave::new(440.0).amplify(3.0); +/// let settings = LimitSettings::new() +/// .with_threshold(-6.0) // Limit peaks above -6dB +/// .with_knee_width(2.0) // 2dB soft knee for smooth limiting +/// .with_attack(Duration::from_millis(3)) // Fast 3ms attack +/// .with_release(Duration::from_millis(50)); // 50ms release +/// +/// let limited = source.limit(settings); +/// ``` +/// +/// ## Common Adjustments +/// +/// ```rust +/// use rodio::source::LimitSettings; +/// use std::time::Duration; +/// +/// // More headroom for dynamic content +/// let conservative = LimitSettings::default() +/// .with_threshold(-3.0) // More headroom +/// .with_knee_width(6.0); // Wide knee for transparency +/// +/// // Tighter control for broadcast/streaming +/// let broadcast = LimitSettings::default() +/// .with_knee_width(2.0) // Narrower knee for firmer limiting +/// .with_attack(Duration::from_millis(3)) // Faster attack +/// .with_release(Duration::from_millis(50)); // Faster release +/// ``` +#[derive(Debug, Clone)] +/// Configuration settings for audio limiting. +/// +/// # dB vs. dBFS Reference +/// +/// This limiter uses **dBFS (decibels relative to Full Scale)** for all level measurements: +/// - **0 dBFS** = maximum possible digital level (1.0 in linear scale) +/// - **Negative dBFS** = levels below maximum (e.g., -6 dBFS = 0.5 in linear scale) +/// - **Positive dBFS** = levels above maximum (causes digital clipping) +/// +/// Unlike absolute dB measurements (dB SPL), dBFS is relative to the digital system's +/// maximum representable value, making it the standard for digital audio processing. +/// +/// ## Common dBFS Reference Points +/// - **0 dBFS**: Digital maximum (clipping threshold) +/// - **-1 dBFS**: Just below clipping (tight limiting) +/// - **-3 dBFS**: Moderate headroom (balanced limiting) +/// - **-6 dBFS**: Generous headroom (gentle limiting) +/// - **-12 dBFS**: Conservative level (preserves significant dynamics) +/// - **-20 dBFS**: Very quiet level (background/ambient sounds) +pub struct LimitSettings { + /// Level where limiting begins (dBFS, must be negative). + /// + /// Specifies the threshold in dBFS where the limiter starts to reduce gain: + /// - `-1.0` = limit at -1 dBFS (tight limiting, prevents clipping) + /// - `-3.0` = limit at -3 dBFS (balanced approach with headroom) + /// - `-6.0` = limit at -6 dBFS (gentle limiting, preserves dynamics) + /// + /// Values must be negative - positive values would attempt limiting above + /// 0 dBFS, which cannot prevent clipping. + pub threshold: f32, + /// Range over which limiting gradually increases (dB). + /// + /// Defines the transition zone width in dB where limiting gradually increases + /// from no effect to full limiting: + /// - `0.0` = hard limiting (abrupt transition) + /// - `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, + /// Time to respond to level increases + pub attack: Duration, + /// Time to recover after level decreases + pub release: Duration, +} + +impl Default for LimitSettings { + fn default() -> Self { + Self { + threshold: -1.0, // -1 dB + knee_width: 4.0, // 4 dB + attack: Duration::from_millis(5), // 5 ms + release: Duration::from_millis(100), // 100 ms + } + } +} + +impl LimitSettings { + /// Creates new limit settings with default values. + /// + /// Equivalent to [`LimitSettings::default()`]. + #[inline] + pub fn new() -> Self { + Self::default() + } + + /// Creates settings optimized for dynamic content like music and sound effects. + /// + /// Designed for content with varying dynamics where you want to preserve + /// the natural feel while preventing occasional peaks from clipping. + /// + /// # Configuration + /// + /// - **Threshold**: -3.0 dBFS (more headroom than default) + /// - **Knee width**: 6.0 dB (wide, transparent transition) + /// - **Attack**: 5 ms (default, balanced response) + /// - **Release**: 100 ms (default, smooth recovery) + /// + /// # Use Cases + /// + /// - Music playback with occasional loud peaks + /// - Sound effects that need natural dynamics + /// - Content where transparency is more important than tight control + /// - Game audio with varying intensity levels + /// + /// # Examples + /// + /// ``` + /// use rodio::source::{SineWave, Source, LimitSettings}; + /// + /// let music = SineWave::new(440.0).amplify(1.5); + /// let limited = music.limit(LimitSettings::dynamic_content()); + /// ``` + #[inline] + pub fn dynamic_content() -> Self { + Self::default() + .with_threshold(-3.0) // More headroom for dynamics + .with_knee_width(6.0) // Wide knee for transparency + } + + /// Creates settings optimized for broadcast and streaming applications. + /// + /// Designed for consistent loudness and reliable peak control in scenarios + /// where clipping absolutely cannot occur and consistent levels are critical. + /// + /// # Configuration + /// + /// - **Threshold**: -1.0 dBFS (default, tight control) + /// - **Knee width**: 2.0 dB (narrower, more decisive limiting) + /// - **Attack**: 3 ms (faster response to catch transients) + /// - **Release**: 50 ms (faster recovery for consistent levels) + /// + /// # Use Cases + /// + /// - Live streaming where clipping would be catastrophic + /// - Broadcast audio that must meet loudness standards + /// - Voice chat applications requiring consistent levels + /// - Podcast production for consistent listening experience + /// - Game voice communication systems + /// + /// # Examples + /// + /// ``` + /// use rodio::source::{SineWave, Source, LimitSettings}; + /// + /// let voice_chat = SineWave::new(440.0).amplify(2.0); + /// let limited = voice_chat.limit(LimitSettings::broadcast()); + /// ``` + #[inline] + pub fn broadcast() -> Self { + Self::default() + .with_knee_width(2.0) // Narrower knee for decisive limiting + .with_attack(Duration::from_millis(3)) // Faster attack for transients + .with_release(Duration::from_millis(50)) // Faster recovery for consistency + } + + /// Creates settings optimized for mastering and final audio production. + /// + /// Designed for the final stage of audio production where tight peak control + /// is needed while maintaining audio quality and preventing any clipping. + /// + /// # Configuration + /// + /// - **Threshold**: -0.5 dBFS (very tight, maximum loudness) + /// - **Knee width**: 1.0 dB (narrow, precise control) + /// - **Attack**: 1 ms (very fast, catches all transients) + /// - **Release**: 200 ms (slower, maintains natural envelope) + /// + /// # Use Cases + /// + /// - Final mastering stage for tight peak control + /// - Preparing audio for streaming platforms (after loudness processing) + /// - Album mastering where consistent peak levels are critical + /// - Audio post-production for film/video + /// + /// # Examples + /// + /// ``` + /// use rodio::source::{SineWave, Source, LimitSettings}; + /// + /// let master_track = SineWave::new(440.0).amplify(3.0); + /// let mastered = master_track.limit(LimitSettings::mastering()); + /// ``` + #[inline] + pub fn mastering() -> Self { + Self { + threshold: -0.5, // Very tight for peak control + knee_width: 1.0, // Narrow knee for precise control + attack: Duration::from_millis(1), // Very fast attack + release: Duration::from_millis(200), // Slower release for natural envelope + } + } + + /// Creates settings optimized for live performance and real-time applications. + /// + /// Designed for scenarios where low latency is critical and the limiter + /// must respond quickly to protect equipment and audiences. + /// + /// # Configuration + /// + /// - **Threshold**: -2.0 dBFS (some headroom for safety) + /// - **Knee width**: 3.0 dB (moderate, good compromise) + /// - **Attack**: 0.5 ms (extremely fast for protection) + /// - **Release**: 30 ms (fast recovery for live feel) + /// + /// # Use Cases + /// + /// - Live concert sound reinforcement + /// - DJ mixing and live electronic music + /// - Real-time audio processing where latency matters + /// - Equipment protection in live settings + /// - Interactive audio applications and games + /// + /// # Examples + /// + /// ``` + /// use rodio::source::{SineWave, Source, LimitSettings}; + /// + /// let live_input = SineWave::new(440.0).amplify(2.5); + /// let protected = live_input.limit(LimitSettings::live_performance()); + /// ``` + #[inline] + pub fn live_performance() -> Self { + Self { + threshold: -2.0, // Some headroom for safety + knee_width: 3.0, // Moderate knee + attack: Duration::from_micros(500), // Extremely fast for protection + release: Duration::from_millis(30), // Fast recovery for live feel + } + } + + /// Creates settings optimized for gaming and interactive audio. + /// + /// Designed for games where audio levels can vary dramatically between + /// quiet ambient sounds and loud action sequences, requiring responsive + /// limiting that maintains immersion. + /// + /// # Configuration + /// + /// - **Threshold**: -3.0 dBFS (balanced headroom for dynamic range) + /// - **Knee width**: 3.0 dB (moderate transition for natural feel) + /// - **Attack**: 2 ms (fast enough for sound effects, not harsh) + /// - **Release**: 75 ms (quick recovery for interactive responsiveness) + /// + /// # Use Cases + /// + /// - Game audio mixing for consistent player experience + /// - Interactive audio applications requiring dynamic response + /// - VR/AR audio where sudden loud sounds could be jarring + /// - Mobile games needing battery-efficient processing + /// - Streaming gameplay audio for viewers + /// + /// # Examples + /// + /// ``` + /// use rodio::source::{SineWave, Source, LimitSettings}; + /// + /// let game_audio = SineWave::new(440.0).amplify(2.0); + /// let limited = game_audio.limit(LimitSettings::gaming()); + /// ``` + #[inline] + pub fn gaming() -> Self { + Self { + threshold: -3.0, // Balanced headroom for dynamics + knee_width: 3.0, // Moderate for natural feel + attack: Duration::from_millis(2), // Fast but not harsh + release: Duration::from_millis(75), // Quick for interactivity + } + } + + /// Sets the threshold level where limiting begins. + /// + /// # Arguments + /// + /// * `threshold` - Level in dBFS where limiting starts (must be negative) + /// - `-1.0` = limiting starts at -1 dBFS (tight limiting, prevents clipping) + /// - `-3.0` = limiting starts at -3 dBFS (balanced approach with headroom) + /// - `-6.0` = limiting starts at -6 dBFS (gentle limiting, preserves dynamics) + /// - `-12.0` = limiting starts at -12 dBFS (very aggressive, significantly reduces dynamics) + /// + /// # dBFS Context + /// + /// Remember that 0 dBFS is the digital maximum. Negative dBFS values represent + /// levels below this maximum: + /// - `-1 dBFS` ≈ 89% of maximum amplitude (very loud, limiting triggers late) + /// - `-3 dBFS` ≈ 71% of maximum amplitude (loud, moderate limiting) + /// - `-6 dBFS` ≈ 50% of maximum amplitude (moderate, gentle limiting) + /// - `-12 dBFS` ≈ 25% of maximum amplitude (quiet, aggressive limiting) + /// + /// Lower thresholds (more negative) trigger limiting earlier and reduce dynamics more. + /// 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 { + self.threshold = threshold; + self + } + + /// Sets the knee width - range over which limiting gradually increases. + /// + /// # Arguments + /// + /// * `knee_width` - Range in dB over which limiting transitions from off to full effect + /// - `0.0` dB = hard knee (abrupt limiting, may sound harsh) + /// - `1.0-2.0` dB = moderate knee (noticeable but controlled limiting) + /// - `4.0` dB = soft knee (smooth, transparent limiting) [default] + /// - `6.0-8.0` dB = very soft knee (very gradual, musical limiting) + /// + /// # How Knee Width Works + /// + /// The knee creates a transition zone around the threshold. For example, with + /// `threshold = -3.0` dBFS and `knee_width = 4.0` dB: + /// - No limiting below -5 dBFS (threshold - knee_width/2) + /// - 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 { + self.knee_width = knee_width; + self + } + + /// Sets the attack time - how quickly the limiter responds to level increases. + /// + /// # Arguments + /// + /// * `attack` - Time duration for the limiter to react to peaks + /// - Shorter (1-5 ms) = faster response, may cause distortion + /// - Longer (10-20 ms) = smoother sound, may allow brief overshoots + #[inline] + pub fn with_attack(mut self, attack: Duration) -> Self { + self.attack = attack; + self + } + + /// Sets the release time - how quickly the limiter recovers after level decreases. + /// + /// # Arguments + /// + /// * `release` - Time duration for the limiter to stop limiting + /// - Shorter (10-50 ms) = quick recovery, may sound pumping + /// - Longer (100-500 ms) = smooth recovery, more natural sound + #[inline] + pub fn with_release(mut self, release: Duration) -> Self { + self.release = release; + self + } +} + +/// Creates a limiter that processes the input audio source. +/// +/// This function applies the specified limiting settings to control audio peaks. +/// The limiter uses feedforward processing with configurable attack/release times +/// and soft-knee characteristics for natural-sounding dynamic range control. +/// +/// # Arguments +/// +/// * `input` - Audio source to process +/// * `settings` - Limiter configuration (threshold, knee, timing) +/// +/// # Returns +/// +/// A [`Limit`] source that applies the limiting to the input audio. +/// +/// # Example +/// +/// ```rust +/// use rodio::source::{SineWave, Source, LimitSettings}; +/// +/// let source = SineWave::new(440.0).amplify(2.0); +/// let settings = LimitSettings::default().with_threshold(-6.0); +/// let limited = source.limit(settings); +/// ``` +pub(crate) fn limit(input: I, settings: LimitSettings) -> Limit { + let sample_rate = input.sample_rate(); + let attack = duration_to_coefficient(settings.attack, sample_rate); + let release = duration_to_coefficient(settings.release, sample_rate); + let channels = input.channels() as usize; + + let base = LimitBase::new(settings.threshold, settings.knee_width, attack, release); + + let inner = match channels { + 1 => LimitInner::Mono(LimitMono { + input, + base, + limiter_integrator: 0.0, + limiter_peak: 0.0, + }), + 2 => LimitInner::Stereo(LimitStereo { + input, + base, + limiter_integrators: [0.0; 2], + limiter_peaks: [0.0; 2], + position: 0, + }), + n => LimitInner::MultiChannel(LimitMulti { + input, + base, + limiter_integrators: vec![0.0; n], + limiter_peaks: vec![0.0; n], + position: 0, + }), + }; + + Limit(inner) +} + +/// A source filter that applies audio limiting to prevent peaks from exceeding a threshold. +/// +/// This filter reduces the amplitude of audio signals that exceed the configured threshold +/// level, helping to prevent clipping and maintain consistent output levels. The limiter +/// automatically adapts to mono, stereo, or multi-channel audio sources by using the +/// appropriate internal implementation. +/// +/// # How It Works +/// +/// The limiter detects peaks in each audio channel independently but applies gain reduction +/// uniformly across all channels. This preserves stereo imaging while ensuring that loud +/// peaks in any channel are controlled. The limiting uses: +/// +/// - **Soft-knee compression**: Gradual gain reduction around the threshold +/// - **Attack/release timing**: Configurable response speed to level changes +/// - **Peak detection**: Tracks maximum levels across all channels +/// - **Gain smoothing**: Prevents audible artifacts from rapid gain changes +/// +/// # Created By +/// +/// Use [`Source::limit()`] with [`LimitSettings`] to create a `Limit` source: +/// +/// ``` +/// use rodio::source::{SineWave, Source}; +/// use rodio::source::LimitSettings; +/// use std::time::Duration; +/// +/// let source = SineWave::new(440.0).amplify(2.0); +/// let settings = LimitSettings::default() +/// .with_threshold(-6.0) // -6 dBFS threshold +/// .with_attack(Duration::from_millis(5)) +/// .with_release(Duration::from_millis(100)); +/// let limited = source.limit(settings); +/// ``` +/// +/// # Performance +/// +/// The limiter automatically selects the most efficient implementation based on channel count: +/// - **Mono**: Single-channel optimized processing +/// - **Stereo**: Two-channel optimized with interleaved processing +/// - **Multi-channel**: Generic implementation for 3+ channels +/// +/// # Channel Count Stability +/// +/// **Important**: The limiter is optimized for sources with fixed channel counts. +/// Most audio files (music, podcasts, etc.) maintain constant channel counts, +/// making this optimization safe and beneficial. +/// +/// If the underlying source changes channel count mid-stream (rare), the limiter +/// will continue to function but performance may be degraded. For such cases, +/// recreate the limiter when the channel count changes. +/// +/// # Type Parameters +/// +/// * `I` - The input audio source type that implements [`Source`] +#[derive(Clone, Debug)] +pub struct Limit(LimitInner) +where + I: Source; + +impl Source for Limit +where + I: Source, +{ + #[inline] + fn current_span_len(&self) -> Option { + self.0.current_span_len() + } + + #[inline] + fn sample_rate(&self) -> SampleRate { + self.0.sample_rate() + } + + #[inline] + fn channels(&self) -> ChannelCount { + self.0.channels() + } + + #[inline] + fn total_duration(&self) -> Option { + self.0.total_duration() + } + + #[inline] + fn try_seek(&mut self, position: Duration) -> Result<(), SeekError> { + self.0.try_seek(position) + } +} + +impl Limit +where + I: Source, +{ + /// Returns a reference to the inner audio source. + /// + /// This allows access to the original source's properties and methods without + /// consuming the limiter. Useful for inspecting source characteristics like + /// sample rate, channels, or duration. + /// + /// Useful for inspecting source properties without consuming the filter. + #[inline] + pub fn inner(&self) -> &I { + self.0.inner() + } + + /// Returns a mutable reference to the inner audio source. + /// + /// This allows modification of the original source while keeping the limiter + /// wrapper. Essential for operations like seeking that need to modify the + /// underlying source. + #[inline] + pub fn inner_mut(&mut self) -> &mut I { + self.0.inner_mut() + } + + /// Consumes the limiter and returns the inner audio source. + /// + /// This dismantles the limiter wrapper to extract the original source, + /// allowing the audio pipeline to continue without limiting overhead. + /// Useful when limiting is no longer needed but the source should continue. + #[inline] + pub fn into_inner(self) -> I { + self.0.into_inner() + } +} + +impl Iterator for Limit +where + I: Source, +{ + type Item = I::Item; + + /// Provides the next limited sample. + #[inline] + fn next(&mut self) -> Option { + self.0.next() + } + + /// Provides size hints from the inner limiter. + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } +} + +/// Internal limiter implementation that adapts to different channel configurations. +/// +/// This enum is private and automatically selects the most efficient implementation +/// based on the number of audio channels: +/// - **Mono**: Single-channel optimized processing with minimal state +/// - **Stereo**: Two-channel optimized with fixed-size arrays for performance +/// - **Multi-channel**: Generic implementation using vectors for arbitrary channel counts +/// +/// The enum is wrapped by the public [`Limit`] struct to provide a clean API while +/// maintaining internal optimization flexibility. +/// +/// # Channel-Specific Optimizations +/// +/// - **Mono**: Direct processing without channel indexing overhead +/// - **Stereo**: Fixed-size arrays avoid heap allocation and provide cache efficiency +/// - **Multi-channel**: Dynamic vectors handle surround sound and custom configurations +/// +/// # Type Parameters +/// +/// * `I` - The input audio source type that implements [`Source`] +#[derive(Clone, Debug)] +enum LimitInner +where + I: Source, +{ + /// Mono channel limiter + Mono(LimitMono), + /// Stereo channel limiter + Stereo(LimitStereo), + /// Multi-channel limiter for arbitrary channel counts + MultiChannel(LimitMulti), +} + +/// Common parameters and processing logic shared across all limiter variants. +/// +/// Handles: +/// * Parameter storage (threshold, knee width, attack/release coefficients) +/// * Per-channel state updates for peak detection +/// * Gain computation through soft-knee limiting +#[derive(Clone, Debug)] +struct LimitBase { + /// Level where limiting begins (dB) + threshold: f32, + /// Width of the soft-knee region (dB) + knee_width: f32, + /// Inverse of 8 times the knee width (precomputed for efficiency) + inv_knee_8: f32, + /// Attack time constant (ms) + attack: f32, + /// Release time constant (ms) + release: f32, +} + +/// Mono channel limiter optimized for single-channel processing. +/// +/// This variant is automatically selected by [`Limit`] for mono audio sources. +/// It uses minimal state (single integrator and peak detector) for optimal +/// performance with single-channel audio. +/// +/// # Internal Use +/// +/// This struct is used internally by [`LimitInner::Mono`] and is not intended +/// for direct construction. Use [`Source::limit()`] instead. +#[derive(Clone, Debug)] +pub struct LimitMono { + /// Input audio source + input: I, + /// Common limiter parameters + base: LimitBase, + /// Peak detection integrator state + limiter_integrator: f32, + /// Peak detection state + limiter_peak: f32, +} + +/// Stereo channel limiter with optimized two-channel processing. +/// +/// This variant is automatically selected by [`Limit`] for stereo audio sources. +/// It uses fixed-size arrays instead of vectors for better cache performance +/// and avoids heap allocation overhead common in stereo audio processing. +/// +/// # Performance +/// +/// The fixed arrays and channel position tracking provide optimal performance +/// for interleaved stereo sample processing, avoiding the dynamic allocation +/// overhead of the multi-channel variant. +/// +/// # Internal Use +/// +/// This struct is used internally by [`LimitInner::Stereo`] and is not intended +/// for direct construction. Use [`Source::limit()`] instead. +#[derive(Clone, Debug)] +pub struct LimitStereo { + /// Input audio source + input: I, + /// Common limiter parameters + base: LimitBase, + /// Peak detection integrator states for left and right channels + limiter_integrators: [f32; 2], + /// Peak detection states for left and right channels + limiter_peaks: [f32; 2], + /// Current channel position (0 = left, 1 = right) + position: u8, +} + +/// Generic multi-channel limiter for surround sound or other configurations. +/// +/// This variant is automatically selected by [`Limit`] for audio sources with +/// 3 or more channels. It uses dynamic vectors to handle arbitrary channel +/// counts, making it suitable for surround sound (5.1, 7.1) and other +/// multi-channel audio configurations. +/// +/// # Flexibility vs Performance +/// +/// While this variant has slightly more overhead than the mono/stereo variants +/// due to vector allocation and dynamic indexing, it provides the flexibility +/// needed for complex audio setups while maintaining good performance. +/// +/// # Internal Use +/// +/// This struct is used internally by [`LimitInner::MultiChannel`] and is not +/// intended for direct construction. Use [`Source::limit()`] instead. +#[derive(Clone, Debug)] +pub struct LimitMulti { + /// Input audio source + input: I, + /// Common limiter parameters + base: LimitBase, + /// Peak detector integrator states (one per channel) + limiter_integrators: Vec, + /// Peak detector states (one per channel) + limiter_peaks: Vec, + /// Current channel position (0 to channels-1) + position: usize, +} + +/// Computes the gain reduction amount in dB based on input level. +/// +/// Implements soft-knee compression with three regions: +/// 1. Below threshold - knee_width: No compression (returns 0.0) +/// 2. Within knee region: Gradual compression with quadratic curve +/// 3. Above threshold + knee_width: Linear compression +/// +/// Optimized for the most common case where samples are below threshold and no limiting is needed +/// (returns `0.0` early). +/// +/// # Arguments +/// +/// * `sample` - Input sample value (with initial gain applied) +/// * `threshold` - Level where limiting begins (dB) +/// * `knee_width` - Width of soft knee region (dB) +/// * `inv_knee_8` - Precomputed value: 1.0 / (8.0 * knee_width) for efficiency +/// +/// # Returns +/// +/// Amount of gain reduction to apply in dB +#[inline] +fn process_sample(sample: Sample, threshold: f32, knee_width: f32, inv_knee_8: f32) -> f32 { + // Add slight DC offset. Some samples are silence, which is -inf dB and gets the limiter stuck. + // Adding a small positive offset prevents this. + let bias_db = math::linear_to_db(sample.abs() + f32::MIN_POSITIVE) - threshold; + let knee_boundary_db = bias_db * 2.0; + if knee_boundary_db < -knee_width { + 0.0 + } else if knee_boundary_db.abs() <= knee_width { + // Faster than powi(2) + let x = knee_boundary_db + knee_width; + x * x * inv_knee_8 + } else { + bias_db + } +} + +impl LimitBase { + fn new(threshold: f32, knee_width: f32, attack: f32, release: f32) -> Self { + let inv_knee_8 = 1.0 / (8.0 * knee_width); + Self { + threshold, + knee_width, + inv_knee_8, + attack, + release, + } + } + + /// Updates the channel's envelope detection state. + /// + /// For each channel, processes: + /// 1. Initial gain and dB conversion + /// 2. Soft-knee limiting calculation + /// 3. Envelope detection with attack/release filtering + /// 4. Peak level tracking + /// + /// The envelope detection uses a dual-stage approach: + /// - First stage: Max of current signal and smoothed release + /// - Second stage: Attack smoothing of the peak detector output + /// + /// Note: Only updates state, gain application is handled by the variant implementations to + /// allow for coupled gain reduction across channels. + #[must_use] + #[inline] + fn process_channel(&self, sample: Sample, integrator: &mut f32, peak: &mut f32) -> 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( + limiter_db, + self.release * *integrator + (1.0 - self.release) * limiter_db, + ); + *peak = self.attack * *peak + (1.0 - self.attack) * *integrator; + + sample + } +} + +impl LimitMono +where + I: Source, +{ + /// Processes the next mono sample through the limiter. + /// + /// Single channel implementation with direct state updates. + #[inline] + fn process_next(&mut self, sample: I::Item) -> I::Item { + let processed = + self.base + .process_channel(sample, &mut self.limiter_integrator, &mut self.limiter_peak); + + // steps 6-8: conversion into level and multiplication into gain stage + processed * math::db_to_linear(-self.limiter_peak) + } +} + +impl LimitStereo +where + I: Source, +{ + /// Processes the next stereo sample through the limiter. + /// + /// Uses efficient channel position tracking with XOR toggle and direct array access for state + /// updates. + #[inline] + fn process_next(&mut self, sample: I::Item) -> I::Item { + let channel = self.position as usize; + self.position ^= 1; + + let processed = self.base.process_channel( + sample, + &mut self.limiter_integrators[channel], + &mut self.limiter_peaks[channel], + ); + + // 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]); + processed * math::db_to_linear(-max_peak) + } +} + +impl LimitMulti +where + I: Source, +{ + /// Processes the next multi-channel sample through the limiter. + /// + /// Generic implementation supporting arbitrary channel counts with `Vec`-based state storage. + #[inline] + fn process_next(&mut self, sample: I::Item) -> I::Item { + let channel = self.position; + self.position = (self.position + 1) % self.limiter_integrators.len(); + + let processed = self.base.process_channel( + sample, + &mut self.limiter_integrators[channel], + &mut self.limiter_peaks[channel], + ); + + // steps 6-8: conversion into level and multiplication into gain stage. Find maximum peak + // across all channels to couple the gain and maintain multi-channel imaging. + let max_peak = self + .limiter_peaks + .iter() + .fold(0.0, |max, &peak| f32::max(max, peak)); + processed * math::db_to_linear(-max_peak) + } +} + +impl LimitInner +where + I: Source, +{ + /// Returns a reference to the inner audio source. + /// + /// This allows access to the original source's properties and methods without + /// consuming the limiter. Useful for inspecting source characteristics like + /// sample rate, channels, or duration. + #[inline] + pub fn inner(&self) -> &I { + match self { + LimitInner::Mono(mono) => &mono.input, + LimitInner::Stereo(stereo) => &stereo.input, + LimitInner::MultiChannel(multi) => &multi.input, + } + } + + /// Returns a mutable reference to the inner audio source. + /// + /// This allows modification of the original source while keeping the limiter + /// wrapper. Essential for operations like seeking that need to modify the + /// underlying source. + #[inline] + pub fn inner_mut(&mut self) -> &mut I { + match self { + LimitInner::Mono(mono) => &mut mono.input, + LimitInner::Stereo(stereo) => &mut stereo.input, + LimitInner::MultiChannel(multi) => &mut multi.input, + } + } + + /// Consumes the filter and returns the inner audio source. + /// + /// This dismantles the limiter wrapper to extract the original source, + /// allowing the audio pipeline to continue without limiting overhead. + /// Useful when limiting is no longer needed but the source should continue. + #[inline] + pub fn into_inner(self) -> I { + match self { + LimitInner::Mono(mono) => mono.input, + LimitInner::Stereo(stereo) => stereo.input, + LimitInner::MultiChannel(multi) => multi.input, + } + } +} + +impl Iterator for LimitInner +where + I: Source, +{ + type Item = I::Item; + + /// Provides the next processed sample. + /// + /// Routes processing to the appropriate channel-specific implementation: + /// * `Mono`: Direct single-channel processing + /// * `Stereo`: Optimized two-channel processing + /// * `MultiChannel`: Generic multi-channel processing + /// + /// # Channel Count Changes + /// + /// **Important**: This limiter assumes a fixed channel count determined at creation time. + /// Most audio sources (files, streams) maintain constant channel counts, making this + /// assumption safe for typical usage. + /// + /// If the underlying source changes its channel count mid-stream (rare), the limiter + /// will continue to function but may experience timing and imaging issues. For optimal + /// performance, recreate the limiter when the channel count changes. + #[inline] + fn next(&mut self) -> Option { + match self { + LimitInner::Mono(mono) => { + let sample = mono.input.next()?; + Some(mono.process_next(sample)) + } + LimitInner::Stereo(stereo) => { + let sample = stereo.input.next()?; + Some(stereo.process_next(sample)) + } + LimitInner::MultiChannel(multi) => { + let sample = multi.input.next()?; + Some(multi.process_next(sample)) + } + } + } + + /// Provides size hints from the inner source. + /// + /// Delegates directly to the source to maintain accurate collection sizing. + /// Used by collection operations for optimization. + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner().size_hint() + } +} + +impl Source for LimitInner +where + I: Source, +{ + /// Returns the number of samples in the current audio frame. + /// + /// Delegates to inner source to maintain frame alignment. + #[inline] + fn current_span_len(&self) -> Option { + self.inner().current_span_len() + } + + /// Returns the number of channels in the audio stream. + /// + /// Channel count determines which limiter variant is used: + /// * 1: Mono + /// * 2: Stereo + /// * >2: MultiChannel + #[inline] + fn channels(&self) -> ChannelCount { + self.inner().channels() + } + + /// Returns the audio sample rate in Hz. + #[inline] + fn sample_rate(&self) -> SampleRate { + self.inner().sample_rate() + } + + /// Returns the total duration of the audio. + /// + /// Returns None for streams without known duration. + #[inline] + fn total_duration(&self) -> Option { + self.inner().total_duration() + } + + /// Attempts to seek to the specified position. + /// + /// Resets limiter state to prevent artifacts after seeking: + /// * Mono: Direct reset of integrator and peak values + /// * Stereo: Efficient array fill for both channels + /// * `MultiChannel`: Resets all channel states via fill + /// + /// # Arguments + /// + /// * `target` - Position to seek to + /// + /// # Errors + /// + /// Returns error if the underlying source fails to seek + fn try_seek(&mut self, target: Duration) -> Result<(), SeekError> { + self.inner_mut().try_seek(target)?; + + match self { + LimitInner::Mono(mono) => { + mono.limiter_integrator = 0.0; + mono.limiter_peak = 0.0; + } + LimitInner::Stereo(stereo) => { + stereo.limiter_integrators.fill(0.0); + stereo.limiter_peaks.fill(0.0); + } + LimitInner::MultiChannel(multi) => { + multi.limiter_integrators.fill(0.0); + multi.limiter_peaks.fill(0.0); + } + } + + Ok(()) + } +} + +/// Converts a time duration to a smoothing coefficient for exponential filtering. +/// +/// Used for both attack and release filtering in the limiter's envelope detector. +/// Creates a coefficient that determines how quickly the limiter responds to level changes: +/// * Longer times = higher coefficients (closer to 1.0) = slower, smoother response +/// * Shorter times = lower coefficients (closer to 0.0) = faster, more immediate response +/// +/// The coefficient is calculated using the formula: `e^(-1 / (duration_seconds * sample_rate))` +/// which provides exponential smoothing behavior suitable for audio envelope detection. +/// +/// # Arguments +/// +/// * `duration` - Desired response time (attack or release duration) +/// * `sample_rate` - Audio sample rate in Hz +/// +/// # Returns +/// +/// Smoothing coefficient in the range [0.0, 1.0] for use in exponential filters +#[must_use] +fn duration_to_coefficient(duration: Duration, sample_rate: SampleRate) -> f32 { + f32::exp(-1.0 / (duration.as_secs_f32() * sample_rate as f32)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::buffer::SamplesBuffer; + use crate::source::{SineWave, Source}; + use std::time::Duration; + + fn create_test_buffer(samples: Vec, channels: u16, sample_rate: u32) -> SamplesBuffer { + SamplesBuffer::new(channels, sample_rate, samples) + } + + #[test] + fn test_limiter_creation() { + // Test mono + let buffer = create_test_buffer(vec![0.5, 0.8, 1.0, 0.3], 1, 44100); + let limiter = limit(buffer, LimitSettings::default()); + assert_eq!(limiter.channels(), 1); + assert_eq!(limiter.sample_rate(), 44100); + matches!(limiter.0, LimitInner::Mono(_)); + + // Test stereo + let buffer = create_test_buffer(vec![0.5, 0.8, 1.0, 0.3, 0.2, 0.6, 0.9, 0.4], 2, 44100); + let limiter = limit(buffer, LimitSettings::default()); + assert_eq!(limiter.channels(), 2); + matches!(limiter.0, LimitInner::Stereo(_)); + + // Test multichannel + let buffer = create_test_buffer(vec![0.5; 12], 3, 44100); + let limiter = limit(buffer, LimitSettings::default()); + assert_eq!(limiter.channels(), 3); + matches!(limiter.0, LimitInner::MultiChannel(_)); + } +} diff --git a/src/source/mod.rs b/src/source/mod.rs index b4691edb..dfb1388e 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -3,9 +3,11 @@ use core::fmt; use core::time::Duration; -use crate::common::{ChannelCount, SampleRate}; -use crate::Sample; -use amplify::to_linear; +use crate::{ + common::{ChannelCount, SampleRate}, + math, Sample, +}; + use dasp_sample::FromSample; pub use self::agc::AutomaticGainControl; @@ -24,6 +26,7 @@ pub use self::fadein::FadeIn; pub use self::fadeout::FadeOut; pub use self::from_factory::{from_factory, FromFactoryIter}; pub use self::from_iter::{from_iter, FromIter}; +pub use self::limit::{Limit, LimitSettings}; pub use self::linear_ramp::LinearGainRamp; pub use self::mix::Mix; pub use self::pausable::Pausable; @@ -60,6 +63,7 @@ mod fadein; mod fadeout; mod from_factory; mod from_iter; +mod limit; mod linear_ramp; mod mix; mod pausable; @@ -251,7 +255,7 @@ pub trait Source: Iterator { where Self: Sized, { - amplify::amplify(self, to_linear(value)) + amplify::amplify(self, math::db_to_linear(value)) } /// Normalized amplification in `[0.0, 1.0]` range. This method better matches the perceived @@ -407,6 +411,60 @@ pub trait Source: Iterator { fadeout::fadeout(self, duration) } + /// Applies limiting to prevent audio peaks from exceeding a threshold. + /// + /// A limiter reduces the amplitude of audio signals that exceed a specified level, + /// preventing clipping and maintaining consistent output levels. The limiter processes + /// each channel independently for envelope detection but applies gain reduction uniformly + /// across all channels to preserve stereo imaging. + /// + /// # Arguments + /// + /// * `settings` - [`LimitSettings`] struct containing: + /// - **threshold** - Level in dB where limiting begins (must be negative) + /// - **knee_width** - Range in dB over which limiting gradually increases + /// - **attack** - Time to respond to level increases + /// - **release** - Time to recover after level decreases + /// + /// # Returns + /// + /// A [`Limit`] source that applies the limiting to the input audio. + /// + /// # Examples + /// + /// ## Basic Usage with Default Settings + /// + /// ``` + /// use rodio::source::{SineWave, Source, LimitSettings}; + /// use std::time::Duration; + /// + /// // Create a loud sine wave and apply default limiting (-1dB threshold) + /// let source = SineWave::new(440.0).amplify(2.0); + /// let limited = source.limit(LimitSettings::default()); + /// ``` + /// + /// ## Custom Settings with Builder Pattern + /// + /// ``` + /// use rodio::source::{SineWave, Source, LimitSettings}; + /// use std::time::Duration; + /// + /// let source = SineWave::new(440.0).amplify(3.0); + /// let settings = LimitSettings::default() + /// .with_threshold(-6.0) // Limit at -6dB + /// .with_knee_width(2.0) // 2dB soft knee + /// .with_attack(Duration::from_millis(3)) // Fast 3ms attack + /// .with_release(Duration::from_millis(50)); // 50ms release + /// + /// let limited = source.limit(settings); + /// ``` + fn limit(self, settings: LimitSettings) -> Limit + where + Self: Sized, + { + limit::limit(self, settings) + } + /// Applies a linear gain ramp to the sound. /// /// If `clamp_end` is `true`, all samples subsequent to the end of the ramp diff --git a/tests/limit.rs b/tests/limit.rs new file mode 100644 index 00000000..31a62298 --- /dev/null +++ b/tests/limit.rs @@ -0,0 +1,163 @@ +use rodio::source::Source; +use std::time::Duration; + +#[test] +fn test_limiting_works() { + // High amplitude sine wave limited to -6dB + let sine_wave = rodio::source::SineWave::new(440.0) + .amplify(3.0) // 3.0 linear = ~9.5dB + .take_duration(Duration::from_millis(60)); // ~2600 samples + + let settings = rodio::source::LimitSettings::default() + .with_threshold(-6.0) // -6dB = ~0.5 linear + .with_knee_width(0.5) + .with_attack(Duration::from_millis(3)) + .with_release(Duration::from_millis(12)); + + let limiter = sine_wave.limit(settings); + 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())); + + assert!( + settled_peak <= 0.6, + "Settled peak should be ~0.5 for -6dB: {:.3}", + settled_peak + ); + assert!( + settled_peak >= 0.4, + "Peak should be reasonably close to 0.5: {:.3}", + settled_peak + ); + + let max_sample = settled_samples + .iter() + .fold(0.0f32, |acc, &x| acc.max(x.abs())); + assert!( + max_sample < 0.8, + "ALL samples should be well below 1.0: max={:.3}", + max_sample + ); +} + +#[test] +fn test_passthrough_below_threshold() { + // Low amplitude signal should pass through unchanged + let sine_wave = rodio::source::SineWave::new(1000.0) + .amplify(0.2) // 0.2 linear, well below -6dB threshold + .take_duration(Duration::from_millis(20)); + + let settings = rodio::source::LimitSettings::default().with_threshold(-6.0); + + let original_samples: Vec = sine_wave.clone().take(880).collect(); + let limiter = sine_wave.limit(settings); + 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()) { + let diff = (orig - limited).abs(); + assert!( + diff < 0.01, + "Below threshold should pass through: diff={:.6}", + diff + ); + } +} + +#[test] +fn test_limiter_with_different_settings() { + // Test limiter with various threshold settings + let test_cases = vec![ + (-1.0, 0.89), // -1 dBFS ≈ 89% amplitude + (-3.0, 0.71), // -3 dBFS ≈ 71% amplitude + (-6.0, 0.50), // -6 dBFS ≈ 50% amplitude + ]; + + for (threshold_db, expected_peak) in test_cases { + let sine_wave = rodio::source::SineWave::new(440.0) + .amplify(2.0) // Ensure signal exceeds all thresholds + .take_duration(Duration::from_millis(50)); + + let settings = rodio::source::LimitSettings::default() + .with_threshold(threshold_db) + .with_knee_width(1.0) + .with_attack(Duration::from_millis(2)) + .with_release(Duration::from_millis(10)); + + let limiter = sine_wave.limit(settings); + 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())); + + assert!( + peak <= expected_peak + 0.1, + "Threshold {}dB: peak {:.3} should be ≤ {:.3}", + threshold_db, + peak, + expected_peak + 0.1 + ); + assert!( + peak >= expected_peak - 0.1, + "Threshold {}dB: peak {:.3} should be ≥ {:.3}", + threshold_db, + peak, + expected_peak - 0.1 + ); + } +} + +#[test] +fn test_limiter_stereo_processing() { + // Test that stereo limiting works correctly + use rodio::buffer::SamplesBuffer; + + // Create stereo test signal - left channel louder than right + let left_samples = (0..1000) + .map(|i| (i as f32 * 0.01).sin() * 1.5) + .collect::>(); + let right_samples = (0..1000) + .map(|i| (i as f32 * 0.01).sin() * 0.8) + .collect::>(); + + let mut stereo_samples = Vec::new(); + for i in 0..1000 { + stereo_samples.push(left_samples[i]); + stereo_samples.push(right_samples[i]); + } + + let buffer = SamplesBuffer::new(2, 44100, stereo_samples); + let settings = rodio::source::LimitSettings::default().with_threshold(-3.0); + + let limiter = buffer.limit(settings); + 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 left_peak = limited_left.iter().fold(0.0f32, |acc, &x| acc.max(x.abs())); + let right_peak = limited_right + .iter() + .fold(0.0f32, |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) + assert!( + left_peak <= 1.5, + "Left channel should be limited: {:.3}", + left_peak + ); + assert!( + right_peak <= 1.5, + "Right channel should be limited: {:.3}", + right_peak + ); +}