Skip to main content

firewheel_nodes/freeverb/
mod.rs

1//! A Rust implementation of Freeverb by Ian Hobson.
2//! The original repo can be found [here](https://github.com/irh/freeverb-rs).
3
4#![allow(missing_docs)]
5#![allow(clippy::module_inception)]
6
7use firewheel_core::{
8    channel_config::{ChannelConfig, ChannelCount},
9    diff::{Diff, Notify, Patch},
10    dsp::declick::{DeclickFadeCurve, DeclickValues, Declicker},
11    event::ProcEvents,
12    node::{
13        AudioNode, AudioNodeInfo, AudioNodeProcessor, ConstructProcessorContext, EmptyConfig,
14        ProcBuffers, ProcExtra, ProcInfo, ProcStreamCtx, ProcessStatus,
15    },
16    param::smoother::{SmoothedParam, SmootherConfig},
17};
18
19mod all_pass;
20mod comb;
21mod delay_line;
22mod freeverb;
23
24/// A simple, relatively cheap stereo reverb.
25///
26/// Freeverb tends to have a somewhat metallic sound, but
27/// its minimal computational cost makes it highly versatile.
28#[derive(Diff, Patch, Clone, Copy, Debug, PartialEq)]
29#[cfg_attr(feature = "bevy", derive(bevy_ecs::component::Component))]
30#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct FreeverbNode {
33    /// Set the size of the emulated room, expressed from 0 to 1.
34    ///
35    /// Values near zero will sound like a small room, while values
36    /// near one will reverberate almost continuously.
37    pub room_size: f32,
38
39    /// Set the high-frequency damping, expressed from 0 to 1.
40    ///
41    /// Values near zero will produce a dark or muffled sound,
42    /// while values near one will sound bright or metallic.
43    pub damping: f32,
44
45    /// Set the left/right blending, expressed from 0 to 1.
46    pub width: f32,
47
48    /// Pause the reverb processing.
49    ///
50    /// This prevents a reverb tail from ringing out when you
51    /// want all sound to momentarily pause.
52    pub pause: bool,
53
54    /// Reset the reverb, clearing its internal state.
55    #[cfg_attr(feature = "serde", serde(skip))]
56    pub reset: Notify<()>,
57
58    /// Adjusts the time in seconds over which parameters are smoothed.
59    ///
60    /// Defaults to `0.015` (15ms).
61    pub smooth_seconds: f32,
62}
63
64impl Default for FreeverbNode {
65    fn default() -> Self {
66        FreeverbNode {
67            room_size: 0.5,
68            damping: 0.5,
69            width: 0.5,
70            pause: false,
71            reset: Notify::new(()),
72            smooth_seconds: 0.015,
73        }
74    }
75}
76
77impl AudioNode for FreeverbNode {
78    type Configuration = EmptyConfig;
79
80    fn info(&self, _: &Self::Configuration) -> AudioNodeInfo {
81        AudioNodeInfo::new()
82            .debug_name("freeverb")
83            .channel_config(ChannelConfig {
84                num_inputs: ChannelCount::STEREO,
85                num_outputs: ChannelCount::STEREO,
86            })
87    }
88
89    fn construct_processor(
90        &self,
91        _: &Self::Configuration,
92        cx: ConstructProcessorContext,
93    ) -> impl AudioNodeProcessor {
94        let freeverb = freeverb::Freeverb::new(cx.stream_info.sample_rate.get() as usize);
95        let smoother_config = SmootherConfig {
96            smooth_seconds: self.smooth_seconds,
97            ..Default::default()
98        };
99
100        let mut processor = FreeverbProcessor {
101            freeverb,
102            damping: SmoothedParam::new(
103                self.damping.clamp(0.0, 1.0),
104                smoother_config,
105                cx.stream_info.sample_rate,
106            ),
107            width: SmoothedParam::new(
108                self.width.clamp(0.0, 1.0),
109                smoother_config,
110                cx.stream_info.sample_rate,
111            ),
112            room_size: SmoothedParam::new(
113                self.room_size.clamp(0.0, 1.0),
114                smoother_config,
115                cx.stream_info.sample_rate,
116            ),
117            paused: self.pause,
118            declicker: if self.pause {
119                Declicker::SettledAt0
120            } else {
121                Declicker::SettledAt1
122            },
123            values: DeclickValues::new(cx.stream_info.declick_frames),
124        };
125
126        processor.apply_parameters();
127
128        processor
129    }
130}
131
132struct FreeverbProcessor {
133    freeverb: freeverb::Freeverb,
134    damping: SmoothedParam,
135    width: SmoothedParam,
136    room_size: SmoothedParam,
137    paused: bool,
138    declicker: Declicker,
139    values: DeclickValues,
140}
141
142impl AudioNodeProcessor for FreeverbProcessor {
143    fn process(
144        &mut self,
145        proc_info: &ProcInfo,
146        buffers: ProcBuffers,
147        events: &mut ProcEvents,
148        _: &mut ProcExtra,
149    ) -> ProcessStatus {
150        for patch in events.drain_patches::<FreeverbNode>() {
151            match patch {
152                FreeverbNodePatch::Damping(value) => {
153                    self.damping.set_value(value.clamp(0.0, 1.0));
154                }
155                FreeverbNodePatch::RoomSize(value) => {
156                    self.room_size.set_value(value.clamp(0.0, 1.0));
157                }
158                FreeverbNodePatch::Width(value) => {
159                    self.width.set_value(value.clamp(0.0, 1.0));
160                }
161                FreeverbNodePatch::Reset(_) => {
162                    self.freeverb.reset();
163                }
164                FreeverbNodePatch::Pause(value) => {
165                    self.paused = value;
166
167                    if value {
168                        self.declicker.fade_to_0(&self.values);
169                    } else {
170                        self.apply_parameters();
171                        self.declicker.fade_to_1(&self.values);
172                    }
173                }
174                FreeverbNodePatch::SmoothSeconds(value) => {
175                    self.room_size
176                        .set_smooth_seconds(value, proc_info.sample_rate);
177                    self.width.set_smooth_seconds(value, proc_info.sample_rate);
178                    self.damping
179                        .set_smooth_seconds(value, proc_info.sample_rate);
180                }
181            }
182        }
183
184        if self.paused && self.declicker.has_settled() {
185            self.damping.reset_to_target();
186            self.room_size.reset_to_target();
187            self.width.reset_to_target();
188
189            return ProcessStatus::ClearAllOutputs;
190        }
191
192        let all_silent = proc_info.in_silence_mask.all_channels_silent(2);
193        if all_silent && proc_info.prev_output_was_silent {
194            self.declicker.reset_to_target();
195            self.damping.reset_to_target();
196            self.room_size.reset_to_target();
197            self.width.reset_to_target();
198
199            return ProcessStatus::ClearAllOutputs;
200        }
201
202        if !all_silent && proc_info.prev_output_was_silent {
203            // re-apply the parameters
204            self.apply_parameters();
205        }
206
207        // just take the slow path if any are smoothing
208        if self.damping.is_smoothing() || self.room_size.is_smoothing() || self.width.is_smoothing()
209        {
210            for frame in 0..proc_info.frames {
211                let damping = self.damping.next_smoothed();
212                let room_size = self.room_size.next_smoothed();
213                let width = self.width.next_smoothed();
214
215                // we assume setting these values is more expensive than
216                // calculating their smoothing
217                if frame.is_multiple_of(4) {
218                    self.freeverb.set_dampening(damping as f64);
219                    self.freeverb.set_room_size(room_size as f64);
220                    self.freeverb.set_width(width as f64);
221
222                    self.freeverb.update_combs();
223                }
224
225                let (left, right) = self.freeverb.tick((
226                    buffers.inputs[0][frame] as f64,
227                    buffers.inputs[1][frame] as f64,
228                ));
229
230                buffers.outputs[0][frame] = left as f32;
231                buffers.outputs[1][frame] = right as f32;
232            }
233
234            self.damping.settle();
235            self.room_size.settle();
236            self.width.settle();
237        } else {
238            for frame in 0..proc_info.frames {
239                let (left, right) = self.freeverb.tick((
240                    buffers.inputs[0][frame] as f64,
241                    buffers.inputs[1][frame] as f64,
242                ));
243
244                buffers.outputs[0][frame] = left as f32;
245                buffers.outputs[1][frame] = right as f32;
246            }
247        }
248
249        // We do this before the declicking just to make sure we
250        // finish declicking if we're paused simultaneously with the
251        // input going silent.
252        if all_silent && !proc_info.prev_output_was_silent {
253            // check the output buffers to see if they pass
254            // the threshold for "completely silent"
255
256            // threshold chosen by ear
257            let threshold = 0.0001;
258            if matches!(
259                buffers.check_for_silence_on_outputs(threshold),
260                ProcessStatus::ClearAllOutputs
261            ) {
262                return ProcessStatus::ClearAllOutputs;
263            }
264        }
265
266        if !self.declicker.has_settled() {
267            self.declicker.process(
268                &mut buffers.outputs[..2],
269                0..proc_info.frames,
270                &self.values,
271                1.0,
272                DeclickFadeCurve::EqualPower3dB,
273            );
274        }
275
276        ProcessStatus::OutputsModified
277    }
278
279    fn new_stream(&mut self, stream_info: &firewheel_core::StreamInfo, _proc: &mut ProcStreamCtx) {
280        // TODO: we could probably attempt to smooth the transition here
281        self.freeverb.resize(stream_info.sample_rate.get() as usize);
282        self.damping.update_sample_rate(stream_info.sample_rate);
283        self.width.update_sample_rate(stream_info.sample_rate);
284        self.room_size.update_sample_rate(stream_info.sample_rate);
285    }
286}
287
288impl FreeverbProcessor {
289    fn apply_parameters(&mut self) {
290        self.freeverb
291            .set_dampening(self.damping.target_value() as f64);
292        self.freeverb
293            .set_room_size(self.room_size.target_value() as f64);
294        self.freeverb.set_width(self.width.target_value() as f64);
295        self.freeverb.update_combs();
296    }
297}