Skip to main content

rescale_core/drc/
envelope.rs

1//! Envelope Follower dengan dukungan mode deteksi Peak & RMS, serta parameter Attack/Release.
2
3/// Batas bawah time constant (detik) agar perhitungan koefisien tetap stabil.
4const MIN_TIME_SECONDS: f32 = 1e-5;
5
6/// Mode deteksi level sinyal.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum DetectionMode {
9    Peak,
10    Rms,
11}
12
13#[derive(Debug, Clone)]
14pub struct EnvelopeFollower {
15    /// Koefisien smoothing pada domain amplitudo (bukan domain internal).
16    attack_coeff: f32,
17    release_coeff: f32,
18    /// Level internal: amplitudo untuk `Peak`, power untuk `Rms`.
19    current_level: f32,
20    mode: DetectionMode,
21}
22
23impl EnvelopeFollower {
24    /// Membuat Envelope Follower baru.
25    ///
26    /// # Arguments
27    /// * `sample_rate` - Frekuensi sampling sinyal (Hz)
28    /// * `attack_ms` - Waktu respon attack (ms)
29    /// * `release_ms` - Waktu pemulihan release (ms)
30    /// * `mode` - Mode estimasi (`Peak` atau `Rms`)
31    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    /// Memperbarui konstanta waktu attack dan release.
43    ///
44    /// Koefisien yang disimpan selalu berada di domain amplitudo; koreksi domain
45    /// power diterapkan saat pemrosesan (lihat `effective_coefficients`).
46    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    /// Memproses satu sampel audio dan mengembalikan envelope linear.
59    #[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, // Domain power
65        };
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    /// Koefisien smoothing satu kutub: `alpha = exp(-1 / (tau * fs))`.
87    #[inline]
88    fn smoothing_coefficient(time_seconds: f32, sample_rate: f32) -> f32 {
89        (-1.0 / (time_seconds * sample_rate)).exp()
90    }
91
92    /// Koefisien efektif untuk domain internal saat ini.
93    ///
94    /// Mode RMS menyimpan level sebagai power = amplitudo². Karena itu koefisien
95    /// amplitudo `r` harus dipakai sebagai `r²` pada domain power — tanpa koreksi
96    /// ini, release RMS berjalan ~2× lebih lambat dari nilai yang dikonfigurasi
97    /// (power meluruh dua kali lebih cepat daripada amplitudo).
98    #[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    /// Konversi level internal ke amplitudo linear.
110    #[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; // 100 ms @ 1 kHz = 100 sampel
126
127    /// Mengukur waktu (dalam sampel) sampai envelope turun ke `1/e` dari nilai awal
128    /// setelah sinyal input berhenti.
129    fn measured_release_samples(mode: DetectionMode) -> f32 {
130        let mut envelope = EnvelopeFollower::new(SAMPLE_RATE, 1.0, RELEASE_MS, mode);
131
132        // Settle: dorong envelope ke level stabil 1.0.
133        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        // RMS dari sine amplitudo 1.0 adalah 1/sqrt(2) ~= 0.707; envelope RMS
174        // yang di-smoothing menuju nilai absolut rata-rata yang sama.
175        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}