rill_fft/effects/
spectral_gate.rs1use num_complex::Complex;
9use rill_core::traits::algorithm::{Algorithm, AlgorithmCategory, AlgorithmMetadata};
10use rill_core::traits::ProcessResult;
11use rill_core::Transcendental;
12
13use crate::real_fft::RealFft;
14
15pub struct SpectralGate<T: Transcendental, const BUF_SIZE: usize> {
22 fft_size: usize,
23 half_plus_one: usize,
24 fft: RealFft<T>,
25 fft_in: Vec<T>,
26 fft_out: Vec<Complex<T>>,
27 ifft_out: Vec<T>,
28 overlap: Vec<T>,
29 threshold: T,
30 ratio: f32,
31}
32
33impl<T: Transcendental, const BUF_SIZE: usize> SpectralGate<T, BUF_SIZE> {
34 pub fn new() -> Self {
36 let fft_size = rill_core::utils::next_power_of_two(2 * BUF_SIZE).max(4);
37 let half_plus_one = fft_size / 2 + 1;
38 let fft = RealFft::new(fft_size);
39 let overlap_len = fft_size - BUF_SIZE;
40
41 Self {
42 fft_size,
43 half_plus_one,
44 fft,
45 fft_in: vec![T::ZERO; fft_size],
46 fft_out: vec![Complex::new(T::ZERO, T::ZERO); half_plus_one],
47 ifft_out: vec![T::ZERO; fft_size],
48 overlap: vec![T::ZERO; overlap_len],
49 threshold: T::from_f32(0.01),
50 ratio: 0.0,
51 }
52 }
53
54 pub fn set_threshold(&mut self, threshold: T) {
56 self.threshold = threshold;
57 }
58
59 pub fn set_ratio(&mut self, ratio: f32) {
61 self.ratio = ratio.clamp(0.0, 1.0);
62 }
63
64 pub fn fft_size(&self) -> usize {
66 self.fft_size
67 }
68
69 pub fn process(&mut self, input: &[T], output: &mut [T]) {
71 assert_eq!(input.len(), BUF_SIZE, "input must have BUF_SIZE elements");
72 assert_eq!(output.len(), BUF_SIZE, "output must have BUF_SIZE elements");
73
74 self.fft_in.fill(T::ZERO);
75 self.fft_in[..BUF_SIZE].copy_from_slice(input);
76 self.fft.forward(&self.fft_in, &mut self.fft_out);
77
78 let one_minus_ratio = T::from_f32(1.0 - self.ratio);
79 for i in 0..self.half_plus_one {
80 let c = self.fft_out[i];
81 let mag_sq = c.re * c.re + c.im * c.im;
82 let mag = mag_sq.to_f64().sqrt() as f32;
83 if mag < self.threshold.to_f32() {
84 let scale = if self.ratio < 0.001 {
85 T::ZERO
86 } else {
87 T::from_f32((mag / self.threshold.to_f32()) * (1.0 - self.ratio))
88 / self.threshold
89 };
90 self.fft_out[i] = Complex::new(c.re * scale, c.im * scale);
91 } else {
92 let above = mag - self.threshold.to_f32();
94 let gain = T::from_f32(1.0) + one_minus_ratio * T::from_f32(above / (1.0 + above));
95 self.fft_out[i] = Complex::new(c.re * gain, c.im * gain);
96 }
97 }
98
99 self.fft.inverse(&self.fft_out, &mut self.ifft_out);
100
101 for (out, (ifft_val, overlap_val)) in output
102 .iter_mut()
103 .zip(self.ifft_out.iter().zip(self.overlap.iter()))
104 {
105 *out = *ifft_val + *overlap_val;
106 }
107
108 let overlap_len = self.fft_size - BUF_SIZE;
109 for i in 0..overlap_len {
110 self.overlap[i] = self.ifft_out[BUF_SIZE + i];
111 }
112 }
113}
114
115impl<T: Transcendental, const BUF_SIZE: usize> Default for SpectralGate<T, BUF_SIZE> {
116 fn default() -> Self {
117 Self::new()
118 }
119}
120
121impl<T: Transcendental, const BUF_SIZE: usize> Algorithm<T> for SpectralGate<T, BUF_SIZE> {
122 fn process(&mut self, input: Option<&[T]>, output: &mut [T]) -> ProcessResult<()> {
123 match input {
124 Some(samples) => {
125 assert_eq!(
126 samples.len(),
127 BUF_SIZE,
128 "SpectralGate expects BUF_SIZE={} input",
129 BUF_SIZE
130 );
131 assert_eq!(
132 output.len(),
133 BUF_SIZE,
134 "SpectralGate expects BUF_SIZE={} output",
135 BUF_SIZE
136 );
137 self.process(samples, output);
138 Ok(())
139 }
140 None => {
141 output.fill(T::ZERO);
142 Ok(())
143 }
144 }
145 }
146
147 fn reset(&mut self) {
148 self.fft_in.fill(T::ZERO);
149 self.fft_out
150 .fill(num_complex::Complex::new(T::ZERO, T::ZERO));
151 self.ifft_out.fill(T::ZERO);
152 self.overlap.fill(T::ZERO);
153 }
154
155 fn metadata(&self) -> AlgorithmMetadata {
156 AlgorithmMetadata {
157 name: "SpectralGate",
158 category: AlgorithmCategory::Effect,
159 description: "Frequency-domain noise gate via FFT",
160 author: "Rill",
161 version: env!("CARGO_PKG_VERSION"),
162 }
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_passthrough_with_high_ratio() {
172 let mut gate = SpectralGate::<f32, 64>::new();
173 gate.set_threshold(0.0);
174 gate.set_ratio(1.0);
175
176 let input: Vec<f32> = (0..64).map(|i| (i as f32 * 0.1).sin()).collect();
177 let mut output = vec![0.0f32; 64];
178 gate.process(&input, &mut output);
179
180 for (i, o) in input.iter().zip(output.iter()) {
181 assert!((i - o).abs() < 0.01, "expected {i}, got {o}");
182 }
183 }
184
185 #[test]
186 fn test_silence_with_zero_threshold_ratio() {
187 let mut gate = SpectralGate::<f32, 64>::new();
188 gate.set_threshold(100.0);
189 gate.set_ratio(0.0);
190
191 let input: Vec<f32> = (0..64).map(|i| (i as f32 * 0.1).sin()).collect();
192 let mut output = vec![0.0f32; 64];
193 gate.process(&input, &mut output);
194
195 for o in output.iter() {
196 assert!(o.abs() < 0.01);
197 }
198 }
199
200 #[test]
201 fn test_roundtrip_multiple_blocks() {
202 let mut gate = SpectralGate::<f32, 64>::new();
203 gate.set_threshold(0.0);
204 gate.set_ratio(1.0);
205
206 let block1: Vec<f32> = (0..64).map(|i| (i as f32 * 0.1).sin()).collect();
207 let block2: Vec<f32> = (64..128).map(|i| (i as f32 * 0.1).sin()).collect();
208
209 let mut out1 = vec![0.0f32; 64];
210 let mut out2 = vec![0.0f32; 64];
211
212 gate.process(&block1, &mut out1);
213 gate.process(&block2, &mut out2);
214
215 for (i, o) in block1.iter().zip(out1.iter()) {
216 assert!((i - o).abs() < 0.05, "block1 idx: expected {i}, got {o}");
217 }
218 for (i, o) in block2.iter().zip(out2.iter()) {
219 assert!((i - o).abs() < 0.05, "block2 idx: expected {i}, got {o}");
220 }
221 }
222}