Skip to main content

embedded_audio/
envelope.rs

1/// ADSR segment in milliseconds (0 = skip segment).
2#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3pub struct AdsrSpec {
4    pub attack_ms: u16,
5    pub decay_ms: u16,
6    pub sustain_q8: u8,
7    pub release_ms: u16,
8}
9
10impl AdsrSpec {
11    pub const fn click() -> Self {
12        Self {
13            attack_ms: 2,
14            decay_ms: 40,
15            sustain_q8: 0,
16            release_ms: 10,
17        }
18    }
19
20    pub const fn pad() -> Self {
21        Self {
22            attack_ms: 5,
23            decay_ms: 80,
24            sustain_q8: 200,
25            release_ms: 120,
26        }
27    }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31enum AdsrPhase {
32    Attack,
33    Decay,
34    Sustain,
35    Release,
36    Idle,
37}
38
39/// Piecewise-linear ADSR in Q8 (0..=255).
40#[derive(Debug, Clone, Copy)]
41pub struct Adsr {
42    spec: AdsrSpec,
43    phase: AdsrPhase,
44    level_q8: u8,
45    samples_in_phase: u32,
46    phase_length_samples: u32,
47    sample_rate_hz: u32,
48}
49
50impl Adsr {
51    pub const fn sample_rate_hz(self) -> u32 {
52        self.sample_rate_hz
53    }
54
55    pub const fn new(spec: AdsrSpec, sample_rate_hz: u32) -> Self {
56        Self {
57            spec,
58            phase: AdsrPhase::Idle,
59            level_q8: 0,
60            samples_in_phase: 0,
61            phase_length_samples: 0,
62            sample_rate_hz,
63        }
64    }
65
66    pub fn trigger(&mut self) {
67        self.phase = AdsrPhase::Attack;
68        self.level_q8 = 0;
69        self.samples_in_phase = 0;
70        self.phase_length_samples = ms_to_samples(self.spec.attack_ms, self.sample_rate_hz);
71        if self.phase_length_samples == 0 {
72            self.begin_decay();
73        }
74    }
75
76    pub fn release(&mut self) {
77        if matches!(self.phase, AdsrPhase::Idle) {
78            return;
79        }
80        self.phase = AdsrPhase::Release;
81        self.samples_in_phase = 0;
82        self.phase_length_samples = ms_to_samples(self.spec.release_ms, self.sample_rate_hz);
83        if self.phase_length_samples == 0 {
84            self.phase = AdsrPhase::Idle;
85            self.level_q8 = 0;
86        }
87    }
88
89    pub fn is_active(&self) -> bool {
90        !matches!(self.phase, AdsrPhase::Idle)
91    }
92
93    /// Current envelope level 0..=255.
94    pub fn level_q8(&self) -> u8 {
95        self.level_q8
96    }
97
98    pub fn tick(&mut self) {
99        match self.phase {
100            AdsrPhase::Idle => {}
101            AdsrPhase::Attack => {
102                self.advance_toward(255);
103                if self.samples_in_phase >= self.phase_length_samples {
104                    self.begin_decay();
105                }
106            }
107            AdsrPhase::Decay => {
108                self.advance_toward(self.spec.sustain_q8);
109                if self.samples_in_phase >= self.phase_length_samples {
110                    self.phase = AdsrPhase::Sustain;
111                    self.level_q8 = self.spec.sustain_q8;
112                    self.samples_in_phase = 0;
113                }
114            }
115            AdsrPhase::Sustain => {}
116            AdsrPhase::Release => {
117                self.advance_toward(0);
118                if self.samples_in_phase >= self.phase_length_samples {
119                    self.phase = AdsrPhase::Idle;
120                    self.level_q8 = 0;
121                }
122            }
123        }
124    }
125
126    fn begin_decay(&mut self) {
127        self.phase = AdsrPhase::Decay;
128        self.samples_in_phase = 0;
129        self.phase_length_samples = ms_to_samples(self.spec.decay_ms, self.sample_rate_hz);
130        if self.phase_length_samples == 0 {
131            self.phase = AdsrPhase::Sustain;
132            self.level_q8 = self.spec.sustain_q8;
133        }
134    }
135
136    fn advance_toward(&mut self, target: u8) {
137        self.samples_in_phase += 1;
138        if self.phase_length_samples == 0 {
139            self.level_q8 = target;
140            return;
141        }
142        let start = self.level_q8 as i32;
143        let end = target as i32;
144        let t = (self.samples_in_phase as i32).min(self.phase_length_samples as i32);
145        let v = start + ((end - start) * t) / self.phase_length_samples as i32;
146        self.level_q8 = v.clamp(0, 255) as u8;
147    }
148}
149
150fn ms_to_samples(ms: u16, sample_rate_hz: u32) -> u32 {
151    if ms == 0 || sample_rate_hz == 0 {
152        return 0;
153    }
154    (ms as u32 * sample_rate_hz) / 1000
155}