use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
const SR: f32 = 48_000.0;
fn make_params(values: &[f32]) -> ParamBlock {
let mut p = ParamBlock::new();
for &v in values { p.add(v); }
p
}
fn rms(buf: &[f32]) -> f32 {
let sum: f32 = buf.iter().map(|x| x * x).sum();
(sum / buf.len() as f32).sqrt()
}
fn max_abs(buf: &[f32]) -> f32 {
buf.iter().map(|x| x.abs()).fold(0.0f32, f32::max)
}
#[test]
fn regression_oscillator_sine_440hz() {
use crate::oscillator::Oscillator;
let mut osc = Oscillator::new();
let mut params = make_params(&[440.0, 1.0, 0.0, -1.0]); let inputs = [None; MAX_INPUTS];
let mut output = [0.0f32; BUFFER_SIZE];
osc.process(&inputs, &mut output, &mut params, SR);
let r = rms(&output);
assert!(r > 0.5 && r < 0.75, "Sine RMS should be ~0.707, got {r}");
assert!(max_abs(&output) <= 1.001, "Sine peak should be ≤ 1.0");
}
#[test]
fn regression_oscillator_silence_at_zero_amplitude() {
use crate::oscillator::Oscillator;
let mut osc = Oscillator::new();
let mut params = make_params(&[440.0, 0.0, 0.0, -1.0]); let inputs = [None; MAX_INPUTS];
let mut output = [0.0f32; BUFFER_SIZE];
osc.process(&inputs, &mut output, &mut params, SR);
assert!(max_abs(&output) < 1e-6, "Zero amplitude should produce silence");
}
#[test]
fn regression_gain_unity() {
use crate::gain::Gain;
let mut gain = Gain;
let mut params = make_params(&[1.0]);
let input = [0.5f32; BUFFER_SIZE];
let inputs: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = {
let mut arr = [None; MAX_INPUTS];
arr[0] = Some(&input);
arr
};
let mut output = [0.0f32; BUFFER_SIZE];
gain.process(&inputs, &mut output, &mut params, SR);
for (i, (&o, &inp)) in output.iter().zip(input.iter()).enumerate() {
assert!((o - inp).abs() < 1e-6, "Unity gain mismatch at sample {i}: {o} != {inp}");
}
}
#[test]
fn regression_gain_half() {
use crate::gain::Gain;
let mut gain = Gain;
let mut params = make_params(&[0.5]);
let input = [1.0f32; BUFFER_SIZE];
let inputs: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = {
let mut arr = [None; MAX_INPUTS];
arr[0] = Some(&input);
arr
};
let mut output = [0.0f32; BUFFER_SIZE];
gain.process(&inputs, &mut output, &mut params, SR);
let last = output[BUFFER_SIZE - 1];
assert!((last - 0.5).abs() < 0.01, "Half gain should produce ~0.5, got {last}");
}
#[test]
fn regression_mixer_two_inputs_sum() {
use crate::mixer::Mixer;
let mut mixer = Mixer;
let mut params = make_params(&[1.0, 1.0, 1.0, 1.0]); let a = [0.3f32; BUFFER_SIZE];
let b = [0.2f32; BUFFER_SIZE];
let inputs: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = {
let mut arr = [None; MAX_INPUTS];
arr[0] = Some(&a);
arr[1] = Some(&b);
arr
};
let mut output = [0.0f32; BUFFER_SIZE];
mixer.process(&inputs, &mut output, &mut params, SR);
for (i, &o) in output.iter().enumerate() {
assert!((o - 0.5).abs() < 1e-5, "Mixer sum mismatch at {i}: {o}");
}
}
#[test]
fn regression_mixer_silence_passthrough() {
use crate::mixer::Mixer;
let mut mixer = Mixer;
let mut params = make_params(&[1.0, 1.0, 1.0, 1.0]);
let inputs = [None; MAX_INPUTS];
let mut output = [0.0f32; BUFFER_SIZE];
mixer.process(&inputs, &mut output, &mut params, SR);
assert!(max_abs(&output) < 1e-6, "Mixer with no inputs should output silence");
}
#[test]
fn regression_compressor_below_threshold_passthrough() {
use crate::compressor::Compressor;
let mut comp = Compressor::new();
let mut params = make_params(&[0.0, 4.0, 1.0, 100.0, 0.0, 0.0]);
let input = [0.1f32; BUFFER_SIZE]; let inputs: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = {
let mut arr = [None; MAX_INPUTS];
arr[0] = Some(&input);
arr
};
let mut output = [0.0f32; BUFFER_SIZE];
comp.process(&inputs, &mut output, &mut params, SR);
let last = output[BUFFER_SIZE - 1];
assert!(last > 0.08, "Below-threshold signal should pass through, got {last}");
}
#[test]
fn regression_compressor_above_threshold_reduces() {
use crate::compressor::Compressor;
let mut comp = Compressor::new();
let mut params = make_params(&[-20.0, 10.0, 1.0, 100.0, 0.0, 0.0]);
let input = [0.5f32; BUFFER_SIZE]; let inputs: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = {
let mut arr = [None; MAX_INPUTS];
arr[0] = Some(&input);
arr
};
let mut output = [0.0f32; BUFFER_SIZE];
comp.process(&inputs, &mut output, &mut params, SR);
let last = output[BUFFER_SIZE - 1];
assert!(last < 0.4, "Above-threshold signal should be reduced, got {last}");
}
#[test]
fn regression_delay_dry_signal() {
use crate::delay::DelayLine;
let mut delay = DelayLine::new();
let mut params = make_params(&[0.1, 0.5, 0.0]);
let input = [0.7f32; BUFFER_SIZE];
let inputs: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = {
let mut arr = [None; MAX_INPUTS];
arr[0] = Some(&input);
arr
};
let mut output = [0.0f32; BUFFER_SIZE];
delay.process(&inputs, &mut output, &mut params, SR);
for (i, (&o, &inp)) in output.iter().zip(input.iter()).enumerate() {
assert!((o - inp).abs() < 1e-5, "Dry delay mismatch at {i}: {o} != {inp}");
}
}
#[test]
fn regression_waveshaper_zero_drive_passthrough() {
use crate::waveshaper::Waveshaper;
let mut ws = Waveshaper::new();
let mut params = make_params(&[0.0, 0.0, 0.5, 1.0]);
let input = [0.3f32; BUFFER_SIZE];
let inputs: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = {
let mut arr = [None; MAX_INPUTS];
arr[0] = Some(&input);
arr
};
let mut output = [0.0f32; BUFFER_SIZE];
ws.process(&inputs, &mut output, &mut params, SR);
let last = output[BUFFER_SIZE - 1];
assert!((last - 0.3).abs() < 0.05, "Zero drive waveshaper should pass through, got {last}");
}
#[test]
fn regression_waveshaper_hard_clip_bounds() {
use crate::waveshaper::Waveshaper;
let mut ws = Waveshaper::new();
let mut params = make_params(&[1.0, 1.0, 0.5, 1.0]);
let input = [2.0f32; BUFFER_SIZE]; let inputs: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = {
let mut arr = [None; MAX_INPUTS];
arr[0] = Some(&input);
arr
};
let mut output = [0.0f32; BUFFER_SIZE];
ws.process(&inputs, &mut output, &mut params, SR);
assert!(max_abs(&output) <= 1.001, "Hard clip should not exceed ±1.0");
}
#[test]
fn regression_chorus_bounded_output() {
use crate::chorus::Chorus;
let mut chorus = Chorus::new();
let mut params = make_params(&[1.0, 0.5, 0.2, 0.5]);
let input = [0.5f32; BUFFER_SIZE];
let inputs: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = {
let mut arr = [None; MAX_INPUTS];
arr[0] = Some(&input);
arr
};
let mut output = [0.0f32; BUFFER_SIZE];
chorus.process(&inputs, &mut output, &mut params, SR);
assert!(max_abs(&output) <= 1.5, "Chorus output should be bounded, got {}", max_abs(&output));
}
#[test]
fn regression_reverb_silence_passthrough() {
use crate::reverb::Reverb;
let mut reverb = Reverb::new(SR);
let mut params = make_params(&[0.5, 0.5, 0.3, 1.0]);
let inputs = [None; MAX_INPUTS];
let mut output = [0.0f32; BUFFER_SIZE];
reverb.process(&inputs, &mut output, &mut params, SR);
assert!(max_abs(&output) < 1e-5, "Reverb with silence input should output silence");
}
#[test]
fn regression_reverb_adds_tail() {
use crate::reverb::Reverb;
let mut reverb = Reverb::new(SR);
let mut params = make_params(&[0.8, 0.3, 0.5, 1.0]);
let input = [0.3f32; BUFFER_SIZE];
let inputs: [Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS] = {
let mut arr = [None; MAX_INPUTS];
arr[0] = Some(&input);
arr
};
for _ in 0..4 {
let mut out = [0.0f32; BUFFER_SIZE];
reverb.process(&inputs, &mut out, &mut params, SR);
}
let mut output = [0.0f32; BUFFER_SIZE];
reverb.process(&inputs, &mut output, &mut params, SR);
assert!(rms(&output) > 0.01, "Reverb output should be non-zero during signal, got {}", rms(&output));
}