Skip to content

fix(audio): keep stream open on clear() so DACs hold lock across pause - #303

Closed
kiwipaulrob wants to merge 1 commit into
Sendspin:mainfrom
kiwipaulrob:fix/keep-stream-open-on-clear
Closed

kiwipaulrob wants to merge 1 commit into
Sendspin:mainfrom
kiwipaulrob:fix/keep-stream-open-on-clear

Conversation

@kiwipaulrob

@kiwipaulrob kiwipaulrob commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Pausing (or seeking/skipping) makes USB DACs with lock-loss muting relays click audibly on every transition. Pause arrives as stream/end, which ran close_stream() and fully closed the PortAudio stream; resume then recreated it via set_format(). Each close/open drops USB lock and the relay ticks. No server-side mute or fade can suppress a hardware relay: I verified live against an SMSL USB DAC on a moOde Raspberry Pi that the click sounds identical through full digital silence.

Evidence

  • AudioPlayer.clear() submitted stream.stop to release the device on pause; close_stream() went further and fully closed it; set_format() unconditionally closed and recreated the stream even for an identical format.
  • Live: MA server log shows the flow stream tearing down/rebuilding on pause/resume; daughter test on the Pi (/proc/asound PCM state) confirmed the device FD closes on pause with the released code.
  • Same symptom class widely reported for XMOS USB DACs (stream-only-when-necessary drivers); the Linux client had no keep-open path at all.

Fix

  • clear() and close_stream() now drain the queue but leave the stream running. The callback already renders silence while starved (_read_input_frames_bulk pads with zeros), so the device stays open at zero audible cost.
  • set_format() skips the close/recreate cycle when the incoming format equals the current one (dataclass equality), so resume on the same format is a no-op and music continues on the held-open stream.
  • Release still happens where it must: stop() (daemon shutdown) and set_format() on a real format change.

Known cosmetic trade-off, disclosed: while paused with the stream held open, DAC-derived playback position keeps advancing (silence still ticks the clock). It resyncs on resume. Happy to freeze the cursor too if preferred - kept out to hold this change minimal and out of the callback hot path.

Verification

  • Regression tests: clear() and close_stream() drain without stopping/closing (queue empty, _stream_started true, same stream object, zero stop/close calls); set_format() with an identical format keeps the existing stream untouched.
  • Full suite: 159 passed. ruff format --check, ruff check, mypy sendspin all clean.
  • Live proof pending: deploying the branch on the moOde Pi and reporting pause/resume/skip before/after.

Types of changes

  • Bugfix: bugfix

@github-actions github-actions Bot added the bugfix Fixes a bug label Oct 25, 2026
@kiwipaulrob
kiwipaulrob force-pushed the fix/keep-stream-open-on-clear branch from c239b81 to f5f0418 Compare October 25, 2026 20:15
Pause arrives as stream/end, which ran close_stream() and fully closed the PortAudio stream; resume then recreated it via set_format(). Each close/open drops USB lock, clicking muting relays on XMOS-based DACs (verified live: the click survives full digital silence, so no server-side mute or fade can suppress it).

clear() and close_stream() now drain the queue but leave the stream running - the callback already renders silence while starved - and set_format() skips recreating the stream when the format is unchanged. The device releases on stop() (shutdown) and on real format changes only.
@kiwipaulrob
kiwipaulrob force-pushed the fix/keep-stream-open-on-clear branch from f5f0418 to 74416f1 Compare October 25, 2026 20:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fixes a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant