pub fn decrackle(signal: &[f32], sample_rate: u32, sensitivity: f32) -> Vec<f32> {
let _ = sample_rate; let n = signal.len();
if n < 8 {
return signal.to_vec();
}
let sensitivity = sensitivity.clamp(1.0, 10.0);
let factor = 12.0 - sensitivity;
let mut det = vec![0.0f32; n];
for i in 1..n - 1 {
det[i] = signal[i] - 0.5 * (signal[i - 1] + signal[i + 1]);
}
let alpha = 0.001f32;
let mut floor = 1e-6f32;
let mut flags = vec![false; n];
for i in 0..n {
let a = det[i].abs();
floor = floor * (1.0 - alpha) + a * alpha;
if a > factor * floor.max(1e-6) {
flags[i] = true;
}
}
let mut out = signal.to_vec();
let mut i = 0;
while i < n {
if flags[i] {
let s = i;
while i < n && flags[i] {
i += 1;
}
repair_hermite(&mut out, s, i);
} else {
i += 1;
}
}
out
}
fn repair_hermite(out: &mut [f32], s: usize, e: usize) {
let n = out.len();
if s == 0 || e >= n {
let left = if s > 0 { out[s - 1] } else { out[e.min(n - 1)] };
let right = if e < n { out[e] } else { out[s.saturating_sub(1)] };
let span = (e - s + 1) as f32;
for (k, o) in out[s..e].iter_mut().enumerate() {
let t = (k + 1) as f32 / span;
*o = left * (1.0 - t) + right * t;
}
return;
}
let p0 = out[s - 1];
let p1 = out[e];
let m0 = if s >= 2 { out[s - 1] - out[s - 2] } else { 0.0 };
let m1 = if e + 1 < n { out[e + 1] - out[e] } else { 0.0 };
let span = (e - (s - 1)) as f32; for (k, o) in out[s..e].iter_mut().enumerate() {
let t = (k + 1) as f32 / span; let t2 = t * t;
let t3 = t2 * t;
let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
let h10 = t3 - 2.0 * t2 + t;
let h01 = -2.0 * t3 + 3.0 * t2;
let h11 = t3 - t2;
*o = h00 * p0 + h10 * (m0 * span) + h01 * p1 + h11 * (m1 * span);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reduces_crackle_keeps_tone() {
let sr = 48_000usize;
let two_pi = 2.0 * std::f32::consts::PI;
let clean: Vec<f32> =
(0..sr).map(|i| 0.4 * (two_pi * 500.0 * i as f32 / sr as f32).sin()).collect();
let mut rng = 0x1234_5678u64;
let mut rnd = || {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
(rng as f32 / u64::MAX as f32) - 0.5
};
let mut noisy = clean.clone();
for _ in 0..600 {
let pos = (rnd().abs() * (sr as f32 - 4.0)) as usize + 1;
noisy[pos] += if rnd() > 0.0 { 0.5 } else { -0.5 };
}
let out = decrackle(&noisy, sr as u32, 6.0);
let err = |x: &[f32]| x.iter().zip(&clean).map(|(a, b)| (a - b).powi(2)).sum::<f32>();
assert!(err(&out) < err(&noisy) * 0.5, "crackle not reduced");
let mag = |x: &[f32]| {
let (mut re, mut im) = (0.0f64, 0.0f64);
for (i, &v) in x.iter().enumerate() {
let p = 2.0 * std::f64::consts::PI * 500.0 * i as f64 / sr as f64;
re += v as f64 * p.cos();
im -= v as f64 * p.sin();
}
(re * re + im * im).sqrt()
};
assert!(mag(&out) > mag(&clean) * 0.8, "tone not preserved");
}
#[test]
fn clean_signal_barely_changes() {
let sr = 48_000usize;
let clean: Vec<f32> = (0..sr)
.map(|i| 0.3 * (2.0 * std::f32::consts::PI * 440.0 * i as f32 / sr as f32).sin())
.collect();
let out = decrackle(&clean, sr as u32, 5.0);
let diff: f32 = out.iter().zip(&clean).map(|(a, b)| (a - b).abs()).sum::<f32>() / sr as f32;
assert!(diff < 0.02, "clean signal altered too much: {diff}");
}
}