Skip to main content

firewheel_core/dsp/
fade.rs

1#[cfg(not(feature = "std"))]
2use num_traits::Float;
3
4use core::f32::consts::FRAC_PI_2;
5
6use crate::{
7    diff::{Diff, Patch},
8    dsp::volume::{amp_to_db, Volume},
9};
10
11/// The algorithm used to map a normalized crossfade/panning value in the
12/// range `[0.0, 1.0]` or `[-1.0, 1.0]` to the corresponding gain values
13/// for two inputs.
14#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Diff, Patch)]
15#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[repr(u32)]
18pub enum FadeCurve {
19    /// This curve makes the combined signal appear to play at a constant volume
20    /// across the entire fade range for most signals.
21    ///
22    /// More specifically this a circular curve with each input at -3dB at
23    /// center.
24    #[default]
25    EqualPower3dB = 0,
26    /// Same as [`FadeCurve::EqualPower3dB`], but each input will be at -6dB
27    /// at center which may be better for some signals.
28    EqualPower6dB,
29    /// This is cheaper to compute than [`FadeCurve::EqualPower3dB`], but is less
30    /// accurate in its perception of constant volume.
31    SquareRoot,
32    /// The cheapest to compute, but is the least accurate in its perception of
33    /// constant volume for some signals (though if the signals are highly
34    /// correlated such as a wet/dry mix, then this mode may actually provide
35    /// better results.)
36    Linear,
37}
38
39impl FadeCurve {
40    /// Compute the raw gain values for both inputs.
41    ///
42    /// * `fade` - The fade amount, where `0.5` is center, `0.0` is fully the
43    /// first input, and `1.0` is fully the second input.
44    pub fn compute_gains_0_to_1(&self, fade: f32) -> (f32, f32) {
45        if fade <= 0.00001 {
46            (1.0, 0.0)
47        } else if fade >= 0.99999 {
48            (0.0, 1.0)
49        } else {
50            match self {
51                Self::EqualPower3dB => {
52                    let fade = FRAC_PI_2 * fade;
53                    let fade_cos = fade.cos();
54                    let fade_sin = fade.sin();
55
56                    (fade_cos, fade_sin)
57                }
58                Self::EqualPower6dB => {
59                    let fade = FRAC_PI_2 * fade;
60                    let fade_cos = fade.cos();
61                    let fade_sin = fade.sin();
62
63                    (fade_cos * fade_cos, fade_sin * fade_sin)
64                }
65                Self::SquareRoot => ((1.0 - fade).sqrt(), fade.sqrt()),
66                Self::Linear => ((1.0 - fade), fade),
67            }
68        }
69    }
70
71    /// Compute the raw gain values for both inputs.
72    ///
73    /// * `fade` - The fade amount, where `0.0` is center, `-1.0` is fully the
74    /// first input, and `1.0` is fully the second input.
75    pub fn compute_gains_neg1_to_1(&self, fade: f32) -> (f32, f32) {
76        if fade <= -0.99999 {
77            (1.0, 0.0)
78        } else if fade >= 0.99999 {
79            (0.0, 1.0)
80        } else {
81            let fade = (fade + 1.0) * 0.5;
82
83            match self {
84                Self::EqualPower3dB => {
85                    let fade = FRAC_PI_2 * fade;
86                    let fade_cos = fade.cos();
87                    let fade_sin = fade.sin();
88
89                    (fade_cos, fade_sin)
90                }
91                Self::EqualPower6dB => {
92                    let fade = FRAC_PI_2 * fade;
93                    let fade_cos = fade.cos();
94                    let fade_sin = fade.sin();
95
96                    (fade_cos * fade_cos, fade_sin * fade_sin)
97                }
98                Self::SquareRoot => ((1.0 - fade).sqrt(), fade.sqrt()),
99                Self::Linear => ((1.0 - fade), fade),
100            }
101        }
102    }
103
104    /// Compute the decibel values for both inputs for crossfading.
105    ///
106    /// * `fade` - The fade amount, where `0.5` is center, `0.0` is fully the
107    /// first input, and `1.0` is fully the second input.
108    pub fn crossfade_decibels(&self, fade: f32) -> (f32, f32) {
109        let (a1, a2) = self.compute_gains_0_to_1(fade);
110
111        (amp_to_db(a1), amp_to_db(a2))
112    }
113
114    /// Compute the [`Volume`]s for both inputs for crossfading.
115    ///
116    /// (Both volumes will be of type [`Volume::Decibels`]).
117    ///
118    /// * `fade` - The fade amount, where `0.5` is center, `0.0` is fully the
119    /// first input, and `1.0` is fully the second input.
120    pub fn crossfade_volumes(&self, fade: f32) -> (Volume, Volume) {
121        let (d1, d2) = self.crossfade_decibels(fade);
122
123        (Volume::Decibels(d1), Volume::Decibels(d2))
124    }
125
126    pub fn from_u32(val: u32) -> Self {
127        match val {
128            1 => Self::EqualPower6dB,
129            2 => Self::SquareRoot,
130            3 => Self::Linear,
131            _ => Self::EqualPower3dB,
132        }
133    }
134}