Skip to content

Resume in place when an audio effect changes - #20

Merged
Isma-L154 merged 1 commit into
mainfrom
feat/effects-resume-position
Aug 21, 2026
Merged

Resume in place when an audio effect changes#20
Isma-L154 merged 1 commit into
mainfrom
feat/effects-resume-position

Conversation

@Isma-L154

Copy link
Copy Markdown
Owner

Closes #9

The problem

Asking for a bass boost four minutes into a song threw you back to 0:00.

An FFmpeg filter chain is fixed for the life of the process, so changing an effect means respawning the stream — and the respawn always started from the top.

The fix

MusicPlayer now tracks playback position and passes it to the new FFmpeg process, so the effect appears to apply in place.

Why -ss goes in before_options

As an input option FFmpeg discards packets without decoding them. As an output option it decodes every frame and throws it away.

On a piped stream that distinction matters less than it sounds, because yt-dlp delivers at network speed rather than in realtime. Measured against a real 30-second source:

Seek Wall time Output
none 0.08s 30.02s
-ss 10 0.03s 20.02s
-ss 25 0.03s 5.02s

Paused time does not count

_start_ts alone would treat a track paused for a coffee break as still playing and resume minutes too late. pause and resume are now routed through the player, which stops and restarts its own clock:

play 3:00 → pause → 10 minutes idle → resume → !bassboost

resumes at 3:00, not 13:00.

After a respawn the clock is backdated by the seek, so a second effect change resumes from the real position rather than from where the respawn happened.

Where seeking is deliberately skipped

Case Behaviour Why
Live stream (no duration) restart, no seek there is no position to seek to
Position within 2s of the end restart, no seek resuming there lands in silence
Position past the end restart, no seek nothing left to play
Position 0 no seek a no-op that only costs a slower start

One subtle thing this touched

The "the source failed to load" heuristic compares elapsed wall time against a 2-second threshold. It read _start_ts, which is no longer wall-clock once backdating is involved — a track resumed at 3:00 would have looked like it had already played 180 seconds. It now measures from the spawn instead. Replays were already excluded from that check, so this was latent rather than broken, but it would have bitten the next person to touch it.

Verification

230 passed, three consecutive runs, 40 of them new.

Through the real audio path — not mocks. tests/test_effect_filters.py renders a 30-second MP3, feeds it through the actual make_pipe_source, drains the resulting AudioSource frame by frame and counts 20 ms frames:

Seek Expected Measured
0s 30.0s 30.0s
10s 20.0s 20.0s
25s 5.0s 5.0s
20s + bassboost filter 10.0s 10.0s

Clock behaviourtests/test_player_seek.py drives a fake clock so the assertions are exact rather than timing-dependent: paused time excluded, repeated pause/resume cycles accumulating correctly, a double !pause not losing the clock, and apply_effect while paused not shifting the resume point.

Notes for review

  • !pause / !resume now go through players.get(...) instead of ctx.voice_client directly. Behaviour when nothing is playing is unchanged and still covered by the existing tests.
  • The 3-second per-guild effect cooldown still applies, so this cannot be used to hammer FFmpeg respawns.
  • The comment in cogs/effects.py claiming effects restart "(from the beginning)" is now correct again.

Asking for a bass boost four minutes into a song threw the listener back to
0:00. An FFmpeg filter chain is fixed for the life of the process, so changing
one means respawning the stream, and the respawn always started from the top.

The player now tracks playback position and passes it to the new process as an
FFmpeg input option, so the effect appears to apply in place. -ss goes in
before_options rather than options: as an input option FFmpeg discards packets
without decoding them, which on a piped stream costs almost nothing because
yt-dlp delivers at network speed rather than in realtime. Measured at 0.03s to
skip 25 seconds.

Position excludes time spent paused. _start_ts alone would have counted a
track paused for a coffee break as still playing and resumed minutes too late,
so pause and resume are routed through the player, which stops and restarts its
own clock. After a respawn the clock is backdated by the seek, so a second
effect change resumes from the real position rather than from the respawn
point.

Seeking is skipped where it would be wrong: live streams report no duration and
have no position to seek to, and a position inside the last two seconds would
resume into silence.

The load-failure heuristic now measures from the spawn instead of _start_ts,
which is no longer wall-clock once backdating is involved.

Verified through the real audio path: seeking 10s into a 30s source yields
20.0s of playback and 25s yields 5.0s, with an effect filter applied on top.

Closes #9
@Isma-L154
Isma-L154 merged commit acc32b0 into main Aug 21, 2026
3 checks passed
@Isma-L154
Isma-L154 deleted the feat/effects-resume-position branch August 21, 2026 02:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Applying an audio effect restarts the track from the beginning

1 participant