Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3f9f1a9
refactor: streamline backup handling and implement fitBackup logic
patrickiel Sep 6, 2026
fd2a845
Remove server sync backend and related configurations
patrickiel Sep 6, 2026
4ee5c7c
Refactor sync storage and backup handling
patrickiel Sep 6, 2026
f964024
feat: enhance EQ preset management with updated timestamps and deleti…
patrickiel Sep 6, 2026
dfbf5a9
refactor: remove clearDeletion function and its usages
patrickiel Sep 6, 2026
d048410
refactor: update timestamp handling to use milliseconds instead of se…
patrickiel Sep 6, 2026
e463ee2
feat: implement backup fixture and enhance backup handling with impro…
patrickiel Sep 6, 2026
dba2c20
feat: enhance backup and sync handling with improved deletion managem…
patrickiel Sep 6, 2026
e5d4e3a
feat: enhance sync handling with improved visibility and status manag…
patrickiel Sep 6, 2026
e29f5bb
feat: enhance backup handling with replacement imports and parameter …
patrickiel Sep 7, 2026
05c4235
refactor: streamline merge logic and enhance data handling in sync pr…
patrickiel Sep 7, 2026
ffd4bdf
refactor: unify track identity handling by using songKey for key deri…
patrickiel Sep 7, 2026
cd77e30
refactor: improve backup handling by enforcing key re-derivation and …
patrickiel Sep 7, 2026
deaa88c
Refactor sync configuration and library persistence
patrickiel Sep 7, 2026
81f934c
refactor: remove sync blob and area implementations
patrickiel Sep 7, 2026
b13171d
refactor: enhance URL normalization and improve library persistence l…
patrickiel Sep 7, 2026
7ccc6ee
refactor: update documentation and improve sync handling in various c…
patrickiel Sep 7, 2026
98cc432
refactor: update sync handling and improve data structure for recent …
patrickiel Sep 7, 2026
96734d3
refactor: introduce song and deletion limits for sync management and …
patrickiel Sep 7, 2026
9bc0d6b
Refactor library persistence and synchronization logic
patrickiel Sep 7, 2026
47090b9
refactor: enhance library synchronization and pruning logic, improve …
patrickiel Sep 7, 2026
8b07d01
feat: implement library state management and sync improvements
patrickiel Sep 7, 2026
c985cf2
Implement library recovery and backup features
patrickiel Sep 8, 2026
7909ec3
feat: enhance library synchronization and session management
patrickiel Sep 8, 2026
441b233
feat: implement library snapshot fitting and optimize upload handling
patrickiel Sep 8, 2026
fc62e5c
feat: save final chord chart on engine loss and update chords handlin…
patrickiel Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 0 additions & 24 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,30 +47,6 @@ jobs:
- name: Build (Firefox)
run: pnpm build:firefox

server:
name: sync server
runs-on: ubuntu-latest
# Separate pnpm workspace with its own lockfile and its own tsconfig
# (Cloudflare Workers types), excluded from the root tsconfig — so `pnpm
# check` above does not cover it.
steps:
- uses: actions/checkout@v4

# No `version:` input — it reads `packageManager` from the root
# package.json, and passing both is an error.
- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 22

- run: pnpm install --frozen-lockfile
working-directory: server

- name: Type check
run: pnpm run check
working-directory: server

# The e2e suite (e2e/run.mjs) is deliberately not run here: it needs a real
# Chrome download plus generated WAV fixtures, and it is not currently
# all-green — see CONTRIBUTING.md, which tracks the count. Run it locally for
Expand Down
8 changes: 3 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ stats-*.json
.wxt
web-ext.config.ts

# Cloudflare Workers
.wrangler
.dev.vars
.dev.vars.*

