use super::common::{env_coef, sanitize_audio, GATE_THRESHOLD_V};
use super::oversample::{Oversample, Oversampler};
use crate::analog::saturation;
use crate::port::{GraphModule, PortDef, PortSpec, PortValues, SignalKind};
use alloc::vec;
use alloc::vec::Vec;
use libm::Libm;
fn rem_euclid_f64(x: f64, n: f64) -> f64 {
let r = Libm::<f64>::fmod(x, n);
if r < 0.0 {
r + Libm::<f64>::fabs(n)
} else {
r
}
}
pub struct Bitcrusher {
hold_sample: f64,
hold_counter: f64,
spec: PortSpec,
}
impl Bitcrusher {
pub fn new() -> Self {
Self {
hold_sample: 0.0,
hold_counter: 0.0,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "bits", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
PortDef::new(2, "downsample", SignalKind::CvUnipolar)
.with_default(0.0)
.with_attenuverter(),
],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
}
}
}
impl Default for Bitcrusher {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for Bitcrusher {
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 bits_cv = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
let downsample_cv = inputs.get_or(2, 0.0).clamp(0.0, 1.0);
let bits = 1.0 + bits_cv * 15.0;
let downsample_factor = 1.0 + downsample_cv * 63.0;
self.hold_counter += 1.0;
if self.hold_counter >= downsample_factor {
self.hold_counter -= downsample_factor;
self.hold_sample = input;
}
let levels = Libm::<f64>::round(Libm::<f64>::pow(2.0, bits)).max(2.0);
let steps = levels - 1.0;
let normalized = ((self.hold_sample / 5.0 + 1.0) * 0.5).clamp(0.0, 1.0);
let quantized = Libm::<f64>::round(normalized * steps) / steps;
outputs.set(10, (quantized * 2.0 - 1.0) * 5.0);
}
fn reset(&mut self) {
self.hold_sample = 0.0;
self.hold_counter = 0.0;
}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"bitcrusher"
}
}
const DISTORTION_TONE_MIN_HZ: f64 = 500.0;
const DISTORTION_TONE_MAX_HZ: f64 = 18_000.0;
pub struct Distortion {
tone_lp: f64,
sample_rate: f64,
oversampler: Oversampler,
spec: PortSpec,
}
impl Distortion {
pub fn new(sample_rate: f64) -> Self {
let sample_rate = if sample_rate > 0.0 {
sample_rate
} else {
44100.0
};
Self {
tone_lp: 0.0,
sample_rate,
oversampler: Oversampler::new(Oversample::Off),
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "drive", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
PortDef::new(2, "tone", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
PortDef::new(3, "mode", SignalKind::CvUnipolar)
.with_default(0.0)
.with_attenuverter(),
PortDef::new(4, "mix", SignalKind::CvUnipolar)
.with_default(1.0)
.with_attenuverter(),
],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
}
}
fn soft_clip(x: f64, drive: f64) -> f64 {
let gained = (x / 5.0) * (1.0 + drive * 10.0);
Libm::<f64>::tanh(gained) * 5.0
}
fn hard_clip(x: f64, drive: f64) -> f64 {
let gained = (x / 5.0) * (1.0 + drive * 10.0);
gained.clamp(-1.0, 1.0) * 5.0
}
fn foldback(x: f64, drive: f64) -> f64 {
let gained = (x / 5.0) * (1.0 + drive * 5.0);
Self::triangle_fold(gained, 1.0) * 5.0
}
fn triangle_fold(x: f64, threshold: f64) -> f64 {
let period = 4.0 * threshold;
threshold - Libm::<f64>::fabs(rem_euclid_f64(x + threshold, period) - 2.0 * threshold)
}
fn asymmetric(x: f64, drive: f64) -> f64 {
let gained = (x / 5.0) * (1.0 + drive * 8.0);
let shaped = if gained >= 0.0 {
1.0 - Libm::<f64>::exp(-gained)
} else {
Libm::<f64>::tanh(gained)
};
shaped * 5.0
}
pub fn set_oversample(&mut self, mode: Oversample) {
self.oversampler = Oversampler::new(mode);
}
pub fn oversample_factor(&self) -> usize {
self.oversampler.factor()
}
}
impl Default for Distortion {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for Distortion {
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 drive = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
let tone = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
let mode = inputs.get_or(3, 0.0).clamp(0.0, 1.0);
let mix = inputs.get_or(4, 1.0).clamp(0.0, 1.0);
let mode_idx = (mode * 3.99) as u8;
let distorted = self.oversampler.process(input, |x| match mode_idx {
0 => Self::soft_clip(x, drive),
1 => Self::hard_clip(x, drive),
2 => Self::foldback(x, drive),
_ => Self::asymmetric(x, drive),
});
let cutoff = DISTORTION_TONE_MIN_HZ
* Libm::<f64>::pow(DISTORTION_TONE_MAX_HZ / DISTORTION_TONE_MIN_HZ, tone);
let alpha =
1.0 - Libm::<f64>::exp(-2.0 * core::f64::consts::PI * cutoff / self.sample_rate);
self.tone_lp += alpha * (distorted - self.tone_lp);
let filtered = self.tone_lp;
outputs.set(10, input * (1.0 - mix) + filtered * mix);
}
fn reset(&mut self) {
self.tone_lp = 0.0;
self.oversampler.reset();
}
fn set_sample_rate(&mut self, sample_rate: f64) {
if sample_rate > 0.0 {
self.sample_rate = sample_rate;
}
self.tone_lp = 0.0;
self.oversampler.reset();
}
fn type_id(&self) -> &'static str {
"distortion"
}
crate::impl_introspect!();
}
pub struct RingModulator {
spec: PortSpec,
}
impl RingModulator {
pub fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![
PortDef::new(0, "carrier", SignalKind::Audio),
PortDef::new(1, "modulator", SignalKind::Audio),
],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
}
}
}
impl Default for RingModulator {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for RingModulator {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let carrier = inputs.get_or(0, 0.0);
let modulator = inputs.get_or(1, 0.0);
let out = (carrier * modulator) / 5.0;
outputs.set(10, out);
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"ring_mod"
}
}
pub struct PitchShifter {
buffer: [f64; 4800],
write_pos: usize,
grain_pos: [f64; 2],
grain_phase: [f64; 2],
sample_rate: f64,
spec: PortSpec,
}
impl PitchShifter {
const BUFFER_SIZE: usize = 4800;
pub fn new(sample_rate: f64) -> Self {
let spec = PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "shift", SignalKind::CvBipolar).with_default(0.0),
PortDef::new(2, "window", SignalKind::CvUnipolar).with_default(0.5),
PortDef::new(3, "mix", SignalKind::CvUnipolar).with_default(1.0),
],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
};
Self {
buffer: [0.0; Self::BUFFER_SIZE],
write_pos: 0,
grain_pos: [0.0, 0.5 * Self::BUFFER_SIZE as f64], grain_phase: [0.0, 0.5], sample_rate,
spec,
}
}
fn hann_window(phase: f64) -> f64 {
0.5 * (1.0 - Libm::<f64>::cos(phase * 2.0 * core::f64::consts::PI))
}
fn read_buffer(&self, pos: f64) -> f64 {
let pos = rem_euclid_f64(pos, Self::BUFFER_SIZE as f64);
let idx0 = pos as usize;
let idx1 = (idx0 + 1) % Self::BUFFER_SIZE;
let frac = pos - Libm::<f64>::floor(pos);
self.buffer[idx0] * (1.0 - frac) + self.buffer[idx1] * frac
}
}
impl Default for PitchShifter {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for PitchShifter {
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 shift_semitones = (inputs.get_or(1, 0.0) / 5.0) * 24.0;
let shift_semitones = shift_semitones.clamp(-24.0, 24.0);
let window_cv = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
let window_ms = 10.0 + window_cv * 90.0;
let mut window_samples = (window_ms * self.sample_rate / 1000.0) as usize;
window_samples = window_samples.min(Self::BUFFER_SIZE / 2);
let mix = inputs.get_or(3, 1.0).clamp(0.0, 1.0);
self.buffer[self.write_pos] = input / 5.0; self.write_pos = (self.write_pos + 1) % Self::BUFFER_SIZE;
let rate = Libm::<f64>::pow(2.0, shift_semitones / 12.0);
if rate > 1.0 {
let max_lead = Self::BUFFER_SIZE as f64 * 0.4;
let window_cap = (max_lead / (rate - 1.0)) as usize;
window_samples = window_samples.min(window_cap);
}
window_samples = window_samples.max(1);
let read_margin =
(rate - 1.0).max(0.0) * window_samples as f64 + window_samples as f64 * 0.5;
let phase_inc = 1.0 / window_samples as f64;
let mut wet_output = 0.0;
for i in 0..2 {
let sample = self.read_buffer(self.grain_pos[i]);
let window = Self::hann_window(self.grain_phase[i]);
wet_output += sample * window;
self.grain_pos[i] += rate;
if self.grain_pos[i] >= Self::BUFFER_SIZE as f64 {
self.grain_pos[i] -= Self::BUFFER_SIZE as f64;
} else if self.grain_pos[i] < 0.0 {
self.grain_pos[i] += Self::BUFFER_SIZE as f64;
}
self.grain_phase[i] += phase_inc;
if self.grain_phase[i] >= 1.0 {
self.grain_phase[i] -= 1.0;
self.grain_pos[i] = rem_euclid_f64(
self.write_pos as f64 - read_margin,
Self::BUFFER_SIZE as f64,
);
}
}
let dry = input / 5.0;
let output = dry * (1.0 - mix) + wet_output * mix;
outputs.set(10, output * 5.0); }
fn reset(&mut self) {
self.buffer = [0.0; Self::BUFFER_SIZE];
self.write_pos = 0;
self.grain_pos = [0.0, Self::BUFFER_SIZE as f64 * 0.5];
self.grain_phase = [0.0, 0.5];
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
self.reset();
}
fn type_id(&self) -> &'static str {
"pitch_shifter"
}
}
const MAX_VOCODER_BANDS: usize = 16;
const VOCODER_FREQ_MIN: f64 = 100.0;
const VOCODER_FREQ_MAX: f64 = 8000.0;
const VOCODER_MAX_SVF_COEF: f64 = 0.95;
pub struct Vocoder {
analysis_state: [[f64; 2]; MAX_VOCODER_BANDS],
synthesis_state: [[f64; 2]; MAX_VOCODER_BANDS],
envelopes: [f64; MAX_VOCODER_BANDS],
band_freqs: [f64; MAX_VOCODER_BANDS],
sample_rate: f64,
spec: PortSpec,
}
impl Vocoder {
pub fn new(sample_rate: f64) -> Self {
let mut vocoder = Self {
analysis_state: [[0.0; 2]; MAX_VOCODER_BANDS],
synthesis_state: [[0.0; 2]; MAX_VOCODER_BANDS],
envelopes: [0.0; MAX_VOCODER_BANDS],
band_freqs: [0.0; MAX_VOCODER_BANDS],
sample_rate,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "carrier", SignalKind::Audio),
PortDef::new(1, "modulator", SignalKind::Audio),
PortDef::new(2, "bands", SignalKind::CvUnipolar).with_default(1.0),
PortDef::new(3, "attack", SignalKind::CvUnipolar).with_default(0.3),
PortDef::new(4, "release", SignalKind::CvUnipolar).with_default(0.3),
],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
};
vocoder.compute_band_freqs();
vocoder
}
fn compute_band_freqs(&mut self) {
let coef_limit_freq = Libm::<f64>::asin(VOCODER_MAX_SVF_COEF / 2.0) * self.sample_rate
/ core::f64::consts::PI;
let freq_max = VOCODER_FREQ_MAX
.min(coef_limit_freq)
.max(VOCODER_FREQ_MIN * 2.0);
let log_min = Libm::<f64>::log2(VOCODER_FREQ_MIN);
let log_max = Libm::<f64>::log2(freq_max);
for i in 0..MAX_VOCODER_BANDS {
let t = i as f64 / (MAX_VOCODER_BANDS - 1) as f64;
let log_freq = log_min + t * (log_max - log_min);
self.band_freqs[i] = Libm::<f64>::exp2(log_freq);
}
}
#[inline]
fn process_svf_bandpass(
state: &mut [f64; 2],
input: f64,
freq: f64,
q: f64,
sample_rate: f64,
) -> f64 {
let f = 2.0 * Libm::<f64>::sin(core::f64::consts::PI * freq / sample_rate);
let f = f.min(0.99);
let q_inv = 1.0 / q;
let low = state[0];
let high = input - low - q_inv * state[1];
let band = f * high + state[1];
let new_low = f * band + low;
state[0] = new_low;
state[1] = band;
band
}
}
impl Default for Vocoder {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for Vocoder {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let carrier = sanitize_audio(inputs.get_or(0, 0.0));
let modulator = sanitize_audio(inputs.get_or(1, 0.0));
let bands_cv = inputs.get_or(2, 1.0).clamp(0.0, 1.0);
let attack_cv = inputs.get_or(3, 0.3).clamp(0.0, 1.0);
let release_cv = inputs.get_or(4, 0.3).clamp(0.0, 1.0);
let num_bands = Libm::<f64>::round(4.0 + bands_cv * 12.0) as usize;
let num_bands = num_bands.min(MAX_VOCODER_BANDS);
let attack_time = 0.01 + attack_cv * 0.19;
let release_time = 0.01 + release_cv * 0.19;
let attack_coef = env_coef(attack_time, self.sample_rate);
let release_coef = env_coef(release_time, self.sample_rate);
let q = 2.0;
let mut output = 0.0;
for i in 0..num_bands {
let freq = self.band_freqs[i * MAX_VOCODER_BANDS / num_bands];
let analysis_band = Self::process_svf_bandpass(
&mut self.analysis_state[i],
modulator,
freq,
q,
self.sample_rate,
);
let rectified = analysis_band.abs();
if rectified > self.envelopes[i] {
self.envelopes[i] =
attack_coef * self.envelopes[i] + (1.0 - attack_coef) * rectified;
} else {
self.envelopes[i] =
release_coef * self.envelopes[i] + (1.0 - release_coef) * rectified;
}
let synthesis_band = Self::process_svf_bandpass(
&mut self.synthesis_state[i],
carrier,
freq,
q,
self.sample_rate,
);
output += synthesis_band * self.envelopes[i];
}
output /= num_bands as f64;
outputs.set(10, output * 4.0);
}
fn reset(&mut self) {
self.analysis_state = [[0.0; 2]; MAX_VOCODER_BANDS];
self.synthesis_state = [[0.0; 2]; MAX_VOCODER_BANDS];
self.envelopes = [0.0; MAX_VOCODER_BANDS];
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
self.compute_band_freqs();
self.reset();
}
fn type_id(&self) -> &'static str {
"vocoder"
}
}
const MAX_GRAINS: usize = 16;
const GRANULAR_BUFFER_SIZE: usize = 96000;
#[derive(Clone, Copy)]
struct Grain {
active: bool,
start_pos: usize,
phase: f64,
size: usize,
speed: f64,
}
impl Default for Grain {
fn default() -> Self {
Self {
active: false,
start_pos: 0,
phase: 0.0,
size: 4410, speed: 1.0,
}
}
}
pub struct Granular {
buffer: Vec<f64>,
write_pos: usize,
grains: [Grain; MAX_GRAINS],
spawn_timer: usize,
rng: crate::rng::Rng,
norm_smooth: f64,
sample_rate: f64,
spec: PortSpec,
}
impl Granular {
pub fn new(sample_rate: f64) -> Self {
Self {
buffer: vec![0.0; GRANULAR_BUFFER_SIZE],
write_pos: 0,
grains: [Grain::default(); MAX_GRAINS],
spawn_timer: 0,
rng: crate::rng::Rng::from_seed(42),
norm_smooth: 1.0,
sample_rate,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "position", SignalKind::CvUnipolar).with_default(0.5),
PortDef::new(2, "size", SignalKind::CvUnipolar).with_default(0.3),
PortDef::new(3, "density", SignalKind::CvUnipolar).with_default(0.5),
PortDef::new(4, "pitch", SignalKind::CvBipolar).with_default(0.0),
PortDef::new(5, "spray", SignalKind::CvUnipolar).with_default(0.1),
PortDef::new(6, "freeze", SignalKind::Gate).with_default(0.0),
],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
}
}
#[inline]
fn hann_window(phase: f64) -> f64 {
0.5 * (1.0 - Libm::<f64>::cos(2.0 * core::f64::consts::PI * phase))
}
#[inline]
pub fn read_buffer(&self, pos: f64) -> f64 {
let pos = pos % GRANULAR_BUFFER_SIZE as f64;
let index = pos as usize;
let frac = pos - index as f64;
let s0 = self.buffer[index % GRANULAR_BUFFER_SIZE];
let s1 = self.buffer[(index + 1) % GRANULAR_BUFFER_SIZE];
s0 + frac * (s1 - s0)
}
fn spawn_grain(&mut self, position: f64, size: usize, speed: f64, spray: f64) {
for grain in &mut self.grains {
if !grain.active {
let spray_offset = if spray > 0.0 {
(self.rng.next_f64() - 0.5) * spray * GRANULAR_BUFFER_SIZE as f64 * 0.5
} else {
0.0
};
let base_pos = position * GRANULAR_BUFFER_SIZE as f64;
let pos = (base_pos + spray_offset) as usize % GRANULAR_BUFFER_SIZE;
grain.active = true;
grain.start_pos = pos;
grain.phase = 0.0;
grain.size = size.max(100); grain.speed = speed;
break;
}
}
}
}
impl Default for Granular {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for Granular {
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 position = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
let size_cv = inputs.get_or(2, 0.3).clamp(0.0, 1.0);
let density_cv = inputs.get_or(3, 0.5).clamp(0.0, 1.0);
let pitch_cv = inputs.get_or(4, 0.0).clamp(-5.0, 5.0);
let spray = inputs.get_or(5, 0.1).clamp(0.0, 1.0);
let freeze = inputs.get_or(6, 0.0);
let grains_per_sec = 1.0 + density_cv * 19.0;
let spawn_interval = (self.sample_rate / grains_per_sec) as usize;
let semitones = (pitch_cv * 4.8).clamp(-24.0, 24.0);
let speed = Libm::<f64>::exp2(semitones / 12.0);
let max_size = (GRANULAR_BUFFER_SIZE as f64 / speed) as usize;
let size_samples = (((0.01 + size_cv * 0.49) * self.sample_rate) as usize).min(max_size);
if freeze <= GATE_THRESHOLD_V {
self.buffer[self.write_pos] = input;
self.write_pos = (self.write_pos + 1) % GRANULAR_BUFFER_SIZE;
}
if self.spawn_timer == 0 {
self.spawn_grain(position, size_samples, speed, spray);
let jitter = 1.0 + (self.rng.next_f64() - 0.5) * 0.4;
self.spawn_timer = ((spawn_interval as f64) * jitter) as usize;
} else {
self.spawn_timer -= 1;
}
let mut output = 0.0;
for i in 0..MAX_GRAINS {
if self.grains[i].active {
let grain = &self.grains[i];
let read_offset = grain.phase * grain.size as f64 * grain.speed;
let read_pos = grain.start_pos as f64 + read_offset;
let envelope = Self::hann_window(grain.phase);
let pos = read_pos % GRANULAR_BUFFER_SIZE as f64;
let index = pos as usize;
let frac = pos - index as f64;
let s0 = self.buffer[index % GRANULAR_BUFFER_SIZE];
let s1 = self.buffer[(index + 1) % GRANULAR_BUFFER_SIZE];
let sample = s0 + frac * (s1 - s0);
output += sample * envelope;
let new_phase = self.grains[i].phase + 1.0 / self.grains[i].size as f64;
self.grains[i].phase = new_phase;
if new_phase >= 1.0 {
self.grains[i].active = false;
}
}
}
let grain_seconds = size_samples as f64 / self.sample_rate;
let expected_overlap = grains_per_sec * grain_seconds;
let target_norm = Libm::<f64>::sqrt(expected_overlap).max(1.0);
let smooth = env_coef(0.05, self.sample_rate); self.norm_smooth = smooth * self.norm_smooth + (1.0 - smooth) * target_norm;
output /= self.norm_smooth.max(1.0);
outputs.set(10, output);
}
fn reset(&mut self) {
self.buffer.iter_mut().for_each(|x| *x = 0.0);
self.write_pos = 0;
self.grains = [Grain::default(); MAX_GRAINS];
self.spawn_timer = 0;
self.rng = crate::rng::Rng::from_seed(42);
self.norm_smooth = 1.0;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
self.reset();
}
fn type_id(&self) -> &'static str {
"granular"
}
}
pub struct Wavefolder {
pub(crate) threshold: f64,
oversampler: Oversampler,
spec: PortSpec,
}
impl Wavefolder {
pub fn new(threshold: f64) -> Self {
Self {
threshold: threshold.max(0.1),
oversampler: Oversampler::new(Oversample::Off),
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "threshold", SignalKind::CvUnipolar)
.with_default(threshold)
.with_attenuverter(),
],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
}
}
pub fn set_oversample(&mut self, mode: Oversample) {
self.oversampler = Oversampler::new(mode);
}
pub fn oversample_factor(&self) -> usize {
self.oversampler.factor()
}
}
impl Default for Wavefolder {
fn default() -> Self {
Self::new(1.0)
}
}
impl GraphModule for Wavefolder {
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 threshold = inputs.get_or(1, self.threshold).max(0.1);
let folded = self
.oversampler
.process(input, |x| saturation::fold(x / 5.0, threshold) * 5.0);
outputs.set(10, folded);
}
fn reset(&mut self) {
self.oversampler.reset();
}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"wavefolder"
}
crate::impl_introspect!();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bitcrusher() {
let mut bc = Bitcrusher::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 2.5);
inputs.set(1, 0.3); inputs.set(2, 0.5); bc.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
assert!(out.is_finite());
}
#[test]
fn test_bitcrusher_default() {
let bc = Bitcrusher::default();
assert_eq!(bc.type_id(), "bitcrusher");
}
#[test]
fn test_ring_modulator() {
let mut rm = RingModulator::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 5.0); inputs.set(1, 5.0); rm.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 5.0).abs() < 0.1);
inputs.set(0, 5.0);
inputs.set(1, -5.0);
rm.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - (-5.0)).abs() < 0.1);
inputs.set(0, 5.0);
inputs.set(1, 0.0);
rm.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap()).abs() < 0.01);
}
#[test]
fn test_ring_modulator_default_reset_sample_rate() {
let mut rm = RingModulator::default();
rm.reset();
rm.set_sample_rate(48000.0);
assert_eq!(rm.type_id(), "ring_mod");
}
#[test]
fn test_pitch_shifter_default_reset_sample_rate() {
let mut ps = PitchShifter::default();
assert_eq!(ps.sample_rate, 44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 2.5); for _ in 0..100 {
ps.tick(&inputs, &mut outputs);
}
assert!(ps.write_pos > 0);
ps.reset();
assert_eq!(ps.write_pos, 0);
assert_eq!(ps.grain_phase, [0.0, 0.5]);
ps.set_sample_rate(48000.0);
assert_eq!(ps.sample_rate, 48000.0);
assert_eq!(ps.type_id(), "pitch_shifter");
assert_eq!(ps.port_spec().inputs.len(), 4);
assert_eq!(ps.port_spec().outputs.len(), 1);
}
#[test]
fn test_pitch_shifter_hann_window() {
let start = PitchShifter::hann_window(0.0);
let peak = PitchShifter::hann_window(0.5);
let end = PitchShifter::hann_window(1.0);
assert!(start.abs() < 0.01, "Window should start at 0: {}", start);
assert!(
(peak - 1.0).abs() < 0.01,
"Window should peak at 1: {}",
peak
);
assert!(end.abs() < 0.01, "Window should end at 0: {}", end);
}
#[test]
fn test_pitch_shifter_passthrough() {
let mut ps = PitchShifter::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.0); inputs.set(3, 1.0);
let mut sum_out = 0.0;
for i in 0..1000 {
let input = Libm::<f64>::sin(i as f64 * 0.1) * 5.0;
inputs.set(0, input);
ps.tick(&inputs, &mut outputs);
sum_out += outputs.get(10).unwrap().abs();
}
assert!(sum_out > 100.0, "Should have output signal: {}", sum_out);
}
#[test]
fn test_pitch_shifter_dry_wet_mix() {
let mut ps = PitchShifter::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.0);
inputs.set(3, 0.0);
let input_val = 2.5; inputs.set(0, input_val);
ps.tick(&inputs, &mut outputs);
let dry_out = outputs.get(10).unwrap();
assert!(
(dry_out - input_val).abs() < 0.1,
"Dry output should match input: {} vs {}",
dry_out,
input_val
);
}
#[test]
fn test_pitch_shifter_shift_changes_output() {
let mut ps = PitchShifter::new(44100.0);
let collect_output = |ps: &mut PitchShifter, shift_cv: f64| -> f64 {
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, shift_cv);
inputs.set(3, 1.0);
ps.reset();
let mut sum = 0.0;
for i in 0..2000 {
let input = Libm::<f64>::sin(i as f64 * 0.05) * 5.0;
inputs.set(0, input);
ps.tick(&inputs, &mut outputs);
sum += outputs.get(10).unwrap();
}
sum
};
let sum_no_shift = collect_output(&mut ps, 0.0);
let sum_up_octave = collect_output(&mut ps, 2.5); let sum_down_octave = collect_output(&mut ps, -2.5);
assert!(
(sum_no_shift - sum_up_octave).abs() > 1.0,
"Up shift should differ"
);
assert!(
(sum_no_shift - sum_down_octave).abs() > 1.0,
"Down shift should differ"
);
}
#[test]
fn test_pitch_shifter_buffer_wraparound() {
let mut ps = PitchShifter::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 2.5);
inputs.set(1, 0.0);
inputs.set(3, 1.0);
for _ in 0..10000 {
ps.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
assert!(out.is_finite(), "Output should be finite");
}
assert!(ps.write_pos < PitchShifter::BUFFER_SIZE);
}
#[test]
fn test_vocoder_default_reset_sample_rate() {
let mut vocoder = Vocoder::default();
assert_eq!(vocoder.sample_rate, 44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.5); inputs.set(1, 0.5); vocoder.tick(&inputs, &mut outputs);
vocoder.reset();
assert_eq!(vocoder.envelopes, [0.0; MAX_VOCODER_BANDS]);
vocoder.set_sample_rate(48000.0);
assert_eq!(vocoder.sample_rate, 48000.0);
assert_eq!(vocoder.type_id(), "vocoder");
assert_eq!(vocoder.port_spec().inputs.len(), 5);
assert_eq!(vocoder.port_spec().outputs.len(), 1);
}
#[test]
fn test_vocoder_band_frequencies() {
let vocoder = Vocoder::new(44100.0);
assert!(vocoder.band_freqs[0] >= VOCODER_FREQ_MIN - 1.0);
assert!(vocoder.band_freqs[MAX_VOCODER_BANDS - 1] <= VOCODER_FREQ_MAX + 1.0);
for i in 1..MAX_VOCODER_BANDS {
assert!(
vocoder.band_freqs[i] > vocoder.band_freqs[i - 1],
"Band frequencies should be ascending"
);
}
}
#[test]
fn test_vocoder_silent_when_no_modulator() {
let mut vocoder = Vocoder::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.8);
inputs.set(1, 0.0);
for _ in 0..1000 {
vocoder.tick(&inputs, &mut outputs);
}
let out = outputs.get(10).unwrap();
assert!(
out.abs() < 0.1,
"Output should be near zero without modulator, got {}",
out
);
}
#[test]
fn test_vocoder_output_when_both_active() {
let mut vocoder = Vocoder::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
let mut total_output = 0.0;
for i in 0..2000 {
let phase = i as f64 * 0.05;
inputs.set(0, Libm::<f64>::sin(phase)); inputs.set(1, Libm::<f64>::sin(phase * 0.1)); vocoder.tick(&inputs, &mut outputs);
total_output += outputs.get(10).unwrap().abs();
}
assert!(
total_output > 1.0,
"Should produce output when both signals active, got {}",
total_output
);
}
#[test]
fn test_vocoder_band_count() {
let mut vocoder_few = Vocoder::new(44100.0);
let mut vocoder_many = Vocoder::new(44100.0);
let mut inputs_few = PortValues::new();
let mut inputs_many = PortValues::new();
let mut outputs_few = PortValues::new();
let mut outputs_many = PortValues::new();
inputs_few.set(2, 0.0); inputs_many.set(2, 1.0);
let mut total_few = 0.0;
let mut total_many = 0.0;
for i in 0..1000 {
let phase = i as f64 * 0.05;
let carrier = Libm::<f64>::sin(phase);
let modulator = Libm::<f64>::sin(phase * 0.2);
inputs_few.set(0, carrier);
inputs_few.set(1, modulator);
inputs_many.set(0, carrier);
inputs_many.set(1, modulator);
vocoder_few.tick(&inputs_few, &mut outputs_few);
vocoder_many.tick(&inputs_many, &mut outputs_many);
total_few += outputs_few.get(10).unwrap().abs();
total_many += outputs_many.get(10).unwrap().abs();
}
assert!(total_few > 0.5, "Few bands should produce output");
assert!(total_many > 0.5, "Many bands should produce output");
}
#[test]
fn test_vocoder_envelope_attack_release() {
let mut vocoder = Vocoder::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(4, 0.0);
for _ in 0..100 {
vocoder.tick(&inputs, &mut outputs);
}
let fast_envelope = vocoder.envelopes[0];
vocoder.reset();
inputs.set(3, 1.0);
for _ in 0..100 {
vocoder.tick(&inputs, &mut outputs);
}
let slow_envelope = vocoder.envelopes[0];
assert!(
fast_envelope > slow_envelope,
"Fast attack should build envelope faster"
);
}
#[test]
fn test_granular_default_reset_sample_rate() {
let mut granular = Granular::default();
assert_eq!(granular.sample_rate, 44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.5);
granular.tick(&inputs, &mut outputs);
assert_eq!(granular.write_pos, 1);
granular.reset();
assert_eq!(granular.write_pos, 0);
assert!(granular.grains.iter().all(|g| !g.active));
granular.set_sample_rate(48000.0);
assert_eq!(granular.sample_rate, 48000.0);
assert_eq!(granular.type_id(), "granular");
assert_eq!(granular.port_spec().inputs.len(), 7);
assert_eq!(granular.port_spec().outputs.len(), 1);
}
#[test]
fn test_granular_hann_window() {
assert!(Granular::hann_window(0.0).abs() < 0.001);
assert!((Granular::hann_window(0.5) - 1.0).abs() < 0.001);
assert!(Granular::hann_window(1.0).abs() < 0.001);
}
#[test]
fn test_granular_records_to_buffer() {
let mut granular = Granular::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
for i in 0..100 {
inputs.set(0, i as f64 * 0.01);
granular.tick(&inputs, &mut outputs);
}
assert!((granular.buffer[50] - 0.5).abs() < 0.01);
}
#[test]
fn test_granular_freeze_stops_recording() {
let mut granular = Granular::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0);
for _ in 0..100 {
granular.tick(&inputs, &mut outputs);
}
let pos_before = granular.write_pos;
inputs.set(6, 5.0);
for _ in 0..100 {
granular.tick(&inputs, &mut outputs);
}
assert_eq!(granular.write_pos, pos_before);
}
#[test]
fn test_granular_produces_output() {
let mut granular = Granular::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.05);
for i in 0..10000 {
let phase = i as f64 * 0.01;
inputs.set(0, Libm::<f64>::sin(phase));
granular.tick(&inputs, &mut outputs);
}
let mut total_output = 0.0;
for _ in 0..5000 {
inputs.set(0, 0.0);
granular.tick(&inputs, &mut outputs);
total_output += outputs.get(10).unwrap().abs();
}
assert!(
total_output > 1.0,
"Granular should produce output, got {}",
total_output
);
}
#[test]
fn test_granular_density_affects_grain_count() {
let mut granular_low = Granular::new(44100.0);
let mut granular_high = Granular::new(44100.0);
let mut inputs_low = PortValues::new();
let mut inputs_high = PortValues::new();
let mut outputs = PortValues::new();
inputs_low.set(3, 0.0); inputs_high.set(3, 1.0);
for i in 0..5000 {
let sample = Libm::<f64>::sin(i as f64 * 0.05);
inputs_low.set(0, sample);
inputs_high.set(0, sample);
granular_low.tick(&inputs_low, &mut outputs);
granular_high.tick(&inputs_high, &mut outputs);
}
let active_low = granular_low.grains.iter().filter(|g| g.active).count();
let active_high = granular_high.grains.iter().filter(|g| g.active).count();
assert!(
active_high >= active_low || (active_low == 0 && active_high == 0),
"Higher density should produce more concurrent grains"
);
}
#[test]
fn test_granular_buffer_interpolation() {
let granular = Granular::new(44100.0);
let mut granular = granular;
granular.buffer[0] = 0.0;
granular.buffer[1] = 1.0;
let val = granular.read_buffer(0.5);
assert!(
(val - 0.5).abs() < 0.01,
"Interpolation should give 0.5, got {}",
val
);
}
#[test]
fn test_grain_default() {
let grain = Grain::default();
assert!(!grain.active);
assert_eq!(grain.phase, 0.0);
assert_eq!(grain.speed, 1.0);
}
#[test]
fn test_distortion_tone_is_real_filter() {
let sr = 44100.0;
let rms = |freq: f64, tone: f64| -> f64 {
let mut d = Distortion::new(sr);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.0); inputs.set(2, tone); inputs.set(3, 0.0); inputs.set(4, 1.0); let n = 8000usize;
let mut sumsq = 0.0;
for i in 0..n {
let x = Libm::<f64>::sin(2.0 * core::f64::consts::PI * freq * i as f64 / sr);
inputs.set(0, x); d.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
if i >= n / 2 {
sumsq += out * out;
}
}
Libm::<f64>::sqrt(sumsq / (n / 2) as f64)
};
let input_rms = 1.0 / Libm::<f64>::sqrt(2.0);
let high_at_min = rms(5000.0, 0.0);
let low_at_min = rms(200.0, 0.0);
assert!(
high_at_min < 0.5 * low_at_min,
"tone min should attenuate highs more than lows: high={high_at_min} low={low_at_min}"
);
let high_at_max = rms(5000.0, 1.0);
assert!(
high_at_max > 0.8 * input_rms,
"tone max should be ~transparent: out_rms={high_at_max} in_rms={input_rms}"
);
assert!(
high_at_max > 3.0 * high_at_min,
"tone max should pass highs that tone min blocks: max={high_at_max} min={high_at_min}"
);
}
#[test]
fn test_distortion_all_algorithms_bounded() {
for drive in [0.0, 0.5, 1.0] {
let mut x = -12.0;
while x <= 12.0 {
for out in [
Distortion::soft_clip(x, drive),
Distortion::hard_clip(x, drive),
Distortion::foldback(x, drive),
Distortion::asymmetric(x, drive),
] {
assert!(
out.is_finite() && out.abs() <= 5.05,
"shaper out {out} exceeds ±5.05 at x={x} drive={drive}"
);
}
x += 0.05;
}
}
for mode_cv in [0.0f64, 0.34, 0.67, 1.0] {
for &v in &[5.0f64, -5.0] {
let mut d = Distortion::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 1.0); inputs.set(2, 1.0); inputs.set(3, mode_cv);
inputs.set(4, 1.0); inputs.set(0, v);
let mut out = 0.0;
for _ in 0..500 {
d.tick(&inputs, &mut outputs);
out = outputs.get(10).unwrap();
}
assert!(
out.abs() <= 5.05,
"mode {mode_cv} at {v}V max drive should stay ≤5.05V, got {out}"
);
}
}
}
#[test]
fn test_distortion_unity_at_low_drive() {
assert!((Distortion::hard_clip(0.5, 0.0) - 0.5).abs() < 1e-9);
let out = Distortion::soft_clip(1.0, 0.0);
assert!((out - 1.0).abs() < 0.05, "soft_clip near unity, got {out}");
}
#[test]
fn test_triangle_fold_matches_reference_loop() {
fn reference(gained: f64, threshold: f64) -> f64 {
let mut folded = gained;
while folded > threshold || folded < -threshold {
if folded > threshold {
folded = 2.0 * threshold - folded;
} else if folded < -threshold {
folded = -2.0 * threshold - folded;
}
}
folded
}
let threshold = 1.0;
let mut x = -1000.0;
while x <= 1000.0 {
let a = Distortion::triangle_fold(x, threshold);
let b = reference(x, threshold);
assert!((a - b).abs() < 1e-6, "fold mismatch at {x}: {a} vs {b}");
x += 0.05;
}
for &x in &[1000.0, -1000.0, 5.0, -5.0, 3.0, -3.0, 1.0, -1.0, 0.0] {
let a = Distortion::triangle_fold(x, threshold);
let b = reference(x, threshold);
assert!(
(a - b).abs() < 1e-6,
"fold mismatch at extreme {x}: {a} vs {b}"
);
}
}
#[test]
fn test_vocoder_band_coefficients_strictly_increasing() {
for &sr in &[44100.0, 22050.0, 32000.0] {
let v = Vocoder::new(sr);
let mut prev = -1.0;
for i in 0..MAX_VOCODER_BANDS {
let coef = (2.0 * Libm::<f64>::sin(core::f64::consts::PI * v.band_freqs[i] / sr))
.min(0.99);
assert!(
coef > prev + 1e-9,
"band {i} coef {coef} not strictly greater than {prev} at sr {sr}"
);
prev = coef;
}
}
}
#[test]
fn test_granular_no_amplitude_zipper() {
let mut g = Granular::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.3); inputs.set(3, 1.0); inputs.set(5, 0.0);
for _ in 0..(GRANULAR_BUFFER_SIZE + 20000) {
g.tick(&inputs, &mut outputs);
}
let mut prev = outputs.get(10).unwrap();
let mut max_delta = 0.0f64;
for _ in 0..30000 {
g.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
max_delta = max_delta.max((out - prev).abs());
prev = out;
}
assert!(
max_delta < 0.05,
"granular output should have no zipper jumps, max delta {max_delta}"
);
}
#[test]
fn test_bitcrusher_fractional_downsample_period() {
let mut bc = Bitcrusher::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(2, 0.5 / 63.0);
inputs.set(1, 1.0);
let n = 3000usize;
let mut transitions = 0usize;
let mut prev = f64::NAN;
for i in 0..n {
inputs.set(0, i as f64 * 0.001); bc.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
if i > 0 && (out - prev).abs() > 1e-9 {
transitions += 1;
}
prev = out;
}
let avg_period = n as f64 / transitions as f64;
assert!(
(avg_period - 1.5).abs() < 0.1,
"fractional downsample average period should be ~1.5, got {avg_period}"
);
}
#[test]
fn test_bitcrusher_no_dc_bias() {
let mut bc = Bitcrusher::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 2.0 / 15.0); inputs.set(2, 0.0); let n = 20000usize;
let mut sum = 0.0;
for i in 0..n {
let v = Libm::<f64>::sin(i as f64 * 0.01) * 4.0; inputs.set(0, v);
bc.tick(&inputs, &mut outputs);
sum += outputs.get(10).unwrap();
}
let mean = sum / n as f64;
assert!(
mean.abs() < 0.1,
"quantizer DC bias should be ~0, got {mean}"
);
}
#[test]
fn test_bitcrusher_full_scale_maps_in_range() {
let mut bc = Bitcrusher::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.3);
inputs.set(2, 0.0); for &(v, expected) in &[(5.0, 5.0), (-5.0, -5.0)] {
inputs.set(0, v);
bc.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
assert!(
out.abs() <= 5.0 + 1e-9 && (out - expected).abs() < 1e-9,
"full-scale {v}V should map to {expected}V in range, got {out}"
);
}
}
#[test]
fn test_granular_pitch_clamped_and_bounded() {
let mut g = Granular::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.1); inputs.set(2, 1.0); inputs.set(4, 5.0); inputs.set(5, 0.0); inputs.set(0, 1.0);
g.tick(&inputs, &mut outputs);
let grain = g
.grains
.iter()
.find(|gr| gr.active)
.expect("a grain should be active after the first tick");
assert!(
(grain.speed - 4.0).abs() < 1e-6,
"pitch +5V should be +24 st (speed 4), got speed {}",
grain.speed
);
assert!(
grain.size as f64 * grain.speed <= GRANULAR_BUFFER_SIZE as f64,
"grain read span {} must not exceed buffer {}",
grain.size as f64 * grain.speed,
GRANULAR_BUFFER_SIZE
);
for &pitch in &[5.0f64, -5.0] {
let mut g = Granular::new(44100.0);
inputs.set(4, pitch);
let mut total = 0.0;
let mut max_abs = 0.0f64;
for i in 0..20000 {
inputs.set(0, Libm::<f64>::sin(i as f64 * 0.05) * 5.0);
g.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
assert!(out.is_finite(), "granular output must be finite");
max_abs = max_abs.max(out.abs());
total += out.abs();
}
assert!(max_abs < 50.0, "output should stay bounded, got {max_abs}");
assert!(total > 1.0, "output should be non-silent, got {total}");
}
}
#[test]
fn test_pitch_shifter_max_pitch_up_bounded() {
let mut ps = PitchShifter::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 5.0); inputs.set(3, 1.0);
let mut total = 0.0;
let mut max_abs = 0.0f64;
for i in 0..10000 {
inputs.set(0, Libm::<f64>::sin(i as f64 * 0.1) * 5.0);
ps.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
assert!(out.is_finite(), "pitch-up output must be finite");
max_abs = max_abs.max(out.abs());
total += out.abs();
}
assert!(
max_abs <= 5.5,
"wet output should stay near ±5V (COLA), got {max_abs}"
);
assert!(
total > 10.0,
"pitch-up output should be non-silent, got {total}"
);
}
fn dft_mag(sig: &[f64], k: usize) -> f64 {
let n = sig.len();
let mut re = 0.0;
let mut im = 0.0;
for (i, &s) in sig.iter().enumerate() {
let ang = -core::f64::consts::TAU * (k as f64) * (i as f64) / (n as f64);
re += s * Libm::<f64>::cos(ang);
im += s * Libm::<f64>::sin(ang);
}
Libm::<f64>::sqrt(re * re + im * im) / (n as f64)
}
fn alias_energy(sig: &[f64], fund: usize) -> f64 {
let n = sig.len();
let mut total = 0.0;
for k in 1..(n / 2) {
if k % fund != 0 {
total += dft_mag(sig, k);
}
}
total
}
fn distortion_hardclip_capture(mode: Oversample, n: usize) -> Vec<f64> {
let sr = 44100.0;
let mut d = Distortion::new(sr);
d.set_oversample(mode);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 1.0); inputs.set(2, 1.0); inputs.set(3, 0.4); inputs.set(4, 1.0);
let freq = 4200.0;
let mut out = Vec::with_capacity(n);
for i in 0..(n * 3) {
let t = i as f64 / sr;
let x = Libm::<f64>::sin(core::f64::consts::TAU * freq * t) * 5.0;
inputs.set(0, x);
d.tick(&inputs, &mut outputs);
if i >= n * 2 {
out.push(outputs.get(10).unwrap());
}
}
out
}
#[test]
fn test_distortion_oversampling_reduces_aliasing() {
let n = 441;
let fund = 42;
let off = distortion_hardclip_capture(Oversample::Off, n);
let x4 = distortion_hardclip_capture(Oversample::X4, n);
let a_off = alias_energy(&off, fund);
let a_x4 = alias_energy(&x4, fund);
assert!(
a_x4 < 0.7 * a_off,
"4x oversampling should materially reduce alias energy: off={a_off} x4={a_x4}"
);
}
#[test]
fn test_distortion_oversample_off_is_default_and_transparent() {
let sr = 44100.0;
let mut a = Distortion::new(sr);
let mut b = Distortion::new(sr);
b.set_oversample(Oversample::Off);
let mut ia = PortValues::new();
let mut oa = PortValues::new();
let mut ib = PortValues::new();
let mut ob = PortValues::new();
for i in 0..500 {
let x = Libm::<f64>::sin(i as f64 * 0.3) * 5.0;
ia.set(0, x);
ib.set(0, x);
a.tick(&ia, &mut oa);
b.tick(&ib, &mut ob);
assert!((oa.get(10).unwrap() - ob.get(10).unwrap()).abs() < 1e-12);
}
}
#[test]
fn test_wavefolder_oversampling_reduces_aliasing() {
let sr = 44100.0;
let n = 441;
let fund = 42;
let freq = 4200.0;
let capture = |mode: Oversample| -> Vec<f64> {
let mut wf = Wavefolder::new(0.3);
wf.set_oversample(mode);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
let mut out = Vec::with_capacity(n);
for i in 0..(n * 3) {
let t = i as f64 / sr;
inputs.set(0, Libm::<f64>::sin(core::f64::consts::TAU * freq * t) * 5.0);
wf.tick(&inputs, &mut outputs);
if i >= n * 2 {
out.push(outputs.get(10).unwrap());
}
}
out
};
let a_off = alias_energy(&capture(Oversample::Off), fund);
let a_x4 = alias_energy(&capture(Oversample::X4), fund);
assert!(
a_x4 < 0.7 * a_off,
"wavefolder 4x oversampling should reduce alias energy: off={a_off} x4={a_x4}"
);
}
#[test]
fn test_distortion_reset_and_sample_rate() {
let mut dist = Distortion::default();
assert_eq!(dist.type_id(), "distortion");
assert_eq!(dist.sample_rate, 44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 3.0);
inputs.set(1, 0.8); inputs.set(2, 0.2); for _ in 0..500 {
dist.tick(&inputs, &mut outputs);
}
assert!(dist.tone_lp != 0.0, "tone low-pass should hold state");
dist.reset();
assert_eq!(dist.tone_lp, 0.0);
dist.set_sample_rate(48000.0);
assert_eq!(dist.sample_rate, 48000.0);
for _ in 0..100 {
dist.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap().is_finite());
}
}
}