| 1 | //! db <-> linear conversions, time constants. |
| 2 | |
| 3 | /// lower bound to avoid `log10(0)`. |
| 4 | pub const PEAK_FLOOR: f32 = 1e-20; |
| 5 | |
| 6 | /// inputs at or below [`PEAK_FLOOR`] clamp to `-200 dB`. |
| 7 | #[must_use] |
| 8 | pub fn lin_to_db(x: f32) -> f32 { |
| 9 | if x <= PEAK_FLOOR { |
| 10 | -200.0 |
| 11 | } else { |
| 12 | 20.0 * x.log10() |
| 13 | } |
| 14 | } |
| 15 | |
| 16 | #[must_use] |
| 17 | pub fn db_to_lin(db: f32) -> f32 { |
| 18 | 10.0_f32.powf(db / 20.0) |
| 19 | } |
| 20 | |
| 21 | /// one-pole smoother coefficient for `y[n] = y[n-1] + alpha * (x[n] - y[n-1])`. |
| 22 | /// alpha = `1 - exp(-1 / (tau * fs))`, tau = `time_ms / 1000`. time_ms <= 0 → 1.0 (instant). |
| 23 | #[must_use] |
| 24 | pub fn time_to_alpha(time_ms: f32, sample_rate: f32) -> f32 { |
| 25 | if time_ms <= 0.0 || sample_rate <= 0.0 { |
| 26 | 1.0 |
| 27 | } else { |
| 28 | let tau_samples = (time_ms * 1e-3) * sample_rate; |
| 29 | 1.0 - (-1.0 / tau_samples).exp() |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | #[cfg(test)] |
| 34 | mod tests { |
| 35 | use super::*; |
| 36 | |
| 37 | #[test] |
| 38 | fn db_round_trips() { |
| 39 | for db in [-60.0, -20.0, -6.0, -0.1, 0.0, 3.0, 6.0_f32] { |
| 40 | let lin = db_to_lin(db); |
| 41 | let back = lin_to_db(lin); |
| 42 | assert!((back - db).abs() < 1e-3, "db={db} back={back}"); |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | #[test] |
| 47 | fn time_to_alpha_endpoints() { |
| 48 | assert_eq!(time_to_alpha(0.0, 48_000.0), 1.0); |
| 49 | assert!(time_to_alpha(1000.0, 48_000.0) < 0.01); |
| 50 | // Very fast attack: alpha approaches 1. |
| 51 | let a_fast = time_to_alpha(0.01, 48_000.0); |
| 52 | assert!(a_fast > 0.05, "fast alpha was {a_fast}"); |
| 53 | } |
| 54 | } |