rodio/
math.rs

1//! Math utilities for audio processing.
2
3/// Linear interpolation between two samples.
4///
5/// The result should be equivalent to
6/// `first * (1 - numerator / denominator) + second * numerator / denominator`.
7///
8/// To avoid numeric overflows pick smaller numerator.
9// TODO (refactoring) Streamline this using coefficient instead of numerator and denominator.
10#[inline]
11pub(crate) fn lerp(first: &f32, second: &f32, numerator: u32, denominator: u32) -> f32 {
12    first + (second - first) * numerator as f32 / denominator as f32
13}
14
15/// Converts decibels to linear amplitude scale.
16///
17/// This function converts a decibel value to its corresponding linear amplitude value
18/// using the formula: `linear = 10^(decibels/20)` for amplitude.
19///
20/// # Arguments
21///
22/// * `decibels` - The decibel value to convert. Common ranges:
23///   - 0 dB = linear value of 1.0 (no change)
24///   - Positive dB values represent amplification (> 1.0)
25///   - Negative dB values represent attenuation (< 1.0)
26///   - -60 dB ≈ 0.001 (barely audible)
27///   - +20 dB = 10.0 (10x amplification)
28///
29/// # Returns
30///
31/// The linear amplitude value corresponding to the input decibels.
32///
33/// # Performance
34///
35/// This implementation is optimized for speed, being ~3-4% faster than the standard
36/// `10f32.powf(decibels * 0.05)` approach, with a maximum error of only 2.48e-7
37/// (representing about -132 dB precision).
38#[inline]
39pub fn db_to_linear(decibels: f32) -> f32 {
40    // ~3-4% faster than using `10f32.powf(decibels * 0.05)`,
41    // with a maximum error of 2.48e-7 representing only about -132 dB.
42    2.0f32.powf(decibels * 0.05 * std::f32::consts::LOG2_10)
43}
44
45/// Converts linear amplitude scale to decibels.
46///
47/// This function converts a linear amplitude value to its corresponding decibel value
48/// using the formula: `decibels = 20 * log10(linear)` for amplitude.
49///
50/// # Arguments
51///
52/// * `linear` - The linear amplitude value to convert. Must be positive for meaningful results:
53///   - 1.0 = 0 dB (no change)
54///   - Values > 1.0 represent amplification (positive dB)
55///   - Values < 1.0 represent attenuation (negative dB)
56///   - 0.0 results in negative infinity
57///   - Negative values are not physically meaningful for amplitude
58///
59/// # Returns
60///
61/// The decibel value corresponding to the input linear amplitude.
62///
63/// # Performance
64///
65/// This implementation is optimized for speed, being faster than the standard
66/// `20.0 * linear.log10()` approach while maintaining high precision.
67///
68/// # Special Cases
69///
70/// - `linear_to_db(0.0)` returns negative infinity
71/// - Very small positive values approach negative infinity
72/// - Negative values return NaN (not physically meaningful for amplitude)
73#[inline]
74pub fn linear_to_db(linear: f32) -> f32 {
75    // Same as `to_linear`: faster than using `20f32.log10() * linear`
76    linear.log2() * std::f32::consts::LOG10_2 * 20.0
77}
78
79#[cfg(test)]
80mod test {
81    use super::*;
82    use num_rational::Ratio;
83    use quickcheck::{quickcheck, TestResult};
84
85    quickcheck! {
86        fn lerp_f32_random(first: u16, second: u16, numerator: u16, denominator: u16) -> TestResult {
87            if denominator == 0 { return TestResult::discard(); }
88
89            let (numerator, denominator) = Ratio::new(numerator, denominator).into_raw();
90            if numerator > 5000 { return TestResult::discard(); }
91
92            let a = first as f64;
93            let b = second as f64;
94            let c = numerator as f64 / denominator as f64;
95            if !(0.0..=1.0).contains(&c) { return TestResult::discard(); };
96
97            let reference = a * (1.0 - c) + b * c;
98            let x = lerp(&(first as f32), &(second as f32), numerator as u32, denominator as u32) as f64;
99            // TODO (review) It seems that the diff tolerance should be a lot lower. Why lerp so imprecise?
100            TestResult::from_bool((x - reference).abs() < 0.01)
101        }
102    }
103
104    /// Tolerance values for precision tests, derived from empirical measurement
105    /// of actual implementation errors across the full ±100dB range.
106    ///
107    /// Methodology:
108    /// 1. Calculated relative errors against mathematically exact `f64` calculations
109    /// 2. Found maximum errors: dB->linear = 2.3x ε, linear->dB = 1.0x ε, round-trip = 8x ε
110    /// 3. Applied 2x safety margins for cross-platform robustness
111    /// 4. All tolerances are much stricter than audio precision requirements:
112    ///    - 16-bit audio: ~6e-6 precision needed
113    ///    - 24-bit audio: ~6e-8 precision needed
114    ///    - Our tolerances: ~6e-7 to 2e-6 (10-1000x better than audio needs)
115    ///
116    /// Range context:
117    /// - Practical audio range (-60dB to +40dB): max errors ~1x ε
118    /// - Extended range (-100dB to +100dB): max errors ~2.3x ε
119    /// - Extreme edge cases beyond ±100dB have larger errors but are rarely used
120
121    /// Based on [Wikipedia's Decibel article].
122    ///
123    /// [Wikipedia's Decibel article]: https://web.archive.org/web/20230810185300/https://en.wikipedia.org/wiki/Decibel
124    const DECIBELS_LINEAR_TABLE: [(f32, f32); 27] = [
125        (100., 100000.),
126        (90., 31623.),
127        (80., 10000.),
128        (70., 3162.),
129        (60., 1000.),
130        (50., 316.2),
131        (40., 100.),
132        (30., 31.62),
133        (20., 10.),
134        (10., 3.162),
135        (5.998, 1.995),
136        (3.003, 1.413),
137        (1.002, 1.122),
138        (0., 1.),
139        (-1.002, 0.891),
140        (-3.003, 0.708),
141        (-5.998, 0.501),
142        (-10., 0.3162),
143        (-20., 0.1),
144        (-30., 0.03162),
145        (-40., 0.01),
146        (-50., 0.003162),
147        (-60., 0.001),
148        (-70., 0.0003162),
149        (-80., 0.0001),
150        (-90., 0.00003162),
151        (-100., 0.00001),
152    ];
153
154    #[test]
155    fn convert_decibels_to_linear() {
156        for (db, wikipedia_linear) in DECIBELS_LINEAR_TABLE {
157            let actual_linear = db_to_linear(db);
158
159            // Calculate the mathematically exact reference value using f64 precision
160            let exact_linear = f64::powf(10.0, db as f64 * 0.05) as f32;
161
162            // Test implementation precision against exact mathematical result
163            let relative_error = ((actual_linear - exact_linear) / exact_linear).abs();
164            const MAX_RELATIVE_ERROR: f32 = 5.0 * f32::EPSILON; // max error: 2.3x ε (at -100dB), with 2x safety margin
165
166            assert!(
167                relative_error < MAX_RELATIVE_ERROR,
168                "Implementation precision failed for {db}dB: exact {exact_linear:.8}, got {actual_linear:.8}, relative error: {relative_error:.2e}"
169            );
170
171            // Sanity check: ensure we're in the right order of magnitude as Wikipedia data
172            // This is lenient to account for rounding in the reference values
173            let magnitude_ratio = actual_linear / wikipedia_linear;
174            assert!(
175                magnitude_ratio > 0.99 && magnitude_ratio < 1.01,
176                "Result magnitude differs significantly from Wikipedia reference for {db}dB: Wikipedia {wikipedia_linear}, got {actual_linear}, ratio: {magnitude_ratio:.4}"
177            );
178        }
179    }
180
181    #[test]
182    fn convert_linear_to_decibels() {
183        // Test the inverse conversion function using the same reference data
184        for (expected_db, linear) in DECIBELS_LINEAR_TABLE {
185            let actual_db = linear_to_db(linear);
186
187            // Calculate the mathematically exact reference value using f64 precision
188            let exact_db = ((linear as f64).log10() * 20.0) as f32;
189
190            // Test implementation precision against exact mathematical result
191            if exact_db.abs() > 10.0 * f32::EPSILON {
192                // Use relative error for non-zero dB values
193                let relative_error = ((actual_db - exact_db) / exact_db.abs()).abs();
194                const MAX_RELATIVE_ERROR: f32 = 5.0 * f32::EPSILON; // max error: 1.0x ε, with 5x safety margin
195
196                assert!(
197                    relative_error < MAX_RELATIVE_ERROR,
198                    "Linear to dB conversion precision failed for {linear}: exact {exact_db:.8}, got {actual_db:.8}, relative error: {relative_error:.2e}"
199                );
200            } else {
201                // Use absolute error for values very close to 0 dB (linear ≈ 1.0)
202                let absolute_error = (actual_db - exact_db).abs();
203                const MAX_ABSOLUTE_ERROR: f32 = 1.0 * f32::EPSILON; // 0 dB case is mathematically exact, minimal tolerance for numerical stability
204
205                assert!(
206                    absolute_error < MAX_ABSOLUTE_ERROR,
207                    "Linear to dB conversion precision failed for {linear}: exact {exact_db:.8}, got {actual_db:.8}, absolute error: {absolute_error:.2e}"
208                );
209            }
210
211            // Sanity check: ensure we're reasonably close to the expected dB value from the table
212            // This accounts for rounding in both the linear and dB reference values
213            let magnitude_ratio = if expected_db.abs() > 10.0 * f32::EPSILON {
214                actual_db / expected_db
215            } else {
216                1.0 // Skip ratio check for values very close to 0 dB
217            };
218
219            if expected_db.abs() > 10.0 * f32::EPSILON {
220                assert!(
221                    magnitude_ratio > 0.99 && magnitude_ratio < 1.01,
222                    "Result differs significantly from table reference for linear {linear}: expected {expected_db}dB, got {actual_db}dB, ratio: {magnitude_ratio:.4}"
223                );
224            }
225        }
226    }
227
228    #[test]
229    fn round_trip_conversion_accuracy() {
230        // Test that converting dB -> linear -> dB gives back the original value
231        let test_db_values = [-60.0, -20.0, -6.0, 0.0, 6.0, 20.0, 40.0];
232
233        for &original_db in &test_db_values {
234            let linear = db_to_linear(original_db);
235            let round_trip_db = linear_to_db(linear);
236
237            let error = (round_trip_db - original_db).abs();
238            const MAX_ROUND_TRIP_ERROR: f32 = 16.0 * f32::EPSILON; // max error: 8x ε (practical audio range), with 2x safety margin
239
240            assert!(
241                error < MAX_ROUND_TRIP_ERROR,
242                "Round-trip conversion failed for {original_db}dB: got {round_trip_db:.8}dB, error: {error:.2e}"
243            );
244        }
245
246        // Test that converting linear -> dB -> linear gives back the original value
247        let test_linear_values = [0.001, 0.1, 1.0, 10.0, 100.0];
248
249        for &original_linear in &test_linear_values {
250            let db = linear_to_db(original_linear);
251            let round_trip_linear = db_to_linear(db);
252
253            let relative_error = ((round_trip_linear - original_linear) / original_linear).abs();
254            const MAX_ROUND_TRIP_RELATIVE_ERROR: f32 = 16.0 * f32::EPSILON; // Same as above, for linear->dB->linear round trips
255
256            assert!(
257                relative_error < MAX_ROUND_TRIP_RELATIVE_ERROR,
258                "Round-trip conversion failed for {original_linear}: got {round_trip_linear:.8}, relative error: {relative_error:.2e}"
259            );
260        }
261    }
262}