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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user