use std::f32::consts::FRAC_PI_2;
use crate::engine::partition::UniformPartitionEngine;
pub struct CrossfadingConvolver {
front: UniformPartitionEngine,
back: UniformPartitionEngine,
block_size: usize,
crossfade_samples: usize,
fade_pos: Option<usize>,
front_gains: Vec<f32>,
back_gains: Vec<f32>,
front_out: Vec<f32>,
back_out: Vec<f32>,
}
impl CrossfadingConvolver {
pub fn new(ir: &[f32], block_size: usize, crossfade_samples: usize) -> Self {
assert!(block_size > 0, "block_size must be > 0");
let (front_gains, back_gains) = build_gain_tables(crossfade_samples);
Self {
front: UniformPartitionEngine::new(ir, block_size),
back: UniformPartitionEngine::new(&[], block_size),
block_size,
crossfade_samples,
fade_pos: None,
front_gains,
back_gains,
front_out: vec![0.0f32; block_size],
back_out: vec![0.0f32; block_size],
}
}
#[inline]
pub fn block_size(&self) -> usize {
self.block_size
}
#[inline]
pub fn crossfade_samples(&self) -> usize {
self.crossfade_samples
}
#[inline]
pub fn is_crossfading(&self) -> bool {
self.fade_pos.is_some()
}
#[inline]
pub fn crossfade_progress(&self) -> Option<usize> {
self.fade_pos
}
pub fn set_ir(&mut self, new_ir: &[f32]) {
self.finish_pending_fade();
self.back.load_ir(new_ir);
self.begin_fade();
}
pub fn swap_engine(&mut self, new_engine: UniformPartitionEngine) -> UniformPartitionEngine {
assert_eq!(
new_engine.block_size(),
self.block_size,
"swap_engine: block_size mismatch"
);
self.finish_pending_fade();
let displaced = std::mem::replace(&mut self.back, new_engine);
self.begin_fade();
displaced
}
pub fn reset(&mut self) {
self.front.reset();
self.back.reset();
self.fade_pos = None;
}
pub fn process(&mut self, input: &[f32], output: &mut [f32]) {
debug_assert_eq!(input.len(), self.block_size, "input length mismatch");
debug_assert_eq!(output.len(), self.block_size, "output length mismatch");
match self.fade_pos {
None => {
output.fill(0.0);
self.front.process(input, output);
}
Some(mut pos) => {
self.front_out.fill(0.0);
self.back_out.fill(0.0);
self.front.process(input, &mut self.front_out);
self.back.process(input, &mut self.back_out);
let crossfade_samples = self.crossfade_samples;
for (i, y) in output.iter_mut().enumerate() {
if pos < crossfade_samples {
let f_g = self.front_gains[pos];
let b_g = self.back_gains[pos];
*y = f_g * self.front_out[i] + b_g * self.back_out[i];
pos += 1;
} else {
*y = self.back_out[i];
}
}
if pos >= crossfade_samples {
std::mem::swap(&mut self.front, &mut self.back);
self.fade_pos = None;
} else {
self.fade_pos = Some(pos);
}
}
}
}
fn begin_fade(&mut self) {
if self.crossfade_samples == 0 {
std::mem::swap(&mut self.front, &mut self.back);
self.fade_pos = None;
} else {
self.fade_pos = Some(0);
}
}
fn finish_pending_fade(&mut self) {
if self.fade_pos.is_some() {
std::mem::swap(&mut self.front, &mut self.back);
self.fade_pos = None;
}
}
}
fn build_gain_tables(crossfade_samples: usize) -> (Vec<f32>, Vec<f32>) {
if crossfade_samples == 0 {
return (Vec::new(), Vec::new());
}
if crossfade_samples == 1 {
return (vec![0.0], vec![1.0]);
}
let denom = (crossfade_samples - 1) as f32;
let mut front_gains = Vec::with_capacity(crossfade_samples);
let mut back_gains = Vec::with_capacity(crossfade_samples);
for i in 0..crossfade_samples {
let alpha = i as f32 / denom;
let theta = alpha * FRAC_PI_2;
front_gains.push(theta.cos());
back_gains.push(theta.sin());
}
(front_gains, back_gains)
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f32 = 1e-5;
fn scaled_impulse(gain: f32, block_size: usize) -> Vec<f32> {
let mut ir = vec![0.0f32; block_size];
ir[0] = gain;
ir
}
fn assert_close(label: &str, got: &[f32], expected: &[f32], tol: f32) {
assert_eq!(got.len(), expected.len(), "{label}: length mismatch");
for (i, (&value, &target)) in got.iter().zip(expected.iter()).enumerate() {
assert!(
(value - target).abs() < tol,
"{label}[{i}]: expected {target}, got {value}"
);
}
}
#[test]
fn gain_tables_handle_degenerate_lengths() {
let (f, b) = build_gain_tables(0);
assert!(f.is_empty() && b.is_empty());
let (f, b) = build_gain_tables(1);
assert_eq!(f, vec![0.0f32]);
assert_eq!(b, vec![1.0f32]);
}
#[test]
fn gain_tables_are_equal_power_and_hit_endpoints() {
let n = 8;
let (f_g, b_g) = build_gain_tables(n);
assert_eq!(f_g.len(), n);
assert_eq!(b_g.len(), n);
assert!((f_g[0] - 1.0).abs() < EPSILON, "f[0]={}", f_g[0]);
assert!(b_g[0].abs() < EPSILON, "b[0]={}", b_g[0]);
assert!(f_g[n - 1].abs() < EPSILON, "f[last]={}", f_g[n - 1]);
assert!((b_g[n - 1] - 1.0).abs() < EPSILON, "b[last]={}", b_g[n - 1]);
for i in 0..n {
let power = f_g[i] * f_g[i] + b_g[i] * b_g[i];
assert!(
(power - 1.0).abs() < EPSILON,
"equal-power violated at {i}: {power}"
);
}
}
#[test]
fn fresh_crossfader_is_not_fading() {
let block_size = 4;
let conv = CrossfadingConvolver::new(&scaled_impulse(1.0, block_size), block_size, 8);
assert!(!conv.is_crossfading());
assert!(conv.crossfade_progress().is_none());
assert_eq!(conv.block_size(), block_size);
assert_eq!(conv.crossfade_samples(), 8);
}
#[test]
fn process_without_fade_uses_front_only() {
let block_size = 4;
let mut conv = CrossfadingConvolver::new(&scaled_impulse(3.0, block_size), block_size, 8);
let input = [1.0f32; 4];
let mut output = [0.0f32; 4];
conv.process(&input, &mut output);
assert_close("no-fade output", &output, &[3.0, 3.0, 3.0, 3.0], EPSILON);
}
#[test]
fn set_ir_starts_a_fade() {
let block_size = 4;
let mut conv = CrossfadingConvolver::new(&scaled_impulse(1.0, block_size), block_size, 8);
conv.set_ir(&scaled_impulse(2.0, block_size));
assert!(conv.is_crossfading());
assert_eq!(conv.crossfade_progress(), Some(0));
}
#[test]
fn zero_length_fade_switches_instantly() {
let block_size = 4;
let mut conv = CrossfadingConvolver::new(&scaled_impulse(1.0, block_size), block_size, 0);
conv.set_ir(&scaled_impulse(5.0, block_size));
assert!(!conv.is_crossfading());
let input = [1.0f32; 4];
let mut output = [0.0f32; 4];
conv.process(&input, &mut output);
assert_close("instant switch", &output, &[5.0f32; 4], EPSILON);
}
#[test]
fn fade_transitions_from_old_to_new_ir() {
let block_size = 4;
let crossfade = 4; let mut conv =
CrossfadingConvolver::new(&scaled_impulse(1.0, block_size), block_size, crossfade);
conv.set_ir(&scaled_impulse(3.0, block_size));
let input = [1.0f32; 4];
let mut output = [0.0f32; 4];
conv.process(&input, &mut output);
assert!(
(output[0] - 1.0).abs() < EPSILON,
"fade start: expected 1.0, got {}",
output[0]
);
assert!(
(output[3] - 3.0).abs() < EPSILON,
"fade end: expected 3.0, got {}",
output[3]
);
for (i, &sample) in output.iter().enumerate().take(3).skip(1) {
assert!(
sample > 1.0 && sample < 3.5,
"fade midpoint out of bounds: output[{i}]={sample}"
);
}
assert!(!conv.is_crossfading());
}
#[test]
fn after_fade_new_ir_is_active() {
let block_size = 4;
let mut conv = CrossfadingConvolver::new(&scaled_impulse(1.0, block_size), block_size, 4);
conv.set_ir(&scaled_impulse(2.0, block_size));
let input = [1.0f32; 4];
let mut output = [0.0f32; 4];
conv.process(&input, &mut output);
assert!(!conv.is_crossfading(), "fade should be done after 1 block");
conv.process(&input, &mut output);
assert_close("post-fade output", &output, &[2.0f32; 4], EPSILON);
}
#[test]
fn multi_block_fade_progress() {
let block_size = 4;
let crossfade = 10;
let mut conv =
CrossfadingConvolver::new(&scaled_impulse(1.0, block_size), block_size, crossfade);
conv.set_ir(&scaled_impulse(4.0, block_size));
let input = [1.0f32; 4];
let mut output = [0.0f32; 4];
conv.process(&input, &mut output);
assert_eq!(conv.crossfade_progress(), Some(4));
conv.process(&input, &mut output);
assert_eq!(conv.crossfade_progress(), Some(8));
conv.process(&input, &mut output);
assert!(!conv.is_crossfading());
assert!(
(output[2] - 4.0).abs() < EPSILON,
"post-fade sample: {}",
output[2]
);
assert!(
(output[3] - 4.0).abs() < EPSILON,
"post-fade sample: {}",
output[3]
);
}
#[test]
fn swap_engine_starts_fade_and_returns_old_engine() {
let block_size = 4;
let mut conv = CrossfadingConvolver::new(&scaled_impulse(1.0, block_size), block_size, 4);
let new_engine = UniformPartitionEngine::new(&scaled_impulse(2.0, block_size), block_size);
let displaced = conv.swap_engine(new_engine);
assert!(conv.is_crossfading());
assert_eq!(displaced.block_size(), block_size);
}
#[test]
#[should_panic(expected = "block_size mismatch")]
fn swap_engine_panics_on_block_size_mismatch() {
let mut conv = CrossfadingConvolver::new(&scaled_impulse(1.0, 4), 4, 4);
let bad = UniformPartitionEngine::new(&scaled_impulse(1.0, 8), 8);
let _ = conv.swap_engine(bad);
}
#[test]
fn chained_set_ir_collapses_pending_fade() {
let block_size = 4;
let mut conv = CrossfadingConvolver::new(&scaled_impulse(1.0, block_size), block_size, 8);
conv.set_ir(&scaled_impulse(2.0, block_size));
let input = [1.0f32; 4];
let mut output = [0.0f32; 4];
conv.process(&input, &mut output);
assert!(conv.is_crossfading());
conv.set_ir(&scaled_impulse(4.0, block_size));
assert_eq!(conv.crossfade_progress(), Some(0));
}
#[test]
fn reset_cancels_fade_and_clears_state() {
let block_size = 4;
let mut conv = CrossfadingConvolver::new(&scaled_impulse(1.0, block_size), block_size, 8);
conv.set_ir(&scaled_impulse(3.0, block_size));
assert!(conv.is_crossfading());
conv.reset();
assert!(!conv.is_crossfading());
let input = [1.0f32; 4];
let mut output = [0.0f32; 4];
conv.process(&input, &mut output);
assert_close("post-reset output", &output, &[1.0f32; 4], EPSILON);
}
#[test]
fn process_overwrites_output() {
let block_size = 4;
let mut conv = CrossfadingConvolver::new(&scaled_impulse(1.0, block_size), block_size, 8);
let input = [1.0f32; 4];
let mut output = [99.0f32; 4]; conv.process(&input, &mut output);
assert_close("overwrite", &output, &[1.0f32; 4], EPSILON);
}
#[test]
fn fade_to_empty_ir_fades_to_silence() {
let block_size = 4;
let mut conv = CrossfadingConvolver::new(&scaled_impulse(1.0, block_size), block_size, 4);
conv.set_ir(&[]);
let input = [1.0f32; 4];
let mut output = [0.0f32; 4];
conv.process(&input, &mut output);
assert!(
(output[0] - 1.0).abs() < EPSILON,
"fade-to-silence start: {}",
output[0]
);
assert!(
output[3].abs() < EPSILON,
"fade-to-silence end: {}",
output[3]
);
}
#[test]
fn fade_in_from_silence() {
let block_size = 4;
let mut conv = CrossfadingConvolver::new(&[], block_size, 4);
conv.set_ir(&scaled_impulse(1.0, block_size));
let input = [1.0f32; 4];
let mut output = [0.0f32; 4];
conv.process(&input, &mut output);
assert!(output[0].abs() < EPSILON, "fade-in start: {}", output[0]);
assert!(
(output[3] - 1.0).abs() < EPSILON,
"fade-in end: {}",
output[3]
);
}
}