Skip to main content

embedded_dsp/
companding.rs

1//! Audio companding: the non-linear µ-law and "A"-law compression curves used by the ITU-T
2//! G.711 telephony standard to compress a wide-dynamic-range linear audio sample down to a
3//! lower bit depth with minimal perceptual loss (Steven W. Smith, "The Scientist and
4//! Engineer's Guide to DSP", Ch. 22, Eq. 22-1 / 22-2).
5//!
6//! All functions operate on samples normalized to `-1.0..=1.0`.
7
8#[allow(unused_imports)]
9use crate::math::FloatMath;
10
11const MU: f32 = 255.0;
12const A_LAW: f32 = 87.6;
13// Precomputed so the per-sample hot path only needs one transcendental call, not two.
14const LN_1P_MU: f32 = 5.545_177_5; // ln(1 + MU)
15const LN_A_LAW: f32 = 4.472_781; // ln(A_LAW)
16const A_LAW_DENOM: f32 = 1.0 + LN_A_LAW; // 1 + ln(A_LAW)
17
18#[inline(always)]
19fn sign(x: f32) -> f32 {
20    if x < 0.0 { -1.0 } else { 1.0 }
21}
22
23/// Compresses a normalized linear sample `x` (`-1.0..=1.0`) using µ255-law companding
24/// (Eq. 22-1), expanding resolution for small amplitudes at the expense of large ones.
25pub fn mu_law_compress_f32(x: f32) -> f32 {
26    let ax = x.abs();
27    sign(x) * (1.0 + MU * ax).ln() / LN_1P_MU
28}
29
30/// Expands a µ-law-compressed sample `y` (`-1.0..=1.0`) back to a normalized linear sample,
31/// inverting [`mu_law_compress_f32`].
32pub fn mu_law_expand_f32(y: f32) -> f32 {
33    let ay = y.abs();
34    sign(y) * ((1.0 + MU).powf(ay) - 1.0) / MU
35}
36
37/// Compresses a normalized linear sample `x` (`-1.0..=1.0`) using "A"-law companding
38/// (Eq. 22-2): a linear segment near zero, transitioning to a logarithmic curve.
39pub fn a_law_compress_f32(x: f32) -> f32 {
40    let ax = x.abs();
41    if ax < 1.0 / A_LAW {
42        sign(x) * (A_LAW * ax) / A_LAW_DENOM
43    } else {
44        sign(x) * (1.0 + (A_LAW * ax).ln()) / A_LAW_DENOM
45    }
46}
47
48/// Expands an "A"-law-compressed sample `y` (`-1.0..=1.0`) back to a normalized linear sample,
49/// inverting [`a_law_compress_f32`].
50pub fn a_law_expand_f32(y: f32) -> f32 {
51    let ay = y.abs();
52    if ay < 1.0 / A_LAW_DENOM {
53        sign(y) * ay * A_LAW_DENOM / A_LAW
54    } else {
55        sign(y) * (ay * A_LAW_DENOM - 1.0).exp() / A_LAW
56    }
57}
58
59// --- ITU-T G.711 8-bit bytes (Sun/CCITT linear ↔ μ-law / A-law) ---
60
61fn top_bit(x: i32) -> i32 {
62    if x <= 0 {
63        -1
64    } else {
65        31 - (x as u32).leading_zeros() as i32
66    }
67}
68
69/// Encode a 16-bit linear PCM sample to a G.711 μ-law byte.
70pub fn linear_to_ulaw(sample: i16) -> u8 {
71    const BIAS: i32 = 0x84;
72    let mut pcm = sample as i32;
73    let mask = if pcm < 0 {
74        pcm = BIAS - pcm;
75        0x7F
76    } else {
77        pcm += BIAS;
78        0xFF
79    };
80    if pcm > 0x7FFF {
81        pcm = 0x7FFF;
82    }
83    let seg = top_bit(pcm | 0xFF) - 7;
84    if seg >= 8 {
85        (0x7F ^ mask) as u8
86    } else {
87        let uval = (seg << 4) | ((pcm >> (seg + 3)) & 0x0F);
88        (uval ^ mask) as u8
89    }
90}
91
92/// Decode a G.711 μ-law byte to 16-bit linear PCM.
93pub fn ulaw_to_linear(u: u8) -> i16 {
94    const BIAS: i32 = 0x84;
95    let u = (!u) as i32;
96    let mut t = ((u & 0x0F) << 3) + BIAS;
97    t <<= (u & 0x70) >> 4;
98    let out = if u & 0x80 != 0 { BIAS - t } else { t - BIAS };
99    out.clamp(i16::MIN as i32, i16::MAX as i32) as i16
100}
101
102/// Encode a 16-bit linear PCM sample to a G.711 A-law byte.
103pub fn linear_to_alaw(sample: i16) -> u8 {
104    let mut pcm = sample as i32;
105    let mask = if pcm >= 0 {
106        0xD5
107    } else {
108        pcm = -pcm - 8;
109        0x55
110    };
111    let seg = top_bit(pcm | 0xFF) - 7;
112    if seg >= 8 {
113        (0x7F ^ mask) as u8
114    } else {
115        let shift = if seg != 0 { seg + 3 } else { 4 };
116        let aval = (seg << 4) | ((pcm >> shift) & 0x0F);
117        (aval ^ mask) as u8
118    }
119}
120
121/// Decode a G.711 A-law byte to 16-bit linear PCM.
122pub fn alaw_to_linear(a: u8) -> i16 {
123    let a = (a ^ 0x55) as i32;
124    let mut t = (a & 0x0F) << 4;
125    let seg = (a & 0x70) >> 4;
126    if seg != 0 {
127        t = (t + 0x108) << (seg - 1);
128    } else {
129        t += 8;
130    }
131    let out = if a & 0x80 != 0 { t } else { -t };
132    out.clamp(i16::MIN as i32, i16::MAX as i32) as i16
133}