Skip to main content

rescale_core/drc/
max_hold.rs

1//! Pelacak nilai maksimum bergerak (sliding maximum) zero-allocation.
2//!
3//! Dipakai limiter lookahead untuk mengetahui puncak sinyal di seluruh window
4//! lookahead, bukan hanya puncak sesaat dari envelope.
5
6/// Antrean monotonik untuk maksimum bergerak pada window tetap.
7///
8/// Setiap sample di-`push`, dan [`SlidingMax::max`] mengembalikan nilai terbesar
9/// dari `window_len` sample terakhir. Biaya O(1) amortisasi per sample dan tidak
10/// ada alokasi memori setelah konstruksi.
11pub struct SlidingMax {
12    window_len: usize,
13    /// Ring nilai per indeks global (`index % window_len`).
14    values: Vec<f32>,
15    /// Ring indeks global yang nilainya monotonik menurun dari depan ke belakang.
16    deque: Vec<usize>,
17    deque_head: usize,
18    deque_len: usize,
19    position: usize,
20}
21
22impl SlidingMax {
23    /// Membuat pelacak dengan panjang window `window_len` (minimal 1).
24    pub fn new(window_len: usize) -> Self {
25        assert!(window_len > 0, "window_len must be greater than 0");
26        Self {
27            window_len,
28            values: vec![0.0; window_len],
29            deque: vec![0; window_len],
30            deque_head: 0,
31            deque_len: 0,
32            position: 0,
33        }
34    }
35
36    /// Memasukkan satu sample baru ke window.
37    pub fn push(&mut self, value: f32) {
38        let index = self.position;
39        self.position += 1;
40        self.values[index % self.window_len] = value;
41
42        self.evict_expired(index);
43        self.evict_smaller_from_back(value);
44
45        let tail = (self.deque_head + self.deque_len) % self.window_len;
46        self.deque[tail] = index;
47        self.deque_len += 1;
48    }
49
50    /// Maksimum pada window saat ini.
51    pub fn max(&self) -> f32 {
52        self.values[self.deque[self.deque_head] % self.window_len]
53    }
54
55    /// Mengosongkan state tanpa mengubah kapasitas.
56    pub fn reset(&mut self) {
57        self.position = 0;
58        self.deque_head = 0;
59        self.deque_len = 0;
60    }
61
62    /// Indeks yang sudah keluar dari window `[position - window_len, position)`.
63    fn evict_expired(&mut self, newest_index: usize) {
64        while self.deque_len > 0 {
65            let front_index = self.deque[self.deque_head];
66            if newest_index - front_index < self.window_len {
67                break;
68            }
69            self.deque_head = (self.deque_head + 1) % self.window_len;
70            self.deque_len -= 1;
71        }
72    }
73
74    /// Nilai lebih kecil di belakang tidak akan pernah menjadi maksimum lagi.
75    fn evict_smaller_from_back(&mut self, value: f32) {
76        while self.deque_len > 0 {
77            let back_index = self.deque[(self.deque_head + self.deque_len - 1) % self.window_len];
78            if self.values[back_index % self.window_len] > value {
79                break;
80            }
81            self.deque_len -= 1;
82        }
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    fn push_all(tracker: &mut SlidingMax, values: &[f32]) -> f32 {
91        for &value in values {
92            tracker.push(value);
93        }
94        tracker.max()
95    }
96
97    #[test]
98    fn tracks_maximum_inside_the_window() {
99        let mut tracker = SlidingMax::new(3);
100
101        assert!((push_all(&mut tracker, &[1.0, 3.0, 2.0]) - 3.0).abs() < 1e-6);
102        // Window 3 sample terakhir: {3.0, 2.0, 0.5} -> maksimum tetap 3.0.
103        tracker.push(0.5);
104        assert!((tracker.max() - 3.0).abs() < 1e-6);
105        // 3.0 keluar window: {2.0, 0.5, 0.1} -> maksimum 2.0.
106        tracker.push(0.1);
107        assert!((tracker.max() - 2.0).abs() < 1e-6);
108        // {0.5, 0.1, 0.0} -> maksimum 0.5.
109        tracker.push(0.0);
110        assert!((tracker.max() - 0.5).abs() < 1e-6);
111        // {0.1, 0.0, 0.0} -> maksimum 0.1.
112        tracker.push(0.0);
113        assert!((tracker.max() - 0.1).abs() < 1e-6);
114    }
115
116    #[test]
117    fn maximum_matches_brute_force_for_random_sequence() {
118        let mut tracker = SlidingMax::new(5);
119        let values: Vec<f32> = (0..200)
120            .map(|index| ((index * 37 % 101) as f32 / 101.0).abs())
121            .collect();
122
123        for index in 0..values.len() {
124            tracker.push(values[index]);
125            let window_start = (index + 1).saturating_sub(5);
126            let expected = values[window_start..=index]
127                .iter()
128                .fold(0.0_f32, |acc, &value| acc.max(value));
129            assert!(
130                (tracker.max() - expected).abs() < 1e-6,
131                "index {index}: {} vs {expected}",
132                tracker.max()
133            );
134        }
135    }
136
137    #[test]
138    fn reset_restarts_the_window() {
139        let mut tracker = SlidingMax::new(2);
140        tracker.push(1.0);
141        tracker.push(0.0);
142        tracker.reset();
143        tracker.push(0.25);
144        assert!((tracker.max() - 0.25).abs() < 1e-6);
145    }
146}