pub struct Crossfeed {
w_lr: [f64; 2],
w_rl: [f64; 2],
b0: f64,
b1: f64,
b2: f64,
a1: f64,
a2: f64,
mix: f64,
enabled: bool,
}
impl Crossfeed {
pub fn new(sample_rate: f64) -> Self {
Self::with_params(sample_rate, 700.0, 0.35)
}
pub fn with_params(sample_rate: f64, cutoff_hz: f64, mix: f64) -> Self {
let (b0, b1, b2, a1, a2) = Self::calc_hpf_coeffs(cutoff_hz, sample_rate);
Self {
w_lr: [0.0; 2],
w_rl: [0.0; 2],
b0,
b1,
b2,
a1,
a2,
mix: mix.clamp(0.0, 1.0),
enabled: true,
}
}
fn calc_hpf_coeffs(cutoff: f64, sr: f64) -> (f64, f64, f64, f64, f64) {
let wc = std::f64::consts::PI * cutoff / sr;
let k = wc.tan();
let k2 = k * k;
let sqrt2_k = std::f64::consts::SQRT_2 * k;
let norm = 1.0 / (1.0 + sqrt2_k + k2);
let b0 = norm;
let b1 = -2.0 * norm;
let b2 = norm;
let a1 = 2.0 * (k2 - 1.0) * norm;
let a2 = (1.0 - sqrt2_k + k2) * norm;
(b0, b1, b2, a1, a2)
}
pub fn set_mix(&mut self, mix: f64) {
self.mix = mix.clamp(0.0, 1.0);
}
pub fn set_sample_rate(&mut self, sample_rate: f64, cutoff_hz: f64) {
let (b0, b1, b2, a1, a2) = Self::calc_hpf_coeffs(cutoff_hz, sample_rate);
self.b0 = b0;
self.b1 = b1;
self.b2 = b2;
self.a1 = a1;
self.a2 = a2;
self.reset();
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn reset(&mut self) {
self.w_lr = [0.0; 2];
self.w_rl = [0.0; 2];
}
pub fn process(&mut self, samples: &mut [f64], channels: usize) {
if !self.enabled || channels != 2 {
return;
}
let b0 = self.b0;
let b1 = self.b1;
let b2 = self.b2;
let a1 = self.a1;
let a2 = self.a2;
let mix = self.mix;
for chunk in samples.chunks_exact_mut(2) {
let l_in = chunk[0];
let r_in = chunk[1];
let hpf_l = Self::process_hpf_df2t_static(&mut self.w_lr, b0, b1, b2, a1, a2, l_in);
let hpf_r = Self::process_hpf_df2t_static(&mut self.w_rl, b0, b1, b2, a1, a2, r_in);
chunk[0] = l_in + hpf_r * mix; chunk[1] = r_in + hpf_l * mix; }
}
#[inline(always)]
fn process_hpf_df2t_static(
w: &mut [f64; 2],
b0: f64,
b1: f64,
b2: f64,
a1: f64,
a2: f64,
input: f64,
) -> f64 {
let output = b0 * input + w[0];
w[0] = b1 * input - a1 * output + w[1];
w[1] = b2 * input - a2 * output;
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))]
{
w[0] = crate::runtime::flush_subnormal_sample(w[0]);
w[1] = crate::runtime::flush_subnormal_sample(w[1]);
}
output
}
pub fn get_settings(&self) -> CrossfeedSettings {
CrossfeedSettings {
mix: self.mix,
enabled: self.enabled,
}
}
}
impl Default for Crossfeed {
fn default() -> Self {
Self::new(44100.0)
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct CrossfeedSettings {
pub mix: f64,
pub enabled: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_crossfeed_stereo() {
let mut cf = Crossfeed::new(44100.0);
cf.set_mix(0.5);
let mut samples = vec![1.0, 0.0, 0.5, 0.0, 0.0, 0.0];
cf.process(&mut samples, 2);
assert!(samples[1].abs() > 0.0); }
#[test]
fn test_crossfeed_mono_passthrough() {
let mut cf = Crossfeed::new(44100.0);
cf.set_enabled(true);
let mut samples = vec![1.0, 0.5, 0.25];
let original = samples.clone();
cf.process(&mut samples, 1);
assert_eq!(samples, original);
}
#[test]
fn test_crossfeed_disabled() {
let mut cf = Crossfeed::new(44100.0);
cf.set_enabled(false);
let mut samples = vec![1.0, 0.0, 0.5, 0.0];
let original = samples.clone();
cf.process(&mut samples, 2);
assert_eq!(samples, original);
}
#[test]
fn test_hpf_coefficients_highpass() {
let (b0, b1, b2, _a1, _a2) = Crossfeed::calc_hpf_coeffs(700.0, 44100.0);
assert!((b0 - b2).abs() < 1e-10); assert!((b1 + 2.0 * b0).abs() < 1e-10);
assert!((b0 + b1 + b2).abs() < 1e-10);
}
#[test]
fn test_hpf_attenuates_low_freq() {
let mut cf = Crossfeed::with_params(44100.0, 700.0, 1.0);
let mut samples: Vec<f64> = vec![0.0; 200]; for i in 0..100 {
samples[i * 2] = 1.0; samples[i * 2 + 1] = 0.0; }
cf.process(&mut samples, 2);
let sum_r: f64 = samples.iter().skip(100).skip(1).step_by(2).take(50).sum();
assert!(sum_r.abs() < 1.0); }
#[test]
fn test_crossfeed_flushes_denormals_with_audio_thread_init() {
crate::runtime::audio_thread_init();
if !crate::runtime::audio_thread_float_mode_is_enabled() {
return;
}
let subnormal = f64::from_bits(1);
let mut state = [subnormal, -subnormal];
let _ = Crossfeed::process_hpf_df2t_static(&mut state, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
assert_eq!(state, [0.0, 0.0]);
}
#[test]
fn test_crossfeed_sustained_subnormal_input_flushes_to_zero() {
crate::runtime::audio_thread_init();
if !crate::runtime::audio_thread_float_mode_is_enabled() {
return;
}
let mut cf = Crossfeed::new(44_100.0);
let subnormal = f64::from_bits(1);
let mut samples = (0..1024)
.flat_map(|_| [subnormal, -subnormal])
.collect::<Vec<_>>();
cf.process(&mut samples, 2);
assert!(samples.iter().all(|sample| *sample == 0.0));
assert_eq!(cf.w_lr, [0.0, 0.0]);
assert_eq!(cf.w_rl, [0.0, 0.0]);
}
}