|
| 1 | +use crate::AudioFrame; |
| 2 | + |
| 3 | +pub fn make_speech_gate_processor( |
| 4 | + threshold: f32, |
| 5 | + attack_ms: f32, |
| 6 | + release_ms: f32, |
| 7 | +) -> Box<dyn FnMut(&AudioFrame) -> AudioFrame> { |
| 8 | + // soft: (knee_width / threshold) |
| 9 | + // 0.01 / 0.0075 (best so far) |
| 10 | + // 0.05 / 0.025 |
| 11 | + // 0.5 / 0.030 (not very good) |
| 12 | + // 0.1 / 0.075 (but not too good.) |
| 13 | + make_speech_gate_processor_soft_rms(threshold, attack_ms, release_ms, 0.01) |
| 14 | +} |
| 15 | + |
| 16 | +// Returns a processing function that can be called for each AudioFrame (mono, 16kHz, i16) |
| 17 | +// #[allow(unused)] |
| 18 | +// pub fn make_speech_gate_processor_( |
| 19 | +// threshold: f32, |
| 20 | +// attack_ms: f32, |
| 21 | +// release_ms: f32, |
| 22 | +// ) -> Box<dyn FnMut(&AudioFrame) -> AudioFrame> { |
| 23 | +// let mut node = simple_speech_gate(threshold, 6.0, attack_ms, release_ms); |
| 24 | +// let mut sample_rate = None; |
| 25 | + |
| 26 | +// Box::new(move |frame: &AudioFrame| { |
| 27 | +// let frame_sample_rate = frame.format.sample_rate as f64; |
| 28 | +// match sample_rate { |
| 29 | +// None => { |
| 30 | +// node.set_sample_rate(frame_sample_rate); |
| 31 | +// sample_rate = Some(frame_sample_rate); |
| 32 | +// } |
| 33 | +// Some(rate) if rate != frame_sample_rate => { |
| 34 | +// panic!("Changing frame sample rate is not supported in the speech gate processor"); |
| 35 | +// } |
| 36 | +// Some(_) => { |
| 37 | +// // same rate, all good |
| 38 | +// } |
| 39 | +// } |
| 40 | + |
| 41 | +// let samples_f32: Vec<f32> = frame.samples.iter().map(|&s| s as f32 / 32768.0).collect(); |
| 42 | +// let processed: Vec<f32> = samples_f32 |
| 43 | +// .iter() |
| 44 | +// .map(|&sample| node.tick(&NumericArray::from([sample]))[0]) |
| 45 | +// .collect(); |
| 46 | +// let processed_i16: Vec<i16> = processed |
| 47 | +// .iter() |
| 48 | +// .map(|&s| (s.clamp(-1.0, 1.0) * 32767.0) as i16) |
| 49 | +// .collect(); |
| 50 | +// AudioFrame { |
| 51 | +// format: frame.format, |
| 52 | +// samples: processed_i16, |
| 53 | +// } |
| 54 | +// }) |
| 55 | +// } |
| 56 | + |
| 57 | +// fn simple_speech_gate( |
| 58 | +// threshold: f32, |
| 59 | +// softness: f32, // e.g., 6.0 dB for soft knee |
| 60 | +// attack_ms: f32, |
| 61 | +// release_ms: f32, |
| 62 | +// ) -> An<impl AudioNode<Inputs = U1, Outputs = U1>> { |
| 63 | +// let envelope_follower = envelope(|x| x * x) |
| 64 | +// >> lowpass_hz(10.0, 1.0) // smoother RMS, ~100 ms |
| 65 | +// >> map(|x| x[0].sqrt()); |
| 66 | + |
| 67 | +// let soft_gate = envelope_follower |
| 68 | +// >> map(move |level| { |
| 69 | +// let db = 20.0 * level[0].log10().max(-120.0); |
| 70 | +// let gain_db = if db < threshold - softness { |
| 71 | +// -60.0 // silence |
| 72 | +// } else if db > threshold + softness { |
| 73 | +// 0.0 // full gain |
| 74 | +// } else { |
| 75 | +// // Linear ramp over 2 * softness dB |
| 76 | +// -60.0 * (1.0 - (db - (threshold - softness)) / (2.0 * softness)) |
| 77 | +// }; |
| 78 | +// db_to_gain(gain_db) |
| 79 | +// }) |
| 80 | +// >> afollow(attack_ms / 1000.0, release_ms / 1000.0); |
| 81 | + |
| 82 | +// pass() * soft_gate |
| 83 | +// } |
| 84 | + |
| 85 | +// Convert dB to linear gain |
| 86 | +// fn db_to_gain(db: f32) -> f32 { |
| 87 | +// 10.0_f32.powf(db / 20.0) |
| 88 | +// } |
| 89 | + |
| 90 | +// #[allow(unused)] |
| 91 | +// fn simple_speech_gate_v1( |
| 92 | +// threshold: f32, |
| 93 | +// attack_ms: f32, |
| 94 | +// release_ms: f32, |
| 95 | +// ) -> An<impl AudioNode<Inputs = U1, Outputs = U1>> { |
| 96 | +// // Proper RMS envelope follower with 10ms window |
| 97 | +// let rms = envelope(|x| x * x) >> lowpass_hz(100.0, 1.0) >> map(|x| x[0].sqrt()); |
| 98 | + |
| 99 | +// // Gate control with smoothing using follow |
| 100 | +// let gate_control = rms |
| 101 | +// >> map(move |level| if level[0] > threshold { 1.0 } else { 0.0 }) |
| 102 | +// >> afollow(attack_ms / 1000.0, release_ms / 1000.0); |
| 103 | + |
| 104 | +// // Apply gating |
| 105 | +// pass() * gate_control |
| 106 | +// } |
| 107 | + |
| 108 | +/// Returns a processing function that applies an attack/release envelope-based speech gate (no fundsp), with lazy sample rate initialization and a hard threshold (no knee). |
| 109 | +/// Works well with 0.0025 threshold for the examples. |
| 110 | +#[allow(unused)] |
| 111 | +pub fn make_speech_gate_processor_hard( |
| 112 | + threshold: f32, // normalized, 0.0 to 1.0 |
| 113 | + attack_ms: f32, |
| 114 | + release_ms: f32, |
| 115 | +) -> Box<dyn FnMut(&AudioFrame) -> AudioFrame> { |
| 116 | + let mut envelope = 0.0f32; |
| 117 | + let mut sample_rate: Option<f32> = None; |
| 118 | + let mut attack_coeff = 0.0f32; |
| 119 | + let mut release_coeff = 0.0f32; |
| 120 | + Box::new(move |frame: &AudioFrame| { |
| 121 | + if sample_rate.is_none() { |
| 122 | + let sr = frame.format.sample_rate as f32; |
| 123 | + sample_rate = Some(sr); |
| 124 | + attack_coeff = (-1.0 / (attack_ms * 0.001 * sr)).exp(); |
| 125 | + release_coeff = (-1.0 / (release_ms * 0.001 * sr)).exp(); |
| 126 | + } |
| 127 | + let mut samples_i16 = Vec::with_capacity(frame.samples.len()); |
| 128 | + for &s in frame.samples.iter() { |
| 129 | + let sample_f32 = s as f32 / 32768.0; |
| 130 | + let energy = sample_f32 * sample_f32; |
| 131 | + if energy > envelope { |
| 132 | + envelope = attack_coeff * (envelope - energy) + energy; |
| 133 | + } else { |
| 134 | + envelope = release_coeff * (envelope - energy) + energy; |
| 135 | + } |
| 136 | + let gain = if envelope >= threshold { 1.0 } else { 0.0 }; |
| 137 | + samples_i16.push((s as f32 * gain) as i16); |
| 138 | + } |
| 139 | + AudioFrame { |
| 140 | + format: frame.format, |
| 141 | + samples: samples_i16, |
| 142 | + } |
| 143 | + }) |
| 144 | +} |
| 145 | + |
| 146 | +/// Returns a processing function that applies an attack/release envelope-based speech gate (no |
| 147 | +/// fundsp), with lazy sample rate initialization and a soft knee. |
| 148 | +/// |
| 149 | +/// # Parameters |
| 150 | +/// - `threshold`: The normalized level (0.0 to 1.0) above which audio passes through. |
| 151 | +/// - `attack_ms`: How quickly the gate opens when audio gets louder (milliseconds). |
| 152 | +/// - `release_ms`: How quickly the gate closes when audio gets quieter (milliseconds). |
| 153 | +/// - `knee_width`: Controls how gradually the gate transitions from closed to open near the |
| 154 | +/// threshold. A small knee width makes the gate act like an on/off switch; a larger knee width |
| 155 | +/// makes the gate fade in and out more smoothly as the audio approaches the threshold. |
| 156 | +/// |
| 157 | +/// In layman's terms: knee width is the "fade zone" around the threshold where the gate is |
| 158 | +/// partially open, making the transition less abrupt. |
| 159 | +#[allow(unused)] |
| 160 | +pub fn make_speech_gate_processor_soft( |
| 161 | + threshold: f32, // normalized, 0.0 to 1.0 |
| 162 | + attack_ms: f32, |
| 163 | + release_ms: f32, |
| 164 | + knee_width: f32, // e.g. 0.05 for a soft knee |
| 165 | +) -> Box<dyn FnMut(&AudioFrame) -> AudioFrame> { |
| 166 | + let mut envelope = 0.0f32; |
| 167 | + let mut sample_rate: Option<f32> = None; |
| 168 | + let mut attack_coeff = 0.0f32; |
| 169 | + let mut release_coeff = 0.0f32; |
| 170 | + Box::new(move |frame: &AudioFrame| { |
| 171 | + if sample_rate.is_none() { |
| 172 | + let sr = frame.format.sample_rate as f32; |
| 173 | + sample_rate = Some(sr); |
| 174 | + attack_coeff = (-1.0 / (attack_ms * 0.001 * sr)).exp(); |
| 175 | + release_coeff = (-1.0 / (release_ms * 0.001 * sr)).exp(); |
| 176 | + } |
| 177 | + let mut samples_i16 = Vec::with_capacity(frame.samples.len()); |
| 178 | + for &s in frame.samples.iter() { |
| 179 | + let sample_f32 = s as f32 / 32768.0; |
| 180 | + let energy = sample_f32 * sample_f32; |
| 181 | + if energy > envelope { |
| 182 | + envelope = attack_coeff * (envelope - energy) + energy; |
| 183 | + } else { |
| 184 | + envelope = release_coeff * (envelope - energy) + energy; |
| 185 | + } |
| 186 | + let gain = if envelope >= threshold + knee_width { |
| 187 | + 1.0 |
| 188 | + } else if envelope <= threshold - knee_width { |
| 189 | + 0.0 |
| 190 | + } else { |
| 191 | + // Linear ramp in the knee region |
| 192 | + 0.5 + 0.5 * (envelope - threshold) / knee_width |
| 193 | + }; |
| 194 | + samples_i16.push((s as f32 * gain) as i16); |
| 195 | + } |
| 196 | + AudioFrame { |
| 197 | + format: frame.format, |
| 198 | + samples: samples_i16, |
| 199 | + } |
| 200 | + }) |
| 201 | +} |
| 202 | + |
| 203 | +/// Returns a processing function that applies an RMS-based speech gate with |
| 204 | +/// attack/release envelope for smoother transitions and better echo suppression. |
| 205 | +/// |
| 206 | +/// # Parameters |
| 207 | +/// - `threshold`: The normalized level (0.0 to 1.0) above which audio passes through. |
| 208 | +/// - `attack_ms`: How quickly the gate opens when audio gets louder (milliseconds). |
| 209 | +/// - `release_ms`: How quickly the gate closes when audio gets quieter (milliseconds). |
| 210 | +/// - `knee_width`: Controls how gradually the gate transitions from closed to open near the |
| 211 | +/// threshold. Recommended value: 0.01-0.05 |
| 212 | +/// |
| 213 | +/// This implementation improves on the basic soft knee gate by using RMS energy detection |
| 214 | +/// for smoother, more natural speech gating and better echo suppression. |
| 215 | +/// |
| 216 | +/// Claude 3.7 |
| 217 | +/// |
| 218 | +/// soft_rms: (knee_width / threshold) |
| 219 | +/// 0.01 / 0.0025 (echo example barely audible) |
| 220 | +/// Optimization: Take the buffer of the AudioFrame and return it, input is not used anymore. |
| 221 | +pub fn make_speech_gate_processor_soft_rms( |
| 222 | + threshold: f32, // normalized, 0.0 to 1.0 |
| 223 | + attack_ms: f32, |
| 224 | + release_ms: f32, |
| 225 | + knee_width: f32, |
| 226 | +) -> Box<dyn FnMut(&AudioFrame) -> AudioFrame + Send + Sync> { |
| 227 | + let mut envelope = 0.0f32; |
| 228 | + let mut sample_rate: Option<f32> = None; |
| 229 | + let mut attack_coeff = 0.0f32; |
| 230 | + let mut release_coeff = 0.0f32; |
| 231 | + |
| 232 | + // For RMS calculation |
| 233 | + const RMS_WINDOW_SIZE: usize = 16; // Small window for responsive detection |
| 234 | + let mut rms_buffer = [0.0f32; RMS_WINDOW_SIZE]; |
| 235 | + let mut buffer_pos = 0; |
| 236 | + |
| 237 | + Box::new(move |frame: &AudioFrame| { |
| 238 | + if sample_rate.is_none() { |
| 239 | + let sr = frame.format.sample_rate as f32; |
| 240 | + sample_rate = Some(sr); |
| 241 | + attack_coeff = (-1.0 / (attack_ms * 0.001 * sr)).exp(); |
| 242 | + release_coeff = (-1.0 / (release_ms * 0.001 * sr)).exp(); |
| 243 | + } |
| 244 | + |
| 245 | + // Pre-calculate threshold boundaries |
| 246 | + let lower_threshold = threshold - knee_width; |
| 247 | + let upper_threshold = threshold + knee_width; |
| 248 | + |
| 249 | + let mut samples_i16 = Vec::with_capacity(frame.samples.len()); |
| 250 | + for &s in frame.samples.iter() { |
| 251 | + let sample_f32 = s as f32 / 32768.0; |
| 252 | + |
| 253 | + // Update RMS calculation with sliding window |
| 254 | + rms_buffer[buffer_pos] = sample_f32 * sample_f32; |
| 255 | + buffer_pos = (buffer_pos + 1) % RMS_WINDOW_SIZE; |
| 256 | + |
| 257 | + // Calculate RMS energy |
| 258 | + let energy = rms_buffer.iter().sum::<f32>() / RMS_WINDOW_SIZE as f32; |
| 259 | + |
| 260 | + // Apply envelope follower with different attack/release |
| 261 | + if energy > envelope { |
| 262 | + envelope = attack_coeff * (envelope - energy) + energy; |
| 263 | + } else { |
| 264 | + envelope = release_coeff * (envelope - energy) + energy; |
| 265 | + } |
| 266 | + |
| 267 | + // Apply soft knee gate |
| 268 | + let gain = if envelope >= upper_threshold { |
| 269 | + 1.0 |
| 270 | + } else if envelope <= lower_threshold { |
| 271 | + 0.0 |
| 272 | + } else { |
| 273 | + // Smoother transition using quadratic curve |
| 274 | + let t = (envelope - lower_threshold) / (upper_threshold - lower_threshold); |
| 275 | + t * t // Quadratic curve sounds more natural than linear |
| 276 | + }; |
| 277 | + |
| 278 | + samples_i16.push((sample_f32 * gain * 32767.0) as i16); |
| 279 | + } |
| 280 | + |
| 281 | + AudioFrame { |
| 282 | + format: frame.format, |
| 283 | + samples: samples_i16, |
| 284 | + } |
| 285 | + }) |
| 286 | +} |
0 commit comments