Skip to content

-o pulse: continuous Lost sync/Regained sync with audible artifacts, no underruns #32

Description

@chrisuthe

Summary

Playing through -o pulse, the sync task reports Lost sync / Regained sync
continuously -- roughly 1-2 times a minute, indefinitely -- and the result is audible
artifacts. Nothing underruns: pw-top reports 0 xruns on both the client node and the
sink for the entire run, and the sink itself never logs a short write or any audio:-tag
warning. Every sample the graph was handed arrived on time, so the corruption is coming
from the sync layer correcting against a latency figure it does not trust.

-o pipewire on the same host, same sink, same server and same source shows none of
this.

Not a regression from #27's review fixes

Checked directly, because 26e61b1 changed the write() wait in pulse_sink.cpp. Both
builds were run on the same Pi, same sink, same server, same tracks:

Build
9379e768 (before the review fixes) reproduces same two clusters, same magnitudes
52d0f2c8 (merged) reproduces same two clusters, same magnitudes

So this is in the PulseAudio backend as originally written, not something the review fixes
introduced.

The signature

The sync errors are not scattered -- they fall into two tight clusters with balanced
counts
, and largely alternate between them:

48 kHz, magnitudes in order (ms):
86.5  7.5  86.9  7.1  91.0  8.6  90.8  6.3  5.9  91.0  6.2  87.3  6.9  91.0
 L    S    L    S    L    S    L    S    S    L    S    L    S    L

Aggregate over both rates:

Stream n small cluster large cluster gap server queue
192 kHz / 24-bit 278 7.7 ms (n=134) 78.3 ms (n=144) 70.7 ms 70 ms
48 kHz / 16-bit 15 6.9 ms (n=8) 89.2 ms (n=7) 82.4 ms 70 ms

Two things stand out:

  • The small cluster sits at ~7 ms at both rates, and the counts are near-balanced
    (134/144 and 8/7) -- consistent with a quantity that is present on some measurements and
    absent on others, rather than with jitter or scheduling noise.
  • At 192 kHz the gap between clusters is 70.7 ms against a 70 ms server queue, which is
    as close as that gets on a 278-event sample. At 48 kHz the same queue is also 70 ms but
    the gap is 82.4 ms. So "one queue depth counted or not" fits one rate very well and the
    other not at all. Recording it as a lead, not a conclusion.

For reference, the sink is opened with the same queue duration at both rates:

192 kHz/24: 80640 B / 6 B-per-frame = 13440 frames = 70 ms
 48 kHz/16: 13440 B / 4 B-per-frame =  3360 frames = 70 ms

and the client node ran a 30.0 ms quantum at both rates (5760 @ 192k, 1440 @ 48k) against a
driver quantum of 1024 @ 48 kHz = 21.3 ms.

Not rate-specific, and not the resampler

Initially this looked like it might be the 4:1 resample from a 192 kHz source down to the
48 kHz HDMI sink. It is not: at 48 kHz, where client and sink rates match and no resampling
happens, the errors continue at a comparable rate.

Environment

sendspin-cli 52d0f2c8 and 9379e768, release artifacts, linux-arm64
sendspin-cpp v0.7.2
Host Raspberry Pi 5, Debian 13 (trixie), aarch64, kernel 6.12.47
Output -o pulse -> pipewire-pulse, libpipewire 1.4.2, wireplumber
Sink Built-in HDMI, s32le 2ch 48000Hz
Server Music Assistant

Where to look

PulseAudioSink::write() derives its sync feedback from pa_stream_get_latency() and
reports it through on_frames_played(frames, finish_us). That call is the whole reason the
native PulseAudio backend exists in preference to -o alsa:pulse -- #27's argument was that
it answers for the server's real playout rather than for a plugin's own buffering. These
numbers put that in question. -o pipewire, which derives the same feedback from pw_time,
is clean on the identical path.

Caveats

  • The 48 kHz sample is small (15 events); the 192 kHz one is not (278).
  • Event rate estimates are unstable across windows on the same binary and rate (1.2/min
    over 5 minutes, 3.3/min over 90 seconds). Treat rates as soft; the cluster locations
    are the stable statistic and they reproduce across builds and rates.
  • The link between these events and the audible artifacts is correlation. It is a strong
    correlation -- resync means dropping or inserting samples, and there are no underruns to
    explain the sound any other way -- but it has not been demonstrated directly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions