Initial commit
This commit is contained in:
256
src/envelope.rs
Normal file
256
src/envelope.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
//! 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user