v0.6.1: the parking brake — stop playing audio at 3am

A paused or stopped QMediaPlayer on Qt's FFmpeg backend keeps its PipeWire
stream open and never corks or drains it, so the few seconds the backend
decoded ahead sit there live. A later audio-graph change — a USB DAC waking
from idle suspend, a device appearing — flushes that stale buffer to the
speakers, hours after the app was last touched.

That is the "LinTunes plays by itself" haunting: the v0.1.4 provenance log
recorded zero control events between the 17:48 pause and the 00:30 incident,
while MPRIS still reported Paused at exactly 6974000us — the position it was
paused at seven hours earlier — and PipeWire showed the stream state=running.
It explains the whole shape of it: always a short burst (only what was
buffered), always mid-song, never a recorded play count.

After 30s idle, release the pipeline: stop() then setSource(QUrl()), which
removes the PipeWire node outright, so no buffer survives to leak. Gain drops
to zero first as an independent second layer. stop() arms the brake too — Qt
leaves the source loaded there, and _advance() takes that path when a playlist
runs out. Resume rebuilds the source and seeks back via the existing
custom-start-time machinery.

Verified end-to-end against the real LocalSink with a silent WAV, watched
through pw-dump: present while playing, still present right after pause, gone
once parked, rebuilt on resume with position and duration intact.

This is a workaround for an upstream Qt Multimedia bug; TASKS.md now carries
the assessment for moving the local sink to GStreamer, which gets correct
pause behavior for free and would unlock the parked equalizer and gapless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 14:01:36 -04:00
co-authored by Claude Opus 5
parent 6fad0247ee
commit f1a949810e
5 changed files with 395 additions and 12 deletions
+34
View File
@@ -358,6 +358,40 @@ Tests land in `tests/test_round17.py`.
decode→filter→output pipeline (replaces playback; risky for
seek/formats) or leaning on the system (PipeWire filter-chain /
EasyEffects). Needs a design decision before building.
**See the backend note below — a GStreamer sink would make this a
drop-in `equalizer-3bands` element instead of a redesign.**
- [ ] Swap the local sink from Qt Multimedia to GStreamer (`GstSink`).
Assessed 2026-08-15 while fixing the Round 34 audio leak. Not urgent —
the parking brake handles the leak — but the case is real and it should
be the plan whenever the equalizer or gapless comes up, since those are
what make it pay for itself.
Why: Qt Multimedia's FFmpeg backend has cost us five workarounds now, all
of them in `LocalSink` — the stale-`LoadedMedia` URL tagging, the
`setSource` no-op dance, the resume nudge, the shutdown-ordering segfault
guard, and now the parking brake. GStreamer is what every other Linux
music player uses (Rhythmbox, Lollypop, Amberol), corks properly on
pause, and unlocks three parked/impossible features: a 3-band EQ
(`equalizer-3bands`), true gapless (`playbin3` `about-to-finish`), and
ReplayGain (`rgvolume`).
Cost: `LocalSink` is ~180 lines and `PlaybackSink` is already a real
seam (proven by `CastSink`), so the sink itself is bounded. The bigger
cost is the tests — 10+ files stub Qt Multimedia *by name* via
`patch.multiple(player_module, QMediaPlayer=…, QAudioOutput=…,
QAudioBufferOutput=…, QMediaDevices=…)`. Also: new system dep
(`python3-gi` + `gstreamer1.0-*` via apt here, `python3-gobject` +
`gstreamer1-plugins-*` via dnf on the Fedora machine — the self-updater
only does `git pull`, so both machines need it installed by hand before
the push lands), GLib bus pumped from a `QTimer` rather than a GLib main
loop, and the visualizer's PCM tee rebuilt on `appsink` instead of
`QAudioBufferOutput`.
Do it incrementally: write `GstSink` alongside `LocalSink`, put it behind
a preference, run it for a week, delete the Qt one when it's trusted.
Already available here: GStreamer 1.26.2 + Python bindings + libav,
pipewire and good/base plugin sets.
- [ ] Smart playlists Phase 2: nested-group editing UI in the criteria
dialog (import + evaluation of nested groups already works; imported
nested playlists are read-only until then). Big change, on hold.