Skip to content

Echo gating, step 0: explicit mic constraints and a coupling probe - #112

Closed
alpha5611331 wants to merge 3 commits into
mainfrom
feat/echo-gate-layer-0
Closed

Echo gating, step 0: explicit mic constraints and a coupling probe#112
alpha5611331 wants to merge 3 commits into
mainfrom
feat/echo-gate-layer-0

Conversation

@alpha5611331

Copy link
Copy Markdown
Member

Refs #111. First step of the echo-gating work: the constraint change everything else is tuned against, and the instrument that does the tuning. No gating yet - nothing in this PR changes what reaches the ASR.

1. State the microphone processing constraints explicitly

Both getUserMedia call sites in live-transcription.service.ts passed only a deviceId, so echoCancellation, noiseSuppression and autoGainControl ran on Chromium's implicit defaults.

This changes no behaviour today - all three already default to true. The point is that they stop moving on their own under a Chromium version bump, and that there is one place to flip them from once the probe says which way they should go. AGC is a named constant because it is the flag most likely to move: it raises gain through quiet passages, which amplifies re-captured interviewer audio on a speaker setup.

The no-device case is an object with no deviceId rather than audio: true, which would have dropped the flags along with it.

2. A manual probe for the coupling

test/manual/echo-probe.mjs drives a real Electron session and reports three numbers per machine:

delayMs arrival-order difference between the two channels, with its sign
correlation normalised cross-correlation peak at that lag - what separates speakers from headphones
erlDb echo return loss: how far below the reference the echo sits
    delayMs   corr    erlDb   ref%   mic%   coupled
    -------   ----    -----   ----   ----   -------
        120   0.87    -18.4     71     74   yes

Two things it is built to catch:

The sign of the delay. The acoustic path is always mic-after-speaker, but what is measured is arrival order at the worklet, and Chromium's getDisplayMedia loopback path carries its own latency. On a machine where it is the slower of the two, the reference arrives after the echo it explains. The search window is two-sided, and deliberately wider (-400..+800 ms) than any gate would ship with - so a peak sitting at the edge is distinguishable from a window that is too small. The summary warns when that happens, and again when the delay comes back negative.

Silence masquerading as coupling. An estimate is only accepted while the reference is actually active. A silent run of an earlier version of this probe reached a correlation of 0.53 with nothing playing at all - two noise floors correlate - which is above the 0.5 that looked like a reasonable CORR_MIN. A peak height alone cannot tell coupling from silence, and the gate will have to carry the same reference-active condition.

It also drives the A/B this PR's first half exists to enable:

pnpm exec electron test/manual/echo-probe.mjs --no-aec
pnpm exec electron test/manual/echo-probe.mjs --no-agc

Manual, like taskbar-probe.mjs: it needs a desktop session, real speakers and someone to play audio into them, so it stays out of test/run.mjs.

Verification

  • pnpm lint, tsc -b, pnpm test:main all green.
  • Probe smoke-tested end to end on Windows 11: acquires both streams, reports the applied constraints back (aec=true ns=true agc=true), runs the correlator, and prints a summary. With nothing playing it correctly reports no correlated frames and points at the ref% column.
  • Not done here: the actual A/B across 2-3 machines. That needs a person playing a recorded interview through speakers and cannot be run from CI or from this branch. It is the next unchecked box on Speaker echo on the mic channel silently suppresses live suggestions #111, and Layer 1's constants depend on it.

Reviewer note

Worth a look at the acceptance criterion in renderer.js (estimate()) and at the two-sided search window - those are the two places where getting it wrong produces a plausible-looking number rather than an obvious failure.

alpha5611331 and others added 2 commits August 27, 2026 20:54
Both getUserMedia call sites passed only a deviceId, so echoCancellation,
noiseSuppression and autoGainControl ran on whatever Chromium currently
defaults to. All three default to true today, so this changes no behaviour -
it stops the behaviour changing on its own under a Chromium version bump, and
gives the echo work one place to flip them from.

AGC is split out as a named constant because it is the flag most likely to
move: it raises gain through quiet passages, which amplifies re-captured
interviewer audio on a speaker setup.

The no-device case is an object with no deviceId rather than `audio: true`,
which would have dropped the flags along with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measures how much of the interviewer's audio the microphone re-captures when
the candidate is on speakers. Reports three numbers per machine: the signed
arrival-order delay between the two channels, the correlation peak at that
lag, and the echo return loss.

The sign matters and is the reason the search window is two-sided. The
acoustic path is always mic-after-speaker, but what is measured here is
arrival order at the worklet, and Chromium's getDisplayMedia loopback path
carries its own latency - so on a machine where it is the slower of the two,
the reference arrives after the echo it explains.

The window searched is wider than any gate would ship with, so a peak sitting
at the edge is distinguishable from a window that is too small; the summary
warns when that happens.

An estimate is only accepted while the reference is actually active. A silent
run reached a correlation of 0.53 - two noise floors correlate - so a peak
height alone cannot tell coupling from silence.

Manual, like taskbar-probe.mjs: it needs a desktop session, real speakers and
someone to play audio into them, so it stays out of test/run.mjs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gitar-bot

gitar-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

Verifying the correlator against synthetic signals turned up the more
important defect. Delay recovery is exact, including the sign: +120, +300,
-150, -250 and 0 ms all come back to the frame, and the ERL matches the
injected gain. But the coupling verdict was wrong in the dangerous direction.

The search takes the max over ~120 candidate lags, and the max of many
correlations is biased upward, so unrelated signals score far higher than
intuition suggests: 0.53 on pure silence, 0.57 on two independent bursty
signals. A CORR_MIN of 0.5 calls both of those coupled, and a false "coupled"
on a headphone user is what would lead a gate to cut a microphone that was
never echoing anything.

Prominence - the peak's height above the median lag - separates them cleanly:
0.28 for the unrelated pair against 0.87-1.13 for a real echo. Both ends of
that gap are optimistic, so the threshold is a starting point to be re-derived
from real runs, and the raw numbers are printed every second regardless.

Also: reject unknown arguments and a non-positive --seconds. A mistyped
--noaec was silently ignored, which runs with echo cancellation ON and reports
a plausible number for the configuration you were trying to rule out; a
non-numeric --seconds reached setTimeout as NaN and ended the run before it
started, which reads like a headphone result.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alpha5611331
alpha5611331 deleted the feat/echo-gate-layer-0 branch August 28, 2026 01:16
@alpha5611331

Copy link
Copy Markdown
Member Author

Closed automatically by GitHub when the head branch was renamed from feat/echo-gate-layer-0 to feat/echo-suppression (the branch was scoped to Layer 0 only; the work continues on one branch for the whole issue). Continued in #113.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant