Add audio-fidelity validator + fix mirror render under-run

Build coop_audio_validate, a tool that turns "the mirror audio sounds off"
into numbers. It plays a known sine (coop_tone, 44.1 kHz on a 48 kHz
endpoint -- the Godot/Brotato case), injects the hook as the host does, and
runs a fidelity analyzer (coop/tone_analysis.hpp: pitch error in cents,
SNR/THD, click + dropout counts), dumping a .wav to listen to. Modes:
--render drives the real AudioMirror and measures its rendered output;
--baseline/--selfcheck give the measurement floor; --listen <pid> records a
live coop_host's output; --wav analyzes a recording. Analyzer + WAV I/O are
unit-tested (tone_analysis_test) against synthesized defects.

Using it, the capture ring measures pristine (~68 dB, 0 gaps) while the
render path dropped to ~18 dB with gaps -- localizing a real defect in
AudioMirror::run_hooked: it re-primed (withheld the feed until ~30 ms had
rebuffered) on any partial fill (to_write < avail). A partial fill is normal
producer jitter, and withholding the feed drains the device, so a one-frame
ring dip became a full ~30 ms drop-out; on a jittery game it fired
constantly. Fix: feed whatever is available each tick and re-prime only on a
genuine starvation (device empty AND ring empty). The policy is factored
into a pure RenderPacer reused by run_hooked + run_loopback and proven by
render_pacer_test (the old policy withholds available data ~168x and drains
to one period from silence on a jittery schedule; the new one never
withholds).

ctest 17/17.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 00:36:20 +02:00
parent 04dcd0f41e
commit 21f62d8288
10 changed files with 1899 additions and 35 deletions

View File

@@ -36,6 +36,22 @@ add_executable(rate_estimator_test rate_estimator_test.cpp)
target_include_directories(rate_estimator_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
add_test(NAME rate_estimator_test COMMAND rate_estimator_test)
# Unit test for the audio fidelity analyzer (pitch error in cents, SNR/THD, click +
# dropout detection) and the WAV reader/writer. Synthesizes adversarial signals (clean /
# wrong-rate pitch shift / injected clicks / silence gaps) and asserts the metrics. Pure
# header logic, no device. Underpins the coop_audio_validate diagnostic tool.
add_executable(tone_analysis_test tone_analysis_test.cpp)
target_link_libraries(tone_analysis_test PRIVATE coop_common)
add_test(NAME tone_analysis_test COMMAND tone_analysis_test)
# Unit test for the audio-mirror render pacing policy (host/src/audio/render_pacer.hpp).
# Simulates a producer/consumer device timeline and asserts the shipping RenderPacer rides
# through producer jitter that makes the old re-prime-on-partial-fill policy glitch
# repeatedly (the under-run / "metallic" bug coop_audio_validate found). Header-only, no device.
add_executable(render_pacer_test render_pacer_test.cpp)
target_include_directories(render_pacer_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
add_test(NAME render_pacer_test COMMAND render_pacer_test)
# Unit test for the per-game audio override store (persist/reload, case-insensitive
# lookup, differing-overwrite detection). Reuses the shipping source. No device.
add_executable(audio_overrides_test
@@ -207,6 +223,8 @@ coop_output_subdir(tests
mkb_ring_test
mkb_map_test
audio_mix_test
tone_analysis_test
render_pacer_test
rate_estimator_test
audio_overrides_test
audio_loopback_test