Skip to content

Ramp the microphone gate closed, so sentences stop ending in a click - #413

Merged
OhMyGuus merged 2 commits into
OhMyGuus:nightlyfrom
greluc:fix/declick-microphone-gate
Sep 8, 2026
Merged

Ramp the microphone gate closed, so sentences stop ending in a click#413
OhMyGuus merged 2 commits into
OhMyGuus:nightlyfrom
greluc:fix/declick-microphone-gate

Conversation

@greluc

@greluc greluc commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

The microphone half of the clicking, split out as promised in #412. Independent of it — I test-merged the two branches and they apply cleanly in either order.

The problem

With Mic Sensitivity on, the voice detector gates the microphone by writing gain.value directly:

onVoiceStop: () => {
    if (this.microphoneGain &&  micSensitivityEnabled ) {
        this.microphoneGain.gain.value = 0;
    }

The zero is what hurts. At the moment the gate closes your signal is still mid-decay, so cutting it is a step the size of whatever you were saying — a click in every other player's ears at the end of every sentence you speak. You never hear it yourself; it happens in the stream you send, not the one you receive.

Scope is narrow, worth being clear about it: the gate only runs when micSensitivityEnabled is on and autoGainControl is off. With the shipped defaults the gain node isn't even created, so most users have never had this.

The change

Release ramped over 20 ms, attack left instant. Not symmetry for its own sake:

  • Closing is where the step is large, because the signal is still going.
  • Opening is where it is near zero by definition, so there is nothing to smooth — and a fade-in would soften the first consonant of every word, on top of the delay the detector already costs before it fires onVoiceStart. Word beginnings are the last thing to spend latency on.

Fast attack, ramped release, the way a noise gate is built.

Opening the gate now also cancels a release still in flight. That is a real bug in the current code as much as a consequence of the ramp: scheduled automation outlives a plain value write, so without the cancel, speaking again inside the ramp window would let the old ramp run on to zero and mute the word that had just re-opened the gate.

Why MicrophoneSoundBar is not touched

It mirrors the same gating for the settings level meter, so it looks like it needs the same fix. It doesn't: its ScriptProcessorNode reads inputBuffer to compute an RMS and never writes outputBuffer, so that graph outputs silence — the connect(ctx.destination) on it is only there to keep the processor running. There is no click to remove, and ramping it would make the meter lag the gate it exists to display.

Verification

npm run typecheck, npm run lint and npm run format:check pass.

Not verified by ear — I don't have a lobby, and this one really wants an ear rather than a reviewer. Two things I'd want someone to check: that the click at the end of a sentence is actually gone, and that 20 ms of release doesn't audibly smear the last syllable. If it does, the constant is the only thing that needs moving.

🤖 Generated with Claude Code

With Mic Sensitivity on, the voice detector gates the microphone by
writing gain.value: full level when it hears you, zero when it stops.
The zero is the problem. At that moment the signal is still mid-decay,
so cutting it is a step the size of whatever you were saying - a click
in every other player's ears at the end of every sentence. You never
hear it yourself, because it happens in the stream you send.

Only the release is ramped, over 20 ms. The attack stays instant: at the
start of speech the signal is close to silence, so there is nothing to
smooth, and a fade-in would soften the first consonant of every word on
top of the delay the detector already costs. Fast attack, ramped
release, the way a noise gate is built.

Opening the gate now also cancels a release still in flight. A plain
value write does not, so speaking again inside the ramp window let the
old ramp run on to zero and mute the word that had just re-opened it.

MicrophoneSoundBar mirrors this gating for the settings level meter, and
is deliberately left alone: its ScriptProcessorNode never writes its
output buffer, so that graph is silent and has no click to remove -
ramping it would only make the meter lag the gate it is there to show.

Signed-off-by: Lucas Greuloch (greluc) <lucas.greuloch@gmail.com>
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Download the artifacts for this pull request:


This service is provided by nightly.link. These artifacts will expire in 90 days and will not be available for download after that time.

@OhMyGuus

OhMyGuus commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Thanks

@OhMyGuus
OhMyGuus merged commit d9d0009 into OhMyGuus:nightly Sep 8, 2026
5 checks passed
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.

2 participants