#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Emphasis {
Fm50,
Fm75,
CdIec,
}
pub fn deemphasis(signal: &[f32], sample_rate: u32, curve: Emphasis) -> Vec<f32> {
if signal.is_empty() || sample_rate == 0 {
return signal.to_vec();
}
let (num_tau, den_tau) = match curve {
Emphasis::Fm50 => (None, 50e-6f32),
Emphasis::Fm75 => (None, 75e-6f32),
Emphasis::CdIec => (Some(15e-6f32), 50e-6f32),
};
let k = 2.0 * sample_rate as f32;
let (n0, n1) = match num_tau {
Some(tau) => (1.0 + k * tau, 1.0 - k * tau),
None => (1.0, 1.0),
};
let d0 = 1.0 + k * den_tau;
let d1 = 1.0 - k * den_tau;
let (b0, b1, a1) = (n0 / d0, n1 / d0, d1 / d0);
let mut out = Vec::with_capacity(signal.len());
let (mut x1, mut y1) = (0.0f32, 0.0f32);
for &x in signal {
let y = b0 * x + b1 * x1 - a1 * y1;
out.push(y);
x1 = x;
y1 = y;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn tone(freq: f32, sr: u32, n: usize) -> Vec<f32> {
(0..n).map(|i| (2.0 * std::f32::consts::PI * freq * i as f32 / sr as f32).sin()).collect()
}
fn rms(x: &[f32]) -> f32 {
(x[x.len() / 2..].iter().map(|v| v * v).sum::<f32>() / (x.len() / 2) as f32).sqrt()
}
#[test]
fn fm_rolls_off_highs_keeps_lows() {
let sr = 48_000u32;
for curve in [Emphasis::Fm50, Emphasis::Fm75] {
let low = deemphasis(&tone(100.0, sr, sr as usize), sr, curve);
let high = deemphasis(&tone(15_000.0, sr, sr as usize), sr, curve);
assert!((rms(&low) - 0.707).abs() < 0.05, "low gain {}", rms(&low));
assert!(rms(&high) < rms(&low) * 0.4, "high not rolled off: {}", rms(&high));
}
}
#[test]
fn cd_shelf_attenuates_highs() {
let sr = 48_000u32;
let low = deemphasis(&tone(100.0, sr, sr as usize), sr, Emphasis::CdIec);
let high = deemphasis(&tone(15_000.0, sr, sr as usize), sr, Emphasis::CdIec);
assert!(rms(&high) < rms(&low) * 0.5, "shelf not applied: {} vs {}", rms(&high), rms(&low));
assert!(rms(&high) > rms(&low) * 0.15, "shelf over-attenuated");
}
}