Skip to main content

embedded_audio/
fixed.rs

1/// 32-bit phase accumulator (upper bits index wavetables).
2pub type Phase = u32;
3
4/// Convert Hz to phase increment per sample: `(freq << 32) / sample_rate`.
5#[inline]
6pub const fn hz_to_phase_inc(freq_hz: u32, sample_rate_hz: u32) -> u32 {
7    if sample_rate_hz == 0 {
8        0
9    } else {
10        (((freq_hz as u64) << 32) / (sample_rate_hz as u64)) as u32
11    }
12}
13
14/// Upper 8 bits of phase, 0..=255.
15#[inline]
16pub const fn phase_index(phase: Phase) -> u8 {
17    (phase >> 24) as u8
18}
19
20/// Parabolic sine approximation on 0..=255 index → -127..=127.
21#[inline]
22pub fn sin_table(index: u8) -> i8 {
23    let x = index as i32;
24    let y = if x < 128 { x * 2 } else { (255 - x) * 2 };
25    let quad = (y * (255 - y)) >> 8;
26    let s = if x < 128 { quad } else { -quad };
27    s.clamp(-127, 127) as i8
28}
29
30/// Linear interpolation between two i8 table entries; `frac` is 0..=255.
31#[inline]
32pub fn lerp_i8(a: i8, b: i8, frac: u8) -> i8 {
33    let ai = a as i32;
34    let bi = b as i32;
35    let v = ai + ((bi - ai) * frac as i32) / 256;
36    v.clamp(-128, 127) as i8
37}
38
39/// Apply Q8 gain (0..=255) to sample.
40#[inline]
41pub fn apply_gain_q8(sample: i8, gain_q8: u8) -> i8 {
42    ((sample as i32 * gain_q8 as i32) >> 8).clamp(-128, 127) as i8
43}
44
45/// Mix two samples with crossfade weight `t_q8` (0 = a only, 255 = b only).
46#[inline]
47pub fn mix_crossfade(a: i8, b: i8, t_q8: u8) -> i8 {
48    let ai = a as i32;
49    let bi = b as i32;
50    let t = t_q8 as i32;
51    let v = ai + ((bi - ai) * t) / 256;
52    v.clamp(-128, 127) as i8
53}
54
55/// Soft clamp to 8-bit audio range.
56#[inline]
57pub fn clamp_sample(v: i32) -> i8 {
58    v.clamp(-128, 127) as i8
59}
60
61/// Convert decibel value (-48.0 dB .. 0.0 dB) to Q8 gain (0 ..= 255).
62///
63/// 0.0 dB maps to 255 (1.0x).
64/// -6.0 dB maps to ~128 (0.5x).
65/// -48.0 dB or lower maps to 0.
66#[allow(clippy::approx_constant)]
67pub fn db_to_q8(db: f32) -> u8 {
68    if db >= 0.0 {
69        return 255;
70    }
71    if db <= -48.0 {
72        return 0;
73    }
74    // Fast fixed-point approximation for 10^(db / 20) in no_std without libm dependency
75    // db is negative, so db / 20 is in range [-2.4, 0]
76    // 10^x ≈ 1 + 2.3026*x + 2.651*x^2 + 2.03*x^3
77    let x = db / 20.0;
78    let lin = 1.0 + 2.3026 * x + 2.651 * x * x + 2.03 * x * x * x;
79    (lin * 255.0).clamp(0.0, 255.0) as u8
80}
81
82/// Convert Q8 gain (0 ..= 255) to decibels (-inf .. 0.0 dB).
83#[allow(clippy::approx_constant)]
84pub fn q8_to_db(gain_q8: u8) -> f32 {
85    if gain_q8 == 0 {
86        return -96.0;
87    }
88    let lin = gain_q8 as f32 / 255.0;
89    // Log base 10 approximation using log2: log10(x) = log2(x) * 0.30103
90    // For lin in (0, 1], fast log2 approximation:
91    let bits = lin.to_bits();
92    let exp = ((bits >> 23) & 0xFF) as i32 - 127;
93    let mantissa = f32::from_bits((bits & 0x007F_FFFF) | 0x3F80_0000);
94    let log2_val = exp as f32 + (mantissa - 1.0) * (1.4427 - 0.4427 * (mantissa - 1.0));
95    let log10_val = log2_val * 0.30103;
96    (20.0 * log10_val).clamp(-96.0, 0.0)
97}