Skip to main content

fyrox_sound/effects/
reverb.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Reverberation module
22//!
23//! # Overview
24//!
25//! This is implementation of [Freeverb reverb effect](https://ccrma.stanford.edu/~jos/pasp/Freeverb.html)
26//! Reverberation gives you scene a "volume" and improves overall perception of sound.
27//!
28//! # Usage
29//!
30//! ```
31//! use std::time::Duration;
32//! use fyrox_sound::context::SoundContext;
33//! use fyrox_sound::effects::reverb::Reverb;
34//! use fyrox_sound::effects::Effect;
35//!
36//! fn set_reverberator(context: &mut SoundContext) {
37//!     let mut reverb = Reverb::new();
38//!     reverb.set_decay_time(10.0);
39//!     context.state().bus_graph_mut().primary_bus_mut().add_effect(Effect::Reverb(reverb));
40//! }
41//! ```
42//!
43//! # Known problems
44//!
45//! This reverberator has little "metallic" tone, but since this is one of the simplest reverberators this
46//! is acceptable. To remove this effect, more complex reverberator should be implemented.
47
48use crate::{
49    dsp::filters::{AllPass, LpfComb},
50    effects::EffectRenderTrait,
51};
52use fyrox_core::{reflect::prelude::*, visitor::prelude::*};
53
54#[derive(Default, Debug, Clone, PartialEq, Visit)]
55struct ChannelReverb {
56    fc: f32,
57    sample_rate: u32,
58    stereo_spread: u32,
59    lp_fb_comb_filters: Vec<LpfComb>,
60    all_pass_filters: Vec<AllPass>,
61}
62
63/// 60 decibels
64const DB60: f32 = 0.001;
65
66/// Sample rate for which this reverb was designed.
67const DESIGN_SAMPLE_RATE: u32 = 44100;
68
69fn calculate_decay(len: usize, sample_rate: u32, decay_time: f32) -> f32 {
70    let time_len = len as f32 / sample_rate as f32;
71    // Asymptotically goes to 1.0 by exponential law
72    DB60.powf(time_len / decay_time)
73}
74
75impl ChannelReverb {
76    /// Total amount of allpass + comb filters per channel
77    const TOTAL_FILTERS_COUNT: f32 = 4.0 + 8.0;
78
79    /// Accumulated gain of all sources of signal (all filters, delay lines, etc.)
80    /// it is used to scale input samples and prevent multiplication of signal gain
81    /// too much which will cause signal overflow.
82    ///
83    /// 2.0 here because left and right signals will be mixed together.
84    const GAIN: f32 = 1.0 / (2.0 * Self::TOTAL_FILTERS_COUNT);
85
86    /// Filter lengths given in samples
87    const COMB_LENGTHS: [usize; 8] = [1557, 1617, 1491, 1422, 1277, 1356, 1188, 1116];
88    const ALLPASS_LENGTHS: [usize; 4] = [225, 556, 441, 341];
89
90    fn new(stereo_spread: u32, fc: f32, feedback: f32, decay_time: f32) -> Self {
91        let mut reverb = Self {
92            fc,
93            stereo_spread,
94            sample_rate: DESIGN_SAMPLE_RATE,
95            lp_fb_comb_filters: Self::COMB_LENGTHS
96                .iter()
97                .map(|len| LpfComb::new(*len + stereo_spread as usize, fc, feedback))
98                .collect(),
99            all_pass_filters: Self::ALLPASS_LENGTHS
100                .iter()
101                .map(|len| AllPass::new(*len + stereo_spread as usize, 0.5))
102                .collect(),
103        };
104
105        reverb.set_decay_time(decay_time);
106
107        reverb
108    }
109
110    fn set_sample_rate(&mut self, sample_rate: usize) {
111        let scale = sample_rate as f32 / DESIGN_SAMPLE_RATE as f32;
112
113        let feedback = self.lp_fb_comb_filters[0].feedback();
114        // TODO: According to many papers delay line lengths should be prime numbers to
115        //       remove metallic ringing effect. But still not sure why then initial lengths
116        //       are not 100% prime, for example 1422 is not prime number.
117        self.lp_fb_comb_filters = Self::COMB_LENGTHS
118            .iter()
119            .map(|len| {
120                LpfComb::new(
121                    (scale * (*len) as f32) as usize + self.stereo_spread as usize,
122                    self.fc,
123                    feedback,
124                )
125            })
126            .collect();
127        self.all_pass_filters = Self::ALLPASS_LENGTHS
128            .iter()
129            .map(|len| {
130                AllPass::new(
131                    (scale * (*len) as f32) as usize + self.stereo_spread as usize,
132                    0.5,
133                )
134            })
135            .collect();
136    }
137
138    fn set_decay_time(&mut self, decay_time: f32) {
139        for comb in self.lp_fb_comb_filters.iter_mut() {
140            comb.set_feedback(calculate_decay(comb.len(), self.sample_rate, decay_time));
141        }
142    }
143
144    fn set_fc(&mut self, fc: f32) {
145        self.fc = fc;
146        for comb in self.lp_fb_comb_filters.iter_mut() {
147            comb.set_fc(fc)
148        }
149    }
150
151    fn feed(&mut self, sample: f32) -> f32 {
152        let input = (sample * Self::GAIN).min(1.0);
153
154        let mut result = 0.0;
155        for comb in self.lp_fb_comb_filters.iter_mut() {
156            result += comb.feed(input);
157        }
158        for allpass in self.all_pass_filters.iter_mut() {
159            result = allpass.feed(result);
160        }
161        result
162    }
163}
164
165/// See module docs.
166#[derive(Debug, Clone, Reflect, PartialEq)]
167pub struct Reverb {
168    dry: f32,
169    wet: f32,
170    #[reflect(setter = "set_decay_time", min_value = 0.0)]
171    decay_time: f32,
172    #[reflect(setter = "set_fc", min_value = 0.0, max_value = 1.0)]
173    fc: f32,
174    #[reflect(hidden)]
175    left: ChannelReverb,
176    #[reflect(hidden)]
177    right: ChannelReverb,
178}
179
180impl Visit for Reverb {
181    fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
182        let mut region = visitor.enter_region(name)?;
183
184        self.dry.visit("Dry", &mut region)?;
185        self.wet.visit("Wet", &mut region)?;
186        self.decay_time.visit("DecayTime", &mut region)?;
187        self.fc.visit("Fc", &mut region)?;
188
189        if region.is_reading() {
190            self.left = ChannelReverb::new(0, self.fc, Reverb::FEEDBACK, self.decay_time);
191            self.right = ChannelReverb::new(23, self.fc, Reverb::FEEDBACK, self.decay_time);
192        }
193
194        Ok(())
195    }
196}
197
198impl Default for Reverb {
199    fn default() -> Self {
200        Self::new()
201    }
202}
203
204impl Reverb {
205    const FEEDBACK: f32 = 0.84;
206
207    /// Creates new instance of reverb effect with cutoff frequency of ~11.2 kHz and
208    /// 5 seconds decay time.
209    pub fn new() -> Self {
210        let fc = 0.25615; // 11296 Hz at 44100 Hz sample rate
211
212        let decay_time = 2.0;
213
214        Self {
215            dry: 1.0,
216            wet: 1.0,
217            decay_time: 2.0,
218            fc,
219            left: ChannelReverb::new(0, fc, Reverb::FEEDBACK, decay_time),
220            right: ChannelReverb::new(23, fc, Reverb::FEEDBACK, decay_time),
221        }
222    }
223
224    /// Sets how much of input signal should be passed to output without any processing.
225    /// Default value is 1.0.
226    pub fn set_dry(&mut self, dry: f32) {
227        self.dry = dry.clamp(0.0, 1.0);
228    }
229
230    /// Returns dry part.
231    pub fn get_dry(&self) -> f32 {
232        self.dry
233    }
234
235    /// Sets stereo mixing of processed signal.
236    /// 0.0 - left is left, right is right
237    /// 1.0 - right is left, left is right.
238    /// 0.5 - left is (left + right) * 0.5, right is (left + right) * 0.5
239    /// and so on.
240    pub fn set_wet(&mut self, wet: f32) {
241        self.wet = wet.clamp(0.0, 1.0);
242    }
243
244    /// Returns stereo mixing coefficient.
245    pub fn get_wet(&self) -> f32 {
246        self.wet
247    }
248
249    /// Sets actual sample rate of effect. It was designed to 44100 Hz sampling rate.
250    /// TODO: This shouldn't be in public API.
251    pub fn set_sample_rate(&mut self, sample_rate: usize) {
252        self.left.set_sample_rate(sample_rate);
253        self.right.set_sample_rate(sample_rate);
254    }
255
256    /// Sets desired duration of reverberation, the more size your environment has,
257    /// the larger duration of reverberation should be.
258    pub fn set_decay_time(&mut self, decay_time: f32) {
259        self.decay_time = decay_time;
260        self.left.set_decay_time(decay_time);
261        self.right.set_decay_time(decay_time)
262    }
263
264    /// Returns current decay time.
265    pub fn decay_time(&self) -> f32 {
266        self.decay_time
267    }
268
269    /// Sets cutoff frequency for lowpass filter in comb filters. Basically this parameter defines
270    /// "tone" of reflections, when frequency is higher - then more high frequencies will be in
271    /// output signal, and vice versa. For example if you have environment with high absorption of
272    /// high frequencies, then sound in reality will be muffled - to simulate this you could set
273    /// frequency to 3-4 kHz.
274    ///
275    /// # Notes
276    ///
277    /// This method uses normalized frequency as input, this means that you should divide your desired
278    /// frequency in hertz by sample rate of sound context. Context has `normalize_frequency` method
279    /// exactly for this purpose.
280    pub fn set_fc(&mut self, fc: f32) {
281        self.fc = fc;
282        self.left.set_fc(fc);
283        self.right.set_fc(fc);
284    }
285
286    /// Returns current cutoff frequency.
287    pub fn fc(&self) -> f32 {
288        self.fc
289    }
290}
291
292impl EffectRenderTrait for Reverb {
293    fn render(&mut self, input: &[(f32, f32)], mix_buf: &mut [(f32, f32)]) {
294        let wet = self.wet;
295        let dry = 1.0 - self.wet;
296
297        for ((out_left, out_right), &(left, right)) in mix_buf.iter_mut().zip(input.iter()) {
298            let mid = (left + right) * 0.5;
299
300            let processed_left = self.left.feed(mid);
301            let processed_right = self.right.feed(mid);
302
303            *out_left = processed_left * wet + processed_right * dry + self.dry * left;
304            *out_right = processed_right * wet + processed_left * dry + self.dry * right;
305        }
306    }
307}
308
309#[cfg(test)]
310mod test {
311    use crate::effects::reverb::{ChannelReverb, Reverb};
312
313    // Test reverberation for convergence and energy conservation law.
314    #[test]
315    fn test_reverb_convergence() {
316        let mut reverb = ChannelReverb::new(0, 0.25615, Reverb::FEEDBACK, 5.0);
317        let impulse_amplitude = 2.0;
318        reverb.feed(impulse_amplitude);
319        let mut counter = 0;
320        while counter < 6 * 44100 {
321            let result = reverb.feed(0.0);
322            if result > 1.0 {
323                panic!(
324                    "Reverb does not converge with initial impulse of \
325                {impulse_amplitude} amplitude. The output sample was {result} \
326                at {counter} iteration. Energy conservation law was violated."
327                )
328            }
329            counter += 1;
330        }
331    }
332}