diff --git a/native/src/windows_capture.cpp b/native/src/windows_capture.cpp index d625422..5987527 100644 --- a/native/src/windows_capture.cpp +++ b/native/src/windows_capture.cpp @@ -844,6 +844,27 @@ private: const DWORD sleepMilliseconds = static_cast( std::max(2, std::min(10, defaultPeriod / 10000 / 2))); + // WASAPI loopback stops delivering packets entirely when no application + // is rendering audio. To keep visualizers ticking through silence at + // real-time pacing (instead of freezing on the last frame), synthesize + // zero-filled chunks once WASAPI has truly stalled past the device + // period. The stall threshold has to be comfortably larger than the + // device period — packet delivery is bursty (typically one packet per + // device period, but our poll loop runs ~2× faster), so a tighter + // threshold would mistake the inter-packet gap for silence and + // interleave zeros into legitimate playback. + const double devicePeriodMs = static_cast(defaultPeriod) / 10000.0; + const double silenceStallThresholdMs = + std::max(25.0, devicePeriodMs * 2.5); + const UINT32 maxSilenceFrames = std::max( + 64, + static_cast( + static_cast(format.sampleRate) * + static_cast(sleepMilliseconds) * 4.0 / 1000.0)); + const double startMs = monotonicMilliseconds(); + double lastChunkPushedAtMs = startMs; + double lastRealPacketAtMs = startMs; + hr = audioClient->Initialize( AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK, 0, 0, mixFormat, nullptr); if (FAILED(hr)) { @@ -877,6 +898,7 @@ private: HANDLE mmcssHandle = AvSetMmThreadCharacteristicsW(L"Audio", &taskIndex); while (!stopRequested_.load()) { + bool pushedThisIteration = false; UINT32 packetFrames = 0; hr = captureClient->GetNextPacketSize(&packetFrames); if (FAILED(hr)) { @@ -915,7 +937,10 @@ private: std::fill(chunk.right.begin(), chunk.right.end(), 0.0f); } + lastChunkPushedAtMs = chunk.capturedAtMilliseconds; + lastRealPacketAtMs = chunk.capturedAtMilliseconds; pushChunk(std::move(chunk)); + pushedThisIteration = true; } captureClient->ReleaseBuffer(framesToRead); @@ -929,6 +954,28 @@ private: break; } + if (!pushedThisIteration) { + const double nowMs = monotonicMilliseconds(); + const double stallMs = nowMs - lastRealPacketAtMs; + const double sinceLastChunkMs = nowMs - lastChunkPushedAtMs; + if (stallMs >= silenceStallThresholdMs && sinceLastChunkMs >= 1.0) { + const UINT32 silenceFrames = std::min( + maxSilenceFrames, + std::max( + 1, + static_cast( + sinceLastChunkMs * + static_cast(format.sampleRate) / 1000.0))); + CapturedChunk silentChunk; + silentChunk.channelCount = std::max(1, format.channels); + silentChunk.capturedAtMilliseconds = nowMs; + silentChunk.left.assign(silenceFrames, 0.0f); + silentChunk.right.assign(silenceFrames, 0.0f); + lastChunkPushedAtMs = nowMs; + pushChunk(std::move(silentChunk)); + } + } + if (WaitForSingleObject(stopEvent_, sleepMilliseconds) == WAIT_OBJECT_0) { break; }