use realfft::num_complex::Complex;
use realfft::{ComplexToReal, RealFftPlanner, RealToComplex};
use std::sync::Arc;
pub struct FftHandler {
fft: Arc<dyn RealToComplex<f32>>,
ifft: Arc<dyn ComplexToReal<f32>>,
scratch: Vec<Complex<f32>>,
fwd_input_buf: Vec<f32>,
}
impl FftHandler {
pub fn new(size: usize) -> Self {
assert!(size > 0, "FFT size must be > 0");
assert!(
size.is_multiple_of(2),
"FFT size must be even for realfft (got {size})"
);
let mut planner = RealFftPlanner::<f32>::new();
let fft = planner.plan_fft_forward(size);
let ifft = planner.plan_fft_inverse(size);
let scratch_len = fft.get_scratch_len().max(ifft.get_scratch_len());
Self {
fft,
ifft,
scratch: vec![Complex::default(); scratch_len],
fwd_input_buf: vec![0.0f32; size],
}
}
#[inline]
pub fn size(&self) -> usize {
self.fwd_input_buf.len()
}
#[inline]
pub fn spectrum_size(&self) -> usize {
self.size() / 2 + 1
}
pub fn forward(&mut self, input: &[f32], output: &mut [Complex<f32>]) {
debug_assert_eq!(input.len(), self.size(), "forward: input length mismatch");
debug_assert_eq!(
output.len(),
self.spectrum_size(),
"forward: output length mismatch"
);
self.fwd_input_buf.copy_from_slice(input);
self.fft
.process_with_scratch(&mut self.fwd_input_buf, output, &mut self.scratch)
.expect("realfft forward: buffer lengths pre-asserted above");
}
pub fn inverse(&mut self, input: &mut [Complex<f32>], output: &mut [f32]) {
let n = self.size();
debug_assert_eq!(
input.len(),
self.spectrum_size(),
"inverse: input length mismatch"
);
debug_assert_eq!(output.len(), n, "inverse: output length mismatch");
self.ifft
.process_with_scratch(input, output, &mut self.scratch)
.expect("realfft inverse: buffer lengths pre-asserted above");
let norm = (n as f32).recip();
for sample in output.iter_mut() {
*sample *= norm;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f32::consts::PI;
const EPSILON: f32 = 1e-5;
#[test]
fn spectrum_size_is_half_plus_one() {
for &n in &[2usize, 4, 8, 16, 32, 64, 128, 256] {
let handler = FftHandler::new(n);
assert_eq!(handler.size(), n, "size({n})");
assert_eq!(handler.spectrum_size(), n / 2 + 1, "spectrum_size({n})");
}
}
#[test]
fn impulse_has_flat_spectrum() {
let n = 16;
let mut handler = FftHandler::new(n);
let mut input = vec![0.0f32; n];
input[0] = 1.0;
let mut spectrum = vec![Complex::default(); handler.spectrum_size()];
handler.forward(&input, &mut spectrum);
for bin in &spectrum {
let magnitude = (bin.re * bin.re + bin.im * bin.im).sqrt();
assert!(
(magnitude - 1.0).abs() < EPSILON,
"expected magnitude 1.0, got {magnitude}"
);
}
}
#[test]
fn dc_bin_matches_sum_and_is_real() {
let n = 8;
let mut handler = FftHandler::new(n);
let input = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let expected_dc: f32 = input.iter().sum();
let mut spectrum = vec![Complex::default(); handler.spectrum_size()];
handler.forward(&input, &mut spectrum);
assert!((spectrum[0].re - expected_dc).abs() < EPSILON);
assert!(spectrum[0].im.abs() < EPSILON, "DC im = {}", spectrum[0].im);
}
#[test]
fn nyquist_bin_is_real() {
let n = 16;
let mut handler = FftHandler::new(n);
let input: Vec<f32> = (0..n).map(|i| ((i * i) as f32) * 0.05).collect();
let mut spectrum = vec![Complex::default(); handler.spectrum_size()];
handler.forward(&input, &mut spectrum);
let nyquist_bin = spectrum[n / 2];
assert!(
nyquist_bin.im.abs() < EPSILON,
"Nyquist im must be 0, got {}",
nyquist_bin.im
);
}
#[test]
fn roundtrip_reconstruction() {
let n = 64;
let mut handler = FftHandler::new(n);
let original: Vec<f32> = (0..n)
.map(|i| (2.0 * PI * 4.0 * i as f32 / n as f32).sin())
.collect();
let mut spectrum = vec![Complex::default(); handler.spectrum_size()];
handler.forward(&original, &mut spectrum);
let mut reconstructed = vec![0.0f32; n];
handler.inverse(&mut spectrum, &mut reconstructed);
for (orig, recon) in original.iter().zip(reconstructed.iter()) {
assert!(
(orig - recon).abs() < EPSILON,
"roundtrip mismatch: {orig} != {recon}"
);
}
}
#[test]
fn linearity() {
let n = 32;
let mut handler = FftHandler::new(n);
let sz = handler.spectrum_size();
let a: Vec<f32> = (0..n).map(|i| i as f32).collect();
let b: Vec<f32> = (0..n).map(|i| (n - i) as f32).collect();
let ab: Vec<f32> = a.iter().zip(b.iter()).map(|(x, y)| x + y).collect();
let mut fa = vec![Complex::default(); sz];
let mut fb = vec![Complex::default(); sz];
let mut fab = vec![Complex::default(); sz];
handler.forward(&a, &mut fa);
handler.forward(&b, &mut fb);
handler.forward(&ab, &mut fab);
for k in 0..sz {
let sum = fa[k] + fb[k];
assert!(
(sum.re - fab[k].re).abs() < EPSILON && (sum.im - fab[k].im).abs() < EPSILON,
"linearity failed at bin {k}"
);
}
}
#[test]
fn forward_does_not_mutate_caller_input() {
let n = 8;
let mut handler = FftHandler::new(n);
let input = [0.1f32, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8];
let snapshot = input;
let mut spectrum = vec![Complex::default(); handler.spectrum_size()];
handler.forward(&input, &mut spectrum);
assert_eq!(input, snapshot, "forward must not mutate caller input");
}
}