Bug Symptoms
After upgrading from 2.0.1 to 2.0.4, the streamed audio is noisy and crackles constantly. Video is fine, and 2.0.1 does not have the problem. Client: Moonlight TV on webOS (LG 85QNED93A), stereo.
Cause (measured, details below): since 2.0.3, Vibepollo honours the continuousAudio=1 launch parameter, which Moonlight TV always sends.
That enables continuous_buffer_t, which writes each WASAPI loopback packet at the frame derived from that packet's pu64QPCPosition.
On this PC the per-packet QPC positions jitter by up to ±47 frames, while u64DevicePosition is perfectly contiguous.
Each jittered packet either leaves a zero-filled hole or overwrites the end of the previous packet, which gives about 90 discontinuities per second.
Evidence 1: WASAPI loopback timestamps on the affected PC
I recorded 15 s with an event-driven, shared-mode loopback client on the endpoint Vibepollo captures. The endpoint is Steam Streaming Speakers (the default device during the stream), mix format float32 48 kHz stereo, device period 10 ms. The probe ran in the interactive session while a stream was playing audio.
| Measure | Value |
|---|---|
| Packets | 1501 × 480 frames |
Gaps in u64DevicePosition | 0 |
| QPC step minus 480 frames | median +0.05, p5 -26.0, p95 +26.2, min -46.8, max +47.5 frames |
| Drift of QPC position vs device position | 0.0 ms over 15 s |
QPC position minus the time GetBuffer returned | median +24.9 ms (range +20.5 to +25.0) |
Evidence 2: replay through the unmodified 2.0.4 buffer
I compiled audio_continuous_buffer.h from 2.0.4 unchanged (gcc 14, -std=c++20) and replayed the recorded packets.
Each packet becomes available at its recorded read time, and the loop follows sample(): drain, take() if ready, otherwise wait for the next packet or wait_ms().
The sample values are a strictly increasing counter, so any break in the output is an artifact.
| Input | Packet size | Zero-filled holes | Forward skips |
|---|---|---|---|
| Recorded QPC positions | 240 frames | 692 in 14.95 s (46.3/s), 1.22% of output samples | 655 (43.8/s), 8764 samples lost |
| Recorded QPC positions | 480 frames | 691 (46.3/s), 1.22% | 655 (43.9/s), 8764 samples lost |
| Control: QPC positions rebuilt from the device position | 240 frames | 0 | 0 |
The buffer is correct for evenly spaced timestamps. The noise comes from treating per-packet QPC positions as sample-accurate.
Replay harness (replay.cpp)
// Replays measured WASAPI loopback packets through Vibepollo 2.0.4's continuous_buffer_t,
// mirroring mic_wasapi_t::sample(): drain, take if ready, else wait for the next event or wait_ms.
// Input CSV columns: read_time_100ns,frames,flags,device_position,qpc_position_100ns,peak
#include "audio_continuous_buffer.h"
#include <cstdio>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
struct Pkt { long long read, qpc; unsigned frames; };
int main(int argc, char **argv) {
unsigned packet_frames = std::stoi(argv[2]);
std::ifstream in(argv[1]); std::string line; std::vector<Pkt> p;
while (std::getline(in, line)) {
if (line.empty() || line[0] == '#' || line[0] == 'n' || line.find("timeout") != std::string::npos) continue;
std::stringstream s(line); std::string f[6]; for (auto &x : f) std::getline(s, x, ',');
p.push_back({std::stoll(f[0]), std::stoll(f[4]), (unsigned) std::stoul(f[1])});
}
const long long origin = p[0].read - 100000;
platf::audio_detail::continuous_buffer_t buf(48000, 1, packet_frames, 480, origin);
std::vector<float> out, pkt(packet_frames), data;
float counter = 1;
size_t idx = 0; long long t = origin;
const long long end = p.back().read;
while (t < end) {
while (idx < p.size() && p[idx].read <= t) { // _fill_buffer(0): drain available packets
data.resize(p[idx].frames);
for (auto &v : data) v = counter++;
buf.put(p[idx].qpc, p[idx].frames, data.data(), false);
++idx;
}
buf.advance(t);
if (buf.take(t, pkt)) { out.insert(out.end(), pkt.begin(), pkt.end()); continue; }
long long wait = std::min<long long>(10, buf.wait_ms(t)) * 10000;
long long next = idx < p.size() ? p[idx].read : t + wait;
t = std::max(t + 1, std::min(t + wait, next));
}
size_t first = 0; while (first < out.size() && out[first] == 0) ++first;
long long zeros = 0, jumps_fwd = 0, jumps_back = 0, holes = 0, dropped = 0;
float prev = 0; bool in_hole = false;
for (size_t i = first; i < out.size(); ++i) {
float v = out[i];
if (v == 0) { ++zeros; if (!in_hole) { ++holes; in_hole = true; } continue; }
in_hole = false;
if (prev != 0 && v != prev + 1) { if (v > prev + 1) { ++jumps_fwd; dropped += (long long)(v - prev - 1); } else { ++jumps_back; } }
prev = v;
}
double secs = (out.size() - first) / 48000.0;
std::printf("packet=%u: %.2fs | holes %lld (%.1f/s, %lld zero samples) | skips %lld (%.1f/s, %lld lost) | rewinds %lld\n",
packet_frames, secs, holes, holes / secs, zeros, jumps_fwd, jumps_fwd / secs, dropped, jumps_back);
}Where it comes from
- Packets are placed by QPC position in
put(), called fromaudio.cpp#L897-L900. - The buffer was added in e7499d55d8a169e40de42568ffb373a195446292 ("fix(audio): Keep continuous loopback audio on one output clock").
It shipped together with the
continuousAudioparsing, so every release that honours continuous audio (2.0.3, 2.0.4) has it. - Before that, as in 2.0.1 and in upstream Sunshine's
sample(), captured packets are appended contiguously and silence is only inserted on a capture timeout, so timestamp jitter cannot create gaps. - Moonlight TV always requests continuous audio (client.c#L559), and Vibepollo has no host setting to ignore it, so webOS users cannot avoid this path.
Possible fixes (not tested)
- Place captured audio by
u64DevicePosition, which had no gaps here, and resync to the QPC clock only onAUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY, a device-position gap, or an offset well above one device period. - Or append packets contiguously as before, and use the QPC clock only to decide when to synthesize silence.
- As a stopgap, a host option to ignore the client's continuous-audio request.
Expected Behavior
Clean audio as in 2.0.1, while still sending silence packets when the host is quiet.
Windows Version
Windows 11 Pro 25H2 (build 26200.8524)
Package
Windows - exe installer
GPU Type
NVIDIA
GPU Model
RTX 3070 Ti
GPU Driver Version
621.01
Capture Method
Not relevant (audio issue)
Apps
undefinedLogs (Optional but Recommended)
Audio lines from a 2.0.4 session with the noise (no audio warnings or errors were logged):
[2026-11-02 17:25:02.455]: Info: Client requested continuous audio
[2026-11-02 17:25:02.936]: Info: Selected audio sink: virtual-Stereo{0.0.0.00000000}.{a1f50aad-c061-4b47-9233-74ec9afab2aa}
[2026-11-02 17:25:02.938]: Info: Virtual audio device will use 16-bit to match default device
[2026-11-02 17:25:02.952]: Info: Changed virtual audio sink format to [S16 48000 2.0]
[2026-11-02 17:25:03.059]: Info: Audio mixer format is 32-bit, 48000 Hz, no resampling needed
[2026-11-02 17:25:03.074]: Info: Audio capture format is [F32 48000 2.0]
[2026-11-02 17:25:03.093]: Info: Opus initialized: 48 kHz, 2 channels, 512 kbps (total), LOWDELAYScreenshot (Optional)
undefinedActivity
3Same issue here
Same issue here after upgrading from 2.0.1
I see the same on my PC, with the same cause.
Setup: Vibepollo 2.0.4 (
18400719), Windows 11 26H2 (build 26300), RTX 4090. The stream's sink is Steam Streaming Speakers (float32, 48 kHz, stereo, 10 ms device period). The client is a fork of Moonlight TV on an LG C1 (webOS 6), stereo, which also sendscontinuousAudio=1. 2.0.1 was clean with the same PC and client.Measured with an event-driven, shared-mode loopback client of my own on that endpoint, beside Vibepollo's capture, while a video played (the same method as above):
u64DevicePositioncontinuous_buffer_t(ported line by line, a counter as the sample value)Client side: once the client stopped sending
continuousAudio=1, 2.0.4 took the contiguous path and the sound was clean by ear. So the noise comes from the continuous path alone.Attached:
vibepollo-703-timings.csv, 1163 packets (1124 with sound), in the columns of the replay harness above:read_time_100ns,frames,flags,device_position,qpc_position_100ns,peak. There is no audio in it.Analysis assisted by Claude Code.
vibepollo-703-timings.csv