614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1 | //! exponential attack/release envelope follower. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 2 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 3 | use crate::util::time_to_alpha; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 4 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 5 | pub struct AttackRelease { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 6 | attack_alpha: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 7 | release_alpha: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 8 | state: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 9 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 10 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 11 | impl AttackRelease { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 12 | #[must_use] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 13 | pub fn new(attack_ms: f32, release_ms: f32, sample_rate: f32) -> Self { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 14 | Self { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 15 | attack_alpha: time_to_alpha(attack_ms, sample_rate), |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 16 | release_alpha: time_to_alpha(release_ms, sample_rate), |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 17 | state: 0.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 18 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 19 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 20 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 21 | pub fn set_times(&mut self, attack_ms: f32, release_ms: f32, sample_rate: f32) { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 22 | self.attack_alpha = time_to_alpha(attack_ms, sample_rate); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 23 | self.release_alpha = time_to_alpha(release_ms, sample_rate); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 24 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 25 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 26 | /// attack on rising input, release on falling. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 27 | pub fn process_peak(&mut self, target: f32) -> f32 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 28 | if target > self.state { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 29 | self.state += self.attack_alpha * (target - self.state); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 30 | } else { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 31 | self.state += self.release_alpha * (target - self.state); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 32 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 33 | self.state |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 34 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 35 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 36 | /// inverse of process_peak: attack on falling gain, release on rising (recover to unity). |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 37 | pub fn process_gain(&mut self, target: f32) -> f32 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 38 | if target < self.state { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 39 | self.state += self.attack_alpha * (target - self.state); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 40 | } else { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 41 | self.state += self.release_alpha * (target - self.state); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 42 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 43 | self.state |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 44 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 45 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 46 | #[must_use] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 47 | pub fn state(&self) -> f32 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 48 | self.state |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 49 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 50 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 51 | pub fn reset(&mut self, value: f32) { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 52 | self.state = value; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 53 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 54 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 55 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 56 | #[cfg(test)] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 57 | mod tests { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 58 | use super::*; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 59 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 60 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 61 | fn peak_mode_attacks_fast_releases_slow() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 62 | let fs = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 63 | let mut env = AttackRelease::new(0.1, 100.0, fs); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 64 | // Drive to 1.0 and let it settle. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 65 | for _ in 0..100 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 66 | env.process_peak(1.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 67 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 68 | assert!(env.state() > 0.99); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 69 | // Drop input to 0.0 and verify slow decay. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 70 | env.process_peak(0.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 71 | assert!(env.state() > 0.999); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 72 | for _ in 0..10 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 73 | env.process_peak(0.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 74 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 75 | // Still well above zero on the release time scale. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 76 | assert!(env.state() > 0.8); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 77 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 78 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 79 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 80 | fn gain_mode_attacks_on_drop() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 81 | let fs = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 82 | let mut env = AttackRelease::new(0.1, 100.0, fs); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 83 | env.reset(1.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 84 | // Demand a gain drop. Should snap down quickly. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 85 | for _ in 0..100 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 86 | env.process_gain(0.5); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 87 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 88 | assert!(env.state() < 0.51); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 89 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 90 | } |