Skip to main content

max / audiofiles

5.4 KB · 172 lines History Blame Raw
1 //! Fade in/out with selectable curve shapes.
2
3 /// Fade curve shape.
4 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
5 pub enum FadeCurve {
6 Linear,
7 Logarithmic,
8 SCurve,
9 }
10
11 impl FadeCurve {
12 /// Compute the gain at position `t` (0.0 = start, 1.0 = end of fade region).
13 /// Returns a value in [0.0, 1.0].
14 fn gain(&self, t: f32) -> f32 {
15 match self {
16 FadeCurve::Linear => t,
17 FadeCurve::Logarithmic => {
18 // Attempt a logarithmic curve: fast rise then flattening
19 // log2(1 + t) / log2(2) = log2(1 + t)
20 (1.0 + t).log2()
21 }
22 FadeCurve::SCurve => {
23 // Smoothstep: 3t^2 - 2t^3
24 t * t * (3.0 - 2.0 * t)
25 }
26 }
27 }
28 }
29
30 /// Apply a fade-in over the first `frames` frames.
31 pub fn apply_fade_in(samples: &mut [f32], channels: u16, frames: usize, curve: FadeCurve) {
32 let ch = channels as usize;
33 if ch == 0 || samples.is_empty() || frames == 0 {
34 return;
35 }
36
37 let total_frames = samples.len() / ch;
38 let fade_frames = frames.min(total_frames);
39
40 let denom = (fade_frames - 1).max(1) as f32;
41 for i in 0..fade_frames {
42 let t = i as f32 / denom;
43 let gain = curve.gain(t);
44 for c in 0..ch {
45 samples[i * ch + c] *= gain;
46 }
47 }
48 }
49
50 /// Apply a fade-out over the last `frames` frames.
51 pub fn apply_fade_out(samples: &mut [f32], channels: u16, frames: usize, curve: FadeCurve) {
52 let ch = channels as usize;
53 if ch == 0 || samples.is_empty() || frames == 0 {
54 return;
55 }
56
57 let total_frames = samples.len() / ch;
58 let fade_frames = frames.min(total_frames);
59 let start_frame = total_frames - fade_frames;
60
61 let denom = (fade_frames - 1).max(1) as f32;
62 for i in 0..fade_frames {
63 let t = 1.0 - (i as f32 / denom);
64 let gain = curve.gain(t);
65 let frame_idx = start_frame + i;
66 for c in 0..ch {
67 samples[frame_idx * ch + c] *= gain;
68 }
69 }
70 }
71
72 #[cfg(test)]
73 mod tests {
74 use super::*;
75
76 #[test]
77 fn fade_in_linear_mono() {
78 let mut samples = vec![1.0; 4];
79 apply_fade_in(&mut samples, 1, 4, FadeCurve::Linear);
80 // Frame 0: gain=0/3, Frame 1: gain=1/3, Frame 2: gain=2/3, Frame 3: gain=3/3
81 assert!((samples[0] - 0.0).abs() < 0.001);
82 assert!((samples[1] - 1.0 / 3.0).abs() < 0.001);
83 assert!((samples[2] - 2.0 / 3.0).abs() < 0.001);
84 assert!((samples[3] - 1.0).abs() < 0.001);
85 }
86
87 #[test]
88 fn fade_out_linear_mono() {
89 let mut samples = vec![1.0; 4];
90 apply_fade_out(&mut samples, 1, 4, FadeCurve::Linear);
91 // Frame 0: gain=3/3, Frame 1: gain=2/3, Frame 2: gain=1/3, Frame 3: gain=0/3
92 assert!((samples[0] - 1.0).abs() < 0.001);
93 assert!((samples[1] - 2.0 / 3.0).abs() < 0.001);
94 assert!((samples[2] - 1.0 / 3.0).abs() < 0.001);
95 assert!((samples[3] - 0.0).abs() < 0.001);
96 }
97
98 #[test]
99 fn fade_in_stereo() {
100 // 3 frames of stereo, all 1.0
101 let mut samples = vec![1.0; 6];
102 apply_fade_in(&mut samples, 2, 3, FadeCurve::Linear);
103 // Frame 0: gain=0/2, Frame 1: gain=1/2, Frame 2: gain=2/2
104 assert!((samples[0]).abs() < 0.001); // L0
105 assert!((samples[1]).abs() < 0.001); // R0
106 assert!((samples[2] - 0.5).abs() < 0.01);
107 assert!((samples[3] - 0.5).abs() < 0.01);
108 assert!((samples[4] - 1.0).abs() < 0.01);
109 assert!((samples[5] - 1.0).abs() < 0.01);
110 }
111
112 #[test]
113 fn fade_in_scurve() {
114 let mut samples = vec![1.0; 4];
115 apply_fade_in(&mut samples, 1, 4, FadeCurve::SCurve);
116 // First sample should be 0 (smoothstep(0) = 0)
117 assert!((samples[0]).abs() < 0.001);
118 // Middle should be between 0 and 1
119 assert!(samples[1] > 0.0 && samples[1] < 1.0);
120 assert!(samples[2] > samples[1]);
121 }
122
123 #[test]
124 fn fade_in_logarithmic() {
125 let mut samples = vec![1.0; 4];
126 apply_fade_in(&mut samples, 1, 4, FadeCurve::Logarithmic);
127 assert!((samples[0]).abs() < 0.001);
128 // Log curve rises quickly — frame 1 should be higher than linear
129 assert!(samples[1] > 0.25);
130 }
131
132 #[test]
133 fn fade_frames_clamped_to_length() {
134 let mut samples = vec![1.0; 3];
135 // Request 10 frames of fade on a 3-frame signal — should clamp
136 apply_fade_in(&mut samples, 1, 10, FadeCurve::Linear);
137 assert!((samples[0]).abs() < 0.001);
138 }
139
140 #[test]
141 fn fade_empty() {
142 let mut samples: Vec<f32> = vec![];
143 apply_fade_in(&mut samples, 1, 10, FadeCurve::Linear);
144 apply_fade_out(&mut samples, 1, 10, FadeCurve::Linear);
145 assert!(samples.is_empty());
146 }
147
148 #[test]
149 fn fade_zero_frames_is_noop() {
150 let original = vec![1.0, 0.5, -0.5];
151 let mut samples = original.clone();
152 apply_fade_in(&mut samples, 1, 0, FadeCurve::Linear);
153 assert_eq!(samples, original);
154 }
155
156 #[test]
157 fn fade_curve_gain_bounds() {
158 for curve in [FadeCurve::Linear, FadeCurve::Logarithmic, FadeCurve::SCurve] {
159 let g0 = curve.gain(0.0);
160 let g1 = curve.gain(1.0);
161 assert!(
162 g0.abs() < 0.001,
163 "{curve:?}: gain(0) should be ~0, got {g0}"
164 );
165 assert!(
166 (g1 - 1.0).abs() < 0.001,
167 "{curve:?}: gain(1) should be ~1, got {g1}"
168 );
169 }
170 }
171 }
172