# Editor directories and files
.vscode/*
!.vscode/extensions.json
Expand All @@ -45,3 +40,6 @@ public/worklets/pcm-tap-worklet.js
# Generated at postinstall / build:before. See scripts/build-vocal-worklet.mjs.
public/worklets/vocal-reducer-worklet.js
export/

# pnpm store created when a local store path is set
.pnpm-store/
64 changes: 52 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pnpm dev:firefox # same, Firefox
pnpm check # svelte-check / TypeScript — the only type/lint gate
pnpm build # production build → .output/chrome-mv3
pnpm zip # store package
pnpm test:dsp # fast DSP unit tests (node --test on src/features/**/*.test.ts)
pnpm test:dsp # fast unit tests: DSP, chords, library and sync records (node --test on src/**/*.test.ts)
pnpm release:dry # show the release plan (version bump, tag) without changing anything
pnpm release # full release: check + test, bump patch, build both zips, commit, tag, push
```
Expand All @@ -35,25 +35,23 @@ pnpm dlx @puppeteer/browsers install chrome@stable --path ./.browsers # once
node e2e/make-tone.mjs ; node e2e/make-stereo-mix.mjs # once, generates WAV fixtures
pnpm wxt build --mode testing # `testing` mode grants <all_urls> host perms so no native prompts block the run
node e2e/run.mjs # add --headful to watch
node e2e/library.mjs # background library + sync integration (pnpm test:e2e:library)
```
The harness plays a 440 Hz tone and asserts on the **processed output** (e.g. 880 Hz after +12 st) via `window.__noteByNoteDebug` in the content script and `window.__panelDebug` in the side panel.

### Sync server (`server/`, separate pnpm workspace)
`cd server ; pnpm install` — it has its own lockfile, `tsconfig.json` (Cloudflare Workers types), and is `exclude`d from the root tsconfig. See [server/README.md](server/README.md) for deploy. `pnpm run dev` there serves `http://localhost:8787`, which the extension's dev build targets automatically.

## Architecture

This is a **multi-context extension**. The single most important structural fact: **the audio engine lives in the page (content script), not in the side panel.** The side panel is a thin UI mirror that connects to the engine over a typed `chrome.runtime` Port. This is why practice flows (loops, sequences, playback) survive the side panel closing.

### Source layout (vertical feature slices)
The tree is organized by **feature**, not by layer:
- **`src/core/`** — shared platform: `engine/` (controller, media-engine/-detect, attach-audio), `audio/` (pipeline, fft, silence-detector), `messaging/` (protocol shell, ports, rpc), `model/` (shared types + defaults + format + track-identity + thumbnail), `persist/` (storage, backup, track-data descriptor registry), `state/` (session, track-sync, connect, view), and `features.ts` (the panel-feature registry).
- **`src/features/<feature>/`** — one folder per product feature (chords, pitch, speed, vocal-reducer, eq, loops, markers, snippets, count-in, library, sync, settings, shortcuts), each with an `engine/` subfolder (content-script code: worklets, schedulers, DSP factories) and/or a `panel/` subfolder (side-panel stores + components), plus optional `protocol.ts` (its wire-message fragment), `panel/panel.ts` (registration object), and `persist.svelte.ts` (per-track descriptor). **`engine/` and `panel/` never cross-import**, so the content and panel bundles stay separate.
- **`src/core/`** — shared platform: `engine/` (controller, media-engine/-detect, attach-audio), `audio/` (pipeline, fft, silence-detector), `messaging/` (protocol shell, ports, rpc), `model/` (shared types + defaults + format + track-identity + thumbnail), `persist/` (library, backup, migration), and `state/` (session, library, track-sync, connect, view).
- **`src/features/<feature>/`** — one folder per product feature (chords, pitch, speed, vocal-reducer, eq, loops, markers, snippets, count-in, library, sync, settings, shortcuts), each with an `engine/` subfolder (content-script code: worklets, schedulers, DSP factories) and/or a `panel/` subfolder (side-panel stores + components), plus optional `protocol.ts` (its wire-message fragment). **`engine/` and `panel/` never cross-import**, so the content and panel bundles stay separate.
- **`src/ui/`** — shared/presentational UI (Workspace, Panel, PanelStack, Timeline, chrome bars, `shared/` primitives, icons, dismiss).
- **`src/dev/`** — preview-only helpers (`browser-shim`, `mock`).
- **`src/entrypoints/`** — thin WXT composition roots (unchanged location).

