Repository navigation
fix: hold sync corrections while a cold output stream's delay settles - #103
Merged
teancom merged 2 commits intoOct 13, 2026
Merged
Conversation
On some backends (observed with ALSA over PulseAudio/PipeWire) the output delay reported for a freshly opened stream overshoots by a few hundred milliseconds in the first second and then relaxes back over several seconds, while the audio plays gaplessly. The sync error is measured against that delay, so it moves with it, and the corrector responded by speeding playback up by up to 4% and then slowing it down: an audible warble for 5-10 seconds after each cold start. - Add OutputDelaySettle, which tracks the floor of the reported output delay over 0.5s windows and reports settled once it has held steady for four consecutive windows. - Hold corrections while the delay is unsettled, for at most 10s after playback starts. Re-seed the delay floor and reset the error filter and engage gate when it settles, since earlier measurements describe the transient. - Limit speed corrections to 0.5% for the first 20s after a start, so any residue of the transient that outlasts the grace period stays inaudible. - Add CorrectionPlanner::with_max_speed_correction. Tests cover settling on a steady delay, period flapping, the cold-start ramp, slow relaxation, a plateau followed by further relaxation, reset, and the capped planner.
Contributor
|
Lovely, thank you! |
Merged
teancom
pushed a commit
to music-assistant/desktop-app
that referenced
this pull request
Oct 14, 2026
## Reuse the open audio output stream between streams Fixes an audible warble (slow/wobbly pitch for 5-10 s) at the start of every playlist on Linux. Addresses #203 for every play after the first; the first play after launch is covered by the library PR linked at the bottom, so this should not close the issue on its own. ### Problem `run_playback_thread` builds a new `SyncedPlayer` on every `CreatePlayer`, i.e. at every `StreamStart`. A freshly opened output stream is cold: while it settles, the output delay the OS reports overshoots by a few hundred milliseconds (about 550 ms within the first second on my setup) and then relaxes back to its resting value (about 250 ms) over ~5 s. The audio plays gaplessly, but the sync error is measured against that delay, so it swings by the same amount. The corrector reads the swing as drift, speeds playback up by up to 4% (`drop_every=25`), overshoots, and then slows it down by about 1.5% (`insert_every=54..70`). ### Evidence - The library's example player (unpatched `sendspin-rs`, same server, same Flatpak sandbox) with one persistent output stream: no warble, start errors of 4-47 ms, corrections of 0.2-0.5%. - The same program with a single change, dropping and rebuilding the output stream at each stream start: the warble comes back with the Companion's signature (358-391 ms initial error, a 4% correction, then a -30 to -37 ms overshoot). - Ruled out: authenticated port 8095 vs direct 8927, the Flatpak sandbox, the `sendspin-rs` version, PipeWire quantum, network. ### Change On `CreatePlayer`, keep the existing output stream if all of these hold, otherwise rebuild exactly as before: - the PCM format is the same, - the freshly resolved output device has the same identity as the one the open stream was built for, - the previous stream ended no more than 10 minutes ago (`OUTPUT_STREAM_REUSE_MAX_IDLE`). The device is still re-resolved on every `StreamStart`, so the behavior from #40 (a device that disappeared or reconnected is picked up at the next play) is kept. If the device identity cannot be determined, the stream is rebuilt. The reuse decision is the pure function `can_reuse_output_stream`, with unit tests. ### Testing - `cargo fmt --check`, `cargo clippy --all-targets --all-features -- -D warnings`, `cargo test` (132 passed, 4 new). - Built and ran on Pop!_OS (ALSA via PulseAudio/PipeWire, USB DAC). Ten stream starts in a row: the first created a stream, the other nine logged `Reusing open audio output stream`, and none of the later plays warbled. - Not tested: macOS, Windows, Bluetooth devices. ### Notes - The first play after launch (or after more than 10 minutes idle) still opens a cold stream. That part is addressed in the library: Sendspin/sendspin-rs#103. This app currently pins `sendspin-rs` at `v0.3.5`, so it will need a version bump once that is released. - With the library fix and this change together, the first play and all later plays are clean on my setup; the strongest speed correction in a run drops from 4% to 0.5%.
teancom
pushed a commit
that referenced
this pull request
Oct 15, 2026
## 🤖 New release * `sendspin`: 0.3.7 -> 0.3.8 (✓ API compatible changes) <details><summary><i><b>Changelog</b></i></summary><p> <blockquote> ## [0.3.8](v0.3.7...v0.3.8) - 2026-10-15 ### Fixed - *(audio)* support unsigned 24-bit output ([#107](#107)) - hold sync corrections while a cold output stream's delay settles ([#103](#103)) ### Other - cover startup synchronization correction transitions ([#106](#106)) ### Fixed - Stop a freshly opened output stream from being corrected at up to 4% speed for several seconds after playback starts. On some backends (observed with ALSA over PulseAudio/PipeWire) the reported output delay of a cold stream overshoots by a few hundred milliseconds and then relaxes while the audio plays gaplessly, and the sync error moves with it. Corrections are now held back until 500ms observation-window floors stay within a 10ms range over a baseline window and two seconds of comparisons, independent of callback size (capped at 10s after playback starts). Speed corrections are limited to 0.5% for the first 20s after a start. Callback gaps of 500ms or more discard settling evidence. The all-time delta floor used for reanchors is re-seeded from the observed floor each time the delay settles. </blockquote> </p></details> --- This PR was generated with [release-plz](https://github.com/release-plz/release-plz/). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix: hold sync corrections while a cold output stream's delay settles
Fixes an audible warble (pitch speeding up, then slowing down, for 5-10 s) after playback starts on a freshly opened output stream. Found while debugging the Music Assistant Companion app on Linux: music-assistant/desktop-app#203.
Problem
On some backends (observed with ALSA over PulseAudio/PipeWire) the output delay reported for a cold stream does not start at its resting value. It overshoots by a few hundred milliseconds in the first second (about 550 ms here) and then relaxes back (to about 250 ms) over several seconds. The audio plays gaplessly; only the estimate moves.
raw_erroris measured against that estimate and follows it almost 1:1 (with a roughly constant offset), so the planner sees a few hundred ms of "drift" that resolves by itself. It engages at the full 4% cap, then overshoots the other way and slows playback down. That is the warble.Trace excerpt from two cold starts (callback,
playback_delta,raw_error):with
Sync correction engaged: ... drop_every=25(4%), laterinsert_every=54..70(about 1.5%). No underruns, no gaps.The existing
SyncErrorFilterfloor-filters one-sided period flapping, but a multi-second relaxation of several hundred milliseconds is a different shape and gets through.Related
App-side change that removes the cold start for every play after the first:
music-assistant/desktop-app#204
This follows the same idea as #71 (don't treat the beginning of a stream as if it may have drifted, and trust the server's audio until the two clocks have had a chance to differ). #71 and the clock warm-up suppression cover the start of the clock; this covers the start of the output stream, whose reported delay is not in steady state for the first seconds on some backends. The two use the same shape: suppress corrections for a bounded period, then resume normal operation.
Change
OutputDelaySettle(insync_correction.rs): tracks the minimum reported delay over 50-callback windows and reports settled once the floor has stayed within 10 ms of the previous window for four consecutive windows. A plateau of a fraction of a second near the peak is not enough (an earlier version with two windows declared itself settled on exactly such a plateau, and the full-rate correction and warble came back at about 7-10 s).CorrectionPlanner::with_max_speed_correction, so any residue of the transient that outlasts the grace period stays inaudible. Thresholds are unchanged.Output delay settled at ...and a trace line for suppressed corrections.Testing
cargo fmt --check,cargo clippy --all-targets,cargo test(346 passed). New unit tests: settles on a steady delay, ignores period flapping, rejects the cold-start ramp, rejects slow relaxation, rejects a plateau followed by more relaxation, un-settles when the delay moves again, reset, and the capped planner.Caveats