From d9081090fd2c67437bd450843f5662caaf9668e9 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Thu, 23 Jul 2026 12:52:06 -0400 Subject: [PATCH] =?UTF-8?q?docs(multi-bank):=20frame=20Phase=20B=20?= =?UTF-8?q?=E2=80=94=20pool=20+=20named=20banks=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the multi-bank pillar as Phase B (lettered, parallel to M-line and Phase D): CONTEXT.md §Multi-bank spec, PLAN.md Phase B block (B1 bank_book pure / B2 persist / B3 actions / B4 panel split), and docs/product/multi-bank.md framing. Additive only — BankIndex, the M0–M11 roadmap, and Phase D untouched. --- CONTEXT.md | 189 ++++++++++++++++++++++ PLAN.md | 103 ++++++++++++ docs/product/multi-bank.md | 324 +++++++++++++++++++++++++++++++++++++ 3 files changed, 616 insertions(+) create mode 100644 docs/product/multi-bank.md diff --git a/CONTEXT.md b/CONTEXT.md index bc44626..5ebb370 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -317,3 +317,192 @@ togglable per selection. parked (ignore-and-prune stale GUIDs on next toggle/open). - Interaction with the user having a screenset active (Design View drives the same flags a screenset recall would; last writer wins — confirm no surprising fight). + +--- + +# Multi-bank — additive phase spec + +> **Additive section.** This is a standalone phase parallel to — not part of — the +> M0–M11 capture roadmap and Phase D above. Nothing above changes. Product framing +> (workflow narrative, pool-privilege reasoning, movement semantics, UI-direction +> calls) lives in `docs/product/multi-bank.md`; this section is the authoritative +> technical spec, matching the house style of the capture and Design View specs. +> Same standing discipline applies: **verify every REAPER API name/signature +> against `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use.** + +## What it is + +The single per-project **bank** (`bank_model` / `BankIndex`) is generalized into a +**multi-bank system**. The existing bank becomes **the pool** — a default, +always-present bank that every capture lands in unless another bank is the active +target. On top of the pool the user creates **named banks** ("Drums", "1-Shots", +"Synth Hits") that group samples for a purpose. Samples move freely between any +banks, including to and from the pool. Exactly one bank is the **active bank** — the +capture target — the pool by default. + +**This is the container generalization of the capture pillar's bank.** The pool is +to banks what Arrange is to modes: structurally one member of an N-collection, but +privileged as the default home. The capture pillar's load-bearing rule is +untouched — capture still writes a file + an index entry and never inserts into the +arrange; the only change is *which* index the entry lands in. + +## Settled decisions + +- **The pool is privileged, not special-cased.** Structurally the pool is one + `BankIndex` among many in the container (mirror of "Arrange is just another + mode"). Semantically it is privileged: it **always exists**, is **un-deletable**, + and is **un-renamable** (fixed id + fixed display name "Pool"). New projects and + migrated single-bank projects start with the pool and **zero** named banks. This + keeps the data model uniform (no pool-shaped special type) while the rules layer + enforces the three privileges. +- **Container in the pure core; a `BankIndex` per bank.** A new pure module + `bank_book` owns an ordered registry of banks, each bank = `{ stable bank id, + display name, ordinal, BankIndex }`. **`BankIndex` is untouched** — the multi-bank + layer wraps it, it does not modify it (additive; no `bank-id` field on `Sample`). + Bank id is the stable key (GUID-style, minted on bank create); display name and + ordinal are mutable (rename / reorder). The pool is the first, seeded, fixed-id + member. `bank_book` is the mirror of `bank_model` and `view_mode_model`: pure, no + REAPER types, unit-tested outside the DAW, JSON round-trip. +- **Active bank lives in the model, routes through the capture path.** `bank_book` + carries the active bank id (defaults to the pool). The capture action family + resolves "which bank does this capture land in?" by asking the session for the + active bank's `BankIndex`, then adds exactly as today. No capture backend changes; + only the add-target is selected upstream. Activating a bank is a model mutation + + a persist write; it never touches the timeline. +- **Movement moves the index entry, not the file (default).** Moving a sample from + bank A to bank B is an **index-only** operation: remove the `Sample` from A's + `BankIndex`, add it to B's. The underlying file stays in the project bank folder — + banks are logical groupings over one shared file pool, not separate folders on + disk. This keeps movement cheap, non-destructive, and immune to path-rewrite bugs. + (Per-bank subfolders on disk are an explicit non-goal — see guardrails.) +- **Dedup-by-hash is per-bank.** Each `BankIndex` dedups within itself, unchanged. + Moving a sample whose hash already exists in the destination bank **collapses** + onto the existing entry there (the move is a no-op add on the destination side, + and the source entry is still removed) — the same collapse semantics + `BankIndex::add` already has, now observed across a move. Cross-bank dedup is + **not** enforced: the same hash may exist in the pool and in a named bank + simultaneously (that is the point — copy lets a sample be grouped into "Drums" + while still living in the pool). +- **Move vs. copy are distinct acts.** *Move* removes from source, adds to + destination (one logical sample, regrouped). *Copy* adds to destination and leaves + the source entry intact (same file, two index entries, two banks). Both are + index-only; both share the destination-collapse rule. Copy is what lets a sample + live in the pool *and* a named group at once. +- **Persistence: a new ext-state key, pool migrates in place.** The multi-bank + state serializes to a new key `banks` in the existing `"reasampler"` namespace, + alongside `bank_index`, `view_state`, and `project_guid`. **Migration:** on load, + if a `banks` key is absent but a legacy `bank_index` key is present, the legacy + index becomes the pool's `BankIndex` and the book is `{ pool }` with zero named + banks — a one-way, lossless promotion. (Whether `bank_index` is retired or kept as + the pool's canonical slot is an open fork — see below.) +- **Vertical-split UI, pool on top.** The bank window splits vertically: **pool on + top**, the **named-banks region below** (a tab-page strip, one tab per named bank, + empty when none exist). Two full-height toggles collapse the split: **pool + full-height** (hide the named-banks region) and **banks full-height** (hide the + pool). The Design View segmented mode switch already in the window header is + **orthogonal** and stays — it governs timeline visibility, not bank grouping; the + two coexist in the header/body without interaction. + +## Precision / invariant implications + +- **Relative-paths-only survives unchanged.** Every `BankIndex` in the book keeps + the relative-path invariant at its `add` boundary — the book adds no new path + handling, because movement is index-only and files never relocate. The invariant + is enforced N times (once per bank) by the exact code that enforces it today. +- **Non-destructive.** Bank create / rename / delete / activate and sample + move / copy mutate only index + ext-state; no file is written, moved, or deleted, + and no timeline item is touched. Deleting a **named** bank does not delete its + samples' files (they may be referenced by the pool or another bank via copy); + file lifecycle stays owned by the capture/prune path, not the bank container. See + open fork on what "delete a named bank" does to its member samples. +- **Travels-with-the-.rpp preserved.** The `banks` blob rides the same ext-state + namespace and the same GUID-primary project-identity / Save-As-relocation + machinery as the bank index does today (M4). One shared physical bank folder, one + ext-state namespace, now three logical sections (banks + view + identity). +- **Determinism / null-test / bit-identical are untouched** — they are properties + of the capture path and the file, and the multi-bank layer sits above the file + entirely. + +## Module architecture (preserve the pure/shell split) + +Pure (no REAPER types, unit-tested — the mirror of `bank_model` / `view_mode_model`): +- `bank_book` — ordered bank registry (`{ bank id, display name, ordinal, + BankIndex }`); pool seeded with fixed id + name; create / rename / reorder / + delete named banks (pool-privilege rules enforced here: reject delete/rename of + pool); active-bank id (get/set, defaults to pool); **move** and **copy** a sample + between banks (index-only, destination-collapse observed); query a bank's index; + JSON round-trip of the whole book (banks + per-bank indices + active id + + ordinals) and legacy-`bank_index`→pool migration on parse. + +REAPER-facing: +- `persist` (slice) — serialize/deserialize the book under the `banks` key in + `"reasampler"`; migrate a legacy `bank_index` key into the pool on first load; + reload-on-open and Save-As survival ride the existing M4 machinery. The session + exposes the book the way it exposes the bank today; the active bank's `BankIndex` + is what the capture layer adds to. +- `bank_panel` (extension) — the vertical split: pool grid on top, named-banks + tab-page region below; two full-height toggles; the active-bank indicator; the + create / rename / delete / activate affordances; sample move/copy affordance + (drag between regions and/or a "send to bank" menu on selection). Reuses the + existing LICE grid render loop per bank region. +- `actions` (entries) — create bank / rename bank / delete bank; activate bank + (direct + cycle); move selected samples → bank; copy selected samples → bank; + pool/banks full-height toggles. Registered with the `command_id` / `gaccel` / + `hookcommand` pattern; bank-activate + move/copy MIDI-bindable to suit the + capture-heavy workflow. + +## REAPER API surface (verify all signatures) + +No new REAPER API is invented at spec stage — the multi-bank layer is pure model + +persistence + panel UI over machinery M0–M6 already established. Shells will need to +verify against the SDK header where they extend existing surfaces: +- **Persistence:** `SetProjExtState` / `GetProjExtState` under `"reasampler"`, new + key `banks` (shared blob machinery from M4 — no new API, new key only). +- **Panel UI:** the docked-window + LICE-grid surface from M5 (`bank_panel`), + extended to two grid regions + a tab strip + toggles. SWELL controls for the tab + strip / toggle affordances follow the M5 docking pattern; **verify SWELL control + usage against the M5 reference**, no new REAPER audio API involved. +- **Actions:** the `command_id` / `gaccel` / `hookcommand` contract from `main.cpp` + (unchanged), new command-id strings under the sampler family prefix. + +## Non-goals / guardrails + +- **No per-bank folders on disk.** Banks are logical groupings over one shared + project bank folder. Do not create a subfolder per bank or move files on + bank-move — reject any such path in review (it reintroduces the path-rewrite + bug class M4 closed). +- **No cross-bank dedup enforcement.** The same hash may exist in multiple banks + (that is what copy is for). Do not add a global dedup that collapses across banks. +- **Pool privileges are inviolable.** No action path may delete or rename the pool, + or leave a project with zero banks. Enforce in the pure rules layer, not just the + UI. +- **Capture still never inserts into the arrange.** The load-bearing principle is + unchanged; multi-bank only redirects which index the capture lands in. +- **Additive only.** Do not alter `BankIndex`, the M0–M11 capture roadmap, or Phase + D semantics. `bank_book` wraps; it does not modify. +- **Verify API names** against the SDK header before use. + +## Open questions to resolve during build + +- **`bank_index` key retirement vs. retention.** Two shapes: (a) the pool's index + rides *inside* the `banks` blob and the legacy `bank_index` key is retired after a + one-way migration; (b) the `bank_index` key is *kept* as the pool's canonical + storage slot and `banks` holds only the named banks + ordering + active id. (a) is + cleaner (one blob, one section) but rewrites where the pool lives; (b) is more + conservative (existing pool persistence untouched, named banks are pure addition) + at the cost of the pool being stored differently from named banks. Leaning (b) for + additive-minimalism against in-flight M7/M8 persist work — but this is a genuine + fork; see product notes. +- **What "delete a named bank" does to its members.** Options: (i) *reabsorb* — + member samples move back to the pool (no sample is ever lost to a bank delete); + (ii) *orphan-check* — delete members whose file is referenced by no other bank, + keep the rest; (iii) *forbid non-empty delete* — require the bank be emptied + first. (i) is the safest and simplest mental model (a named bank is a *grouping*, + deleting the group returns things home) and pairs naturally with index-only + movement. Leaning (i); flag for Daniel. +- **Move via drag vs. menu as the primary affordance**, and whether the named-banks + region is REAPER-native tabs (SWELL tab control) or LICE-drawn tabs matching the + grid aesthetic. UI-mechanics detail for the panel build; verify SWELL tab-control + availability against the M5 reference. +- **Active-bank indicator placement** — in the pool/bank region headers, or a + single header readout. Panel-polish detail. diff --git a/PLAN.md b/PLAN.md index 5d6561d..426df77 100644 --- a/PLAN.md +++ b/PLAN.md @@ -79,3 +79,106 @@ landed milestone. in int64 but exceed `INT_MAX` are implementation-defined. Hardening candidate — add bounds check before the cast when integer-field validation is in scope. +--- + +# Phase B — Multi-bank (parallel to the M0–M11 capture roadmap and Phase D) + +> **Separate phase namespace.** The M-numbers belong to the capture pillar +> (M0–M11); the D-letters belong to Design View. Multi-bank is a third orthogonal +> pillar — generalizing the single bank into a pool + named banks — so it takes its +> own **lettered** namespace (B1, B2, …). "B" reads for **Banks** and, like Phase D, +> keeps the roadmaps from colliding on numbering: Phase B is not "the twelfth +> capture step," it is a different pillar. Authoritative spec: **CONTEXT.md +> §Multi-bank**. Product framing: `docs/product/multi-bank.md`. When a point lands, +> doc-keeper moves it to `COMPLETED.md`. + +## B1 — bank_book (pure) +**Goal:** REAPER-free bank registry wrapping N `BankIndex` instances: pool seeded + +privileged, create/rename/reorder/delete named banks, active-bank id, move/copy a +sample between banks, JSON round-trip + legacy-migration. The heart of the phase; +mirror of `bank_model` / `view_mode_model`; **`BankIndex` untouched (additive)**. +CONTEXT.md §Multi-bank (Module architecture — pure). +**Verify:** CTest green. Pool always present, un-deletable, un-renamable (rules +rejected in-model). Active-bank defaults to pool. Move is index-only (source loses +entry, destination gains it) and observes destination collapse-by-hash; copy leaves +source intact. JSON round-trip lossless across banks + per-bank indices + ordinals + +active id. Legacy `bank_index` JSON parses into `{ pool }` with zero named banks. + +- [ ] Bank registry: ordered `{ bank id, display name, ordinal, BankIndex }`; pool + seeded with fixed id + fixed name; create / rename / reorder / delete named banks. +- [ ] Pool-privilege rules enforced in-model: reject delete-pool, reject + rename-pool, never allow zero banks. +- [ ] Active-bank id (get/set; defaults to pool); resolve active bank's `BankIndex`. +- [ ] Move sample between banks (index-only; destination collapse-by-hash observed; + source entry removed). +- [ ] Copy sample between banks (index-only; source entry retained; destination + collapse-by-hash observed). +- [ ] JSON round-trip: banks + per-bank indices + ordinals + active id. +- [ ] Legacy migration: a bare `bank_index` JSON promotes to the pool's index with + zero named banks (one-way, lossless). +- [ ] Tests: pool privileges (delete/rename rejected); move source-loses/dest-gains; + copy source-retained; cross-bank same-hash coexistence; dest collapse on move into + a bank already holding the hash; JSON lossless; legacy migration. + +## B2 — persist slice (banks ↔ project ext state) +**Goal:** Serialize the book under the `banks` key in `"reasampler"` alongside the +existing sections; migrate a legacy `bank_index` key into the pool on first load; +reload-on-open and Save-As survival via the existing M4 machinery. CONTEXT.md +§Multi-bank (persist). **Verify (in DAW):** Banks + named banks + active bank + all +per-bank samples survive Save / Save As / close+reopen; **relative paths only**; +bank travels with the `.rpp`; a project saved before this phase (legacy `bank_index` +only) loads as pool + zero named banks with no sample loss. +**Depends on:** B1. Resolve the `bank_index` retirement-vs-retention fork first +(see Phase B open questions). + +- [ ] Serialize/deserialize the book under the `banks` key (shared blob, distinct + section from `bank_index` / `view_state`). +- [ ] Legacy-migration path on load: absent `banks` + present `bank_index` → pool. +- [ ] Session exposes the book; the active bank's `BankIndex` is the capture add + target (route the M7 capture family through it — additive to M7, no M7 rewrite). +- [ ] Confirm survival across Save / Save As; confirm legacy-project load path. + +## B3 — actions +**Goal:** Bindable action set for the multi-bank workflow. CONTEXT.md §Multi-bank +(actions). **Verify (in DAW):** Each action registered (bindable in Actions list); +bank-activate + move/copy MIDI-bindable; create/rename/delete drive the B1 model via +the B2-persisted session. +**Depends on:** B1, B2. + +- [ ] Create bank / rename bank / delete bank (delete honors the resolved + member-disposition rule — see open questions). +- [ ] Activate bank (direct-by-id + cycle). +- [ ] Move selected samples → bank / copy selected samples → bank. +- [ ] Pool full-height / banks full-height toggles. +- [ ] Register each (`command_id`/`gaccel`/`hookcommand`); bank-activate + move/copy + MIDI-bindable. + +## B4 — bank_panel vertical split (UI) +**Goal:** The vertical-split bank window — pool on top, named-banks tab-page region +below, full-height toggles — extending the M5 docked grid. CONTEXT.md §Multi-bank +(bank_panel). **Verify (in DAW):** Pool grid renders on top; named-banks tab strip +below (empty when no named banks, one tab per named bank); active-bank indicated; +both full-height toggles collapse the split correctly; sample move/copy affordance +works (drag and/or menu); the Design View mode switch in the header is unaffected. +**Depends on:** B1, B2, B3. + +- [ ] Vertical split: pool grid region (top) + named-banks tab-page region (bottom). +- [ ] Named-banks tab strip: one tab per named bank; empty state when none. +- [ ] Pool full-height / banks full-height toggle affordances wired to B3. +- [ ] Active-bank indicator. +- [ ] Create / rename / delete / activate affordances driving B3 actions. +- [ ] Sample move/copy affordance (drag between regions and/or "send to bank" menu). + +## Phase B open questions +- **`bank_index` key retirement vs. retention** — (a) fold the pool into the `banks` + blob and retire the legacy key, vs (b) keep `bank_index` as the pool's canonical + slot and store only named banks under `banks`. Leaning (b) for additive-minimalism + against in-flight M7/M8 persist work. Resolve before B2. (blocks B2) +- **Named-bank delete → member disposition** — (i) reabsorb into pool, (ii) + orphan-check by file reference, (iii) forbid non-empty delete. Leaning (i). + (touches B1/B3) +- **Move affordance + tab rendering** — drag vs. menu as primary; SWELL-native vs. + LICE-drawn tabs. Verify SWELL tab-control availability against the M5 reference. + (touches B4) +- **Active-bank indicator placement** — per-region headers vs. single header + readout. (touches B4) diff --git a/docs/product/multi-bank.md b/docs/product/multi-bank.md new file mode 100644 index 0000000..71bcf86 --- /dev/null +++ b/docs/product/multi-bank.md @@ -0,0 +1,324 @@ +# Multi-bank — product notes + +Framing, rationale, and design-direction calls behind the **Multi-bank** phase. +The tickable spec lives in `PLAN.md` (Phase B) and the authoritative technical +detail in `CONTEXT.md` (§Multi-bank). This doc holds the *why* — the workflow +narrative, the pool-privilege reasoning, the movement semantics, and the +design-direction recommendations — so those don't clutter the build docs. + +Status: framed by product-designer (2026-07-23), pending Daniel review. Open items +for Daniel are listed at the bottom. + +--- + +## What it is (and what it is not) + +**Multi-bank generalizes the single per-project bank into a pool plus named +banks.** Today ReaSampler has exactly one bank (`BankIndex`) — every capture lands +in it, and it is the whole library. That is fine at ten samples and unusable at two +hundred. Multi-bank keeps the one bank as **the pool** — the default catch-all +every capture still lands in — and lets the user create **named banks** ("Drums", +"1-Shots", "Synth Hits") that group samples for a purpose. Samples move (or copy) +freely between any banks, including to and from the pool. One bank at a time is the +**active bank**: the capture target. The pool is active by default. + +**It is not a new file layout.** Banks are *logical groupings over one shared file +pool*, not folders on disk. Moving a sample between banks moves an index entry, not +a `.wav`. This is deliberate: it keeps movement cheap and non-destructive, and it +keeps every file exactly where M4's project-relative path resolution already puts +it. Anyone expecting "move to Drums" to reorganize the bank folder on disk will be +surprised — and that expectation is headed off in the spec, not discovered in +review. (See *movement semantics* below.) + +**It is not a change to how capture works.** The capture pillar's load-bearing rule +stands untouched: capturing writes a file plus an index entry and never inserts into +the arrange. Multi-bank changes exactly one thing — *which index* the entry lands +in. Everything downstream of "add a `Sample` to a `BankIndex`" is unchanged. + +--- + +## The pool is to banks what Arrange is to modes + +This phase deliberately reuses the structural pattern Phase D established for modes, +because the problems are the same shape. + +In Design View, **Arrange** is structurally just another mode in an N-mode registry, +but semantically privileged: it is the default home for every untagged leaf. The +data model is uniform (no Arrange-shaped special type); the *rules layer* enforces +Arrange's privilege. + +In Multi-bank, **the pool** is structurally just one `BankIndex` in an N-bank +registry, but semantically privileged: it always exists, is un-deletable, and is +un-renamable, and it is the default capture target and the default home for a +sample that isn't grouped anywhere else. Same move: uniform data model (a bank is a +bank), privilege enforced in the rules layer (`bank_book` rejects delete-pool / +rename-pool / zero-banks). + +Why this over a pool-shaped special type? A special pool type would fork every +operation into "pool path" and "named-bank path" — serialize, iterate, render, +move-target-resolution all branch. Treating the pool as bank-zero with three +enforced rules keeps one code path and one JSON shape, and the privileges live in +exactly one place. This is the same reasoning that kept Arrange from being a boolean +special case. + +--- + +## Why a container, not a bank-id on the sample + +The obvious alternative to a container is a `bankId` field on `Sample` plus one flat +`BankIndex`: filter by `bankId` to get a bank's contents. Rejected, for three +reasons: + +1. **It modifies `BankIndex`, the tested heart.** The whole discipline is that + `bank_model` is the pure, hard-tested core. A `bankId` field threads bank + awareness through the one module that should stay bank-agnostic. A container + *wraps* `BankIndex` and leaves it byte-for-byte as tested. +2. **Dedup-by-hash wants per-bank scope.** `BankIndex` already dedups by content + hash *within itself*. That is exactly the semantics we want per bank — and it is + what makes copy meaningful (the same hash can live in the pool and in "Drums"). + A flat index with a `bankId` field would need dedup to become "dedup within a + `bankId` partition," reimplementing per-bank scoping that a container gets for + free. +3. **It mirrors the two pure cores we already have.** `bank_model` and + `view_mode_model` are both "a pure registry with JSON round-trip, unit-tested + outside the DAW." `bank_book` is the third instance of that exact pattern. A + `bankId` field would be a fourth, different pattern bolted onto the first. + +So: `bank_book` is an ordered registry of `{ bank id, display name, ordinal, +BankIndex }`, pool seeded as bank-zero. Bank id is the stable key (minted GUID-style +on create); name and ordinal are mutable. `BankIndex` is untouched. This is the +defer-the-feature, design-the-seam principle: the seam is a container above the +tested core, not a modification of it. + +--- + +## Movement semantics (the settled rules, in prose) + +- **Move is index-only.** Moving a sample from bank A to bank B removes the `Sample` + from A's `BankIndex` and adds it to B's. The file never moves — one shared file + pool under the project bank folder, exactly where M4 put it. Cheap, + non-destructive, immune to the path-rewrite bug class M4 closed. +- **Copy is index-only too.** Copy adds the sample to B and *leaves it in A*. Two + index entries, one file, two banks. Copy is the mechanism that lets a sample live + in the pool *and* in a named group at once — the pool stays the complete library, + and "Drums" is a curated view into part of it. +- **Dedup-by-hash is per-bank, and collapse is observed across a move.** Each + `BankIndex` dedups within itself, unchanged. If you move (or copy) a sample into a + bank that already holds its hash, the destination *collapses* onto its existing + entry — the same collapse `BankIndex::add` already does, now visible across a + move. On a *move* the source entry is still removed, so the sample ends up in the + destination once, as expected. +- **No cross-bank dedup.** The same hash may exist in the pool and in a named bank + simultaneously. That is the whole point of copy — do not add a global dedup that + collapses across banks (guardrail in the spec). + +Why index-only movement over moving files into per-bank subfolders? The subfolder +approach makes "which bank" a filesystem fact, which reads tidy — but it reintroduces +exactly the path-rewrite fragility M4 spent three iterations eliminating (Save-As +relocation, relative-path resolution, GUID-primary identity). Every bank-move would +be a file operation that has to stay non-destructive and relative-path-correct +across Save-As. Index-only movement sidesteps all of it: files never move, so paths +never rewrite, so none of that machinery is re-exercised. Banks are a *view* concept, +files are a *storage* concept, and keeping them separate is what keeps movement +trivially correct. + +--- + +## Persistence and migration + +The book rides the existing `"reasampler"` project ext-state namespace under a new +key, `banks`, alongside `bank_index`, `view_state`, and `project_guid`. Same +namespace, same travel-with-the-`.rpp` guarantee, same GUID-primary identity and +Save-As-relocation machinery from M4. One shared physical bank folder; one +ext-state namespace; now three logical sections. + +**Migration of an existing single-bank project is the load-bearing edge case.** A +project saved before this phase has a `bank_index` key and no `banks` key. On load, +that legacy index becomes *the pool's* `BankIndex`, and the book is `{ pool }` with +zero named banks. One-way, lossless promotion — no sample is lost, no path changes, +the user opens their old project and finds everything in the pool exactly as before, +now with the ability to add named banks. This must be a first-class, tested path +(B1 pure-model migration + B2 in-DAW load), not an afterthought. + +There is one genuine fork here, called out in the open items: whether the pool's +index lives *inside* the `banks` blob (retire the legacy `bank_index` key) or the +`bank_index` key is *kept* as the pool's canonical slot with `banks` holding only +named banks. Both migrate cleanly; the trade is cleanliness (one blob) vs. +additive-minimalism (existing pool persistence untouched, named banks pure +addition). The lean recommendation is the conservative one — see open items. + +--- + +## Precision-invariant implications + +Multi-bank sits *above* the file entirely, so the capture-side invariants +(determinism, bit-identical repeats, null test, exact bounds) are untouched — they +are properties of the capture path and the file. The invariants it *does* touch: + +- **Relative-paths-only** is enforced N times instead of once — once per + `BankIndex` in the book, by the exact code that enforces it today. Movement adds + no path handling because files never relocate. +- **Non-destructive** extends to bank operations: create / rename / delete / + activate and sample move / copy mutate only index + ext-state. No file is written, + moved, or deleted; no timeline item is touched. In particular, **deleting a named + bank does not delete its samples' files** — a file may be referenced by the pool + or another bank via copy, and file lifecycle stays owned by the capture/prune + path, never the bank container. +- **Travels-with-the-.rpp** is preserved by riding the M4 machinery unchanged. + +--- + +## Design-direction recommendations (opinionated) + +### The pool stays the complete library; named banks are curated views + +The strongest mental model — and the one the index-only, copy-friendly design is +built for — is: **the pool is everything you've captured; named banks are curated +subsets you assemble by hand.** Copy (not move) into a named bank keeps the pool +complete, so the user can always fall back to "it's in the pool somewhere" while +"Drums" stays a clean working set. Move is there for the user who wants a strict +partition, but copy is the gentler default the UI should make easy. This is +borrowed from playlist-vs-library models (music apps, Lightroom collections): the +library is authoritative and complete; collections are lightweight views over it. + +Recommendation: make **copy the low-friction gesture** (drag, or a one-click "add to +bank") and **move the deliberate one** (explicit "move" menu item). Do not force the +user to choose partition semantics up front. + +### The vertical split: pool on top, named banks as a tab strip below + +Daniel's directive is a vertical split — pool on top, named banks below — with +full-height toggles for either region. This reads well against the "pool is the +library, banks are views" model: the library is the persistent top region, the +curated views are the swappable bottom region. + +- **Named banks as a tab strip** (one tab per named bank, one bank visible at a + time in the region) rather than a stack of grids. A tab strip scales to many banks + without eating vertical space, and "one active tab" pairs with "one active bank" + cleanly (though *shown* tab and *capture-active* bank are distinct — see below). + Borrowed from browser/IDE tab strips and sample-library browsers (Ableton's + collections rail, Kontakt's multi rack). +- **Full-height toggles** collapse the split to one region: *pool full-height* (hide + named banks — "I'm just capturing into the pool right now") and *banks full-height* + (hide the pool — "I'm organizing"). These are mode-of-work toggles, not layout + fiddling, and they map to the two halves of the workflow (capture vs. curate). + +### Keep "active bank" distinct from "shown tab" + +A subtle but important call: the **capture-active** bank (where new captures land) +and the **currently-shown** named-bank tab are *different* concepts. You might be +looking at "Drums" while capturing into the pool, or vice versa. Conflating them +("the tab you're viewing is the capture target") would make it too easy to +capture into the wrong bank by merely browsing. + +Recommendation: **active bank is an explicit state with a clear indicator**, set by +an explicit "activate" affordance (and action), *not* implied by which tab is shown. +The pool is active by default; activating a named bank is a deliberate act. The +indicator should be unmistakable — the capture target is a thing you want to be sure +of before you hit capture. Placement (per-region header vs. single readout) is a +panel-polish open item. + +### The Design View mode switch and multi-bank are orthogonal — and should read that way + +The window header already carries the Design View segmented mode switch +(`[ Arrange | Design ]`). That governs *timeline visibility*. Multi-bank governs +*sample grouping*. They are fully orthogonal — you can be in Design mode capturing +into "Synth Hits," or in Arrange mode capturing into the pool. The UI must not +suggest a coupling: keep the mode switch where it is (header, timeline concern) and +the bank controls in the bank body (library concern). Do not, for instance, put bank +tabs next to the mode segments as if they were the same kind of switch. + +### Tagging vs. banking are different verbs — don't blur them + +Worth stating because both phases involve "putting a thing into a named group." +Design View **tags tracks** into modes (a track-visibility concern). Multi-bank +**moves samples** into banks (a library-organization concern). Different objects +(tracks vs. samples), different purpose (timeline stance vs. library grouping). The +vocabulary should stay distinct — "tag into Design" vs. "move to Drums" — so a user +never conflates the two systems. They rhyme structurally (both are N-collections +with a privileged default) but they are not the same feature and should not share +UI metaphors beyond what's genuinely shared. + +--- + +## Action set (proposed) + +Stable command-id strings follow the sampler family prefix. Bank-activate and +move/copy are MIDI-bindable to suit the capture-heavy, hands-on workflow. + +- **Create bank** / **Rename bank** / **Delete bank** — manage the named-bank set. + (Pool is un-deletable / un-renamable — the actions refuse on the pool.) +- **Activate bank** (direct-by-id) / **Cycle active bank** — set the capture target; + distinct from browsing a tab. +- **Move selected samples → bank** / **Copy selected samples → bank** — the two + movement verbs; copy the low-friction default per the recommendation above. +- **Pool full-height** / **Banks full-height** (toggles) — collapse the split. + +--- + +## Module breakdown (pure / persist / actions / UI) + +Mirrors the capture and Design View pillars exactly. + +**Pure `bank_book` (REAPER-free, unit-tested — the mirror of `bank_model` / +`view_mode_model`):** +- Ordered bank registry: `{ bank id, display name, ordinal, BankIndex }`; pool + seeded with fixed id + fixed name. +- Create / rename / reorder / delete named banks; pool-privilege rules enforced + here (reject delete-pool, reject rename-pool, never zero banks). +- Active-bank id (get/set, defaults to pool); resolve the active bank's `BankIndex`. +- Move / copy a sample between banks — index-only, destination collapse-by-hash + observed, move removes the source entry. +- JSON round-trip of the whole book (banks + per-bank indices + ordinals + active + id); legacy-`bank_index`→pool migration on parse. +- `BankIndex` is untouched — `bank_book` wraps, never modifies it. + +**`persist` slice:** +- Serialize/deserialize the book under the `banks` key in `"reasampler"` (shared + blob, distinct section from `bank_index` / `view_state`). +- Migrate a legacy `bank_index` key into the pool on first load. +- Reload-on-open and Save-As survival via the existing M4 machinery; the session + exposes the book; the active bank's `BankIndex` is the capture add target. + +**`actions` entries:** the set listed above, registered with the +`command_id` / `gaccel` / `hookcommand` pattern; bank-activate + move/copy +MIDI-bindable. + +**UI (extending the M5 `bank_panel`):** the vertical split (pool grid top, +named-banks tab-page region bottom); the two full-height toggles; the active-bank +indicator; create / rename / delete / activate affordances; the sample move/copy +affordance (drag between regions and/or a "send to bank" menu on selection). Reuses +the M5 LICE grid render loop per region. + +--- + +## Open items for Daniel + +1. **`bank_index` key retirement vs. retention.** Two persistence shapes: (a) fold + the pool's index *into* the `banks` blob and retire the legacy `bank_index` key + after a one-way migration — cleaner, one blob, one section; (b) *keep* the + `bank_index` key as the pool's canonical storage slot and store only named banks + + ordering + active id under `banks` — more conservative, existing pool + persistence untouched, named banks a pure addition. I lean **(b)** because M7/M8 + persist work is in flight in another worktree and (b) leaves the existing + `bank_index` write path alone. But it's a genuine fork with a real trade — your + call before B2 is scoped. +2. **Named-bank delete → member disposition.** When a user deletes a *named* bank + holding samples: (i) *reabsorb* — members move back to the pool, nothing is ever + lost to a delete; (ii) *orphan-check* — delete members no other bank references, + keep the rest; (iii) *forbid non-empty delete* — require emptying first. I lean + **(i)** — a named bank is a grouping, deleting the group returns things home, and + it pairs naturally with index-only movement. Confirm or redirect. +3. **Move vs. copy defaults.** I'm recommending **copy as the low-friction gesture** + (drag / one-click) and **move as the deliberate one** (explicit menu), on the + "pool is the complete library" model. If you'd rather move be the default (strict + partition mental model), say so — it changes the primary affordance. +4. **Active-bank vs. shown-tab separation.** I'm recommending these stay *distinct* + (browsing a tab does not change the capture target; activation is explicit). If + you'd prefer the simpler "the tab you're viewing is the capture target" coupling, + flag it — it's a real simplification but risks capturing into the wrong bank by + browsing. +5. **Tab rendering + move affordance mechanics.** SWELL-native tab control vs. + LICE-drawn tabs matching the grid aesthetic; drag-between-regions vs. menu as the + primary move gesture. Panel-build detail — noted for B4, not phase-defining, and + the SWELL tab-control availability needs verification against the M5 reference.