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}