#[derive(Clone, Copy, PartialEq, Default)]
enum Stage {
#[default]
Idle,
Attack,
Decay,
Sustain,
Release,
}
#[derive(Clone, Default)]
pub struct Adsr {
stage: Stage,
level: f32,
attack_rate: f32,
decay_coeff: f32,
decay_target: f32,
release_coeff: f32,
sustain_level: f32,
sample_rate: f32,
}
fn exp_coeff(time_ms: f32, sample_rate: f32) -> f32 {
let samples = (time_ms * 0.001 * sample_rate).max(1.0);
(-6.908 / samples).exp() }
#[cfg(test)]
fn run_for(env: &mut Adsr, n: usize) -> f32 {
let mut v = 0.0;
for _ in 0..n {
v = env.next_sample();
}
v
}
impl Adsr {
pub fn new(
attack_ms: f32,
decay_ms: f32,
sustain: f32,
release_ms: f32,
sample_rate: f32,
) -> Self {
let sustain = sustain.clamp(0.0, 1.0);
let mut s = Self {
stage: Stage::Idle,
level: 0.0,
attack_rate: 1.0 / (attack_ms * 0.001 * sample_rate).max(1.0),
decay_coeff: exp_coeff(decay_ms, sample_rate),
decay_target: sustain,
release_coeff: exp_coeff(release_ms, sample_rate),
sustain_level: sustain,
sample_rate,
};
s.decay_target = sustain;
s
}
pub fn set_params(&mut self, attack_ms: f32, decay_ms: f32, sustain: f32, release_ms: f32) {
let sr = self.sample_rate.max(1.0);
let sustain = sustain.clamp(0.0, 1.0);
self.attack_rate = 1.0 / (attack_ms * 0.001 * sr).max(1.0);
self.decay_coeff = exp_coeff(decay_ms, sr);
self.decay_target = sustain;
self.release_coeff = exp_coeff(release_ms, sr);
self.sustain_level = sustain;
}
pub fn trigger(&mut self) {
self.stage = Stage::Attack;
}
pub fn release(&mut self) {
if self.stage != Stage::Idle {
self.stage = Stage::Release;
}
}
pub fn is_idle(&self) -> bool {
self.stage == Stage::Idle
}
pub fn level(&self) -> f32 {
self.level
}
pub fn next_sample(&mut self) -> f32 {
match self.stage {
Stage::Idle => {
self.level = 0.0;
}
Stage::Attack => {
self.level += self.attack_rate;
if self.level >= 1.0 {
self.level = 1.0;
self.stage = Stage::Decay;
}
}
Stage::Decay => {
self.level =
self.level * self.decay_coeff + self.decay_target * (1.0 - self.decay_coeff);
if (self.level - self.sustain_level).abs() < 0.0001 {
self.level = self.sustain_level;
self.stage = Stage::Sustain;
}
}
Stage::Sustain => {
}
Stage::Release => {
self.level *= self.release_coeff;
if self.level < 1e-6 {
self.level = 0.0;
self.stage = Stage::Idle;
}
}
}
self.level
}
}
#[cfg(test)]
mod tests {
use super::*;
const SR: f32 = 44100.0;
#[test]
fn test_adsr_at_t0_output_is_zero() {
let mut env = Adsr::new(10.0, 100.0, 0.7, 200.0, SR);
let s = env.next_sample();
assert!(s.abs() < 1e-10, "Idle envelope should produce 0, got {}", s);
}
#[test]
fn test_adsr_attack_rises() {
let attack_ms = 50.0_f32;
let attack_samples = (attack_ms * 0.001 * SR) as usize;
let mut env = Adsr::new(attack_ms, 200.0, 0.7, 200.0, SR);
env.trigger();
let early = run_for(&mut env, attack_samples / 4);
assert!(
early > 0.0 && early < 1.0,
"Level should be rising during attack, got {}",
early
);
let peak = run_for(&mut env, attack_samples * 2);
assert!(
peak >= 0.5,
"Level should be >= sustain after attack+decay, got {}",
peak
);
}
#[test]
fn test_adsr_decay_falls_to_sustain() {
let sustain = 0.5_f32;
let mut env = Adsr::new(1.0, 200.0, sustain, 500.0, SR);
env.trigger();
run_for(&mut env, 100);
let after_decay = run_for(&mut env, 20000);
assert!(
(after_decay - sustain).abs() < 0.01,
"After decay level should be at sustain {}, got {}",
sustain,
after_decay
);
}
#[test]
fn test_adsr_sustain_is_constant() {
let sustain = 0.6_f32;
let mut env = Adsr::new(1.0, 50.0, sustain, 500.0, SR);
env.trigger();
run_for(&mut env, 5000);
let s1 = env.next_sample();
let s2 = run_for(&mut env, 100);
assert!(
(s1 - s2).abs() < 0.005,
"Sustain level should be constant: {} vs {}",
s1,
s2
);
}
#[test]
fn test_adsr_release_falls_to_zero() {
let mut env = Adsr::new(1.0, 50.0, 0.7, 200.0, SR);
env.trigger();
run_for(&mut env, 5000);
env.release();
let after_release = run_for(&mut env, 20000);
assert!(
after_release.abs() < 1e-5,
"Level should reach 0 after release, got {}",
after_release
);
assert!(
env.is_idle(),
"Envelope should be idle after release completes"
);
}
#[test]
fn test_adsr_zero_duration_stages_do_not_panic() {
let mut env = Adsr::new(0.0, 0.0, 0.5, 0.0, SR);
env.trigger();
for _ in 0..1000 {
env.next_sample();
}
env.release();
for _ in 0..1000 {
env.next_sample();
}
assert!(env.level().is_finite());
}
#[test]
fn test_adsr_set_params_updates_sustain() {
let mut env = Adsr::new(1.0, 50.0, 0.7, 500.0, SR);
env.trigger();
run_for(&mut env, 5000);
env.set_params(1.0, 50.0, 0.3, 500.0);
env.trigger();
run_for(&mut env, 5000);
let level = env.level();
assert!(
(level - 0.3).abs() < 0.05,
"After set_params(sustain=0.3) and re-trigger, level should be near 0.3, got {}",
level
);
}
}