**Dependency direction:** `entrypoints → core composition roots (pipeline, controller, protocol, App, features.ts, track-sync) → features → core primitives (model, messaging, audio/fft, ui)`. Composition roots **import feature contributions** (the "light registration"); **features never import the orchestrators**. Domain types stay central in `core/model/types.ts` (they are the shared engine↔panel wire + persistence contract).
**Dependency direction:** `entrypoints → core composition roots (pipeline, controller, protocol, App, track-sync) → features → core primitives (model, messaging, audio/fft, ui)`. Composition roots wire feature behavior directly; features never import the orchestrators. Domain types stay central in `core/model/types.ts` (the shared engine↔panel contract).

### Execution contexts (`src/entrypoints/`)
- **`sidepanel/`** — the Svelte UI. Holds no engine state of its own; mirrors the active tab's engine.
Expand Down Expand Up @@ -93,17 +91,59 @@ Both worklet processors are shipped as **static files under `public/worklets/`**

### State layer (Svelte 5 runes stores, `*.svelte.ts`)
Runes stores (classes with `$state`), one singleton exported per file. All panel-side. Split by ownership:
- **Core (`src/core/state/`):** `session` — mirror of the active tab's engine + the command surface panels call (while no engine is attached, commands fall back to **optimistic local state**, staged and pushed on connect); `connection` (`connect.svelte.ts`) — owns the port lifecycle (one `<all_urls>` prompt from the banner's Connect button in a user gesture, injection, reconnect, capture start/stop; a `#generation` counter drops stale async work) and iterates the **panel-feature registry** ([core/features.ts](src/core/features.ts)) to route engine events into feature stores; `track-sync` — reacts to track changes (auto-save to Recent, reset/remember/carry-over params) and iterates the **per-track descriptor registry** ([core/persist/track-data.ts](src/core/persist/track-data.ts)) to swap each feature's slice in/out of storage; `view`.
- **Core (`src/core/state/`):** `session` mirrors the active tab's engine and provides panel commands; `library` holds the panel's single saved-data snapshot; `connection` owns permissions, injection, port lifecycle and capture, routing chord events directly; `track-sync` loads saved practice data and wires feature edits; `view` selects the open panel.
- **Feature-owned (`src/features/<f>/panel/`):** `markers`, `snippets`, `chords`, `settings`, `favorites`/`history` (library), `eq-presets`, `shortcuts`. Preview data (`mock`) lives in `src/dev/`.
- Features contribute boot init + event routing via `panel/panel.ts` (registered in `core/features.ts`) and per-track persistence via `persist.svelte.ts` (registered in `core/persist/track-data.ts`).
- App loads the library once. Settings, UI preferences, presets and song lists derive from it; App effects apply the theme and send engine settings. Features submit per-track edits through track-sync. UI preferences are device-local: the panel shows an edit at once and drops the overlay when the storage watch confirms it, so a toggle never waits on the worker.

