fix(audio): a frame mixed for the old device is dropped when the new one has another channel count #448

Open
jarynclouatre wants to merge 1 commit from jarynclouatre/Longshot:fix/audio-frame-new-channel-count into main
Contributor

Why

When Windows' default output changes between a stereo device and a 5.1 one (or the first device appears after none), one audio frame mixed for the old device is sent to the new one with the other channel count: 5 ms with the channels in the wrong places and, from stereo to 5.1, stale samples behind it. A click at the switch.

renderer_main in host/nn_audio.c mixes, gains and mono-mixes each frame for out_channels, then output_frame may open the output again (output_move on a default change, output_open_default when no device was open; output_open sets out_channels) and goes on to copy the same samples with the new count.

What changed

  • audio (host/nn_audio.c, output_frame): keeps the channel count its frame was mixed for, and when a reopen in this call changed it, returns without queueing: that frame is dropped and the next one is mixed for the new device. A move that keeps the count still queues its frame. The WAV dump at the top is unchanged (a dump keeps one layout).
  • tests: tests/audio_output_errors_test.c and tests/test_audio_output_errors.py: three new cases over the real output section. upmix: stereo fallback, then the default moves to a device that takes 5.1; nothing of the stereo frame reaches a block (a sentinel past its end never appears), the next frame is queued with 6 channels. downmix: the reverse, 5.1 to stereo. retry: no device at the start, then one that takes 5.1. The default case now also checks that a stereo-to-stereo move keeps its frame.

Flags and settings

None.

Measurements

main: 5e072fc8 (2026-11-14)
this PR: fix/audio-frame-new-channel-count @ 4093570f
Hardware: none used (game-free checks on a Linux x86-64 PC, gcc 15.2, Python 3.14)
Scene: none, the game was not run
Method: python3 -I -B tests/run_all.py, two runs each, interleaved (main, the other branch, PR, twice). The new cases also run against main's host/nn_audio.c (--source).
How to reproduce: the commands above in a checkout of each commit

main @ 5e072fc8 this PR @ 4093570f
tests/run_all.py (run 1, run 2) 474 passed, 4 failed, 83 skipped (both) 474 passed, 4 failed, 83 skipped (both)
failing checks test_bake_portability.py, test_menu_keys.py, test_module_ids.py, test_pad_layout.py the same four
tests/test_audio_output_errors.py new cases upmix, downmix, retry fail (the old frame is copied with the new count) pass
main's host/nn_audio.c:
FAIL upmix:174: fill_block == 0 && fill_frames == 0 && !blocks_hold(7777)
FAIL downmix:217: fill_block == 0 && fill_frames == 0
FAIL retry:190: fill_frames == 0 && !write_calls && !blocks_hold(7777)
PASS default: actual output ring

test_bake_portability.py fails here for a missing Python module (zstandard). test_menu_keys.py doesn't build with g++ and test_pad_layout.py's fixture lacks host/hold_toggle.h, both on main since #137 (#331 and #338 fix them). test_module_ids.py looks for a source marker that main no longer has.

Testing

Tested: the game-free suite on a Linux PC, as in Measurements; the test compiles the changed output section with gcc and runs it with waveOut faked. host/nn_audio.c compiles (-fsyntax-only -Wall -Wextra) with llvm-mingw 20261104's x86_64-w64-mingw32-clang with the same 5 warnings as main's.

