Skip to content

fix(player): play the end of a track out of the ring before the next one starts - #933

Merged
InstaZDLL merged 2 commits into
mainfrom
fix/track-end-cut
Oct 21, 2026
Merged

InstaZDLL merged 2 commits into
mainfrom
fix/track-end-cut

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Refs #932, its first point: "every track stops about 1 second before its real end and the next track starts immediately". The second point, single-click play, is being clarified with the reporter.

Cause

The decoder runs up to a ring ahead of the output. RING_CAPACITY is 96 000 samples: 1 s at 48 kHz stereo, 0.27 s on an 8-channel mixer at 44.1 kHz. Two things followed from that, and together they cut the end of every track that had another one after it.

  • A natural end was declared at the decoder's EOF, with the end of the track still queued in the ring. The advance that followed sent LoadAndPlay, and its unpause_and_drain_ring (right for a skip) threw that audio away.
  • The gapless and crossfade triggers read the heard clock (current_position_ms), a ring behind the decoder:
    • Gapless asked for the next track 500 ms before the end as heard, a point the decoder reached only after its EOF. So the in-place swap, the one path that keeps the tail, never ran.
    • A crossfade started a ring late and lasted a ring less, and one shorter than the ring never started.

This has been the behaviour since the drain on load arrived in April, so it is not a 1.9.5 regression. The last track of a queue was spared, because nothing follows it.

Fix

  • SharedPlayback::decoded_position_ms(queued): where the decoder is, counted from the raw counters as samples played plus samples queued, minus the lead-in. It is not computed as the heard position plus the queue, because the heard position stops at 0 while a gapless lead-in plays, and adding the queue to it would count the finished track's tail as the new track's. Both triggers now read it.

  • TailDrain: at EOF, play_track and play_dop_track wait until the output has played the ring out, then emit player:track-ended. While they wait:

    • the loop keeps serving commands: a pause holds the wait, and a seek or another track ends it;
    • the gapless trigger keeps running, so a next track that arrives in time still takes over in the swap;
    • if the ring has not moved for 1.5 s while playing (a lost device), the wait gives up.

    As a side effect, the sleep timer's end of track and listened_ms now match the end that was heard.

  • Docs: docs/features/playback.md explains both rules and the measurement.

Checks

  • End-to-end measurement: sandboxed debug builds of main and of this branch were driven over CDP. A queue of three FLAC files was played, then seeked to 4 s before the end of the first one (139.2 s long). The test machine's mixer is 8 channels, so its ring is 0.27 s.

    build last position of track 1 heard of the last 4 s
    main 138.9 s 3.82 s
    this branch 139.15 s 4.09 s
  • Tests:

    • decoded_position_ms: one test for the ring ahead, including at double speed, and one for the gapless lead-in;
    • TailDrain: an empty ring, a ring being played out, a pause, and a stall.

    The audio module's 179 tests pass on macOS, where the app crate's test binary runs.

  • cargo fmt --check and cargo clippy --workspace --all-targets pass on Windows and on macOS (-D warnings).

  • The local CodeRabbit review found the lead-in miscount described above, which is now fixed.

Known side effect

Now that the gapless swap actually runs, the analytics CrossfadeStarted it sends (title, cover) arrives at the decoder's EOF, so about a ring before the next track is heard. The position stays at 0 through the lead-in, so lyrics and the seek bar are not ahead. Moving that UI switch is left for a follow-up.

Summary by CodeRabbit

  • Corrections
    • Amélioration de la détection de fin de piste afin d’éviter les coupures prématurées.
    • Lecture plus fiable des derniers échantillons avant le passage à la piste suivante.
    • Meilleure gestion des transitions sans interruption et des fondus enchaînés, y compris lorsque la piste arrive à sa fin.
    • Les commandes concurrentes, pauses, recherches et boucles A-B sont désormais mieux prises en compte pendant ces transitions.
    • Ajout d’un délai de sécurité pour éviter qu’une piste ne reste bloquée en attente.

…one starts

The decoder runs a ring (1 s at 48 kHz stereo) ahead of the output. A natural end was declared at the decoder's EOF and the advance's LoadAndPlay drained what was still queued, cutting the last second of every track; the gapless and crossfade triggers read the heard clock, so the gapless swap never ran. The triggers now read the decoder's position and both play loops wait for the ring to play out before declaring the end. Refs #932
@InstaZDLL InstaZDLL added scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets labels Oct 21, 2026
@InstaZDLL InstaZDLL added type: fix Bug fix size: l 200-500 lines labels Oct 21, 2026
A pause blocks the decoder in drain_commands, so the tail wait counted the paused time as a stall and cut the rest on resume; a gap between checks now restarts the stall clock. An A-B jump made while the end plays out cleared nothing and ended the track; it now resets the EOF state like a seek.
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: InstaZDLL/WaveFlow/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 0d4ecc45-d680-4acc-be30-91d0a65663c1
📥 Commits

