docs(multi-bank): settle Phase B forks 1-4; draft fork-5 options analysis
Fold Daniel's decisions into CONTEXT.md, PLAN.md, and the product notes: - Fork 1 (persistence): pool folds into the `banks` blob; legacy `bank_index` key retired after a one-way lossless migration. - Fork 2 (delete): delete drops member index entries; add an `evacuate` verb (B1 pure op + B3 action) returning members to the pool. Design the orphaned-until-prune window and confirm-on-non-empty-delete guardrail. - Fork 3 (move): move is the default gesture, copy the deliberate secondary. - Fork 4 (active bank): active-bank and shown-tab stay distinct; the active-bank indicator must be visually unmistakable. Fork 5 (tab rendering + move affordance) remains open with an options analysis and pending recommendations. Additive; M0-M11 and Phase D untouched.
This commit is contained in:
+79
-55
@@ -369,7 +369,7 @@ arrange; the only change is *which* index the entry lands in.
|
|||||||
active bank's `BankIndex`, then adds exactly as today. No capture backend changes;
|
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 +
|
only the add-target is selected upstream. Activating a bank is a model mutation +
|
||||||
a persist write; it never touches the timeline.
|
a persist write; it never touches the timeline.
|
||||||
- **Movement moves the index entry, not the file (default).** Moving a sample from
|
- **Movement moves the index entry, not the file.** Moving a sample from
|
||||||
bank A to bank B is an **index-only** operation: remove the `Sample` from A's
|
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 —
|
`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
|
banks are logical groupings over one shared file pool, not separate folders on
|
||||||
@@ -383,18 +383,36 @@ arrange; the only change is *which* index the entry lands in.
|
|||||||
**not** enforced: the same hash may exist in the pool and in a named bank
|
**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"
|
simultaneously (that is the point — copy lets a sample be grouped into "Drums"
|
||||||
while still living in the pool).
|
while still living in the pool).
|
||||||
- **Move vs. copy are distinct acts.** *Move* removes from source, adds to
|
- **Move vs. copy are distinct acts; move is the default.** *Move* removes from
|
||||||
destination (one logical sample, regrouped). *Copy* adds to destination and leaves
|
source, adds to destination (one logical sample, regrouped) — it is the **primary,
|
||||||
the source entry intact (same file, two index entries, two banks). Both are
|
low-friction gesture**, so a sample lives in exactly one bank at a time. *Copy* adds
|
||||||
index-only; both share the destination-collapse rule. Copy is what lets a sample
|
to destination and leaves the source entry intact (same file, two index entries, two
|
||||||
live in the pool *and* a named group at once.
|
banks) — the **deliberate secondary act** for the "in two places at once" case. Both
|
||||||
- **Persistence: a new ext-state key, pool migrates in place.** The multi-bank
|
are index-only; both share the destination-collapse rule. Under move-as-default the
|
||||||
state serializes to a new key `banks` in the existing `"reasampler"` namespace,
|
pool is the default home and staging ground, not a permanent superset: moving a
|
||||||
alongside `bank_index`, `view_state`, and `project_guid`. **Migration:** on load,
|
sample into a named bank takes it out of the pool. (See product notes for the
|
||||||
if a `banks` key is absent but a legacy `bank_index` key is present, the legacy
|
mental-model reconciliation.)
|
||||||
index becomes the pool's `BankIndex` and the book is `{ pool }` with zero named
|
- **Delete drops members; evacuate returns them.** Deleting a **named** bank drops
|
||||||
banks — a one-way, lossless promotion. (Whether `bank_index` is retired or kept as
|
its member index entries (files are **not** deleted — file lifecycle stays owned by
|
||||||
the pool's canonical slot is an open fork — see below.)
|
the capture/prune path). A separate **evacuate** operation moves all of a bank's
|
||||||
|
members back to the pool (index-only, same destination-collapse-by-hash as move),
|
||||||
|
leaving the bank empty. Intended workflow: *evacuate then delete* to keep the
|
||||||
|
samples, *plain delete* to drop the grouping and its members. A plain delete of a
|
||||||
|
non-empty bank orphans those members out of every index — their files persist on
|
||||||
|
disk until prune, referenced by no bank — so the UI **confirms on non-empty delete**
|
||||||
|
and offers evacuate as the alternative. Evacuate cannot be applied to the pool.
|
||||||
|
- **Persistence: a new ext-state key; the pool folds in and the legacy key is
|
||||||
|
retired.** The multi-bank state serializes to a new key `banks` in the existing
|
||||||
|
`"reasampler"` namespace, alongside `view_state` and `project_guid`. The pool's
|
||||||
|
index rides *inside* the `banks` blob as bank-zero — persisted identically to any
|
||||||
|
named bank (one blob, one section, one JSON shape). **Migration:** on load, if a
|
||||||
|
`banks` key is absent but a legacy `bank_index` key is present, the legacy index is
|
||||||
|
promoted into the pool inside a freshly-minted `banks` blob and the book is
|
||||||
|
`{ pool }` with zero named banks — a one-way, lossless promotion. After migration
|
||||||
|
the `banks` blob is **authoritative**; the legacy `bank_index` key is **retired** (not
|
||||||
|
written or read back going forward). The one-way retirement trades pre-multi-bank
|
||||||
|
backward-read compatibility for the clean single-blob shape — an accepted,
|
||||||
|
forward-only migration consistent with how M4 project state already moves forward.
|
||||||
- **Vertical-split UI, pool on top.** The bank window splits vertically: **pool on
|
- **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,
|
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
|
empty when none exist). Two full-height toggles collapse the split: **pool
|
||||||
@@ -409,12 +427,15 @@ arrange; the only change is *which* index the entry lands in.
|
|||||||
the relative-path invariant at its `add` boundary — the book adds no new path
|
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
|
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.
|
is enforced N times (once per bank) by the exact code that enforces it today.
|
||||||
- **Non-destructive.** Bank create / rename / delete / activate and sample
|
- **Non-destructive.** Bank create / rename / delete / activate / evacuate and sample
|
||||||
move / copy mutate only index + ext-state; no file is written, moved, or deleted,
|
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
|
and no timeline item is touched. Deleting a **named** bank drops its member index
|
||||||
samples' files (they may be referenced by the pool or another bank via copy);
|
entries but does **not** delete their files; file lifecycle stays owned by the
|
||||||
file lifecycle stays owned by the capture/prune path, not the bank container. See
|
capture/prune path, not the bank container. A file referenced *only* by the deleted
|
||||||
open fork on what "delete a named bank" does to its member samples.
|
bank becomes an orphan on disk — present but indexed by no bank — until the
|
||||||
|
capture/prune path reclaims it. That orphaned-until-prune window is designed, not
|
||||||
|
accidental; the *evacuate* verb and the confirm-on-non-empty-delete guardrail exist
|
||||||
|
to keep the user out of it unintentionally.
|
||||||
- **Travels-with-the-.rpp preserved.** The `banks` blob rides the same ext-state
|
- **Travels-with-the-.rpp preserved.** The `banks` blob rides the same ext-state
|
||||||
namespace and the same GUID-primary project-identity / Save-As-relocation
|
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
|
machinery as the bank index does today (M4). One shared physical bank folder, one
|
||||||
@@ -429,27 +450,33 @@ Pure (no REAPER types, unit-tested — the mirror of `bank_model` / `view_mode_m
|
|||||||
- `bank_book` — ordered bank registry (`{ bank id, display name, ordinal,
|
- `bank_book` — ordered bank registry (`{ bank id, display name, ordinal,
|
||||||
BankIndex }`); pool seeded with fixed id + name; create / rename / reorder /
|
BankIndex }`); pool seeded with fixed id + name; create / rename / reorder /
|
||||||
delete named banks (pool-privilege rules enforced here: reject delete/rename of
|
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
|
pool; delete drops member index entries); **evacuate** a bank (move every member to
|
||||||
between banks (index-only, destination-collapse observed); query a bank's index;
|
the pool, index-only, destination-collapse observed; pool cannot be evacuated);
|
||||||
JSON round-trip of the whole book (banks + per-bank indices + active id +
|
active-bank id (get/set, defaults to pool); **move** and **copy** a sample between
|
||||||
ordinals) and legacy-`bank_index`→pool migration on parse.
|
banks (index-only, destination-collapse observed); query a bank's index; JSON
|
||||||
|
round-trip of the whole book (pool-as-bank-zero inside the blob + named banks +
|
||||||
|
per-bank indices + active id + ordinals) and legacy-`bank_index`→pool migration on
|
||||||
|
parse (one-way; blob authoritative thereafter).
|
||||||
|
|
||||||
REAPER-facing:
|
REAPER-facing:
|
||||||
- `persist` (slice) — serialize/deserialize the book under the `banks` key in
|
- `persist` (slice) — serialize/deserialize the book under the `banks` key in
|
||||||
`"reasampler"`; migrate a legacy `bank_index` key into the pool on first load;
|
`"reasampler"` (pool-as-bank-zero inside the blob; no separate `bank_index` key
|
||||||
reload-on-open and Save-As survival ride the existing M4 machinery. The session
|
going forward); migrate a legacy `bank_index` key into the pool on first load
|
||||||
exposes the book the way it exposes the bank today; the active bank's `BankIndex`
|
(one-way; blob authoritative thereafter, legacy key retired); reload-on-open and
|
||||||
is what the capture layer adds to.
|
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
|
- `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
|
tab-page region below; two full-height toggles; the active-bank indicator; the
|
||||||
create / rename / delete / activate affordances; sample move/copy affordance
|
create / rename / delete / activate affordances; sample move/copy affordance
|
||||||
(drag between regions and/or a "send to bank" menu on selection). Reuses the
|
(drag between regions and/or a "send to bank" menu on selection). Reuses the
|
||||||
existing LICE grid render loop per bank region.
|
existing LICE grid render loop per bank region.
|
||||||
- `actions` (entries) — create bank / rename bank / delete bank; activate bank
|
- `actions` (entries) — create bank / rename bank / delete bank (confirm on
|
||||||
(direct + cycle); move selected samples → bank; copy selected samples → bank;
|
non-empty delete); evacuate bank → pool; activate bank (direct + cycle); move
|
||||||
pool/banks full-height toggles. Registered with the `command_id` / `gaccel` /
|
selected samples → bank; copy selected samples → bank; pool/banks full-height
|
||||||
`hookcommand` pattern; bank-activate + move/copy MIDI-bindable to suit the
|
toggles. Registered with the `command_id` / `gaccel` / `hookcommand` pattern;
|
||||||
capture-heavy workflow.
|
bank-activate + move/copy + evacuate MIDI-bindable to suit the capture-heavy
|
||||||
|
workflow.
|
||||||
|
|
||||||
## REAPER API surface (verify all signatures)
|
## REAPER API surface (verify all signatures)
|
||||||
|
|
||||||
@@ -474,8 +501,12 @@ verify against the SDK header where they extend existing surfaces:
|
|||||||
- **No cross-bank dedup enforcement.** The same hash may exist in multiple banks
|
- **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.
|
(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,
|
- **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
|
leave a project with zero banks, or **evacuate** the pool (the pool is evacuation's
|
||||||
UI.
|
destination, not a source). Enforce in the pure rules layer, not just the UI.
|
||||||
|
- **Delete drops members; files are never deleted by a bank op.** Deleting a named
|
||||||
|
bank removes its member index entries only. No bank operation writes, moves, or
|
||||||
|
deletes a file — file lifecycle stays with capture/prune. The UI **confirms on
|
||||||
|
non-empty delete** and offers evacuate; do not silently orphan members.
|
||||||
- **Capture still never inserts into the arrange.** The load-bearing principle is
|
- **Capture still never inserts into the arrange.** The load-bearing principle is
|
||||||
unchanged; multi-bank only redirects which index the capture lands in.
|
unchanged; multi-bank only redirects which index the capture lands in.
|
||||||
- **Additive only.** Do not alter `BankIndex`, the M0–M11 capture roadmap, or Phase
|
- **Additive only.** Do not alter `BankIndex`, the M0–M11 capture roadmap, or Phase
|
||||||
@@ -484,25 +515,18 @@ verify against the SDK header where they extend existing surfaces:
|
|||||||
|
|
||||||
## Open questions to resolve during build
|
## Open questions to resolve during build
|
||||||
|
|
||||||
- **`bank_index` key retirement vs. retention.** Two shapes: (a) the pool's index
|
Forks 1–4 are settled (see product notes → *Settled forks*, and the settled-decision
|
||||||
rides *inside* the `banks` blob and the legacy `bank_index` key is retired after a
|
prose above). Two panel-build items remain open.
|
||||||
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
|
- **Fork 5 — tab rendering + move affordance (B4).** Two sub-questions: (5a) the
|
||||||
cleaner (one blob, one section) but rewrites where the pool lives; (b) is more
|
named-banks region as a SWELL-native tab control vs. LICE-drawn tabs matching the
|
||||||
conservative (existing pool persistence untouched, named banks are pure addition)
|
grid aesthetic; (5b) the move gesture as drag-between-regions vs. a "send to bank"
|
||||||
at the cost of the pool being stored differently from named banks. Leaning (b) for
|
menu vs. both. Full options analysis (pros/cons across visual consistency, keyboard
|
||||||
additive-minimalism against in-flight M7/M8 persist work — but this is a genuine
|
nav, HiDPI, cross-platform SWELL parity, scaling, discoverability, mis-drop risk,
|
||||||
fork; see product notes.
|
MIDI-bindability, implementation cost) and pending recommendations in product notes
|
||||||
- **What "delete a named bank" does to its members.** Options: (i) *reabsorb* —
|
→ *Fork 5*. **Verify SWELL tab-control availability and cross-platform behavior
|
||||||
member samples move back to the pool (no sample is ever lost to a bank delete);
|
parity against the SWELL headers / SWS reference** before committing to native tabs.
|
||||||
(ii) *orphan-check* — delete members whose file is referenced by no other bank,
|
Decision pending Daniel.
|
||||||
keep the rest; (iii) *forbid non-empty delete* — require the bank be emptied
|
- **Active-bank indicator placement (B4 polish)** — per-region headers vs. a single
|
||||||
first. (i) is the safest and simplest mental model (a named bank is a *grouping*,
|
header readout vs. lit-tab treatment. The "visually unmistakable" requirement is
|
||||||
deleting the group returns things home) and pairs naturally with index-only
|
settled (fork 4); only the placement is open. Panel-polish detail.
|
||||||
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.
|
|
||||||
|
|||||||
@@ -98,42 +98,53 @@ privileged, create/rename/reorder/delete named banks, active-bank id, move/copy
|
|||||||
sample between banks, JSON round-trip + legacy-migration. The heart of the phase;
|
sample between banks, JSON round-trip + legacy-migration. The heart of the phase;
|
||||||
mirror of `bank_model` / `view_mode_model`; **`BankIndex` untouched (additive)**.
|
mirror of `bank_model` / `view_mode_model`; **`BankIndex` untouched (additive)**.
|
||||||
CONTEXT.md §Multi-bank (Module architecture — pure).
|
CONTEXT.md §Multi-bank (Module architecture — pure).
|
||||||
**Verify:** CTest green. Pool always present, un-deletable, un-renamable (rules
|
**Verify:** CTest green. Pool always present, un-deletable, un-renamable,
|
||||||
rejected in-model). Active-bank defaults to pool. Move is index-only (source loses
|
un-evacuable (rules rejected in-model). Active-bank defaults to pool. Move is
|
||||||
entry, destination gains it) and observes destination collapse-by-hash; copy leaves
|
index-only (source loses entry, destination gains it) and observes destination
|
||||||
source intact. JSON round-trip lossless across banks + per-bank indices + ordinals +
|
collapse-by-hash; copy leaves source intact. Delete drops member index entries.
|
||||||
|
Evacuate moves all members to the pool, leaving the bank empty. JSON round-trip
|
||||||
|
lossless across pool-as-bank-zero + named banks + per-bank indices + ordinals +
|
||||||
active id. Legacy `bank_index` JSON parses into `{ pool }` with zero named banks.
|
active id. Legacy `bank_index` JSON parses into `{ pool }` with zero named banks.
|
||||||
|
|
||||||
- [ ] Bank registry: ordered `{ bank id, display name, ordinal, BankIndex }`; pool
|
- [ ] Bank registry: ordered `{ bank id, display name, ordinal, BankIndex }`; pool
|
||||||
seeded with fixed id + fixed name; create / rename / reorder / delete named banks.
|
seeded with fixed id + fixed name; create / rename / reorder / delete named banks
|
||||||
|
(delete drops the bank's member index entries).
|
||||||
- [ ] Pool-privilege rules enforced in-model: reject delete-pool, reject
|
- [ ] Pool-privilege rules enforced in-model: reject delete-pool, reject
|
||||||
rename-pool, never allow zero banks.
|
rename-pool, reject evacuate-pool, never allow zero banks.
|
||||||
- [ ] Active-bank id (get/set; defaults to pool); resolve active bank's `BankIndex`.
|
- [ ] Active-bank id (get/set; defaults to pool); resolve active bank's `BankIndex`.
|
||||||
- [ ] Move sample between banks (index-only; destination collapse-by-hash observed;
|
- [ ] Move sample between banks (index-only; destination collapse-by-hash observed;
|
||||||
source entry removed).
|
source entry removed).
|
||||||
- [ ] Copy sample between banks (index-only; source entry retained; destination
|
- [ ] Copy sample between banks (index-only; source entry retained; destination
|
||||||
collapse-by-hash observed).
|
collapse-by-hash observed).
|
||||||
- [ ] JSON round-trip: banks + per-bank indices + ordinals + active id.
|
- [ ] Evacuate bank: move every member to the pool (index-only; destination
|
||||||
|
collapse-by-hash observed), leaving the bank empty; pool cannot be evacuated.
|
||||||
|
- [ ] JSON round-trip: pool-as-bank-zero inside the blob + named banks + per-bank
|
||||||
|
indices + ordinals + active id.
|
||||||
- [ ] Legacy migration: a bare `bank_index` JSON promotes to the pool's index with
|
- [ ] Legacy migration: a bare `bank_index` JSON promotes to the pool's index with
|
||||||
zero named banks (one-way, lossless).
|
zero named banks (one-way, lossless; blob authoritative thereafter).
|
||||||
- [ ] Tests: pool privileges (delete/rename rejected); move source-loses/dest-gains;
|
- [ ] Tests: pool privileges (delete/rename/evacuate rejected); move
|
||||||
copy source-retained; cross-bank same-hash coexistence; dest collapse on move into
|
source-loses/dest-gains; copy source-retained; evacuate empties source into pool
|
||||||
a bank already holding the hash; JSON lossless; legacy migration.
|
with dest collapse; 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)
|
## B2 — persist slice (banks ↔ project ext state)
|
||||||
**Goal:** Serialize the book under the `banks` key in `"reasampler"` alongside the
|
**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;
|
existing sections, with the pool folded in as bank-zero; migrate a legacy
|
||||||
reload-on-open and Save-As survival via the existing M4 machinery. CONTEXT.md
|
`bank_index` key into the pool on first load and retire the legacy key; reload-on-open
|
||||||
§Multi-bank (persist). **Verify (in DAW):** Banks + named banks + active bank + all
|
and Save-As survival via the existing M4 machinery. CONTEXT.md §Multi-bank (persist).
|
||||||
per-bank samples survive Save / Save As / close+reopen; **relative paths only**;
|
**Verify (in DAW):** Banks + named banks + active bank + all per-bank samples survive
|
||||||
bank travels with the `.rpp`; a project saved before this phase (legacy `bank_index`
|
Save / Save As / close+reopen; **relative paths only**; bank travels with the `.rpp`;
|
||||||
only) loads as pool + zero named banks with no sample loss.
|
a project saved before this phase (legacy `bank_index` only) loads as pool + zero
|
||||||
**Depends on:** B1. Resolve the `bank_index` retirement-vs-retention fork first
|
named banks with no sample loss, and after save carries `banks` with no `bank_index`
|
||||||
(see Phase B open questions).
|
written.
|
||||||
|
**Depends on:** B1. (Persistence-key fork settled — fork 1 (a): pool inside the
|
||||||
|
`banks` blob, legacy key retired after one-way migration.)
|
||||||
|
|
||||||
- [ ] Serialize/deserialize the book under the `banks` key (shared blob, distinct
|
- [ ] Serialize/deserialize the book under the `banks` key (pool-as-bank-zero inside
|
||||||
section from `bank_index` / `view_state`).
|
the blob; distinct section from `view_state`; no `bank_index` key written going
|
||||||
- [ ] Legacy-migration path on load: absent `banks` + present `bank_index` → pool.
|
forward).
|
||||||
|
- [ ] Legacy-migration path on load: absent `banks` + present `bank_index` → promote
|
||||||
|
into pool, mint the blob, treat blob as authoritative (legacy key retired).
|
||||||
- [ ] Session exposes the book; the active bank's `BankIndex` is the capture add
|
- [ ] 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).
|
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.
|
- [ ] Confirm survival across Save / Save As; confirm legacy-project load path.
|
||||||
@@ -141,44 +152,54 @@ only) loads as pool + zero named banks with no sample loss.
|
|||||||
## B3 — actions
|
## B3 — actions
|
||||||
**Goal:** Bindable action set for the multi-bank workflow. CONTEXT.md §Multi-bank
|
**Goal:** Bindable action set for the multi-bank workflow. CONTEXT.md §Multi-bank
|
||||||
(actions). **Verify (in DAW):** Each action registered (bindable in Actions list);
|
(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
|
bank-activate + move/copy + evacuate MIDI-bindable; create/rename/delete/evacuate
|
||||||
the B2-persisted session.
|
drive the B1 model via the B2-persisted session.
|
||||||
**Depends on:** B1, B2.
|
**Depends on:** B1, B2.
|
||||||
|
|
||||||
- [ ] Create bank / rename bank / delete bank (delete honors the resolved
|
- [ ] Create bank / rename bank / delete bank (delete drops member index entries;
|
||||||
member-disposition rule — see open questions).
|
confirm-on-non-empty offered at the UI layer in B4).
|
||||||
|
- [ ] Evacuate bank → pool (move all members back to the pool; refuses on the pool).
|
||||||
- [ ] Activate bank (direct-by-id + cycle).
|
- [ ] Activate bank (direct-by-id + cycle).
|
||||||
- [ ] Move selected samples → bank / copy selected samples → bank.
|
- [ ] Move selected samples → bank / copy selected samples → bank (move is default).
|
||||||
- [ ] Pool full-height / banks full-height toggles.
|
- [ ] Pool full-height / banks full-height toggles.
|
||||||
- [ ] Register each (`command_id`/`gaccel`/`hookcommand`); bank-activate + move/copy
|
- [ ] Register each (`command_id`/`gaccel`/`hookcommand`); bank-activate + move/copy
|
||||||
MIDI-bindable.
|
+ evacuate MIDI-bindable.
|
||||||
|
|
||||||
## B4 — bank_panel vertical split (UI)
|
## B4 — bank_panel vertical split (UI)
|
||||||
**Goal:** The vertical-split bank window — pool on top, named-banks tab-page region
|
**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
|
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
|
(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;
|
below (empty when no named banks, one tab per named bank); active-bank **unmistakably**
|
||||||
both full-height toggles collapse the split correctly; sample move/copy affordance
|
indicated; both full-height toggles collapse the split correctly; sample move/copy
|
||||||
works (drag and/or menu); the Design View mode switch in the header is unaffected.
|
affordance works; non-empty delete confirms and offers evacuate; the Design View mode
|
||||||
**Depends on:** B1, B2, B3.
|
switch in the header is unaffected.
|
||||||
|
**Depends on:** B1, B2, B3. (Tab rendering + move-affordance mechanics — fork 5 —
|
||||||
|
pending Daniel's decision; see Phase B open questions and product notes → *Fork 5*.)
|
||||||
|
|
||||||
- [ ] Vertical split: pool grid region (top) + named-banks tab-page region (bottom).
|
- [ ] 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.
|
- [ ] Named-banks tab strip: one tab per named bank; empty state when none.
|
||||||
|
(SWELL-native vs. LICE-drawn — fork 5a, pending.)
|
||||||
- [ ] Pool full-height / banks full-height toggle affordances wired to B3.
|
- [ ] Pool full-height / banks full-height toggle affordances wired to B3.
|
||||||
- [ ] Active-bank indicator.
|
- [ ] Active-bank indicator — **visually unmistakable** (settled constraint);
|
||||||
- [ ] Create / rename / delete / activate affordances driving B3 actions.
|
placement (per-region header / single readout / lit-tab) is the residual polish
|
||||||
- [ ] Sample move/copy affordance (drag between regions and/or "send to bank" menu).
|
detail.
|
||||||
|
- [ ] Create / rename / delete / activate / evacuate affordances driving B3 actions.
|
||||||
|
- [ ] Delete confirms on a non-empty bank, naming the evacuate alternative.
|
||||||
|
- [ ] Sample move/copy affordance (drag between regions and/or "send to bank" menu —
|
||||||
|
fork 5b, pending).
|
||||||
|
|
||||||
## Phase B open questions
|
## Phase B open questions
|
||||||
- **`bank_index` key retirement vs. retention** — (a) fold the pool into the `banks`
|
Forks 1–4 settled by Daniel (2026-07-23): persistence key = fold pool into `banks`,
|
||||||
blob and retire the legacy key, vs (b) keep `bank_index` as the pool's canonical
|
retire legacy key (1a); delete drops members + add evacuate verb (2); move is the
|
||||||
slot and store only named banks under `banks`. Leaning (b) for additive-minimalism
|
default gesture (3); active-bank/shown-tab distinct with an unmistakable indicator
|
||||||
against in-flight M7/M8 persist work. Resolve before B2. (blocks B2)
|
(4). Folded into CONTEXT.md §Multi-bank + the B1–B4 points above. Remaining:
|
||||||
- **Named-bank delete → member disposition** — (i) reabsorb into pool, (ii)
|
|
||||||
orphan-check by file reference, (iii) forbid non-empty delete. Leaning (i).
|
- **Fork 5 — tab rendering + move affordance** — (5a) SWELL-native tab control vs.
|
||||||
(touches B1/B3)
|
LICE-drawn tabs matching the grid aesthetic; (5b) drag-between-regions vs.
|
||||||
- **Move affordance + tab rendering** — drag vs. menu as primary; SWELL-native vs.
|
"send to bank" menu vs. both. Options analysis + pending recommendations in product
|
||||||
LICE-drawn tabs. Verify SWELL tab-control availability against the M5 reference.
|
notes → *Fork 5*. Verify SWELL tab-control availability + cross-platform parity
|
||||||
|
against the SWELL headers / SWS reference before choosing native tabs. Decision
|
||||||
|
pending Daniel. (touches B4)
|
||||||
|
- **Active-bank indicator placement** — per-region headers vs. single header readout
|
||||||
|
vs. lit-tab. "Unmistakable" is settled; only placement is open. Polish detail.
|
||||||
(touches B4)
|
(touches B4)
|
||||||
- **Active-bank indicator placement** — per-region headers vs. single header
|
|
||||||
readout. (touches B4)
|
|
||||||
|
|||||||
+287
-90
@@ -6,8 +6,9 @@ detail in `CONTEXT.md` (§Multi-bank). This doc holds the *why* — the workflow
|
|||||||
narrative, the pool-privilege reasoning, the movement semantics, and the
|
narrative, the pool-privilege reasoning, the movement semantics, and the
|
||||||
design-direction recommendations — so those don't clutter the build docs.
|
design-direction recommendations — so those don't clutter the build docs.
|
||||||
|
|
||||||
Status: framed by product-designer (2026-07-23), pending Daniel review. Open items
|
Status: framed by product-designer (2026-07-23). Forks 1–4 settled by Daniel
|
||||||
for Daniel are listed at the bottom.
|
(2026-07-23); fork 5 (tab rendering + move affordance) still open — options
|
||||||
|
analysis at the bottom. Settled forks are folded into the prose below.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -18,9 +19,11 @@ banks.** Today ReaSampler has exactly one bank (`BankIndex`) — every capture l
|
|||||||
in it, and it is the whole library. That is fine at ten samples and unusable at two
|
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
|
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",
|
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)
|
"1-Shots", "Synth Hits") that group samples for a purpose. **Move** is the primary
|
||||||
freely between any banks, including to and from the pool. One bank at a time is the
|
gesture — a sample relocates from one bank to another, living in exactly one bank at
|
||||||
**active bank**: the capture target. The pool is active by default.
|
a time — with **copy** the deliberate secondary act when the user wants a sample in
|
||||||
|
two banks at once. 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
|
**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
|
pool*, not folders on disk. Moving a sample between banks moves an index entry, not
|
||||||
@@ -94,14 +97,17 @@ tested core, not a modification of it.
|
|||||||
|
|
||||||
## Movement semantics (the settled rules, in prose)
|
## Movement semantics (the settled rules, in prose)
|
||||||
|
|
||||||
- **Move is index-only.** Moving a sample from bank A to bank B removes the `Sample`
|
- **Move is index-only, and is the default gesture.** Moving a sample from bank A to
|
||||||
from A's `BankIndex` and adds it to B's. The file never moves — one shared file
|
bank B removes the `Sample` from A's `BankIndex` and adds it to B's. The file never
|
||||||
pool under the project bank folder, exactly where M4 put it. Cheap,
|
moves — one shared file pool under the project bank folder, exactly where M4 put it.
|
||||||
non-destructive, immune to the path-rewrite bug class M4 closed.
|
Cheap, non-destructive, immune to the path-rewrite bug class M4 closed. After a move
|
||||||
- **Copy is index-only too.** Copy adds the sample to B and *leaves it in A*. Two
|
the sample lives in exactly one bank; this is the primary, low-friction affordance
|
||||||
index entries, one file, two banks. Copy is the mechanism that lets a sample live
|
(see design-direction, below).
|
||||||
in the pool *and* in a named group at once — the pool stays the complete library,
|
- **Copy is index-only too, and is the deliberate secondary act.** Copy adds the
|
||||||
and "Drums" is a curated view into part of it.
|
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 —
|
||||||
|
or in two named groups. It is the explicit way to keep a sample in more than one
|
||||||
|
place; move (the default) puts it in one place.
|
||||||
- **Dedup-by-hash is per-bank, and collapse is observed across a move.** Each
|
- **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
|
`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
|
bank that already holds its hash, the destination *collapses* onto its existing
|
||||||
@@ -127,10 +133,18 @@ trivially correct.
|
|||||||
## Persistence and migration
|
## Persistence and migration
|
||||||
|
|
||||||
The book rides the existing `"reasampler"` project ext-state namespace under a new
|
The book rides the existing `"reasampler"` project ext-state namespace under a new
|
||||||
key, `banks`, alongside `bank_index`, `view_state`, and `project_guid`. Same
|
key, `banks`, alongside `view_state` and `project_guid`. Same namespace, same
|
||||||
namespace, same travel-with-the-`.rpp` guarantee, same GUID-primary identity and
|
travel-with-the-`.rpp` guarantee, same GUID-primary identity and Save-As-relocation
|
||||||
Save-As-relocation machinery from M4. One shared physical bank folder; one
|
machinery from M4. One shared physical bank folder; one ext-state namespace.
|
||||||
ext-state namespace; now three logical sections.
|
|
||||||
|
**The pool folds into the `banks` blob; the legacy `bank_index` key is retired
|
||||||
|
(fork 1, settled (a)).** The pool's index is stored *inside* the `banks` blob as
|
||||||
|
bank-zero, exactly like every named bank — one blob, one section, one JSON shape.
|
||||||
|
The old top-level `bank_index` key is not written going forward; after migration the
|
||||||
|
`banks` blob is the single authoritative source for all bank state including the
|
||||||
|
pool. This is the cleaner shape and keeps the "pool is just bank-zero" data-model
|
||||||
|
uniformity honest all the way down to storage: the pool is not persisted
|
||||||
|
differently from a named bank.
|
||||||
|
|
||||||
**Migration of an existing single-bank project is the load-bearing edge case.** A
|
**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,
|
project saved before this phase has a `bank_index` key and no `banks` key. On load,
|
||||||
@@ -140,12 +154,17 @@ the user opens their old project and finds everything in the pool exactly as bef
|
|||||||
now with the ability to add named banks. This must be a first-class, tested path
|
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.
|
(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
|
**The migration is a one-time promotion, and it is one-way.** On load: legacy
|
||||||
index lives *inside* the `banks` blob (retire the legacy `bank_index` key) or the
|
`bank_index` present, `banks` absent → promote the legacy index into the pool inside
|
||||||
`bank_index` key is *kept* as the pool's canonical slot with `banks` holding only
|
a freshly-minted `banks` blob, then treat the blob as authoritative. Once the `banks`
|
||||||
named banks. Both migrate cleanly; the trade is cleanliness (one blob) vs.
|
blob exists it is the only source consulted; the legacy key is not read back or
|
||||||
additive-minimalism (existing pool persistence untouched, named banks pure
|
kept in sync. A project touched by this version and saved will carry `banks` and
|
||||||
addition). The lean recommendation is the conservative one — see open items.
|
(going forward) will not carry a written `bank_index` — the retirement is complete
|
||||||
|
after the first save. The promotion is lossless: no sample, no path, no ordinal is
|
||||||
|
dropped. This trades a small amount of forward-only commitment (a project saved by
|
||||||
|
this version will not re-open cleanly in a pre-multi-bank build that only knows
|
||||||
|
`bank_index`) for the clean single-blob shape — an acceptable, one-directional
|
||||||
|
migration, consistent with how M4 project state already moves forward.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -159,31 +178,87 @@ are properties of the capture path and the file. The invariants it *does* touch:
|
|||||||
`BankIndex` in the book, by the exact code that enforces it today. Movement adds
|
`BankIndex` in the book, by the exact code that enforces it today. Movement adds
|
||||||
no path handling because files never relocate.
|
no path handling because files never relocate.
|
||||||
- **Non-destructive** extends to bank operations: create / rename / delete /
|
- **Non-destructive** extends to bank operations: create / rename / delete /
|
||||||
activate and sample move / copy mutate only index + ext-state. No file is written,
|
activate / evacuate and sample move / copy mutate only index + ext-state. No file
|
||||||
moved, or deleted; no timeline item is touched. In particular, **deleting a named
|
is written, moved, or deleted; no timeline item is touched. In particular,
|
||||||
bank does not delete its samples' files** — a file may be referenced by the pool
|
**deleting a named bank drops its member index entries but does not delete their
|
||||||
or another bank via copy, and file lifecycle stays owned by the capture/prune
|
files** — file lifecycle stays owned by the capture/prune path, never the bank
|
||||||
path, never the bank container.
|
container (fork 2, settled). A file that was referenced *only* by the deleted bank
|
||||||
|
becomes an orphan on disk — present but indexed by no bank — until the
|
||||||
|
capture/prune path reclaims it. That orphaned-until-prune window is a designed
|
||||||
|
consequence of "delete drops members, files persist," not an accident (see
|
||||||
|
*design-direction → evacuate* below for how the workflow avoids it).
|
||||||
- **Travels-with-the-.rpp** is preserved by riding the M4 machinery unchanged.
|
- **Travels-with-the-.rpp** is preserved by riding the M4 machinery unchanged.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Design-direction recommendations (opinionated)
|
## Design-direction (settled forks 2–4, plus rationale)
|
||||||
|
|
||||||
### The pool stays the complete library; named banks are curated views
|
### Move is the default; the pool is a bank, not a permanent mirror
|
||||||
|
|
||||||
The strongest mental model — and the one the index-only, copy-friendly design is
|
**Settled (fork 3): move is the primary, low-friction gesture; copy is the
|
||||||
built for — is: **the pool is everything you've captured; named banks are curated
|
deliberate secondary act.** This inverts the earlier framing (which leaned
|
||||||
subsets you assemble by hand.** Copy (not move) into a named bank keeps the pool
|
copy-as-default on a "pool is the complete library, banks are views" model). Daniel's
|
||||||
complete, so the user can always fall back to "it's in the pool somewhere" while
|
call is a *partition* mental model: a sample lives in one bank at a time, and moving
|
||||||
"Drums" stays a clean working set. Move is there for the user who wants a strict
|
it to "Drums" takes it *out* of the pool. Copy remains available for the user who
|
||||||
partition, but copy is the gentler default the UI should make easy. This is
|
genuinely wants a sample in two banks at once, but it is the explicit, deliberate act
|
||||||
borrowed from playlist-vs-library models (music apps, Lightroom collections): the
|
— not the default.
|
||||||
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
|
This changes what "the pool is the complete library" means, and the docs must be
|
||||||
bank") and **move the deliberate one** (explicit "move" menu item). Do not force the
|
honest about it. Under move-as-default the pool is **not** a permanent mirror of
|
||||||
user to choose partition semantics up front.
|
everything ever captured — it is the default catch-all every capture lands in *and*
|
||||||
|
the from-which/back-to-which home for organizing, but once a user moves a sample into
|
||||||
|
a named bank it is no longer in the pool. The honest framing is:
|
||||||
|
|
||||||
|
- **The pool is the default home and the staging ground**, not a guaranteed superset.
|
||||||
|
Fresh captures land there; from there the user *moves* them into named banks. What
|
||||||
|
is "in the pool" is what hasn't been filed yet plus whatever the user deliberately
|
||||||
|
keeps there.
|
||||||
|
- **Named banks are where filed samples live**, not lightweight views over a
|
||||||
|
still-complete library. A sample in "Drums" is *in Drums* and nowhere else unless
|
||||||
|
copied.
|
||||||
|
- **Copy is the escape hatch** for the "I want this in two places" case — borrowed
|
||||||
|
from the move-vs-copy distinction every file manager makes. It is deliberately the
|
||||||
|
less prominent of the two so the default stays a clean partition.
|
||||||
|
|
||||||
|
Prior art: this is the file-manager / DAW-media-pool model (move relocates, copy
|
||||||
|
duplicates) rather than the playlist-library model. It suits a capture-heavy workflow
|
||||||
|
where the user is actively filing rather than curating views over a fixed corpus.
|
||||||
|
|
||||||
|
### Evacuate: the "keep the samples, drop the grouping" gesture
|
||||||
|
|
||||||
|
**Settled (fork 2): delete drops a bank's member index entries; a separate
|
||||||
|
*evacuate* operation moves all members back to the pool.** These are two distinct
|
||||||
|
verbs for two distinct intents:
|
||||||
|
|
||||||
|
- **Delete** removes the bank *and its member index entries*. Files are not touched
|
||||||
|
(capture/prune owns file lifecycle). Use it when you want the grouping *and its
|
||||||
|
contents' index entries* gone.
|
||||||
|
- **Evacuate** moves every member of the bank back to the pool (index-only, observing
|
||||||
|
the same destination-collapse-by-hash as a normal move), leaving the bank empty.
|
||||||
|
Use it when you want to dissolve the grouping *but keep the samples*.
|
||||||
|
|
||||||
|
The intended workflow is **"evacuate, then delete"** when the user wants to keep the
|
||||||
|
samples and drop only the grouping, versus a **plain delete** when they want the
|
||||||
|
grouping and its members gone. Evacuate is a pure `bank_book` operation (B1) and a
|
||||||
|
bindable action (B3). Pool-privilege rules still apply: you cannot evacuate the pool
|
||||||
|
(it has nowhere to evacuate *to*, and it is the evacuation destination).
|
||||||
|
|
||||||
|
**The non-empty-delete hazard, and the recommended guardrail.** Because delete drops
|
||||||
|
members (rather than reabsorbing them, the earlier lean) and move is the default (so a
|
||||||
|
sample is often in exactly one bank), a plain delete of a *non-empty* named bank can
|
||||||
|
orphan those samples out of every index — their files persist on disk until prune,
|
||||||
|
but no bank references them. That is the orphaned-until-prune window called out in the
|
||||||
|
precision-invariant section. It is a real footgun: "I deleted Drums and my kick is
|
||||||
|
gone from the library."
|
||||||
|
|
||||||
|
Recommendation (B4, panel-polish, pending Daniel): **confirm on non-empty delete**,
|
||||||
|
with the confirmation naming the escape hatch. Something like *"Delete 'Drums'? Its 14
|
||||||
|
samples will be removed from the library (files remain on disk until pruned).
|
||||||
|
Evacuate to pool instead?"* — a three-way affordance (Delete / Evacuate then delete /
|
||||||
|
Cancel) turns the hazard into a teaching moment for the evacuate verb. An *empty*
|
||||||
|
bank deletes with no confirmation (nothing to lose). This keeps delete fast where it's
|
||||||
|
safe and guarded where it bites. I recommend adopting the confirm-on-non-empty
|
||||||
|
guardrail; the exact copy and button set are a B4 detail.
|
||||||
|
|
||||||
### The vertical split: pool on top, named banks as a tab strip below
|
### The vertical split: pool on top, named banks as a tab strip below
|
||||||
|
|
||||||
@@ -203,20 +278,25 @@ curated views are the swappable bottom region.
|
|||||||
(hide the pool — "I'm organizing"). These are mode-of-work toggles, not layout
|
(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).
|
fiddling, and they map to the two halves of the workflow (capture vs. curate).
|
||||||
|
|
||||||
### Keep "active bank" distinct from "shown tab"
|
### Active bank and shown tab stay distinct (settled fork 4)
|
||||||
|
|
||||||
A subtle but important call: the **capture-active** bank (where new captures land)
|
**Settled (fork 4): the capture-active bank and the currently-shown named-bank tab
|
||||||
and the **currently-shown** named-bank tab are *different* concepts. You might be
|
are distinct concepts, and the active bank carries an unmistakable visual
|
||||||
looking at "Drums" while capturing into the pool, or vice versa. Conflating them
|
indicator.** The **capture-active** bank (where new captures land) and the
|
||||||
("the tab you're viewing is the capture target") would make it too easy to
|
**currently-shown** tab are *different* things — you might be looking at "Drums" while
|
||||||
capture into the wrong bank by merely browsing.
|
capturing into the pool, or vice versa. Browsing a tab **never** retargets capture.
|
||||||
|
Conflating them ("the tab you're viewing is the capture target") was considered and
|
||||||
|
rejected: it makes it too easy to capture into the wrong bank by merely browsing, and
|
||||||
|
capture-into-wrong-bank is a silent error the user won't notice until later.
|
||||||
|
|
||||||
Recommendation: **active bank is an explicit state with a clear indicator**, set by
|
Active bank is an explicit state, set by an explicit "activate" affordance (and
|
||||||
an explicit "activate" affordance (and action), *not* implied by which tab is shown.
|
action), *not* implied by which tab is shown. The pool is active by default;
|
||||||
The pool is active by default; activating a named bank is a deliberate act. The
|
activating a named bank is a deliberate act. **The indicator must be visually
|
||||||
indicator should be unmistakable — the capture target is a thing you want to be sure
|
unmistakable** — this is now a settled constraint, not a suggestion. The capture
|
||||||
of before you hit capture. Placement (per-region header vs. single readout) is a
|
target is a thing the user must be able to verify at a glance before hitting capture.
|
||||||
panel-polish open item.
|
The indicator's exact placement (per-region header vs. single header readout vs.
|
||||||
|
lit-tab treatment) remains a B4 panel-polish detail — but that it reads unmistakably
|
||||||
|
is fixed.
|
||||||
|
|
||||||
### The Design View mode switch and multi-bank are orthogonal — and should read that way
|
### The Design View mode switch and multi-bank are orthogonal — and should read that way
|
||||||
|
|
||||||
@@ -247,11 +327,14 @@ 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.
|
move/copy are MIDI-bindable to suit the capture-heavy, hands-on workflow.
|
||||||
|
|
||||||
- **Create bank** / **Rename bank** / **Delete bank** — manage the named-bank set.
|
- **Create bank** / **Rename bank** / **Delete bank** — manage the named-bank set.
|
||||||
(Pool is un-deletable / un-renamable — the actions refuse on the pool.)
|
(Pool is un-deletable / un-renamable — the actions refuse on the pool.) Delete on a
|
||||||
|
non-empty bank confirms (see evacuate, above).
|
||||||
|
- **Evacuate bank → pool** — move all of a bank's members back to the pool, leaving
|
||||||
|
it empty; the "keep the samples, drop the grouping" verb. (Refuses on the pool.)
|
||||||
- **Activate bank** (direct-by-id) / **Cycle active bank** — set the capture target;
|
- **Activate bank** (direct-by-id) / **Cycle active bank** — set the capture target;
|
||||||
distinct from browsing a tab.
|
distinct from browsing a tab.
|
||||||
- **Move selected samples → bank** / **Copy selected samples → bank** — the two
|
- **Move selected samples → bank** / **Copy selected samples → bank** — the two
|
||||||
movement verbs; copy the low-friction default per the recommendation above.
|
movement verbs; move is the low-friction default, copy the deliberate secondary.
|
||||||
- **Pool full-height** / **Banks full-height** (toggles) — collapse the split.
|
- **Pool full-height** / **Banks full-height** (toggles) — collapse the split.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -269,20 +352,25 @@ Mirrors the capture and Design View pillars exactly.
|
|||||||
- Active-bank id (get/set, defaults to pool); resolve the active bank's `BankIndex`.
|
- 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
|
- Move / copy a sample between banks — index-only, destination collapse-by-hash
|
||||||
observed, move removes the source entry.
|
observed, move removes the source entry.
|
||||||
|
- Evacuate a bank — move every member to the pool (index-only, same destination
|
||||||
|
collapse-by-hash as move), leaving the bank empty; pool cannot be evacuated.
|
||||||
- JSON round-trip of the whole book (banks + per-bank indices + ordinals + active
|
- JSON round-trip of the whole book (banks + per-bank indices + ordinals + active
|
||||||
id); legacy-`bank_index`→pool migration on parse.
|
id), pool included as bank-zero *inside* the blob; legacy-`bank_index`→pool
|
||||||
|
migration on parse (one-way, blob becomes authoritative).
|
||||||
- `BankIndex` is untouched — `bank_book` wraps, never modifies it.
|
- `BankIndex` is untouched — `bank_book` wraps, never modifies it.
|
||||||
|
|
||||||
**`persist` slice:**
|
**`persist` slice:**
|
||||||
- Serialize/deserialize the book under the `banks` key in `"reasampler"` (shared
|
- Serialize/deserialize the book under the `banks` key in `"reasampler"` (shared
|
||||||
blob, distinct section from `bank_index` / `view_state`).
|
namespace, distinct section from `view_state`; the pool rides *inside* the `banks`
|
||||||
- Migrate a legacy `bank_index` key into the pool on first load.
|
blob as bank-zero — no separate `bank_index` key going forward).
|
||||||
|
- Migrate a legacy `bank_index` key into the pool on first load (one-way, lossless;
|
||||||
|
after migration the `banks` blob is authoritative and the legacy key is retired).
|
||||||
- Reload-on-open and Save-As survival via the existing M4 machinery; the session
|
- 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.
|
exposes the book; the active bank's `BankIndex` is the capture add target.
|
||||||
|
|
||||||
**`actions` entries:** the set listed above, registered with the
|
**`actions` entries:** the set listed above (including **evacuate bank → pool**),
|
||||||
`command_id` / `gaccel` / `hookcommand` pattern; bank-activate + move/copy
|
registered with the `command_id` / `gaccel` / `hookcommand` pattern; bank-activate +
|
||||||
MIDI-bindable.
|
move/copy + evacuate MIDI-bindable.
|
||||||
|
|
||||||
**UI (extending the M5 `bank_panel`):** the vertical split (pool grid top,
|
**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
|
named-banks tab-page region bottom); the two full-height toggles; the active-bank
|
||||||
@@ -292,33 +380,142 @@ the M5 LICE grid render loop per region.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Open items for Daniel
|
## Settled forks (Daniel, 2026-07-23)
|
||||||
|
|
||||||
1. **`bank_index` key retirement vs. retention.** Two persistence shapes: (a) fold
|
Four of the five open forks are now decided; their implications are folded into the
|
||||||
the pool's index *into* the `banks` blob and retire the legacy `bank_index` key
|
prose above. Recorded here as a scan-line so the decisions don't have to be
|
||||||
after a one-way migration — cleaner, one blob, one section; (b) *keep* the
|
reconstructed from the body text.
|
||||||
`bank_index` key as the pool's canonical storage slot and store only named banks
|
|
||||||
+ ordering + active id under `banks` — more conservative, existing pool
|
1. **Persistence key — (a), retire `bank_index`.** The pool folds into the `banks`
|
||||||
persistence untouched, named banks a pure addition. I lean **(b)** because M7/M8
|
blob as bank-zero; the legacy `bank_index` key is retired after a one-way,
|
||||||
persist work is in flight in another worktree and (b) leaves the existing
|
lossless migration; the blob is authoritative thereafter. (See *persistence and
|
||||||
`bank_index` write path alone. But it's a genuine fork with a real trade — your
|
migration*.)
|
||||||
call before B2 is scoped.
|
2. **Delete drops members; add an *evacuate* verb.** Deleting a named bank drops its
|
||||||
2. **Named-bank delete → member disposition.** When a user deletes a *named* bank
|
member index entries (files untouched, owned by capture/prune). A separate
|
||||||
holding samples: (i) *reabsorb* — members move back to the pool, nothing is ever
|
*evacuate* operation moves all members back to the pool. Workflow: "evacuate then
|
||||||
lost to a delete; (ii) *orphan-check* — delete members no other bank references,
|
delete" to keep samples; plain delete to drop the grouping and its members.
|
||||||
keep the rest; (iii) *forbid non-empty delete* — require emptying first. I lean
|
Non-empty delete should confirm and offer evacuate. (See *design-direction →
|
||||||
**(i)** — a named bank is a grouping, deleting the group returns things home, and
|
evacuate*; orphaned-until-prune consequence in *precision-invariant implications*.)
|
||||||
it pairs naturally with index-only movement. Confirm or redirect.
|
3. **Move is the default gesture; copy is the deliberate secondary.** A sample lives
|
||||||
3. **Move vs. copy defaults.** I'm recommending **copy as the low-friction gesture**
|
in one bank at a time; copy is the explicit "in two places" escape hatch. (See
|
||||||
(drag / one-click) and **move as the deliberate one** (explicit menu), on the
|
*design-direction → move is the default*.)
|
||||||
"pool is the complete library" model. If you'd rather move be the default (strict
|
4. **Active bank and shown tab stay distinct; active bank gets an unmistakable
|
||||||
partition mental model), say so — it changes the primary affordance.
|
indicator.** Browsing never retargets capture; the "visually unmistakable"
|
||||||
4. **Active-bank vs. shown-tab separation.** I'm recommending these stay *distinct*
|
requirement is a settled constraint (placement remains a B4 detail). (See
|
||||||
(browsing a tab does not change the capture target; activation is explicit). If
|
*design-direction → active bank and shown tab stay distinct*.)
|
||||||
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.
|
## Fork 5 — options for discussion, decision pending
|
||||||
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 one fork Daniel wants to weigh before deciding. Two sub-questions, both B4
|
||||||
the SWELL tab-control availability needs verification against the M5 reference.
|
panel-build mechanics, neither phase-defining but both shaping the panel's feel.
|
||||||
|
Nothing below is decided; the recommendations at the end are *pending Daniel's call*.
|
||||||
|
|
||||||
|
### 5a — Named-banks tab rendering: SWELL-native tabs vs. LICE-drawn custom tabs
|
||||||
|
|
||||||
|
The named-banks region needs a tab strip (one tab per named bank). Two ways to draw
|
||||||
|
it.
|
||||||
|
|
||||||
|
**Option A — SWELL-native tab control** (`SWELL`'s tab-control, the cross-platform
|
||||||
|
Win32 `SysTabControl32` analog). *Verify against the SWELL headers* — SWELL tab-control
|
||||||
|
availability and behavior-parity across Win/mac/Linux is a known soft spot and must be
|
||||||
|
checked, not assumed; the SWS extension is the reference for which SWELL controls are
|
||||||
|
safe to lean on.
|
||||||
|
|
||||||
|
- **Pros:** near-zero custom draw/hit-test code; keyboard navigation (arrow between
|
||||||
|
tabs, focus handling) comes for free on platforms where SWELL implements it;
|
||||||
|
gets native-ish affordances (overflow, tooltips) without hand-rolling.
|
||||||
|
- **Cons:** *visual inconsistency* — a native tab control will not match the M5 LICE
|
||||||
|
grid aesthetic or the Design View segmented switch; it will read as a bolted-on OS
|
||||||
|
widget in an otherwise custom-drawn surface. HiDPI/scaling behavior of SWELL controls
|
||||||
|
is uneven across platforms. Cross-platform parity is the real risk: the control may
|
||||||
|
look and behave three different ways on Win/mac/Linux, and SWELL's tab
|
||||||
|
implementation is less battle-tested than its buttons/edits.
|
||||||
|
- **Scaling:** native tab controls handle many tabs via built-in overflow/scroll, so
|
||||||
|
it degrades more gracefully past ~10–15 banks than a naive custom strip would.
|
||||||
|
|
||||||
|
**Option B — LICE-drawn custom tabs** matching the grid aesthetic (the same render
|
||||||
|
path M5 already uses, and the same approach the Design View segmented mode switch
|
||||||
|
`[ Arrange | Design ]` took).
|
||||||
|
|
||||||
|
- **Pros:** *visual consistency* — tabs match the grid and the existing segmented
|
||||||
|
switch; one coherent custom-drawn surface. Full control over HiDPI (draw at the
|
||||||
|
panel's own scale factor, as M5 already must). Identical look and behavior on all
|
||||||
|
three platforms because we own every pixel — no SWELL-parity risk. Precedent
|
||||||
|
exists in-repo: the Design View segmented switch is already a LICE-drawn
|
||||||
|
multi-segment selector, so the pattern and hit-testing are partly solved.
|
||||||
|
- **Cons:** we hand-roll hit-testing, keyboard navigation, and overflow. Keyboard nav
|
||||||
|
(tab/arrow to move between banks) is real work we'd otherwise get free from a native
|
||||||
|
control. Overflow past the visible width needs a scroll or chevron affordance we
|
||||||
|
build ourselves.
|
||||||
|
- **Scaling:** a naive fixed-width custom strip breaks down when named banks exceed the
|
||||||
|
region width (~8–12 tabs depending on name length) unless we build scroll/overflow.
|
||||||
|
This is the sharpest cost of B — but the Design View switch only ever has two
|
||||||
|
segments, so its precedent doesn't cover the many-tabs case; we'd extend it.
|
||||||
|
|
||||||
|
*Framing note:* the choice is largely "buy keyboard-nav + overflow from SWELL and pay
|
||||||
|
in visual inconsistency + cross-platform risk" (A) vs. "pay to build keyboard-nav +
|
||||||
|
overflow and get visual consistency + platform certainty" (B). How many named banks
|
||||||
|
we expect in practice is the swing factor: if a user rarely exceeds ~6–8 banks, B's
|
||||||
|
overflow cost is small and its consistency win is large; if banks routinely run into
|
||||||
|
the dozens, A's built-in overflow starts to matter.
|
||||||
|
|
||||||
|
### 5b — Move affordance: drag-between-regions vs. "send to bank" menu vs. both
|
||||||
|
|
||||||
|
The move gesture (now the default, per fork 3) needs a surface. Three shapes.
|
||||||
|
|
||||||
|
**Option A — drag between regions** (drag selected samples from the pool grid onto a
|
||||||
|
named-bank tab, or between tabs).
|
||||||
|
|
||||||
|
- **Pros:** spatially intuitive; matches the mental model ("put this there"); direct.
|
||||||
|
- **Cons:** *precision / mis-drop risk* is real on a dense grid — dragging onto the
|
||||||
|
wrong tab or dropping in dead space is easy, and a mis-drop that silently moves a
|
||||||
|
sample to the wrong bank is exactly the "silent wrong-bank" error we're guarding
|
||||||
|
against elsewhere. Not keyboard- or MIDI-bindable — but B3 already specifies the
|
||||||
|
move action *is* MIDI-bindable, so drag can't be the *only* surface. Drag also has
|
||||||
|
to interoperate cleanly with the M5 grid's existing multi-select drag behavior
|
||||||
|
(verify there's no gesture collision).
|
||||||
|
|
||||||
|
**Option B — "send to bank" / "move to bank" menu** on the current selection (a
|
||||||
|
context menu or a toolbar dropdown listing banks; the selected samples move to the
|
||||||
|
chosen bank).
|
||||||
|
|
||||||
|
- **Pros:** *precise* — you pick the destination from a named list, no mis-drop. Maps
|
||||||
|
directly onto the B3 bindable action (the menu is just a visual front-end for the
|
||||||
|
same `move → bankId` operation, so it's consistent with keyboard/MIDI binding).
|
||||||
|
Consistent with how the M5 grid already handles selection (act-on-current-selection,
|
||||||
|
not per-item widgets — the Design View tag/untag already works this way). Scales to
|
||||||
|
many banks (a menu list handles dozens of destinations fine). Cheapest to build in
|
||||||
|
the LICE/SWELL surface (a menu, not a drag-and-drop hit-test system).
|
||||||
|
- **Cons:** less *discoverable* than drag for a first-time user; an extra click vs. a
|
||||||
|
direct drag; feels less "direct-manipulation" than dragging.
|
||||||
|
|
||||||
|
**Option C — both.** Menu as the precise, bindable, discoverable-via-right-click
|
||||||
|
primary; drag as the direct-manipulation accelerator for users who prefer it.
|
||||||
|
|
||||||
|
- **Pros:** covers both interaction styles; the menu carries the bindable/precise
|
||||||
|
path while drag serves direct manipulation. This is what most media managers do
|
||||||
|
(Finder, Lightroom, Ableton browser all offer drag *and* a menu/right-click move).
|
||||||
|
- **Cons:** most implementation cost (build both, and make them agree); drag still
|
||||||
|
carries its mis-drop risk even as a secondary path.
|
||||||
|
|
||||||
|
### Recommendations (pending Daniel's decision)
|
||||||
|
|
||||||
|
- **5a — recommend LICE-drawn custom tabs (Option B).** Visual consistency with the
|
||||||
|
M5 grid and the Design View segmented switch is worth the hand-rolled keyboard-nav
|
||||||
|
and overflow, *and* it removes the cross-platform SWELL-parity risk entirely (we own
|
||||||
|
every pixel on all three platforms). The Design View segmented switch is a partial
|
||||||
|
precedent to extend rather than a from-scratch build. The one thing that would flip
|
||||||
|
me to native tabs: evidence that users routinely run dozens of named banks, where
|
||||||
|
built-in overflow earns its keep. Recommend B; revisit if the expected bank count is
|
||||||
|
high. *Verify SWELL tab-control availability against the headers regardless* — even
|
||||||
|
choosing B, we should know what the native fallback offers.
|
||||||
|
- **5b — recommend the "send to bank" menu as primary (Option B), with drag as a
|
||||||
|
later accelerator (toward C).** The menu is precise, cheap, scales to many banks,
|
||||||
|
is the natural front-end for the already-bindable B3 move action, and matches how
|
||||||
|
the M5 grid and Design View already act on the current selection. It sidesteps the
|
||||||
|
mis-drop hazard that would otherwise fight our "no silent wrong-bank" stance. Drag
|
||||||
|
is a nice accelerator but should not be the primary or only surface (it can't be
|
||||||
|
MIDI-bound, and B3 requires the action be bindable). Recommend menu-first; add drag
|
||||||
|
in a later B4 pass if it earns the cost. Net: ship B, leave the seam for C.
|
||||||
|
|||||||
Reference in New Issue
Block a user