use crate::DitherSource;
#[inline]
pub fn quantize_dithered(x: f32, bits: u32, eps: f32, source: &mut impl DitherSource) -> f32 {
assert!(bits >= 2 && bits <= 31, "bits must be in [2, 31]");
let qmax = ((1u32 << (bits - 1)) - 1) as f32;
let lsb = 1.0 / qmax;
let dither = source.next(eps * lsb);
let shifted = (x + dither) * qmax;
let rounded = shifted.round().clamp(-qmax, qmax);
rounded / qmax
}
pub fn quantize_slice_dithered(
xs: &mut [f32],
bits: u32,
eps: f32,
source: &mut impl DitherSource,
) {
assert!(bits >= 2 && bits <= 31, "bits must be in [2, 31]");
let qmax = ((1u32 << (bits - 1)) - 1) as f32;
let lsb = 1.0 / qmax;
for x in xs.iter_mut() {
let dither = source.next(eps * lsb);
let shifted = (*x + dither) * qmax;
*x = shifted.round().clamp(-qmax, qmax) / qmax;
}
}
#[inline]
pub fn quantize_to_code(x: f32, bits: u32, eps: f32, source: &mut impl DitherSource) -> i32 {
assert!(bits >= 2 && bits <= 31, "bits must be in [2, 31]");
let qmax = ((1u32 << (bits - 1)) - 1) as f32;
let lsb = 1.0 / qmax;
let dither = source.next(eps * lsb);
((x + dither) * qmax).round().clamp(-qmax, qmax) as i32
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{GoldenRatioDither, PiDither};
#[test]
fn output_in_unit_range() {
let mut d = GoldenRatioDither::new(0.0);
for bits in [3u32, 5, 7, 8] {
for &x in &[-1.0_f32, -0.5, 0.0, 0.5, 1.0] {
let q = quantize_dithered(x, bits, 0.5, &mut d);
assert!(q >= -1.0 && q <= 1.0, "bits={bits}, x={x}, q={q}");
}
}
}
#[test]
fn dither_reduces_idle_tones() {
let bits = 5u32;
let qmax = ((1u32 << (bits - 1)) - 1) as f32;
let lsb = 1.0 / qmax;
let x = 0.5 * lsb;
let mut codes_with: Vec<i32> = Vec::with_capacity(256);
let mut d = GoldenRatioDither::new(0.0);
for _ in 0..256 {
codes_with.push(quantize_to_code(x, bits, 0.5, &mut d));
}
let unique: std::collections::HashSet<i32> = codes_with.iter().copied().collect();
assert!(unique.len() > 1, "dithered signal must produce >1 unique code");
}
#[test]
fn slice_quantize_in_bounds() {
let mut vals: Vec<f32> = (-50..=50).map(|i| i as f32 * 0.02).collect();
let mut pi = PiDither::new(0);
quantize_slice_dithered(&mut vals, 7, 0.5, &mut pi);
for v in vals {
assert!(v >= -1.0 && v <= 1.0, "out of range: {v}");
}
}
#[test]
fn deterministic_with_same_seed() {
let input = vec![0.1_f32, 0.4, -0.7, 0.9];
let quantize = |input: &[f32]| {
let mut buf = input.to_vec();
let mut d = GoldenRatioDither::new(0.5);
quantize_slice_dithered(&mut buf, 8, 0.5, &mut d);
buf
};
assert_eq!(quantize(&input), quantize(&input));
}
}