Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,24 @@ residual-echo estimate, spectral suppression, comfort noise matched to
the near-end noise floor; plus the integrated chain type. House
workflow: scratch-measure first, thresholds with margin, rooms from both
generator families. RT contract as everywhere.
*DONE — `mutap/postfilter.h` (`residual_suppressor` + `aec_chain`) +
`tests/test_postfilter.cpp` (12 tests, measured-first; every Stage 2
deliverable inside its margin target — the measured table and the four
design decisions the numbers forced live in the matrix's "Stage 2
delivered" section). The headline discovery: the AEC chain must NOT
use PEM — open-loop AEC has an exogenous far end and the predictor
refit floors misalignment near -20 dB where the raw FD-Kalman core
(transition 0.9998, initial uncertainty 10 — the measured AEC sweet
spot) reaches -75 dBm0(A) bare with double-talk immunity from its own
noise-PSD tracker. The suppressor correlates the MIC, not E, against
the echo estimate (orthogonality principle: coh(E,Yhat) saturates at
0.34 while adapting), and takes suppression DEPTH from a
coherence-gated leakage estimate so double-talk transparency is the
shape of the rule, not a detector. Two instrument fixes en route:
A-weighting poles above Nyquist now prewarp-clamped (16 kHz tests went
NaN), and pem_afc grew echo_estimate_block(). mutap.h includes the new
header; emulated-target selections unchanged (double-only ITU suites
stay host-side).*

**Stage 3 — Compliance suite** (`tests/test_itu_*.cpp`). One gtest per
matrix row asserting requirement + margin policy. Swept across fixture
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,23 @@ algorithmically complete. What exists today:
double-talk the warped predictor beat the speech cascade's suppression
in **all 18 room × seed pairs** measured (per-room medians 17.3…19.6 vs
15.2…16.7 dB, Kalman core).
- **Residual-echo post-filter + comfort noise**
([`mutap/postfilter.h`](include/mutap/postfilter.h)) — the Stage 2
deliverable of the [ITU compliance plan](docs/itu-compliance.md):
a per-bin Wiener suppressor driven by mic-vs-echo-estimate coherence
gating a learned leakage estimate, comfort noise matched to the
near-end floor by two-window minimum statistics, and `mutap::aec_chain`
composing it with a linear canceller (default: the **raw** FD-Kalman
core — open-loop AEC has an exogenous far end, so PEM's decorrelation
buys nothing and its predictor refit floors misalignment near −20 dB
where the raw core reaches −75 dBm0(A) bare). Measured on the ITU
battery ([`tests/test_postfilter.cpp`](tests/test_postfilter.cpp)):
single-talk residual **−79.9/−88.9 dBm0(A)** (cabin/studio; the P.1120
clause wants < −58, our margin target < −64), double-talk near-end
attenuation **1.05 dB** (clause ≤ 3, target ≤ 1.5), double-talk echo
loss **≥ 34.1 dB in every band** (clause ≥ 27, target ≥ 33), comfort
noise matched **−1.2 dB / ≤ 1.7 dB per band** (clause +2/−5, half-mask
spectrum), noise pumping 3.3 dB, near-end build-up 20.9 ms.

Next up (see [HANDOFF.md](HANDOFF.md) "What's next"): in-Max listening in
a real room and the default-engine decision, then the M55 performance
Expand Down
50 changes: 50 additions & 0 deletions docs/itu-compliance.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,53 @@ analogues), Hoth-spectrum noise for the P.340 rows.
- Switching dynamics inside 25 ms build-up at the margin targets.
- No noise pumping beyond 5 dB around speech bursts.
- The chain's ERL trajectory above the convergence masks at 2x speed.

### Stage 2 delivered (mutap/postfilter.h + tests/test_postfilter.cpp)

All of the above, measured against this file's margin targets on the
golden model (double, block 256, 2048-tap unit-energy paths, 48 kHz;
one required-rate check at 16 kHz). Chain = RAW partitioned FD-Kalman
(transition 0.9998, initial uncertainty 10) + residual suppressor at
defaults:

| Deliverable | Margin target | Measured |
|---|---|---|
| ST residual, cabin / studio | < -64 dBm0(A) | **-79.9 / -88.9** |
| ST residual, NLMS core / 16 kHz | < -64 dBm0(A) | -65.0 / -69.1 |
| DT send attenuation, cabin / studio | <= 1.5 dB | **1.05 / 0.93** |
| DT echo loss, worst band 200-6950 Hz | >= 33 dB | **38.0 / 34.1** |
| ERL by 600 / 1200 ms | >= 40 / >= 46 dB | 43.2 / 46.9 |
| Comfort-noise level match | +1 / -2.5 dB | -1.20 |
| Comfort-noise spectrum, worst band | half-mask (+-3..6) | 1.69 dB |
| Noise pumping | <= 5 dB | 3.3 |
| Near-end build-up at DT onset | <= 25 ms | 20.9 ms |

Design decisions the numbers forced (full derivations in
postfilter.h's comments, rejected designs kept in git history):

1. **The AEC chain does not use PEM.** Open-loop AEC has an exogenous
far end — no closed-loop bias to remove — and the predictor's
block-by-block refit injects gradient noise that floors misalignment
near -20 dB. The raw Kalman core measures -75.6 dBm0(A) bare on the
same scenario where PEM-Kalman plateaus at -28.9, and its per-bin
noise-PSD tracking IS the double-talk defense (post-DT residual
-70.9). PEM remains the right structure for the closed loop (AFC),
and aec_chain still composes with pem_afc.
2. **The suppressor's discriminator correlates the MIC (E + Yhat),
never E, against Yhat**: a converged adaptive filter keeps E
orthogonal to its reference, so coh(E, Yhat) saturates at 0.34 while
adapting; coh(D, Yhat) measures 0.99.
3. **Suppression depth comes from a leakage estimate gated by that
coherence, not from the coherence itself** — the AM-FM plans
interleave 20 Hz apart at the low end, no realizable gain filter
notches that selectively, and a coherence-proportional gain measured
5.5 dB of near-end attenuation there. The Wiener-on-E form rides to
unity wherever near-end energy inflates |E|^2.
4. **Comfort noise fills to a two-window minimum-statistics floor
(bias x4)** — asymmetric-rate one-pole trackers measured either
-8 dB undershoot (raw-minimum bias) or tens-of-seconds acquisition.

Still Stage 3's to prove: the full per-row multi-rate suite (both
required rates x both cores x all rooms), the spectral echo mask, the
switching/activation battery, stability sweep, G.168-adapted rows, and
the TCL / time-variant-path rows.
1 change: 1 addition & 0 deletions include/mutap/mutap.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@
#include "mutap/fft.h"
#include "mutap/lpc.h"
#include "mutap/pem_afc.h"
#include "mutap/postfilter.h"
6 changes: 6 additions & 0 deletions include/mutap/pem_afc.h
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ namespace mutap {
/// Current feedback-path estimate as filter_length() time-domain taps.
void copy_impulse_response(Sample* dest) noexcept { m_fdaf.copy_impulse_response(dest); }

/// The echo estimate of the LAST processed block — block_size()
/// time-domain samples of what the cancellation subtracted (the
/// residual suppressor's reference, postfilter.h). Valid until
/// the next process_block()/reset().
const Sample* echo_estimate_block() const noexcept { return m_time.data() + block_size(); }

private:
static config validated(const config& cfg) {
if (cfg.analysis_window < 2 * cfg.fdaf.block_size) {
Expand Down
Loading
Loading