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