Skip to content

fix: play and record on Windows devices that mix in more than two channels - #358

Merged
devopvoid merged 4 commits into
mainfrom
fix/windows-multichannel-audio
Oct 13, 2026
Merged

devopvoid merged 4 commits into
mainfrom
fix/windows-multichannel-audio

Conversation

@devopvoid

@devopvoid devopvoid commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Addresses #240: There is no sound when using gaming headphones (7.1).

Summary

On Windows, some headsets play no sound: those whose shared-mode mix format has more than two channels, as gaming headsets that mix in 7.1 have (discuss-webrtc thread). WebRTC's Windows audio device module asks such a device only for mono or stereo, at a few rates. A shared-mode stream must have the channel count of the mix format, so the device rejects every one of them and InitPlayout() fails.

Chrome and Firefox open such a device in its own format and convert the channels themselves: Chrome opens its output in the device's channel layout and mixes into it, and takes the closest match on input with its own converter; Firefox's cubeb starts from the mix format and up- or downmixes with its own mixer. Neither leaves it to Windows (AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM). This does the same, as a WebRTC patch, patches/windows/wasapi_mix_format.patch, applied by the existing per-OS patch step:

  • When the device accepts no mono or stereo format, the stream is opened in its mix format: 16-bit PCM in its channel count, speaker mask and rate. The audio engine converts 16-bit PCM to its float format itself, as it did before.
  • The module converts between that and WebRTC's mono or stereo at the WASAPI buffer, in the render and capture threads. WebRTC above the module keeps getting one or two channels, which is all AudioTransportImpl accepts.
  • Devices that accept mono or stereo are opened exactly as before.

The conversion is built from WebRTC's port of Chrome's channel mixing matrix (audio/utility/channel_mixing_matrix) and applied in one of two ways, in CoreAudioChannelConversion:

  • Copy. When the matrix only routes channels, as for playout to a 7.1 device (left and right to front left and right, the rest silent) or of mono, the samples are copied by channel, as Chrome's ChannelMixer does.
  • Mix. Otherwise, as for capture from 7.1 to stereo, each output channel keeps only its non-zero terms, with integer coefficients and loops for none, one, two and more terms, as Firefox's cubeb mixer mixes 16-bit audio; it clamps only when a matrix can leave the 16-bit range. Unlike cubeb, sums are 64-bit: a row of WebRTC's 7.1 or 5.1 downmix sums to 3.1 to 6 in coefficients, so full-scale input reaches 3.4 to 6.4 billion, past cubeb's 32-bit int. That also lets the coefficients have 24 fractional bits rather than cubeb's 15, which holds WebRTC's coefficients exactly.

The WebRTC cache key on CI now covers the patches, in the part that restore-keys matches on, as the FFmpeg key does; otherwise a changed patch would restore a build without it. The first run after this rebuilds WebRTC on every platform.

PlayoutIntegrationTest plays silence on the default speaker through the platform module and checks that WebRTC is asked for mono or stereo 10 ms blocks. It runs on Windows only and is skipped without a playout device, as on CI.

macOS and Linux need none of this: WebRTC's Core Audio module puts an AudioConverter between the device's format and WebRTC's, and its PulseAudio module opens a stereo stream that the sound server remixes to the device.

Commits

  1. The fix, with the conversion through the float matrix.
  2. The copy, when the matrix only routes channels.
  3. The mix as cubeb does it, with Q15 coefficients.
  4. 24-bit coefficients, which the 64-bit sums allow.

Testing

  • The patch applies the way the CMake step applies it, to the CRLF files of a checkout with core.autocrlf=true, as here and on Windows CI, both as an LF and as a CRLF patch.

  • WebRTC builds with it (an incremental ninja of out/x64), without warnings.

  • mvn -pl webrtc-jni,webrtc package against that build: 286 tests, no failures, 3 skipped (the hardware codec ones that skip here anyway). PlayoutIntegrationTest runs on the stereo speakers here.

  • The new path, on those speakers: with a local change that forced it and asked for mono, WebRTC was asked for mono 48 kHz blocks and the render thread converted them to the device's stereo; 20 blocks arrived in half a second.

  • Speed, 10 ms at 48 kHz, the patch's code on WebRTC's matrices (median of five runs):

    case path first commit now
    playout stereo → 7.1 copy 16.4 µs 1.0 µs
    playout stereo → mono mix 2.9 µs 0.4 µs
    capture 7.1 → stereo mix 8.4 µs 4.3 µs
    capture 5.1 → stereo mix 7.3 µs 3.8 µs

    At most about 0.04% of a core.

  • Precision, against the exact mix in double precision, on one second of noise, a sine and full-scale input: every output sample is the exact mix rounded half up, in every case, and the signal to noise is that of the float matrix (100.1 dB for 7.1 to stereo on noise), set by the 16-bit output itself. With Q15 coefficients, 15 to 29% of the samples of a downmix were one or two off.

