extern crate alloc;
use alloc::string::ToString;
use alloc::vec::Vec;
use resonant_fft::radix2;
use resonant_fft::Complex;
use crate::chunk::Chunk;
use crate::error::StreamError;
use crate::node::DspNode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FftOutput {
Magnitude,
Power,
}
#[derive(Debug, Clone)]
pub struct FftNode {
output: FftOutput,
}
impl FftNode {
#[must_use]
pub fn new() -> Self {
Self {
output: FftOutput::Magnitude,
}
}
#[must_use]
pub fn with_output(output: FftOutput) -> Self {
Self { output }
}
#[inline]
#[must_use]
pub fn output(&self) -> FftOutput {
self.output
}
}
impl Default for FftNode {
fn default() -> Self {
Self::new()
}
}
impl DspNode for FftNode {
fn process(&mut self, input: Chunk) -> Result<Chunk, StreamError> {
let sample_rate = input.sample_rate();
let channels = input.channels();
let samples = input.into_data();
let n = samples.len();
if n == 0 {
return Ok(Chunk::empty(sample_rate, channels));
}
let mut buf: Vec<Complex<f32>> = samples.iter().map(|&s| Complex::new(s, 0.0)).collect();
radix2::fft(&mut buf).map_err(|e| StreamError::ProcessingError(e.to_string()))?;
let out: Vec<f32> = match self.output {
FftOutput::Magnitude => buf.iter().map(|c: &Complex<f32>| c.norm()).collect(),
FftOutput::Power => buf.iter().map(|c: &Complex<f32>| c.norm_sqr()).collect(),
};
Ok(Chunk::new(out, sample_rate, channels))
}
fn reset(&mut self) {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dc_signal_energy_in_bin_zero() {
let mut node = FftNode::new();
let chunk = Chunk::new(vec![1.0; 8], 44100, 1);
let out = node.process(chunk).ok();
let data = out.as_ref().map(|c| c.data());
assert!(data.is_some());
let data = data.map(|d| d.to_vec());
let data = data.as_deref();
let dc = data.map(|d| d[0]);
assert!(dc.is_some_and(|v| v > 0.0));
if let Some(d) = data {
for &val in &d[1..] {
assert!(val.abs() < 1e-4, "non-DC bin has energy: {val}");
}
}
}
#[test]
fn power_output_mode() {
let mut node = FftNode::with_output(FftOutput::Power);
let chunk = Chunk::new(vec![1.0; 4], 44100, 1);
let out = node.process(chunk).ok();
let data = out.as_ref().map(|c| c.data());
assert!(data.is_some());
let dc_power = data.map(|d| d[0]);
assert!(dc_power.is_some_and(|v| (v - 16.0).abs() < 1e-4));
}
#[test]
fn non_power_of_two_fails() {
let mut node = FftNode::new();
let chunk = Chunk::new(vec![1.0; 3], 44100, 1);
let err = node.process(chunk).err();
assert!(err.is_some());
if let Some(StreamError::ProcessingError(msg)) = &err {
assert!(msg.contains("not a power of two"));
}
}
#[test]
fn empty_chunk_passthrough() {
let mut node = FftNode::new();
let chunk = Chunk::empty(44100, 1);
let out = node.process(chunk).ok();
assert_eq!(out.as_ref().map(|c| c.is_empty()), Some(true));
}
#[test]
fn preserves_metadata() {
let mut node = FftNode::new();
let chunk = Chunk::new(vec![0.0; 8], 48000, 2);
let out = node.process(chunk).ok();
let out = out.as_ref();
assert_eq!(out.map(|c| c.sample_rate()), Some(48000));
assert_eq!(out.map(|c| c.channels()), Some(2));
}
#[test]
fn output_length_matches_input() {
let mut node = FftNode::new();
let chunk = Chunk::new(vec![0.0; 16], 44100, 1);
let out = node.process(chunk).ok();
assert_eq!(out.as_ref().map(|c| c.len()), Some(16));
}
#[test]
fn default_is_magnitude() {
let node = FftNode::default();
assert_eq!(node.output(), FftOutput::Magnitude);
}
#[test]
fn sine_wave_has_peak() {
let mut node = FftNode::new();
let n = 16;
let samples: Vec<f32> = (0..n)
.map(|i| (2.0 * core::f32::consts::PI * i as f32 / n as f32).sin())
.collect();
let chunk = Chunk::new(samples, 44100, 1);
let out = node.process(chunk).ok();
let data = out.as_ref().map(|c| c.data().to_vec());
if let Some(d) = data {
let peak_bin = d[1..n / 2]
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal))
.map(|(i, _)| i);
assert_eq!(peak_bin, Some(0)); }
}
}