1const JUMP: i32 = 8000;
8const RAIL: i32 = 30000;
10const RATIO: f32 = 8.0;
12const PRE_MS: u32 = 2;
13const HOLD_MS: u32 = 20;
14const FADE_MS: u32 = 4;
15const ENV_MS: f32 = 100.0;
16
17pub struct Declicker {
18 delay: Vec<i16>,
19 head: usize,
20 prev: i32,
21 env: f32,
22 alpha: f32,
23 dc_x1: f32,
26 dc_y1: f32,
27 dc_r: f32,
28 hold: usize,
29 fade: usize,
30 mute_left: usize,
31 fade_left: usize,
32 triggers: u32,
33}
34
35impl Declicker {
36 pub fn new(rate: u32) -> Self {
37 let pre = (rate * PRE_MS / 1000).max(1) as usize;
38 Self {
39 delay: vec![0; pre],
40 head: 0,
41 prev: 0,
42 env: 300.0,
43 alpha: 1.0 / (ENV_MS / 1000.0 * rate as f32),
44 dc_x1: 0.0,
45 dc_y1: 0.0,
46 dc_r: 1.0 - 2.0 * std::f32::consts::PI * 20.0 / rate as f32,
47 hold: (rate * HOLD_MS / 1000) as usize,
48 fade: (rate * FADE_MS / 1000).max(1) as usize,
49 mute_left: 0,
50 fade_left: 0,
51 triggers: 0,
52 }
53 }
54
55 pub fn triggers(&self) -> u32 {
57 self.triggers
58 }
59
60 pub fn process(&mut self, block: &mut [i16]) {
62 for x in block.iter_mut() {
63 let xin = *x as f32;
65 let y = xin - self.dc_x1 + self.dc_r * self.dc_y1;
66 self.dc_x1 = xin;
67 self.dc_y1 = y;
68 *x = y.clamp(-32768.0, 32767.0) as i16;
69
70 let now = *x as i32;
71 let jump = (now - self.prev).abs();
72 self.prev = now;
73 let slam = now.abs() > RAIL && jump > JUMP;
76 let spike = jump > JUMP && (now.abs() as f32) > RATIO * self.env;
77 if self.mute_left == 0 && (slam || spike) {
78 self.triggers += 1;
79 self.mute_left = self.hold;
80 self.delay.iter_mut().for_each(|d| *d = 0);
81 }
82 let out = self.delay[self.head];
84 let mut input = *x;
85 if self.mute_left > 0 {
86 input = 0;
87 self.mute_left -= 1;
88 if self.mute_left == 0 {
89 self.fade_left = self.fade;
90 }
91 } else if self.fade_left > 0 {
92 input = ((*x as f32) * (1.0 - self.fade_left as f32 / self.fade as f32)) as i16;
93 self.fade_left -= 1;
94 } else {
95 let f = now as f32;
96 self.env = ((1.0 - self.alpha) * self.env * self.env + self.alpha * f * f).sqrt();
97 }
98 self.delay[self.head] = input;
99 self.head = (self.head + 1) % self.delay.len();
100 *x = out;
101 }
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
112 #[ignore]
113 fn real_recording() {
114 let Ok(path) = std::env::var("DECLICK_WAV") else { return };
115 let mut reader = hound::WavReader::open(&path).unwrap();
116 let spec = reader.spec();
117 let ch = spec.channels as usize;
118 let mut pcm: Vec<i16> = reader.samples::<i16>().map(|s| s.unwrap()).step_by(ch).collect();
119 let before: Vec<i16> = pcm.clone();
120 let mut d = Declicker::new(spec.sample_rate);
121 let mut last = 0u32;
122 let mut cuts = Vec::new();
123 for (i, block) in pcm.chunks_mut(spec.sample_rate as usize / 100).enumerate() {
124 d.process(block);
125 if d.triggers() != last {
126 last = d.triggers();
127 cuts.push(i as f32 / 100.0);
128 }
129 }
130 let peak_before = before.iter().map(|s| s.abs()).max().unwrap();
131 let peak_after = pcm.iter().map(|s| s.abs()).max().unwrap();
132 eprintln!("{path}: {} cut(s) at {:?} s; peak {peak_before} -> {peak_after}", d.triggers(), cuts);
133 }
134
135 fn voice(rate: u32, secs: f32) -> Vec<i16> {
136 let mut phase = 0f32;
138 (0..(rate as f32 * secs) as usize)
139 .map(|i| {
140 phase += 2.0 * std::f32::consts::PI * 140.0 / rate as f32;
141 let on = (i / (rate as usize / 5)) % 2 == 0;
142 let v: f32 = (1..=8).map(|h| (phase * h as f32).sin() / h as f32).sum();
143 if on { (v * 6000.0) as i16 } else { 0 }
144 })
145 .collect()
146 }
147
148 #[test]
149 fn a_cable_slam_is_cut_and_speech_is_not() {
150 let rate = 16_000;
151 let clean = voice(rate, 2.0);
152 let mut hit = clean.clone();
153 let at = rate as usize;
155 for k in 0..24 {
156 hit[at + k] = -32766;
157 }
158 for k in 24..(24 + 480) {
159 hit[at + k] = (20000.0 * (-(k as f32 - 24.0) / 160.0).exp()) as i16;
160 }
161 let mut d = Declicker::new(rate);
162 d.process(&mut hit);
163 let lag = (rate * PRE_MS / 1000) as usize;
164 let region = &hit[at - 16 + lag..at + 24 + 320 + lag];
166 assert!(region.iter().all(|s| s.abs() < 800), "residue {}", region.iter().map(|s| s.abs()).max().unwrap());
167 assert_eq!(d.triggers(), 1);
168 let mut pristine = clean.clone();
170 let mut d2 = Declicker::new(rate);
171 d2.process(&mut pristine);
172 assert_eq!(d2.triggers(), 0);
173 let rms = |v: &[i16]| (v.iter().map(|s| (*s as f64).powi(2)).sum::<f64>() / v.len() as f64).sqrt();
174 let ratio = rms(&pristine[lag..]) / rms(&clean[..clean.len() - lag]);
175 assert!((0.97..1.03).contains(&ratio), "loudness ratio {ratio}");
176 }
177}