Skip to main content

truce_core/
util.rs

1use truce_params::sample::Float;
2
3/// Convert decibels to linear gain.
4///
5/// Generic over `f32` and `f64` via [`Float`]. The bound is `Float`
6/// rather than `Sample` because a gain value isn't an audio sample
7/// - it's a scaling coefficient.
8#[inline]
9#[must_use]
10pub fn db_to_linear<F: Float>(db: F) -> F {
11    (db * F::from_f64(std::f64::consts::LN_10 / 20.0)).exp()
12}
13
14/// Convert linear gain to decibels.
15///
16/// Generic over `f32` and `f64` - see [`db_to_linear`].
17#[inline]
18#[must_use]
19pub fn linear_to_db<F: Float>(linear: F) -> F {
20    F::from_f64(20.0) * linear.log10()
21}
22
23/// Convert a MIDI note number to frequency in Hz (A4 = 440 Hz).
24///
25/// Generic over `f32` and `f64` - see [`db_to_linear`]. A frequency
26/// isn't an audio sample either; bound on [`Float`].
27#[inline]
28#[must_use]
29pub fn midi_note_to_freq<F: Float>(note: u8) -> F {
30    let semitones = F::from_f64(f64::from(note) - 69.0);
31    F::from_f64(440.0) * F::from_f64(2.0).powf(semitones / F::from_f64(12.0))
32}
33
34/// Convert a linear peak level to a smoothed 0.0–1.0 display value for meters.
35///
36/// Maps -60 dB → 0.0, 0 dB → 1.0 (linear scale in dB domain).
37/// Values above 0 dB clamp to 1.0. Silence (< -60 dB) maps to 0.0.
38/// Apply smoothing externally (e.g., exponential decay per frame).
39#[inline]
40#[must_use]
41pub fn meter_display(linear_peak: f32) -> f32 {
42    if linear_peak < 1e-6 {
43        return 0.0;
44    }
45    let db = 20.0 * linear_peak.log10();
46    // Map -60..0 dB → 0.0..1.0
47    ((db + 60.0) / 60.0).clamp(0.0, 1.0)
48}
49
50// `slugify` lives in `truce-utils` (dependency-free) so it can be
51// shared with `cargo-truce` without forcing the `truce-core` →
52// `truce-params` chain into the CLI's publish dependencies.
53// Re-exported at the crate root via `pub use truce_utils::slugify`.
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn db_linear_round_trip_f64() {
61        let db = -6.0_f64;
62        let linear = db_to_linear(db);
63        let back = linear_to_db(linear);
64        assert!((back - db).abs() < 1e-10);
65    }
66
67    #[test]
68    fn db_linear_round_trip_f32() {
69        // f32 carries ~7 decimal digits; the round-trip survives
70        // well under audible thresholds (1e-5 dB ≈ 200 dB below
71        // unity).
72        let db = -6.0_f32;
73        let linear = db_to_linear(db);
74        let back = linear_to_db(linear);
75        assert!((back - db).abs() < 1e-5);
76    }
77
78    #[test]
79    fn zero_db_is_unity_f64() {
80        let linear: f64 = db_to_linear(0.0_f64);
81        assert!((linear - 1.0).abs() < 1e-10);
82    }
83
84    #[test]
85    fn zero_db_is_unity_f32() {
86        let linear: f32 = db_to_linear(0.0_f32);
87        assert!((linear - 1.0).abs() < 1e-6);
88    }
89
90    #[test]
91    fn a4_is_440_f64() {
92        let freq: f64 = midi_note_to_freq(69);
93        assert!((freq - 440.0).abs() < 1e-10);
94    }
95
96    #[test]
97    fn a4_is_440_f32() {
98        let freq: f32 = midi_note_to_freq(69);
99        assert!((freq - 440.0).abs() < 1e-3);
100    }
101}