//! Fade in/out with selectable curve shapes. /// Fade curve shape. #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum FadeCurve { Linear, Logarithmic, SCurve, } impl FadeCurve { /// Compute the gain at position `t` (0.0 = start, 1.0 = end of fade region). /// Returns a value in [0.0, 1.0]. fn gain(&self, t: f32) -> f32 { match self { FadeCurve::Linear => t, FadeCurve::Logarithmic => { // Attempt a logarithmic curve: fast rise then flattening // log2(1 + t) / log2(2) = log2(1 + t) (1.0 + t).log2() } FadeCurve::SCurve => { // Smoothstep: 3t^2 - 2t^3 t * t * (3.0 - 2.0 * t) } } } } /// Apply a fade-in over the first `frames` frames. pub fn apply_fade_in(samples: &mut [f32], channels: u16, frames: usize, curve: FadeCurve) { let ch = channels as usize; if ch == 0 || samples.is_empty() || frames == 0 { return; } let total_frames = samples.len() / ch; let fade_frames = frames.min(total_frames); let denom = (fade_frames - 1).max(1) as f32; for i in 0..fade_frames { let t = i as f32 / denom; let gain = curve.gain(t); for c in 0..ch { samples[i * ch + c] *= gain; } } } /// Apply a fade-out over the last `frames` frames. pub fn apply_fade_out(samples: &mut [f32], channels: u16, frames: usize, curve: FadeCurve) { let ch = channels as usize; if ch == 0 || samples.is_empty() || frames == 0 { return; } let total_frames = samples.len() / ch; let fade_frames = frames.min(total_frames); let start_frame = total_frames - fade_frames; let denom = (fade_frames - 1).max(1) as f32; for i in 0..fade_frames { let t = 1.0 - (i as f32 / denom); let gain = curve.gain(t); let frame_idx = start_frame + i; for c in 0..ch { samples[frame_idx * ch + c] *= gain; } } } #[cfg(test)] mod tests { use super::*; #[test] fn fade_in_linear_mono() { let mut samples = vec![1.0; 4]; apply_fade_in(&mut samples, 1, 4, FadeCurve::Linear); // Frame 0: gain=0/3, Frame 1: gain=1/3, Frame 2: gain=2/3, Frame 3: gain=3/3 assert!((samples[0] - 0.0).abs() < 0.001); assert!((samples[1] - 1.0 / 3.0).abs() < 0.001); assert!((samples[2] - 2.0 / 3.0).abs() < 0.001); assert!((samples[3] - 1.0).abs() < 0.001); } #[test] fn fade_out_linear_mono() { let mut samples = vec![1.0; 4]; apply_fade_out(&mut samples, 1, 4, FadeCurve::Linear); // Frame 0: gain=3/3, Frame 1: gain=2/3, Frame 2: gain=1/3, Frame 3: gain=0/3 assert!((samples[0] - 1.0).abs() < 0.001); assert!((samples[1] - 2.0 / 3.0).abs() < 0.001); assert!((samples[2] - 1.0 / 3.0).abs() < 0.001); assert!((samples[3] - 0.0).abs() < 0.001); } #[test] fn fade_in_stereo() { // 3 frames of stereo, all 1.0 let mut samples = vec![1.0; 6]; apply_fade_in(&mut samples, 2, 3, FadeCurve::Linear); // Frame 0: gain=0/2, Frame 1: gain=1/2, Frame 2: gain=2/2 assert!((samples[0]).abs() < 0.001); // L0 assert!((samples[1]).abs() < 0.001); // R0 assert!((samples[2] - 0.5).abs() < 0.01); assert!((samples[3] - 0.5).abs() < 0.01); assert!((samples[4] - 1.0).abs() < 0.01); assert!((samples[5] - 1.0).abs() < 0.01); } #[test] fn fade_in_scurve() { let mut samples = vec![1.0; 4]; apply_fade_in(&mut samples, 1, 4, FadeCurve::SCurve); // First sample should be 0 (smoothstep(0) = 0) assert!((samples[0]).abs() < 0.001); // Middle should be between 0 and 1 assert!(samples[1] > 0.0 && samples[1] < 1.0); assert!(samples[2] > samples[1]); } #[test] fn fade_in_logarithmic() { let mut samples = vec![1.0; 4]; apply_fade_in(&mut samples, 1, 4, FadeCurve::Logarithmic); assert!((samples[0]).abs() < 0.001); // Log curve rises quickly — frame 1 should be higher than linear assert!(samples[1] > 0.25); } #[test] fn fade_frames_clamped_to_length() { let mut samples = vec![1.0; 3]; // Request 10 frames of fade on a 3-frame signal — should clamp apply_fade_in(&mut samples, 1, 10, FadeCurve::Linear); assert!((samples[0]).abs() < 0.001); } #[test] fn fade_empty() { let mut samples: Vec = vec![]; apply_fade_in(&mut samples, 1, 10, FadeCurve::Linear); apply_fade_out(&mut samples, 1, 10, FadeCurve::Linear); assert!(samples.is_empty()); } #[test] fn fade_zero_frames_is_noop() { let original = vec![1.0, 0.5, -0.5]; let mut samples = original.clone(); apply_fade_in(&mut samples, 1, 0, FadeCurve::Linear); assert_eq!(samples, original); } #[test] fn fade_curve_gain_bounds() { for curve in [FadeCurve::Linear, FadeCurve::Logarithmic, FadeCurve::SCurve] { let g0 = curve.gain(0.0); let g1 = curve.gain(1.0); assert!( g0.abs() < 0.001, "{curve:?}: gain(0) should be ~0, got {g0}" ); assert!( (g1 - 1.0).abs() < 0.001, "{curve:?}: gain(1) should be ~1, got {g1}" ); } } }