rescale_core/drc/
envelope.rs1const MIN_TIME_SECONDS: f32 = 1e-5;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum DetectionMode {
9 Peak,
10 Rms,
11}
12
13#[derive(Debug, Clone)]
14pub struct EnvelopeFollower {
15 attack_coeff: f32,
17 release_coeff: f32,
18 current_level: f32,
20 mode: DetectionMode,
21}
22
23impl EnvelopeFollower {
24 pub fn new(sample_rate: f32, attack_ms: f32, release_ms: f32, mode: DetectionMode) -> Self {
32 let mut follower = Self {
33 attack_coeff: 0.0,
34 release_coeff: 0.0,
35 current_level: 0.0,
36 mode,
37 };
38 follower.set_time_constants(sample_rate, attack_ms, release_ms);
39 follower
40 }
41
42 pub fn set_time_constants(&mut self, sample_rate: f32, attack_ms: f32, release_ms: f32) {
47 let attack_seconds = (attack_ms * 0.001).max(MIN_TIME_SECONDS);
48 let release_seconds = (release_ms * 0.001).max(MIN_TIME_SECONDS);
49
50 self.attack_coeff = Self::smoothing_coefficient(attack_seconds, sample_rate);
51 self.release_coeff = Self::smoothing_coefficient(release_seconds, sample_rate);
52 }
53
54 pub fn set_mode(&mut self, mode: DetectionMode) {
55 self.mode = mode;
56 }
57
58 #[inline(always)]
60 pub fn process_sample(&mut self, input: f32) -> f32 {
61 let (attack_coeff, release_coeff) = self.effective_coefficients();
62 let target = match self.mode {
63 DetectionMode::Peak => input.abs(),
64 DetectionMode::Rms => input * input, };
66
67 let coefficient = if target > self.current_level {
68 attack_coeff
69 } else {
70 release_coeff
71 };
72 self.current_level = coefficient * self.current_level + (1.0 - coefficient) * target;
73
74 self.to_linear()
75 }
76
77 #[inline(always)]
78 pub fn current_level(&self) -> f32 {
79 self.to_linear()
80 }
81
82 pub fn reset(&mut self) {
83 self.current_level = 0.0;
84 }
85
86 #[inline]
88 fn smoothing_coefficient(time_seconds: f32, sample_rate: f32) -> f32 {
89 (-1.0 / (time_seconds * sample_rate)).exp()
90 }
91
92 #[inline(always)]
99 fn effective_coefficients(&self) -> (f32, f32) {
100 match self.mode {
101 DetectionMode::Peak => (self.attack_coeff, self.release_coeff),
102 DetectionMode::Rms => (
103 self.attack_coeff * self.attack_coeff,
104 self.release_coeff * self.release_coeff,
105 ),
106 }
107 }
108
109 #[inline(always)]
111 fn to_linear(&self) -> f32 {
112 match self.mode {
113 DetectionMode::Peak => self.current_level,
114 DetectionMode::Rms => self.current_level.sqrt(),
115 }
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 const SAMPLE_RATE: f32 = 1_000.0;
124 const RELEASE_MS: f32 = 100.0;
125 const TARGET_TIME_CONSTANT_SAMPLES: f32 = RELEASE_MS; fn measured_release_samples(mode: DetectionMode) -> f32 {
130 let mut envelope = EnvelopeFollower::new(SAMPLE_RATE, 1.0, RELEASE_MS, mode);
131
132 for _ in 0..500 {
134 envelope.process_sample(1.0);
135 }
136
137 let settled_level = envelope.process_sample(0.0);
138 let threshold = settled_level / std::f32::consts::E;
139
140 let mut samples = 0_u32;
141 loop {
142 let level = envelope.process_sample(0.0);
143 samples += 1;
144 if level <= threshold || samples > 10 * TARGET_TIME_CONSTANT_SAMPLES as u32 {
145 break;
146 }
147 }
148 samples as f32
149 }
150
151 #[test]
152 fn rms_release_time_matches_configured_value() {
153 let measured = measured_release_samples(DetectionMode::Rms);
154 assert!(
155 (measured - TARGET_TIME_CONSTANT_SAMPLES).abs() <= 5.0,
156 "release RMS {measured:.1} sampel, seharusnya ~{TARGET_TIME_CONSTANT_SAMPLES:.1}"
157 );
158 }
159
160 #[test]
161 fn peak_release_time_matches_configured_value() {
162 let measured = measured_release_samples(DetectionMode::Peak);
163 assert!(
164 (measured - TARGET_TIME_CONSTANT_SAMPLES).abs() <= 5.0,
165 "release Peak {measured:.1} sampel, seharusnya ~{TARGET_TIME_CONSTANT_SAMPLES:.1}"
166 );
167 }
168
169 #[test]
170 fn rms_level_of_full_scale_sine_stays_near_unity() {
171 let mut envelope = EnvelopeFollower::new(SAMPLE_RATE, 1.0, RELEASE_MS, DetectionMode::Rms);
172
173 for _ in 0..2_000 {
176 envelope.process_sample(1.0);
177 }
178
179 assert!(
180 envelope.current_level() > 0.95,
181 "RMS envelope untuk input DC 1.0 seharusnya mendekati 1.0, dapat {}",
182 envelope.current_level()
183 );
184 }
185}