#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use crate::api::{Direction, Flags, Plan};
use crate::kernel::{Complex, Float};
use super::window::WindowFunction;
use super::RingBuffer;
pub struct StreamingFft<T: Float> {
fft_size: usize,
hop_size: usize,
window: Vec<T>,
forward_plan: Option<Plan<T>>,
inverse_plan: Option<Plan<T>>,
input_buffer: RingBuffer<T>,
output_buffer: Vec<T>,
output_pos: usize,
pending_frames: Vec<Vec<Complex<T>>>,
}
impl<T: Float> StreamingFft<T> {
pub fn new(fft_size: usize, hop_size: usize, window: WindowFunction) -> Self {
let window_coeffs = window.generate(fft_size);
let forward_plan = Plan::dft_1d(fft_size, Direction::Forward, Flags::ESTIMATE);
let inverse_plan = Plan::dft_1d(fft_size, Direction::Backward, Flags::ESTIMATE);
Self {
fft_size,
hop_size,
window: window_coeffs,
forward_plan,
inverse_plan,
input_buffer: RingBuffer::new(fft_size),
output_buffer: vec![T::ZERO; fft_size * 2],
output_pos: 0,
pending_frames: Vec::new(),
}
}
pub fn feed(&mut self, samples: &[T]) -> usize {
self.input_buffer.push_slice(samples);
let mut frames_processed = 0;
while self.input_buffer.len() >= self.fft_size {
if let Some(frame) = self.process_frame() {
self.pending_frames.push(frame);
frames_processed += 1;
}
self.input_buffer.advance(self.hop_size);
}
frames_processed
}
pub fn pop_frame(&mut self) -> Option<Vec<Complex<T>>> {
if self.pending_frames.is_empty() {
None
} else {
Some(self.pending_frames.remove(0))
}
}
fn process_frame(&self) -> Option<Vec<Complex<T>>> {
let plan = self.forward_plan.as_ref()?;
let mut frame = vec![T::ZERO; self.fft_size];
self.input_buffer.read_last(&mut frame);
let input: Vec<Complex<T>> = frame
.iter()
.zip(self.window.iter())
.map(|(&s, &w)| Complex::new(s * w, T::ZERO))
.collect();
let mut output = vec![Complex::<T>::zero(); self.fft_size];
plan.execute(&input, &mut output);
Some(output)
}
pub fn analyze_frame(&self, frame: &[T]) -> Vec<Complex<T>> {
if frame.len() != self.fft_size {
return vec![Complex::<T>::zero(); self.fft_size];
}
let plan = match &self.forward_plan {
Some(p) => p,
None => return vec![Complex::<T>::zero(); self.fft_size],
};
let input: Vec<Complex<T>> = frame
.iter()
.zip(self.window.iter())
.map(|(&s, &w)| Complex::new(s * w, T::ZERO))
.collect();
let mut output = vec![Complex::<T>::zero(); self.fft_size];
plan.execute(&input, &mut output);
output
}
pub fn synthesize_frame(&self, spectrum: &[Complex<T>]) -> Vec<T> {
if spectrum.len() != self.fft_size {
return vec![T::ZERO; self.fft_size];
}
let plan = match &self.inverse_plan {
Some(p) => p,
None => return vec![T::ZERO; self.fft_size],
};
let mut output = vec![Complex::<T>::zero(); self.fft_size];
plan.execute(spectrum, &mut output);
let scale = T::ONE / T::from_usize(self.fft_size);
output
.iter()
.zip(self.window.iter())
.map(|(c, &w)| c.re * scale * w)
.collect()
}
pub fn fft_size(&self) -> usize {
self.fft_size
}
pub fn hop_size(&self) -> usize {
self.hop_size
}
pub fn window(&self) -> &[T] {
&self.window
}
pub fn clear(&mut self) {
self.input_buffer.clear();
self.pending_frames.clear();
for v in &mut self.output_buffer {
*v = T::ZERO;
}
self.output_pos = 0;
}
}
pub fn stft<T: Float>(
signal: &[T],
fft_size: usize,
hop_size: usize,
window: WindowFunction,
) -> Vec<Vec<Complex<T>>> {
if signal.len() < fft_size || fft_size == 0 || hop_size == 0 {
return Vec::new();
}
let window_coeffs: Vec<T> = window.generate(fft_size);
let plan = match Plan::dft_1d(fft_size, Direction::Forward, Flags::ESTIMATE) {
Some(p) => p,
None => return Vec::new(),
};
let num_frames = (signal.len() - fft_size) / hop_size + 1;
let mut spectrogram = Vec::with_capacity(num_frames);
for frame_idx in 0..num_frames {
let start = frame_idx * hop_size;
let end = start + fft_size;
let input: Vec<Complex<T>> = signal[start..end]
.iter()
.zip(window_coeffs.iter())
.map(|(&s, &w)| Complex::new(s * w, T::ZERO))
.collect();
let mut output = vec![Complex::<T>::zero(); fft_size];
plan.execute(&input, &mut output);
spectrogram.push(output);
}
spectrogram
}
pub fn istft<T: Float>(
spectrogram: &[Vec<Complex<T>>],
hop_size: usize,
window: WindowFunction,
) -> Vec<T> {
if spectrogram.is_empty() || hop_size == 0 {
return Vec::new();
}
let fft_size = spectrogram[0].len();
if fft_size == 0 {
return Vec::new();
}
let window_coeffs: Vec<T> = window.generate(fft_size);
let plan = match Plan::dft_1d(fft_size, Direction::Backward, Flags::ESTIMATE) {
Some(p) => p,
None => return Vec::new(),
};
let num_frames = spectrogram.len();
let output_len = fft_size + (num_frames - 1) * hop_size;
let mut output = vec![T::ZERO; output_len];
let mut window_sum = vec![T::ZERO; output_len];
let scale = T::ONE / T::from_usize(fft_size);
for (frame_idx, spectrum) in spectrogram.iter().enumerate() {
if spectrum.len() != fft_size {
continue;
}
let mut frame = vec![Complex::<T>::zero(); fft_size];
plan.execute(spectrum, &mut frame);
let start = frame_idx * hop_size;
for i in 0..fft_size {
let w = window_coeffs[i];
output[start + i] = output[start + i] + frame[i].re * scale * w;
window_sum[start + i] = window_sum[start + i] + w * w;
}
}
let threshold = T::from_f64(1e-10);
for i in 0..output_len {
if window_sum[i] > threshold {
output[i] = output[i] / window_sum[i];
}
}
output
}
pub fn magnitude_spectrogram<T: Float>(spectrogram: &[Vec<Complex<T>>]) -> Vec<Vec<T>> {
spectrogram
.iter()
.map(|frame| frame.iter().map(|c| c.norm()).collect())
.collect()
}
pub fn power_spectrogram<T: Float>(spectrogram: &[Vec<Complex<T>>]) -> Vec<Vec<T>> {
spectrogram
.iter()
.map(|frame| frame.iter().map(|c| c.norm_sqr()).collect())
.collect()
}
pub fn phase_spectrogram<T: Float>(spectrogram: &[Vec<Complex<T>>]) -> Vec<Vec<T>> {
spectrogram
.iter()
.map(|frame| frame.iter().map(|c| c.im.atan2(c.re)).collect())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_streaming_fft_basic() {
let mut processor: StreamingFft<f64> = StreamingFft::new(8, 4, WindowFunction::Hann);
let samples = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let frames = processor.feed(&samples);
assert!(frames > 0);
assert!(processor.pop_frame().is_some());
}
#[test]
fn test_streaming_fft_analyze_synthesize() {
let processor: StreamingFft<f64> = StreamingFft::new(8, 4, WindowFunction::Hann);
let frame = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let spectrum = processor.analyze_frame(&frame);
let reconstructed = processor.synthesize_frame(&spectrum);
assert_eq!(spectrum.len(), 8);
assert_eq!(reconstructed.len(), 8);
}
#[test]
fn test_stft_basic() {
let signal: Vec<f64> = vec![0.0; 128];
let spectrogram = stft(&signal, 32, 16, WindowFunction::Hann);
assert_eq!(spectrogram.len(), 7);
assert_eq!(spectrogram[0].len(), 32);
}
#[test]
fn test_stft_istft_roundtrip() {
let n = 256;
let signal: Vec<f64> = (0..n).map(|i| (f64::from(i) / 10.0).sin()).collect();
let fft_size = 64;
let hop_size = 16;
let window = WindowFunction::Hann;
let spectrogram = stft(&signal, fft_size, hop_size, window.clone());
let reconstructed = istft(&spectrogram, hop_size, window);
let start = fft_size;
let end = reconstructed.len().saturating_sub(fft_size);
if end > start {
for i in start..end.min(signal.len()) {
let diff = (reconstructed[i] - signal[i]).abs();
assert!(
diff < 0.1,
"Mismatch at {}: {} vs {}",
i,
reconstructed[i],
signal[i]
);
}
}
}
#[test]
fn test_magnitude_spectrogram() {
let signal: Vec<f64> = vec![1.0; 64];
let spectrogram = stft(&signal, 16, 8, WindowFunction::Rectangular);
let magnitudes = magnitude_spectrogram(&spectrogram);
assert!(!magnitudes.is_empty());
assert!(magnitudes[0][0] > 0.0);
}
#[test]
fn test_power_spectrogram() {
let signal: Vec<f64> = vec![1.0; 64];
let spectrogram = stft(&signal, 16, 8, WindowFunction::Rectangular);
let powers = power_spectrogram(&spectrogram);
assert!(!powers.is_empty());
assert!(powers[0][0] >= 0.0);
}
#[test]
fn test_phase_spectrogram() {
let signal: Vec<f64> = (0..64).map(|i| f64::from(i).sin()).collect();
let spectrogram = stft(&signal, 16, 8, WindowFunction::Hann);
let phases = phase_spectrogram(&spectrogram);
assert!(!phases.is_empty());
for frame in &phases {
for &phase in frame {
assert!(phase >= -core::f64::consts::PI - 0.01);
assert!(phase <= core::f64::consts::PI + 0.01);
}
}
}
}