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::dsp::coeff_update::{CoeffUpdateFactor, CoeffUpdateMask};
8use firewheel_core::dsp::filter::smoothing_filter::DEFAULT_SMOOTH_SECONDS;
9use firewheel_core::node::NodeError;
10use firewheel_core::{
11    channel_config::{ChannelConfig, ChannelCount},
12    diff::{Diff, Notify, Patch},
13    dsp::{
14        declick::{DeclickFadeCurve, DeclickValues, Declicker},
15        volume::DEFAULT_MIN_AMP,
16    },
17    event::ProcEvents,
18    node::{
19        AudioNode, AudioNodeInfo, AudioNodeProcessor, ConstructProcessorContext, EmptyConfig,
20        ProcBuffers, ProcExtra, ProcInfo, ProcStreamCtx, ProcessStatus,
21    },
22    param::smoother::{SmoothedParam, SmootherConfig},
23};
24
25use crate::freeverb::freeverb::Freeverb;
26
27mod all_pass;
28mod comb;
29mod delay_line;
30mod freeverb;
31
32/// A simple, relatively cheap stereo reverb.
33///
34/// Freeverb tends to have a somewhat metallic sound, but
35/// its minimal computational cost makes it highly versatile.
36#[derive(Diff, Patch, Clone, Copy, Debug, PartialEq)]
37#[cfg_attr(feature = "bevy", derive(bevy_ecs::component::Component))]
38#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub struct FreeverbNode {
41    /// Set the size of the emulated room, expressed from 0 to 1.
42    ///
43    /// Values near zero will sound like a small room, while values
44    /// near one will reverberate almost continuously.
45    pub room_size: f32,
46
47    /// Set the high-frequency damping, expressed from 0 to 1.
48    ///
49    /// Values near zero will produce a dark or muffled sound,
50    /// while values near one will sound bright or metallic.
51    pub damping: f32,
52
53    /// Set the left/right blending, expressed from 0 to 1.
54    pub width: f32,
55
56    /// Pause the reverb processing.
57    ///
58    /// This prevents a reverb tail from ringing out when you
59    /// want all sound to momentarily pause.
60    pub pause: bool,
61
62    /// Reset the reverb, clearing its internal state.
63    #[cfg_attr(feature = "serde", serde(skip))]
64    pub reset: Notify<()>,
65
66    /// Adjusts the time in seconds over which parameters are smoothed.
67    ///
68    /// By default this is set to `0.062` (62ms). This value is chosen such that
69    /// the stair-stepping effect isn't noticeable for a typical block size of 1024
70    /// samples.
71    pub smooth_seconds: f32,
72
73    /// An exponent representing the rate at which DSP coefficients are
74    /// updated when parameters are being smoothed.
75    ///
76    /// Smaller values will produce less "stair-stepping" artifacts,
77    /// but will also consume more CPU.
78    ///
79    /// The resulting number of frames (samples in a single channel of audio)
80    /// that will elapse between each update is calculated as
81    /// `2^coeff_update_factor`.
82    ///
83    /// By default this is set to `4`.
84    pub coeff_update_factor: CoeffUpdateFactor,
85}
86
87impl Default for FreeverbNode {
88    fn default() -> Self {
89        FreeverbNode {
90            room_size: 0.5,
91            damping: 0.5,
92            width: 0.5,
93            pause: false,
94            reset: Notify::new(()),
95            smooth_seconds: DEFAULT_SMOOTH_SECONDS,
96            coeff_update_factor: CoeffUpdateFactor::default(),
97        }
98    }
99}
100
101impl AudioNode for FreeverbNode {
102    type Configuration = EmptyConfig;
103
104    fn info(&self, _: &Self::Configuration) -> Result<AudioNodeInfo, NodeError> {
105        Ok(AudioNodeInfo::new()
106            .debug_name("freeverb")
107            .channel_config(ChannelConfig {
108                num_inputs: ChannelCount::STEREO,
109                num_outputs: ChannelCount::STEREO,
110            }))
111        // TODO: If and when the scheduler gets proper in-place processing support, use
112        // in-place processing for this node.
113    }
114
115    fn construct_processor(
116        &self,
117        _: &Self::Configuration,
118        cx: ConstructProcessorContext,
119    ) -> Result<impl AudioNodeProcessor, NodeError> {
120        let freeverb = freeverb::Freeverb::new(cx.stream_info.sample_rate.get() as usize);
121        let smoother_config = SmootherConfig {
122            smooth_seconds: self.smooth_seconds,
123            ..Default::default()
124        };
125
126        let mut processor = FreeverbProcessor {
127            freeverb,
128            damping: SmoothedParam::new(
129                self.damping.clamp(0.0, 1.0),
130                1.0,
131                smoother_config,
132                cx.stream_info.sample_rate,
133            ),
134            width: SmoothedParam::new(
135                self.width.clamp(0.0, 1.0),
136                1.0,
137                smoother_config,
138                cx.stream_info.sample_rate,
139            ),
140            room_size: SmoothedParam::new(
141                self.room_size.clamp(0.0, 1.0),
142                1.0,
143                smoother_config,
144                cx.stream_info.sample_rate,
145            ),
146            paused: self.pause,
147            pause_declicker: if self.pause {
148                Declicker::SettledAt0
149            } else {
150                Declicker::SettledAt1
151            },
152            values: DeclickValues::new(cx.stream_info.declick_frames),
153            coeff_update_mask: self.coeff_update_factor.mask(),
154            prev_output_was_silent: true,
155        };
156
157        processor.apply_parameters();
158
159        Ok(processor)
160    }
161}
162
163struct FreeverbProcessor {
164    freeverb: freeverb::Freeverb,
165    damping: SmoothedParam,
166    width: SmoothedParam,
167    room_size: SmoothedParam,
168    paused: bool,
169    pause_declicker: Declicker,
170    values: DeclickValues,
171    coeff_update_mask: CoeffUpdateMask,
172    prev_output_was_silent: bool,
173}
174
175impl FreeverbProcessor {
176    fn reset(&mut self, reset_reverb: bool) {
177        self.pause_declicker.reset_to_target();
178        self.damping.reset_to_target();
179        self.room_size.reset_to_target();
180        self.width.reset_to_target();
181
182        if reset_reverb {
183            self.freeverb.reset();
184        }
185    }
186}
187
188impl AudioNodeProcessor for FreeverbProcessor {
189    fn events(&mut self, info: &ProcInfo, events: &mut ProcEvents, _extra: &mut ProcExtra) {
190        for patch in events.drain_patches::<FreeverbNode>() {
191            match patch {
192                FreeverbNodePatch::Damping(value) => {
193                    self.damping.set_value(value.clamp(0.0, 1.0));
194                }
195                FreeverbNodePatch::RoomSize(value) => {
196                    self.room_size.set_value(value.clamp(0.0, 1.0));
197                }
198                FreeverbNodePatch::Width(value) => {
199                    self.width.set_value(value.clamp(0.0, 1.0));
200                }
201                FreeverbNodePatch::Reset(_) => {
202                    self.freeverb.reset();
203                }
204                FreeverbNodePatch::Pause(value) => {
205                    self.paused = value;
206
207                    if value {
208                        self.pause_declicker.fade_to_0(&self.values);
209                    } else {
210                        self.apply_parameters();
211                        self.pause_declicker.fade_to_1(&self.values);
212                    }
213                }
214                FreeverbNodePatch::SmoothSeconds(value) => {
215                    self.room_size.set_smooth_seconds(value, info.sample_rate);
216                    self.width.set_smooth_seconds(value, info.sample_rate);
217                    self.damping.set_smooth_seconds(value, info.sample_rate);
218                }
219                FreeverbNodePatch::CoeffUpdateFactor(value) => {
220                    self.coeff_update_mask = value.mask();
221                }
222            }
223        }
224    }
225
226    fn bypassed(&mut self, bypassed: bool) {
227        if !bypassed {
228            self.reset(true);
229        }
230    }
231
232    fn process(
233        &mut self,
234        info: &ProcInfo,
235        buffers: ProcBuffers,
236        _: &mut ProcExtra,
237    ) -> ProcessStatus {
238        let all_silent = info.in_silence_mask.all_channels_silent(2);
239
240        if (self.paused && self.pause_declicker.has_settled())
241            || (all_silent && self.prev_output_was_silent)
242        {
243            self.reset(false);
244
245            self.prev_output_was_silent = true;
246            return ProcessStatus::ClearAllOutputs;
247        }
248
249        if !all_silent && self.prev_output_was_silent {
250            // re-apply the parameters
251            self.apply_parameters();
252        }
253
254        assert!(buffers.inputs[0].len() >= info.frames);
255        assert!(buffers.inputs[1].len() >= info.frames);
256        assert!(buffers.outputs[0].len() >= info.frames);
257        assert!(buffers.outputs[1].len() >= info.frames);
258
259        // just take the slow path if any are smoothing
260        if self.damping.is_smoothing() || self.room_size.is_smoothing() || self.width.is_smoothing()
261        {
262            for frame in 0..info.frames {
263                let damping = self.damping.next_smoothed();
264                let room_size = self.room_size.next_smoothed();
265                let width = self.width.next_smoothed();
266
267                // we assume setting these values is more expensive than
268                // calculating their smoothing
269                if self.coeff_update_mask.do_update(frame) {
270                    calc_coeffs(&mut self.freeverb, damping, room_size, width);
271                }
272
273                let (left, right) = self.freeverb.tick((
274                    buffers.inputs[0][frame] as f64,
275                    buffers.inputs[1][frame] as f64,
276                ));
277
278                buffers.outputs[0][frame] = left as f32;
279                buffers.outputs[1][frame] = right as f32;
280            }
281
282            self.damping.settle();
283            self.room_size.settle();
284            self.width.settle();
285        } else {
286            for frame in 0..info.frames {
287                let (left, right) = self.freeverb.tick((
288                    buffers.inputs[0][frame] as f64,
289                    buffers.inputs[1][frame] as f64,
290                ));
291
292                buffers.outputs[0][frame] = left as f32;
293                buffers.outputs[1][frame] = right as f32;
294            }
295        }
296
297        // We do this before the declicking just to make sure we
298        // finish declicking if we're paused simultaneously with the
299        // input going silent.
300        if all_silent && !self.prev_output_was_silent {
301            // check the output buffers to see if they pass
302            // the threshold for "completely silent"
303
304            if matches!(
305                buffers.check_for_silence_on_outputs(DEFAULT_MIN_AMP),
306                ProcessStatus::ClearAllOutputs
307            ) {
308                self.prev_output_was_silent = true;
309                return ProcessStatus::ClearAllOutputs;
310            }
311        }
312
313        if !self.pause_declicker.has_settled() {
314            self.pause_declicker.process(
315                &mut buffers.outputs[..2],
316                0..info.frames,
317                &self.values,
318                1.0,
319                DeclickFadeCurve::EqualPower3dB,
320            );
321        }
322
323        self.prev_output_was_silent = false;
324
325        ProcessStatus::OutputsModified
326    }
327
328    fn new_stream(&mut self, stream_info: &firewheel_core::StreamInfo, _proc: &mut ProcStreamCtx) {
329        self.freeverb.resize(stream_info.sample_rate.get() as usize);
330        self.damping.update_sample_rate(stream_info.sample_rate);
331        self.width.update_sample_rate(stream_info.sample_rate);
332        self.room_size.update_sample_rate(stream_info.sample_rate);
333        self.reset(true);
334        self.prev_output_was_silent = true;
335    }
336}
337
338impl FreeverbProcessor {
339    fn apply_parameters(&mut self) {
340        self.freeverb
341            .set_dampening(self.damping.target_value() as f64);
342        self.freeverb
343            .set_room_size(self.room_size.target_value() as f64);
344        self.freeverb.set_width(self.width.target_value() as f64);
345        self.freeverb.update_combs();
346    }
347}
348
349#[cold]
350#[inline(never)]
351fn calc_coeffs(freeverb: &mut Freeverb, damping: f32, room_size: f32, width: f32) {
352    freeverb.set_dampening(damping as f64);
353    freeverb.set_room_size(room_size as f64);
354    freeverb.set_width(width as f64);
355
356    freeverb.update_combs();
357}