Not tested: a real device change in the game on Windows (a stereo headset and 5.1 speakers switched as Windows' default while sound plays).

Needs other testers: None.

Screenshots

Nothing on screen.

Risk and rollback

At a switch between channel counts one 5 ms frame is now silent instead of garbled. Reverting the commit puts the copy back.

Issues

None filed. Found reading output_frame after #2190's default-device move (Hookshot).

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
## Why When Windows' default output changes between a stereo device and a 5.1 one (or the first device appears after none), one audio frame mixed for the old device is sent to the new one with the other channel count: 5 ms with the channels in the wrong places and, from stereo to 5.1, stale samples behind it. A click at the switch. `renderer_main` in `host/nn_audio.c` mixes, gains and mono-mixes each frame for `out_channels`, then `output_frame` may open the output again (`output_move` on a default change, `output_open_default` when no device was open; `output_open` sets `out_channels`) and goes on to copy the same samples with the new count. ## What changed - audio (`host/nn_audio.c`, `output_frame`): keeps the channel count its frame was mixed for, and when a reopen in this call changed it, returns without queueing: that frame is dropped and the next one is mixed for the new device. A move that keeps the count still queues its frame. The WAV dump at the top is unchanged (a dump keeps one layout). - tests: `tests/audio_output_errors_test.c` and `tests/test_audio_output_errors.py`: three new cases over the real output section. `upmix`: stereo fallback, then the default moves to a device that takes 5.1; nothing of the stereo frame reaches a block (a sentinel past its end never appears), the next frame is queued with 6 channels. `downmix`: the reverse, 5.1 to stereo. `retry`: no device at the start, then one that takes 5.1. The `default` case now also checks that a stereo-to-stereo move keeps its frame. ## Flags and settings None. ## Measurements main: 5e072fc8 (2026-11-14) this PR: fix/audio-frame-new-channel-count @ 4093570f Hardware: none used (game-free checks on a Linux x86-64 PC, gcc 15.2, Python 3.14) Scene: none, the game was not run Method: `python3 -I -B tests/run_all.py`, two runs each, interleaved (main, the other branch, PR, twice). The new cases also run against main's `host/nn_audio.c` (`--source`). How to reproduce: the commands above in a checkout of each commit | | main @ 5e072fc8 | this PR @ 4093570f | |---|---|---| | `tests/run_all.py` (run 1, run 2) | 474 passed, 4 failed, 83 skipped (both) | 474 passed, 4 failed, 83 skipped (both) | | failing checks | `test_bake_portability.py`, `test_menu_keys.py`, `test_module_ids.py`, `test_pad_layout.py` | the same four | | `tests/test_audio_output_errors.py` new cases | `upmix`, `downmix`, `retry` fail (the old frame is copied with the new count) | pass | ``` main's host/nn_audio.c: FAIL upmix:174: fill_block == 0 && fill_frames == 0 && !blocks_hold(7777) FAIL downmix:217: fill_block == 0 && fill_frames == 0 FAIL retry:190: fill_frames == 0 && !write_calls && !blocks_hold(7777) PASS default: actual output ring ``` `test_bake_portability.py` fails here for a missing Python module (zstandard). `test_menu_keys.py` doesn't build with g++ and `test_pad_layout.py`'s fixture lacks `host/hold_toggle.h`, both on main since #137 (#331 and #338 fix them). `test_module_ids.py` looks for a source marker that main no longer has. ## Testing **Tested:** the game-free suite on a Linux PC, as in Measurements; the test compiles the changed output section with gcc and runs it with waveOut faked. `host/nn_audio.c` compiles (`-fsyntax-only -Wall -Wextra`) with llvm-mingw 20261104's x86_64-w64-mingw32-clang with the same 5 warnings as main's. **Not tested:** a real device change in the game on Windows (a stereo headset and 5.1 speakers switched as Windows' default while sound plays). **Needs other testers:** None. ## Screenshots Nothing on screen. ## Risk and rollback At a switch between channel counts one 5 ms frame is now silent instead of garbled. Reverting the commit puts the copy back. ## Issues None filed. Found reading `output_frame` after #2190's default-device move (Hookshot). ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened.
fix(audio): a frame mixed for the old device is dropped when the new one has another channel count
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
4093570fe9
renderer_main mixes each frame for out_channels, then output_frame may
open the output again (Windows' default device changed, or the first
device appeared after none) and copied the same samples with the new
device's count. Between stereo and 5.1 that one 5 ms frame went out with
its channels in the wrong places and, from stereo to 5.1, with stale
samples past its end: a click at the switch.

output_frame now keeps the count the frame was mixed for and drops the
frame when the reopen changed it; the next frame is mixed for the new
device. A move that keeps the count still queues its frame.
jarynclouatre changed title from WIP: fix(audio): a frame mixed for the old device is dropped when the new one has another channel count to fix(audio): a frame mixed for the old device is dropped when the new one has another channel count 2026-11-14 14:31:42 +00:00
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.
Some workflows are waiting to be reviewed.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'fix/audio-frame-new-channel-count:jarynclouatre-fix/audio-frame-new-channel-count'
git switch 'jarynclouatre-fix/audio-frame-new-channel-count'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!448
No description provided.