bbx_dsp/blocks/modulators/
envelope.rs

1//! ADSR envelope generator block.
2
3use crate::{
4    block::{Block, DEFAULT_MODULATOR_INPUT_COUNT, DEFAULT_MODULATOR_OUTPUT_COUNT},
5    context::DspContext,
6    parameter::{ModulationOutput, Parameter},
7    sample::Sample,
8};
9
10/// ADSR envelope stages.
11///
12/// The envelope progresses through: Idle -> Attack -> Decay -> Sustain -> Release -> Idle.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum EnvelopeStage {
15    Idle,
16    Attack,
17    Decay,
18    Sustain,
19    Release,
20}
21
22/// ADSR envelope generator block for amplitude and parameter modulation.
23pub struct EnvelopeBlock<S: Sample> {
24    /// Attack time in seconds.
25    pub attack: Parameter<S>,
26    /// Decay time in seconds.
27    pub decay: Parameter<S>,
28    /// Sustain level (0.0 - 1.0).
29    pub sustain: Parameter<S>,
30    /// Release time in seconds.
31    pub release: Parameter<S>,
32
33    stage: EnvelopeStage,
34    level: f64,
35    stage_time: f64,
36    release_level: f64,
37}
38
39impl<S: Sample> EnvelopeBlock<S> {
40    const MODULATION_OUTPUTS: &'static [ModulationOutput] = &[ModulationOutput {
41        name: "Envelope",
42        min_value: 0.0,
43        max_value: 1.0,
44    }];
45
46    /// Minimum envelope time in seconds.
47    const MIN_TIME: f64 = 0.001;
48    /// Maximum envelope time in seconds.
49    const MAX_TIME: f64 = 10.0;
50    /// Envelope floor threshold (~-120dB) for reliable release termination.
51    const ENVELOPE_FLOOR: f64 = 1e-6;
52
53    /// Create an `EnvelopeBlock` with given ADSR parameters.
54    /// Times are in seconds, sustain is a level from 0.0 to 1.0.
55    pub fn new(attack: f64, decay: f64, sustain: f64, release: f64) -> Self {
56        Self {
57            attack: Parameter::Constant(S::from_f64(attack)),
58            decay: Parameter::Constant(S::from_f64(decay)),
59            sustain: Parameter::Constant(S::from_f64(sustain)),
60            release: Parameter::Constant(S::from_f64(release)),
61            stage: EnvelopeStage::Idle,
62            level: 0.0,
63            stage_time: 0.0,
64            release_level: 0.0,
65        }
66    }
67
68    /// Trigger the envelope (note on).
69    pub fn note_on(&mut self) {
70        self.stage = EnvelopeStage::Attack;
71        self.stage_time = 0.0;
72    }
73
74    /// Release the envelope (note off).
75    pub fn note_off(&mut self) {
76        if self.stage != EnvelopeStage::Idle {
77            self.release_level = self.level;
78            self.stage = EnvelopeStage::Release;
79            self.stage_time = 0.0;
80        }
81    }
82
83    /// Reset the envelope to idle state.
84    pub fn reset(&mut self) {
85        self.stage = EnvelopeStage::Idle;
86        self.level = 0.0;
87        self.stage_time = 0.0;
88        self.release_level = 0.0;
89    }
90
91    #[inline]
92    fn clamp_time(time: f64) -> f64 {
93        time.clamp(Self::MIN_TIME, Self::MAX_TIME)
94    }
95}
96
97impl<S: Sample> Block<S> for EnvelopeBlock<S> {
98    fn process(&mut self, _inputs: &[&[S]], outputs: &mut [&mut [S]], modulation_values: &[S], context: &DspContext) {
99        let attack_time = Self::clamp_time(self.attack.get_value(modulation_values).to_f64());
100        let decay_time = Self::clamp_time(self.decay.get_value(modulation_values).to_f64());
101        let sustain_level = self.sustain.get_value(modulation_values).to_f64().clamp(0.0, 1.0);
102        let release_time = Self::clamp_time(self.release.get_value(modulation_values).to_f64());
103
104        let time_per_sample = 1.0 / context.sample_rate;
105
106        for sample_index in 0..context.buffer_size {
107            match self.stage {
108                EnvelopeStage::Idle => {
109                    self.level = 0.0;
110                }
111                EnvelopeStage::Attack => {
112                    self.level = self.stage_time / attack_time;
113                    if self.level >= 1.0 {
114                        self.level = 1.0;
115                        self.stage = EnvelopeStage::Decay;
116                        self.stage_time = 0.0;
117                    }
118                }
119                EnvelopeStage::Decay => {
120                    let decay_progress = self.stage_time / decay_time;
121                    self.level = 1.0 - (1.0 - sustain_level) * decay_progress;
122                    if self.level <= sustain_level {
123                        self.level = sustain_level;
124                        self.stage = EnvelopeStage::Sustain;
125                        self.stage_time = 0.0;
126                    }
127                }
128                EnvelopeStage::Sustain => {
129                    self.level = sustain_level;
130                }
131                EnvelopeStage::Release => {
132                    let release_progress = self.stage_time / release_time;
133                    self.level = self.release_level * (1.0 - release_progress);
134                    // Avoids floating-point precision issues with exact zero comparison
135                    if self.level <= Self::ENVELOPE_FLOOR {
136                        self.level = 0.0;
137                        self.stage = EnvelopeStage::Idle;
138                        self.stage_time = 0.0;
139                    }
140                }
141            }
142
143            outputs[0][sample_index] = S::from_f64(self.level);
144
145            if self.stage != EnvelopeStage::Idle && self.stage != EnvelopeStage::Sustain {
146                self.stage_time += time_per_sample;
147            }
148        }
149    }
150
151    #[inline]
152    fn input_count(&self) -> usize {
153        DEFAULT_MODULATOR_INPUT_COUNT
154    }
155
156    #[inline]
157    fn output_count(&self) -> usize {
158        DEFAULT_MODULATOR_OUTPUT_COUNT
159    }
160
161    #[inline]
162    fn modulation_outputs(&self) -> &[ModulationOutput] {
163        Self::MODULATION_OUTPUTS
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use crate::channel::ChannelLayout;
171
172    fn test_context(buffer_size: usize, sample_rate: f64) -> DspContext {
173        DspContext {
174            sample_rate,
175            num_channels: 1,
176            buffer_size,
177            current_sample: 0,
178            channel_layout: ChannelLayout::Mono,
179        }
180    }
181
182    fn process_envelope<S: Sample>(env: &mut EnvelopeBlock<S>, context: &DspContext) -> Vec<S> {
183        let inputs: [&[S]; 0] = [];
184        let mut output = vec![S::ZERO; context.buffer_size];
185        let mut outputs: [&mut [S]; 1] = [&mut output];
186        env.process(&inputs, &mut outputs, &[], context);
187        output
188    }
189
190    #[test]
191    fn test_envelope_input_output_counts_f32() {
192        let env = EnvelopeBlock::<f32>::new(0.01, 0.1, 0.5, 0.2);
193        assert_eq!(env.input_count(), DEFAULT_MODULATOR_INPUT_COUNT);
194        assert_eq!(env.output_count(), DEFAULT_MODULATOR_OUTPUT_COUNT);
195    }
196
197    #[test]
198    fn test_envelope_input_output_counts_f64() {
199        let env = EnvelopeBlock::<f64>::new(0.01, 0.1, 0.5, 0.2);
200        assert_eq!(env.input_count(), DEFAULT_MODULATOR_INPUT_COUNT);
201        assert_eq!(env.output_count(), DEFAULT_MODULATOR_OUTPUT_COUNT);
202    }
203
204    #[test]
205    fn test_envelope_modulation_output_f32() {
206        let env = EnvelopeBlock::<f32>::new(0.01, 0.1, 0.5, 0.2);
207        let outputs = env.modulation_outputs();
208        assert_eq!(outputs.len(), 1);
209        assert_eq!(outputs[0].name, "Envelope");
210        assert!((outputs[0].min_value - 0.0).abs() < 1e-10);
211        assert!((outputs[0].max_value - 1.0).abs() < 1e-10);
212    }
213
214    #[test]
215    fn test_envelope_idle_produces_zero_f32() {
216        let mut env = EnvelopeBlock::<f32>::new(0.01, 0.1, 0.5, 0.2);
217        let context = test_context(512, 44100.0);
218        let output = process_envelope(&mut env, &context);
219
220        for (i, &sample) in output.iter().enumerate() {
221            assert!(
222                sample.abs() < 1e-6,
223                "Idle envelope should produce zero: output[{}]={}",
224                i,
225                sample
226            );
227        }
228    }
229
230    #[test]
231    fn test_envelope_idle_produces_zero_f64() {
232        let mut env = EnvelopeBlock::<f64>::new(0.01, 0.1, 0.5, 0.2);
233        let context = test_context(512, 44100.0);
234        let output = process_envelope(&mut env, &context);
235
236        for (i, &sample) in output.iter().enumerate() {
237            assert!(
238                sample.abs() < 1e-12,
239                "Idle envelope should produce zero: output[{}]={}",
240                i,
241                sample
242            );
243        }
244    }
245
246    #[test]
247    fn test_envelope_output_range_f32() {
248        let mut env = EnvelopeBlock::<f32>::new(0.01, 0.05, 0.7, 0.1);
249        let context = test_context(512, 44100.0);
250
251        env.note_on();
252
253        for _ in 0..100 {
254            let output = process_envelope(&mut env, &context);
255            for &sample in &output {
256                assert!(
257                    sample >= 0.0 && sample <= 1.0,
258                    "Envelope should be in [0, 1] range: {}",
259                    sample
260                );
261            }
262        }
263    }
264
265    #[test]
266    fn test_envelope_output_range_f64() {
267        let mut env = EnvelopeBlock::<f64>::new(0.01, 0.05, 0.7, 0.1);
268        let context = test_context(512, 44100.0);
269
270        env.note_on();
271
272        for _ in 0..100 {
273            let output = process_envelope(&mut env, &context);
274            for &sample in &output {
275                assert!(
276                    sample >= 0.0 && sample <= 1.0,
277                    "Envelope should be in [0, 1] range: {}",
278                    sample
279                );
280            }
281        }
282    }
283
284    #[test]
285    fn test_envelope_attack_rises_f32() {
286        let attack_time = 0.1;
287        let mut env = EnvelopeBlock::<f32>::new(attack_time, 0.1, 0.5, 0.1);
288        let sample_rate = 44100.0;
289        let context = test_context(512, sample_rate);
290
291        env.note_on();
292
293        let output = process_envelope(&mut env, &context);
294
295        assert!(output[0] < output[output.len() - 1], "Attack should rise over time");
296        assert!(output[0] < 0.5, "Attack should start low");
297    }
298
299    #[test]
300    fn test_envelope_attack_rises_f64() {
301        let attack_time = 0.1;
302        let mut env = EnvelopeBlock::<f64>::new(attack_time, 0.1, 0.5, 0.1);
303        let sample_rate = 44100.0;
304        let context = test_context(512, sample_rate);
305
306        env.note_on();
307
308        let output = process_envelope(&mut env, &context);
309
310        assert!(output[0] < output[output.len() - 1], "Attack should rise over time");
311        assert!(output[0] < 0.5, "Attack should start low");
312    }
313
314    #[test]
315    fn test_envelope_reaches_peak_f32() {
316        let attack_time = 0.01;
317        let mut env = EnvelopeBlock::<f32>::new(attack_time, 0.5, 0.5, 0.5);
318        let sample_rate = 44100.0;
319        let context = test_context(512, sample_rate);
320
321        env.note_on();
322
323        for _ in 0..10 {
324            let output = process_envelope(&mut env, &context);
325            let max = output.iter().fold(0.0f32, |acc, &x| acc.max(x));
326            if (max - 1.0).abs() < 0.05 {
327                return;
328            }
329        }
330        panic!("Envelope should reach peak of 1.0 during attack");
331    }
332
333    #[test]
334    fn test_envelope_reaches_peak_f64() {
335        let attack_time = 0.01;
336        let mut env = EnvelopeBlock::<f64>::new(attack_time, 0.5, 0.5, 0.5);
337        let sample_rate = 44100.0;
338        let context = test_context(512, sample_rate);
339
340        env.note_on();
341
342        for _ in 0..10 {
343            let output = process_envelope(&mut env, &context);
344            let max = output.iter().fold(0.0f64, |acc, &x| acc.max(x));
345            if (max - 1.0).abs() < 0.05 {
346                return;
347            }
348        }
349        panic!("Envelope should reach peak of 1.0 during attack");
350    }
351
352    #[test]
353    fn test_envelope_sustain_level_f32() {
354        let sustain = 0.6;
355        let mut env = EnvelopeBlock::<f32>::new(0.001, 0.001, sustain, 0.5);
356        let sample_rate = 44100.0;
357        let context = test_context(512, sample_rate);
358
359        env.note_on();
360
361        for _ in 0..20 {
362            let _ = process_envelope(&mut env, &context);
363        }
364
365        let output = process_envelope(&mut env, &context);
366        let avg: f32 = output.iter().sum::<f32>() / output.len() as f32;
367
368        assert!(
369            (avg - sustain as f32).abs() < 0.05,
370            "Sustain should hold at sustain level: expected={}, got={}",
371            sustain,
372            avg
373        );
374    }
375
376    #[test]
377    fn test_envelope_sustain_level_f64() {
378        let sustain = 0.6;
379        let mut env = EnvelopeBlock::<f64>::new(0.001, 0.001, sustain, 0.5);
380        let sample_rate = 44100.0;
381        let context = test_context(512, sample_rate);
382
383        env.note_on();
384
385        for _ in 0..20 {
386            let _ = process_envelope(&mut env, &context);
387        }
388
389        let output = process_envelope(&mut env, &context);
390        let avg: f64 = output.iter().sum::<f64>() / output.len() as f64;
391
392        assert!(
393            (avg - sustain).abs() < 0.05,
394            "Sustain should hold at sustain level: expected={}, got={}",
395            sustain,
396            avg
397        );
398    }
399
400    #[test]
401    fn test_envelope_release_falls_f32() {
402        let sustain = 0.7;
403        let mut env = EnvelopeBlock::<f32>::new(0.001, 0.001, sustain, 0.1);
404        let sample_rate = 44100.0;
405        let context = test_context(512, sample_rate);
406
407        env.note_on();
408
409        for _ in 0..10 {
410            let _ = process_envelope(&mut env, &context);
411        }
412
413        env.note_off();
414
415        let output1 = process_envelope(&mut env, &context);
416        let output2 = process_envelope(&mut env, &context);
417
418        let first_avg: f32 = output1.iter().sum::<f32>() / output1.len() as f32;
419        let second_avg: f32 = output2.iter().sum::<f32>() / output2.len() as f32;
420
421        assert!(
422            first_avg > second_avg,
423            "Release should fall over time: first={}, second={}",
424            first_avg,
425            second_avg
426        );
427    }
428
429    #[test]
430    fn test_envelope_release_falls_f64() {
431        let sustain = 0.7_f64;
432        let mut env = EnvelopeBlock::<f64>::new(0.001, 0.001, sustain, 0.1);
433        let sample_rate = 44100.0;
434        let context = test_context(512, sample_rate);
435
436        env.note_on();
437
438        for _ in 0..10 {
439            let _ = process_envelope(&mut env, &context);
440        }
441
442        env.note_off();
443
444        let output1 = process_envelope(&mut env, &context);
445        let output2 = process_envelope(&mut env, &context);
446
447        let first_avg: f64 = output1.iter().sum::<f64>() / output1.len() as f64;
448        let second_avg: f64 = output2.iter().sum::<f64>() / output2.len() as f64;
449
450        assert!(
451            first_avg > second_avg,
452            "Release should fall over time: first={}, second={}",
453            first_avg,
454            second_avg
455        );
456    }
457
458    #[test]
459    fn test_envelope_release_returns_to_zero_f32() {
460        let mut env = EnvelopeBlock::<f32>::new(0.001, 0.001, 0.5, 0.01);
461        let sample_rate = 44100.0;
462        let context = test_context(512, sample_rate);
463
464        env.note_on();
465
466        for _ in 0..5 {
467            let _ = process_envelope(&mut env, &context);
468        }
469
470        env.note_off();
471
472        for _ in 0..20 {
473            let output = process_envelope(&mut env, &context);
474            let last = output[output.len() - 1];
475            if last.abs() < 1e-5 {
476                return;
477            }
478        }
479        panic!("Release should return to zero");
480    }
481
482    #[test]
483    fn test_envelope_release_returns_to_zero_f64() {
484        let mut env = EnvelopeBlock::<f64>::new(0.001, 0.001, 0.5, 0.01);
485        let sample_rate = 44100.0;
486        let context = test_context(512, sample_rate);
487
488        env.note_on();
489
490        for _ in 0..5 {
491            let _ = process_envelope(&mut env, &context);
492        }
493
494        env.note_off();
495
496        for _ in 0..20 {
497            let output = process_envelope(&mut env, &context);
498            let last = output[output.len() - 1];
499            if last.abs() < 1e-5 {
500                return;
501            }
502        }
503        panic!("Release should return to zero");
504    }
505
506    #[test]
507    fn test_envelope_reset_f32() {
508        let mut env = EnvelopeBlock::<f32>::new(0.1, 0.1, 0.5, 0.1);
509        let context = test_context(512, 44100.0);
510
511        env.note_on();
512        let _ = process_envelope(&mut env, &context);
513
514        env.reset();
515
516        let output = process_envelope(&mut env, &context);
517        for &sample in &output {
518            assert!(sample.abs() < 1e-6, "Reset should return to zero");
519        }
520    }
521
522    #[test]
523    fn test_envelope_reset_f64() {
524        let mut env = EnvelopeBlock::<f64>::new(0.1, 0.1, 0.5, 0.1);
525        let context = test_context(512, 44100.0);
526
527        env.note_on();
528        let _ = process_envelope(&mut env, &context);
529
530        env.reset();
531
532        let output = process_envelope(&mut env, &context);
533        for &sample in &output {
534            assert!(sample.abs() < 1e-12, "Reset should return to zero");
535        }
536    }
537
538    #[test]
539    fn test_envelope_note_off_from_idle_f32() {
540        let mut env = EnvelopeBlock::<f32>::new(0.1, 0.1, 0.5, 0.1);
541        let context = test_context(512, 44100.0);
542
543        env.note_off();
544
545        let output = process_envelope(&mut env, &context);
546        for &sample in &output {
547            assert!(sample.abs() < 1e-6, "note_off from idle should stay at zero");
548        }
549    }
550
551    #[test]
552    fn test_envelope_retrigger_f32() {
553        let mut env = EnvelopeBlock::<f32>::new(0.001, 0.001, 0.5, 0.1);
554        let context = test_context(512, 44100.0);
555
556        env.note_on();
557        for _ in 0..10 {
558            let _ = process_envelope(&mut env, &context);
559        }
560
561        env.note_on();
562
563        let output = process_envelope(&mut env, &context);
564        assert!(
565            output.iter().any(|&x| x < 0.5),
566            "Retrigger should restart attack from beginning"
567        );
568    }
569
570    #[test]
571    fn test_envelope_time_clamping_f32() {
572        let mut env = EnvelopeBlock::<f32>::new(0.0001, 0.0001, 0.5, 0.0001);
573        let context = test_context(512, 44100.0);
574
575        env.note_on();
576
577        for _ in 0..100 {
578            let output = process_envelope(&mut env, &context);
579            for &sample in &output {
580                assert!(
581                    sample >= 0.0 && sample <= 1.0,
582                    "Very short times should still work: {}",
583                    sample
584                );
585            }
586        }
587    }
588}