From f9d21b914323f05a5b530ba5591d44fa421edfc7 Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Thu, 1 Oct 2026 00:31:10 +0200 Subject: [PATCH] fix(windows): end the audio track where the video ends Every Windows take carried 0.44 to 0.58 s of audio past its video (measured on three takes of 13 to 113 s, a constant gap, so a tail). The stop sequence quiesces WGC and stops the microphone, loopback, device watcher, keep-alive and webcam before it stops the mixer, and the mixer is clock-driven: it kept writing chunks through that half second. The mixer is now paused on the stop request, the same request the video writer stops on. The pause writes what the cushion holds up to that instant and nothing after; a pause already in place is left as it is. New test: a pause followed 300 ms later by a stop ends the track at the pause (410 ms for a pause at 418, the chunk grid). Fixes #942 --- .../src/audio_sample_utils_test.cpp | 20 +++++++++++++++++++ electron/native/wgc-capture/src/main.cpp | 11 ++++++++++ 2 files changed, 31 insertions(+) diff --git a/electron/native/wgc-capture/src/audio_sample_utils_test.cpp b/electron/native/wgc-capture/src/audio_sample_utils_test.cpp index ea235d1e9..3ef9d9f8c 100644 --- a/electron/native/wgc-capture/src/audio_sample_utils_test.cpp +++ b/electron/native/wgc-capture/src/audio_sample_utils_test.cpp @@ -1572,6 +1572,26 @@ int main() { expect("mixer-resume-continues-at-the-pause", first != left.end() && std::abs(at - pausedAtMs) <= 40.0, detail); } + // (5) A pause followed by a stop, as the helper ends every take (#942): + // the track ends at the pause, not at the stop that comes later. + { + const auto packet = dcPacket(0.5f); + double pausedAtMs = 0.0; + const auto left = runTake(false, true, [&](AudioMixer& mixer, Clock::time_point t0) { + capture( + [&] { mixer.pushMicrophone(packet.data(), static_cast(packet.size())); }, + t0, 40, 1000, 0); + pausedAtMs = std::chrono::duration(Clock::now() - t0).count(); + mixer.setPaused(true); + std::this_thread::sleep_for(milliseconds(300)); + }); + const double lengthMs = msAt(left.size()); + char detail[96]{}; + sprintf_s(detail, "track %.1f ms long, paused at %.1f ms", lengthMs, pausedAtMs); + std::cout << "JITTER_RAW stop-after-pause " << detail << std::endl; + expect("mixer-track-ends-at-the-pause-before-a-stop", std::abs(lengthMs - pausedAtMs) <= 20.0, detail); + } + // (4) The same with a resume that follows the pause at once, before the // mixer has had a chance to see the pause: the resume must wait for the // cushion to be written, or it throws the cushion away and what follows diff --git a/electron/native/wgc-capture/src/main.cpp b/electron/native/wgc-capture/src/main.cpp index b094e31f5..7b043d1f3 100644 --- a/electron/native/wgc-capture/src/main.cpp +++ b/electron/native/wgc-capture/src/main.cpp @@ -1549,6 +1549,17 @@ int wmain(int argc, wchar_t* argv[]) { control.waitForStop(); + // The take ends here, for the sound as for the picture: the video writer + // stops on this same request. The mixer is clock-driven, so left running + // it would go on writing chunks through the half second the steps below + // take, and every take ended with ~0.5 s of audio past its video + // (getopenscreen/openscreen#942, measured on three takes). A pause writes + // what the cushion holds up to this instant and then nothing; one already + // in place is left where it is. + if (audioMixer) { + audioMixer->setPaused(true); + } + const auto stopStart = std::chrono::steady_clock::now(); auto stopElapsedMs = [&] { return std::chrono::duration_cast(