use crate::filter::lowpass;
#[derive(Debug, Clone, Copy)]
struct Biquad {
b0: f32,
b1: f32,
b2: f32,
a1: f32,
a2: f32,
x1: f32,
x2: f32,
y1: f32,
y2: f32,
}
impl Biquad {
fn process(&mut self, x: f32) -> f32 {
let y = self.b0 * x + self.b1 * self.x1 + self.b2 * self.x2
- self.a1 * self.y1
- self.a2 * self.y2;
self.x2 = self.x1;
self.x1 = x;
self.y2 = self.y1;
self.y1 = y;
y
}
}
fn riaa_biquad(sample_rate: u32) -> Biquad {
let (b0, b1, b2, a1, a2) = match sample_rate {
44_100 => (1.0, -0.721_892_2, -0.186_052_05, -1.700_724, 0.702_938_15),
48_000 => (1.0, -0.755_552_1, -0.164_625_71, -1.732_765_5, 0.734_553_44),
88_200 => (1.0, -0.847_957_7, -0.112_763_2, -1.855_464_8, 0.855_972_14),
96_000 => (1.0, -0.853_533_1, -0.110_459_51, -1.866_608_3, 0.867_038_3),
_ => unreachable!("fallback rates use `riaa_fallback`"),
};
Biquad { b0, b1, b2, a1, a2, x1: 0.0, x2: 0.0, y1: 0.0, y2: 0.0 }
}
#[derive(Debug, Clone, Copy)]
struct FirstOrder {
b0: f32,
b1: f32,
a1: f32,
x1: f32,
y1: f32,
}
impl FirstOrder {
fn design(num_tau: Option<f32>, den_tau: f32, sample_rate: f32) -> Self {
let k = 2.0 * sample_rate;
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;
Self { b0: n0 / d0, b1: n1 / d0, a1: d1 / d0, x1: 0.0, y1: 0.0 }
}
fn process(&mut self, x: f32) -> f32 {
let y = self.b0 * x + self.b1 * self.x1 - self.a1 * self.y1;
self.x1 = x;
self.y1 = y;
y
}
}
const RIAA_T1: f32 = 3180e-6;
const RIAA_T2: f32 = 318e-6;
const RIAA_T3: f32 = 75e-6;
fn riaa_fallback(signal: &[f32], sample_rate: u32) -> Vec<f32> {
let fs = sample_rate as f32;
let mut s1 = FirstOrder::design(Some(RIAA_T1), RIAA_T2, fs);
let mut s2 = FirstOrder::design(None, RIAA_T3, fs);
signal.iter().map(|&x| s2.process(s1.process(x))).collect()
}
pub fn riaa_deemphasis(signal: &[f32], sample_rate: u32) -> Vec<f32> {
if signal.is_empty() {
return Vec::new();
}
let mut out = match sample_rate {
44_100 | 48_000 | 88_200 | 96_000 => {
let mut f = riaa_biquad(sample_rate);
signal.iter().map(|&x| f.process(x)).collect()
}
_ => riaa_fallback(signal, sample_rate),
};
let gain = riaa_gain_at_1khz(sample_rate);
if gain > 1e-6 {
let scale = 1.0 / gain;
for s in out.iter_mut() {
*s *= scale;
}
}
out
}
pub fn elliptical_mono(
left: &[f32],
right: &[f32],
sample_rate: u32,
crossover_hz: f32,
) -> (Vec<f32>, Vec<f32>) {
let n = left.len().min(right.len());
if n == 0 {
return (Vec::new(), Vec::new());
}
let low_l = lowpass(&left[..n], sample_rate, crossover_hz);
let low_r = lowpass(&right[..n], sample_rate, crossover_hz);
let mut out_l = Vec::with_capacity(n);
let mut out_r = Vec::with_capacity(n);
for i in 0..n {
let mono_low = (low_l[i] + low_r[i]) * 0.5;
out_l.push(left[i] - low_l[i] + mono_low);
out_r.push(right[i] - low_r[i] + mono_low);
}
(out_l, out_r)
}
pub fn vinyl_restore(
left: &[f32],
right: &[f32],
sample_rate: u32,
elliptical_hz: Option<f32>,
) -> (Vec<f32>, Vec<f32>) {
let mut l = riaa_deemphasis(left, sample_rate);
let mut r = riaa_deemphasis(right, sample_rate);
if let Some(crossover) = elliptical_hz {
(l, r) = elliptical_mono(&l, &r, sample_rate, crossover);
}
(l, r)
}
fn riaa_gain_at_1khz(sample_rate: u32) -> f32 {
let fs = sample_rate;
let n = fs as usize * 2;
let tone: Vec<f32> = (0..n)
.map(|i| (2.0 * std::f32::consts::PI * 1000.0 * i as f32 / fs as f32).sin())
.collect();
let out = riaa_deemphasis_unscaled(&tone, fs);
let peak = |s: &[f32]| s.iter().map(|x| x.abs()).fold(0.0f32, f32::max);
peak(&out[n / 2..]) / peak(&tone[n / 2..]).max(1e-10)
}
fn riaa_deemphasis_unscaled(signal: &[f32], sample_rate: u32) -> Vec<f32> {
match sample_rate {
44_100 | 48_000 | 88_200 | 96_000 => {
let mut f = riaa_biquad(sample_rate);
signal.iter().map(|&x| f.process(x)).collect()
}
_ => riaa_fallback(signal, sample_rate),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn riaa_is_unity_at_1khz() {
let fs = 48_000u32;
let n = fs as usize * 2;
let tone: Vec<f32> = (0..n)
.map(|i| (2.0 * std::f32::consts::PI * 1000.0 * i as f32 / fs as f32).sin())
.collect();
let out = riaa_deemphasis(&tone, fs);
let rms = |s: &[f32]| {
let tail = &s[s.len() / 2..];
(tail.iter().map(|x| x * x).sum::<f32>() / tail.len() as f32).sqrt()
};
let ratio = rms(&out) / rms(&tone);
assert!((ratio - 1.0).abs() < 0.15, "1 kHz should be near unity, ratio={ratio}");
}
#[test]
fn riaa_boosts_bass_relative_to_mids() {
let fs = 48_000u32;
let n = fs as usize * 4;
let tone = |f: f32| {
(0..n)
.map(|i| (2.0 * std::f32::consts::PI * f * i as f32 / fs as f32).sin())
.collect::<Vec<_>>()
};
let rms = |s: &[f32]| {
let tail = &s[s.len() / 2..];
(tail.iter().map(|x| x * x).sum::<f32>() / tail.len() as f32).sqrt()
};
let bass_out = riaa_deemphasis(&tone(80.0), fs);
let mid_out = riaa_deemphasis(&tone(1000.0), fs);
assert!(rms(&bass_out) > rms(&mid_out) * 1.2, "RIAA should lift bass vs 1 kHz");
}
#[test]
fn elliptical_collapses_lows_to_mono() {
let fs = 48_000u32;
let n = fs as usize * 2;
let t = |f: f32, i: usize| (2.0 * std::f32::consts::PI * f * i as f32 / fs as f32).sin();
let left: Vec<f32> = (0..n).map(|i| t(60.0, i)).collect();
let right: Vec<f32> = (0..n).map(|i| t(60.0, i) * 0.8).collect();
let (l, r) = elliptical_mono(&left, &right, fs, 200.0);
let out_low_l = lowpass(&l, fs, 200.0);
let out_low_r = lowpass(&r, fs, 200.0);
let diff: f32 =
out_low_l.iter().zip(&out_low_r).map(|(a, b)| (a - b).abs()).sum::<f32>() / n as f32;
assert!(diff < 0.08, "low band should be mono, mean |L-R|={diff}");
}
}