Skip to main content

math_audio_dsp/
envelope_follower.rs

1// ============================================================================
2// Envelope Follower — Attack/Release envelope for modulation signals
3// ============================================================================
4//
5// Produces a smooth envelope from an audio signal's amplitude. Used for:
6// - Dynamic saturation (drive modulated by input level)
7// - Adaptive thresholds in spectral processors
8// - Any DSP where a control signal tracks audio energy
9//
10// Uses branching attack/release: attack coefficient when input > envelope,
11// release coefficient when input < envelope.
12//
13// HARD RULES:
14// - No allocations in process()
15// - All state is f32 for cache efficiency
16
17/// Attack-release envelope follower producing a smooth modulation signal.
18#[derive(Debug, Clone, Copy)]
19pub struct EnvelopeFollower {
20    envelope: f32,
21    attack_coeff: f32,
22    release_coeff: f32,
23}
24
25impl EnvelopeFollower {
26    /// Create a new envelope follower.
27    ///
28    /// `attack_ms`: Time to reach ~63% of a step increase.
29    /// `release_ms`: Time to decay to ~37% of a step decrease.
30    pub fn new(attack_ms: f32, release_ms: f32, sample_rate: u32) -> Self {
31        Self {
32            envelope: 0.0,
33            attack_coeff: Self::ms_to_coeff(attack_ms, sample_rate),
34            release_coeff: Self::ms_to_coeff(release_ms, sample_rate),
35        }
36    }
37
38    fn ms_to_coeff(time_ms: f32, sample_rate: u32) -> f32 {
39        if time_ms <= 0.0 {
40            return 0.0;
41        }
42        (-1.0 / (time_ms * 0.001 * sample_rate as f32)).exp()
43    }
44
45    /// Process one sample (provide absolute value of input).
46    ///
47    /// Returns the current envelope value. NaN/inf inputs are treated as zero
48    /// to prevent permanent envelope corruption.
49    #[inline]
50    pub fn process(&mut self, input_abs: f32) -> f32 {
51        let input_abs = if input_abs.is_finite() {
52            input_abs
53        } else {
54            0.0
55        };
56        let coeff = if input_abs > self.envelope {
57            self.attack_coeff
58        } else {
59            self.release_coeff
60        };
61        self.envelope = input_abs + coeff * (self.envelope - input_abs);
62        self.envelope
63    }
64
65    /// Process a block and return the final envelope value.
66    ///
67    /// Useful when you only need the envelope at block boundaries.
68    #[inline]
69    pub fn process_block(&mut self, samples: &[f32]) -> f32 {
70        for &sample in samples {
71            self.process(sample.abs());
72        }
73        self.envelope
74    }
75
76    /// Update attack/release times.
77    pub fn set_times(&mut self, attack_ms: f32, release_ms: f32, sample_rate: u32) {
78        self.attack_coeff = Self::ms_to_coeff(attack_ms, sample_rate);
79        self.release_coeff = Self::ms_to_coeff(release_ms, sample_rate);
80    }
81
82    /// Reset envelope to zero.
83    pub fn reset(&mut self) {
84        self.envelope = 0.0;
85    }
86
87    /// Get current envelope value without processing.
88    #[inline]
89    pub fn current(&self) -> f32 {
90        self.envelope
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_attack_from_silence() {
100        let mut env = EnvelopeFollower::new(10.0, 100.0, 48000);
101        // Feed constant 1.0 — envelope should rise
102        let mut prev = 0.0;
103        for _ in 0..480 {
104            // 10ms
105            let val = env.process(1.0);
106            assert!(val >= prev);
107            prev = val;
108        }
109        // After 10ms (one time constant), should be ~63% of target
110        assert!(prev > 0.5, "Envelope too slow: {prev}");
111        assert!(prev < 0.8, "Envelope too fast: {prev}");
112    }
113
114    #[test]
115    fn test_release_from_peak() {
116        let mut env = EnvelopeFollower::new(0.1, 50.0, 48000);
117        // Instant attack
118        for _ in 0..48 {
119            env.process(1.0);
120        }
121        let peak = env.current();
122        assert!(peak > 0.9);
123
124        // Now release — feed silence
125        for _ in 0..2400 {
126            // 50ms
127            env.process(0.0);
128        }
129        let after_release = env.current();
130        assert!(after_release < 0.4, "Release too slow: {after_release}");
131    }
132
133    #[test]
134    fn test_process_block() {
135        let mut env = EnvelopeFollower::new(1.0, 100.0, 48000);
136        let block: Vec<f32> = (0..480).map(|_| 0.5).collect();
137        let result = env.process_block(&block);
138        assert!(result > 0.3);
139    }
140
141    #[test]
142    fn test_reset() {
143        let mut env = EnvelopeFollower::new(1.0, 100.0, 48000);
144        env.process(1.0);
145        assert!(env.current() > 0.0);
146        env.reset();
147        assert_eq!(env.current(), 0.0);
148    }
149
150    #[test]
151    fn test_zero_attack_time() {
152        let mut env = EnvelopeFollower::new(0.0, 100.0, 48000);
153        // Zero attack = instant tracking
154        let val = env.process(0.75);
155        assert!((val - 0.75).abs() < 1e-6);
156    }
157}