### Persistence & sync
- [storage.ts](src/core/persist/storage.ts) — WXT `storage.defineItem` wrappers (the full storage schema stays central here). **Per-track data is keyed by a normalized track identity** ([track-identity.ts](src/core/model/track-identity.ts)): site-aware URL normalization (strips `t`/`si`/`utm_*` etc.; collapses YouTube to `watch?v=`) + rounded duration, hashed to `local:track:<key>`. The per-track `TrackData` record is assembled/scattered by feature descriptors ([core/persist/track-data.ts](src/core/persist/track-data.ts)). EQ presets and granted origins live in their own items so "Reset Settings" can't wipe them.
- Optional **cross-device sync** (`src/features/sync/` + `server/`): last-write-wins backup snapshots to a Cloudflare Worker + KV. The secret sync ID **is the whole capability** (open CORS, no other auth). The Worker URLs live once in [sync-hosts.ts](src/features/sync/sync-hosts.ts) (env-free, so `wxt.config.ts` imports it for the manifest); [endpoint.ts](src/features/sync/endpoint.ts) picks localhost in dev, the deployed Worker in prod. The ID rides `storage.sync` between devices and is additionally kept as a **cookie on the sync host** so it survives an uninstall ([id-cookie.ts](src/features/sync/panel/id-cookie.ts) is the canonical explanation). That needs the `cookies` permission plus host access to the sync origin, which is an **optional** host permission requested from the Sync settings / on enable / on connect (a required one would disable the extension on update in Chrome and is opt-in on Firefox); `sync.durable` mirrors whether it is held. The background worker filters the sync host out of the site-grant machinery (`siteOrigins` in [background.ts](src/entrypoints/background.ts)) so it is neither registered for the engine nor removed by Revoke Permissions.

- One local library owns shared songs/settings/presets/order and device-local Recent,
UI preferences, last-used parameters and analysis. See core/persist/library.ts.
- The background service is the only writer (library-background.ts). Panels use
library-client.ts commands and one storage watch. Commands patch the latest saved
data; Recent and Favorites are projections, not persistent song copies. A saved
song outlives Recent (its practice returns when the page is replayed, which is
what Auto Save off relies on); clearing history is what removes every
non-favorited song, listed or not.
- Track-sync loads a practice session once and submits edits to the saved library.
Restoration uses the initialized panel mirror and does not emit user edits.
Parameter, marker and snippet edits capture their track and values before being
coalesced; pending patches remain readable until the library watch acknowledges
their committed revision. Switching tracks or hiding the panel flushes the batch.
Receiving remote changes never reloads or silently replaces the active session.
Explicit imports reload active sessions and advance a device-local import
revision; the writer rejects practice edits carrying an older revision.
Feature persistence is wired directly in track-sync; there is no descriptor registry.
- The writer validates the resulting library before every command commit. Connection
failures use connection state; only library initialization can open recovery.
- Sync copies the same SharedLibrary snapshot used locally (records.ts). One
updatedAt timestamp chooses the whole winner; equal dates adopt the remote copy.
There are no field merges or deletion markers. Song dates only support display
and sorting. Gzip data spans fixed size-limited slots; a hash prevents partial or
mixed snapshots from being applied. An over-budget upload drops the least
recently used non-favorited songs (fitSnapshot bisects for the smallest cut) and
saves that re-dated trimmed copy locally, so the library and the upload stay one
snapshot; nothing is trimmed while sync is off, and an overflow of favorites
alone still fails, preserving local data and the last successful upload.
Background alarms retry independently of panels.
Identical snapshots do not rewrite storage or toggle sync status. Missing
headers are recovered from complete gzip chunks; partial headerless uploads
get a persisted grace period before repair from the complete local copy.
- Backups use the readable v2 library schema. legacy-backup.ts reads the
released v1 format, and
library-migration.ts collapses its old copies once.
Only released formats need compatibility adapters; intermediate PR formats do not.
Old local storage is retained for recovery, but only local:library is used after
migration.
Automatic legacy migration salvages fields independently (library-recovery.ts).
Invalid current libraries remain untouched and open a recovery screen; a valid
recovery import retains the damaged value under local:libraryRecovery.
- Web identity uses provider ID/normalized URL. Title and duration are metadata.
Local files retain a filename discriminator independent of the extension URL.

