rescale_core/drc/
limiter.rs1use super::max_hold::SlidingMax;
10use crate::buffer::delay_line::{DelayLine, InterpolationType};
11use crate::gain::Gain;
12
13const MIN_CEILING_LINEAR: f32 = 1e-6;
15const DELAY_CAPACITY_HEADROOM: usize = 8;
17const MIN_RELEASE_MS: f32 = 0.1;
19
20pub struct LookaheadDetector {
25 ceiling_linear: f32,
26 lookahead_samples: usize,
27 max_peak: SlidingMax,
28 current_gain: f32,
29 release_coeff: f32,
30 hold_frames_remaining: usize,
31}
32
33impl LookaheadDetector {
34 pub fn new(sample_rate: f32, ceiling_db: f32, release_ms: f32, lookahead_ms: f32) -> Self {
40 let ceiling_linear = Gain::db_to_linear(ceiling_db.min(0.0));
41 let lookahead_samples = ((lookahead_ms * 0.001 * sample_rate).round() as usize).max(1);
42 let release_seconds = (release_ms.max(MIN_RELEASE_MS) * 0.001).max(MIN_RELEASE_MS * 0.001);
43 let release_coeff = (-1.0 / (release_seconds * sample_rate)).exp();
44
45 Self {
46 ceiling_linear,
47 lookahead_samples,
48 max_peak: SlidingMax::new(lookahead_samples + 1),
50 current_gain: 1.0,
51 release_coeff,
52 hold_frames_remaining: 0,
53 }
54 }
55
56 pub fn latency_samples(&self) -> usize {
58 self.lookahead_samples
59 }
60
61 pub fn current_gain(&self) -> f32 {
62 self.current_gain
63 }
64
65 pub fn ceiling_linear(&self) -> f32 {
66 self.ceiling_linear
67 }
68
69 pub fn process_input_peak(&mut self, input_peak: f32) -> f32 {
76 let peak = if input_peak.is_finite() {
77 input_peak.abs()
78 } else {
79 0.0
80 };
81 self.max_peak.push(peak);
82
83 let window_peak = self.max_peak.max();
84 let target_gain =
85 if window_peak > self.ceiling_linear && self.ceiling_linear > MIN_CEILING_LINEAR {
86 self.ceiling_linear / window_peak
87 } else {
88 1.0
89 };
90
91 if target_gain < self.current_gain {
92 self.current_gain = target_gain;
93 self.hold_frames_remaining = self.lookahead_samples;
94 } else if self.hold_frames_remaining > 0 {
95 self.hold_frames_remaining -= 1;
96 } else {
97 self.current_gain =
98 self.release_coeff * self.current_gain + (1.0 - self.release_coeff) * target_gain;
99 }
100
101 self.current_gain.clamp(0.0, 1.0)
102 }
103
104 pub fn reset(&mut self) {
105 self.max_peak.reset();
106 self.current_gain = 1.0;
107 self.hold_frames_remaining = 0;
108 }
109}
110
111pub struct BrickwallLimiter {
113 ceiling_db: f32,
114 ceiling_linear: f32,
115 lookahead_delay: DelayLine,
116 detector: LookaheadDetector,
117}
118
119impl BrickwallLimiter {
120 pub fn new(sample_rate: f32, ceiling_db: f32, release_ms: f32, lookahead_ms: f32) -> Self {
128 let detector = LookaheadDetector::new(sample_rate, ceiling_db, release_ms, lookahead_ms);
129 Self {
130 ceiling_db,
131 ceiling_linear: detector.ceiling_linear(),
132 lookahead_delay: DelayLine::new(detector.latency_samples() + DELAY_CAPACITY_HEADROOM),
133 detector,
134 }
135 }
136
137 pub fn ceiling_db(&self) -> f32 {
138 self.ceiling_db
139 }
140
141 pub fn ceiling_linear(&self) -> f32 {
142 self.ceiling_linear
143 }
144
145 pub fn latency_samples(&self) -> usize {
146 self.detector.latency_samples()
147 }
148
149 #[inline(always)]
151 pub fn process_sample(&mut self, input: f32) -> f32 {
152 let delay_samples = self.detector.latency_samples() as f32;
153 self.lookahead_delay.push(input);
154 let delayed_input = self
155 .lookahead_delay
156 .read_delay(delay_samples, InterpolationType::None);
157 let gain = self.detector.process_input_peak(input);
158 let limited_output = delayed_input * gain;
159
160 limited_output.clamp(-self.ceiling_linear, self.ceiling_linear)
162 }
163
164 pub fn process_buffer(&mut self, buffer: &mut [f32]) {
166 for sample in buffer.iter_mut() {
167 *sample = self.process_sample(*sample);
168 }
169 }
170
171 pub fn reset(&mut self) {
172 self.lookahead_delay.clear();
173 self.detector.reset();
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 const SAMPLE_RATE: f32 = 44_100.0;
182 const CEILING_DB: f32 = -1.0;
183 const LOOKAHEAD_MS: f32 = 2.0;
184
185 fn ceiling_linear() -> f32 {
186 Gain::db_to_linear(CEILING_DB)
187 }
188
189 fn lookahead_samples() -> usize {
190 (LOOKAHEAD_MS * 0.001 * SAMPLE_RATE).round() as usize
191 }
192
193 #[test]
194 fn lookahead_detector_gain_matches_window_peak() {
195 let mut detector = LookaheadDetector::new(1_000.0, Gain::linear_to_db(0.5), 50.0, 4.0);
197
198 assert!((detector.process_input_peak(0.2) - 1.0).abs() < 1e-6);
199 assert!((detector.process_input_peak(0.4) - 1.0).abs() < 1e-6);
200 assert!((detector.process_input_peak(0.8) - 0.625).abs() < 1e-6);
201 }
202
203 #[test]
204 fn percussive_transients_never_exceed_ceiling() {
205 let mut limiter = BrickwallLimiter::new(SAMPLE_RATE, CEILING_DB, 50.0, LOOKAHEAD_MS);
206 let mut signal = vec![0.0_f32; 2_000];
207 for index in [100, 400, 401, 900, 1_500] {
208 signal[index] = 0.95;
209 }
210
211 limiter.process_buffer(&mut signal);
212
213 let peak = signal
214 .iter()
215 .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
216 assert!(
217 peak <= ceiling_linear() + 1e-6,
218 "peak {peak} melebihi ceiling {}",
219 ceiling_linear()
220 );
221 }
222
223 #[test]
224 fn gain_reduction_is_active_before_the_transient_reaches_output() {
225 let mut limiter = BrickwallLimiter::new(SAMPLE_RATE, CEILING_DB, 50.0, LOOKAHEAD_MS);
226 let latency = lookahead_samples();
227 let quiet_level = 0.1_f32;
228 let burst_level = 0.95_f32;
229 let burst_index = 200;
230
231 let mut signal = vec![quiet_level; 1_000];
232 signal[burst_index] = burst_level;
233 limiter.process_buffer(&mut signal);
234
235 assert!(
237 (signal[100] - quiet_level).abs() < 1e-4,
238 "sampel awal tidak boleh teredam, dapat {}",
239 signal[100]
240 );
241 let attenuated = quiet_level * (ceiling_linear() / burst_level);
243 assert!(
244 (signal[burst_index + 40] - attenuated).abs() < 1e-3,
245 "gain reduction harus aktif sebelum transient mencapai output, dapat {}",
246 signal[burst_index + 40]
247 );
248 assert!(
250 (signal[burst_index + latency] - ceiling_linear()).abs() < 1e-4,
251 "transient harus keluar tepat di ceiling, dapat {}",
252 signal[burst_index + latency]
253 );
254 }
255
256 #[test]
257 fn reset_restores_unity_gain() {
258 let mut limiter = BrickwallLimiter::new(SAMPLE_RATE, CEILING_DB, 50.0, LOOKAHEAD_MS);
259 let mut hot = vec![0.95_f32; 200];
260 limiter.process_buffer(&mut hot);
261 limiter.reset();
262
263 let mut quiet = vec![0.0_f32; 200];
264 limiter.process_buffer(&mut quiet);
265 assert!(quiet.iter().all(|&sample| sample.abs() < 1e-6));
266 }
267}