1#![allow(
2 clippy::cast_possible_truncation,
3 clippy::cast_precision_loss,
4 clippy::cast_sign_loss
5)]
6
7use crate::biquad::flush_denormal;
8use crate::{ProcessCtx, Processor, StateBlob, StateError};
9
10#[derive(Clone, Debug)]
12pub struct SchroederReverb {
13 room_size: f32,
14 damping: f32,
16 damping_coefficient: f32,
18 wet: f32,
19 combs: Vec<DelayLine>,
20 allpasses: Vec<DelayLine>,
21 bypass: bool,
22}
23
24const MAX_LINE_INPUT: f32 = 1.0e30;
26
27const REFERENCE_RATE: f64 = 44_100.0;
28const COMB_LENGTHS: [usize; 4] = [1116, 1188, 1277, 1356];
30const ALLPASS_LENGTHS: [usize; 2] = [556, 441];
32
33fn line_rate(rate: u32) -> u32 {
36 rate.clamp(1, crate::MAX_RESAMPLE_RATE)
37}
38
39fn line_length(length: usize, rate: u32) -> usize {
40 (length as f32 * (rate as f32 / REFERENCE_RATE as f32)).round() as usize
41}
42
43fn damping_at(damping: f32, rate: u32) -> f32 {
46 if damping > 0.0 && damping < 1.0 {
47 libm::pow(f64::from(damping), REFERENCE_RATE / f64::from(rate)) as f32
48 } else {
49 damping
50 }
51}
52
53#[derive(Clone, Debug)]
54struct DelayLine {
55 samples: Vec<f32>,
56 cursor: usize,
57 filter: f32,
58}
59
60impl DelayLine {
61 fn new(length: usize) -> Self {
62 Self {
63 samples: vec![0.0; length.max(1)],
64 cursor: 0,
65 filter: 0.0,
66 }
67 }
68 fn comb(&mut self, input: f32, feedback: f32, damping: f32) -> f32 {
69 let delayed = self.samples[self.cursor];
70 self.filter = flush_denormal(delayed * (1.0 - damping) + self.filter * damping);
71 self.samples[self.cursor] = flush_denormal(input + self.filter * feedback);
72 self.cursor = (self.cursor + 1) % self.samples.len();
73 delayed
74 }
75 fn allpass(&mut self, input: f32) -> f32 {
76 let delayed = self.samples[self.cursor];
77 let output = delayed - input;
78 self.samples[self.cursor] = flush_denormal(input + delayed * 0.5);
79 self.cursor = (self.cursor + 1) % self.samples.len();
80 output
81 }
82}
83
84impl SchroederReverb {
85 #[must_use]
88 pub fn new(room_size: f32, damping: f32, wet: f32) -> Self {
89 Self {
90 room_size: unit(room_size, 0.7),
91 damping: unit(damping, 0.3),
92 damping_coefficient: unit(damping, 0.3),
93 wet: unit(wet, 0.25),
94 combs: Vec::new(),
95 allpasses: Vec::new(),
96 bypass: false,
97 }
98 }
99 pub fn set_bypass(&mut self, bypass: bool) {
101 self.bypass = bypass;
102 }
103 #[must_use]
107 pub fn tail_samples(&self, sample_rate: u32) -> u64 {
108 let safe_rate = line_rate(sample_rate);
109 let longest = line_length(COMB_LENGTHS[COMB_LENGTHS.len() - 1], safe_rate) as f64;
110 let damping = f64::from(damping_at(self.damping, safe_rate));
111 let comb = if damping >= 1.0 {
112 longest
114 } else {
115 let feedback = f64::from(self.feedback());
116 let cycles = libm::log(1_000.0) / -libm::log(feedback);
119 longest + cycles * (longest + damping / (1.0 - damping))
120 };
121 let allpasses = ALLPASS_LENGTHS
123 .into_iter()
124 .map(|length| line_length(length, safe_rate) as f64 * 10.0)
125 .sum::<f64>();
126 libm::ceil(comb + allpasses) as u64
127 }
128 fn feedback(&self) -> f32 {
130 0.65 + self.room_size * 0.3
131 }
132}
133
134impl Processor for SchroederReverb {
135 fn history_samples(&self) -> u32 {
136 let delay = self
137 .combs
138 .iter()
139 .map(|line| line.samples.len())
140 .max()
141 .unwrap_or(0);
142 let delay = u32::try_from(delay).unwrap_or(u32::MAX);
143 let comb = if self.damping_coefficient >= 1.0 {
144 f64::from(delay)
146 } else {
147 let damping = f64::from(self.damping_coefficient);
148 let feedback = f64::from(self.feedback());
149 (f64::from(delay) + 1.0 / (1.0 - damping)) * 32.0 / (1.0 - feedback)
150 };
151 let comb = libm::ceil(comb) as u32;
152 self.allpasses.iter().fold(comb, |total, line| {
153 total.saturating_add(
154 u32::try_from(line.samples.len())
155 .unwrap_or(u32::MAX)
156 .saturating_mul(crate::processor::settling_samples(0.5)),
157 )
158 })
159 }
160
161 fn prepare(&mut self, sample_rate: u32, _max_block: usize) {
162 let safe_rate = line_rate(sample_rate);
163 self.combs = COMB_LENGTHS
164 .into_iter()
165 .map(|length| DelayLine::new(line_length(length, safe_rate)))
166 .collect();
167 self.allpasses = ALLPASS_LENGTHS
168 .into_iter()
169 .map(|length| DelayLine::new(line_length(length, safe_rate)))
170 .collect();
171 self.damping_coefficient = damping_at(self.damping, safe_rate);
172 }
173 fn process(&mut self, _ctx: &ProcessCtx, inputs: &[&[f32]], outputs: &mut [&mut [f32]]) {
174 let channels = inputs.len().min(outputs.len()).min(2);
175 let frames = inputs
176 .iter()
177 .take(channels)
178 .map(|v| v.len())
179 .chain(outputs.iter().take(channels).map(|v| v.len()))
180 .min()
181 .unwrap_or(0);
182 if self.combs.is_empty() {
183 return;
184 }
185 for frame in 0..frames {
186 let mono = inputs[..channels]
187 .iter()
188 .map(|channel| channel[frame])
189 .sum::<f32>()
190 / channels.max(1) as f32;
191 let mono = if mono.is_finite() {
194 mono.clamp(-MAX_LINE_INPUT, MAX_LINE_INPUT)
195 } else {
196 0.0
197 };
198 let (feedback, damping) = (self.feedback(), self.damping_coefficient);
199 let mut reverberant = self
200 .combs
201 .iter_mut()
202 .map(|comb| comb.comb(mono, feedback, damping))
203 .sum::<f32>()
204 / self.combs.len() as f32;
205 for allpass in &mut self.allpasses {
206 reverberant = allpass.allpass(reverberant);
207 }
208 for channel in 0..channels {
209 let input = finite_or_zero(inputs[channel][frame]);
210 outputs[channel][frame] = if self.bypass {
211 input
212 } else {
213 input * (1.0 - self.wet) + reverberant * self.wet
214 };
215 }
216 }
217 }
218 fn reset(&mut self) {
219 for delay in self.combs.iter_mut().chain(&mut self.allpasses) {
220 delay.samples.fill(0.0);
221 delay.cursor = 0;
222 delay.filter = 0.0;
223 }
224 }
225 fn save_state(&self) -> StateBlob {
226 let mut bytes = Vec::new();
227 for delay in self.combs.iter().chain(&self.allpasses) {
228 bytes.extend_from_slice(&(u64::try_from(delay.cursor).unwrap_or(0)).to_le_bytes());
229 bytes.extend_from_slice(&delay.filter.to_le_bytes());
230 for sample in &delay.samples {
231 bytes.extend_from_slice(&sample.to_le_bytes());
232 }
233 }
234 StateBlob::new(bytes)
235 }
236 fn load_state(&mut self, state: &StateBlob) -> Result<(), StateError> {
237 let bytes = state.as_bytes();
238 let expected = self
240 .combs
241 .iter()
242 .chain(&self.allpasses)
243 .map(|delay| 12 + delay.samples.len() * 4)
244 .sum::<usize>();
245 if bytes.len() != expected {
246 return Err(StateError::Mismatch);
247 }
248 let mut offset = 0;
249 for delay in self.combs.iter().chain(&self.allpasses) {
250 let cursor = usize::try_from(u64::from_le_bytes(
251 bytes[offset..offset + 8].try_into().expect("fixed range"),
252 ))
253 .ok();
254 let values = bytes[offset + 8..offset + 12 + delay.samples.len() * 4]
255 .as_chunks::<4>()
256 .0
257 .iter()
258 .map(|word| f32::from_le_bytes(*word));
259 if cursor.is_none_or(|value| value >= delay.samples.len())
260 || values.clone().any(|value| !value.is_finite())
261 {
262 return Err(StateError::Invalid);
263 }
264 offset += 12 + delay.samples.len() * 4;
265 }
266 let mut offset = 0;
267 for delay in self.combs.iter_mut().chain(&mut self.allpasses) {
268 let mut values = bytes[offset + 8..offset + 12 + delay.samples.len() * 4]
269 .as_chunks::<4>()
270 .0
271 .iter()
272 .map(|word| f32::from_le_bytes(*word));
273 delay.cursor = usize::try_from(u64::from_le_bytes(
274 bytes[offset..offset + 8].try_into().expect("fixed range"),
275 ))
276 .unwrap_or(0);
277 delay.filter = values.next().unwrap_or(0.0);
278 for (target, value) in delay.samples.iter_mut().zip(values) {
279 *target = value;
280 }
281 offset += 12 + delay.samples.len() * 4;
282 }
283 Ok(())
284 }
285}
286
287fn unit(value: f32, fallback: f32) -> f32 {
288 if value.is_finite() {
289 value.clamp(0.0, 1.0)
290 } else {
291 fallback
292 }
293}
294
295fn finite_or_zero(value: f32) -> f32 {
296 if value.is_finite() { value } else { 0.0 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::SchroederReverb;
302 use crate::{ProcessCtx, Processor, TransportState};
303 use alpha2_time::MediaTime;
304
305 fn impulse_response(room_size: f32, damping: f32, sample_rate: u32, seconds: f64) -> Vec<f32> {
307 let mut reverb = SchroederReverb::new(room_size, damping, 1.0);
308 reverb.prepare(sample_rate, 512);
309 let ctx = ProcessCtx {
310 position: MediaTime::ZERO,
311 sample_position: 0,
312 sample_rate,
313 transport: TransportState::Playing,
314 };
315 let frames = (f64::from(sample_rate) * seconds) as usize;
316 let mut input = vec![0.0; frames];
317 input[0] = 1.0;
318 let mut output = vec![0.0; frames];
319 for (input, output) in input.chunks(512).zip(output.chunks_mut(512)) {
320 reverb.process(&ctx, &[input], &mut [output]);
321 }
322 output
323 }
324
325 fn band_decay_db_per_second(response: &[f32], sample_rate: u32, frequency: f64) -> f64 {
328 let omega = core::f64::consts::TAU * frequency / f64::from(sample_rate);
329 let alpha = libm::sin(omega) / (2.0 * 1.414);
330 let a0 = 1.0 + alpha;
331 let (b0, b2, a1, a2) = (
332 alpha / a0,
333 -alpha / a0,
334 -2.0 * libm::cos(omega) / a0,
335 (1.0 - alpha) / a0,
336 );
337 let mut signal = response
338 .iter()
339 .map(|&sample| f64::from(sample))
340 .collect::<Vec<_>>();
341 for _ in 0..2 {
342 let (mut x1, mut x2, mut y1, mut y2) = (0.0, 0.0, 0.0, 0.0);
343 for sample in &mut signal {
344 let y = b0 * *sample + b2 * x2 - a1 * y1 - a2 * y2;
345 (x2, x1, y2, y1) = (x1, *sample, y1, y);
346 *sample = y;
347 }
348 }
349 let energy = |from: f64, to: f64| {
350 let rate = f64::from(sample_rate);
351 signal[(from * rate) as usize..(to * rate) as usize]
352 .iter()
353 .map(|sample| sample * sample)
354 .sum::<f64>()
355 };
356 10.0 * libm::log10(energy(0.2, 0.4) / energy(0.6, 0.8)) / 0.4
357 }
358
359 #[test]
360 fn a_room_decays_alike_in_every_band_at_any_project_rate() {
361 for (room_size, damping) in [(0.7, 0.3), (0.7, 0.5), (0.9, 0.2)] {
362 for frequency in [500.0, 5_000.0, 8_000.0] {
363 let reference = band_decay_db_per_second(
364 &impulse_response(room_size, damping, 48_000, 1.0),
365 48_000,
366 frequency,
367 );
368 for sample_rate in [44_100, 96_000] {
369 let decay = band_decay_db_per_second(
370 &impulse_response(room_size, damping, sample_rate, 1.0),
371 sample_rate,
372 frequency,
373 );
374 let difference = (decay / reference - 1.0).abs();
375 assert!(
376 difference < 0.08,
377 "room {room_size} damping {damping} at {frequency} Hz: {decay:.1} dB/s at {sample_rate} Hz, {reference:.1} at 48 kHz"
378 );
379 }
380 }
381 }
382 }
383
384 #[test]
385 fn the_tail_bound_outlasts_every_room_without_overstating_it() {
386 for sample_rate in [44_100, 48_000, 96_000] {
387 for room_size in [1.0, 0.95, 0.9, 0.5, 0.0] {
388 for damping in [0.0, 0.3, 0.7, 1.0] {
389 let response = impulse_response(room_size, damping, sample_rate, 5.5);
390 let peak = response
391 .iter()
392 .fold(0.0_f32, |peak, sample| peak.max(sample.abs()));
393 let rings = response
395 .iter()
396 .rposition(|sample| sample.abs() >= peak * 1.0e-3)
397 .expect("a response") as u64;
398 assert!(
399 rings < response.len() as u64 - u64::from(sample_rate) / 4,
400 "the response is long enough to measure"
401 );
402 let mut reverb = SchroederReverb::new(room_size, damping, 1.0);
403 let tail = reverb.tail_samples(sample_rate);
404 reverb.prepare(sample_rate, 512);
405 assert_eq!(reverb.tail_samples(sample_rate), tail, "prepared or not");
406 assert!(
407 rings <= tail,
408 "room {room_size} damping {damping} at {sample_rate} Hz rings {rings} samples, past {tail}"
409 );
410 assert!(
411 tail <= rings * 3,
412 "room {room_size} damping {damping} at {sample_rate} Hz rings {rings} samples, far under {tail}"
413 );
414 }
415 }
416 }
417 }
418
419 #[test]
420 fn a_decaying_tail_never_leaves_subnormal_state() {
421 let mut reverb = SchroederReverb::new(0.0, 0.0, 1.0);
423 reverb.prepare(48_000, 128);
424 let ctx = ProcessCtx {
425 position: MediaTime::ZERO,
426 sample_position: 0,
427 sample_rate: 48_000,
428 transport: TransportState::Playing,
429 };
430 let burst = [0.5_f32; 128];
431 let silence = [0.0_f32; 128];
432 let mut output = [0.0_f32; 128];
433 reverb.process(&ctx, &[&burst], &mut [&mut output]);
434 for block in 0..3_750 {
435 reverb.process(&ctx, &[&silence], &mut [&mut output]);
436 let subnormal = reverb
437 .combs
438 .iter()
439 .chain(&reverb.allpasses)
440 .flat_map(|line| line.samples.iter().chain([&line.filter]))
441 .find(|value| value.is_subnormal());
442 assert_eq!(subnormal, None, "block {block}");
443 }
444 }
445}