Not verified

  • No device that mixes in more than two channels: there is none here. Opening such a device in its mix format is what Chrome and Firefox do, but this patch has not done it.
  • The capture side compiles but has not run on a device: there is no active capture device here. Its mixing ran in the speed and precision tests only.

…nnels

WebRTC's Windows audio device module asks a device only for mono or
stereo. A shared-mode stream must have the channel count of the device's
mix format, so a device that mixes in 7.1, as some gaming headsets do,
rejects every format, and initializing playout fails with no sound.

A WebRTC patch (patches/windows/wasapi_mix_format.patch) handles that
case the way Chrome and Firefox do: when no mono or stereo format is
accepted, the stream is opened in the device's mix format (16-bit PCM in
its channel count, speaker mask and rate), and the module converts
between it and WebRTC's mono or stereo at the WASAPI buffer, with
WebRTC's port of Chrome's channel mixing matrix. WebRTC above the module
keeps getting one or two channels, which is all it accepts. Devices that
accept mono or stereo are opened exactly as before.

The WebRTC cache key on CI now covers the patches, so a changed patch
rebuilds WebRTC instead of restoring a build without it.

PlayoutIntegrationTest plays out on the default speaker and checks that
WebRTC is asked for mono or stereo 10 ms blocks; it is skipped on hosts
without a playout device.
…s them

When a device is opened in its mix format, the module converted every
sample through the full channel matrix. For playout to more channels
than WebRTC's, which is the case of a 7.1 headset, that matrix only
routes channels: left and right go to front left and right, the rest is
silent, and mono goes to both. ChannelMixingMatrix reports this, and
Chrome's ChannelMixer then copies instead of mixing. The module does the
same now, with a map of the input channel of each output channel, since
its samples are interleaved. The output is the same.

On 10 ms at 48 kHz, stereo to 7.1 drops from about 10 to about 1 us per
block. Capture from 7.1 to stereo mixes, and keeps the matrix.
Channels that have to be mixed, rather than only routed, went through
the full float matrix for every sample: every coefficient, zeros
included, then a rounding call per output sample. They are now mixed
the way Firefox's cubeb mixer mixes 16-bit audio: each output channel
keeps only its non-zero terms, as Q15 integer coefficients, with loops
for none, one, two and more terms, rounding by (sum + 16384) >> 15, and
clamping only when a matrix can leave the 16-bit range.

Unlike cubeb, sums are taken in 64 bits. A row of WebRTC's 7.1 or 5.1
downmix sums to 3.1 to 6 in coefficients, so full-scale input on every
channel reaches 3.4 to 6.4 billion, past what cubeb's 32-bit int holds.

The matrix and the channel map become one CoreAudioChannelConversion
per direction, which ConvertChannels() applies: a copy when the matrix
only routes channels, as before, the terms otherwise.

On 10 ms at 48 kHz, capture from 7.1 to stereo drops from about 8.5 to
4.3 us, 5.1 to stereo from 7.3 to 3.8, 7.1 to mono from 3.8 to 3.0, and
playout from stereo to mono from 2.9 to 0.4. The output is that of the
float matrix within 1 to 2 of 32768, and the same at full scale.
The mix took Q15 coefficients from Firefox's cubeb mixer, which its
32-bit sums require. The sums here are 64-bit, so the coefficients now
have 24 fractional bits. A float coefficient from 1/2 to 1, as all of
WebRTC's downmix coefficients are, fits them exactly, and the sum of
full-scale input over a row of the 7.1 downmix stays below 2^42.

Against the exact mix, Q15 coefficients put 15 to 29% of the samples of
a 7.1 or 5.1 downmix one or two off, at 1 to 5 dB less signal to noise
than the float matrix had. With 24 bits, every sample of every case is
the exact mix rounded half up, the float matrix is matched to the dB,
and the speed is the same. Only the tie of a stereo to mono mix, half a
step, rounds up rather than to even.
@devopvoid
devopvoid merged commit 513d489 into main Oct 13, 2026
22 checks passed
@devopvoid
devopvoid deleted the fix/windows-multichannel-audio branch October 13, 2026 11:48
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