Skip to content

Retire the outage gap in the Pulse, PortAudio and PipeWire sinks - #59

Merged
chrisuthe merged 3 commits into
mainfrom
chrisuthe/task/retire-the-outage-gap-in-the-pulse-portaudio-and
Sep 17, 2026
Merged

chrisuthe merged 3 commits into
mainfrom
chrisuthe/task/retire-the-outage-gap-in-the-pulse-portaudio-and

Conversation

@chrisuthe

Copy link
Copy Markdown
Member

Fixes #58

Depends on #55 -- merge that first. This branch merges #55's head (a6b1c23) and builds on its SinkRecovery discard API.

While a sink has no live stream, write() still reports the audio as consumed, so the player's buffered-frame count takes it in, but nothing ever reports it as played. After recovery the playhead jumps ahead by the whole outage and sync cycles between Lost and Regained. This applies #55's ALSA fix to the other three device sinks.

  • Pulse: count discarded frames. Take the gap with the pa_stream_get_latency() timestamp under the lock, and report it together with the write's frames in one saturated on_frames_played call. Frames written to a stream that dies mid-write join the gap; a clear() flush does not count.
  • PortAudio and PipeWire: their callbacks take no lock, so write() hands the gap to them through a new atomic OutageGapHandoff. A callback retires it only when it reports real frames with a timestamp; a PipeWire cycle without timing leaves it pending. Frames still in the ring when recovery closes a lost stream are added to the gap.
  • All three: clear() and configure() drop the gap, handoff included, without refilling the recovery budget. Nothing reports the gap from reopen_in_place_() or poll().

Testing

  • Builds with ALSA, PortAudio, Pulse and PipeWire enabled under SENDSPIN_CLI_WERROR=ON.
  • New unit tests for the handoff and the saturating report count.
  • sendspin-cli-tests passes 440/440. Under ctest in a deeply nested build directory, two control-socket tests fail because the socket path goes past the 107-byte limit; they pass from a shorter path and are unrelated to this change.
  • Not yet tested on hardware by restarting the sound server or replugging a device mid-track.

Known imprecision

These cases are rare:

  • A clear() whose ring drain is still pending when a PortAudio or PipeWire device dies leaves a ring tail that can't be split into flushed and newer audio.
  • A Pulse configure() that fails while a write is waiting can count that one write's frames.

The pull-model sinks report from a lock-free callback that cannot read SinkRecovery, so write() hands the gap over through an atomic OutageGapHandoff once the stream is alive again.
# Conflicts:
#	src/pipewire_sink.cpp
#	src/portaudio_sink.cpp
#	src/pulse_sink.cpp
@chrisuthe
chrisuthe marked this pull request as ready for review September 17, 2026 00:45
@chrisuthe
chrisuthe merged commit 2236e42 into main Sep 17, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pulse, PortAudio and PipeWire sinks leave outage frames in the playback clock

1 participant