Add gapless and crossfade playlist transitions with a mode toggle, and a waveform seek bar for audio-only playback

This commit is contained in:
Jonathan Sykes
2026-09-19 16:12:33 +08:00
parent 71fc51fb50
commit ca07e1fdf1
5 changed files with 485 additions and 4 deletions

View File

@@ -115,6 +115,28 @@ JSON shapes mirror the Tauri Rust bridge exactly — don't change one side alone
forever and Chrome killed the tab ("Target crashed" in Playwright, no JS
error). Never cap with a loop whose exit depends on a deferred removal.
## Playlist transitions + waveform (`Transition` / `Wave` in app.js)
- `data.settings.transition` = `off` | `gapless` | `crossfade` (`crossfadeSec`,
default 6); the ↦/⇥/⤨ button cycles it with a 6-s toast. Applies only to
playlist/queue playback with a next song, not with loop-one or an A-B loop.
- A hidden **bridge `<audio>`** preloads the next song's audio ~25 s early
(offline copy, else `/api/streams`). Crossfade starts it `crossfadeSec` early
and ramps both volumes; gapless starts it when the song ends. Then the queue
advances normally with `Player._handoff` set: `_startPlayback()` seeks the
new track to the bridge's position, plays, and releases the bridge on
`playing` (failsafe 5 s / 20 s). afterLoad skips resume/A-marker seeks
during a handoff; `onTrackEnded` ignores `ended` while one runs.
- iOS ignores `element.volume`, so crossfade degrades to gapless there.
- Waveform: `GET /api/media/:id/peaks` = 400 RMS buckets (0..100, ^0.7 so
quiet verses stay visible) from ffmpeg over the server-cached copy, memoised
per file path (gen). Audio-only mode draws it on `#waveCanvas` BEHIND the
range input (made transparent, still the control), + hover time tooltip and
"⚡ Jump to the loudest part" (loudest ~5 s window). Not cached on the server
→ plain seek bar. Envelopes are kept in localStorage (`ytpPeaks`, 80 songs).
- Tested with real audio: a local server whose media_cache holds 20-s clips
(upsertMedia rows + `<id>.1.m4a/.mp4` files) — Playwright's Chromium plays
AAC; launch with `--autoplay-policy=no-user-gesture-required`.
## Phone remote (`server/remote.js`, `Remote` in app.js)
- Desktop tab playing to a TV = **host**; phone = **remote**. Both are browser
tabs of this app, so the server relays over `/ws/remote` (Bun `server.upgrade`