Skip to main content

rill_patchbay/automaton/
envelope.rs

1//! Envelope automata for amplitude, filter, and parameter modulation over time.
2//!
3//! Supports ADSR, AR, ASR, and AHDSR envelope types.
4
5use crate::engine::{Automaton, Range, Time};
6use rill_core::traits::ParamValue;
7
8/// Envelope shape type.
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub enum EnvelopeType {
12    /// Attack, Decay, Sustain, Release.
13    ADSR,
14    /// Attack, Release (suitable for percussion).
15    AR,
16    /// Attack, Sustain, Release (suitable for organ sounds).
17    ASR,
18    /// Attack, Hold, Decay, Sustain, Release.
19    AHDSR,
20}
21
22/// Phase of the envelope lifecycle.
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum EnvelopeStage {
26    /// The signal is rising toward the peak level.
27    Attack,
28    /// The signal holds at the peak level (AHDSR only).
29    Hold,
30    /// The signal falls from the peak to the sustain level.
31    Decay,
32    /// The signal remains at the sustain level while gate is on.
33    Sustain,
34    /// The signal falls to zero after gate-off.
35    Release,
36    /// The envelope is silent.
37    Off,
38}
39
40impl EnvelopeStage {
41    /// Return the human-readable name of this stage.
42    pub fn name(&self) -> &'static str {
43        match self {
44            EnvelopeStage::Attack => "Attack",
45            EnvelopeStage::Hold => "Hold",
46            EnvelopeStage::Decay => "Decay",
47            EnvelopeStage::Sustain => "Sustain",
48            EnvelopeStage::Release => "Release",
49            EnvelopeStage::Off => "Off",
50        }
51    }
52}
53
54/// An envelope automaton that generates time-varying control signals.
55///
56/// The envelope progresses through stages (Attack, Decay, Sustain, Release, etc.)
57/// and outputs a value that can be mapped to a parameter. The stage curve
58/// controls the shape: 1.0 = linear, >1.0 = exponential, <1.0 = logarithmic.
59#[derive(Debug, Clone)]
60pub struct EnvelopeAutomaton {
61    name: String,
62    env_type: EnvelopeType,
63    attack: f64,
64    hold: f64,
65    decay: f64,
66    sustain: f64,
67    release: f64,
68    range: Range,
69    curve: f64,
70}
71
72/// Internal envelope state: (stage, stage_start_time, stage_start_level).
73type EnvelopeInternal = (EnvelopeStage, f64, f64);
74
75impl EnvelopeAutomaton {
76    /// Create a new ADSR envelope.
77    pub fn adsr(name: &str, attack: f64, decay: f64, sustain: f64, release: f64) -> Self {
78        Self {
79            name: name.to_string(),
80            env_type: EnvelopeType::ADSR,
81            attack: attack.max(0.001),
82            hold: 0.0,
83            decay: decay.max(0.001),
84            sustain: sustain.clamp(0.0, 1.0),
85            release: release.max(0.001),
86            range: Range::unipolar(),
87            curve: 1.0,
88        }
89    }
90
91    /// Create a new AR envelope (suitable for percussion).
92    pub fn ar(name: &str, attack: f64, release: f64) -> Self {
93        Self {
94            name: name.to_string(),
95            env_type: EnvelopeType::AR,
96            attack: attack.max(0.001),
97            hold: 0.0,
98            decay: 0.0,
99            sustain: 0.0,
100            release: release.max(0.001),
101            range: Range::unipolar(),
102            curve: 1.0,
103        }
104    }
105
106    /// Create a new ASR envelope (suitable for organ sounds).
107    pub fn asr(name: &str, attack: f64, sustain: f64, release: f64) -> Self {
108        Self {
109            name: name.to_string(),
110            env_type: EnvelopeType::ASR,
111            attack: attack.max(0.001),
112            hold: 0.0,
113            decay: 0.0,
114            sustain: sustain.clamp(0.0, 1.0),
115            release: release.max(0.001),
116            range: Range::unipolar(),
117            curve: 1.0,
118        }
119    }
120
121    /// Create a new AHDSR envelope with an additional hold stage.
122    pub fn ahdsr(
123        name: &str,
124        attack: f64,
125        hold: f64,
126        decay: f64,
127        sustain: f64,
128        release: f64,
129    ) -> Self {
130        Self {
131            name: name.to_string(),
132            env_type: EnvelopeType::AHDSR,
133            attack: attack.max(0.001),
134            hold: hold.max(0.001),
135            decay: decay.max(0.001),
136            sustain: sustain.clamp(0.0, 1.0),
137            release: release.max(0.001),
138            range: Range::unipolar(),
139            curve: 1.0,
140        }
141    }
142
143    /// Set the stage curve exponent (1.0 = linear).
144    pub fn with_curve(mut self, curve: f64) -> Self {
145        self.curve = curve.max(0.1);
146        self
147    }
148
149    /// Set the output range.
150    pub fn with_range(mut self, range: Range) -> Self {
151        self.range = range;
152        self
153    }
154
155    /// Compute the curved interpolation factor.
156    fn apply_curve(&self, t: f64) -> f64 {
157        if self.curve == 1.0 {
158            t
159        } else {
160            t.powf(self.curve)
161        }
162    }
163
164    /// Get the duration for a given stage.
165    fn stage_duration(&self, stage: EnvelopeStage) -> f64 {
166        match stage {
167            EnvelopeStage::Attack => self.attack,
168            EnvelopeStage::Hold => self.hold,
169            EnvelopeStage::Decay => self.decay,
170            EnvelopeStage::Release => self.release,
171            EnvelopeStage::Sustain | EnvelopeStage::Off => f64::INFINITY,
172        }
173    }
174
175    /// Get the target level for a given stage.
176    fn stage_target(&self, stage: EnvelopeStage) -> f64 {
177        match stage {
178            EnvelopeStage::Attack => 1.0,
179            EnvelopeStage::Hold => 1.0,
180            EnvelopeStage::Decay => self.sustain,
181            EnvelopeStage::Sustain => self.sustain,
182            EnvelopeStage::Release => 0.0,
183            EnvelopeStage::Off => 0.0,
184        }
185    }
186
187    /// Transition to the next stage after the current one ends.
188    fn next_stage(&self, current: EnvelopeStage) -> EnvelopeStage {
189        match (current, self.env_type) {
190            (EnvelopeStage::Attack, EnvelopeType::ADSR) => EnvelopeStage::Decay,
191            (EnvelopeStage::Attack, EnvelopeType::AR) => EnvelopeStage::Release,
192            (EnvelopeStage::Attack, EnvelopeType::ASR) => EnvelopeStage::Sustain,
193            (EnvelopeStage::Attack, EnvelopeType::AHDSR) => EnvelopeStage::Hold,
194            (EnvelopeStage::Hold, _) => EnvelopeStage::Decay,
195            (EnvelopeStage::Decay, _) => EnvelopeStage::Sustain,
196            (EnvelopeStage::Release, _) => EnvelopeStage::Off,
197            (EnvelopeStage::Sustain, _) => EnvelopeStage::Sustain,
198            (EnvelopeStage::Off, _) => EnvelopeStage::Off,
199        }
200    }
201}
202
203/// Control action for an envelope automaton.
204///
205/// Use `GateOn` to trigger the attack phase and `GateOff` to start the release.
206#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
207#[derive(Debug, Clone, Default)]
208pub enum EnvelopeAction {
209    #[default]
210    /// No action — the envelope continues its current stage.
211    None,
212    /// Start the attack phase.
213    GateOn,
214    /// Start the release phase.
215    GateOff,
216}
217
218impl Automaton for EnvelopeAutomaton {
219    type Internal = EnvelopeInternal;
220    type Action = EnvelopeAction;
221
222    fn step(
223        &self,
224        internal: &mut Self::Internal,
225        current: &ParamValue,
226        time: Time,
227        action: &Self::Action,
228    ) -> ParamValue {
229        let (stage, stage_start_time, stage_start_level) = *internal;
230        let current_level = current.as_f32().unwrap_or(0.0) as f64;
231
232        let (new_stage, new_start_time, new_start_level) = match action {
233            EnvelopeAction::GateOn => (EnvelopeStage::Attack, time, current_level),
234            EnvelopeAction::GateOff => (EnvelopeStage::Release, time, current_level),
235            EnvelopeAction::None => (stage, stage_start_time, stage_start_level),
236        };
237
238        let elapsed = time - new_start_time;
239        let duration = self.stage_duration(new_stage);
240        let target = self.stage_target(new_stage);
241
242        let (next_stage, next_start_time, next_start_level, level) = if elapsed >= duration {
243            let next = self.next_stage(new_stage);
244            let next_target = self.stage_target(next);
245            let next_dur = self.stage_duration(next);
246            if next_dur.is_infinite() {
247                (next, time, next_target, next_target)
248            } else {
249                // Stage ended exactly — use target level as output, start next stage
250                (next, time, target, target)
251            }
252        } else {
253            let t = elapsed / duration;
254            let curved = self.apply_curve(t);
255            let lvl = new_start_level + (target - new_start_level) * curved;
256            (new_stage, new_start_time, new_start_level, lvl)
257        };
258
259        *internal = (next_stage, next_start_time, next_start_level);
260        let value = self.range.denormalize(level);
261        ParamValue::Float(value as f32)
262    }
263
264    fn initial_internal(&self) -> Self::Internal {
265        (EnvelopeStage::Off, 0.0, 0.0)
266    }
267
268    fn name(&self) -> &str {
269        &self.name
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn test_adsr_envelope() {
279        let env = EnvelopeAutomaton::adsr("ADSR", 0.1, 0.2, 0.7, 0.3);
280        let mut internal = env.initial_internal();
281        let current = ParamValue::Float(0.0);
282
283        assert_eq!(internal.0, EnvelopeStage::Off);
284
285        let value = env.step(&mut internal, &current, 0.0, &EnvelopeAction::GateOn);
286        assert_eq!(internal.0, EnvelopeStage::Attack);
287
288        let value = env.step(&mut internal, &value, 0.05, &EnvelopeAction::None);
289        let val = value.as_f32().unwrap();
290        assert!(val > 0.0);
291        assert!(val < 1.0);
292
293        let _value = env.step(&mut internal, &value, 0.5, &EnvelopeAction::GateOff);
294        assert_eq!(internal.0, EnvelopeStage::Release);
295    }
296}