Skip to content

feat: add waveform seekbar with composite PK cache and singleflight (#90)#106

Open
thedavidweng wants to merge 3 commits into
feat/crossfade-playback-89from
feat/90-waveform-seekbar
Open

feat: add waveform seekbar with composite PK cache and singleflight (#90)#106
thedavidweng wants to merge 3 commits into
feat/crossfade-playback-89from
feat/90-waveform-seekbar

Conversation

@thedavidweng

@thedavidweng thedavidweng commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add cached amplitude waveform rendering behind the existing seek rail
  • Backend decodes each (song_hash, bucket_count) once across concurrent WebViews via cancellation-safe singleflight
  • Frontend uses bounded LRU (96), deterministic loading placeholder, and DPR-aware canvas
  • Preserves all existing seek behavior and accessibility attributes

Closes #90.

Test plan

  • Rust unit tests: frame/channel bucketing, short/empty audio, non-finite input, exact output length
  • Rust cache tests: BLOB little-endian round trip, invalid-row conditions, composite key isolation
  • Migration tests: empty/initialized libraries, FK cascade deletes
  • Singleflight tests: simultaneous callers, error/panic/cancellation/poison recovery
  • Frontend tests: LRU promotion/eviction, resize bucket calculation, request-generation cancellation
  • Existing click/drag/keyboard/aria/time-label tests unchanged
  • pnpm format, pnpm lint, pnpm check:i18n, pnpm knip, pnpm build, pnpm test, pnpm test:coverage, pnpm check:patch-coverage pass
  • cargo fmt --check, cargo test -q pass

Open in Devin Review

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 78.62% (🎯 65%) 3990 / 5075
🔵 Statements 78.1% (🎯 65%) 4245 / 5435
🔵 Functions 69.4% (🎯 60%) 1082 / 1559
🔵 Branches 70.83% (🎯 60%) 2225 / 3141
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/components/Player/SeekBar.tsx 91.19% 75.8% 92.1% 96.26% 85, 98, 107, 116-117, 158-160, 173, 188, 209, 241
src/lib/waveform-cache.ts 90.62% 72.22% 100% 90.62% 70-72
src/locales/en.json 100% 100% 100% 100%
src/locales/zh-CN.json 100% 100% 100% 100%
src/stores/settings-store.ts 100% 77.77% 100% 100%
Generated in workflow #889 for commit e5ca2fb by the Vitest Coverage Report Action

@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.6%. Comparing base (cb8fa6e) to head (e5ca2fb).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/components/Player/SeekBar.tsx 94.6% 5 Missing ⚠️
src/lib/waveform-cache.ts 92.8% 3 Missing ⚠️
Additional details and impacted files
@@                     Coverage Diff                      @@
##           feat/crossfade-playback-89    #106     +/-   ##
============================================================
+ Coverage                        78.2%   78.6%   +0.3%     
============================================================
  Files                             131     132      +1     
  Lines                            6804    6940    +136     
  Branches                         2112    2142     +30     
============================================================
+ Hits                             5325    5457    +132     
- Misses                           1435    1439      +4     
  Partials                           44      44             
Flag Coverage Δ
frontend 78.6% <94.1%> (+0.3%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
frontend 78.6% <94.1%> (+0.3%) ⬆️
rust ∅ <ø> (∅)
Files with missing lines Coverage Δ
src/locales/en.json 100.0% <ø> (ø)
src/locales/zh-CN.json 100.0% <ø> (ø)
src/stores/settings-store.ts 100.0% <ø> (ø)
src/lib/waveform-cache.ts 92.8% <92.8%> (ø)
src/components/Player/SeekBar.tsx 96.5% <94.6%> (+4.3%) ⬆️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

devin-ai-integration[bot]

This comment was marked as resolved.

@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a waveform-backed seekbar with cached backend waveform generation. The main changes are:

  • New get_waveform IPC command for local songs, with remote songs returning empty peaks.
  • Composite-key SQLite waveforms cache keyed by song_hash and bucket count.
  • Cancellation-safe backend singleflight for shared waveform computation across concurrent callers.
  • Deterministic peak bucketing, cache validation, and sanitized waveform errors.
  • DPR-aware SeekBar canvas rendering with bounded frontend LRU caching and stale-response guards.
  • Updated IPC contracts, generated schema docs, changelog, mocks, and test coverage.

Confidence Score: 5/5

This PR appears safe to merge.

No blocking correctness or security issues were identified. The waveform path clamps and validates bucket counts, sanitizes cache contents and errors, avoids holding playback locks during computation, and uses frontend generation guards for stale async responses.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • Ran the waveform seekbar Vitest suite and confirmed all 181 tests passed across the four target files, with React warnings limited to non-failing act messages.
  • Reviewed the UI proof artifacts to verify the app player area is visible after playback and during seekbar interaction.
  • Inspected the runtime artifacts to verify the Vite/Playwright capture setup and successful app startup.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
src-tauri/migrations/011_waveforms.sql Adds a composite-key waveform cache table with bucket bounds and song cascade cleanup.
src-tauri/src/audio/waveform.rs Implements deterministic peak bucketing with validation and edge-case tests.
src-tauri/src/cache/waveforms.rs Adds validated little-endian BLOB encoding/decoding and composite-key waveform cache operations.
src-tauri/src/commands/playback.rs Adds get_waveform IPC with bucket clamping, remote short-circuiting, and shared singleflight completion.
src-tauri/src/services/waveform.rs Adds the blocking waveform pipeline with cache lookup, local decode, computation, and best-effort persistence.
src-tauri/src/state/playback.rs Adds cancellation-safe process-wide waveform singleflight state and tests.
src/components/Player/SeekBar.tsx Adds waveform fetching, LRU use, DPR-aware canvas rendering, and generation guards while preserving seek behavior.
src/lib/tauri/playback.ts Adds a typed getWaveform IPC wrapper that packages backend peak arrays as waveform data.
src/lib/waveform-cache.ts Implements bucket calculation and a 96-entry waveform LRU keyed by song hash and bucket count.
src/types/ipc.ts Adds the frontend WaveformData IPC shape and related playback type updates.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant SeekBar
participant LRU as Waveform LRU
participant IPC as get_waveform IPC
participant SF as WaveformSingleflight
participant Worker as Blocking waveform task
participant DB as SQLite waveforms cache
participant Audio as Audio decode/peaks

SeekBar->>SeekBar: "derive buckets from rail width * DPR"
SeekBar->>LRU: get(songHash, buckets)
alt cache hit
    LRU-->>SeekBar: peaks
else cache miss
    SeekBar->>IPC: get_waveform(hash, buckets)
    IPC->>DB: lookup song and skip remote sources
    IPC->>SF: register(songHash, buckets)
    alt first waiter
        SF->>Worker: spawn shared computation
        Worker->>DB: read cached waveform
        alt DB cache miss
            Worker->>Audio: decode song and compute peaks
            Worker->>DB: save peaks blob
        end
        Worker-->>SF: fan out result to waiters
    else existing computation
        SF-->>IPC: await shared waiter
    end
    IPC-->>SeekBar: peaks[]
    SeekBar->>LRU: store non-empty peaks
end
SeekBar->>SeekBar: draw DPR-aware canvas behind seek progress
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant SeekBar
participant LRU as Waveform LRU
participant IPC as get_waveform IPC
participant SF as WaveformSingleflight
participant Worker as Blocking waveform task
participant DB as SQLite waveforms cache
participant Audio as Audio decode/peaks

SeekBar->>SeekBar: "derive buckets from rail width * DPR"
SeekBar->>LRU: get(songHash, buckets)
alt cache hit
    LRU-->>SeekBar: peaks
else cache miss
    SeekBar->>IPC: get_waveform(hash, buckets)
    IPC->>DB: lookup song and skip remote sources
    IPC->>SF: register(songHash, buckets)
    alt first waiter
        SF->>Worker: spawn shared computation
        Worker->>DB: read cached waveform
        alt DB cache miss
            Worker->>Audio: decode song and compute peaks
            Worker->>DB: save peaks blob
        end
        Worker-->>SF: fan out result to waiters
    else existing computation
        SF-->>IPC: await shared waiter
    end
    IPC-->>SeekBar: peaks[]
    SeekBar->>LRU: store non-empty peaks
end
SeekBar->>SeekBar: draw DPR-aware canvas behind seek progress
Loading

Reviews (2): Last reviewed commit: "style: remove extra blank line in tauri-..." | Re-trigger Greptile

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Owner Author

Maintainer acceptance — merge slot 10, strictly after #104

BLOCKED — changes required.

Missing #90 frontend contract

SeekBar hard-codes DEFAULT_WAVEFORM_BUCKETS = 200 and fetches only on song change. Resize merely stretches/redraws the same data. There is no client-side bounded 96-entry LRU or src/lib/waveform-cache.ts, despite #90 requiring width-derived bucket calculation, bucket-sensitive keys, request-generation cancellation, and a bounded LRU. The backend's composite (song_hash, bucket_count) cache is therefore never exercised as intended by the UI.

Implement the required client cache and width/DPR bucket selection, refetch safely on relevant resize, and add the specified stale-generation/LRU/bucket tests.

Branch/scope blocker

This PR targets old main but its diff contains the predecessor EQ, peak, next-track, and crossfade stack. #90 is explicitly after #89. Rebase/retarget it onto the final #104 result so one issue/one PR remains true, then rerun CI. GitHub currently reports this head as not mergeable.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Owner Author

Re-review: waveform implementation and stack cleanup required

The current source at this head still has a fixed DEFAULT_WAVEFORM_BUCKETS = 200 path in SeekBar, fetches only on song identity, and does not contain the required bounded frontend cache/request-generation protection. The PR description is not sufficient; implement the Issue #90 contract in source.

Do the following:

  1. Replace the fixed-bucket request with a deterministic bucket count derived from the rendered CSS rail width and device-pixel ratio. Quantize and bound it according to the issue contract so minor resize noise does not thrash the backend.
  2. Key every frontend cache/request by both the song hash and bucket count. A resize or DPR change must request the appropriate waveform instead of merely redrawing/stretches old data.
  3. Implement a bounded LRU with a hard capacity of 96 entries. Promote on read, evict least-recently-used entries deterministically, and do not retain unbounded waveform arrays.
  4. Add request-generation/cancellation protection. When song, width, DPR, or bucket count changes, a stale async response must never overwrite a newer waveform or draw after unmount.
  5. Keep existing seeking, keyboard, ARIA, time labels, and minimum rail behavior unchanged. Do not make waveform decoding block the audio callback/UI thread.
  6. Ensure backend caching uses the exact (song_hash, bucket_count) identity and remains safe for concurrent callers; failures must not poison a later request.

Add tests for width/DPR-driven bucket selection, resize refresh, 96-entry LRU promotion/eviction, stale deferred-response rejection, unmount cleanup, and unchanged seek interactions. Then, after #104 is final, rebase onto the merged main so this PR no longer carries predecessor features. Run the full frontend/Rust verification and report the rebased SHA and test list.

@thedavidweng
thedavidweng force-pushed the feat/90-waveform-seekbar branch from d148599 to b0f7c76 Compare July 15, 2026 08:56
@thedavidweng
thedavidweng changed the base branch from main to feat/crossfade-playback-89 July 15, 2026 09:43
@thedavidweng
thedavidweng force-pushed the feat/90-waveform-seekbar branch from b0f7c76 to ff4aeff Compare July 15, 2026 09:47
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from 1f6007d to d1d5abf Compare July 15, 2026 20:00
@thedavidweng
thedavidweng force-pushed the feat/90-waveform-seekbar branch from 9b05bd0 to 51a261d Compare July 15, 2026 20:00
@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from d1d5abf to cf0947a Compare July 15, 2026 20:43
@thedavidweng
thedavidweng force-pushed the feat/90-waveform-seekbar branch 2 times, most recently from 899f3f8 to 685aed2 Compare July 15, 2026 21:01
@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from dbe353c to 049d783 Compare July 15, 2026 21:18
@thedavidweng
thedavidweng force-pushed the feat/90-waveform-seekbar branch from 685aed2 to 414cbe1 Compare July 15, 2026 21:19

Copy link
Copy Markdown
Owner Author

Re-review — still blocked: waveform request identity is not DPR-aware

Reviewed at 414cbe1. The LRU and generation cleanup are present, but bucketsForRailWidth accepts only CSS width and SeekBar never includes window.devicePixelRatio in its request identity. A ResizeObserver also does not reliably fire when a window moves between displays with the same CSS width but a different DPR.

Implement the issue contract exactly:

  1. Change bucket calculation to take both CSS width and normalized DPR, using the documented clamp/quantization on physical sampling width (for example round(cssWidth * dpr / 3), clamped to 24…1000).
  2. Store/observe DPR in SeekBar; update it on window resize and a resolution media-query change, re-registering the query when DPR changes. Keep ResizeObserver for CSS geometry.
  3. Include the resulting bucket count in the existing (songHash, bucketCount) request/LRU key, so a DPR-only change refetches instead of stretching the old waveform.
  4. Add deterministic tests for 1× vs 2× DPR at the same rail width, DPR-only display migration, quantized no-op resize, stale response rejection, and the existing 96-entry LRU behavior.

The current comment explicitly describes width as “DPR-independent CSS”; that contradicts the #90 acceptance contract.

@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from 049d783 to a96c2f7 Compare July 16, 2026 03:50
@thedavidweng
thedavidweng force-pushed the feat/90-waveform-seekbar branch from 414cbe1 to ca2befd Compare July 16, 2026 03:50
@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from a96c2f7 to 5a85bb7 Compare July 16, 2026 04:00
@thedavidweng
thedavidweng force-pushed the feat/90-waveform-seekbar branch from ca2befd to 56a5c96 Compare July 16, 2026 04:01
devin-ai-integration[bot]

This comment was marked as resolved.

@thedavidweng
thedavidweng force-pushed the feat/90-waveform-seekbar branch from 56a5c96 to ff9db59 Compare July 16, 2026 04:17
devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Owner Author

Re-review — blocked: a quantized DPR-only migration leaves the canvas at the old pixel scale

Reviewed at ff9db59. dpr state changes on resize / media-query events, but the canvas draw effect depends only on waveformVersion. When a DPR change preserves effectiveBuckets (the intended “quantized no-op” cache case), neither the fetch effect nor waveformVersion changes. The canvas therefore keeps its old backing-store dimensions and transform.

For example, at 299 CSS px a 2x → 2.005x migration retains bucket 199 and correctly does not refetch, but it also never repaints at the new DPR.

Please make DPR itself invalidate the canvas draw (for example, depend on dpr in the draw effect and use that state value for physical dimensions / setTransform, or explicitly bump the draw version when DPR changes). Preserve the no-refetch behavior when the bucket count is unchanged.

Extend the existing quantized-no-op test to assert both:

  1. getWaveform is not called again; and
  2. the existing waveform canvas is reconfigured/redrawn at the new DPR without a ResizeObserver notification.

Copy link
Copy Markdown
Owner Author

Re-review — DPR fix and regression test verified; held only by stack integration

Reviewed at f77c995. The draw effect now depends on tracked dpr, and the new test correctly proves a same-bucket DPR migration redraws the physical canvas without refetching waveform data. This resolves the implementation blocker.

GitHub currently reports the PR unmergeable and it is two commits behind its #104 base; there is no fresh CI/Packaging result for this head. After #104 has its remaining test update and is rebased/accepted, rebase this PR onto that exact updated head, preserve the DPR regression, and wait for fresh CI + Packaging before requesting LGTM.

@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from 4392601 to 3f1091f Compare July 16, 2026 18:38
@thedavidweng
thedavidweng force-pushed the feat/90-waveform-seekbar branch from f77c995 to 5e0369a Compare July 16, 2026 18:51
devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Owner Author

Re-review — DPR fix remains correct, but this head is not CI-clean

Reviewed at 5e0369a. The tracked-dpr draw dependency and same-bucket DPR regression are still correct.

CI run 29525679883 fails before the test suite because oxfmt --check reports one unformatted file:

src/lib/tauri/tauri-wrappers.test.ts

Run:

pnpm format:write src/lib/tauri/tauri-wrappers.test.ts
pnpm format

Commit only the formatter output and rerun CI. The missing coverage/Codecov messages are downstream of the format step skipping tests; they are not a separate product failure.

No LGTM until a fresh CI run is green.

devin-ai-integration[bot]

This comment was marked as resolved.

@thedavidweng

Copy link
Copy Markdown
Owner Author

@greptile review

Copy link
Copy Markdown
Owner Author

Stack status update: the code-format blocker on head b334c09855a782717dc5a1bfacadec1e33105aee is resolved and that head's CI is green, but the base #104 has since advanced by four commits to 7bb62b7857b0a245ebd1af39173d608c82fca19d. The current comparison is four commits behind, so the existing run does not validate the final combined graph.

After #104 is conflict-free, green, and accepted, update/retarget #106 onto the accepted #104 head and run fresh full CI. Final LGTM remains gated on manual waveform rendering/seek interaction/DPR acceptance.

@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from 7bb62b7 to 53ae363 Compare July 17, 2026 00:20
@thedavidweng
thedavidweng force-pushed the feat/90-waveform-seekbar branch from b334c09 to 71329dc Compare July 17, 2026 00:20
@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from 79c8d05 to 067862e Compare July 17, 2026 04:06
@thedavidweng
thedavidweng force-pushed the feat/90-waveform-seekbar branch from f5de12e to 1711c22 Compare July 17, 2026 04:08
@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from 067862e to bc756ec Compare July 17, 2026 07:21
devin-ai-integration[bot]

This comment was marked as resolved.

@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from bc756ec to db309ce Compare July 17, 2026 08:10
devin-ai-integration[bot]

This comment was marked as resolved.

@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from a15384a to 5046ec5 Compare July 17, 2026 10:14
@thedavidweng
thedavidweng force-pushed the feat/90-waveform-seekbar branch from 5ddbd3c to d691ca1 Compare July 17, 2026 10:15
@thedavidweng
thedavidweng force-pushed the feat/crossfade-playback-89 branch from 5046ec5 to ce496d5 Compare July 17, 2026 11:04
@thedavidweng
thedavidweng force-pushed the feat/90-waveform-seekbar branch from d691ca1 to e828019 Compare July 17, 2026 11:04
)

Rebased onto feat/crossfade-playback-89 head. Includes all waveform-specific
fixes: LRU cache, width buckets, request generation, DPR-aware canvas,
RAII completion guard, zero-channel panic guard, and transition snapshot
capture at switch time.
@thedavidweng
thedavidweng force-pushed the feat/90-waveform-seekbar branch from c0f1ce4 to 81464a3 Compare July 17, 2026 12:15
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.

feat: add waveform seekbar with composite PK cache and singleflight

1 participant