Skip to main content

rodio/
math.rs

1//! Math utilities for audio processing.
2
3use crate::common::SampleRate;
4use std::time::Duration;
5
6/// Nanoseconds per second, used for Duration calculations.
7pub(crate) const NANOS_PER_SEC: u64 = 1_000_000_000;
8
9// Re-export float constants with appropriate precision for the Float type.
10// This centralizes all cfg gating for constants in one place.
11#[cfg(not(feature = "64bit"))]
12pub use std::f32::consts::{E, LN_10, LN_2, LOG10_2, LOG10_E, LOG2_10, LOG2_E, PI, TAU};
13#[cfg(feature = "64bit")]
14pub use std::f64::consts::{E, LN_10, LN_2, LOG10_2, LOG10_E, LOG2_10, LOG2_E, PI, TAU};
15
16/// Linear interpolation between two samples.
17///
18/// The result should be equivalent to
19/// `first * (1 - numerator / denominator) + second * numerator / denominator`.
20///
21/// To avoid numeric overflows pick smaller numerator.
22// TODO (refactoring) Streamline this using coefficient instead of numerator and denominator.
23#[inline]
24pub(crate) fn lerp(first: Sample, second: Sample, numerator: u32, denominator: u32) -> Sample {
25    first + (second - first) * numerator as Float / denominator as Float
26}
27
28/// Converts decibels to linear amplitude scale.
29///
30/// This function converts a decibel value to its corresponding linear amplitude value
31/// using the formula: `linear = 10^(decibels/20)` for amplitude.
32///
33/// # Arguments
34///
35/// * `decibels` - The decibel value to convert. Common ranges:
36///   - 0 dB = linear value of 1.0 (no change)
37///   - Positive dB values represent amplification (> 1.0)
38///   - Negative dB values represent attenuation (< 1.0)
39///   - -60 dB ≈ 0.001 (barely audible)
40///   - +20 dB = 10.0 (10x amplification)
41///
42/// # Returns
43///
44/// The linear amplitude value corresponding to the input decibels.
45///
46/// # Performance
47///
48/// This implementation is optimized for speed, being ~3-4% faster than the standard
49/// `10f32.powf(decibels * 0.05)` approach, with a maximum error of only 2.48e-7
50/// (representing about -132 dB precision).
51#[inline]
52pub fn db_to_linear(decibels: Float) -> Float {
53    // ~3-4% faster than using `10f32.powf(decibels * 0.05)`,
54    // with a maximum error of 2.48e-7 representing only about -132 dB.
55    Float::powf(2.0, decibels * 0.05 * LOG2_10)
56}
57
58/// Converts linear amplitude scale to decibels.
59///
60/// This function converts a linear amplitude value to its corresponding decibel value
61/// using the formula: `decibels = 20 * log10(linear)` for amplitude.
62///
63/// # Arguments
64///
65/// * `linear` - The linear amplitude value to convert. Must be positive for meaningful results:
66///   - 1.0 = 0 dB (no change)
67///   - Values > 1.0 represent amplification (positive dB)
68///   - Values < 1.0 represent attenuation (negative dB)
69///   - 0.0 results in negative infinity
70///   - Negative values are not physically meaningful for amplitude
71///
72/// # Returns
73///
74/// The decibel value corresponding to the input linear amplitude.
75///
76/// # Performance
77///
78/// This implementation is optimized for speed, being faster than the standard
79/// `20.0 * linear.log10()` approach while maintaining high precision.
80///
81/// # Special Cases
82///
83/// - `linear_to_db(0.0)` returns negative infinity
84/// - Very small positive values approach negative infinity
85/// - Negative values return NaN (not physically meaningful for amplitude)
86#[inline]
87pub fn linear_to_db(linear: Float) -> Float {
88    // Same as `to_linear`: faster than using `20f32.log10() * linear`
89    linear.log2() * LOG10_2 * 20.0
90}
91
92/// Converts a time duration to a smoothing coefficient for exponential filtering.
93///
94/// Used for both attack and release filtering in the limiter's envelope detector.
95/// Creates a coefficient that determines how quickly the limiter responds to level changes:
96/// * Longer times = higher coefficients (closer to 1.0) = slower, smoother response
97/// * Shorter times = lower coefficients (closer to 0.0) = faster, more immediate response
98///
99/// The coefficient is calculated using the formula: `e^(-1 / (duration_seconds * sample_rate))`
100/// which provides exponential smoothing behavior suitable for audio envelope detection.
101///
102/// # Arguments
103///
104/// * `duration` - Desired response time (attack or release duration)
105/// * `sample_rate` - Audio sample rate in Hz
106///
107/// # Returns
108///
109/// Smoothing coefficient in the range [0.0, 1.0] for use in exponential filters
110#[must_use]
111pub(crate) fn duration_to_coefficient(duration: Duration, sample_rate: SampleRate) -> Float {
112    Float::exp(-1.0 / (duration_to_float(duration) * sample_rate.get() as Float))
113}
114
115/// Convert Duration to Float with appropriate precision for the Sample type.
116#[inline]
117#[must_use]
118pub(crate) fn duration_to_float(duration: Duration) -> Float {
119    #[cfg(not(feature = "64bit"))]
120    {
121        duration.as_secs_f32()
122    }
123    #[cfg(feature = "64bit")]
124    {
125        duration.as_secs_f64()
126    }
127}
128
129#[must_use]
130pub(crate) fn nearest_multiple_of_two(n: u32) -> u32 {
131    if n <= 1 {
132        return 1;
133    }
134    let next = n.next_power_of_two();
135    let prev = next >> 1;
136    if n - prev <= next - n {
137        prev
138    } else {
139        next
140    }
141}
142
143/// Utility macro for getting a `NonZero` from a literal. Especially
144/// useful for passing in `ChannelCount` and `Samplerate`.
145/// Equivalent to: `const { core::num::NonZero::new($n).unwrap() }`
146///
147/// # Example
148/// ```
149/// use rodio::nz;
150/// use rodio::static_buffer::StaticSamplesBuffer;
151/// let buffer = StaticSamplesBuffer::new(nz!(2), nz!(44_100), &[0.0, 0.5, 0.0, 0.5]);
152/// ```
153///
154/// # Panics
155/// If the literal passed in is zero this panicks.
156#[macro_export]
157macro_rules! nz {
158    ($n:literal) => {
159        const { core::num::NonZero::new($n).unwrap() }
160    };
161}
162
163pub use nz;
164
165use crate::{common::Float, Sample};
166
167#[cfg(test)]
168mod test {
169    use super::*;
170    use num_rational::Ratio;
171    use quickcheck::{quickcheck, TestResult};
172
173    quickcheck! {
174        fn lerp_random(first: Sample, second: Sample, numerator: u32, denominator: u32) -> TestResult {
175            if denominator == 0 { return TestResult::discard(); }
176
177            // Constrain to realistic audio sample range [-1.0, 1.0]
178            // Audio samples rarely exceed this range, and large values cause floating-point error accumulation
179            if first.abs() > 1.0 || second.abs() > 1.0 { return TestResult::discard(); }
180
181            // Discard infinite or NaN samples (can occur in quickcheck)
182            if !first.is_finite() || !second.is_finite() { return TestResult::discard(); }
183
184            let (numerator, denominator) = Ratio::new(numerator, denominator).into_raw();
185            // Reduce max numerator to avoid floating-point error accumulation with large ratios
186            if numerator > 1000 { return TestResult::discard(); }
187
188            let a = first as f64;
189            let b = second as f64;
190            let c = numerator as f64 / denominator as f64;
191            if !(0.0..=1.0).contains(&c) { return TestResult::discard(); };
192
193            let reference = a * (1.0 - c) + b * c;
194            let x = lerp(first, second, numerator, denominator);
195
196            // With realistic audio-range inputs, lerp should be very precise
197            // f32 has ~7 decimal digits, so 1e-6 tolerance is reasonable
198            // This is well below 16-bit audio precision (~1.5e-5)
199            let tolerance = 1e-6;
200            TestResult::from_bool((x as f64 - reference).abs() < tolerance)
201        }
202    }
203
204    /// Tolerance values for precision tests, derived from empirical measurement
205    /// of actual implementation errors across the full ±100dB range.
206    ///
207    /// Methodology:
208    /// 1. Calculated relative errors against mathematically exact `f64` calculations
209    /// 2. Found maximum errors: dB->linear = 2.3x ε, linear->dB = 1.0x ε, round-trip = 8x ε
210    /// 3. Applied 2x safety margins for cross-platform robustness
211    /// 4. All tolerances are much stricter than audio precision requirements:
212    ///    - 16-bit audio: ~6e-6 precision needed
213    ///    - 24-bit audio: ~6e-8 precision needed
214    ///    - Our tolerances: ~6e-7 to 2e-6 (10-1000x better than audio needs)
215    ///
216    /// Range context:
217    /// - Practical audio range (-60dB to +40dB): max errors ~1x ε
218    /// - Extended range (-100dB to +100dB): max errors ~2.3x ε
219    /// - Extreme edge cases beyond ±100dB have larger errors but are rarely used
220    ///
221    /// Based on [Wikipedia's Decibel article].
222    ///
223    /// [Wikipedia's Decibel article]: https://web.archive.org/web/20230810185300/https://en.wikipedia.org/wiki/Decibel
224    const DECIBELS_LINEAR_TABLE: [(Float, Float); 27] = [
225        (100., 100000.),
226        (90., 31623.),
227        (80., 10000.),
228        (70., 3162.),
229        (60., 1000.),
230        (50., 316.2),
231        (40., 100.),
232        (30., 31.62),
233        (20., 10.),
234        (10., 3.162),
235        (5.998, 1.995),
236        (3.003, 1.413),
237        (1.002, 1.122),
238        (0., 1.),
239        (-1.002, 0.891),
240        (-3.003, 0.708),
241        (-5.998, 0.501),
242        (-10., 0.3162),
243        (-20., 0.1),
244        (-30., 0.03162),
245        (-40., 0.01),
246        (-50., 0.003162),
247        (-60., 0.001),
248        (-70., 0.0003162),
249        (-80., 0.0001),
250        (-90., 0.00003162),
251        (-100., 0.00001),
252    ];
253
254    #[test]
255    fn convert_decibels_to_linear() {
256        for (db, wikipedia_linear) in DECIBELS_LINEAR_TABLE {
257            let actual_linear = db_to_linear(db);
258
259            // Sanity check: ensure we're in the right order of magnitude as Wikipedia data
260            // This is lenient to account for rounding in the reference values
261            let magnitude_ratio = actual_linear / wikipedia_linear;
262            assert!(
263                magnitude_ratio > 0.99 && magnitude_ratio < 1.01,
264                "Result magnitude differs significantly from Wikipedia reference for {db}dB: Wikipedia {wikipedia_linear}, got {actual_linear}, ratio: {magnitude_ratio:.4}"
265            );
266        }
267    }
268
269    #[test]
270    fn convert_linear_to_decibels() {
271        // Test the inverse conversion function using the same reference data
272        for (expected_db, linear) in DECIBELS_LINEAR_TABLE {
273            let actual_db = linear_to_db(linear);
274
275            // Sanity check: ensure we're reasonably close to the expected dB value from the table
276            // This accounts for rounding in both the linear and dB reference values
277            let magnitude_ratio = if expected_db.abs() > 10.0 * Float::EPSILON {
278                actual_db / expected_db
279            } else {
280                1.0 // Skip ratio check for values very close to 0 dB
281            };
282
283            if expected_db.abs() > 10.0 * Float::EPSILON {
284                assert!(
285                    magnitude_ratio > 0.99 && magnitude_ratio < 1.01,
286                    "Result differs significantly from table reference for linear {linear}: expected {expected_db}dB, got {actual_db}dB, ratio: {magnitude_ratio:.4}"
287                );
288            }
289        }
290    }
291
292    #[test]
293    fn round_trip_conversion_accuracy() {
294        // Test that converting dB -> linear -> dB gives back the original value
295        let test_db_values = [-60.0, -20.0, -6.0, 0.0, 6.0, 20.0, 40.0];
296
297        for &original_db in &test_db_values {
298            let linear = db_to_linear(original_db);
299            let round_trip_db = linear_to_db(linear);
300
301            let error = (round_trip_db - original_db).abs();
302            const MAX_ROUND_TRIP_ERROR: Float = 16.0 * Float::EPSILON; // max error: 8x ε (practical audio range), with 2x safety margin
303
304            assert!(
305                error < MAX_ROUND_TRIP_ERROR,
306                "Round-trip conversion failed for {original_db}dB: got {round_trip_db:.8}dB, error: {error:.2e}"
307            );
308        }
309
310        // Test that converting linear -> dB -> linear gives back the original value
311        let test_linear_values = [0.001, 0.1, 1.0, 10.0, 100.0];
312
313        for &original_linear in &test_linear_values {
314            let db = linear_to_db(original_linear);
315            let round_trip_linear = db_to_linear(db);
316
317            let relative_error = ((round_trip_linear - original_linear) / original_linear).abs();
318            const MAX_ROUND_TRIP_RELATIVE_ERROR: Float = 16.0 * Float::EPSILON; // Same as above, for linear->dB->linear round trips
319
320            assert!(
321                relative_error < MAX_ROUND_TRIP_RELATIVE_ERROR,
322                "Round-trip conversion failed for {original_linear}: got {round_trip_linear:.8}, relative error: {relative_error:.2e}"
323            );
324        }
325    }
326}