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::control::{Automaton, Range, Time};
6
7/// Envelope shape type.
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum EnvelopeType {
11    /// Attack, Decay, Sustain, Release.
12    ADSR,
13    /// Attack, Release (suitable for percussion).
14    AR,
15    /// Attack, Sustain, Release (suitable for organ sounds).
16    ASR,
17    /// Attack, Hold, Decay, Sustain, Release.
18    AHDSR,
19}
20
21/// Phase of the envelope lifecycle.
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23#[derive(Debug, Clone, Copy, PartialEq)]
24pub enum EnvelopeStage {
25    /// The signal is rising toward the peak level.
26    Attack,
27    /// The signal holds at the peak level (AHDSR only).
28    Hold,
29    /// The signal falls from the peak to the sustain level.
30    Decay,
31    /// The signal remains at the sustain level while gate is on.
32    Sustain,
33    /// The signal falls to zero after gate-off.
34    Release,
35    /// The envelope is silent.
36    Off,
37}
38
39impl EnvelopeStage {
40    /// Return the human-readable name of this stage.
41    pub fn name(&self) -> &'static str {
42        match self {
43            EnvelopeStage::Attack => "Attack",
44            EnvelopeStage::Hold => "Hold",
45            EnvelopeStage::Decay => "Decay",
46            EnvelopeStage::Sustain => "Sustain",
47            EnvelopeStage::Release => "Release",
48            EnvelopeStage::Off => "Off",
49        }
50    }
51}
52
53/// Runtime state of an envelope automaton.
54///
55/// Tracks the current stage, output level, timing information, and gate status.
56#[derive(Debug, Clone)]
57pub struct EnvelopeState {
58    /// Current envelope phase.
59    pub stage: EnvelopeStage,
60    /// Current output level (0.0 – 1.0).
61    pub level: f64,
62    /// Time when the current stage began.
63    pub stage_start_time: Time,
64    /// Output level at the start of the current stage.
65    pub stage_start_level: f64,
66    /// Target level of the current stage.
67    pub stage_target_level: f64,
68    /// Duration of the current stage in seconds.
69    pub stage_duration: f64,
70    /// Whether the gate is on (triggered).
71    pub gate: bool,
72}
73
74/// An envelope automaton that generates time-varying control signals.
75///
76/// The envelope progresses through stages (Attack, Decay, Sustain, Release, etc.)
77/// and outputs a value that can be mapped to a parameter. The stage curve
78/// controls the shape: 1.0 = linear, >1.0 = exponential, <1.0 = logarithmic.
79#[derive(Debug, Clone)]
80pub struct EnvelopeAutomaton {
81    name: String,
82    env_type: EnvelopeType,
83    attack: f64,
84    hold: f64,
85    decay: f64,
86    sustain: f64,
87    release: f64,
88    range: Range,
89    curve: f64,
90}
91
92impl EnvelopeAutomaton {
93    /// Create a new ADSR envelope.
94    pub fn adsr(name: &str, attack: f64, decay: f64, sustain: f64, release: f64) -> Self {
95        Self {
96            name: name.to_string(),
97            env_type: EnvelopeType::ADSR,
98            attack: attack.max(0.001),
99            hold: 0.0,
100            decay: decay.max(0.001),
101            sustain: sustain.clamp(0.0, 1.0),
102            release: release.max(0.001),
103            range: Range::unipolar(),
104            curve: 1.0,
105        }
106    }
107
108    /// Create a new AR envelope (suitable for percussion).
109    pub fn ar(name: &str, attack: f64, release: f64) -> Self {
110        Self {
111            name: name.to_string(),
112            env_type: EnvelopeType::AR,
113            attack: attack.max(0.001),
114            hold: 0.0,
115            decay: 0.0,
116            sustain: 0.0,
117            release: release.max(0.001),
118            range: Range::unipolar(),
119            curve: 1.0,
120        }
121    }
122
123    /// Create a new ASR envelope (suitable for organ sounds).
124    pub fn asr(name: &str, attack: f64, sustain: f64, release: f64) -> Self {
125        Self {
126            name: name.to_string(),
127            env_type: EnvelopeType::ASR,
128            attack: attack.max(0.001),
129            hold: 0.0,
130            decay: 0.0,
131            sustain: sustain.clamp(0.0, 1.0),
132            release: release.max(0.001),
133            range: Range::unipolar(),
134            curve: 1.0,
135        }
136    }
137
138    /// Create a new AHDSR envelope with an additional hold stage.
139    pub fn ahdsr(
140        name: &str,
141        attack: f64,
142        hold: f64,
143        decay: f64,
144        sustain: f64,
145        release: f64,
146    ) -> Self {
147        Self {
148            name: name.to_string(),
149            env_type: EnvelopeType::AHDSR,
150            attack: attack.max(0.001),
151            hold: hold.max(0.001),
152            decay: decay.max(0.001),
153            sustain: sustain.clamp(0.0, 1.0),
154            release: release.max(0.001),
155            range: Range::unipolar(),
156            curve: 1.0,
157        }
158    }
159
160    /// Set the stage curve exponent (1.0 = linear).
161    pub fn with_curve(mut self, curve: f64) -> Self {
162        self.curve = curve.max(0.1);
163        self
164    }
165
166    /// Set the output range.
167    pub fn with_range(mut self, range: Range) -> Self {
168        self.range = range;
169        self
170    }
171
172    /// Compute the curved interpolation factor.
173    fn apply_curve(&self, t: f64) -> f64 {
174        if self.curve == 1.0 {
175            t
176        } else {
177            t.powf(self.curve)
178        }
179    }
180
181    /// Advance the envelope stage based on elapsed time.
182    fn update_stage(&self, state: &mut EnvelopeState, time: Time) {
183        let elapsed = time - state.stage_start_time;
184
185        match state.stage {
186            EnvelopeStage::Attack => {
187                if elapsed >= self.attack {
188                    match self.env_type {
189                        EnvelopeType::ADSR => {
190                            state.stage = EnvelopeStage::Decay;
191                            state.stage_start_time = time;
192                            state.stage_start_level = 1.0;
193                            state.stage_target_level = self.sustain;
194                            state.stage_duration = self.decay;
195                        }
196                        EnvelopeType::AR => {
197                            state.stage = EnvelopeStage::Release;
198                            state.stage_start_time = time;
199                            state.stage_start_level = 1.0;
200                            state.stage_target_level = 0.0;
201                            state.stage_duration = self.release;
202                        }
203                        EnvelopeType::ASR => {
204                            state.stage = EnvelopeStage::Sustain;
205                            state.stage_start_time = time;
206                            state.stage_start_level = 1.0;
207                            state.stage_target_level = self.sustain;
208                            state.stage_duration = 0.0;
209                        }
210                        EnvelopeType::AHDSR => {
211                            state.stage = EnvelopeStage::Hold;
212                            state.stage_start_time = time;
213                            state.stage_start_level = 1.0;
214                            state.stage_target_level = 1.0;
215                            state.stage_duration = self.hold;
216                        }
217                    }
218                } else {
219                    let t = elapsed / self.attack;
220                    state.level = state.stage_start_level
221                        + (state.stage_target_level - state.stage_start_level)
222                            * self.apply_curve(t);
223                }
224            }
225
226            EnvelopeStage::Hold => {
227                if elapsed >= self.hold {
228                    state.stage = EnvelopeStage::Decay;
229                    state.stage_start_time = time;
230                    state.stage_start_level = 1.0;
231                    state.stage_target_level = self.sustain;
232                    state.stage_duration = self.decay;
233                } else {
234                    state.level = 1.0;
235                }
236            }
237
238            EnvelopeStage::Decay => {
239                if elapsed >= self.decay {
240                    state.stage = EnvelopeStage::Sustain;
241                    state.level = self.sustain;
242                } else {
243                    let t = elapsed / self.decay;
244                    state.level = state.stage_start_level
245                        + (state.stage_target_level - state.stage_start_level)
246                            * self.apply_curve(t);
247                }
248            }
249
250            EnvelopeStage::Sustain => {
251                state.level = self.sustain;
252            }
253
254            EnvelopeStage::Release => {
255                if elapsed >= self.release {
256                    state.stage = EnvelopeStage::Off;
257                    state.level = 0.0;
258                } else {
259                    let t = elapsed / self.release;
260                    state.level = state.stage_start_level
261                        + (state.stage_target_level - state.stage_start_level)
262                            * self.apply_curve(t);
263                }
264            }
265
266            EnvelopeStage::Off => {
267                state.level = 0.0;
268            }
269        }
270    }
271}
272
273/// Control action for an envelope automaton.
274///
275/// Use `GateOn` to trigger the attack phase and `GateOff` to start the release.
276#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
277#[derive(Debug, Clone, Default)]
278pub enum EnvelopeAction {
279    #[default]
280    /// No action — the envelope continues its current stage.
281    None,
282    /// Start the attack phase.
283    GateOn,
284    /// Start the release phase.
285    GateOff,
286}
287
288impl Automaton for EnvelopeAutomaton {
289    type State = EnvelopeState;
290    type Action = EnvelopeAction;
291
292    fn step(
293        &self,
294        time: Time,
295        action: &Self::Action,
296        state: &Self::State,
297    ) -> (Self::State, Option<f64>) {
298        let mut new_state = state.clone();
299
300        match action {
301            EnvelopeAction::GateOn => {
302                new_state.gate = true;
303                new_state.stage = EnvelopeStage::Attack;
304                new_state.stage_start_time = time;
305                new_state.stage_start_level = new_state.level;
306                new_state.stage_target_level = 1.0;
307            }
308            EnvelopeAction::GateOff => {
309                new_state.gate = false;
310                new_state.stage = EnvelopeStage::Release;
311                new_state.stage_start_time = time;
312                new_state.stage_start_level = new_state.level;
313                new_state.stage_target_level = 0.0;
314            }
315            EnvelopeAction::None => {}
316        }
317
318        self.update_stage(&mut new_state, time);
319
320        let value = self.range.denormalize(new_state.level);
321
322        (new_state, Some(value))
323    }
324
325    fn initial_state(&self) -> Self::State {
326        EnvelopeState {
327            stage: EnvelopeStage::Off,
328            level: 0.0,
329            stage_start_time: 0.0,
330            stage_start_level: 0.0,
331            stage_target_level: 0.0,
332            stage_duration: 0.0,
333            gate: false,
334        }
335    }
336
337    fn name(&self) -> &str {
338        &self.name
339    }
340
341    fn extract_value(&self, state: &Self::State) -> f64 {
342        self.range.denormalize(state.level)
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn test_adsr_envelope() {
352        let env = EnvelopeAutomaton::adsr("ADSR", 0.1, 0.2, 0.7, 0.3);
353        let mut state = env.initial_state();
354
355        assert_eq!(state.stage, EnvelopeStage::Off);
356
357        let (_s, _value) = env.step(0.0, &EnvelopeAction::GateOn, &state);
358        state = _s;
359        assert_eq!(state.stage, EnvelopeStage::Attack);
360
361        let (s, value) = env.step(0.05, &EnvelopeAction::None, &state);
362        state = s;
363        assert!(value.unwrap() > 0.0);
364        assert!(value.unwrap() < 1.0);
365
366        let (s, _) = env.step(0.5, &EnvelopeAction::GateOff, &state);
367        assert_eq!(s.stage, EnvelopeStage::Release);
368    }
369}