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