pub const DEEMPHASIS_ALPHA_P: f64 = 0.850_006_103_5;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DeemphasisFilter {
mem: f64,
}
impl Default for DeemphasisFilter {
fn default() -> Self {
Self::new()
}
}
impl DeemphasisFilter {
#[must_use]
pub fn new() -> Self {
Self { mem: 0.0 }
}
#[must_use]
pub fn with_memory(mem: f64) -> Self {
Self { mem }
}
#[must_use]
pub fn memory(&self) -> f64 {
self.mem
}
pub fn reset(&mut self) {
self.mem = 0.0;
}
#[must_use]
pub fn step(&mut self, x: f64) -> f64 {
let y = x + DEEMPHASIS_ALPHA_P * self.mem;
self.mem = y;
y
}
pub fn process_in_place(&mut self, samples: &mut [f64]) {
for x in samples.iter_mut() {
*x = self.step(*x);
}
}
pub fn process(&mut self, input: &[f64], output: &mut [f64]) -> Result<usize, DeemphasisError> {
if output.len() < input.len() {
return Err(DeemphasisError::OutputBufferTooSmall {
input_len: input.len(),
output_len: output.len(),
});
}
for (i, &x) in input.iter().enumerate() {
output[i] = self.step(x);
}
Ok(input.len())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeemphasisError {
OutputBufferTooSmall {
input_len: usize,
output_len: usize,
},
}
impl core::fmt::Display for DeemphasisError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
DeemphasisError::OutputBufferTooSmall {
input_len,
output_len,
} => write!(
f,
"de-emphasis output buffer too small: need {input_len} samples, got {output_len}"
),
}
}
}
impl std::error::Error for DeemphasisError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alpha_p_constant_matches_rfc() {
assert_eq!(DEEMPHASIS_ALPHA_P, 0.850_006_103_5);
const { assert!(DEEMPHASIS_ALPHA_P > 0.0 && DEEMPHASIS_ALPHA_P < 1.0) };
}
#[test]
fn new_has_zero_memory() {
let f = DeemphasisFilter::new();
assert_eq!(f.memory(), 0.0);
assert_eq!(DeemphasisFilter::default(), f);
}
#[test]
fn first_sample_passes_through() {
let mut f = DeemphasisFilter::new();
assert_eq!(f.step(3.5), 3.5);
assert_eq!(f.memory(), 3.5);
}
#[test]
fn recurrence_matches_hand_computation() {
let a = DEEMPHASIS_ALPHA_P;
let mut f = DeemphasisFilter::new();
let y0 = f.step(1.0);
let y1 = f.step(2.0);
let y2 = f.step(-1.0);
assert_eq!(y0, 1.0);
assert_eq!(y1, 2.0 + a * 1.0);
assert_eq!(y2, -1.0 + a * y1);
assert_eq!(f.memory(), y2);
}
#[test]
fn constant_input_converges_to_dc_gain() {
let a = DEEMPHASIS_ALPHA_P;
let dc_gain = 1.0 / (1.0 - a);
let mut f = DeemphasisFilter::new();
let mut y = 0.0;
for _ in 0..10_000 {
y = f.step(1.0);
}
assert!((y - dc_gain).abs() < 1e-6, "y={y} dc_gain={dc_gain}");
}
#[test]
fn inverts_pre_emphasis() {
let a = DEEMPHASIS_ALPHA_P;
let s = [0.3_f64, -0.7, 1.2, 0.0, -0.4, 0.9, 0.05, -1.1];
let mut prev = 0.0_f64;
let x: Vec<f64> = s
.iter()
.map(|&sn| {
let xn = sn - a * prev;
prev = sn;
xn
})
.collect();
let mut f = DeemphasisFilter::new();
for (i, &xn) in x.iter().enumerate() {
let yn = f.step(xn);
assert!(
(yn - s[i]).abs() < 1e-12,
"round-trip mismatch at {i}: got {yn}, want {}",
s[i]
);
}
}
#[test]
fn memory_carries_across_blocks() {
let stream = [0.5_f64, -0.2, 0.9, 1.0, -0.3, 0.1, 0.7, -0.8];
let mut whole = DeemphasisFilter::new();
let mut ref_out = stream;
whole.process_in_place(&mut ref_out);
let mut split = DeemphasisFilter::new();
let mut a = stream[..3].to_vec();
let mut b = stream[3..].to_vec();
split.process_in_place(&mut a);
split.process_in_place(&mut b);
for (i, v) in a.iter().chain(b.iter()).enumerate() {
assert_eq!(*v, ref_out[i], "block-split mismatch at {i}");
}
}
#[test]
fn with_memory_seeds_recurrence() {
let a = DEEMPHASIS_ALPHA_P;
let mut f = DeemphasisFilter::with_memory(2.0);
assert_eq!(f.memory(), 2.0);
assert_eq!(f.step(0.0), a * 2.0);
}
#[test]
fn reset_zeroes_memory() {
let mut f = DeemphasisFilter::new();
let _ = f.step(5.0);
assert_ne!(f.memory(), 0.0);
f.reset();
assert_eq!(f.memory(), 0.0);
}
#[test]
fn process_in_place_equals_stepping() {
let input = [1.0_f64, 2.0, 3.0, -1.0, 0.5];
let mut by_step = DeemphasisFilter::new();
let stepped: Vec<f64> = input.iter().map(|&x| by_step.step(x)).collect();
let mut by_block = DeemphasisFilter::new();
let mut block = input;
by_block.process_in_place(&mut block);
assert_eq!(block.to_vec(), stepped);
assert_eq!(by_step.memory(), by_block.memory());
}
#[test]
fn process_writes_output_buffer() {
let input = [0.1_f64, -0.2, 0.3, 0.4];
let mut out = [0.0_f64; 4];
let mut f = DeemphasisFilter::new();
let n = f.process(&input, &mut out).unwrap();
assert_eq!(n, 4);
let mut g = DeemphasisFilter::new();
let mut ref_out = input;
g.process_in_place(&mut ref_out);
assert_eq!(out, ref_out);
assert_eq!(f.memory(), g.memory());
}
#[test]
fn process_accepts_longer_output() {
let input = [1.0_f64, 2.0];
let mut out = [9.0_f64; 5];
let mut f = DeemphasisFilter::new();
let n = f.process(&input, &mut out).unwrap();
assert_eq!(n, 2);
assert_eq!(out[0], 1.0);
assert_eq!(out[1], 2.0 + DEEMPHASIS_ALPHA_P);
assert_eq!(&out[2..], &[9.0, 9.0, 9.0]);
}
#[test]
fn process_rejects_short_output() {
let input = [1.0_f64, 2.0, 3.0];
let mut out = [0.0_f64; 2];
let mut f = DeemphasisFilter::new();
let before = f.memory();
let err = f.process(&input, &mut out).unwrap_err();
assert_eq!(
err,
DeemphasisError::OutputBufferTooSmall {
input_len: 3,
output_len: 2,
}
);
assert_eq!(f.memory(), before, "state must be unchanged on error");
}
#[test]
fn empty_input_is_noop() {
let mut f = DeemphasisFilter::with_memory(1.5);
let mut empty: [f64; 0] = [];
f.process_in_place(&mut empty);
assert_eq!(f.memory(), 1.5);
let mut out: [f64; 0] = [];
assert_eq!(f.process(&[], &mut out).unwrap(), 0);
assert_eq!(f.memory(), 1.5);
}
#[test]
fn error_display() {
let s = DeemphasisError::OutputBufferTooSmall {
input_len: 4,
output_len: 1,
}
.to_string();
assert!(s.contains("need 4"));
assert!(s.contains("got 1"));
}
}