Files
peerspeak/src/dsp/window.rs
T
molluskandClaude Opus 4.8 aa94e861b1 feat(dsp): spectrogram + AEC measurement toolkit (specview)
Add a dependency-free signal-analysis toolkit and a `specview` dev CLI to
evaluate audio (especially echo cancellation) with objective numbers and a
terminal spectrogram instead of ear alone.

- src/dsp/: hand-written radix-2 FFT, Hann window, STFT, seeded test-signal
  generators (sine/log-sweep/white/pink/impulse), metrics (RMS/dBFS/peak/ERLE/
  per-band energy), minimal WAV read+write, and a 24-bit-ANSI half-block
  spectrogram renderer (magma colormap, freq/time axes, dB legend, ASCII
  fallback). Pure layers have no I/O; only `wav` touches the filesystem.
- src/bin/specview.rs: `gen` (conjure a test signal -> WAV), `show` (spectrogram
  + per-band energy summary), `erle` (broadband + per-band echo-return-loss
  between a before/after pair).
- 27 unit tests (FFT correctness, ERLE landmarks, WAV round-trip, render shape).
  Verified end-to-end: log sweep renders as the expected exponential curve;
  a 20 dB-quieter copy reads +20.0 dB ERLE broadband and per band.

Measurement substrate for upcoming AEC refinement (no shipped-path changes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 02:25:58 -04:00

56 lines
1.7 KiB
Rust

//! Analysis windows for the STFT.
//!
//! A raw rectangular frame leaks spectral energy across bins (the abrupt edges
//! look like discontinuities to the FFT). A Hann window tapers each frame to
//! zero at its edges, trading a little main-lobe width for much lower side-lobe
//! leakage — the standard choice for a spectrogram.
use std::f64::consts::PI;
/// Returns a length-`n` periodic Hann window, `w[i] = 0.5·(1 - cos(2πi/n))`.
///
/// The *periodic* form (denominator `n`, not `n-1`) is used because STFT frames
/// tile the signal; it gives perfect overlap-add reconstruction at 50% hop.
pub fn hann(n: usize) -> Vec<f32> {
if n <= 1 {
return vec![1.0; n];
}
(0..n)
.map(|i| (0.5 - 0.5 * (2.0 * PI * i as f64 / n as f64).cos()) as f32)
.collect()
}
/// Applies `window` to `frame` element-wise into a new buffer. Lengths must match.
pub fn apply(frame: &[f32], window: &[f32]) -> Vec<f32> {
debug_assert_eq!(frame.len(), window.len());
frame.iter().zip(window).map(|(&s, &w)| s * w).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hann_endpoints_are_zero_and_center_is_one() {
let w = hann(8);
assert!(w[0].abs() < 1e-6, "first sample tapers to ~0, got {}", w[0]);
// Periodic Hann peaks at the midpoint n/2.
assert!((w[4] - 1.0).abs() < 1e-6, "center peaks at 1, got {}", w[4]);
}
#[test]
fn hann_is_symmetric_about_center() {
let w = hann(16);
// Periodic window is symmetric across indices 1..n-1.
for i in 1..8 {
assert!((w[i] - w[16 - i]).abs() < 1e-6);
}
}
#[test]
fn apply_scales_samples() {
let out = apply(&[2.0, 2.0], &[0.5, 0.25]);
assert_eq!(out, vec![1.0, 0.5]);
}
}