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.
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