use crate::{generate_window, AnalysisConfig, AnalysisError, Result};
use oxifft::Complex;
#[derive(Debug, Clone)]
pub struct OlaBlock {
pub sample_offset: usize,
pub magnitude: Vec<f32>,
pub power: Vec<f32>,
}
pub struct OverlapSaveAnalyzer {
fft_size: usize,
step: usize,
window: Vec<f32>,
overlap_buf: Vec<f32>,
}
impl OverlapSaveAnalyzer {
pub fn new(config: &AnalysisConfig, step: usize) -> Result<Self> {
if step == 0 || step > config.fft_size {
return Err(AnalysisError::InvalidConfig(format!(
"step ({step}) must be in 1..={fft}",
fft = config.fft_size
)));
}
let window = generate_window(config.window_type, config.fft_size);
let overlap_len = config.fft_size - step;
Ok(Self {
fft_size: config.fft_size,
step,
window,
overlap_buf: vec![0.0_f32; overlap_len],
})
}
pub fn default_50pct(config: &AnalysisConfig) -> Result<Self> {
Self::new(config, config.fft_size / 2)
}
pub fn process(&mut self, samples: &[f32]) -> Result<Vec<OlaBlock>> {
let overlap_len = self.fft_size - self.step;
let mut blocks = Vec::with_capacity(samples.len() / self.step + 1);
let mut pos = 0usize;
while pos + self.step <= samples.len() {
let mut frame = Vec::with_capacity(self.fft_size);
frame.extend_from_slice(&self.overlap_buf);
frame.extend_from_slice(&samples[pos..pos + self.step]);
debug_assert_eq!(frame.len(), self.fft_size);
let windowed: Vec<Complex<f64>> = frame
.iter()
.zip(self.window.iter())
.map(|(&s, &w)| Complex::new(f64::from(s * w), 0.0))
.collect();
let fft_out = oxifft::fft(&windowed);
let half = self.fft_size / 2 + 1;
let magnitude: Vec<f32> = fft_out[..half]
.iter()
.map(|c| c.norm() as f32)
.collect();
let power: Vec<f32> = magnitude.iter().map(|&m| m * m).collect();
blocks.push(OlaBlock {
sample_offset: pos,
magnitude,
power,
});
let src_start = self.fft_size - overlap_len;
self.overlap_buf.copy_from_slice(&frame[src_start..]);
pos += self.step;
}
Ok(blocks)
}
pub fn reset(&mut self) {
for s in &mut self.overlap_buf {
*s = 0.0;
}
}
}
pub fn overlap_save_power_envelope(
samples: &[f32],
config: &AnalysisConfig,
) -> Result<Vec<f32>> {
let mut ola = OverlapSaveAnalyzer::default_50pct(config)?;
let blocks = ola.process(samples)?;
Ok(blocks
.iter()
.map(|b| b.power.iter().copied().sum::<f32>() / b.power.len() as f32)
.collect())
}
pub fn overlap_save_spectra(
samples: &[f32],
config: &AnalysisConfig,
) -> Result<Vec<Vec<f32>>> {
let mut ola = OverlapSaveAnalyzer::default_50pct(config)?;
let blocks = ola.process(samples)?;
Ok(blocks.into_iter().map(|b| b.magnitude).collect())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AnalysisConfig;
fn make_config(fft_size: usize) -> AnalysisConfig {
AnalysisConfig {
fft_size,
..Default::default()
}
}
#[test]
fn test_overlap_save_step_zero_is_error() {
let cfg = make_config(512);
assert!(OverlapSaveAnalyzer::new(&cfg, 0).is_err());
}
#[test]
fn test_overlap_save_step_too_large_is_error() {
let cfg = make_config(512);
assert!(OverlapSaveAnalyzer::new(&cfg, 513).is_err());
}
#[test]
fn test_overlap_save_produces_blocks_for_long_signal() {
let cfg = make_config(256);
let step = 128;
let mut ola =
OverlapSaveAnalyzer::new(&cfg, step).expect("valid config");
let signal: Vec<f32> = (0..1024)
.map(|i| (i as f32 * 0.01).sin())
.collect();
let blocks = ola.process(&signal).expect("process should succeed");
assert_eq!(blocks.len(), 8, "expected 8 blocks for 1024 samples / step 128");
for block in &blocks {
assert_eq!(block.magnitude.len(), cfg.fft_size / 2 + 1);
assert_eq!(block.power.len(), cfg.fft_size / 2 + 1);
}
}
#[test]
fn test_overlap_save_power_envelope_non_empty() {
let cfg = make_config(256);
let signal: Vec<f32> = (0..2048).map(|_| 0.5_f32).collect();
let env = overlap_save_power_envelope(&signal, &cfg).expect("power envelope");
assert!(!env.is_empty(), "power envelope must not be empty");
for &p in &env {
assert!(p >= 0.0, "power must be non-negative");
}
}
#[test]
fn test_overlap_save_reset_clears_state() {
let cfg = make_config(256);
let mut ola = OverlapSaveAnalyzer::default_50pct(&cfg).expect("valid");
let signal: Vec<f32> = (0..512).map(|i| i as f32).collect();
let _ = ola.process(&signal).expect("first pass");
ola.reset();
assert!(
ola.overlap_buf.iter().all(|&s| s == 0.0),
"overlap buffer should be zeroed after reset"
);
}
#[test]
fn test_overlap_save_spectra_has_correct_shape() {
let cfg = make_config(256);
let signal: Vec<f32> = (0..1024).map(|i| (i as f32).sin()).collect();
let spectra = overlap_save_spectra(&signal, &cfg).expect("spectra");
assert!(!spectra.is_empty());
for spec in &spectra {
assert_eq!(spec.len(), cfg.fft_size / 2 + 1);
}
}
}