## Conventions & gotchas
- Path alias `@/` → `src/` (so `@/core/*`, `@/features/*`, `@/ui/*`, `@/dev/*` all resolve). WXT provides the `#imports` virtual module (`storage`, `defineBackground`, `defineContentScript`, the `browser` global) — no explicit import of `browser`.
- **`@/` does not work in two contexts** (they don't share the WXT/Vite resolver): the `node --test` DSP files (`src/features/**/*.test.ts` and the modules they import as *values* — `fft.ts`, `center-cut-dsp.ts`, `detect-bpm.ts`) must use **relative imports with explicit `.ts` extensions**; the esbuild worklet bundles (`src/features/*/engine/*.worklet.ts`) must use **relative imports**. (`import type` is erased, so type-only imports may omit the extension.)
- **`@/` does not work in two contexts** (they don't share the WXT/Vite resolver): the `node --test` files (`src/**/*.test.ts` and the modules they import as *values* — `fft.ts`, `center-cut-dsp.ts`, `detect-bpm.ts`, `backup-codec.ts`, `sync/persist/records.ts`, `core/persist/{library,library-migration,legacy-backup,rekey}.ts`, and what those pull in: `defaults.ts`, `thumbnail.ts`, `track-identity.ts`) must use **relative imports with explicit `.ts` extensions**; the esbuild worklet bundles (`src/features/*/engine/*.worklet.ts`) must use **relative imports**. (`import type` is erased, so type-only imports may omit the extension.)
- **Both browsers build MV3** (`manifestVersion: 3` is pinned in [wxt.config.ts](wxt.config.ts) — Firefox would otherwise default to MV2 and drop `optional_host_permissions`). Chromium-only APIs are gated on the build-time flags in [core/platform.ts](src/core/platform.ts) (`CAN_CAPTURE_TAB`, `HAS_SIDE_PANEL_API`), never on runtime `browser.*` probes: Firefox has no `tabCapture`/`offscreen` (so no capture fallback — the offscreen entrypoint is excluded from that build) and no `sidePanel` (the same page is registered as `sidebar_action`). Panel-side the capability travels as a **prop**: an absent `oncapture`/`ontabaudio` is what makes the shared UI drop the affordance.
- Debug globals are named `__noteByNote*` / `__panelDebug`. The processor name literal is `note-by-note-center-cut` and **must match on both sides** (`vocal-reducer.worklet.ts` registers it, `vocal-reducer.ts` constructs it) — mismatches throw `InvalidStateError` at runtime and `tsc` won't catch them.
- A `MediaElementSource` can be created only once per element per document lifetime, so **extension reloads require a page reload** to reattach.
Expand Down
15 changes: 8 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ files.

```powershell
pnpm check # svelte-check / TypeScript — the only type or lint gate
pnpm test:dsp # node --test, the DSP unit tests
pnpm test:dsp # node --test, the unit tests (DSP, chords, library, backup migration, sync records)
pnpm build # production build → .output/chrome-mv3
```

CI runs all three on every pull request, plus the Firefox build and `server/`'s
own `pnpm run check`. Run them locally first anyway; the
turnaround is much faster than waiting on a runner. (A first-time contributor's
CI runs all three on every pull request, plus the Firefox build. Run them
locally first anyway; the turnaround is much faster than waiting on a runner.
(A first-time contributor's
workflow run needs a maintainer to click approve, so it may sit for a bit.)
There is no ESLint or Prettier config, deliberately — match the style of the
code around you.
Expand All @@ -54,10 +54,11 @@ For the browser-level e2e suite (it plays a 440 Hz tone and asserts on the
- **A media element can host exactly one `MediaElementSourceNode`** for the
lifetime of the document. Reloading the extension therefore means reloading
the page too.
- **The e2e suite is not currently all-green** — 22 of 30. The audio path passes
end to end; the failures are in marker chips, loop and sequence bounds, the
- **The e2e suite is not currently all-green.** The audio path passes end to
end; the failures are in marker chips, loop and sequence bounds, the
tab-capture CTA, and the vocal-reducer control. They pre-date any change you
are about to make; compare against a clean checkout before assuming otherwise.
are about to make; compare the tally against a clean checkout before assuming
otherwise.

## Architecture in one paragraph

Expand Down
Loading
Loading