#![allow(dead_code)]
use std::f32::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CrossfadeShape {
Linear,
EqualPower,
Sine,
SCurve,
SquareRoot,
Logarithmic,
}
impl CrossfadeShape {
pub fn fade_in(&self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
match self {
Self::Linear => t,
Self::EqualPower => (t * PI * 0.5).sin(),
Self::Sine => (t * PI * 0.5).sin(),
Self::SCurve => {
let s = t * PI;
(1.0 - s.cos()) * 0.5
}
Self::SquareRoot => t.sqrt(),
Self::Logarithmic => {
if t <= 0.0 {
0.0
} else {
#[allow(clippy::cast_precision_loss)]
let v = (1.0 + t * 99.0).log10() / 2.0_f32.log10();
v.clamp(0.0, 1.0)
}
}
}
}
pub fn fade_out(&self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
match self {
Self::Linear => 1.0 - t,
Self::EqualPower => ((1.0 - t) * PI * 0.5).sin(),
Self::Sine => ((1.0 - t) * PI * 0.5).sin(),
Self::SCurve => {
let s = t * PI;
(1.0 + s.cos()) * 0.5
}
Self::SquareRoot => (1.0 - t).sqrt(),
Self::Logarithmic => {
let inv = 1.0 - t;
if inv <= 0.0 {
0.0
} else {
#[allow(clippy::cast_precision_loss)]
let v = (1.0 + inv * 99.0).log10() / 2.0_f32.log10();
v.clamp(0.0, 1.0)
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct CrossfadeProcessor {
pub shape: CrossfadeShape,
pub length: usize,
}
impl CrossfadeProcessor {
pub fn new(shape: CrossfadeShape, length: usize) -> Self {
Self {
shape,
length: length.max(1),
}
}
pub fn apply(&self, source_a: &[f32], source_b: &[f32]) -> Vec<f32> {
let len = self.length.min(source_a.len()).min(source_b.len());
let mut output = Vec::with_capacity(len);
for i in 0..len {
#[allow(clippy::cast_precision_loss)]
let t = if len <= 1 {
1.0
} else {
i as f32 / (len - 1) as f32
};
let a_gain = self.shape.fade_out(t);
let b_gain = self.shape.fade_in(t);
output.push(source_a[i] * a_gain + source_b[i] * b_gain);
}
output
}
pub fn apply_into(&self, source_a: &[f32], source_b: &[f32], dest: &mut [f32]) {
let len = self
.length
.min(source_a.len())
.min(source_b.len())
.min(dest.len());
for i in 0..len {
#[allow(clippy::cast_precision_loss)]
let t = if len <= 1 {
1.0
} else {
i as f32 / (len - 1) as f32
};
dest[i] = source_a[i] * self.shape.fade_out(t) + source_b[i] * self.shape.fade_in(t);
}
}
pub fn length_samples(&self) -> usize {
self.length
}
pub fn gains_at(&self, position: usize) -> (f32, f32) {
#[allow(clippy::cast_precision_loss)]
let t = if self.length <= 1 {
1.0
} else {
position as f32 / (self.length - 1) as f32
};
(self.shape.fade_out(t), self.shape.fade_in(t))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_linear_fade_endpoints() {
let shape = CrossfadeShape::Linear;
assert!((shape.fade_in(0.0)).abs() < 1e-6);
assert!((shape.fade_in(1.0) - 1.0).abs() < 1e-6);
assert!((shape.fade_out(0.0) - 1.0).abs() < 1e-6);
assert!((shape.fade_out(1.0)).abs() < 1e-6);
}
#[test]
fn test_equal_power_endpoints() {
let shape = CrossfadeShape::EqualPower;
assert!((shape.fade_in(0.0)).abs() < 1e-6);
assert!((shape.fade_in(1.0) - 1.0).abs() < 1e-6);
assert!((shape.fade_out(0.0) - 1.0).abs() < 1e-6);
assert!((shape.fade_out(1.0)).abs() < 1e-6);
}
#[test]
fn test_equal_power_midpoint_no_dip() {
let shape = CrossfadeShape::EqualPower;
let a = shape.fade_out(0.5);
let b = shape.fade_in(0.5);
let power = a * a + b * b;
assert!((power - 1.0).abs() < 1e-4);
}
#[test]
fn test_sine_endpoints() {
let shape = CrossfadeShape::Sine;
assert!(shape.fade_in(0.0) < 1e-6);
assert!((shape.fade_in(1.0) - 1.0).abs() < 1e-6);
}
#[test]
fn test_s_curve_endpoints() {
let shape = CrossfadeShape::SCurve;
assert!(shape.fade_in(0.0).abs() < 1e-6);
assert!((shape.fade_in(1.0) - 1.0).abs() < 1e-6);
assert!((shape.fade_out(0.0) - 1.0).abs() < 1e-6);
assert!(shape.fade_out(1.0).abs() < 1e-6);
}
#[test]
fn test_square_root_monotonic() {
let shape = CrossfadeShape::SquareRoot;
let mut prev = 0.0;
for i in 0..=10 {
#[allow(clippy::cast_precision_loss)]
let t = i as f32 / 10.0;
let v = shape.fade_in(t);
assert!(v >= prev - 1e-6);
prev = v;
}
}
#[test]
fn test_logarithmic_endpoints() {
let shape = CrossfadeShape::Logarithmic;
assert!(shape.fade_in(0.0).abs() < 1e-6);
assert!((shape.fade_in(1.0) - 1.0).abs() < 0.01);
}
#[test]
fn test_processor_apply() {
let proc = CrossfadeProcessor::new(CrossfadeShape::Linear, 5);
let a = vec![1.0; 5];
let b = vec![0.0; 5];
let mixed = proc.apply(&a, &b);
assert_eq!(mixed.len(), 5);
assert!((mixed[0] - 1.0).abs() < 1e-6);
assert!(mixed[4].abs() < 1e-6);
}
#[test]
fn test_processor_apply_into() {
let proc = CrossfadeProcessor::new(CrossfadeShape::EqualPower, 4);
let a = vec![1.0; 4];
let b = vec![1.0; 4];
let mut dest = vec![0.0; 4];
proc.apply_into(&a, &b, &mut dest);
for &v in &dest {
assert!(v >= 0.99);
}
}
#[test]
fn test_processor_length() {
let proc = CrossfadeProcessor::new(CrossfadeShape::Linear, 128);
assert_eq!(proc.length_samples(), 128);
}
#[test]
fn test_gains_at_endpoints() {
let proc = CrossfadeProcessor::new(CrossfadeShape::Linear, 100);
let (fo, fi) = proc.gains_at(0);
assert!((fo - 1.0).abs() < 1e-6);
assert!(fi.abs() < 1e-6);
let (fo, fi) = proc.gains_at(99);
assert!(fo.abs() < 1e-6);
assert!((fi - 1.0).abs() < 1e-6);
}
#[test]
fn test_clamp_out_of_range() {
let shape = CrossfadeShape::Linear;
assert!(shape.fade_in(-0.5).abs() < 1e-6);
assert!((shape.fade_in(2.0) - 1.0).abs() < 1e-6);
}
#[test]
fn test_minimum_length_is_one() {
let proc = CrossfadeProcessor::new(CrossfadeShape::Linear, 0);
assert_eq!(proc.length_samples(), 1);
}
#[test]
fn test_all_shapes_compile_and_run() {
let shapes = [
CrossfadeShape::Linear,
CrossfadeShape::EqualPower,
CrossfadeShape::Sine,
CrossfadeShape::SCurve,
CrossfadeShape::SquareRoot,
CrossfadeShape::Logarithmic,
];
for shape in &shapes {
let proc = CrossfadeProcessor::new(*shape, 16);
let a = vec![1.0; 16];
let b = vec![0.5; 16];
let mixed = proc.apply(&a, &b);
assert_eq!(mixed.len(), 16);
}
}
}