#55 fixes this for the ALSA sink (#54). The other three device-backed sinks have the same gap.
When the stream is gone, each sink's write() accepts audio so the sync task does not spin:
src/pulse_sink.cpp:452
src/portaudio_sink.cpp:532
src/pipewire_sink.cpp:437
The sync task adds those bytes to buffered_frames, but no on_frames_played report ever retires them. After the stream recovers, the first real progress report computes finish_timestamp + frames_to_us(buffered_frames), so the playhead jumps ahead by the whole outage. The result is sync-loss cycling until the next stream resets the count.
Port the ALSA approach from #55:
- Count discarded frames in
SinkRecovery (discard_frames()), under the sink's lock.
- Do not report the gap from the recovery path. Take it with the first report that has a device timestamp, and retire it in that same
on_frames_played call, saturating at UINT32_MAX.
- Count frames written just before a loss (the stream closed mid-write) as discarded too.
- Drop the gap in
clear() and at the top of configure() with forget_discarded_frames(), without refilling the budget.
Only Pulse reports progress from write() (pulse_sink.cpp:581), so it can follow ALSA directly. PortAudio (portaudio_sink.cpp:1010) and PipeWire (pipewire_sink.cpp:711) report from their audio callbacks, so the gap has to be taken there, under whatever guards the count on that thread.
#55 fixes this for the ALSA sink (#54). The other three device-backed sinks have the same gap.
When the stream is gone, each sink's
write()accepts audio so the sync task does not spin:src/pulse_sink.cpp:452src/portaudio_sink.cpp:532src/pipewire_sink.cpp:437The sync task adds those bytes to
buffered_frames, but noon_frames_playedreport ever retires them. After the stream recovers, the first real progress report computesfinish_timestamp + frames_to_us(buffered_frames), so the playhead jumps ahead by the whole outage. The result is sync-loss cycling until the next stream resets the count.Port the ALSA approach from #55:
SinkRecovery(discard_frames()), under the sink's lock.on_frames_playedcall, saturating atUINT32_MAX.clear()and at the top ofconfigure()withforget_discarded_frames(), without refilling the budget.Only Pulse reports progress from
write()(pulse_sink.cpp:581), so it can follow ALSA directly. PortAudio (portaudio_sink.cpp:1010) and PipeWire (pipewire_sink.cpp:711) report from their audio callbacks, so the gap has to be taken there, under whatever guards the count on that thread.