fix(audio): apply receive-side NR to stereo mic streams
Some checks failed
Build Linux Binaries / linux/amd64 (push) Has been cancelled
Build Linux Binaries / linux/arm64 (push) Has been cancelled

The per-listener noise-reduction toggle (vc_set_remote_stream) did nothing
on Windows/macOS/iOS. The decode loop gated the RNNoise pass on
dec_channels == 1 as a proxy for "this stream is voice" (assuming
stereo => screen-share). The stereo-mic capture commit broke that: a stereo
mic with send-side NR off transmits stereo Opus, so the receiver decoded
two channels and skipped NR entirely. gain/mute have no channel guard, which
is why only NR appeared broken.

Thread the stream kind through init_recv_stream into RemoteStream::is_voice
(set from si.kind() == STREAM_MIC), gate receive NR on is_voice instead of
channel count, and fold a stereo voice frame to mono -> denoise -> duplicate
back across both channels in place (symmetric with the send-side downmix;
RNNoise is mono-only). Screen-audio shares are never denoised.

New test test_recv_noise_reduction drives AudioEngine and asserts a stereo
voice stream's noise floor collapses with NR on (RMS 1046 -> 0.1) while a
screen-audio share stays unchanged. ctest --preset dev green 29/29.
Docs: voice.md section 10. Shared-core fix; clients need only a rebuild.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 21:11:03 +02:00
parent f72219ddf3
commit b44a200b95
12 changed files with 261 additions and 20 deletions

View File

@@ -321,8 +321,12 @@ and every Opus frame size (480/960/1920/2880) is a multiple of 480, so frames ar
whole 480-sample chunks with no resampling. Because it's mono-only:
- **Send-side:** a stereo mic is downmixed to mono **only when NR is enabled** — with NR off a
stereo mic keeps full stereo (we never collapse mic quality unless asked).
- **Receive-side:** NR is skipped on stereo streams (a stereo stream is a screen-audio share,
not voice).
- **Receive-side:** NR applies to **voice (MIC) streams only**, gated on the stream *kind* — not
on its channel count, since a stereo mic with send-side NR off now arrives as stereo voice.
When enabled on such a stream the decoded stereo frame is folded to mono, denoised, and
duplicated back across both channels (symmetric with the send-side downmix), so that stream
plays as mono while NR is on. A **screen-audio share is never voice and is left untouched**
denoising music/video with a speech denoiser would mangle it.
Implementation: a per-`ssrc` NS instance (`RemoteStream::recv_ns`) on the receive path,
instantiated lazily only for streams the listener has flagged; the send-side instance