Files
doux-copy/src/envelope.rs
2026-01-18 15:39:46 +01:00

257 lines
8.0 KiB
Rust

//! ADSR envelope generation for audio synthesis.
//!
//! This module provides a state-machine based ADSR (Attack, Decay, Sustain, Release)
//! envelope generator with configurable curve shapes. The envelope responds to gate
//! signals and produces amplitude values in the range `[0.0, 1.0]`.
//!
//! # Curve Shaping
//!
//! Attack and decay/release phases use exponential curves controlled by internal
//! parameters. Positive exponents create convex curves (slow start, fast finish),
//! while negative exponents create concave curves (fast start, slow finish).
use crate::fastmath::powf;
/// Attempt to scale the input `x` from range `[0, 1]` to range `[y0, y1]` with an exponent `exp`.
///
/// Attempt because the expression `powf(1.0 - x, -exp)` can lead to a NaN when `exp` is greater than 1.0.
/// Using this function on 1.0 - x reverses the curve direction.
///
/// - `exp > 0`: Convex curve (slow start, accelerates toward end)
/// - `exp < 0`: Concave curve (fast start, decelerates toward end)
/// - `exp == 0`: Linear interpolation
fn lerp(x: f32, y0: f32, y1: f32, exp: f32) -> f32 {
if x <= 0.0 {
return y0;
}
if x >= 1.0 {
return y1;
}
let curved = if exp == 0.0 {
x
} else if exp > 0.0 {
powf(x, exp)
} else {
1.0 - powf(1.0 - x, -exp)
};
y0 + (y1 - y0) * curved
}
/// Current phase of the ADSR envelope state machine.
#[derive(Clone, Copy)]
pub enum AdsrState {
/// Envelope is inactive, outputting zero.
Off,
/// Rising from current value toward peak (1.0).
Attack,
/// Falling from peak toward sustain level.
Decay,
/// Holding at sustain level while gate remains high.
Sustain,
/// Falling from current value toward zero after gate release.
Release,
}
/// State-machine ADSR envelope generator.
///
/// Tracks envelope phase and timing internally. Call [`Adsr::update`] each sample
/// with the current time and gate signal to produce envelope values.
///
/// # Curve Parameters
///
/// Default curves use an exponent of `2.0` for attack (convex) and decay/release
/// (concave when negated internally), producing natural-sounding amplitude shapes.
#[derive(Clone, Copy)]
pub struct Adsr {
state: AdsrState,
start_time: f32,
start_val: f32,
attack_curve: f32,
decay_curve: f32,
}
impl Default for Adsr {
fn default() -> Self {
Self {
state: AdsrState::Off,
start_time: 0.0,
start_val: 0.0,
attack_curve: 2.0,
decay_curve: 2.0,
}
}
}
impl Adsr {
/// Returns `true` if the envelope is in the [`AdsrState::Off`] state.
pub fn is_off(&self) -> bool {
matches!(self.state, AdsrState::Off)
}
/// Advances the envelope state machine and returns the current amplitude.
///
/// The envelope responds to gate transitions:
/// - Gate going high (`> 0.0`) triggers attack from current value
/// - Gate going low (`<= 0.0`) triggers release from current value
///
/// This allows retriggering during any phase without clicks, as the envelope
/// always starts from its current position rather than jumping to zero.
///
/// # Parameters
///
/// - `time`: Current time in seconds (must be monotonically increasing)
/// - `gate`: Gate signal (`> 0.0` = note on, `<= 0.0` = note off)
/// - `attack`: Attack duration in seconds
/// - `decay`: Decay duration in seconds
/// - `sustain`: Sustain level in range `[0.0, 1.0]`
/// - `release`: Release duration in seconds
///
/// # Returns
///
/// Envelope amplitude in range `[0.0, 1.0]`.
pub fn update(
&mut self,
time: f32,
gate: f32,
attack: f32,
decay: f32,
sustain: f32,
release: f32,
) -> f32 {
match self.state {
AdsrState::Off => {
if gate > 0.0 {
self.state = AdsrState::Attack;
self.start_time = time;
self.start_val = 0.0;
}
0.0
}
AdsrState::Attack => {
let t = time - self.start_time;
if t > attack {
self.state = AdsrState::Decay;
self.start_time = time;
return 1.0;
}
lerp(t / attack, self.start_val, 1.0, self.attack_curve)
}
AdsrState::Decay => {
let t = time - self.start_time;
let val = lerp(t / decay, 1.0, sustain, -self.decay_curve);
if gate <= 0.0 {
self.state = AdsrState::Release;
self.start_time = time;
self.start_val = val;
return val;
}
if t > decay {
self.state = AdsrState::Sustain;
self.start_time = time;
return sustain;
}
val
}
AdsrState::Sustain => {
if gate <= 0.0 {
self.state = AdsrState::Release;
self.start_time = time;
self.start_val = sustain;
}
sustain
}
AdsrState::Release => {
let t = time - self.start_time;
if t > release {
self.state = AdsrState::Off;
return 0.0;
}
let val = lerp(t / release, self.start_val, 0.0, -self.decay_curve);
if gate > 0.0 {
self.state = AdsrState::Attack;
self.start_time = time;
self.start_val = val;
}
val
}
}
}
}
/// Parsed envelope parameters with activation flag.
///
/// Used to pass envelope configuration from pattern parsing to voice rendering.
/// The `active` field indicates whether the user explicitly specified any
/// envelope parameters, allowing voices to skip envelope processing when unused.
#[derive(Clone, Copy, Default)]
pub struct EnvelopeParams {
/// Overall envelope amplitude multiplier.
pub env: f32,
/// Attack time in seconds.
pub att: f32,
/// Decay time in seconds.
pub dec: f32,
/// Sustain level in range `[0.0, 1.0]`.
pub sus: f32,
/// Release time in seconds.
pub rel: f32,
/// Whether envelope parameters were explicitly provided.
pub active: bool,
}
/// Constructs envelope parameters from optional user inputs.
///
/// Applies sensible defaults and infers sustain level from context:
/// - If sustain is explicit, use it (clamped to `1.0`)
/// - If only attack is set, sustain defaults to `1.0` (full level after attack)
/// - If decay is set (with or without attack), sustain defaults to `0.0`
/// - Otherwise, sustain defaults to `1.0`
///
/// When no parameters are provided, returns inactive defaults suitable for
/// bypassing envelope processing entirely.
///
/// # Default Values
///
/// | Parameter | Default |
/// |-----------|---------|
/// | `env` | `1.0` |
/// | `att` | `0.001` |
/// | `dec` | `0.0` |
/// | `sus` | `1.0` |
/// | `rel` | `0.005` |
pub fn init_envelope(
env: Option<f32>,
att: Option<f32>,
dec: Option<f32>,
sus: Option<f32>,
rel: Option<f32>,
) -> EnvelopeParams {
if env.is_none() && att.is_none() && dec.is_none() && sus.is_none() && rel.is_none() {
return EnvelopeParams {
env: 1.0,
att: 0.001,
dec: 0.0,
sus: 1.0,
rel: 0.005,
active: false,
};
}
let sus_val = match (sus, att, dec) {
(Some(s), _, _) => s.min(1.0),
(None, Some(_), None) => 1.0,
(None, None, Some(_)) => 0.0,
(None, Some(_), Some(_)) => 0.0,
_ => 1.0,
};
EnvelopeParams {
env: env.unwrap_or(1.0),
att: att.unwrap_or(0.001),
dec: dec.unwrap_or(0.0),
sus: sus_val,
rel: rel.unwrap_or(0.005),
active: true,
}
}