firewheel_core/dsp/
distance_attenuation.rs

1#[cfg(not(feature = "std"))]
2use num_traits::Float;
3
4use core::num::NonZeroU32;
5
6use firewheel_macros::{Diff, Patch};
7
8use crate::{
9    dsp::{
10        coeff_update::{CoeffUpdateFactor, CoeffUpdateMask},
11        filter::single_pole_iir::{OnePoleIirLPFCoeff, OnePoleIirLPFCoeffSimd, OnePoleIirLPFSimd},
12    },
13    param::smoother::{SmoothedParam, SmootherConfig},
14};
15
16pub const MUFFLE_CUTOFF_HZ_MIN: f32 = 20.0;
17pub const MUFFLE_CUTOFF_HZ_MAX: f32 = 20_480.0;
18const MUFFLE_CUTOFF_HZ_RANGE_RECIP: f32 = 1.0 / (MUFFLE_CUTOFF_HZ_MAX - MUFFLE_CUTOFF_HZ_MIN);
19
20/// The method in which to calculate the volume of a sound based on the distance from
21/// the listener.
22///
23/// Based on <https://developer.mozilla.org/en-US/docs/Web/API/PannerNode/distanceModel>
24///
25/// Interactive graph of the different models: <https://www.desmos.com/calculator/g1pbsc5m9y>
26#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Diff, Patch)]
27#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
28#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
29pub enum DistanceModel {
30    #[default]
31    /// A linear distance model calculates the gain by:
32    ///
33    /// `reference_distance / (reference_distance + rolloff_factor * (max(distance, reference_distance) - reference_distance))`
34    ///
35    /// This mostly closely matches how sound is attenuated in the real world, and is the default model.
36    Inverse,
37    /// A linear distance model calculates the gain by:
38    ///
39    /// `(1.0 - rolloff_factor * (distance - reference_distance) / (max_distance - reference_distance)).clamp(0.0, 1.0)`
40    Linear,
41    /// An exponential distance model calculates the gain by:
42    ///
43    /// `pow((max(distance, reference_distance) / reference_distance, -rolloff_factor)`
44    ///
45    /// This is equivalent to [`DistanceModel::Inverse`] when `rolloff_factor = 1.0`.
46    Exponential,
47}
48
49impl DistanceModel {
50    fn calculate_gain(
51        &self,
52        distance: f32,
53        distance_gain_factor: f32,
54        reference_distance: f32,
55        maximum_distance: f32,
56    ) -> f32 {
57        if distance <= reference_distance || distance_gain_factor <= 0.00001 {
58            return 1.0;
59        }
60
61        match self {
62            DistanceModel::Inverse => {
63                reference_distance
64                    / (reference_distance
65                        + (distance_gain_factor * (distance - reference_distance)))
66            }
67            DistanceModel::Linear => {
68                if maximum_distance <= reference_distance {
69                    1.0
70                } else {
71                    (1.0 - (distance_gain_factor * (distance - reference_distance)
72                        / (maximum_distance - reference_distance)))
73                        .clamp(0.0, 1.0)
74                }
75            }
76            DistanceModel::Exponential => {
77                (distance / reference_distance).powf(-distance_gain_factor)
78            }
79        }
80    }
81}
82
83/// The parameters which describe how to attenuate a sound based on its distance from
84/// the listener.
85#[derive(Diff, Patch, Debug, Clone, Copy, PartialEq)]
86#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
87#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
88pub struct DistanceAttenuation {
89    /// The method in which to calculate the volume of a sound based on the distance from
90    /// the listener.
91    ///
92    /// by default this is set to [`DistanceModel::Inverse`].
93    ///
94    /// Based on <https://developer.mozilla.org/en-US/docs/Web/API/PannerNode/distanceModel>
95    ///
96    /// Interactive graph of the different models: <https://www.desmos.com/calculator/g1pbsc5m9y>
97    pub distance_model: DistanceModel,
98
99    /// The factor by which the sound gets quieter the farther away it is from the
100    /// listener.
101    ///
102    /// Values less than `1.0` will attenuate the sound less per unit distance, and values
103    /// greater than `1.0` will attenuate the sound more per unit distance.
104    ///
105    /// Set to a value `<= 0.00001` to disable attenuating the sound.
106    ///
107    /// By default this is set to `1.0`.
108    ///
109    /// See <https://www.desmos.com/calculator/g1pbsc5m9y> for an interactive graph of
110    /// how these parameters affect the final volume of a sound for each distance model.
111    pub distance_gain_factor: f32,
112
113    /// The minimum distance at which a sound is considered to be at the maximum volume.
114    /// (Distances less than this value will be clamped at the maximum volume).
115    ///
116    /// If this value is `< 0.00001`, then it will be clamped to `0.00001`.
117    ///
118    /// By default this is set to `5.0`.
119    ///
120    /// See <https://www.desmos.com/calculator/g1pbsc5m9y> for an interactive graph of
121    /// how these parameters affect the final volume of a sound for each distance model.
122    pub reference_distance: f32,
123
124    /// When using [`DistanceModel::Linear`], the maximum reference distance (at a
125    /// rolloff factor of `1.0`) of a sound before it is considered to be "silent".
126    /// (Distances greater than this value will be clamped to silence).
127    ///
128    /// If this value is `< 0.0`, then it will be clamped to `0.0`.
129    ///
130    /// By default this is set to `200.0`.
131    ///
132    /// See <https://www.desmos.com/calculator/g1pbsc5m9y> for an interactive graph of
133    /// how these parameters affect the final volume of a sound for each distance model.
134    pub max_distance: f32,
135
136    /// The factor which determines the curve of the high frequency damping (lowpass)
137    /// in relation to distance.
138    ///
139    /// Higher values dampen the high frequencies faster, while smaller values dampen
140    /// the high frequencies slower.
141    ///
142    /// Set to a value `<= 0.00001` to disable muffling the sound based on distance.
143    ///
144    /// By default this is set to `1.9`.
145    ///
146    /// See <https://www.desmos.com/calculator/jxp8t9ero4> for an interactive graph of
147    /// how these parameters affect the final lowpass cuttoff frequency.
148    pub distance_muffle_factor: f32,
149
150    /// The distance at which the high frequencies of a sound become fully muffled
151    /// (lowpassed).
152    ///
153    /// Distances less than `reference_distance` will have no muffling.
154    ///
155    /// This has no effect if `muffle_factor` is `None`.
156    ///
157    /// By default this is set to `200.0`.
158    ///
159    /// See <https://www.desmos.com/calculator/jxp8t9ero4> for an interactive graph of
160    /// how these parameters affect the final lowpass cuttoff frequency.
161    pub max_muffle_distance: f32,
162
163    /// The amount of muffling (lowpass) at `max_muffle_distance` in the range
164    /// `[20.0, 20_480.0]`, where `20_480.0` is no muffling and `20.0` is maximum
165    /// muffling.
166    ///
167    /// This has no effect if `muffle_factor` is `None`.
168    ///
169    /// By default this is set to `20.0`.
170    ///
171    /// See <https://www.desmos.com/calculator/jxp8t9ero4> for an interactive graph of
172    /// how these parameters affect the final lowpass cuttoff frequency.
173    pub max_distance_muffle_cutoff_hz: f32,
174}
175
176impl Default for DistanceAttenuation {
177    fn default() -> Self {
178        Self {
179            distance_model: DistanceModel::Inverse,
180            distance_gain_factor: 1.0,
181            reference_distance: 5.0,
182            max_distance: 200.0,
183            distance_muffle_factor: 1.9,
184            max_muffle_distance: 200.0,
185            max_distance_muffle_cutoff_hz: 20.0,
186        }
187    }
188}
189
190pub struct DistanceAttenuatorStereoDsp {
191    pub gain: SmoothedParam,
192    pub muffle_cutoff_hz: SmoothedParam,
193    pub damping_disabled: bool,
194
195    pub filter: OnePoleIirLPFSimd<2>,
196    coeff_update_mask: CoeffUpdateMask,
197}
198
199impl DistanceAttenuatorStereoDsp {
200    pub fn new(
201        smoother_config: SmootherConfig,
202        sample_rate: NonZeroU32,
203        // TODO: Add this back in version 0.8
204        //coeff_update_factor: CoeffUpdateFactor,
205    ) -> Self {
206        Self {
207            gain: SmoothedParam::new(1.0, smoother_config, sample_rate),
208            muffle_cutoff_hz: SmoothedParam::new(
209                MUFFLE_CUTOFF_HZ_MAX,
210                smoother_config,
211                sample_rate,
212            ),
213            damping_disabled: true,
214            filter: OnePoleIirLPFSimd::default(),
215            coeff_update_mask: CoeffUpdateFactor::default().mask(),
216            //coeff_update_mask: coeff_update_factor.mask(),
217        }
218    }
219
220    pub fn set_coeff_update_factor(&mut self, coeff_update_factor: CoeffUpdateFactor) {
221        self.coeff_update_mask = coeff_update_factor.mask();
222    }
223
224    pub fn is_silent(&self) -> bool {
225        self.gain.target_value() == 0.0 && !self.gain.is_smoothing()
226    }
227
228    pub fn compute_values(
229        &mut self,
230        distance: f32,
231        params: &DistanceAttenuation,
232        muffle_cutoff_hz: f32,
233        min_gain: f32,
234    ) {
235        let reference_distance = params.reference_distance.max(0.00001);
236        let max_distance = params.max_distance.max(0.0);
237        let max_distance_muffle_cutoff_hz = params
238            .max_distance_muffle_cutoff_hz
239            .max(MUFFLE_CUTOFF_HZ_MIN);
240
241        let distance_gain = params.distance_model.calculate_gain(
242            distance,
243            params.distance_gain_factor,
244            reference_distance,
245            max_distance,
246        );
247
248        let gain = if distance_gain <= min_gain {
249            0.0
250        } else {
251            distance_gain
252        };
253
254        let distance_cutoff_norm = if params.distance_muffle_factor <= 0.00001
255            || distance <= reference_distance
256            || params.max_muffle_distance <= reference_distance
257            || max_distance_muffle_cutoff_hz >= MUFFLE_CUTOFF_HZ_MAX
258        {
259            1.0
260        } else {
261            let num = distance - reference_distance;
262            let den = params.max_muffle_distance - reference_distance;
263
264            let norm = 1.0 - (num / den).powf(params.distance_muffle_factor.recip());
265
266            let min_norm = (max_distance_muffle_cutoff_hz - MUFFLE_CUTOFF_HZ_MIN)
267                * MUFFLE_CUTOFF_HZ_RANGE_RECIP;
268
269            norm.max(min_norm)
270        };
271
272        let muffle_cutoff_hz = if (muffle_cutoff_hz < MUFFLE_CUTOFF_HZ_MAX - 0.01)
273            || distance_cutoff_norm < 1.0
274        {
275            let hz = if distance_cutoff_norm < 1.0 {
276                let muffle_cutoff_norm =
277                    (muffle_cutoff_hz - MUFFLE_CUTOFF_HZ_MIN) * MUFFLE_CUTOFF_HZ_RANGE_RECIP;
278                let final_norm = muffle_cutoff_norm * distance_cutoff_norm;
279
280                (final_norm * (MUFFLE_CUTOFF_HZ_MAX - MUFFLE_CUTOFF_HZ_MIN)) + MUFFLE_CUTOFF_HZ_MIN
281            } else {
282                muffle_cutoff_hz
283            };
284
285            Some(hz.clamp(MUFFLE_CUTOFF_HZ_MIN, MUFFLE_CUTOFF_HZ_MAX))
286        } else {
287            None
288        };
289
290        self.gain.set_value(gain);
291
292        if let Some(cutoff_hz) = muffle_cutoff_hz {
293            self.muffle_cutoff_hz.set_value(cutoff_hz);
294            self.damping_disabled = false;
295        } else {
296            self.muffle_cutoff_hz.set_value(MUFFLE_CUTOFF_HZ_MAX);
297            self.damping_disabled = true;
298        }
299    }
300
301    /// Returns `true` if the output buffers should be cleared with silence, `false`
302    /// otherwise.
303    pub fn process(
304        &mut self,
305        frames: usize,
306        out1: &mut [f32],
307        out2: &mut [f32],
308        sample_rate_recip: f64,
309    ) -> bool {
310        // Make doubly sure that the compiler optimizes away the bounds checking
311        // in the loop.
312        let out1 = &mut out1[..frames];
313        let out2 = &mut out2[..frames];
314
315        if !self.gain.is_smoothing() && !self.muffle_cutoff_hz.is_smoothing() {
316            if !self.gain.is_smoothing() && self.gain.target_value() == 0.0 {
317                self.gain.reset();
318                self.muffle_cutoff_hz.reset();
319                self.filter.reset();
320
321                return true;
322            } else if self.damping_disabled {
323                for i in 0..frames {
324                    out1[i] = out1[i] * self.gain.target_value();
325                    out2[i] = out2[i] * self.gain.target_value();
326                }
327            } else {
328                // The cutoff parameter is not currently smoothing, so we can optimize by
329                // only updating the filter coefficients once.
330                let coeff = OnePoleIirLPFCoeffSimd::splat(OnePoleIirLPFCoeff::new(
331                    self.muffle_cutoff_hz.target_value(),
332                    sample_rate_recip as f32,
333                ));
334
335                for i in 0..frames {
336                    let s = [
337                        out1[i] * self.gain.target_value(),
338                        out2[i] * self.gain.target_value(),
339                    ];
340
341                    let [l, r] = self.filter.process(s, &coeff);
342
343                    out1[i] = l;
344                    out2[i] = r;
345                }
346            }
347        } else {
348            if self.damping_disabled && !self.muffle_cutoff_hz.is_smoothing() {
349                for i in 0..frames {
350                    let gain = self.gain.next_smoothed();
351
352                    out1[i] = out1[i] * gain;
353                    out2[i] = out2[i] * gain;
354                }
355            } else {
356                let mut coeff = OnePoleIirLPFCoeffSimd::default();
357
358                for i in 0..frames {
359                    let cutoff_hz = self.muffle_cutoff_hz.next_smoothed();
360                    let gain = self.gain.next_smoothed();
361
362                    // Because recalculating filter coefficients is expensive, a trick like
363                    // this can be used to only recalculate them every few frames.
364                    //
365                    // TODO: use core::hint::cold_path() once that stabilizes
366                    //
367                    // TODO: Alternatively, this could be optimized using a lookup table
368                    if self.coeff_update_mask.do_update(i) {
369                        coeff = OnePoleIirLPFCoeffSimd::splat(OnePoleIirLPFCoeff::new(
370                            cutoff_hz,
371                            sample_rate_recip as f32,
372                        ));
373                    }
374
375                    let s = [out1[i] * gain, out2[i] * gain];
376
377                    let [l, r] = self.filter.process(s, &coeff);
378
379                    out1[i] = l;
380                    out2[i] = r;
381                }
382            }
383
384            self.gain.settle();
385            self.muffle_cutoff_hz.settle();
386        }
387
388        false
389    }
390
391    pub fn reset(&mut self) {
392        self.gain.reset();
393        self.muffle_cutoff_hz.reset();
394        self.filter.reset();
395    }
396
397    pub fn set_smooth_seconds(&mut self, seconds: f32, sample_rate: NonZeroU32) {
398        self.gain.set_smooth_seconds(seconds, sample_rate);
399        self.muffle_cutoff_hz
400            .set_smooth_seconds(seconds, sample_rate);
401    }
402
403    pub fn update_sample_rate(&mut self, sample_rate: NonZeroU32) {
404        self.gain.update_sample_rate(sample_rate);
405        self.muffle_cutoff_hz.update_sample_rate(sample_rate);
406    }
407}