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.
Summary
Playing through
-o pulse, the sync task reportsLost sync/Regained synccontinuously -- roughly 1-2 times a minute, indefinitely -- and the result is audible
artifacts. Nothing underruns:
pw-topreports 0 xruns on both the client node and thesink for the entire run, and the sink itself never logs a short write or any
audio:-tagwarning. 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 pipewireon the same host, same sink, same server and same source shows none ofthis.
Not a regression from #27's review fixes
Checked directly, because 26e61b1 changed the
write()wait inpulse_sink.cpp. Bothbuilds were run on the same Pi, same sink, same server, same tracks:
9379e768(before the review fixes)52d0f2c8(merged)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:
Aggregate over both rates:
Two things stand out:
(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.
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:
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
52d0f2c8and9379e768, release artifacts,linux-arm64-o pulse->pipewire-pulse, libpipewire 1.4.2, wireplumbers32le 2ch 48000HzWhere to look
PulseAudioSink::write()derives its sync feedback frompa_stream_get_latency()andreports it through
on_frames_played(frames, finish_us). That call is the whole reason thenative PulseAudio backend exists in preference to
-o alsa:pulse-- #27's argument was thatit 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 frompw_time,is clean on the identical path.
Caveats
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.
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.