1use 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 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 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 pub fn set_mix(&mut self, mix: f32) {
74 self.mix = T::from_f32(mix.clamp(0.0, 1.0));
75 }
76
77 pub fn set_feedback(&mut self, feedback: f32) {
79 self.feedback = T::from_f32(feedback.clamp(0.0, 0.99));
80 }
81
82 pub fn fft_size(&self) -> usize {
84 self.fft_size
85 }
86
87 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 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 for _ in 0..10 {
253 delay.process(&input, &mut output);
254 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}