The CH badge by the scrubber is now a button opening a list of every chapter
(number, title, start time); the playing one is highlighted and scrolled into
view, a tap seeks there (and resumes if paused). Works with shared chapters and
YouTube's.
- frontend/wasm/loudness.c → loudness-wasm.js (1.5 KB, base64-embedded; built by
scripts/build-loudness-wasm.sh with clang or Zig's clang): K-weighting + 100 ms
energy steps. loudness.js does the 400 ms gating (abs -70 LUFS, rel -10 LU)
with an identical JS fallback. EBU Tech 3341 sine cases measure -23.0/-33.0
on both engines; a real track reads -21.7 vs ffmpeg ebur128 -21.8.
- The device measures from audio it already has (saved copy or the server
cache's m4a sidecar, songs up to 12 min, decoded natively at 22.05 kHz) and
reports the number to /api/loudness, so each song is measured once for
everyone; the server stores numbers only, no audio processing.
- The EQ graph gains a level stage (gain -> limiter) that eases each track
toward -14 LUFS (-12..+9 dB). Settings -> Playback -> Level volume: on by
default on Android/desktop, opt-in on iPhone (Web Audio stops sound when the
screen locks there).
- Server: chapters come from yt-dlp's `chapters` (creator chapters and
description timestamps) whenever a video is resolved, kept in a yt_chapters
table; GET /api/chapters?v= serves them (resolving once if needed, 30-day
cache) so server-cached copies get them too. Never fails the player.
- Player: a video's shared chapters win; with none, YouTube's stand in —
listed as "Chapters from YouTube", read-only (no edit/delete; + Chapter
copies them into an editable list). They drive the existing seek-bar ticks
(classic and Glass Stage), the chapter badge and the chapter list, and are
cached offline with the video's notes.
- Keys: [ previous chapter (restarts the current one when >3 s in), ] next.
- Channel pages join the select-mode views: tap Select in the filter row, pick
videos (or Select all — respects the filter), then + Queue / + Playlist.
Delete is hidden there (nothing to delete on a channel).
- --bottom-chrome (the fixed bottom nav + mini player height) is now measured
whenever the mini player shows/hides and on resize/rotation, not only while
the section rail scrolls, and the list pane pads its end by it, so the last
card scrolls fully clear with or without the mini player.
Shown as soon as an update is offered, so Later, a backdrop tap or a failed
download all leave a one-tap way to update; it targets the newest build and
disappears while updating. The search form and box now shrink so the icon
never pushes Search off a phone screen.
- A single dock button cycles the service view; video is skipped in audio-only mode
- Visualizer reacts to the audio where the EQ graph is available and falls back
to an ambient pulse on iOS so background playback is never routed through Web Audio
- Album art shows the song artwork with the letterbox cropped
- A-/A+ box steps lyrics, title and channel size from -2 to +2 in every theme
- Glass Stage dock is slightly more compact
- Playlist title wraps within 90% of the width; rename and share are icons beside it,
and tapping the title opens the rename box showing the full name
- Offline, pin, select and delete are icon buttons to the right of the filter input
- Transport is one row again: prev, back 10, play, forward 10, next
- Update download stages each file as it arrives so a retry only fetches what is
missing, keeps 4 requests in flight and allows 5 minutes