# 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.