pub fn crossfade(a: &[i16], b: &[i16], duration_ms: u32, sample_rate: u32) -> Vec<i16> {
let fade_samples = ((sample_rate as usize * duration_ms as usize) / 1000)
.min(a.len())
.min(b.len());
if fade_samples == 0 {
let mut out = a.to_vec();
out.extend_from_slice(b);
return out;
}
let a_body = a.len() - fade_samples;
let mut out = Vec::with_capacity(a.len() + b.len() - fade_samples);
out.extend_from_slice(&a[..a_body]);
for i in 0..fade_samples {
let t = i as f32 / fade_samples as f32;
let angle = t * std::f32::consts::FRAC_PI_2;
let gain_a = angle.cos(); let gain_b = angle.sin();
let sample = (a[a_body + i] as f32 * gain_a + b[i] as f32 * gain_b)
.round()
.clamp(i16::MIN as f32, i16::MAX as f32);
out.push(sample as i16);
}
out.extend_from_slice(&b[fade_samples..]);
out
}
#[cfg(test)]
mod tests {
use super::*;
const SR: u32 = 24_000;
fn constant(value: i16, n: usize) -> Vec<i16> {
vec![value; n]
}
#[test]
fn zero_duration_concatenates() {
let a = constant(100, 1000);
let b = constant(200, 1000);
let out = crossfade(&a, &b, 0, SR);
assert_eq!(out.len(), 2000);
assert_eq!(out[999], 100);
assert_eq!(out[1000], 200);
}
#[test]
fn output_length_is_sum_minus_fade() {
let a = constant(100, 24_000);
let b = constant(200, 24_000);
let fade_ms = 50u32;
let fade_samples = SR as usize * fade_ms as usize / 1000;
let out = crossfade(&a, &b, fade_ms, SR);
assert_eq!(out.len(), 24_000 + 24_000 - fade_samples);
}
#[test]
fn fade_region_is_between_the_two_signals() {
let n = 24_000usize;
let a: Vec<i16> = (0..n).map(|_| 10_000i16).collect();
let b: Vec<i16> = (0..n).map(|_| -10_000i16).collect();
let fade_ms = 100u32;
let out = crossfade(&a, &b, fade_ms, SR);
let fade_samples = SR as usize * fade_ms as usize / 1000;
let a_body = n - fade_samples;
let mid = out[a_body + fade_samples / 2];
assert!(
mid < 10_000 && mid > -10_000,
"mid-fade sample {} not between signals",
mid
);
}
#[test]
fn crossfade_into_empty_slice_works() {
let a = constant(100, 1000);
let out = crossfade(&a, &[], 50, SR);
assert_eq!(out.len(), a.len());
}
}