use super::common::{env_coef, read_interpolated, sanitize_audio};
use crate::port::{GraphModule, PortDef, PortSpec, PortValues, SignalKind};
use alloc::vec;
use alloc::vec::Vec;
use core::f64::consts::TAU;
use libm::Libm;
pub struct UnitDelay {
buffer: f64,
spec: PortSpec,
}
impl UnitDelay {
pub fn new() -> Self {
Self {
buffer: 0.0,
spec: PortSpec {
inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
}
}
}
impl Default for UnitDelay {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for UnitDelay {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = inputs.get_or(0, 0.0);
outputs.set(10, self.buffer);
self.buffer = input;
}
fn reset(&mut self) {
self.buffer = 0.0;
}
fn set_sample_rate(&mut self, _: f64) {}
fn breaks_feedback_cycle(&self) -> bool {
true
}
fn type_id(&self) -> &'static str {
"unit_delay"
}
}
pub struct DelayLine {
buffer: Vec<f64>,
write_pos: usize,
sample_rate: f64,
smoothed_delay: f64,
delay_smooth_coef: f64,
delay_primed: bool,
spec: PortSpec,
}
impl DelayLine {
const MAX_DELAY_SECS: f64 = 2.0;
const DELAY_SMOOTH_SECS: f64 = 0.005;
pub fn new(sample_rate: f64) -> Self {
let buffer_size = (sample_rate * Self::MAX_DELAY_SECS) as usize + 1;
Self {
buffer: vec![0.0; buffer_size],
write_pos: 0,
sample_rate,
smoothed_delay: 0.0,
delay_smooth_coef: env_coef(Self::DELAY_SMOOTH_SECS, sample_rate),
delay_primed: false,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "time", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
PortDef::new(2, "feedback", SignalKind::CvUnipolar)
.with_default(0.0)
.with_attenuverter(),
PortDef::new(3, "mix", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
}
}
}
impl Default for DelayLine {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for DelayLine {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = sanitize_audio(inputs.get_or(0, 0.0));
let time_cv = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
let feedback = inputs.get_or(2, 0.0).clamp(0.0, 0.99); let mix = inputs.get_or(3, 0.5).clamp(0.0, 1.0);
let min_delay_ms = 1.0;
let max_delay_ms = Self::MAX_DELAY_SECS * 1000.0;
let delay_ms = min_delay_ms * Libm::<f64>::pow(max_delay_ms / min_delay_ms, time_cv);
let target_delay =
(delay_ms * self.sample_rate / 1000.0).clamp(1.0, (self.buffer.len() - 1) as f64);
if self.delay_primed {
self.smoothed_delay =
target_delay + (self.smoothed_delay - target_delay) * self.delay_smooth_coef;
} else {
self.smoothed_delay = target_delay;
self.delay_primed = true;
}
let delay_samples = self.smoothed_delay;
let delayed = read_interpolated(&self.buffer, self.write_pos, delay_samples);
self.buffer[self.write_pos] = input + delayed * feedback;
self.write_pos = (self.write_pos + 1) % self.buffer.len();
let output = input * (1.0 - mix) + delayed * mix;
outputs.set(10, output);
}
fn reset(&mut self) {
self.buffer.fill(0.0);
self.write_pos = 0;
self.smoothed_delay = 0.0;
self.delay_primed = false;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
let buffer_size = (sample_rate * Self::MAX_DELAY_SECS) as usize + 1;
self.buffer = vec![0.0; buffer_size];
self.write_pos = 0;
self.smoothed_delay = 0.0;
self.delay_smooth_coef = env_coef(Self::DELAY_SMOOTH_SECS, sample_rate);
self.delay_primed = false;
}
fn breaks_feedback_cycle(&self) -> bool {
true
}
fn type_id(&self) -> &'static str {
"delay_line"
}
}
pub struct Chorus {
delay_buffers: [Vec<f64>; 3],
write_pos: usize,
lfo_phases: [f64; 3],
sample_rate: f64,
spec: PortSpec,
}
impl Chorus {
const MAX_MOD_DELAY_MS: f64 = 25.0;
const BASE_DELAY_MS: f64 = 7.0;
#[inline]
fn voice_delay_samples(base_delay_samples: f64, mod_depth_samples: f64, lfo_val: f64) -> f64 {
base_delay_samples + (lfo_val * 0.5 + 0.5) * mod_depth_samples
}
pub fn new(sample_rate: f64) -> Self {
let buffer_size =
((Self::MAX_MOD_DELAY_MS + Self::BASE_DELAY_MS) * sample_rate / 1000.0) as usize + 10;
Self {
delay_buffers: [
vec![0.0; buffer_size],
vec![0.0; buffer_size],
vec![0.0; buffer_size],
],
write_pos: 0,
lfo_phases: [0.0, 0.33, 0.67],
sample_rate,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "rate", SignalKind::CvUnipolar)
.with_default(0.3)
.with_attenuverter(),
PortDef::new(2, "depth", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
PortDef::new(3, "mix", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
],
outputs: vec![
PortDef::new(10, "out", SignalKind::Audio),
PortDef::new(11, "left", SignalKind::Audio),
PortDef::new(12, "right", SignalKind::Audio),
],
},
}
}
}
impl Default for Chorus {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for Chorus {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = sanitize_audio(inputs.get_or(0, 0.0));
let rate_cv = inputs.get_or(1, 0.3).clamp(0.0, 1.0);
let depth_cv = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
let mix = inputs.get_or(3, 0.5).clamp(0.0, 1.0);
let lfo_freq = 0.1 * Libm::<f64>::pow(50.0, rate_cv);
let mod_depth_ms = depth_cv * Self::MAX_MOD_DELAY_MS;
let base_delay_samples = Self::BASE_DELAY_MS * self.sample_rate / 1000.0;
let mod_depth_samples = mod_depth_ms * self.sample_rate / 1000.0;
let mut wet_sum = 0.0;
let mut left_sum = 0.0;
let mut right_sum = 0.0;
for i in 0..3 {
let lfo_val = Libm::<f64>::sin(self.lfo_phases[i] * core::f64::consts::TAU);
let delay_samples =
Self::voice_delay_samples(base_delay_samples, mod_depth_samples, lfo_val)
.clamp(1.0, (self.delay_buffers[i].len() - 1) as f64);
let delayed = read_interpolated(&self.delay_buffers[i], self.write_pos, delay_samples);
wet_sum += delayed;
match i {
0 => {
left_sum += delayed * 0.5;
right_sum += delayed * 0.5;
}
1 => left_sum += delayed,
2 => right_sum += delayed,
_ => {}
}
self.delay_buffers[i][self.write_pos] = input;
let freq_mult = 1.0 + (i as f64 - 1.0) * 0.1; let phase_inc = lfo_freq * freq_mult / self.sample_rate;
self.lfo_phases[i] += phase_inc;
if self.lfo_phases[i] >= 1.0 {
self.lfo_phases[i] -= 1.0;
}
}
wet_sum /= 3.0;
left_sum /= 2.0;
right_sum /= 2.0;
self.write_pos = (self.write_pos + 1) % self.delay_buffers[0].len();
let mono_out = input * (1.0 - mix) + wet_sum * mix;
let left_out = input * (1.0 - mix) + left_sum * mix;
let right_out = input * (1.0 - mix) + right_sum * mix;
outputs.set(10, mono_out);
outputs.set(11, left_out);
outputs.set(12, right_out);
}
fn reset(&mut self) {
for buffer in &mut self.delay_buffers {
buffer.fill(0.0);
}
self.write_pos = 0;
self.lfo_phases = [0.0, 0.33, 0.67];
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
let buffer_size =
((Self::MAX_MOD_DELAY_MS + Self::BASE_DELAY_MS) * sample_rate / 1000.0) as usize + 10;
for buffer in &mut self.delay_buffers {
*buffer = vec![0.0; buffer_size];
}
self.write_pos = 0;
}
fn type_id(&self) -> &'static str {
"chorus"
}
}
pub struct Flanger {
buffers: [Vec<f64>; 2],
write_pos: usize,
lfo_phase: f64,
sample_rate: f64,
spec: PortSpec,
}
impl Flanger {
const MAX_DELAY_MS: f64 = 10.0;
pub fn new(sample_rate: f64) -> Self {
let buffer_size = (sample_rate * Self::MAX_DELAY_MS / 1000.0) as usize + 10;
Self {
buffers: [vec![0.0; buffer_size], vec![0.0; buffer_size]],
write_pos: 0,
lfo_phase: 0.0,
sample_rate,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "rate", SignalKind::CvUnipolar)
.with_default(0.3)
.with_attenuverter(),
PortDef::new(2, "depth", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
PortDef::new(3, "feedback", SignalKind::CvBipolar)
.with_default(0.0)
.with_attenuverter(),
PortDef::new(4, "mix", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
PortDef::new(5, "spread", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
],
outputs: vec![
PortDef::new(10, "out", SignalKind::Audio),
PortDef::new(11, "left", SignalKind::Audio),
PortDef::new(12, "right", SignalKind::Audio),
],
},
}
}
}
impl Default for Flanger {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for Flanger {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = sanitize_audio(inputs.get_or(0, 0.0));
let rate_cv = inputs.get_or(1, 0.3).clamp(0.0, 1.0);
let depth_cv = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
let feedback = inputs.get_or(3, 0.0).clamp(-0.95, 0.95);
let mix = inputs.get_or(4, 0.5).clamp(0.0, 1.0);
let spread = inputs.get_or(5, 0.5).clamp(0.0, 1.0);
let lfo_freq = 0.05 * Libm::<f64>::pow(100.0, rate_cv);
let base_delay_ms = 1.0;
let mod_depth_ms = depth_cv * (Self::MAX_DELAY_MS - base_delay_ms);
let phase_offset = spread * 0.5;
let max_read = (self.buffers[0].len() - 1) as f64;
let mut wet = [0.0; 2];
for (ch, w) in wet.iter_mut().enumerate() {
let phase = self.lfo_phase + if ch == 0 { 0.0 } else { phase_offset };
let lfo = (Libm::<f64>::sin(phase * TAU) + 1.0) * 0.5;
let delay_ms = base_delay_ms + lfo * mod_depth_ms;
let delay_samples = (delay_ms * self.sample_rate / 1000.0).clamp(1.0, max_read);
let delayed = read_interpolated(&self.buffers[ch], self.write_pos, delay_samples);
self.buffers[ch][self.write_pos] = input + delayed * feedback;
*w = delayed;
}
self.lfo_phase += lfo_freq / self.sample_rate;
if self.lfo_phase >= 1.0 {
self.lfo_phase -= 1.0;
}
self.write_pos = (self.write_pos + 1) % self.buffers[0].len();
let left = input * (1.0 - mix) + wet[0] * mix;
let right = input * (1.0 - mix) + wet[1] * mix;
outputs.set(10, left);
outputs.set(11, left);
outputs.set(12, right);
}
fn reset(&mut self) {
for buffer in &mut self.buffers {
buffer.fill(0.0);
}
self.write_pos = 0;
self.lfo_phase = 0.0;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
let buffer_size = (sample_rate * Self::MAX_DELAY_MS / 1000.0) as usize + 10;
for buffer in &mut self.buffers {
*buffer = vec![0.0; buffer_size];
}
self.write_pos = 0;
}
fn type_id(&self) -> &'static str {
"flanger"
}
}
pub struct Phaser {
allpass_x1: [[f64; 6]; 2],
allpass_y1: [[f64; 6]; 2],
lfo_phase: f64,
sample_rate: f64,
spec: PortSpec,
}
impl Phaser {
pub fn new(sample_rate: f64) -> Self {
Self {
allpass_x1: [[0.0; 6]; 2],
allpass_y1: [[0.0; 6]; 2],
lfo_phase: 0.0,
sample_rate,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "rate", SignalKind::CvUnipolar)
.with_default(0.3)
.with_attenuverter(),
PortDef::new(2, "depth", SignalKind::CvUnipolar)
.with_default(0.7)
.with_attenuverter(),
PortDef::new(3, "feedback", SignalKind::CvBipolar)
.with_default(0.0)
.with_attenuverter(),
PortDef::new(4, "mix", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
PortDef::new(5, "stages", SignalKind::CvUnipolar).with_default(1.0),
PortDef::new(6, "spread", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
],
outputs: vec![
PortDef::new(10, "out", SignalKind::Audio),
PortDef::new(11, "left", SignalKind::Audio),
PortDef::new(12, "right", SignalKind::Audio),
],
},
}
}
fn allpass(input: f64, x1: &mut f64, y1: &mut f64, coef: f64) -> f64 {
let output = coef * input + *x1 - coef * *y1;
*x1 = input;
*y1 = output;
output
}
}
impl Default for Phaser {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for Phaser {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = sanitize_audio(inputs.get_or(0, 0.0));
let rate_cv = inputs.get_or(1, 0.3).clamp(0.0, 1.0);
let depth = inputs.get_or(2, 0.7).clamp(0.0, 1.0);
let feedback = inputs.get_or(3, 0.0).clamp(-0.95, 0.95);
let mix = inputs.get_or(4, 0.5).clamp(0.0, 1.0);
let stages_cv = inputs.get_or(5, 1.0).clamp(0.0, 1.0);
let num_stages = if stages_cv < 0.33 {
2
} else if stages_cv < 0.66 {
4
} else {
6
};
let spread = inputs.get_or(6, 0.5).clamp(0.0, 1.0);
let lfo_freq = 0.05 * Libm::<f64>::pow(100.0, rate_cv);
let min_freq = 200.0;
let max_freq = 4000.0;
let phase_offset = spread * 0.5;
let mut wet = [0.0; 2];
for (ch, w) in wet.iter_mut().enumerate() {
let phase = self.lfo_phase + if ch == 0 { 0.0 } else { phase_offset };
let lfo = Libm::<f64>::sin(phase * TAU);
let freq = min_freq + (lfo * 0.5 + 0.5) * depth * (max_freq - min_freq);
let omega = TAU * freq / self.sample_rate;
let tan_w = Libm::<f64>::tan(omega * 0.5);
let coef = (1.0 - tan_w) / (1.0 + tan_w);
let mut signal = input + self.allpass_y1[ch][num_stages - 1] * feedback;
for i in 0..num_stages {
signal = Self::allpass(
signal,
&mut self.allpass_x1[ch][i],
&mut self.allpass_y1[ch][i],
coef,
);
}
*w = signal;
}
self.lfo_phase += lfo_freq / self.sample_rate;
if self.lfo_phase >= 1.0 {
self.lfo_phase -= 1.0;
}
let left = input * (1.0 - mix) + wet[0] * mix;
let right = input * (1.0 - mix) + wet[1] * mix;
outputs.set(10, left);
outputs.set(11, left);
outputs.set(12, right);
}
fn reset(&mut self) {
self.allpass_x1 = [[0.0; 6]; 2];
self.allpass_y1 = [[0.0; 6]; 2];
self.lfo_phase = 0.0;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
}
fn type_id(&self) -> &'static str {
"phaser"
}
}
pub struct Tremolo {
lfo_phase: f64,
sample_rate: f64,
spec: PortSpec,
}
impl Tremolo {
pub fn new(sample_rate: f64) -> Self {
Self {
lfo_phase: 0.0,
sample_rate,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "rate", SignalKind::CvUnipolar)
.with_default(0.3)
.with_attenuverter(),
PortDef::new(2, "depth", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
PortDef::new(3, "shape", SignalKind::CvUnipolar)
.with_default(0.0)
.with_attenuverter(),
],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
}
}
}
impl Default for Tremolo {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for Tremolo {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = inputs.get_or(0, 0.0);
let rate_cv = inputs.get_or(1, 0.3).clamp(0.0, 1.0);
let depth = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
let shape = inputs.get_or(3, 0.0).clamp(0.0, 1.0);
let lfo_freq = 0.1 * Libm::<f64>::pow(200.0, rate_cv);
let phase_rad = self.lfo_phase * TAU;
let sine = Libm::<f64>::sin(phase_rad);
let triangle = 1.0 - 4.0 * Libm::<f64>::fabs(self.lfo_phase - 0.5);
let lfo = sine * (1.0 - shape) + triangle * shape;
self.lfo_phase += lfo_freq / self.sample_rate;
if self.lfo_phase >= 1.0 {
self.lfo_phase -= 1.0;
}
let modulation = 1.0 - depth * 0.5 * (1.0 - lfo);
outputs.set(10, input * modulation);
}
fn reset(&mut self) {
self.lfo_phase = 0.0;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
}
fn type_id(&self) -> &'static str {
"tremolo"
}
}
pub struct Vibrato {
buffer: Vec<f64>,
write_pos: usize,
lfo_phase: f64,
sample_rate: f64,
spec: PortSpec,
}
impl Vibrato {
const MAX_DELAY_MS: f64 = 20.0;
pub fn new(sample_rate: f64) -> Self {
let buffer_size = (sample_rate * Self::MAX_DELAY_MS / 1000.0) as usize + 10;
Self {
buffer: vec![0.0; buffer_size],
write_pos: 0,
lfo_phase: 0.0,
sample_rate,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "rate", SignalKind::CvUnipolar)
.with_default(0.3)
.with_attenuverter(),
PortDef::new(2, "depth", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
PortDef::new(3, "mix", SignalKind::CvUnipolar)
.with_default(1.0)
.with_attenuverter(),
],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
}
}
}
impl Default for Vibrato {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for Vibrato {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = inputs.get_or(0, 0.0);
let rate_cv = inputs.get_or(1, 0.3).clamp(0.0, 1.0);
let depth = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
let mix = inputs.get_or(3, 1.0).clamp(0.0, 1.0);
let lfo_freq = 0.1 * Libm::<f64>::pow(150.0, rate_cv);
let base_delay_ms = Self::MAX_DELAY_MS * 0.5;
let mod_depth_ms = depth * base_delay_ms * 0.9;
let lfo = Libm::<f64>::sin(self.lfo_phase * TAU);
self.lfo_phase += lfo_freq / self.sample_rate;
if self.lfo_phase >= 1.0 {
self.lfo_phase -= 1.0;
}
let delay_ms = base_delay_ms + lfo * mod_depth_ms;
let delay_samples =
(delay_ms * self.sample_rate / 1000.0).clamp(1.0, (self.buffer.len() - 1) as f64);
let delayed = read_interpolated(&self.buffer, self.write_pos, delay_samples);
self.buffer[self.write_pos] = input;
self.write_pos = (self.write_pos + 1) % self.buffer.len();
outputs.set(10, input * (1.0 - mix) + delayed * mix);
}
fn reset(&mut self) {
self.buffer.fill(0.0);
self.write_pos = 0;
self.lfo_phase = 0.0;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
let buffer_size = (sample_rate * Self::MAX_DELAY_MS / 1000.0) as usize + 10;
self.buffer = vec![0.0; buffer_size];
self.write_pos = 0;
self.lfo_phase = 0.0;
}
fn type_id(&self) -> &'static str {
"vibrato"
}
}
const COMB_TUNINGS_44100: [usize; 8] = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617];
const ALLPASS_TUNINGS_44100: [usize; 4] = [556, 441, 341, 225];
const STEREO_SPREAD: usize = 23;
const MAX_COMB_SIZE: usize = 4096;
const MAX_ALLPASS_SIZE: usize = 1500;
const MAX_PREDELAY_SIZE: usize = 9600;
pub struct Reverb {
comb_buffers_l: Vec<Vec<f64>>,
comb_buffers_r: Vec<Vec<f64>>,
comb_pos_l: [usize; 8],
comb_pos_r: [usize; 8],
comb_filter_state_l: [f64; 8], comb_filter_state_r: [f64; 8],
allpass_buffers_l: Vec<Vec<f64>>,
allpass_buffers_r: Vec<Vec<f64>>,
allpass_pos_l: [usize; 4],
allpass_pos_r: [usize; 4],
predelay_buffer: Vec<f64>,
predelay_pos: usize,
comb_lengths: [usize; 8],
allpass_lengths: [usize; 4],
stereo_spread: usize,
sample_rate: f64,
spec: PortSpec,
}
impl Reverb {
pub fn new(sample_rate: f64) -> Self {
let mut reverb = Self {
comb_buffers_l: (0..8).map(|_| vec![0.0; MAX_COMB_SIZE]).collect(),
comb_buffers_r: (0..8).map(|_| vec![0.0; MAX_COMB_SIZE]).collect(),
comb_pos_l: [0; 8],
comb_pos_r: [0; 8],
comb_filter_state_l: [0.0; 8],
comb_filter_state_r: [0.0; 8],
allpass_buffers_l: (0..4).map(|_| vec![0.0; MAX_ALLPASS_SIZE]).collect(),
allpass_buffers_r: (0..4).map(|_| vec![0.0; MAX_ALLPASS_SIZE]).collect(),
allpass_pos_l: [0; 4],
allpass_pos_r: [0; 4],
predelay_buffer: vec![0.0; MAX_PREDELAY_SIZE],
predelay_pos: 0,
comb_lengths: [0; 8],
allpass_lengths: [0; 4],
stereo_spread: STEREO_SPREAD,
sample_rate,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "size", SignalKind::CvUnipolar).with_default(0.5),
PortDef::new(2, "damping", SignalKind::CvUnipolar).with_default(0.5),
PortDef::new(3, "mix", SignalKind::CvUnipolar).with_default(0.5),
PortDef::new(4, "predelay", SignalKind::CvUnipolar).with_default(0.0),
],
outputs: vec![
PortDef::new(10, "left", SignalKind::Audio),
PortDef::new(11, "right", SignalKind::Audio),
],
},
};
reverb.update_tunings();
reverb
}
fn update_tunings(&mut self) {
let ratio = self.sample_rate / 44100.0;
for (i, &base) in COMB_TUNINGS_44100.iter().enumerate() {
self.comb_lengths[i] = ((base as f64 * ratio) as usize).min(MAX_COMB_SIZE - 1);
}
for (i, &base) in ALLPASS_TUNINGS_44100.iter().enumerate() {
self.allpass_lengths[i] = ((base as f64 * ratio) as usize).min(MAX_ALLPASS_SIZE - 1);
}
self.stereo_spread = (Libm::<f64>::round(STEREO_SPREAD as f64 * ratio) as usize).max(1);
}
#[inline]
fn process_comb(
buffer: &mut [f64],
pos: &mut usize,
filter_state: &mut f64,
input: f64,
length: usize,
feedback: f64,
damping: f64,
) -> f64 {
let output = buffer[*pos];
*filter_state = output * (1.0 - damping) + *filter_state * damping;
buffer[*pos] = input + *filter_state * feedback;
*pos += 1;
if *pos >= length {
*pos = 0;
}
output
}
#[inline]
fn process_allpass(buffer: &mut [f64], pos: &mut usize, input: f64, length: usize) -> f64 {
const ALLPASS_FEEDBACK: f64 = 0.5;
let buffered = buffer[*pos];
let output = -input + buffered;
buffer[*pos] = input + buffered * ALLPASS_FEEDBACK;
*pos += 1;
if *pos >= length {
*pos = 0;
}
output
}
}
impl Default for Reverb {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for Reverb {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = sanitize_audio(inputs.get_or(0, 0.0));
let size = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
let damping = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
let mix = inputs.get_or(3, 0.5).clamp(0.0, 1.0);
let predelay_cv = inputs.get_or(4, 0.0).clamp(0.0, 1.0);
let room_scale = 0.28 + size * 0.7;
let damp = damping * 0.4;
let predelay_samples =
(predelay_cv * 0.1 * self.sample_rate).min(MAX_PREDELAY_SIZE as f64 - 1.0) as usize;
self.predelay_buffer[self.predelay_pos] = input;
let predelay_read_pos = if self.predelay_pos >= predelay_samples {
self.predelay_pos - predelay_samples
} else {
MAX_PREDELAY_SIZE - (predelay_samples - self.predelay_pos)
};
let predelayed = if predelay_samples > 0 {
self.predelay_buffer[predelay_read_pos]
} else {
input
};
self.predelay_pos = (self.predelay_pos + 1) % MAX_PREDELAY_SIZE;
let mut comb_out_l = 0.0;
let mut comb_out_r = 0.0;
for i in 0..8 {
let length_l = self.comb_lengths[i];
comb_out_l += Self::process_comb(
&mut self.comb_buffers_l[i],
&mut self.comb_pos_l[i],
&mut self.comb_filter_state_l[i],
predelayed,
length_l,
room_scale,
damp,
);
let length_r = (self.comb_lengths[i] + self.stereo_spread).min(MAX_COMB_SIZE - 1);
comb_out_r += Self::process_comb(
&mut self.comb_buffers_r[i],
&mut self.comb_pos_r[i],
&mut self.comb_filter_state_r[i],
predelayed,
length_r,
room_scale,
damp,
);
}
comb_out_l *= 0.125;
comb_out_r *= 0.125;
let mut allpass_out_l = comb_out_l;
let mut allpass_out_r = comb_out_r;
for i in 0..4 {
let length_l = self.allpass_lengths[i];
allpass_out_l = Self::process_allpass(
&mut self.allpass_buffers_l[i],
&mut self.allpass_pos_l[i],
allpass_out_l,
length_l,
);
let length_r = (self.allpass_lengths[i] + self.stereo_spread).min(MAX_ALLPASS_SIZE - 1);
allpass_out_r = Self::process_allpass(
&mut self.allpass_buffers_r[i],
&mut self.allpass_pos_r[i],
allpass_out_r,
length_r,
);
}
let left = input * (1.0 - mix) + allpass_out_l * mix;
let right = input * (1.0 - mix) + allpass_out_r * mix;
outputs.set(10, left);
outputs.set(11, right);
}
fn reset(&mut self) {
for buf in &mut self.comb_buffers_l {
buf.iter_mut().for_each(|x| *x = 0.0);
}
for buf in &mut self.comb_buffers_r {
buf.iter_mut().for_each(|x| *x = 0.0);
}
self.comb_pos_l = [0; 8];
self.comb_pos_r = [0; 8];
self.comb_filter_state_l = [0.0; 8];
self.comb_filter_state_r = [0.0; 8];
for buf in &mut self.allpass_buffers_l {
buf.iter_mut().for_each(|x| *x = 0.0);
}
for buf in &mut self.allpass_buffers_r {
buf.iter_mut().for_each(|x| *x = 0.0);
}
self.allpass_pos_l = [0; 4];
self.allpass_pos_r = [0; 4];
self.predelay_buffer.iter_mut().for_each(|x| *x = 0.0);
self.predelay_pos = 0;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
self.update_tunings();
self.reset();
}
fn type_id(&self) -> &'static str {
"reverb"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unit_delay() {
let mut delay = UnitDelay::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0);
delay.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 0.0).abs() < 0.01);
inputs.set(0, 2.0);
delay.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 1.0).abs() < 0.01); }
#[test]
fn test_delay_line() {
let mut delay = DelayLine::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.0); inputs.set(2, 0.0); inputs.set(3, 1.0);
inputs.set(0, 1.0);
delay.tick(&inputs, &mut outputs);
let first_out = outputs.get(10).unwrap();
assert!(first_out.abs() < 0.1);
inputs.set(0, 0.0);
for _ in 0..100 {
delay.tick(&inputs, &mut outputs);
}
let out = outputs.get(10).unwrap();
assert!(out.is_finite());
}
#[test]
fn test_delay_line_feedback() {
let mut delay = DelayLine::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.0); inputs.set(2, 0.5); inputs.set(3, 0.5);
inputs.set(0, 1.0);
delay.tick(&inputs, &mut outputs);
inputs.set(0, 0.0);
for _ in 0..1000 {
delay.tick(&inputs, &mut outputs);
}
let out = outputs.get(10).unwrap();
assert!(out.is_finite());
}
#[test]
fn test_delay_line_reset() {
let mut delay = DelayLine::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0);
for _ in 0..100 {
delay.tick(&inputs, &mut outputs);
}
delay.reset();
inputs.set(0, 0.0);
inputs.set(3, 1.0); delay.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
assert!(out.abs() < 0.01);
}
#[test]
fn test_chorus() {
let mut chorus = Chorus::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.5);
for _ in 0..1000 {
chorus.tick(&inputs, &mut outputs);
}
let mono = outputs.get(10).unwrap();
let left = outputs.get(11).unwrap();
let right = outputs.get(12).unwrap();
assert!(mono.is_finite());
assert!(left.is_finite());
assert!(right.is_finite());
}
#[test]
fn test_chorus_stereo_spread() {
let mut chorus = Chorus::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0); inputs.set(1, 0.5); inputs.set(2, 0.5); inputs.set(3, 1.0);
let mut left_sum = 0.0;
let mut right_sum = 0.0;
for _ in 0..10000 {
chorus.tick(&inputs, &mut outputs);
left_sum += outputs.get(11).unwrap().abs();
right_sum += outputs.get(12).unwrap().abs();
}
assert!(left_sum > 1.0);
assert!(right_sum > 1.0);
}
#[test]
fn test_chorus_reset() {
let mut chorus = Chorus::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0);
for _ in 0..1000 {
chorus.tick(&inputs, &mut outputs);
}
chorus.reset();
inputs.set(0, 0.0);
inputs.set(3, 1.0); chorus.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
assert!(out.abs() < 0.1);
}
#[test]
fn test_delay_line_type_id() {
let delay = DelayLine::new(44100.0);
assert_eq!(delay.type_id(), "delay_line");
}
#[test]
fn test_chorus_type_id() {
let chorus = Chorus::new(44100.0);
assert_eq!(chorus.type_id(), "chorus");
}
#[test]
fn test_delay_line_default() {
let delay = DelayLine::default();
assert_eq!(delay.type_id(), "delay_line");
}
#[test]
fn test_chorus_default() {
let chorus = Chorus::default();
assert_eq!(chorus.type_id(), "chorus");
}
#[test]
fn test_flanger() {
let mut flanger = Flanger::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0);
for _ in 0..1000 {
flanger.tick(&inputs, &mut outputs);
}
let out = outputs.get(10).unwrap();
assert!(out.is_finite());
}
#[test]
fn test_flanger_default() {
let flanger = Flanger::default();
assert_eq!(flanger.type_id(), "flanger");
}
#[test]
fn test_phaser() {
let mut phaser = Phaser::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0);
for _ in 0..1000 {
phaser.tick(&inputs, &mut outputs);
}
let out = outputs.get(10).unwrap();
assert!(out.is_finite());
}
#[test]
fn test_phaser_default() {
let phaser = Phaser::default();
assert_eq!(phaser.type_id(), "phaser");
}
#[test]
fn test_phaser_stages() {
let mut phaser = Phaser::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0);
inputs.set(5, 0.0);
for _ in 0..100 {
phaser.tick(&inputs, &mut outputs);
}
let out_2 = outputs.get(10).unwrap();
phaser.reset();
inputs.set(5, 1.0);
for _ in 0..100 {
phaser.tick(&inputs, &mut outputs);
}
let out_6 = outputs.get(10).unwrap();
assert!(out_2.is_finite());
assert!(out_6.is_finite());
}
#[test]
fn test_flanger_out_mirrors_left_and_mono_at_zero_spread() {
let mut flanger = Flanger::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(2, 0.8); inputs.set(4, 1.0); inputs.set(5, 0.0);
for k in 0..5000 {
inputs.set(0, Libm::<f64>::sin(k as f64 * 0.03));
flanger.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
let left = outputs.get(11).unwrap();
let right = outputs.get(12).unwrap();
assert_eq!(out, left, "out must equal left");
assert_eq!(left, right, "spread=0 must give bit-identical L/R");
}
}
#[test]
fn test_flanger_stereo_decorrelation() {
let mut flanger = Flanger::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.5); inputs.set(2, 0.9); inputs.set(4, 1.0); inputs.set(5, 1.0);
let mut diff = 0.0;
for k in 0..20000 {
inputs.set(0, Libm::<f64>::sin(k as f64 * 0.05));
flanger.tick(&inputs, &mut outputs);
assert_eq!(outputs.get(10).unwrap(), outputs.get(11).unwrap());
let left = outputs.get(11).unwrap();
let right = outputs.get(12).unwrap();
diff += (left - right).abs();
}
assert!(
diff > 1.0,
"left/right should decorrelate with spread; diff = {diff}"
);
}
#[test]
fn test_phaser_out_mirrors_left_and_mono_at_zero_spread() {
let mut phaser = Phaser::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(2, 0.8); inputs.set(4, 1.0); inputs.set(6, 0.0);
for k in 0..5000 {
inputs.set(0, Libm::<f64>::sin(k as f64 * 0.03));
phaser.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
let left = outputs.get(11).unwrap();
let right = outputs.get(12).unwrap();
assert_eq!(out, left, "out must equal left");
assert_eq!(left, right, "spread=0 must give bit-identical L/R");
}
}
#[test]
fn test_phaser_stereo_decorrelation() {
let mut phaser = Phaser::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(2, 0.9); inputs.set(4, 1.0); inputs.set(6, 1.0);
let mut diff = 0.0;
for k in 0..20000 {
inputs.set(0, Libm::<f64>::sin(k as f64 * 0.07));
phaser.tick(&inputs, &mut outputs);
assert_eq!(outputs.get(10).unwrap(), outputs.get(11).unwrap());
let left = outputs.get(11).unwrap();
let right = outputs.get(12).unwrap();
diff += (left - right).abs();
}
assert!(
diff > 1.0,
"phaser left/right should decorrelate with spread; diff = {diff}"
);
}
#[test]
fn test_unit_delay_default_reset_sample_rate() {
let mut delay = UnitDelay::default();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 5.0);
delay.tick(&inputs, &mut outputs);
delay.reset();
assert!(delay.buffer == 0.0);
delay.set_sample_rate(48000.0);
assert_eq!(delay.type_id(), "unit_delay");
}
#[test]
fn test_reverb_default_reset_sample_rate() {
let mut reverb = Reverb::default();
assert_eq!(reverb.sample_rate, 44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.5);
reverb.tick(&inputs, &mut outputs);
reverb.reset();
assert_eq!(reverb.predelay_pos, 0);
assert_eq!(reverb.comb_pos_l, [0; 8]);
assert_eq!(reverb.comb_pos_r, [0; 8]);
assert_eq!(reverb.allpass_pos_l, [0; 4]);
assert_eq!(reverb.allpass_pos_r, [0; 4]);
reverb.set_sample_rate(48000.0);
assert_eq!(reverb.sample_rate, 48000.0);
assert_eq!(reverb.type_id(), "reverb");
assert_eq!(reverb.port_spec().inputs.len(), 5);
assert_eq!(reverb.port_spec().outputs.len(), 2);
}
#[test]
fn test_reverb_stereo_output() {
let mut reverb = Reverb::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0);
inputs.set(3, 1.0); reverb.tick(&inputs, &mut outputs);
inputs.set(0, 0.0);
let mut total_energy = 0.0;
for _ in 0..3000 {
reverb.tick(&inputs, &mut outputs);
total_energy += outputs.get(10).unwrap().abs();
total_energy += outputs.get(11).unwrap().abs();
}
assert!(
total_energy > 0.01,
"Reverb should produce output after impulse, got total_energy={}",
total_energy
);
}
#[test]
fn test_reverb_dry_signal() {
let mut reverb = Reverb::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.75);
inputs.set(3, 0.0); reverb.tick(&inputs, &mut outputs);
let left = outputs.get(10).unwrap();
let right = outputs.get(11).unwrap();
assert!(
(left - 0.75).abs() < 0.001,
"Full dry should pass through: got {}",
left
);
assert!(
(right - 0.75).abs() < 0.001,
"Full dry should pass through: got {}",
right
);
}
#[test]
fn test_reverb_room_size() {
let mut reverb1 = Reverb::new(44100.0);
let mut reverb2 = Reverb::new(44100.0);
let mut inputs1 = PortValues::new();
let mut inputs2 = PortValues::new();
let mut outputs1 = PortValues::new();
let mut outputs2 = PortValues::new();
inputs1.set(0, 1.0);
inputs1.set(1, 0.1); inputs1.set(3, 1.0); reverb1.tick(&inputs1, &mut outputs1);
inputs2.set(0, 1.0);
inputs2.set(1, 0.9); inputs2.set(3, 1.0); reverb2.tick(&inputs2, &mut outputs2);
inputs1.set(0, 0.0);
inputs2.set(0, 0.0);
let mut energy1 = 0.0;
let mut energy2 = 0.0;
for _ in 0..5000 {
reverb1.tick(&inputs1, &mut outputs1);
reverb2.tick(&inputs2, &mut outputs2);
energy1 += outputs1.get(10).unwrap().abs();
energy2 += outputs2.get(10).unwrap().abs();
}
assert!(
energy2 > energy1,
"Larger room should have longer decay: small={}, large={}",
energy1,
energy2
);
}
#[test]
fn test_reverb_predelay() {
let mut reverb = Reverb::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0); inputs.set(3, 1.0); inputs.set(4, 1.0);
reverb.tick(&inputs, &mut outputs);
let first_output = outputs.get(10).unwrap();
inputs.set(0, 0.0);
let mut total_energy = 0.0;
for _ in 0..6000 {
reverb.tick(&inputs, &mut outputs);
total_energy += outputs.get(10).unwrap().abs();
}
assert!(
total_energy > 0.01,
"Reverb should appear after predelay period, got energy={}",
total_energy
);
assert!(
first_output.abs() < 0.001,
"First sample should be near zero due to predelay, got {}",
first_output
);
}
#[test]
fn test_reverb_damping() {
let mut reverb_low = Reverb::new(44100.0);
let mut reverb_high = Reverb::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs_low = PortValues::new();
let mut outputs_high = PortValues::new();
inputs.set(0, 1.0);
inputs.set(2, 0.1); inputs.set(3, 1.0);
reverb_low.tick(&inputs, &mut outputs_low);
inputs.set(2, 0.9); reverb_high.tick(&inputs, &mut outputs_high);
inputs.set(0, 0.0);
for _ in 0..3000 {
reverb_low.tick(&inputs, &mut outputs_low);
reverb_high.tick(&inputs, &mut outputs_high);
}
let out_low = outputs_low.get(10).unwrap();
let out_high = outputs_high.get(10).unwrap();
assert!(out_low.is_finite());
assert!(out_high.is_finite());
}
#[test]
fn test_reverb_tunings_scale_with_sample_rate() {
let reverb_44 = Reverb::new(44100.0);
let reverb_48 = Reverb::new(48000.0);
let ratio = 48000.0 / 44100.0;
for i in 0..8 {
let expected = (reverb_44.comb_lengths[i] as f64 * ratio) as usize;
assert!(
(reverb_48.comb_lengths[i] as i64 - expected as i64).abs() < 2,
"Comb filter {} should scale with sample rate",
i
);
}
}
#[cfg(test)]
fn allpass_rms_gain(coef: f64, freq_norm: f64) -> f64 {
let mut x1 = 0.0;
let mut y1 = 0.0;
let n = 40_000;
let warmup = 8_000;
let mut sum_in = 0.0;
let mut sum_out = 0.0;
for i in 0..n {
let x = Libm::<f64>::sin(TAU * freq_norm * i as f64);
let y = Phaser::allpass(x, &mut x1, &mut y1, coef);
if i >= warmup {
sum_in += x * x;
sum_out += y * y;
}
}
Libm::<f64>::sqrt(sum_out / sum_in)
}
#[test]
fn test_phaser_allpass_unit_magnitude() {
for &coef in &[-0.6, -0.2, 0.2, 0.5, 0.8] {
let dc_gain = allpass_rms_gain(coef, 0.001);
let nyq_gain = allpass_rms_gain(coef, 0.499);
assert!(
(dc_gain - 1.0).abs() < 0.01,
"DC gain {} not ~1.0 for coef {}",
dc_gain,
coef
);
assert!(
(nyq_gain - 1.0).abs() < 0.01,
"Nyquist gain {} not ~1.0 for coef {}",
nyq_gain,
coef
);
}
}
#[test]
fn test_chorus_delay_stays_positive() {
let sample_rate = 44100.0;
let base = Chorus::BASE_DELAY_MS * sample_rate / 1000.0;
let mod_depth = Chorus::MAX_MOD_DELAY_MS * sample_rate / 1000.0;
let steps = 2000;
let mut min_delay = f64::INFINITY;
for k in 0..steps {
let lfo = Libm::<f64>::sin((k as f64 / steps as f64) * TAU);
let delay = Chorus::voice_delay_samples(base, mod_depth, lfo);
min_delay = min_delay.min(delay);
}
assert!(
min_delay > 1.0,
"minimum chorus delay {} hit the clamp floor",
min_delay
);
let trough = Chorus::voice_delay_samples(base, mod_depth, -1.0);
assert!(
(trough - base).abs() < 1e-9,
"trough {} != base {}",
trough,
base
);
}
#[test]
fn test_delay_line_time_smoothing() {
let mut delay = DelayLine::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.0);
inputs.set(1, 0.0); inputs.set(2, 0.0);
inputs.set(3, 0.5);
delay.tick(&inputs, &mut outputs); let start = delay.smoothed_delay;
inputs.set(1, 1.0);
delay.tick(&inputs, &mut outputs);
let after_one = delay.smoothed_delay;
let full_jump = (delay.buffer.len() - 1) as f64 - start;
let moved = after_one - start;
assert!(moved > 0.0, "smoother did not move toward setpoint");
assert!(
moved < full_jump * 0.05,
"smoother jumped {} of a {}-sample step in one tick",
moved,
full_jump
);
let mut ticks = 1;
while delay.smoothed_delay < start + full_jump * 0.9 && ticks < 100_000 {
delay.tick(&inputs, &mut outputs);
ticks += 1;
}
assert!(
ticks > 100,
"smoother converged too fast in {} ticks",
ticks
);
}
#[test]
fn test_vibrato_exact_delay() {
let sample_rate = 44100.0;
let mut vib = Vibrato::new(sample_rate);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.3); inputs.set(2, 0.0); inputs.set(3, 1.0);
let expected = (Vibrato::MAX_DELAY_MS * 0.5 * sample_rate / 1000.0).round() as usize;
inputs.set(0, 1.0); vib.tick(&inputs, &mut outputs);
let mut peak_idx = if outputs.get(10).unwrap().abs() > 0.5 {
Some(0usize)
} else {
None
};
inputs.set(0, 0.0);
for i in 1..(expected + 50) {
vib.tick(&inputs, &mut outputs);
if peak_idx.is_none() && outputs.get(10).unwrap().abs() > 0.5 {
peak_idx = Some(i);
}
}
assert_eq!(
peak_idx,
Some(expected),
"impulse emerged at {:?}, expected {}",
peak_idx,
expected
);
}
#[test]
fn test_reverb_stereo_spread_scales_with_sample_rate() {
let reverb_44 = Reverb::new(44100.0);
assert_eq!(reverb_44.stereo_spread, STEREO_SPREAD);
let reverb_88 = Reverb::new(88200.0);
assert_eq!(reverb_88.stereo_spread, 46);
for i in 0..8 {
let length_l = reverb_88.comb_lengths[i];
let length_r = (length_l + reverb_88.stereo_spread).min(MAX_COMB_SIZE - 1);
assert_eq!(
length_r - length_l,
46,
"right comb {} should lead left by the scaled spread",
i
);
}
}
#[test]
fn test_tremolo_am_depth() {
let mut trem = Tremolo::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0); inputs.set(1, 1.0); inputs.set(3, 0.0);
inputs.set(2, 1.0); let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
for _ in 0..44_100 {
trem.tick(&inputs, &mut outputs);
let o = outputs.get(10).unwrap();
assert!(o.is_finite());
lo = lo.min(o);
hi = hi.max(o);
}
assert!(
lo < 0.1 && hi > 0.9,
"full-depth AM must reach near 0 and near the carrier: lo={lo} hi={hi}"
);
trem.reset();
inputs.set(2, 0.0); let (mut lo0, mut hi0) = (f64::INFINITY, f64::NEG_INFINITY);
for _ in 0..4410 {
trem.tick(&inputs, &mut outputs);
let o = outputs.get(10).unwrap();
lo0 = lo0.min(o);
hi0 = hi0.max(o);
}
assert!(
(hi0 - lo0) < 1e-9 && (hi0 - 1.0).abs() < 1e-9,
"zero-depth tremolo must pass the carrier unchanged: span={}",
hi0 - lo0
);
}
#[test]
fn test_tremolo_reset_and_sample_rate() {
let mut trem = Tremolo::default();
assert_eq!(trem.type_id(), "tremolo");
assert_eq!(trem.sample_rate, 44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0);
inputs.set(1, 0.5);
for _ in 0..500 {
trem.tick(&inputs, &mut outputs);
}
assert!(trem.lfo_phase != 0.0);
trem.reset();
assert_eq!(trem.lfo_phase, 0.0);
trem.set_sample_rate(48000.0);
assert_eq!(trem.sample_rate, 48000.0);
trem.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap().is_finite());
}
fn zero_crossing_interval_spread(sig: &[f64]) -> f64 {
let mut crossings = Vec::new();
for i in 1..sig.len() {
if sig[i - 1] <= 0.0 && sig[i] > 0.0 {
crossings.push(i);
}
}
if crossings.len() < 3 {
return 0.0;
}
let mut min_iv = f64::INFINITY;
let mut max_iv = f64::NEG_INFINITY;
for w in crossings.windows(2) {
let iv = (w[1] - w[0]) as f64;
min_iv = min_iv.min(iv);
max_iv = max_iv.max(iv);
}
max_iv - min_iv
}
#[test]
fn test_vibrato_pitch_modulation_depth() {
let run = |depth: f64| -> f64 {
let mut vib = Vibrato::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.78); inputs.set(2, depth);
inputs.set(3, 1.0); let mut out = Vec::with_capacity(20_000);
let dt = 500.0 / 44100.0;
let mut phase = 0.0f64;
for _ in 0..20_000 {
let s = Libm::<f64>::sin(TAU * phase);
phase += dt;
if phase >= 1.0 {
phase -= 1.0;
}
inputs.set(0, s);
vib.tick(&inputs, &mut outputs);
out.push(outputs.get(10).unwrap());
}
zero_crossing_interval_spread(&out)
};
let spread_off = run(0.0);
let spread_on = run(0.8);
assert!(
spread_off < 3.0,
"zero-depth vibrato should have near-constant pitch: spread={spread_off}"
);
assert!(
spread_on > spread_off + 10.0,
"depth-0.8 vibrato must wobble the pitch: on={spread_on} off={spread_off}"
);
}
#[test]
fn test_vibrato_reset_and_sample_rate() {
let mut vib = Vibrato::default();
assert_eq!(vib.type_id(), "vibrato");
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0);
inputs.set(2, 0.5);
for _ in 0..500 {
vib.tick(&inputs, &mut outputs);
}
assert!(vib.lfo_phase != 0.0);
vib.reset();
assert_eq!(vib.lfo_phase, 0.0);
assert_eq!(vib.write_pos, 0);
assert!(vib.buffer.iter().all(|&x| x == 0.0));
vib.set_sample_rate(48000.0);
assert_eq!(vib.sample_rate, 48000.0);
vib.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap().is_finite());
}
#[test]
fn test_vibrato_lowering_sample_rate_does_not_panic() {
let mut vib = Vibrato::new(96000.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.5);
inputs.set(2, 0.5);
for _ in 0..1000 {
vib.tick(&inputs, &mut outputs);
}
vib.set_sample_rate(22050.0);
assert_eq!(vib.write_pos, 0, "write_pos must be reset after resize");
vib.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap().is_finite());
}
}