The repo never enforced rustfmt, so formatting had drifted broadly. This is a
single mechanical `cargo fmt` pass over the whole crate (no behavioral change;
lib suite green, 493 passed). Going forward fmt should be enforced (planned CI
fmt --check step). Part of the 0.6.1 hygiene pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add Geigel double-talk detection so the adaptive filter stops diverging when
the near-end talks over the far-end (the failure the Stage B demo exposed:
ERLE went negative as the filter mistook near-end voice for echo).
- src/dsp/aec.rs: DoubleTalkDetector — Geigel test (mic level vs a sliding-
window peak of the far-end, via an O(1) monotonic-deque max) with a hangover
latch. EchoCanceller — wraps Nlms + the detector, freezing adaptation while
double-talk is declared; reports the double-talk rate; DTD can be toggled off
for A/B. 5 aec tests (detector fires on near-end not echo; DTD protects a
converged filter through double-talk, +15 dB over raw NLMS).
- specview aec: now uses EchoCanceller with tunable --dtd-threshold / --hangover
/ --near-onset / --no-dtd, and prints the double-talk %.
Tuning found with the harness (white far-end, -12 dB echo, near-end onset
mid-call): DTD threshold 0.5 is the sweet spot — late-call ERLE +29.6 dB with
DTD vs -4.0 dB without (a 33 dB swing); 0.35 over-triggers (never learns), 0.7
under-triggers (drifts). Default set to 0.5. Also documents the false-positive
/miss tradeoff and that correlated (pink/speech) far-ends converge slower.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stage A+B of the in-process AEC: a Normalized LMS adaptive-filter echo
canceller and a synthetic echo-path simulator, driven end-to-end by a new
`specview aec` command so cancellation can be measured fully headless on
conjured signals (no speakers/mic needed).
- src/dsp/echo_path.rs: EchoPath — synthetic acoustic echo (bulk delay +
exponentially-decaying diffuse RIR, energy-normalized to a target
attenuation) convolved over a far-end signal. Gives ground-truth echo.
- src/dsp/aec.rs: Nlms — sample-at-a-time NLMS adaptive FIR (ring-buffered
reference history, energy-normalized update, freezable for double-talk).
Cleaned output = mic minus the learned echo estimate.
- specview aec: far -> sim echo -> (+ optional near-end) -> cancel -> measure.
ERLE via oracle residual (cleaned - near), broadband + early/late
(convergence) + per voice band, optional before/after spectrograms.
- 6 new tests (path delay/attenuation, NLMS convergence >20 dB on a known
path, frozen-filter no-op, near-end passthrough). 33 dsp tests total.
Verified: single-talk pink-noise echo cancels +14.5 -> +32.0 dB ERLE as the
filter converges; double-talk (no DTD yet) drives ERLE negative as the filter
diverges onto the near-end tone — the motivating result for Stage C (DTD).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>