use crate::fixed::{Q0_16, Q4_28};
#[must_use]
pub(crate) const fn div_65535(n: u32) -> u32 {
n.saturating_add(n >> 16).saturating_add(1) >> 16
}
#[must_use]
pub(crate) const fn rem_65535(n: u32, q: u32) -> u16 {
n.saturating_add(q).saturating_sub(q << 16) as u16
}
#[must_use]
pub(crate) const fn mul_acc(acc: i64, coef: Q4_28, ch: Q0_16) -> i64 {
let raw = coef.to_raw();
let ch = ch.to_raw() as u32;
let hi = (raw >> 16).saturating_mul(ch as i32);
let lo = (raw as u32 & 0xffff).saturating_mul(ch);
let product = ((hi as i64) << 16).saturating_add(lo as i64);
acc.saturating_add(product)
}
#[must_use]
pub(crate) const fn mul_acc3(coefs: [Q4_28; 3], chs: [Q0_16; 3]) -> Q0_16 {
let [c0, c1, c2] = coefs;
let [x, y, z] = chs;
let acc = mul_acc(0, c0, x);
let acc = mul_acc(acc, c1, y);
let acc = mul_acc(acc, c2, z);
shift_round_sat_q0_16(acc)
}
#[must_use]
pub(crate) const fn shift_round_sat_q0_16(acc: i64) -> Q0_16 {
sat_i64_to_q0_16(shift_round_away_from_zero(acc, Q4_28::FRAC_BITS))
}
#[must_use]
pub(crate) const fn shift_round_away_from_zero(value: i64, shift: u32) -> i64 {
if shift == 0 {
return value;
}
let Some(divisor) = 1i64.checked_shl(shift) else {
return if value < 0 { i64::MIN } else { i64::MAX };
};
let half = divisor >> 1;
let mask = divisor.saturating_sub(1);
let sign = value >> 63;
let biased = value.saturating_add((half ^ sign).saturating_sub(sign));
biased.saturating_add(biased >> 63 & mask) >> shift
}
const fn sat_i64_to_q0_16(value: i64) -> Q0_16 {
let low = (value.saturating_sub(1) >> 63) as u64; let high = (65534i64.saturating_sub(value) >> 63) as u64; let clamped = ((value as u64) & !low & !high) | (high & u16::MAX as u64);
Q0_16::from_raw(clamped as u16)
}
#[must_use]
pub(crate) const fn lerp_q0_16(a: Q0_16, b: Q0_16, t: Q0_16) -> Q0_16 {
let den = u16::MAX as u32;
let t = t.to_raw() as u32;
let num = (a.to_raw() as u32)
.saturating_mul(den.saturating_sub(t))
.saturating_add((b.to_raw() as u32).saturating_mul(t))
.saturating_add(den >> 1);
Q0_16::from_raw(div_65535(num) as u16)
}
#[cfg(test)]
mod tests {
use super::*;
const COEF_FRAC_BITS: u32 = Q4_28::FRAC_BITS;
const HALF_LSB: i64 = 1 << (COEF_FRAC_BITS - 1);
const ZERO: Q4_28 = Q4_28::ZERO;
const ONE: Q4_28 = Q4_28::ONE;
fn ch(raw: u16) -> Q0_16 {
Q0_16::from_raw(raw)
}
#[test]
fn identity_gain_preserves_full_scale() {
assert_eq!(
mul_acc3([ONE, ZERO, ZERO], [ch(65535), ch(0), ch(0)]),
ch(65535)
);
assert_eq!(mul_acc3([ONE, ZERO, ZERO], [ch(0), ch(0), ch(0)]), ch(0));
assert_eq!(mul_acc3([ONE, ZERO, ZERO], [ch(1), ch(0), ch(0)]), ch(1));
}
#[test]
fn positive_midpoint_rounds_away_from_zero() {
let acc = (3i64 << COEF_FRAC_BITS).saturating_add(HALF_LSB);
assert_eq!(shift_round_away_from_zero(acc, COEF_FRAC_BITS), 4);
assert_eq!(shift_round_sat_q0_16(acc), ch(4));
}
#[test]
fn negative_midpoint_rounds_away_from_zero() {
let acc = (-3i64 << COEF_FRAC_BITS).saturating_sub(HALF_LSB);
assert_eq!(shift_round_away_from_zero(acc, COEF_FRAC_BITS), -4);
assert_eq!(shift_round_sat_q0_16(acc), ch(0));
}
#[test]
fn below_half_does_not_round_away() {
let acc = (3i64 << COEF_FRAC_BITS).saturating_add(HALF_LSB.saturating_sub(1));
assert_eq!(shift_round_away_from_zero(acc, COEF_FRAC_BITS), 3);
let neg = (-3i64 << COEF_FRAC_BITS).saturating_add(1 - HALF_LSB);
assert_eq!(shift_round_away_from_zero(neg, COEF_FRAC_BITS), -3);
}
#[test]
fn half_unit_channel_rounds_away() {
let half = Q4_28::from_raw(ONE.to_raw() / 2);
assert_eq!(shift_round_sat_q0_16(mul_acc(0, half, ch(1))), ch(1));
}
#[test]
fn positive_overflow_saturates_to_u16_max() {
let max = Q4_28::from_raw(i32::MAX);
let acc = mul_acc(0, max, ch(65535));
let acc = mul_acc(acc, max, ch(65535));
let acc = mul_acc(acc, max, ch(65535));
assert!(
acc > i64::from(i32::MAX),
"three-term product must not fit in i32"
);
assert_eq!(shift_round_sat_q0_16(acc), ch(65535));
assert_eq!(
mul_acc3([max, max, max], [ch(65535), ch(65535), ch(65535)]),
ch(65535)
);
}
#[test]
fn negative_overflow_saturates_to_zero() {
let min = Q4_28::from_raw(i32::MIN);
assert_eq!(
mul_acc3([min, min, min], [ch(65535), ch(65535), ch(65535)]),
ch(0)
);
}
#[test]
fn div_and_rem_65535_match_hardware_over_the_proven_range() {
const MAX: u32 = 65535 * 65535 + 32767;
let mut n: u32 = 0;
while n <= MAX {
let q = div_65535(n);
assert_eq!(q, n / 65535, "quotient at n={n}");
assert_eq!(u32::from(rem_65535(n, q)), n % 65535, "remainder at n={n}");
n = match n.checked_add(65_521) {
Some(next) => next,
None => break,
};
}
for n in [
0u32,
1,
65534,
65535,
65536,
65535 * 65535 - 1,
65535 * 65535,
MAX,
] {
let q = div_65535(n);
assert_eq!(q, n / 65535, "quotient at boundary n={n}");
assert_eq!(u32::from(rem_65535(n, q)), n % 65535, "remainder at n={n}");
}
}
#[test]
fn rem_65535_of_a_clamped_index_is_full_scale() {
let pos: u32 = 65535 * 256;
assert_eq!(rem_65535(pos, 255), u16::MAX);
}
#[test]
fn lerp_matches_explicit_round_half_up() {
for a in [0u16, 1, 12345, 32768, 65534, 65535] {
for b in [0u16, 1, 999, 40000, 65535] {
for t in [0u16, 1, 16384, 32767, 32768, 65534, 65535] {
let num = u32::from(a) * (65535 - u32::from(t)) + u32::from(b) * u32::from(t);
let (q, r) = (num / 65535, num % 65535);
let want = if r * 2 >= 65535 { q + 1 } else { q };
assert_eq!(
lerp_q0_16(ch(a), ch(b), ch(t)).to_raw(),
want as u16,
"a={a} b={b} t={t}"
);
}
}
}
}
#[test]
fn lerp_endpoints_need_no_special_case() {
for a in [0u16, 7, 30000, 65535] {
for b in [0u16, 9, 51234, 65535] {
assert_eq!(lerp_q0_16(ch(a), ch(b), Q0_16::ZERO), ch(a));
assert_eq!(lerp_q0_16(ch(a), ch(b), Q0_16::ONE), ch(b));
}
}
}
#[test]
fn mul_acc_is_the_exact_widening_product() {
for coef in [
i32::MIN,
i32::MIN + 1,
-(7 << 28),
-1,
0,
1,
7 << 28,
i32::MAX,
1 << 28,
] {
for chan in [0u16, 1, 2, 32767, 32768, 65534, 65535] {
assert_eq!(
mul_acc(0, Q4_28::from_raw(coef), ch(chan)),
i64::from(coef) * i64::from(chan),
"coef={coef} ch={chan}"
);
}
}
}
#[test]
fn saturation_boundaries_are_exact() {
assert_eq!(sat_i64_to_q0_16(i64::MIN), Q0_16::ZERO);
assert_eq!(sat_i64_to_q0_16(-1), Q0_16::ZERO);
assert_eq!(sat_i64_to_q0_16(0), Q0_16::ZERO);
assert_eq!(sat_i64_to_q0_16(1), ch(1));
assert_eq!(sat_i64_to_q0_16(65534), ch(65534));
assert_eq!(sat_i64_to_q0_16(65535), Q0_16::ONE);
assert_eq!(sat_i64_to_q0_16(i64::MAX), Q0_16::ONE);
}
#[test]
fn exact_integers_do_not_round() {
let acc = 5i64 << COEF_FRAC_BITS;
assert_eq!(shift_round_away_from_zero(acc, COEF_FRAC_BITS), 5);
assert_eq!(shift_round_sat_q0_16(acc), ch(5));
}
}