pub struct Mixer {
ir_gain: f32,
dry_gain: f32,
wet_gain: f32,
}
impl Mixer {
pub fn new(ir: &[f32], dry_wet: f32) -> Self {
let energy: f32 = ir.iter().map(|&x| x * x).sum();
let ir_gain = if energy > 0.0 {
1.0 / energy.sqrt()
} else {
1.0
};
let mut m = Self {
ir_gain,
dry_gain: 1.0,
wet_gain: 0.0,
};
m.set_dry_wet(dry_wet);
m
}
pub fn set_dry_wet(&mut self, dry_wet: f32) {
let theta = dry_wet.clamp(0.0, 1.0) * std::f32::consts::FRAC_PI_2;
self.dry_gain = theta.cos();
self.wet_gain = theta.sin() * self.ir_gain;
}
#[inline]
pub fn ir_gain(&self) -> f32 {
self.ir_gain
}
#[inline]
pub fn dry_gain(&self) -> f32 {
self.dry_gain
}
#[inline]
pub fn wet_gain(&self) -> f32 {
self.wet_gain
}
pub fn process(&self, dry: &[f32], wet: &[f32], output: &mut [f32]) {
debug_assert_eq!(dry.len(), wet.len(), "dry and wet length mismatch");
debug_assert_eq!(dry.len(), output.len(), "dry and output length mismatch");
let n = dry.len().min(wet.len()).min(output.len());
let dry = &dry[..n];
let wet = &wet[..n];
let output = &mut output[..n];
for ((&d, &w), y) in dry.iter().zip(wet.iter()).zip(output.iter_mut()) {
*y = d * self.dry_gain + w * self.wet_gain;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f32 = 1e-6;
fn assert_approx(label: &str, got: f32, expected: f32) {
assert!(
(got - expected).abs() < EPSILON,
"{label}: expected {expected}, got {got}"
);
}
#[test]
fn unit_impulse_ir_has_gain_one() {
let ir = [1.0f32, 0.0, 0.0, 0.0];
let m = Mixer::new(&ir, 0.0);
assert_approx("ir_gain", m.ir_gain(), 1.0);
}
#[test]
fn gain_two_ir_has_ir_gain_half() {
let ir = [2.0f32, 0.0, 0.0, 0.0];
let m = Mixer::new(&ir, 0.0);
assert_approx("ir_gain", m.ir_gain(), 0.5);
}
#[test]
fn silent_ir_gives_gain_one() {
let m = Mixer::new(&[0.0f32; 8], 0.5);
assert_approx("ir_gain", m.ir_gain(), 1.0);
assert!(m.dry_gain().is_finite());
assert!(m.wet_gain().is_finite());
}
#[test]
fn empty_ir_gives_gain_one() {
let m = Mixer::new(&[], 0.0);
assert_approx("ir_gain", m.ir_gain(), 1.0);
}
#[test]
fn fully_dry_passes_dry_only() {
let ir = [1.0f32, 0.0, 0.0, 0.0];
let m = Mixer::new(&ir, 0.0);
assert_approx("dry_gain", m.dry_gain(), 1.0);
assert_approx("wet_gain", m.wet_gain(), 0.0);
let dry = [1.0f32, 2.0, 3.0, 4.0];
let wet = [9.0f32; 4];
let mut out = [0.0f32; 4];
m.process(&dry, &wet, &mut out);
for (&d, &y) in dry.iter().zip(out.iter()) {
assert_approx("out", y, d);
}
}
#[test]
fn fully_wet_passes_wet_only() {
let ir = [1.0f32, 0.0, 0.0, 0.0];
let m = Mixer::new(&ir, 1.0);
assert_approx("dry_gain", m.dry_gain(), 0.0);
assert_approx("wet_gain", m.wet_gain(), 1.0);
let dry = [9.0f32; 4];
let wet = [1.0f32, 2.0, 3.0, 4.0];
let mut out = [0.0f32; 4];
m.process(&dry, &wet, &mut out);
for (&w, &y) in wet.iter().zip(out.iter()) {
assert_approx("out", y, w);
}
}
#[test]
fn equal_power_invariant_at_half() {
let ir = [1.0f32, 0.0, 0.0, 0.0];
let m = Mixer::new(&ir, 0.5);
let sin_part = m.wet_gain() / m.ir_gain();
let power_sum = m.dry_gain().powi(2) + sin_part.powi(2);
assert!(
(power_sum - 1.0).abs() < 1e-6,
"equal-power invariant violated: {power_sum}"
);
}
#[test]
fn set_dry_wet_updates_gains() {
let ir = [1.0f32, 0.0, 0.0, 0.0];
let mut m = Mixer::new(&ir, 0.0);
assert_approx("dry_gain before", m.dry_gain(), 1.0);
m.set_dry_wet(1.0);
assert_approx("dry_gain after", m.dry_gain(), 0.0);
assert_approx("wet_gain after", m.wet_gain(), 1.0);
}
#[test]
fn set_dry_wet_clamps_out_of_range() {
let ir = [1.0f32];
let mut m = Mixer::new(&ir, 2.0); assert_approx("clamped high: dry_gain", m.dry_gain(), 0.0);
m.set_dry_wet(-0.5); assert_approx("clamped low: dry_gain", m.dry_gain(), 1.0);
assert_approx("clamped low: wet_gain", m.wet_gain(), 0.0);
}
#[test]
fn ir_normalization_scales_wet_output() {
let ir = [2.0f32, 0.0];
let m = Mixer::new(&ir, 1.0);
let dry = [0.0f32; 4];
let wet = [1.0f32; 4];
let mut out = [0.0f32; 4];
m.process(&dry, &wet, &mut out);
for &y in &out {
assert_approx("out", y, 0.5);
}
}
#[test]
fn process_overwrites_output() {
let m = Mixer::new(&[1.0f32], 0.0); let dry = [3.0f32; 4];
let wet = [0.0f32; 4];
let mut out = [99.0f32; 4]; m.process(&dry, &wet, &mut out);
for &y in &out {
assert_approx("out", y, 3.0);
}
}
#[test]
fn process_shorter_input_leaves_tail_untouched() {
let m = Mixer::new(&[1.0f32], 0.0); let dry = [1.0f32, 2.0]; let wet = [0.0f32, 0.0];
let mut out = [99.0f32; 5];
if !cfg!(debug_assertions) {
m.process(&dry, &wet, &mut out);
assert_approx("out[0]", out[0], 1.0);
assert_approx("out[1]", out[1], 2.0);
for (i, &y) in out.iter().enumerate().skip(2) {
assert_approx(&format!("out[{i}]"), y, 99.0);
}
}
}
}