Fix Brotato hooked-audio double-play: mute guessed streams via SILENT flag
The hooked path must capture the game's frames AND mute its local playback
("no echo"). The mute was implemented as zero-the-buffer (memset) + release
with AUDCLNT_BUFFERFLAGS_SILENT. Zeroing num_frames*block is only safe when
block is the real frame size; for a guessed format (late attach -- the
Brotato case, where we never saw Initialize) the guessed block can exceed
the real buffer, so the conservative code skipped the whole mute for guessed
streams. That left the game audible: it played locally AND the mirror
re-rendered the same audio a few ms later = a metallic, out-of-sync double.
Fix: AUDCLNT_BUFFERFLAGS_SILENT already makes WASAPI ignore the buffer
contents and play silence -- it mutes with no write at all, so it's safe for
any format. Decouple the two: always mute via the flag; keep the memset only
for an exact/override format (belt-and-suspenders). One-line behavior change;
the byte-incompatible cases confirm the flag-mute never over-writes.
Test-first (now a documented rule, README "Tests"): added an
audio_frames_silenced() counter and a mute assertion to audio_hook_test for
both the exact and guessed paths. The guessed assertion FAILS on the unfixed
code (3 rates) and passes after the fix -- the regression guard for this bug.
README also updates the now-correct no-echo limitation and adds a
lessons-learned writeup.
ctest 17/17. Brotato confirmed fixed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,7 @@ void test_see_init(hook::IpcClient& ipc, AudioRingHeader* ring, SharedBlock* blo
|
||||
const ToneFormat& f = tone.format();
|
||||
|
||||
// Exact format publishes at registration; render briefly so capture fills the ring.
|
||||
const std::uint64_t silenced_before = hook::audio_frames_silenced();
|
||||
const DWORD end = GetTickCount() + 300;
|
||||
while (GetTickCount() < end)
|
||||
{
|
||||
@@ -116,6 +117,8 @@ void test_see_init(hook::IpcClient& ipc, AudioRingHeader* ring, SharedBlock* blo
|
||||
fail += expect(s.sample_rate == f.rate, "see-init: HookStatus rate == exact rate");
|
||||
fail += expect(s.format_state == AudioFormat_Exact, "see-init: provenance == Exact");
|
||||
fail += expect(ring_has_nonsilent(ring), "see-init: non-silent audio captured");
|
||||
fail += expect(hook::audio_frames_silenced() > silenced_before,
|
||||
"see-init: local playback muted (no echo)");
|
||||
std::printf(" %s see-init %s -> ring %uHz/%uch/%ubit state=%u\n", fail == 0 ? "PASS" : "FAIL",
|
||||
fmt_desc(f, d, sizeof(d)), ring->sample_rate, ring->channels, ring->bits, s.format_state);
|
||||
|
||||
@@ -165,6 +168,8 @@ void test_guess(hook::IpcClient& ipc, AudioRingHeader* ring, SharedBlock* block,
|
||||
tone.render_step(30);
|
||||
hook::republish_audio_format();
|
||||
}
|
||||
// Format is published; from here the hook must capture AND mute (the no-echo path).
|
||||
const std::uint64_t silenced_before = hook::audio_frames_silenced();
|
||||
const DWORD cap_end = GetTickCount() + 200;
|
||||
while (GetTickCount() < cap_end)
|
||||
{
|
||||
@@ -178,6 +183,11 @@ void test_guess(hook::IpcClient& ipc, AudioRingHeader* ring, SharedBlock* block,
|
||||
fail += expect(s.sample_rate == f.rate, "guess: HookStatus rate == rendered rate");
|
||||
fail += expect(s.format_state == AudioFormat_Measured, "guess: provenance == Measured");
|
||||
fail += expect(ring_has_nonsilent(ring), "guess: non-silent audio captured");
|
||||
// THE BROTATO BUG: a guessed (late-attach) stream is byte-compatible here (device
|
||||
// channels/bits), so the hook must mute the game's local playback too -- otherwise the
|
||||
// game plays locally AND the mirror re-renders it, slightly delayed = a metallic double.
|
||||
fail += expect(hook::audio_frames_silenced() > silenced_before,
|
||||
"guess: local playback muted (no echo) -- the Brotato double-audio bug");
|
||||
std::printf(" %s guess %u Hz (device %uch/%ubit) -> measured %uHz state=%u\n", fail == 0 ? "PASS" : "FAIL",
|
||||
rate, f.channels, f.bits, ring->sample_rate, s.format_state);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user