1pub struct BrickwallLimiter {
9 ceiling: f64,
11 delay_buf: Vec<f64>,
13 write_pos: usize,
15 lookahead_samples: usize,
17 gain: f64,
19 release_coeff: f64,
21 channels: usize,
23}
24
25impl BrickwallLimiter {
26 pub fn new(
34 ceiling_db: f64,
35 lookahead_ms: f64,
36 release_ms: f64,
37 sample_rate: u32,
38 channels: usize,
39 ) -> Self {
40 let ceiling = 10.0_f64.powf(ceiling_db / 20.0);
41 let lookahead_samples = ((lookahead_ms / 1000.0) * sample_rate as f64).ceil() as usize;
42 let release_coeff = (-1.0 / ((release_ms / 1000.0) * sample_rate as f64)).exp();
43 let buf_size = lookahead_samples * channels;
44
45 Self {
46 ceiling,
47 delay_buf: vec![0.0; buf_size],
48 write_pos: 0,
49 lookahead_samples,
50 gain: 1.0,
51 release_coeff,
52 channels,
53 }
54 }
55
56 pub fn default_safe(sample_rate: u32, channels: usize) -> Self {
58 Self::new(-1.0, 1.0, 50.0, sample_rate, channels)
59 }
60
61 pub fn process(&mut self, data: &mut [f64], frames: usize) {
65 let ch = self.channels;
66
67 for frame in 0..frames {
68 let offset = frame * ch;
69
70 let mut peak = 0.0_f64;
72 for c in 0..ch {
73 let idx = offset + c;
74 if idx < data.len() {
75 peak = peak.max(data[idx].abs());
76 }
77 }
78
79 let target_gain = if peak > self.ceiling {
81 self.ceiling / peak
82 } else {
83 1.0
84 };
85
86 if target_gain < self.gain {
88 self.gain = target_gain;
90 } else {
91 self.gain = self.gain * self.release_coeff + target_gain * (1.0 - self.release_coeff);
93 }
94
95 for c in 0..ch {
97 let buf_idx = self.write_pos * ch + c;
98 let data_idx = offset + c;
99 if data_idx < data.len() && buf_idx < self.delay_buf.len() {
100 let delayed = self.delay_buf[buf_idx];
102 data[data_idx] = delayed * self.gain;
103 self.delay_buf[buf_idx] = data[data_idx + 0]; }
106 }
107
108 }
111 }
112
113 pub fn process_block(&mut self, input: &[f64], output: &mut [f64], frames: usize) {
117 let ch = self.channels;
118 let la = self.lookahead_samples;
119
120 for frame in 0..frames {
121 let offset = frame * ch;
122
123 let mut peak = 0.0_f64;
125 for c in 0..ch {
126 let idx = offset + c;
127 if idx < input.len() {
128 peak = peak.max(input[idx].abs());
129 }
130 }
131
132 let target_gain = if peak > self.ceiling {
134 self.ceiling / peak
135 } else {
136 1.0
137 };
138
139 if target_gain < self.gain {
141 self.gain = target_gain;
142 } else {
143 self.gain = self.gain * self.release_coeff + target_gain * (1.0 - self.release_coeff);
144 }
145
146 for c in 0..ch {
148 let buf_idx = self.write_pos * ch + c;
149 let data_idx = offset + c;
150
151 if data_idx < input.len() && buf_idx < self.delay_buf.len() {
152 let delayed = self.delay_buf[buf_idx];
154 self.delay_buf[buf_idx] = input[data_idx];
156 if data_idx < output.len() {
158 output[data_idx] = delayed * self.gain;
159 }
160 }
161 }
162
163 self.write_pos = (self.write_pos + 1) % la.max(1);
165 }
166 }
167
168 pub fn gain_reduction_db(&self) -> f64 {
170 if self.gain >= 1.0 { 0.0 } else { 20.0 * self.gain.log10() }
171 }
172
173 pub fn ceiling_db(&self) -> f64 {
175 20.0 * self.ceiling.log10()
176 }
177
178 pub fn reset(&mut self) {
180 self.gain = 1.0;
181 for s in self.delay_buf.iter_mut() { *s = 0.0; }
182 self.write_pos = 0;
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn test_below_ceiling_passes_through() {
192 let mut lim = BrickwallLimiter::default_safe(44100, 2);
193 let input = vec![0.5, -0.5, 0.3, -0.3]; let mut output = vec![0.0; 4];
195 lim.process_block(&input, &mut output, 2);
198 let mut output2 = vec![0.0; 4];
199 lim.process_block(&input, &mut output2, 2);
200 for s in &output2 {
203 assert!(s.abs() <= 1.0, "Output should be below ceiling");
204 }
205 }
206
207 #[test]
208 fn test_above_ceiling_gets_limited() {
209 let mut lim = BrickwallLimiter::new(-1.0, 0.0, 50.0, 44100, 2);
210 let hot_signal: Vec<f64> = (0..128).map(|_| 2.0).collect(); let mut output = vec![0.0; 128];
213 lim.process_block(&hot_signal, &mut output, 64);
214 let ceiling_linear = 10.0_f64.powf(-1.0 / 20.0);
215 for &s in &output {
216 assert!(
217 s.abs() <= ceiling_linear + 0.01,
218 "Sample {} exceeds ceiling {}", s, ceiling_linear
219 );
220 }
221 }
222
223 #[test]
224 fn test_gain_reduction_reports() {
225 let mut lim = BrickwallLimiter::new(-1.0, 0.0, 50.0, 44100, 2);
226 let hot = vec![2.0, 2.0];
227 let mut out = vec![0.0; 2];
228 lim.process_block(&hot, &mut out, 1);
229 assert!(lim.gain_reduction_db() < 0.0, "Should report gain reduction");
230 }
231
232 #[test]
233 fn test_silence_no_reduction() {
234 let mut lim = BrickwallLimiter::default_safe(44100, 2);
235 let silence = vec![0.0; 128];
236 let mut out = vec![0.0; 128];
237 lim.process_block(&silence, &mut out, 64);
238 assert!((lim.gain_reduction_db() - 0.0).abs() < 0.01);
239 }
240
241 #[test]
242 fn test_reset_clears_state() {
243 let mut lim = BrickwallLimiter::new(-1.0, 0.0, 50.0, 44100, 2);
244 let hot = vec![5.0, 5.0];
245 let mut out = vec![0.0; 2];
246 lim.process_block(&hot, &mut out, 1);
247 assert!(lim.gain < 1.0);
248 lim.reset();
249 assert!((lim.gain - 1.0).abs() < 1e-10);
250 }
251
252 #[test]
253 fn test_ceiling_db_correct() {
254 let lim = BrickwallLimiter::new(-3.0, 1.0, 50.0, 44100, 2);
255 assert!((lim.ceiling_db() - (-3.0)).abs() < 0.01);
256 }
257
258 #[test]
259 fn test_speaker_protection_extreme() {
260 let mut lim = BrickwallLimiter::new(-1.0, 0.0, 50.0, 44100, 2);
262 let extreme: Vec<f64> = (0..256).map(|_| 100.0).collect();
263 let mut out = vec![0.0; 256];
264 lim.process_block(&extreme, &mut out, 128);
265 let ceiling_linear = 10.0_f64.powf(-1.0 / 20.0);
266 for &s in &out {
267 assert!(
268 s.abs() <= ceiling_linear + 0.01,
269 "100x overdrive: {} exceeds ceiling", s
270 );
271 }
272 }
273}