Skip to main content

aether_nodes/
reverb.rs

1//! Freeverb algorithmic reverb.
2//!
3//! Classic Schroeder/Moorer design: 8 comb filters + 4 allpass filters.
4//! Sounds like a real room. No heap allocation after initialization.
5//!
6//! Param layout:
7//!   0 = room size (0.0 – 1.0)
8//!   1 = damping   (0.0 – 1.0)
9//!   2 = wet       (0.0 – 1.0)
10//!   3 = width     (0.0 – 1.0, stereo spread)
11
12use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
13
14// Freeverb tuning constants (at 44.1kHz, scaled for other rates)
15const COMB_TUNING: [usize; 8] = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617];
16const ALLPASS_TUNING: [usize; 4] = [556, 441, 341, 225];
17const STEREO_SPREAD: usize = 23;
18const SCALE_ROOM: f32 = 0.28;
19const OFFSET_ROOM: f32 = 0.7;
20const SCALE_DAMP: f32 = 0.4;
21const FIXED_GAIN: f32 = 0.015;
22
23struct CombFilter {
24    buf: Vec<f32>,
25    pos: usize,
26    feedback: f32,
27    damp1: f32,
28    damp2: f32,
29    filterstore: f32,
30}
31
32impl CombFilter {
33    fn new(size: usize) -> Self {
34        Self {
35            buf: vec![0.0; size],
36            pos: 0,
37            feedback: 0.5,
38            damp1: 0.5,
39            damp2: 0.5,
40            filterstore: 0.0,
41        }
42    }
43
44    #[inline(always)]
45    fn process(&mut self, input: f32) -> f32 {
46        let output = self.buf[self.pos];
47        self.filterstore = output * self.damp2 + self.filterstore * self.damp1;
48        self.buf[self.pos] = input + self.filterstore * self.feedback;
49        self.pos = (self.pos + 1) % self.buf.len();
50        output
51    }
52
53    fn set_feedback(&mut self, v: f32) {
54        self.feedback = v;
55    }
56    fn set_damp(&mut self, v: f32) {
57        self.damp1 = v;
58        self.damp2 = 1.0 - v;
59    }
60}
61
62struct AllpassFilter {
63    buf: Vec<f32>,
64    pos: usize,
65}
66
67impl AllpassFilter {
68    fn new(size: usize) -> Self {
69        Self {
70            buf: vec![0.0; size],
71            pos: 0,
72        }
73    }
74
75    #[inline(always)]
76    fn process(&mut self, input: f32) -> f32 {
77        let bufout = self.buf[self.pos];
78        let output = -input + bufout;
79        self.buf[self.pos] = input + bufout * 0.5;
80        self.pos = (self.pos + 1) % self.buf.len();
81        output
82    }
83}
84
85pub struct Reverb {
86    comb_l: [CombFilter; 8],
87    comb_r: [CombFilter; 8],
88    allpass_l: [AllpassFilter; 4],
89    allpass_r: [AllpassFilter; 4],
90}
91
92impl Reverb {
93    pub fn new(sample_rate: f32) -> Self {
94        let scale = sample_rate / 44100.0;
95        let scaled = |base: usize| ((base as f32 * scale) as usize).max(1);
96
97        macro_rules! make_combs {
98            ($spread:expr) => {
99                [
100                    CombFilter::new(scaled(COMB_TUNING[0] + $spread)),
101                    CombFilter::new(scaled(COMB_TUNING[1] + $spread)),
102                    CombFilter::new(scaled(COMB_TUNING[2] + $spread)),
103                    CombFilter::new(scaled(COMB_TUNING[3] + $spread)),
104                    CombFilter::new(scaled(COMB_TUNING[4] + $spread)),
105                    CombFilter::new(scaled(COMB_TUNING[5] + $spread)),
106                    CombFilter::new(scaled(COMB_TUNING[6] + $spread)),
107                    CombFilter::new(scaled(COMB_TUNING[7] + $spread)),
108                ]
109            };
110        }
111
112        macro_rules! make_allpasses {
113            ($spread:expr) => {
114                [
115                    AllpassFilter::new(scaled(ALLPASS_TUNING[0] + $spread)),
116                    AllpassFilter::new(scaled(ALLPASS_TUNING[1] + $spread)),
117                    AllpassFilter::new(scaled(ALLPASS_TUNING[2] + $spread)),
118                    AllpassFilter::new(scaled(ALLPASS_TUNING[3] + $spread)),
119                ]
120            };
121        }
122
123        let mut r = Self {
124            comb_l: make_combs!(0),
125            comb_r: make_combs!(STEREO_SPREAD),
126            allpass_l: make_allpasses!(0),
127            allpass_r: make_allpasses!(STEREO_SPREAD),
128        };
129        r.set_params(0.5, 0.5);
130        r
131    }
132
133    fn set_params(&mut self, room_size: f32, damping: f32) {
134        let feedback = room_size * SCALE_ROOM + OFFSET_ROOM;
135        let damp = damping * SCALE_DAMP;
136        for c in self.comb_l.iter_mut().chain(self.comb_r.iter_mut()) {
137            c.set_feedback(feedback);
138            c.set_damp(damp);
139        }
140    }
141
142    #[inline(always)]
143    fn process_sample(&mut self, input: f32) -> (f32, f32) {
144        let input_gain = input * FIXED_GAIN;
145        let mut out_l = 0.0f32;
146        let mut out_r = 0.0f32;
147
148        for c in &mut self.comb_l {
149            out_l += c.process(input_gain);
150        }
151        for c in &mut self.comb_r {
152            out_r += c.process(input_gain);
153        }
154        for a in &mut self.allpass_l {
155            out_l = a.process(out_l);
156        }
157        for a in &mut self.allpass_r {
158            out_r = a.process(out_r);
159        }
160
161        (out_l, out_r)
162    }
163}
164
165impl DspNode for Reverb {
166    fn process(
167        &mut self,
168        inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
169        output: &mut [f32; BUFFER_SIZE],
170        params: &mut ParamBlock,
171        _sample_rate: f32,
172    ) {
173        let silence = [0.0f32; BUFFER_SIZE];
174        let input = inputs[0].unwrap_or(&silence);
175
176        for (i, out) in output.iter_mut().enumerate() {
177            let room = params.get(0).current.clamp(0.0, 1.0);
178            let damp = params.get(1).current.clamp(0.0, 1.0);
179            let wet = params.get(2).current.clamp(0.0, 1.0);
180
181            self.set_params(room, damp);
182            let (wet_l, wet_r) = self.process_sample(input[i]);
183            // Mono mix of stereo reverb output
184            *out = input[i] * (1.0 - wet) + (wet_l + wet_r) * 0.5 * wet;
185            params.tick_all();
186        }
187    }
188
189    fn type_name(&self) -> &'static str {
190        "Reverb"
191    }
192}