Skip to main content

rill_fft/effects/
spectral_delay.rs

1// rill-fft/src/effects/spectral_delay.rs
2//! Spectral delay — frequency-dependent delay with feedback.
3//!
4//! Each frequency bin can be delayed independently, creating metallic
5//! resonances, comb filtering, and spectral shimmer effects. The delay
6//! is implemented as a circular buffer of FFT frames.
7
8use 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
15/// Spectral delay effect — applies different delay times to different frequency bins.
16///
17/// Stores a circular buffer of past FFT frames. Each bin's output is a mix
18/// of the current bin and a delayed version with optional feedback.
19///
20/// # Type parameters
21///
22/// - `T` — sample type (`f32` or `f64`)
23/// - `BUF_SIZE` — processing block size in samples
24/// - `MAX_DELAY` — maximum delay in FFT frames
25pub struct SpectralDelay<T: Transcendental, const BUF_SIZE: usize, const MAX_DELAY: usize> {
26    fft_size: usize,
27    half_plus_one: usize,
28    fft: RealFft<T>,
29    fft_in: Vec<T>,
30    fft_out: Vec<Complex<T>>,
31    ifft_out: Vec<T>,
32    overlap: Vec<T>,
33    delay_buffer: Vec<Vec<Complex<T>>>,
34    write_head: usize,
35    mix: T,
36    feedback: T,
37}
38
39impl<T: Transcendental, const BUF_SIZE: usize, const MAX_DELAY: usize>
40    SpectralDelay<T, BUF_SIZE, MAX_DELAY>
41{
42    /// Create a new spectral delay.
43    ///
44    /// # Panics
45    ///
46    /// Panics if `MAX_DELAY` is 0.
47    pub fn new() -> Self {
48        assert!(MAX_DELAY > 0, "MAX_DELAY must be at least 1");
49
50        let fft_size = rill_core::utils::next_power_of_two(2 * BUF_SIZE).max(4);
51        let half_plus_one = fft_size / 2 + 1;
52        let fft = RealFft::new(fft_size);
53        let overlap_len = fft_size - BUF_SIZE;
54
55        let delay_buffer = vec![vec![Complex::new(T::ZERO, T::ZERO); half_plus_one]; MAX_DELAY];
56
57        Self {
58            fft_size,
59            half_plus_one,
60            fft,
61            fft_in: vec![T::ZERO; fft_size],
62            fft_out: vec![Complex::new(T::ZERO, T::ZERO); half_plus_one],
63            ifft_out: vec![T::ZERO; fft_size],
64            overlap: vec![T::ZERO; overlap_len],
65            delay_buffer,
66            write_head: 0,
67            mix: T::from_f32(0.5),
68            feedback: T::from_f32(0.3),
69        }
70    }
71
72    /// Set wet/dry mix (0.0 = dry, 1.0 = wet).
73    pub fn set_mix(&mut self, mix: f32) {
74        self.mix = T::from_f32(mix.clamp(0.0, 1.0));
75    }
76
77    /// Set feedback amount (0.0 = no feedback, 0.99 = near infinite).
78    pub fn set_feedback(&mut self, feedback: f32) {
79        self.feedback = T::from_f32(feedback.clamp(0.0, 0.99));
80    }
81
82    /// Returns the FFT size.
83    pub fn fft_size(&self) -> usize {
84        self.fft_size
85    }
86
87    /// Process one block of samples.
88    ///
89    /// Combines the current spectrum with a delayed spectrum bin-by-bin.
90    /// Lower-frequency bins can have longer delays than higher ones,
91    /// creating unique spatial effects.
92    pub fn process(&mut self, input: &[T], output: &mut [T]) {
93        assert_eq!(input.len(), BUF_SIZE, "input must have BUF_SIZE elements");
94        assert_eq!(output.len(), BUF_SIZE, "output must have BUF_SIZE elements");
95
96        self.fft_in.fill(T::ZERO);
97        self.fft_in[..BUF_SIZE].copy_from_slice(input);
98        self.fft.forward(&self.fft_in, &mut self.fft_out);
99
100        let current = self.fft_out.clone();
101
102        let one = T::ONE;
103        let one_minus_mix = one - self.mix;
104
105        for (i, _cur) in current.iter().enumerate() {
106            let freq_ratio = T::from_usize(i) / T::from_usize(self.half_plus_one - 1);
107
108            let delay_frames = (one - freq_ratio) * T::from_usize(MAX_DELAY - 1);
109            let delay_int = delay_frames.to_f32() as usize;
110            let delay_frac = delay_frames - T::from_usize(delay_int);
111
112            let read_idx = if self.write_head >= delay_int {
113                self.write_head - delay_int
114            } else {
115                MAX_DELAY + self.write_head - delay_int
116            };
117
118            let delayed = self.delay_buffer[read_idx][i];
119
120            let delayed_mix = if delay_int + 1 < MAX_DELAY {
121                let next_idx = if read_idx > 0 {
122                    read_idx - 1
123                } else {
124                    MAX_DELAY - 1
125                };
126                let next = self.delay_buffer[next_idx][i];
127                Complex::new(
128                    delayed.re * (one - delay_frac) + next.re * delay_frac,
129                    delayed.im * (one - delay_frac) + next.im * delay_frac,
130                )
131            } else {
132                delayed
133            };
134
135            self.fft_out[i] = Complex::new(
136                current[i].re * one_minus_mix + delayed_mix.re * self.mix,
137                current[i].im * one_minus_mix + delayed_mix.im * self.mix,
138            );
139
140            // Store in delay buffer (current + feedback from delayed)
141            self.delay_buffer[self.write_head][i] = Complex::new(
142                current[i].re + delayed_mix.re * self.feedback,
143                current[i].im + delayed_mix.im * self.feedback,
144            );
145        }
146
147        self.write_head = (self.write_head + 1) % MAX_DELAY;
148
149        self.fft.inverse(&self.fft_out, &mut self.ifft_out);
150
151        for (out, (ifft_val, overlap_val)) in output
152            .iter_mut()
153            .zip(self.ifft_out.iter().zip(self.overlap.iter()))
154        {
155            *out = *ifft_val + *overlap_val;
156        }
157
158        let overlap_len = self.fft_size - BUF_SIZE;
159        self.overlap
160            .copy_from_slice(&self.ifft_out[BUF_SIZE..BUF_SIZE + overlap_len]);
161    }
162}
163
164impl<T: Transcendental, const BUF_SIZE: usize, const MAX_DELAY: usize> Default
165    for SpectralDelay<T, BUF_SIZE, MAX_DELAY>
166{
167    fn default() -> Self {
168        Self::new()
169    }
170}
171
172impl<T: Transcendental, const BUF_SIZE: usize, const MAX_DELAY: usize> Algorithm<T>
173    for SpectralDelay<T, BUF_SIZE, MAX_DELAY>
174{
175    fn process(&mut self, input: Option<&[T]>, output: &mut [T]) -> ProcessResult<()> {
176        match input {
177            Some(samples) => {
178                assert_eq!(
179                    samples.len(),
180                    BUF_SIZE,
181                    "SpectralDelay expects BUF_SIZE={} input",
182                    BUF_SIZE
183                );
184                assert_eq!(
185                    output.len(),
186                    BUF_SIZE,
187                    "SpectralDelay expects BUF_SIZE={} output",
188                    BUF_SIZE
189                );
190                self.process(samples, output);
191                Ok(())
192            }
193            None => {
194                output.fill(T::ZERO);
195                Ok(())
196            }
197        }
198    }
199
200    fn reset(&mut self) {
201        self.fft_in.fill(T::ZERO);
202        self.fft_out
203            .fill(num_complex::Complex::new(T::ZERO, T::ZERO));
204        self.ifft_out.fill(T::ZERO);
205        self.overlap.fill(T::ZERO);
206        self.delay_buffer.iter_mut().for_each(|buf| {
207            buf.fill(num_complex::Complex::new(T::ZERO, T::ZERO));
208        });
209        self.write_head = 0;
210    }
211
212    fn metadata(&self) -> AlgorithmMetadata {
213        AlgorithmMetadata {
214            name: "SpectralDelay",
215            category: AlgorithmCategory::Effect,
216            description: "Frequency-dependent delay via FFT",
217            author: "Rill",
218            version: env!("CARGO_PKG_VERSION"),
219        }
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_passthrough_no_delay() {
229        let mut delay = SpectralDelay::<f32, 64, 8>::new();
230        delay.set_mix(0.0);
231        delay.set_feedback(0.0);
232
233        let input: Vec<f32> = (0..64).map(|i| (i as f32 * 0.1).sin()).collect();
234        let mut output = vec![0.0f32; 64];
235        delay.process(&input, &mut output);
236
237        for (i, o) in input.iter().zip(output.iter()) {
238            assert!((i - o).abs() < 0.05, "expected {i}, got {o}");
239        }
240    }
241
242    #[test]
243    fn test_process_does_not_panic() {
244        let mut delay = SpectralDelay::<f32, 64, 8>::new();
245        delay.set_mix(0.5);
246        delay.set_feedback(0.3);
247
248        let input: Vec<f32> = (0..64).map(|i| (i as f32 * 0.15).sin()).collect();
249        let mut output = vec![0.0f32; 64];
250
251        // Process multiple blocks to ensure stability
252        for _ in 0..10 {
253            delay.process(&input, &mut output);
254            // Output should be finite (no NaN or infinity)
255            for o in output.iter() {
256                assert!(o.is_finite());
257            }
258        }
259    }
260
261    #[test]
262    fn test_zero_feedback_is_passthrough() {
263        let mut delay = SpectralDelay::<f32, 64, 4>::new();
264        delay.set_mix(0.0);
265        delay.set_feedback(0.0);
266
267        let block1: Vec<f32> = (0..64).map(|i| (i as f32 * 0.1).sin()).collect();
268        let block2: Vec<f32> = (64..128).map(|i| (i as f32 * 0.1).sin()).collect();
269
270        let mut out1 = vec![0.0f32; 64];
271        let mut out2 = vec![0.0f32; 64];
272
273        delay.process(&block1, &mut out1);
274        delay.process(&block2, &mut out2);
275
276        for (i, o) in block1.iter().zip(out1.iter()) {
277            assert!((i - o).abs() < 0.05, "block1: expected {i}, got {o}");
278        }
279        for (i, o) in block2.iter().zip(out2.iter()) {
280            assert!((i - o).abs() < 0.05, "block2: expected {i}, got {o}");
281        }
282    }
283}