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