docs(multi-bank): frame Phase B — pool + named banks spec
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.
This commit is contained in:
+189
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user