Reviewing files that changed from the base of the PR and between e9e7c18 and 59d2718.

📒 Files selected for processing: 3
  • docs/features/playback.md
  • src-tauri/crates/app/src/audio/decoder.rs
  • src-tauri/crates/app/src/audio/state.rs

Included review availability: This review used your included allowance. 1 included review remains after this review. Your included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

📝 Walkthrough

La lecture utilise la position décodée pour planifier les transitions gapless et crossfade. À l’EOF, les parcours PCM et DoP attendent le drainage de l’audio en file avant de signaler la fin naturelle, tout en tenant compte des pauses, recherches et boucles A-B.

Changes: Fin de lecture audio

Layer / File(s) Summary
Position décodée et planification
src-tauri/crates/app/src/audio/state.rs, src-tauri/crates/app/src/audio/decoder.rs
decoded_position_ms inclut les échantillons en file. Le crossfade et le préchargement gapless utilisent cette position pour déterminer leur déclenchement. Les tests couvrent la vitesse de lecture et les transitions gapless.
Drainage à la fin de piste
src-tauri/crates/app/src/audio/decoder.rs, docs/features/playback.md
Les parcours PCM et DoP attendent le drainage de la file avant la fin naturelle. Les recherches et les boucles A-B réinitialisent le drainage. Une pause suspend son délai, qui expire après 1,5 seconde sans progression. La documentation décrit ces règles.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Decoder
  participant TailDrain
  participant AudioRing
  participant Player
  Decoder->>TailDrain: vérifie le drainage à l’EOF
  TailDrain->>AudioRing: lit les échantillons en file
  AudioRing-->>TailDrain: renvoie l’état de la file
  TailDrain-->>Decoder: indique si l’attente continue ou se termine
  Decoder->>Player: émet player:track-ended après le drainage
Loading

Possibly related issues

  • Issue 932 décrit une lecture qui s’arrête environ une seconde avant la fin d’une piste, puis démarre immédiatement la suivante. Cette modification ajuste l’attente de fin de piste et le traitement de l’audio encore en file.
Merge Risk: ⚪ Minimal · up to 59d27

No confirmed playback issue blocks merging after normal checks.

Security architecture risk: 🔵 Low · up to 59d27

Playback now waits for buffered audio before declaring a track finished. No security regression was established, although simultaneous playback controls and device failures have not been confirmed in operation.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated exposure is playback-session timing and its existing downstream queue and listening-history effects. Earlier prefetch can open the selected successor sooner, but the inspected changes do not establish access to additional files, services or privileges.

Trust Boundaries and Controls

  • observed — The new wait returns to the existing command handler. Replacement loads still pass load-intent ordering checks before becoming pending commands. Stop, shutdown and seek remain handled before the next EOF decision; these are playback-control and ordering mechanisms, not new authentication controls.

Resilience and Maintainability Implications

  • observed — Failure containment remains command-driven rather than dependent solely on successful output draining. Replacement returns an interrupted outcome, shutdown preserves its outer-loop termination command, and producer-swap commands remain explicitly handled during playback and pause.
Pre-merge checks: Passed 5
Check name Status Explanation
Title check Passed Le titre est concis, suit le format Conventional Commits et décrit clairement la correction principale : préserver la fin de piste avant le démarrage de la suivante.
Description check Passed La description est complète et directement liée au changement. Elle explique la cause, la correction, les tests, les mesures et l’effet secondaire connu. Elle ne reprend pas les rubriques et la checkl…
Docstring Coverage Passed Docstring coverage is 94.12% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 2 files. (1 skipped: 1 …
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

La description est complète et directement liée au changement. Elle explique la cause, la correction, les tests, les mesures et l’effet secondaire connu. Elle ne reprend pas les rubriques et la checklist du modèle à l’identique, mais les informations essentielles sont présentes.

Full details: Docstring Coverage

Explanation

Docstring coverage is 94.12% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 2 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@InstaZDLL InstaZDLL self-assigned this Oct 21, 2026
@InstaZDLL
InstaZDLL merged commit 705d85a into main Oct 21, 2026
17 checks passed
@InstaZDLL
InstaZDLL deleted the fix/track-end-cut branch October 21, 2026 16:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets size: l 200-500 lines type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant