Skip to main content

fft_convolver/
lib.rs

1#![deny(missing_debug_implementations)]
2#![doc = include_str!("../README.md")]
3
4mod fft;
5mod two_stage;
6mod utilities;
7use crate::fft::Fft;
8use crate::utilities::{
9    complex_multiply_accumulate, complex_size, copy_and_pad, next_power_of_2, sum,
10};
11use realfft::num_complex::Complex;
12use realfft::num_traits::Zero;
13use realfft::{FftError, FftNum};
14use rtsan_standalone::nonblocking;
15use thiserror::Error;
16pub use two_stage::TwoStageFFTConvolver;
17pub use utilities::compute_tail_block_size;
18
19#[derive(Error, Debug)]
20pub enum FFTConvolverError {
21    #[error("block size is not allowed to be zero")]
22    BlockSizeZero,
23    #[error("impulse response exceeds configured capacity")]
24    ImpulseResponseExceedsCapacity,
25    #[error("input and output buffers must have the same length")]
26    InputOutputLengthMismatch,
27    #[error("error in fft: {0}")]
28    Fft(#[from] FftError),
29}
30
31/// FFTConvolver
32/// Implementation of a partitioned FFT convolution algorithm with uniform block size.
33///
34/// Some notes on how to use it:
35/// - After initialization with an impulse response, subsequent data portions of
36///   arbitrary length can be convolved. The convolver internally can handle
37///   this by using appropriate buffering.
38/// - The convolver works without "latency" (except for the required
39///   processing time, of course), i.e. the output always is the convolved
40///   input for each processing call.
41///
42/// - The convolver is suitable for real-time processing which means that no
43///   "unpredictable" operations like allocations, locking, API calls, etc. are
44///   performed during processing (all necessary allocations and preparations take
45///   place during initialization).
46#[derive(Clone, Debug)]
47pub struct FFTConvolver<F: FftNum> {
48    ir_len: usize,
49    block_size: usize,
50    seg_size: usize,
51    seg_count: usize,
52    active_seg_count: usize,
53    fft_complex_size: usize,
54    segments: Vec<Vec<Complex<F>>>,
55    segments_ir: Vec<Vec<Complex<F>>>,
56    fft_buffer: Vec<F>,
57    fft: Fft<F>,
58    pre_multiplied: Vec<Complex<F>>,
59    conv: Vec<Complex<F>>,
60    overlap: Vec<F>,
61    current: usize,
62    input_buffer: Vec<F>,
63    input_buffer_fill: usize,
64}
65
66impl<F: FftNum> Default for FFTConvolver<F> {
67    fn default() -> Self {
68        Self {
69            ir_len: Default::default(),
70            block_size: Default::default(),
71            seg_size: Default::default(),
72            seg_count: Default::default(),
73            active_seg_count: Default::default(),
74            fft_complex_size: Default::default(),
75            segments: Default::default(),
76            segments_ir: Default::default(),
77            fft_buffer: Default::default(),
78            fft: Default::default(),
79            pre_multiplied: Default::default(),
80            conv: Default::default(),
81            overlap: Default::default(),
82            current: Default::default(),
83            input_buffer: Default::default(),
84            input_buffer_fill: Default::default(),
85        }
86    }
87}
88
89impl<F: FftNum> FFTConvolver<F> {
90    /// Initializes the convolver with an impulse response
91    ///
92    /// This method sets up all internal buffers and prepares the convolver for processing.
93    /// The block size determines the internal partition size and affects efficiency.
94    /// It will be rounded up to the next power of 2.
95    ///
96    /// All memory allocations happen during initialization, making subsequent processing
97    /// operations real-time safe.
98    ///
99    /// # Arguments
100    ///
101    /// * `block_size` - Block size internally used by the convolver (partition size).
102    ///   Will be rounded up to the next power of 2. Must be > 0.
103    /// * `impulse_response` - The impulse response to convolve with. Can be empty.
104    ///
105    /// # Returns
106    ///
107    /// Returns `BlockSizeZero` if block_size is 0.
108    ///
109    /// # Example
110    ///
111    /// ```
112    /// use fft_convolver::FFTConvolver;
113    ///
114    /// let mut convolver = FFTConvolver::<f32>::default();
115    /// let ir = vec![0.5, 0.3, 0.2, 0.1];
116    /// convolver.init(128, &ir).unwrap();
117    /// ```
118    pub fn init(
119        &mut self,
120        block_size: usize,
121        impulse_response: &[F],
122    ) -> Result<(), FFTConvolverError> {
123        if block_size == 0 {
124            return Err(FFTConvolverError::BlockSizeZero);
125        }
126
127        *self = Self::default();
128
129        self.ir_len = impulse_response.len();
130
131        if self.ir_len == 0 {
132            return Ok(());
133        }
134
135        self.block_size = next_power_of_2(block_size);
136        self.seg_size = 2 * self.block_size;
137        self.seg_count = (self.ir_len as f64 / self.block_size as f64).ceil() as usize;
138        self.active_seg_count = self.seg_count;
139        self.fft_complex_size = complex_size(self.seg_size);
140
141        // FFT
142        self.fft.init(self.seg_size);
143        self.fft_buffer = vec![F::zero(); self.seg_size];
144
145        // prepare segments
146        self.segments = vec![vec![Complex::zero(); self.fft_complex_size]; self.seg_count];
147
148        // prepare ir
149        self.segments_ir = vec![vec![Complex::zero(); self.fft_complex_size]; self.seg_count];
150        for (i, segment) in self.segments_ir.iter_mut().enumerate() {
151            let remaining = self.ir_len - (i * self.block_size);
152            let size_copy = if remaining >= self.block_size {
153                self.block_size
154            } else {
155                remaining
156            };
157            copy_and_pad(
158                &mut self.fft_buffer,
159                &impulse_response[i * self.block_size..],
160                size_copy,
161            );
162            self.fft.forward(&mut self.fft_buffer, segment)?;
163        }
164
165        // prepare convolution buffers
166        self.pre_multiplied = vec![Complex::zero(); self.fft_complex_size];
167        self.conv = vec![Complex::zero(); self.fft_complex_size];
168        self.overlap.resize(self.block_size, F::zero());
169
170        // prepare input buffer
171        self.input_buffer = vec![F::zero(); self.block_size];
172        self.input_buffer_fill = 0;
173
174        // reset current position
175        self.current = 0;
176
177        Ok(())
178    }
179
180    /// Updates the impulse response without reallocating buffers
181    ///
182    /// This method allows changing the impulse response at runtime while maintaining
183    /// real-time safety by avoiding allocations. The new impulse response must not
184    /// exceed the length of the original impulse response used during initialization.
185    ///
186    /// # Arguments
187    ///
188    /// * `impulse_response` - The new impulse response (must be ≤ original length)
189    ///
190    /// # Returns
191    ///
192    /// Returns `ImpulseResponseExceedsCapacity` if the new impulse response is longer
193    /// than the original one.
194    ///
195    /// # Example
196    ///
197    /// ```
198    /// use fft_convolver::FFTConvolver;
199    ///
200    /// let mut convolver = FFTConvolver::<f32>::default();
201    /// let ir1 = vec![0.5, 0.3, 0.2, 0.1];
202    /// convolver.init(4, &ir1).unwrap();
203    ///
204    /// // Update to a different impulse response of same or shorter length
205    /// let ir2 = vec![0.8, 0.6, 0.4];
206    /// convolver.set_response(&ir2).unwrap();
207    /// ```
208    #[nonblocking]
209    pub fn set_response(&mut self, impulse_response: &[F]) -> Result<(), FFTConvolverError> {
210        if impulse_response.len() > self.ir_len {
211            return Err(FFTConvolverError::ImpulseResponseExceedsCapacity);
212        }
213
214        self.fft_buffer.fill(F::zero());
215        self.conv.fill(Complex::zero());
216        self.pre_multiplied.fill(Complex::zero());
217        self.overlap.fill(F::zero());
218
219        self.active_seg_count =
220            (impulse_response.len() as f64 / self.block_size as f64).ceil() as usize;
221
222        // Prepare IR
223        for (i, segment) in self
224            .segments_ir
225            .iter_mut()
226            .enumerate()
227            .take(self.active_seg_count)
228        {
229            let remaining = impulse_response.len() - (i * self.block_size);
230            let size_copy = if remaining >= self.block_size {
231                self.block_size
232            } else {
233                remaining
234            };
235            copy_and_pad(
236                &mut self.fft_buffer,
237                &impulse_response[i * self.block_size..],
238                size_copy,
239            );
240            self.fft.forward(&mut self.fft_buffer, segment)?;
241        }
242
243        // Clear remaining segments
244        for segment in self.segments_ir.iter_mut().skip(self.active_seg_count) {
245            segment.fill(Complex::zero());
246        }
247
248        self.input_buffer.fill(F::zero());
249        self.input_buffer_fill = 0;
250        self.current = 0;
251        for segment in &mut self.segments {
252            segment.fill(Complex::zero());
253        }
254
255        Ok(())
256    }
257
258    /// Convolves the input samples with the impulse response and outputs the result
259    ///
260    /// This is a real-time safe operation that performs no allocations. Internal buffering
261    /// handles arbitrary sizes and ensures the output is always properly aligned with the
262    /// input (zero latency except for processing time).
263    ///
264    /// If the convolver has no active impulse response, the output is filled with zeros.
265    ///
266    /// # Arguments
267    ///
268    /// * `input` - The input samples to convolve
269    /// * `output` - Buffer to write the convolution result. Must have the same length as `input`.
270    ///
271    /// # Returns
272    ///
273    /// Returns `InputOutputLengthMismatch` if `input` and `output` have different lengths.
274    /// Returns `Fft` error if an FFT operation fails.
275    ///
276    /// # Example
277    ///
278    /// ```
279    /// use fft_convolver::FFTConvolver;
280    ///
281    /// let mut convolver = FFTConvolver::<f32>::default();
282    /// let ir = vec![0.5, 0.3, 0.2];
283    /// convolver.init(128, &ir).unwrap();
284    ///
285    /// let input = vec![1.0; 256];
286    /// let mut output = vec![0.0; 256];
287    /// convolver.process(&input, &mut output).unwrap();
288    /// ```
289    #[nonblocking]
290    pub fn process(&mut self, input: &[F], output: &mut [F]) -> Result<(), FFTConvolverError> {
291        if input.len() != output.len() {
292            return Err(FFTConvolverError::InputOutputLengthMismatch);
293        }
294
295        if self.active_seg_count == 0 {
296            output.fill(F::zero());
297            return Ok(());
298        }
299
300        let mut processed = 0;
301        while processed < output.len() {
302            let input_buffer_was_empty = self.input_buffer_fill == 0;
303            let processing = std::cmp::min(
304                output.len() - processed,
305                self.block_size - self.input_buffer_fill,
306            );
307
308            let input_buffer_pos = self.input_buffer_fill;
309            self.input_buffer[input_buffer_pos..input_buffer_pos + processing]
310                .copy_from_slice(&input[processed..processed + processing]);
311
312            // Forward FFT
313            copy_and_pad(&mut self.fft_buffer, &self.input_buffer, self.block_size);
314            if let Err(err) = self
315                .fft
316                .forward(&mut self.fft_buffer, &mut self.segments[self.current])
317            {
318                output.fill(F::zero());
319                return Err(err.into());
320            }
321
322            // complex multiplication
323            if input_buffer_was_empty {
324                self.pre_multiplied.fill(Complex::zero());
325                for i in 1..self.active_seg_count {
326                    let index_ir = i;
327                    let index_audio = (self.current + i) % self.active_seg_count;
328                    complex_multiply_accumulate(
329                        &mut self.pre_multiplied,
330                        &self.segments_ir[index_ir],
331                        &self.segments[index_audio],
332                    );
333                }
334            }
335            self.conv.copy_from_slice(&self.pre_multiplied);
336            complex_multiply_accumulate(
337                &mut self.conv,
338                &self.segments[self.current],
339                &self.segments_ir[0],
340            );
341
342            // Backward FFT
343            if let Err(err) = self.fft.inverse(&mut self.conv, &mut self.fft_buffer) {
344                output.fill(F::zero());
345                return Err(err.into());
346            }
347
348            // Add overlap
349            sum(
350                &mut output[processed..processed + processing],
351                &self.fft_buffer[input_buffer_pos..input_buffer_pos + processing],
352                &self.overlap[input_buffer_pos..input_buffer_pos + processing],
353            );
354
355            // Input buffer full => Next block
356            self.input_buffer_fill += processing;
357            if self.input_buffer_fill == self.block_size {
358                // Input buffer is empty again now
359                self.input_buffer.fill(F::zero());
360                self.input_buffer_fill = 0;
361                // Save the overlap
362                self.overlap
363                    .copy_from_slice(&self.fft_buffer[self.block_size..self.block_size * 2]);
364
365                // Update the current segment
366                self.current = if self.current > 0 {
367                    self.current - 1
368                } else {
369                    self.active_seg_count - 1
370                };
371            }
372            processed += processing;
373        }
374        Ok(())
375    }
376
377    /// Clears the internal processing state while preserving the impulse response
378    ///
379    /// This real-time safe operation resets all internal buffers that store the
380    /// convolution state, effectively removing any "history" or "tail" from previous
381    /// processing. The impulse response configuration remains intact, so processing
382    /// can continue immediately.
383    ///
384    /// This is useful when handling stream discontinuities such as:
385    /// - Seeking in audio playback
386    /// - Pause/resume operations with large time gaps
387    /// - Switching between different audio sources
388    ///
389    /// After calling `reset()`, the next `process()` call will produce output as if
390    /// the convolver had just been initialized.
391    ///
392    /// # Example
393    ///
394    /// ```
395    /// use fft_convolver::FFTConvolver;
396    ///
397    /// let mut convolver = FFTConvolver::<f32>::default();
398    /// let ir = vec![0.5, 0.3, 0.2];
399    /// convolver.init(128, &ir).unwrap();
400    ///
401    /// let input = vec![1.0; 256];
402    /// let mut output = vec![0.0; 256];
403    /// convolver.process(&input, &mut output).unwrap();
404    ///
405    /// // Clear the state when seeking to a new position
406    /// convolver.reset();
407    ///
408    /// // Continue processing with fresh state
409    /// convolver.process(&input, &mut output).unwrap();
410    /// ```
411    #[nonblocking]
412    pub fn reset(&mut self) {
413        self.input_buffer.fill(F::zero());
414        self.input_buffer_fill = 0;
415
416        self.fft_buffer.fill(F::zero());
417        for segment in &mut self.segments {
418            segment.fill(Complex::zero());
419        }
420
421        self.conv.fill(Complex::zero());
422        self.pre_multiplied.fill(Complex::zero());
423
424        self.overlap.fill(F::zero());
425        self.current = 0;
426    }
427}
428
429// Tests
430#[cfg(test)]
431mod tests {
432    use crate::{FFTConvolver, FFTConvolverError};
433
434    #[test]
435    fn init_test() {
436        let mut convolver = FFTConvolver::default();
437        let ir = vec![1., 0., 0., 0.];
438        convolver.init(10, &ir).unwrap();
439
440        assert_eq!(convolver.ir_len, 4);
441        assert_eq!(convolver.block_size, 16);
442        assert_eq!(convolver.seg_size, 32);
443        assert_eq!(convolver.seg_count, 1);
444        assert_eq!(convolver.active_seg_count, 1);
445        assert_eq!(convolver.fft_complex_size, 17);
446
447        assert_eq!(convolver.segments.len(), 1);
448        assert_eq!(convolver.segments.first().unwrap().len(), 17);
449        for seg in &convolver.segments {
450            for num in seg {
451                assert_eq!(num.re, 0.);
452                assert_eq!(num.im, 0.);
453            }
454        }
455
456        assert_eq!(convolver.segments_ir.len(), 1);
457        assert_eq!(convolver.segments_ir.first().unwrap().len(), 17);
458        for seg in &convolver.segments_ir {
459            for num in seg {
460                assert_eq!(num.re, 1.);
461                assert_eq!(num.im, 0.);
462            }
463        }
464
465        assert_eq!(convolver.fft_buffer.len(), 32);
466        assert_eq!(*convolver.fft_buffer.first().unwrap(), 1.);
467        for i in 1..convolver.fft_buffer.len() {
468            assert_eq!(convolver.fft_buffer[i], 0.);
469        }
470
471        assert_eq!(convolver.pre_multiplied.len(), 17);
472        for num in &convolver.pre_multiplied {
473            assert_eq!(num.re, 0.);
474            assert_eq!(num.im, 0.);
475        }
476
477        assert_eq!(convolver.conv.len(), 17);
478        for num in &convolver.conv {
479            assert_eq!(num.re, 0.);
480            assert_eq!(num.im, 0.);
481        }
482
483        assert_eq!(convolver.overlap.len(), 16);
484        for num in &convolver.overlap {
485            assert_eq!(*num, 0.);
486        }
487
488        assert_eq!(convolver.input_buffer.len(), 16);
489        for num in &convolver.input_buffer {
490            assert_eq!(*num, 0.);
491        }
492
493        assert_eq!(convolver.input_buffer_fill, 0);
494    }
495
496    #[test]
497    fn process_test() {
498        let mut convolver = FFTConvolver::<f32>::default();
499        let ir = vec![1., 0., 0., 0.];
500        convolver.init(2, &ir).unwrap();
501
502        let input = vec![0., 1., 2., 3.];
503        let mut output = vec![0.; 4];
504        convolver.process(&input, &mut output).unwrap();
505
506        for i in 0..output.len() {
507            assert_eq!(input[i], output[i]);
508        }
509    }
510
511    #[test]
512    fn reset_test() {
513        // Create an impulse response with actual filtering characteristics
514        let ir = vec![0.5, 0.3, 0.2, 0.1];
515        let block_size = 4;
516
517        // First convolver: process data, then clear, then process again
518        let mut convolver1 = FFTConvolver::<f32>::default();
519        convolver1.init(block_size, &ir).unwrap();
520
521        // Process some data to build up history
522        let history_input = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
523        let mut history_output = vec![0.0; 8];
524        convolver1
525            .process(&history_input, &mut history_output)
526            .unwrap();
527
528        // Clear the history
529        convolver1.reset();
530
531        // Process fresh data after clearing
532        let test_input = vec![1.0, 1.0, 1.0, 1.0];
533        let mut output1 = vec![0.0; 4];
534        convolver1.process(&test_input, &mut output1).unwrap();
535
536        // Second convolver: freshly initialized, process the same data
537        let mut convolver2 = FFTConvolver::<f32>::default();
538        convolver2.init(block_size, &ir).unwrap();
539        let mut output2 = vec![0.0; 4];
540        convolver2.process(&test_input, &mut output2).unwrap();
541
542        // The outputs should be identical if clear() truly cleared all history
543        for i in 0..output1.len() {
544            assert!(
545                (output1[i] - output2[i]).abs() < 1e-5,
546                "Mismatch at index {}: cleared convolver produced {}, fresh convolver produced {}",
547                i,
548                output1[i],
549                output2[i]
550            );
551        }
552    }
553
554    #[test]
555    fn reset_preserves_configuration() {
556        // Test that clear() preserves the convolver configuration
557        let ir = vec![0.5, 0.3, 0.2, 0.1];
558        let block_size = 4;
559
560        let mut convolver = FFTConvolver::<f32>::default();
561        convolver.init(block_size, &ir).unwrap();
562
563        let ir_len = convolver.ir_len;
564        let block_size_actual = convolver.block_size;
565        let seg_size = convolver.seg_size;
566        let seg_count = convolver.seg_count;
567
568        // Process some data
569        let input = vec![1.0, 2.0, 3.0, 4.0];
570        let mut output = vec![0.0; 4];
571        convolver.process(&input, &mut output).unwrap();
572
573        // Clear
574        convolver.reset();
575
576        // Configuration should be unchanged
577        assert_eq!(convolver.ir_len, ir_len);
578        assert_eq!(convolver.block_size, block_size_actual);
579        assert_eq!(convolver.seg_size, seg_size);
580        assert_eq!(convolver.seg_count, seg_count);
581    }
582
583    #[test]
584    fn set_response_equals_init() {
585        // Test that set_response produces the same results as init
586        let ir1 = vec![0.5, 0.3, 0.2, 0.1];
587        let ir2 = vec![0.8, 0.6, 0.4, 0.2];
588        let block_size = 4;
589
590        // Convolver 1: Initialize with ir1, then set_response to ir2
591        let mut convolver1 = FFTConvolver::<f32>::default();
592        convolver1.init(block_size, &ir1).unwrap();
593        convolver1.set_response(&ir2).unwrap();
594
595        // Convolver 2: Initialize directly with ir2
596        let mut convolver2 = FFTConvolver::<f32>::default();
597        convolver2.init(block_size, &ir2).unwrap();
598
599        // Process the same input with both convolvers
600        let input = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
601        let mut output1 = vec![0.0; 8];
602        let mut output2 = vec![0.0; 8];
603
604        convolver1.process(&input, &mut output1).unwrap();
605        convolver2.process(&input, &mut output2).unwrap();
606
607        // The outputs should be identical
608        for i in 0..output1.len() {
609            assert!(
610                (output1[i] - output2[i]).abs() < 1e-5,
611                "Mismatch at index {}: set_response produced {}, init produced {}",
612                i,
613                output1[i],
614                output2[i]
615            );
616        }
617    }
618
619    #[test]
620    fn set_response_with_shorter_ir() {
621        // Test that set_response works correctly with a shorter impulse response
622        let ir1 = vec![0.5, 0.3, 0.2, 0.1, 0.05, 0.02];
623        let ir2 = vec![0.8, 0.6, 0.4];
624        let block_size = 4;
625
626        // Initialize with longer IR, then set to shorter IR
627        let mut convolver1 = FFTConvolver::<f32>::default();
628        convolver1.init(block_size, &ir1).unwrap();
629        convolver1.set_response(&ir2).unwrap();
630
631        // Initialize directly with shorter IR
632        let mut convolver2 = FFTConvolver::<f32>::default();
633        convolver2.init(block_size, &ir2).unwrap();
634
635        // Process the same input
636        let input = vec![1.0, 1.0, 1.0, 1.0];
637        let mut output1 = vec![0.0; 4];
638        let mut output2 = vec![0.0; 4];
639
640        convolver1.process(&input, &mut output1).unwrap();
641        convolver2.process(&input, &mut output2).unwrap();
642
643        // The outputs should be identical
644        for i in 0..output1.len() {
645            assert!(
646                (output1[i] - output2[i]).abs() < 1e-5,
647                "Mismatch at index {}: set_response produced {}, init produced {}",
648                i,
649                output1[i],
650                output2[i]
651            );
652        }
653    }
654
655    #[test]
656    fn set_response_too_long_returns_error() {
657        // Test that set_response returns an error when IR is too long
658        let ir1 = vec![0.5, 0.3, 0.2, 0.1];
659        let ir2 = vec![0.8, 0.6, 0.4, 0.2, 0.1, 0.05];
660        let block_size = 4;
661
662        let mut convolver = FFTConvolver::<f32>::default();
663        convolver.init(block_size, &ir1).unwrap();
664
665        // Attempting to set a longer IR should fail
666        let result = convolver.set_response(&ir2);
667        assert!(result.is_err());
668        assert!(matches!(
669            result.unwrap_err(),
670            FFTConvolverError::ImpulseResponseExceedsCapacity
671        ));
672    }
673
674    #[test]
675    fn test_zero_latency() {
676        // Test that the algorithm has zero latency (no algorithmic delay)
677        // An impulse at input[0] should produce output starting at output[0]
678        let mut convolver = FFTConvolver::<f32>::default();
679
680        // Use a simple impulse response: just pass through with some gain
681        let ir = vec![0.5, 0.3, 0.2, 0.1];
682        convolver.init(4, &ir).unwrap();
683
684        // Send an impulse at the very first sample
685        let mut input = vec![0.0; 16];
686        input[0] = 1.0; // Impulse at position 0
687
688        let mut output = vec![0.0; 16];
689        convolver.process(&input, &mut output).unwrap();
690
691        // Check that the first output sample has the impulse response
692        // If there were latency, output[0] would be 0.0
693        assert!(
694            output[0].abs() > 0.0,
695            "Output[0] should be non-zero, indicating zero latency. Got: {}",
696            output[0]
697        );
698
699        // Verify the output matches the impulse response
700        assert!(
701            (output[0] - 0.5).abs() < 1e-5,
702            "output[0] should be 0.5, got {}",
703            output[0]
704        );
705        assert!(
706            (output[1] - 0.3).abs() < 1e-5,
707            "output[1] should be 0.3, got {}",
708            output[1]
709        );
710        assert!(
711            (output[2] - 0.2).abs() < 1e-5,
712            "output[2] should be 0.2, got {}",
713            output[2]
714        );
715        assert!(
716            (output[3] - 0.1).abs() < 1e-5,
717            "output[3] should be 0.1, got {}",
718            output[3]
719        );
720    }
721
722    #[test]
723    fn reinit_with_shorter_ir_matches_fresh_init() {
724        let long_ir = vec![0.5_f32; 1000];
725        let short_ir = vec![0.8_f32, 0.6, 0.4, 0.2];
726        let block_size = 4;
727
728        // Re-initialized convolver
729        let mut reinit = FFTConvolver::<f32>::default();
730        reinit.init(block_size, &long_ir).unwrap();
731        reinit.init(block_size, &short_ir).unwrap();
732
733        // Freshly initialized convolver
734        let mut fresh = FFTConvolver::<f32>::default();
735        fresh.init(block_size, &short_ir).unwrap();
736
737        let input = vec![1.0_f32; 64];
738        let mut output_reinit = vec![0.0_f32; 64];
739        let mut output_fresh = vec![0.0_f32; 64];
740
741        reinit.process(&input, &mut output_reinit).unwrap();
742        fresh.process(&input, &mut output_fresh).unwrap();
743
744        for i in 0..output_reinit.len() {
745            assert!(
746                (output_reinit[i] - output_fresh[i]).abs() < 1e-5,
747                "Mismatch at index {}: reinit produced {}, fresh produced {}",
748                i,
749                output_reinit[i],
750                output_fresh[i]
751            );
752        }
753    }
754
755    #[test]
756    fn process_mismatched_lengths_returns_error() {
757        let mut convolver = FFTConvolver::<f32>::default();
758        let ir = vec![1., 0., 0., 0.];
759        convolver.init(4, &ir).unwrap();
760
761        let input = vec![1.0; 4];
762        let mut output = vec![0.0; 8];
763        let result = convolver.process(&input, &mut output);
764        assert!(matches!(
765            result.unwrap_err(),
766            FFTConvolverError::InputOutputLengthMismatch
767        ));
768    }
769
770    #[test]
771    fn reinit_with_empty_ir_produces_silence() {
772        let long_ir = vec![0.5_f32; 1000];
773        let block_size = 4;
774
775        let mut convolver = FFTConvolver::<f32>::default();
776        convolver.init(block_size, &long_ir).unwrap();
777        convolver.init(block_size, &[]).unwrap();
778
779        let input = vec![1.0_f32; 64];
780        let mut output = vec![0.0_f32; 64];
781        convolver.process(&input, &mut output).unwrap();
782
783        for (i, &sample) in output.iter().enumerate() {
784            assert!(
785                sample.abs() < 1e-10,
786                "Expected silence at index {}, got {}",
787                i,
788                sample
789            );
790        }
791    }
792
793    #[test]
794    fn test_block_size_one() {
795        let mut convolver = FFTConvolver::<f32>::default();
796        let ir = vec![0.5_f32, 0.3, 0.2, 0.1];
797        convolver.init(1, &ir).unwrap();
798        assert_eq!(convolver.block_size, 1);
799
800        let mut input = vec![0.0_f32; 16];
801        input[0] = 1.0;
802        let mut output = vec![0.0_f32; 16];
803        convolver.process(&input, &mut output).unwrap();
804
805        assert!((output[0] - 0.5).abs() < 1e-5);
806        assert!((output[1] - 0.3).abs() < 1e-5);
807        assert!((output[2] - 0.2).abs() < 1e-5);
808        assert!((output[3] - 0.1).abs() < 1e-5);
809    }
810
811    #[test]
812    fn test_large_ir() {
813        // IR longer than a single block; verify output matches expectations over many segments
814        let ir_len = 8192_usize;
815        let block_size = 512;
816
817        let mut ir = vec![0.0_f32; ir_len];
818        ir[0] = 1.0; // identity
819
820        let mut convolver = FFTConvolver::<f32>::default();
821        convolver.init(block_size, &ir).unwrap();
822
823        let input: Vec<f32> = (0..ir_len).map(|i| i as f32 * 0.001).collect();
824        let mut output = vec![0.0_f32; ir_len];
825        convolver.process(&input, &mut output).unwrap();
826
827        for i in 0..ir_len {
828            assert!(
829                (output[i] - input[i]).abs() < 1e-4,
830                "Mismatch at {}: input={}, output={}",
831                i,
832                input[i],
833                output[i]
834            );
835        }
836    }
837}