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}