feat: debounce rapid next/prev into a single deferred track load - #350
Open
palchrb wants to merge 1 commit into
Open
feat: debounce rapid next/prev into a single deferred track load#350palchrb wants to merge 1 commit into
palchrb wants to merge 1 commit into
Conversation
This was referenced Aug 3, 2026
Holding down next on a Connect controller currently loads every track it passes through: each press resolves a track, requests an audio key and opens a stream, only to throw all of it away when the next press arrives milliseconds later. Skipping ten tracks costs ten audio-key requests, and Spotify starts answering 429 well before a listener has found what they were looking for. Split the advance into a pointer move and a load. A skip that arrives inside skip_debounce_ms of the previous one moves the pointer and arms a settle timer instead of loading; the timer is reset by every further skip, so a burst of any length resolves to exactly one load of the track the user actually stopped on. The first skip of a burst still loads immediately, so a single press is as responsive as before, and setting skip_debounce_ms to 0 disables debouncing entirely. The settle timer is a case in the existing Run loop select, alongside the prefetch and state timers, so the deferred load runs on the same goroutine as every other player mutation and needs no locking. Clients are not left guessing during the wait: every pointer move emits will_play with the pending track, and the daemon exposes the pending position so a UI can follow the burst in real time. The window defaults to 600ms, chosen from measurement rather than taste. Burst detection measures the gap between one skip finishing and the next arriving, and that gap is far wider than the interval between presses when the controller is the Spotify app, which paces its Connect commands. Cached tracks widen it further rather than narrowing it: they load in a fraction of the time, so the daemon spends longer idle between skips and each press starts to look like the first press of a new burst. Across two measured sessions of continuous mashing from the app, the gaps ranged from 74ms to 495ms — a 400ms window classified a third of one burst as fresh presses. The costs of mis-sizing are asymmetric: a window that is slightly too small splits a burst and costs one extra load, while a window that is too large adds its full length to the silence after the last press of every burst. 600ms covers everything observed with headroom without making the settle noticeably laggy. Deferred pointer moves do not publish the connect state. Each one is superseded within the window, so PUTting them describes a track that is never loaded and outruns the endpoint's coalescing interval — the same 429 the feature exists to avoid, on a different endpoint. Only the first deferral publishes, which is what tells the app that skipping has started, and the settled load publishes the track that was chosen. A client driving its own UI from the websocket is unaffected, since will_play still fires on every move. Several paths need to know a load is outstanding. Prefetching is suppressed during a settle, since it would perform the very requests being deferred. The not-playing and stop player events are ignored, because the pointer has already moved past the track that ended and the output will be reopened by the settle. A seek flushes the pending load first, so it acts on the track the user chose rather than the one still playing. The wait itself is not counted as playback, so the settled track starts at 0:00 instead of skip_debounce_ms in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019FHWG4ossSBydy7jbVFWho
palchrb
force-pushed
the
claude/upstream-skip-debounce
branch
from
August 4, 2026 05:14
c7fade9 to
70376a8
Compare
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.
Closes the first half of #346.
A burst of next/prev presses currently loads every track it passes through — one audio-key request per press — and Spotify answers 429 well before the listener finds their song. This splits the skip into a pointer move and a deferred load: presses move the selection immediately (published via will_play and a new pending_track_uri in /status), and only the track the user stops on gets loaded.
The window (skip_debounce_ms, default 600, 0 disables) was measured, not guessed: across two sessions of continuous skipping from the Spotify iOS app, gaps between Connect commands ranged 74–495ms. Deferred pointer moves also skip the connect-state PUT (only the first of a burst publishes) — the burst otherwise outruns the endpoint's tolerance and reproduces the same 429 on a different endpoint. Field-tested on a Pi Zero 2 W: a 20-press burst now costs 2 loads and no rate limiting.