use super::common::{
db_to_gain, env_coef, flush_denorm, gain_to_db, sanitize_audio, GATE_HIGH_V, GATE_THRESHOLD_V,
};
use crate::port::{
GraphModule, ModulatedParam, ParamRange, PortDef, PortSpec, PortValues, SignalKind,
};
use alloc::vec;
use libm::Libm;
#[derive(Debug, Clone, Copy, PartialEq)]
enum AdsrStage {
Idle,
Attack,
Decay,
Sustain,
Release,
}
pub struct Adsr {
stage: AdsrStage,
level: f64,
sample_rate: f64,
prev_gate: f64,
prev_retrig: f64,
release_start_level: f64,
spec: PortSpec,
}
impl Adsr {
pub fn new(sample_rate: f64) -> Self {
Self {
stage: AdsrStage::Idle,
level: 0.0,
sample_rate,
prev_gate: 0.0,
prev_retrig: 0.0,
release_start_level: 0.0,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "gate", SignalKind::Gate),
PortDef::new(1, "retrig", SignalKind::Trigger),
PortDef::new(2, "attack", SignalKind::CvUnipolar)
.with_default(0.1)
.with_attenuverter(),
PortDef::new(3, "decay", SignalKind::CvUnipolar)
.with_default(0.3)
.with_attenuverter(),
PortDef::new(4, "sustain", SignalKind::CvUnipolar)
.with_default(0.7)
.with_attenuverter(),
PortDef::new(5, "release", SignalKind::CvUnipolar)
.with_default(0.4)
.with_attenuverter(),
PortDef::new(6, "shape", SignalKind::Gate).with_default(0.0),
],
outputs: vec![
PortDef::new(10, "env", SignalKind::CvUnipolar),
PortDef::new(11, "inv", SignalKind::CvUnipolar),
PortDef::new(12, "eoc", SignalKind::Trigger),
],
},
}
}
fn cv_to_time(&self, cv: f64) -> f64 {
0.001 * Libm::<f64>::pow(10000.0, cv.clamp(0.0, 1.0))
}
}
impl Default for Adsr {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for Adsr {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let gate = inputs.get_or(0, 0.0);
let retrig = inputs.get_or(1, 0.0);
let attack_time = self.cv_to_time(inputs.get_or(2, 0.1));
let decay_time = self.cv_to_time(inputs.get_or(3, 0.3));
let sustain_level = inputs.get_or(4, 0.7).clamp(0.0, 1.0);
let release_time = self.cv_to_time(inputs.get_or(5, 0.4));
let exp_mode = inputs.get_or(6, 0.0) > GATE_THRESHOLD_V;
let gate_high = gate > GATE_THRESHOLD_V;
let gate_rising = gate_high && self.prev_gate <= GATE_THRESHOLD_V;
let gate_falling = !gate_high && self.prev_gate > GATE_THRESHOLD_V;
let retrig_rising = retrig > GATE_THRESHOLD_V && self.prev_retrig <= GATE_THRESHOLD_V;
if gate_rising || (retrig_rising && gate_high) {
self.stage = AdsrStage::Attack;
} else if gate_falling && self.stage != AdsrStage::Idle {
self.release_start_level = self.level;
self.stage = AdsrStage::Release;
}
let attack_rate = 1.0 / (attack_time * self.sample_rate);
let decay_rate = (1.0 - sustain_level) / (decay_time * self.sample_rate);
let release_rate = self.release_start_level / (release_time * self.sample_rate);
let attack_coef = env_coef(attack_time, self.sample_rate);
let decay_coef = env_coef(decay_time, self.sample_rate);
let release_coef = env_coef(release_time, self.sample_rate);
const EXP_DONE: f64 = 1e-3;
let mut eoc = 0.0;
match self.stage {
AdsrStage::Idle => {
self.level = 0.0;
}
AdsrStage::Attack => {
if exp_mode {
self.level += (1.0 - self.level) * (1.0 - attack_coef);
if self.level >= 1.0 - EXP_DONE {
self.level = 1.0;
self.stage = AdsrStage::Decay;
}
} else {
self.level += attack_rate;
if self.level >= 1.0 {
self.level = 1.0;
self.stage = AdsrStage::Decay;
}
}
}
AdsrStage::Decay => {
if exp_mode {
self.level += (sustain_level - self.level) * (1.0 - decay_coef);
if self.level - sustain_level <= EXP_DONE {
self.level = sustain_level;
self.stage = AdsrStage::Sustain;
}
} else {
self.level -= decay_rate;
if self.level <= sustain_level {
self.level = sustain_level;
self.stage = AdsrStage::Sustain;
}
}
}
AdsrStage::Sustain => {
self.level = sustain_level;
}
AdsrStage::Release => {
if exp_mode {
self.level += (0.0 - self.level) * (1.0 - release_coef);
if self.level <= EXP_DONE {
self.level = 0.0;
self.stage = AdsrStage::Idle;
eoc = GATE_HIGH_V; }
} else {
self.level -= release_rate;
if self.level <= 0.0 {
self.level = 0.0;
self.stage = AdsrStage::Idle;
eoc = GATE_HIGH_V; }
}
}
}
self.prev_gate = gate;
self.prev_retrig = retrig;
outputs.set(10, self.level * 10.0); outputs.set(11, (1.0 - self.level) * 10.0); outputs.set(12, eoc);
}
fn reset(&mut self) {
self.stage = AdsrStage::Idle;
self.level = 0.0;
self.prev_gate = 0.0;
self.prev_retrig = 0.0;
self.release_start_level = 0.0;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
}
fn type_id(&self) -> &'static str {
"adsr"
}
}
pub struct Vca {
spec: PortSpec,
}
impl Vca {
pub fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "cv", SignalKind::CvUnipolar)
.with_default(10.0)
.with_attenuverter(),
PortDef::new(2, "response", SignalKind::Gate).with_default(0.0),
PortDef::new(3, "gain", SignalKind::CvUnipolar).with_default(1.0),
],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
}
}
}
impl Default for Vca {
fn default() -> Self {
Self::new()
}
}
impl GraphModule for Vca {
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 cv = inputs.get_or(1, 10.0).clamp(0.0, 10.0) / 10.0;
let exp_response = inputs.get_or(2, 0.0) > GATE_THRESHOLD_V;
let gain_scale = inputs.get_or(3, 1.0).clamp(0.0, 2.0);
let base_gain = if exp_response { cv * cv } else { cv };
outputs.set(10, input * base_gain * gain_scale);
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
fn type_id(&self) -> &'static str {
"vca"
}
}
pub struct Limiter {
sample_rate: f64,
envelope: f64,
spec: PortSpec,
}
impl Limiter {
pub fn new(sample_rate: f64) -> Self {
Self {
sample_rate,
envelope: 0.0,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "threshold", SignalKind::CvUnipolar)
.with_default(0.8)
.with_attenuverter(),
PortDef::new(2, "release", SignalKind::CvUnipolar)
.with_default(0.3)
.with_attenuverter(),
PortDef::new(3, "soft", SignalKind::Gate).with_default(5.0),
PortDef::new(4, "sidechain", SignalKind::Audio),
],
outputs: vec![
PortDef::new(10, "out", SignalKind::Audio),
PortDef::new(11, "gr", SignalKind::CvUnipolar),
],
},
}
}
}
impl Default for Limiter {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for Limiter {
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 threshold = inputs.get_or(1, 0.8).clamp(0.01, 1.0) * 5.0;
let release_cv = inputs.get_or(2, 0.3).clamp(0.0, 1.0);
let soft_mode = inputs.get_or(3, 5.0) > GATE_THRESHOLD_V;
let sidechain = sanitize_audio(inputs.get_or(4, input));
let release_ms = 10.0 + release_cv * 990.0;
let release_coef = env_coef(release_ms / 1000.0, self.sample_rate);
let abs_input = Libm::<f64>::fabs(sidechain);
if abs_input > self.envelope {
self.envelope = abs_input;
} else {
self.envelope = release_coef * self.envelope + (1.0 - release_coef) * abs_input;
}
self.envelope = flush_denorm(self.envelope);
let gain = if soft_mode {
let knee_start = 0.5 * threshold;
if self.envelope > knee_start {
let span = threshold - knee_start; let target =
knee_start + span * Libm::<f64>::tanh((self.envelope - knee_start) / span);
target / self.envelope
} else {
1.0
}
} else if self.envelope > threshold {
threshold / self.envelope
} else {
1.0
};
let out = (input * gain).clamp(-threshold, threshold);
outputs.set(10, out);
outputs.set(11, (1.0 - gain) * 10.0);
}
fn reset(&mut self) {
self.envelope = 0.0;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
}
fn type_id(&self) -> &'static str {
"limiter"
}
}
pub struct NoiseGate {
sample_rate: f64,
envelope: f64,
gate_state: f64,
gate_open: bool,
hold_counter: u32,
spec: PortSpec,
}
impl NoiseGate {
const FADE_MS: f64 = 5.0;
const HOLD_MS: f64 = 10.0;
pub fn new(sample_rate: f64) -> Self {
Self {
sample_rate,
envelope: 0.0,
gate_state: 0.0,
gate_open: false,
hold_counter: 0,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "threshold", SignalKind::CvUnipolar)
.with_default(0.1)
.with_attenuverter(),
PortDef::new(2, "attack", SignalKind::CvUnipolar)
.with_default(0.1)
.with_attenuverter(),
PortDef::new(3, "release", SignalKind::CvUnipolar)
.with_default(0.3)
.with_attenuverter(),
PortDef::new(4, "range", SignalKind::CvUnipolar)
.with_default(1.0)
.with_attenuverter(),
PortDef::new(5, "sidechain", SignalKind::Audio),
],
outputs: vec![
PortDef::new(10, "out", SignalKind::Audio),
PortDef::new(11, "gate", SignalKind::Gate),
],
},
}
}
}
impl Default for NoiseGate {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for NoiseGate {
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 threshold = inputs.get_or(1, 0.1).clamp(0.0, 1.0) * 5.0;
let attack_cv = inputs.get_or(2, 0.1).clamp(0.0, 1.0);
let release_cv = inputs.get_or(3, 0.3).clamp(0.0, 1.0);
let range = inputs.get_or(4, 1.0).clamp(0.0, 1.0);
let sidechain = sanitize_audio(inputs.get_or(5, input));
let attack_ms = 0.1 + attack_cv * 49.9;
let release_ms = 10.0 + release_cv * 490.0;
let attack_coef = env_coef(attack_ms / 1000.0, self.sample_rate);
let release_coef = env_coef(release_ms / 1000.0, self.sample_rate);
let abs_input = Libm::<f64>::fabs(sidechain);
if abs_input > self.envelope {
self.envelope = attack_coef * self.envelope + (1.0 - attack_coef) * abs_input;
} else {
self.envelope = release_coef * self.envelope + (1.0 - release_coef) * abs_input;
}
self.envelope = flush_denorm(self.envelope);
let open_threshold = threshold;
let close_threshold = threshold * 0.7;
let hold_samples = (Self::HOLD_MS * self.sample_rate / 1000.0) as u32;
if self.envelope > open_threshold {
self.gate_open = true;
self.hold_counter = hold_samples;
} else if self.hold_counter > 0 {
self.hold_counter -= 1;
} else if self.envelope < close_threshold {
self.gate_open = false;
}
let fade_coef = env_coef(Self::FADE_MS / 1000.0, self.sample_rate);
let target = if self.gate_open { 1.0 } else { 0.0 };
self.gate_state = fade_coef * self.gate_state + (1.0 - fade_coef) * target;
self.gate_state = flush_denorm(self.gate_state);
let gain = (1.0 - range) + range * self.gate_state;
outputs.set(10, input * gain);
outputs.set(
11,
if self.gate_state > 0.5 {
GATE_HIGH_V
} else {
0.0
},
);
}
fn reset(&mut self) {
self.envelope = 0.0;
self.gate_state = 0.0;
self.gate_open = false;
self.hold_counter = 0;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
}
fn type_id(&self) -> &'static str {
"noise_gate"
}
}
pub struct Compressor {
sample_rate: f64,
envelope: f64,
spec: PortSpec,
}
impl Compressor {
pub fn new(sample_rate: f64) -> Self {
Self {
sample_rate,
envelope: 0.0,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "threshold", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
PortDef::new(2, "ratio", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
PortDef::new(3, "attack", SignalKind::CvUnipolar)
.with_default(0.2)
.with_attenuverter(),
PortDef::new(4, "release", SignalKind::CvUnipolar)
.with_default(0.3)
.with_attenuverter(),
PortDef::new(5, "makeup", SignalKind::CvUnipolar)
.with_default(0.0)
.with_attenuverter(),
PortDef::new(6, "sidechain", SignalKind::Audio),
],
outputs: vec![
PortDef::new(10, "out", SignalKind::Audio),
PortDef::new(11, "gr", SignalKind::CvUnipolar),
],
},
}
}
}
impl Default for Compressor {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for Compressor {
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 threshold_cv = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
let ratio_cv = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
let attack_cv = inputs.get_or(3, 0.2).clamp(0.0, 1.0);
let release_cv = inputs.get_or(4, 0.3).clamp(0.0, 1.0);
let makeup_cv = inputs.get_or(5, 0.0).clamp(0.0, 1.0);
let sidechain = sanitize_audio(inputs.get_or(6, input));
let threshold = threshold_cv * 5.0;
let ratio = 1.0 + ratio_cv * 19.0;
let attack_ms = 0.1 + attack_cv * 99.9;
let release_ms = 10.0 + release_cv * 990.0;
let makeup_gain = 1.0 + makeup_cv * 3.0;
let attack_coef = env_coef(attack_ms / 1000.0, self.sample_rate);
let release_coef = env_coef(release_ms / 1000.0, self.sample_rate);
let abs_sidechain = Libm::<f64>::fabs(sidechain);
if abs_sidechain > self.envelope {
self.envelope = attack_coef * self.envelope + (1.0 - attack_coef) * abs_sidechain;
} else {
self.envelope = release_coef * self.envelope + (1.0 - release_coef) * abs_sidechain;
}
self.envelope = flush_denorm(self.envelope);
let gain = if self.envelope > threshold && threshold > 0.0 {
let over_db = gain_to_db(self.envelope / threshold);
let compressed_db = over_db / ratio;
let gain_reduction_db = over_db - compressed_db;
db_to_gain(-gain_reduction_db)
} else {
1.0
};
outputs.set(10, input * gain * makeup_gain);
outputs.set(11, (1.0 - gain) * 10.0);
}
fn reset(&mut self) {
self.envelope = 0.0;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
}
fn type_id(&self) -> &'static str {
"compressor"
}
}
pub struct Ducker {
sample_rate: f64,
envelope: f64,
amount: ModulatedParam,
threshold: ModulatedParam,
spec: PortSpec,
}
impl Ducker {
pub fn new(sample_rate: f64) -> Self {
Self {
sample_rate: if sample_rate > 0.0 {
sample_rate
} else {
44100.0
},
envelope: 0.0,
amount: ModulatedParam::new(ParamRange::Linear { min: 0.0, max: 1.0 }).with_base(1.0),
threshold: ModulatedParam::new(ParamRange::Linear { min: 0.0, max: 5.0 })
.with_base(0.2),
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "key", SignalKind::Audio),
PortDef::new(2, "amount", SignalKind::CvBipolar).with_attenuverter(),
PortDef::new(3, "threshold", SignalKind::CvBipolar).with_attenuverter(),
PortDef::new(4, "attack", SignalKind::CvUnipolar)
.with_default(0.1)
.with_attenuverter(),
PortDef::new(5, "release", SignalKind::CvUnipolar)
.with_default(0.3)
.with_attenuverter(),
],
outputs: vec![
PortDef::new(10, "out", SignalKind::Audio),
PortDef::new(11, "gr", SignalKind::CvUnipolar),
],
},
}
}
pub fn set_amount(&mut self, amount: f64) {
self.amount.base = amount.clamp(0.0, 1.0);
}
pub fn amount(&self) -> f64 {
self.amount.base
}
pub fn set_threshold(&mut self, threshold: f64) {
self.threshold.base = threshold.clamp(0.0, 1.0);
}
pub fn threshold(&self) -> f64 {
self.threshold.base
}
}
impl Default for Ducker {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for Ducker {
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 key = sanitize_audio(inputs.get_or(1, 0.0));
let amount_cv = inputs.get_or(2, 0.0);
let threshold_cv = inputs.get_or(3, 0.0);
let attack_cv = inputs.get_or(4, 0.1).clamp(0.0, 1.0);
let release_cv = inputs.get_or(5, 0.3).clamp(0.0, 1.0);
self.amount.set_cv(amount_cv);
self.threshold.set_cv(threshold_cv);
let amount = self.amount.value().clamp(0.0, 1.0);
let threshold = self.threshold.value().max(0.0);
let attack_ms = 0.1 + attack_cv * 99.9;
let release_ms = 10.0 + release_cv * 990.0;
let attack_coef = env_coef(attack_ms / 1000.0, self.sample_rate);
let release_coef = env_coef(release_ms / 1000.0, self.sample_rate);
let abs_key = Libm::<f64>::fabs(key);
if abs_key > self.envelope {
self.envelope = attack_coef * self.envelope + (1.0 - attack_coef) * abs_key;
} else {
self.envelope = release_coef * self.envelope + (1.0 - release_coef) * abs_key;
}
self.envelope = flush_denorm(self.envelope);
let ratio = if threshold > 1e-9 {
(self.envelope / threshold).clamp(0.0, 1.0)
} else {
if self.envelope > 1e-9 {
1.0
} else {
0.0
}
};
let gr = amount * ratio;
let gain = 1.0 - gr;
outputs.set(10, input * gain);
outputs.set(11, gr * 10.0);
}
fn reset(&mut self) {
self.envelope = 0.0;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
if sample_rate > 0.0 {
self.sample_rate = sample_rate;
}
}
fn type_id(&self) -> &'static str {
"ducker"
}
crate::impl_introspect!();
}
pub struct EnvelopeFollower {
sample_rate: f64,
envelope: f64,
spec: PortSpec,
}
impl EnvelopeFollower {
pub fn new(sample_rate: f64) -> Self {
Self {
sample_rate,
envelope: 0.0,
spec: PortSpec {
inputs: vec![
PortDef::new(0, "in", SignalKind::Audio),
PortDef::new(1, "attack", SignalKind::CvUnipolar)
.with_default(0.2)
.with_attenuverter(),
PortDef::new(2, "release", SignalKind::CvUnipolar)
.with_default(0.3)
.with_attenuverter(),
PortDef::new(3, "gain", SignalKind::CvUnipolar)
.with_default(0.5)
.with_attenuverter(),
],
outputs: vec![
PortDef::new(10, "out", SignalKind::CvUnipolar),
PortDef::new(11, "inv", SignalKind::CvUnipolar),
],
},
}
}
}
impl Default for EnvelopeFollower {
fn default() -> Self {
Self::new(44100.0)
}
}
impl GraphModule for EnvelopeFollower {
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 attack_cv = inputs.get_or(1, 0.2).clamp(0.0, 1.0);
let release_cv = inputs.get_or(2, 0.3).clamp(0.0, 1.0);
let gain = inputs.get_or(3, 0.5).clamp(0.0, 1.0) * 4.0;
let attack_ms = 0.1 + attack_cv * 99.9;
let release_ms = 1.0 + release_cv * 999.0;
let attack_coef = env_coef(attack_ms / 1000.0, self.sample_rate);
let release_coef = env_coef(release_ms / 1000.0, self.sample_rate);
let abs_input = Libm::<f64>::fabs(input);
if abs_input > self.envelope {
self.envelope = attack_coef * self.envelope + (1.0 - attack_coef) * abs_input;
} else {
self.envelope = release_coef * self.envelope + (1.0 - release_coef) * abs_input;
}
self.envelope = flush_denorm(self.envelope);
let out = (self.envelope * gain).clamp(0.0, 10.0);
outputs.set(10, out);
outputs.set(11, 10.0 - out);
}
fn reset(&mut self) {
self.envelope = 0.0;
}
fn set_sample_rate(&mut self, sample_rate: f64) {
self.sample_rate = sample_rate;
}
fn type_id(&self) -> &'static str {
"envelope_follower"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analog::Saturator;
use crate::modules::common::{measure_max_output, SAFE_AUDIO_LIMIT};
#[test]
fn test_adsr_envelope() {
let mut adsr = Adsr::new(1000.0); let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(2, 0.1);
inputs.set(0, 5.0);
for _ in 0..100 {
adsr.tick(&inputs, &mut outputs);
}
let level = outputs.get(10).unwrap();
assert!(level > 0.0);
}
#[test]
fn test_vca() {
let mut vca = Vca::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 5.0); inputs.set(1, 5.0);
vca.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
assert!((out - 2.5).abs() < 0.01);
}
#[test]
fn test_limiter() {
let mut limiter = Limiter::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 10.0); inputs.set(1, 0.5); for _ in 0..100 {
limiter.tick(&inputs, &mut outputs);
}
let out = outputs.get(10).unwrap();
assert!(out.abs() < 10.0);
assert!(out.is_finite());
}
#[test]
fn test_limiter_default() {
let limiter = Limiter::default();
assert_eq!(limiter.type_id(), "limiter");
}
#[test]
fn test_noise_gate() {
let mut gate = NoiseGate::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.01); inputs.set(1, 0.5); for _ in 0..1000 {
gate.tick(&inputs, &mut outputs);
}
let out = outputs.get(10).unwrap();
assert!(out.abs() < 0.1);
let gate_out = outputs.get(11).unwrap();
assert!(gate_out < 2.5);
}
#[test]
fn test_noise_gate_default() {
let gate = NoiseGate::default();
assert_eq!(gate.type_id(), "noise_gate");
}
#[test]
fn test_compressor() {
let mut comp = Compressor::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 5.0);
inputs.set(1, 0.2); inputs.set(2, 0.8); for _ in 0..100 {
comp.tick(&inputs, &mut outputs);
}
let out = outputs.get(10).unwrap();
assert!(out.is_finite());
let gr = outputs.get(11).unwrap();
assert!(gr >= 0.0);
}
#[test]
fn test_compressor_default() {
let comp = Compressor::default();
assert_eq!(comp.type_id(), "compressor");
}
#[test]
fn test_envelope_follower() {
let mut ef = EnvelopeFollower::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 5.0);
for _ in 0..1000 {
ef.tick(&inputs, &mut outputs);
}
let out = outputs.get(10).unwrap();
assert!(out > 0.0);
assert!(out.is_finite());
let inv = outputs.get(11).unwrap();
assert!(inv.is_finite());
}
#[test]
fn test_envelope_follower_default() {
let ef = EnvelopeFollower::default();
assert_eq!(ef.type_id(), "envelope_follower");
}
#[test]
fn test_adsr_default_reset_sample_rate() {
let mut adsr = Adsr::default();
assert!(adsr.sample_rate == 44100.0);
adsr.set_sample_rate(48000.0);
assert!(adsr.sample_rate == 48000.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 5.0); for _ in 0..100 {
adsr.tick(&inputs, &mut outputs);
}
adsr.reset();
assert!(adsr.level == 0.0);
assert!(adsr.stage == AdsrStage::Idle);
assert_eq!(adsr.type_id(), "adsr");
}
#[test]
fn test_vca_default_reset_sample_rate() {
let mut vca = Vca::default();
vca.reset();
vca.set_sample_rate(48000.0);
assert_eq!(vca.type_id(), "vca");
}
#[test]
fn test_adsr_full_cycle() {
let mut adsr = Adsr::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 10.0); inputs.set(2, 10.0); inputs.set(3, 5.0); inputs.set(4, 10.0);
inputs.set(0, 5.0);
for _ in 0..1000 {
adsr.tick(&inputs, &mut outputs);
}
let peak = outputs.get(10).unwrap();
assert!(peak > 0.0);
for _ in 0..1000 {
adsr.tick(&inputs, &mut outputs);
}
inputs.set(0, 0.0);
for _ in 0..1000 {
adsr.tick(&inputs, &mut outputs);
}
let after_release = outputs.get(10).unwrap();
assert!(after_release < 0.1);
}
#[test]
fn test_adsr_output_bounded() {
let mut adsr = Adsr::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(2, 0.0); inputs.set(3, 0.0); inputs.set(4, 1.0); inputs.set(5, 0.0);
inputs.set(0, 5.0);
let max = measure_max_output(10000, || {
adsr.tick(&inputs, &mut outputs);
outputs.get(10).unwrap_or(0.0).abs()
});
assert!(
max <= 10.5, "ADSR output {} exceeds expected 0-10V range",
max
);
}
#[test]
fn test_limiter_prevents_spikes() {
let mut limiter = Limiter::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.3);
inputs.set(0, 10.0);
limiter.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap_or(0.0);
assert!(
out.abs() <= 5.0,
"Limiter failed to limit 10V input, got {}",
out
);
}
#[test]
fn test_saturator_prevents_spikes() {
let mut sat = Saturator::new(0.8); let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 20.0);
sat.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap_or(0.0);
assert!(
out.abs() <= SAFE_AUDIO_LIMIT,
"Saturator failed to limit input, got {}",
out
);
}
#[test]
fn test_limiter_brickwall_never_exceeds_threshold() {
let fs = 44100.0;
for &thr_cv in &[0.2_f64, 0.5, 0.8, 1.0] {
let mut lim = Limiter::new(fs);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, thr_cv); let threshold = thr_cv.clamp(0.01, 1.0) * 5.0;
let mut max_out = 0.0f64;
for i in 0..4000 {
let x = 25.0 * (i as f64 * 0.05).sin();
inputs.set(0, x);
lim.tick(&inputs, &mut outputs);
max_out = max_out.max(outputs.get(10).unwrap().abs());
}
assert!(
max_out <= threshold + 1e-9,
"soft limiter exceeded threshold {}: peak {}",
threshold,
max_out
);
}
}
#[test]
fn test_limiter_passes_gentle_signals() {
let mut lim = Limiter::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.8); for i in 0..1000 {
let x = (i as f64 * 0.05).sin(); inputs.set(0, x);
lim.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
assert!(
(out - x).abs() < 1e-9,
"gentle signal altered: in={} out={}",
x,
out
);
}
}
#[test]
fn test_limiter_soft_knee_c0_continuous() {
let mut lim = Limiter::new(48000.0);
let threshold = 0.8 * 5.0; let step = 0.01_f64;
let steady_output = |lim: &mut Limiter, a: f64| -> f64 {
lim.reset();
let mut inputs = PortValues::new();
inputs.set(0, a); inputs.set(1, 0.8); inputs.set(2, 0.3); inputs.set(3, 5.0); let mut outputs = PortValues::new();
for _ in 0..8 {
outputs = PortValues::new();
lim.tick(&inputs, &mut outputs);
}
outputs.get(10).unwrap()
};
let mut prev: Option<(f64, f64)> = None;
let mut a = 1.0_f64; while a <= 6.0 {
let out = steady_output(&mut lim, a);
assert!(
out <= threshold + 1e-9,
"soft limiter output {out} exceeds threshold {threshold} at a={a}"
);
if let Some((pa, pout)) = prev {
let jump = (out - pout).abs();
assert!(
jump <= (a - pa) + 1e-6,
"soft-knee discontinuity: output stepped {jump} between \
a={pa} and a={a} (amplitude step {})",
a - pa
);
}
prev = Some((a, out));
a += step;
}
}
#[test]
fn test_adsr_decay_release_durations() {
let fs = 1000.0;
let mut adsr = Adsr::new(fs);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(2, 0.0); inputs.set(3, 0.5); inputs.set(4, 0.5); inputs.set(5, 0.5); inputs.set(0, 5.0);
loop {
adsr.tick(&inputs, &mut outputs);
if adsr.stage == AdsrStage::Decay {
break;
}
}
let mut decay_samples = 0u32;
while adsr.stage == AdsrStage::Decay {
adsr.tick(&inputs, &mut outputs);
decay_samples += 1;
}
assert!(
(decay_samples as f64 - 100.0).abs() <= 5.0,
"decay lasted {} samples, expected ~100",
decay_samples
);
inputs.set(0, 0.0);
let mut release_samples = 0u32;
loop {
adsr.tick(&inputs, &mut outputs);
match adsr.stage {
AdsrStage::Release => release_samples += 1,
AdsrStage::Idle => {
release_samples += 1;
break;
}
_ => break,
}
}
assert!(
(release_samples as f64 - 100.0).abs() <= 5.0,
"release lasted {} samples, expected ~100",
release_samples
);
}
#[test]
fn test_noise_gate_no_chatter_near_threshold() {
let fs = 44100.0;
let mut gate = NoiseGate::new(fs);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.2); let mut transitions = 0;
let mut last_hi = false;
for i in 0..(fs as usize) {
let amp = if i % 2 == 0 { 1.3 } else { 0.9 };
inputs.set(0, amp);
gate.tick(&inputs, &mut outputs);
let hi = outputs.get(11).unwrap() > GATE_THRESHOLD_V;
if hi != last_hi {
transitions += 1;
last_hi = hi;
}
}
assert!(
transitions <= 2,
"gate chattered near threshold: {} transitions",
transitions
);
}
#[test]
fn test_noise_gate_fade_rate_independent_of_detector() {
fn measure_open_fade(attack_cv: f64) -> usize {
let fs = 44100.0;
let mut gate = NoiseGate::new(fs);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(1, 0.2); inputs.set(2, attack_cv); inputs.set(0, 5.0); let mut started = false;
let mut count = 0usize;
for _ in 0..200_000 {
gate.tick(&inputs, &mut outputs);
if started {
count += 1;
if gate.gate_state >= 0.632 {
return count;
}
} else if gate.gate_state > 0.0 {
started = true;
count = 1;
if gate.gate_state >= 0.632 {
return count;
}
}
}
count
}
let fast = measure_open_fade(0.0); let slow = measure_open_fade(1.0); assert!(
(fast as i64 - slow as i64).abs() <= 2,
"fade rate varied with detector attack: fast={} slow={}",
fast,
slow
);
let expected = (NoiseGate::FADE_MS * 44100.0 / 1000.0) as i64;
assert!(
(fast as i64 - expected).abs() <= 3,
"fade tc {} samples != expected {}",
fast,
expected
);
}
#[test]
fn test_dynamics_detectors_flush_to_zero() {
let fs = 44100.0;
const BUDGET: usize = 500_000;
{
let mut m = EnvelopeFollower::new(fs);
let mut i = PortValues::new();
let mut o = PortValues::new();
i.set(2, 0.0); i.set(0, 5.0);
for _ in 0..2000 {
m.tick(&i, &mut o);
}
i.set(0, 0.0);
let mut n = 0;
while m.envelope != 0.0 && n < BUDGET {
m.tick(&i, &mut o);
n += 1;
}
assert!(
m.envelope == 0.0,
"EnvelopeFollower left tail {}",
m.envelope
);
}
{
let mut m = Limiter::new(fs);
let mut i = PortValues::new();
let mut o = PortValues::new();
i.set(2, 0.0);
i.set(0, 5.0);
for _ in 0..2000 {
m.tick(&i, &mut o);
}
i.set(0, 0.0);
let mut n = 0;
while m.envelope != 0.0 && n < BUDGET {
m.tick(&i, &mut o);
n += 1;
}
assert!(m.envelope == 0.0, "Limiter left tail {}", m.envelope);
}
{
let mut m = Compressor::new(fs);
let mut i = PortValues::new();
let mut o = PortValues::new();
i.set(4, 0.0);
i.set(0, 5.0);
for _ in 0..2000 {
m.tick(&i, &mut o);
}
i.set(0, 0.0);
let mut n = 0;
while m.envelope != 0.0 && n < BUDGET {
m.tick(&i, &mut o);
n += 1;
}
assert!(m.envelope == 0.0, "Compressor left tail {}", m.envelope);
}
{
let mut m = NoiseGate::new(fs);
let mut i = PortValues::new();
let mut o = PortValues::new();
i.set(3, 0.0); i.set(0, 5.0);
for _ in 0..2000 {
m.tick(&i, &mut o);
}
i.set(0, 0.0);
let mut n = 0;
while (m.envelope != 0.0 || m.gate_state != 0.0) && n < BUDGET {
m.tick(&i, &mut o);
n += 1;
}
assert!(
m.envelope == 0.0 && m.gate_state == 0.0,
"NoiseGate left tail: env {} gate {}",
m.envelope,
m.gate_state
);
}
}
#[test]
fn test_adsr_exp_mode_reaches_sustain() {
let fs = 1000.0;
let mut adsr = Adsr::new(fs);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(2, 0.3); inputs.set(3, 0.5); inputs.set(4, 0.6); inputs.set(5, 0.5); inputs.set(6, 5.0); inputs.set(0, 5.0);
let mut reached = None;
for i in 0..5000 {
adsr.tick(&inputs, &mut outputs);
if adsr.stage == AdsrStage::Sustain {
reached = Some(i);
break;
}
}
let reached = reached.expect("exponential envelope should reach sustain");
assert!(
(adsr.level - 0.6).abs() < 1e-6,
"exp sustain level {} != 0.6",
adsr.level
);
assert!(
reached < 2000,
"exp env took {} samples to reach sustain",
reached
);
}
#[test]
fn test_adsr_linear_mode_is_linear() {
let fs = 1000.0;
let mut adsr = Adsr::new(fs);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(2, 0.5); inputs.set(0, 5.0); let mut levels = [0.0f64; 10];
for l in levels.iter_mut() {
adsr.tick(&inputs, &mut outputs);
*l = adsr.level;
}
for (i, &lvl) in levels.iter().enumerate() {
let expected = 0.01 * (i as f64 + 1.0);
assert!(
(lvl - expected).abs() < 1e-9,
"linear attack sample {} = {}, expected {}",
i,
lvl,
expected
);
}
}
#[test]
fn test_vca_default_golden() {
let mut vca = Vca::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
let cases = [
(1.0_f64, 10.0_f64, 1.0_f64),
(2.0, 5.0, 1.0),
(-4.0, 2.0, -0.8),
(3.0, 7.0, 2.1),
(5.0, 0.0, 0.0),
(0.5, 10.0, 0.5),
];
for (inp, cv, expected) in cases {
inputs.set(0, inp);
inputs.set(1, cv);
vca.tick(&inputs, &mut outputs);
let out = outputs.get(10).unwrap();
assert!(
(out - expected).abs() < 1e-12,
"in={} cv={} => {} (want {})",
inp,
cv,
out,
expected
);
}
}
#[test]
fn test_vca_exponential_response() {
let mut vca = Vca::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(2, 5.0); inputs.set(0, 1.0);
inputs.set(1, 5.0);
vca.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 0.25).abs() < 1e-9);
let mut prev = -1.0;
for k in 0..=20 {
let cv = k as f64 * 0.5;
inputs.set(1, cv);
vca.tick(&inputs, &mut outputs);
let g = outputs.get(10).unwrap();
assert!(g >= prev - 1e-12, "not monotonic at cv={}", cv);
prev = g;
}
inputs.set(1, 10.0);
vca.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 1.0).abs() < 1e-9);
inputs.set(1, 0.0);
vca.tick(&inputs, &mut outputs);
assert!(outputs.get(10).unwrap().abs() < 1e-12);
}
#[test]
fn test_vca_boost() {
let mut vca = Vca::new();
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 1.0);
inputs.set(1, 10.0); inputs.set(3, 2.0); vca.tick(&inputs, &mut outputs);
assert!(
(outputs.get(10).unwrap() - 2.0).abs() < 1e-9,
"boost failed: {}",
outputs.get(10).unwrap()
);
inputs.set(3, 5.0);
vca.tick(&inputs, &mut outputs);
assert!((outputs.get(10).unwrap() - 2.0).abs() < 1e-9);
}
#[test]
fn test_noise_gate_opens_from_sidechain() {
let mut gate = NoiseGate::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.01); inputs.set(1, 0.3); inputs.set(5, 5.0);
for _ in 0..2000 {
gate.tick(&inputs, &mut outputs);
}
assert!(
outputs.get(11).unwrap() > GATE_THRESHOLD_V,
"sidechain key should open the gate"
);
}
#[test]
fn test_noise_gate_sidechain_unpatched_matches_input() {
let mut gate = NoiseGate::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.01);
inputs.set(1, 0.5);
for _ in 0..2000 {
gate.tick(&inputs, &mut outputs);
}
assert!(outputs.get(11).unwrap() < GATE_THRESHOLD_V);
}
#[test]
fn test_limiter_sidechain_drives_gain_reduction() {
let mut limiter = Limiter::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 0.5); inputs.set(1, 0.5); inputs.set(4, 10.0); for _ in 0..200 {
limiter.tick(&inputs, &mut outputs);
}
assert!(
outputs.get(11).unwrap() > 0.0,
"sidechain key should drive limiting"
);
}
#[test]
fn test_ducker_attenuates_on_key_and_recovers() {
let sr = 44100.0;
let mut ducker = Ducker::new(sr);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 4.0); inputs.set(4, 0.0); inputs.set(5, 0.0);
inputs.set(1, 5.0);
for _ in 0..2000 {
ducker.tick(&inputs, &mut outputs);
}
let ducked = outputs.get(10).unwrap();
assert!(
ducked.abs() < 3.5,
"output should be attenuated while key active, got {ducked}"
);
assert!(
outputs.get(11).unwrap() > 0.0,
"gain-reduction CV should be positive while ducking"
);
inputs.set(1, 0.0);
for _ in 0..4000 {
ducker.tick(&inputs, &mut outputs);
}
let recovered = outputs.get(10).unwrap();
assert!(
(recovered - 4.0).abs() < 0.2,
"output should recover after key release, got {recovered}"
);
}
#[test]
fn test_ducker_default_type_id() {
let ducker = Ducker::default();
assert_eq!(ducker.type_id(), "ducker");
}
#[test]
fn test_ducker_no_key_passes_through() {
let mut ducker = Ducker::new(44100.0);
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
inputs.set(0, 3.0);
inputs.set(1, 0.0);
for _ in 0..500 {
ducker.tick(&inputs, &mut outputs);
}
assert!((outputs.get(10).unwrap() - 3.0).abs() < 1e-9);
assert!(outputs.get(11).unwrap().abs() < 1e-9);
}
fn assert_detector_recovers<M: GraphModule>(
module: &mut M,
poison_ports: &[u32],
clean: &[(u32, f64)],
envelope: impl Fn(&M) -> f64,
) {
let mut inputs = PortValues::new();
let mut outputs = PortValues::new();
for &bad in &[f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
for &p in poison_ports {
inputs.set(p, bad);
}
module.tick(&inputs, &mut outputs);
}
let mut inputs = PortValues::new();
for &(port, value) in clean {
inputs.set(port, value);
}
for _ in 0..2000 {
module.tick(&inputs, &mut outputs);
}
assert!(
envelope(module).is_finite(),
"envelope stayed non-finite after a NaN input"
);
assert!(
outputs.get(10).unwrap().is_finite(),
"output stayed non-finite after a NaN input"
);
}
#[test]
fn test_limiter_nan_recovery() {
let mut m = Limiter::new(44100.0);
assert_detector_recovers(&mut m, &[0, 4], &[(0, 0.5), (4, 0.5)], |m| m.envelope);
}
#[test]
fn test_noise_gate_nan_recovery() {
let mut m = NoiseGate::new(44100.0);
assert_detector_recovers(&mut m, &[0, 5], &[(0, 0.5), (5, 0.5)], |m| m.envelope);
}
#[test]
fn test_compressor_nan_recovery() {
let mut m = Compressor::new(44100.0);
assert_detector_recovers(&mut m, &[0, 6], &[(0, 0.5), (6, 0.5)], |m| m.envelope);
}
#[test]
fn test_ducker_nan_recovery() {
let mut m = Ducker::new(44100.0);
assert_detector_recovers(&mut m, &[0, 1], &[(0, 0.5), (1, 0.5)], |m| m.envelope);
}
#[test]
fn test_envelope_follower_nan_recovery() {
let mut m = EnvelopeFollower::new(44100.0);
assert_detector_recovers(&mut m, &[0], &[(0, 0.5)], |m| m.envelope);
}
}