# ReaSampler — implementation briefing A native C++ REAPER extension that captures any arbitrary audio source into a per-project **sample bank** (cached files + a docked grid), decoupled from the arrange view, with keyboard/MIDI-bindable capture and placement. Built as a precision tool: deterministic, non-destructive, no clutter. This document is the spec. Read the existing scaffold first (`src/main.cpp` is the REAPER<->extension contract; the pure/testable-core split in `src/mpe_model.*` is the pattern to preserve — the MPE model is being replaced, the *discipline* is not). Verify every REAPER API name and signature against `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use — the API names in this brief are correct-by-intent but treat them as hints, not gospel, and check argument order/types. ## The load-bearing principle **Capture and placement are separate acts.** Capturing audio writes a file to the bank and adds an index entry — it NEVER puts an item in the arrange. Placement is a distinct, on-demand action (insert / drag). Any code path that auto-inserts a capture into the timeline violates the entire point of the tool and must be rejected in review. This single rule is why the tool exists. ## Settled decisions - **Capture modes:** offline render (deterministic, the default) AND realtime record (for hardware / performed FX). Both sit behind one capture interface and produce identical bank entries. - **Bank scope:** per-project, travels with the `.rpp`. Files live in a project-relative subfolder; the index persists in project ext state. No absolute paths anywhere in the index. - **Material:** must handle full-mix/stem bounces, chops/one-shots, and single-cycle/wavetable grabs equally. That means exact sample-accurate bounds, explicit tail control, channel-count preservation, and loop/zero-crossing handling all matter from day one. ## Module architecture Preserve the scaffold's split: pure, REAPER-free logic in one set of files (unit-tested outside the DAW via the existing `tests/` + CTest harness), REAPER- facing shells in another. Pure (no REAPER types, fully unit-tested): - `bank_model` — the `Sample` metadata struct and the `BankIndex` (add / remove / query / tier moves / dedup-by-hash) plus JSON serialize/deserialize to a `std::string`. This is the heart; test it hard. - `peaks` — compute waveform min/max bins from raw PCM. Feed it a known signal (sine, ramp) and assert the envelope. We compute our own thumbnails from the captured file rather than depending on REAPER's peak API — we own the file format, so this is simpler, testable, and dependency-free. REAPER-facing: - `capture` — the `ICaptureBackend` interface plus `OfflineRenderBackend` and `RealtimeRecordBackend`. Input: a `CaptureRequest` (source mode, time range, wet/dry, tail, SR/bit-depth/channels, output path). Output: a finished file + a populated `Sample` handed to `bank_model`. - `insert` — placement via `InsertMedia`; conform-to-project-tempo vs literal, as an explicit flag (never silent stretching). - `bank_panel` — the docked grid: LICE-drawn thumbnails, audition, multi-select, keyboard navigation. Reuses the docking setup already in `mpe_view.cpp`. - `persist` — project ext state <-> `bank_model` JSON; project-relative path resolution (resolve bank folder from the current project path). - `actions` — registers the capture/placement/slot action family and routes each to the modules above (the `command_id` + `gaccel` + `hookcommand` pattern from `main.cpp`). ## Data model (sketch — refine in code) `Sample`: id, display name, relative file path, source mode, source range (start/ end in project time + PPQ), track GUID(s) if applicable, wet/dry, channel count, sample rate, length (seconds + musical/beats), capture tempo, optional key, peak/RMS/LUFS, clip flag, tier (scratch | archive), content hash, provenance (parent sample id + FX-chain snapshot string, when resampled from another sample), created timestamp. `BankIndex`: ordered collection of `Sample`, keyed by id; hash lookup for dedup; tier filtering; JSON round-trip. Scratch tier is auto-prunable; archive is kept. ## REAPER API surface (verify all signatures) Offline render (the crux — prototype this first): - Drive render settings with `GetSetProjectInfo` (`RENDER_BOUNDSFLAG`, `RENDER_STARTPOS`, `RENDER_ENDPOS`, `RENDER_TAILFLAG`, `RENDER_TAILMS`, `RENDER_SRATE`, `RENDER_CHANNELS`, `RENDER_SETTINGS` for source = master mix / selected tracks / selected items / time selection, wet vs dry) and `GetSetProjectInfo_String` (`RENDER_FILE`, `RENDER_PATTERN`, `RENDER_FORMAT`). - Trigger a no-dialog render via the appropriate render action / `RENDER_SETTINGS` bit. Confirm the exact command id and the "render without opening dialog" flag against current REAPER — do not assume; test that it runs headless. - Determinism is a hard requirement: two identical requests must produce bit-identical files (enables the null test below). Realtime record: - Standard "resample track" recipe: create a hidden track, set its record mode to record-output (latency-compensated) or route the source to it via a send, arm (`I_RECARM`), `CSurf_OnRecord`, run for the range, `CSurf_OnStop`, then move the recorded source file into the bank and delete the temp track. Verify `I_RECMODE` / `I_RECINPUT` values for output-recording. Sources & metadata: - Time selection: `GetSet_LoopTimeRange`. Razor edits: `GetSetMediaTrackInfo_String(track, "P_RAZOREDITS", ...)`. Tempo: `Master_GetTempo` / `TimeMap2_timeToBeats` / `GetProjectTimeSignature2`. Selected items/tracks: `CountSelectedMediaItems` / `GetSelectedTrack`. Placement: - `InsertMedia(path, mode)` at edit cursor / new track / replace selection (verify mode bits). `SetEditCurPos`. Wrap edits in `Undo_BeginBlock2` / `Undo_EndBlock2`. Persistence & paths: - `SetProjExtState` / `GetProjExtState` (namespace e.g. `"reasampler"`) for the index JSON. Resolve project folder via `EnumProjects` / `GetProjectPathEx`; store the bank under a project-relative subfolder; keep only relative paths in the index. ## Build order (each milestone independently testable) 1. `bank_model` + JSON round-trip + unit tests. (pure — no REAPER) 2. `peaks` + unit tests. (pure) 3. Offline capture of the time-selection master mix to a wav in the project bank folder; add a `Sample`; log it to the console. (the render-driving spike) 4. `persist`: write the index to proj ext state, reload on project open; confirm it survives Save / Save As. (bank travels with the .rpp) 5. `bank_panel`: docked grid with thumbnails, audition, selection. 6. `insert`: "insert selected sample at edit cursor" action via `InsertMedia`. 7. Capture action family: master / selected tracks / selected items / razor area, each with wet-dry and tail options, all registered as bindable actions. 8. `RealtimeRecordBackend` behind the same interface. 9. Slots: "capture to slot N" / "insert slot N", MIDI-bindable (MPC-style). 10. Provenance + "re-capture from source"; null-test verify action. 11. Polish: batch capture (per selected item / per razor area), resample-and-mute- source, conform-on-insert, drag-out to OS. ## Precision invariants (enforce, test) - **Null test:** a dry offline capture of a range, re-inserted at its source position, nulls to silence against the source. Ship this as a verification action; it is the tool's trust anchor. - **Bit-identical repeats:** identical offline requests produce identical files. - **Non-destructive:** capture never mutates source items or tracks (realtime's temp track is created and removed cleanly; source routing is restored). - **Exact bounds:** no rounding of the requested range; no added silence unless a tail is explicitly requested; channel count preserved (no silent stereo fold). - **Relative paths only** in the persisted index. ## Non-goals / guardrails - No auto-insertion of captures into the arrange (see the load-bearing principle). - Native OS drag-out is deferred to the final milestone — `InsertMedia`-driven placement is the primary path and must work first. - Do not depend on REAPER's peak API for thumbnails; compute from the captured file. - Do not silently time-stretch on insert; conform is opt-in. - Do not trust this brief's API names blindly — verify against the SDK header. ## Open questions to resolve during build - Exact no-dialog render command/flag on the current REAPER build. - Realtime record routing that captures wet master output without altering the user's monitoring. - Audio format for the bank (wav bit depth default; allow float for wavetable fidelity). - Thumbnail cache: recompute vs store peak bins alongside the index. --- # Design View — additive phase spec > **Additive section.** This is a standalone phase parallel to — not part of — the > M0–M11 capture roadmap above. Nothing above changes. Product framing (workflow > narrative, screenset differentiation, N-mode reasoning, design-direction calls) > lives in `docs/product/design-view.md`; this section is the authoritative > technical spec, matching the house style of the capture spec. Same standing > discipline applies: **verify every REAPER API name/signature against > `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use.** ## What it is A **track-visibility-plus-processing "mode" system**, toggled from the ReaSampler window. Tracks used purely for sound design (scratch oscillators, FX mangling, resampling sources) are tagged into **Design** mode; the arrangement's real tracks are **Arrange** mode (the default). Toggling to a mode **hides and disables** the tracks that don't belong to it. Workflow value: mental separation + clutter elimination — be in Design view, resample into the bank, flip to Arrange, place it. **This is not a separate canvas.** REAPER has exactly one arrange timeline. Design View is the *same timeline* with a curated, filtered track set and the inactive tracks parked — not a second surface, window, or duplicated project. It is the visibility/processing analog of the capture pillar's load-bearing rule: **designing and arranging are separate stances on one timeline**, and the tool enforces the separation **without ever destroying the user's real state.** ## Settled decisions - **Membership.** Default = Arrange; every untagged leaf belongs to it. Leaves **opt in** to Design (or any mode). No track appears in two modes at once except (a) via an explicit **show-both** toggle, or (b) parent/folder derivation. - **Parents are derived, never tagged.** A parent/folder track is visible in mode M if **either** (a) any descendant leaf is visible in M (descendant-derived), **or** (b) the parent belongs to M by its **own membership** — and an untagged parent is an Arrange member by default. So an untagged folder carrying its own FX/media above all-Design leaves shows in **both** Arrange (its own default) and Design (derived from its children). A parent is **never parked** in any mode it is visible in. Rule of thumb: **tag leaves; parents follow** — the common case, since a content-bearing folder still surfaces wherever its own membership places it. Master track is always visible and never touched. - **N-mode model, two-mode UI.** The data model carries arbitrarily many modes; the UI ships **Arrange** + **Design**. A mode is (stable id, display name, ordinal). Arrange is special only as the default home for untagged leaves. - **Parking a track** (inactive-mode leaf): `B_SHOWINTCP=0`, `B_SHOWINMIXER=0` (hide both panels), `B_MAINSEND=0` (out of mix), `I_FXEN=0` (FX bypassed), and `TrackFX_SetOffline(track, fx, true)` for **each** FX (reclaim CPU). Full CPU-park is the deliberate choice over mix-removal-only. - **Never touches `B_MUTE` / `I_SOLO`.** The tool owns only visibility, `B_MAINSEND`, `I_FXEN`, and per-FX offline — on every managed leaf, tagged or untagged. User mute/solo survives every toggle untouched. - **Persistence.** Membership index + last-active mode + per-track flag snapshots ride in the existing `"reasampler"` project ext-state namespace and travel with the `.rpp`. On project open, reapply the active mode's visibility + processing. ## Precision invariants (enforce, test) - **Non-destructive restore.** For every flag the tool drives, snapshot the prior value **before** parking; on toggle-back restore **from the snapshot**, never to a hardcoded "on." Round-trip (snapshot → park → restore) returns every driven flag to its captured value. This is the phase's trust anchor — the analog of the capture null test — and is enforced in the pure layer. - **Mute/solo untouched.** No toggle ever reads or writes `B_MUTE` / `I_SOLO`. - **Master untouched.** The tool never drives the master track's visibility flags (the SDK forbids `B_SHOWINTCP`/`B_SHOWINMIXER` on master; the invariant agrees). - **GUID-keyed, reorder-safe.** Membership keys on track GUID (`GetTrackGUID`), never track index; tolerates unknown/stale GUIDs (prune on reconcile). - **Relative/portable state only** in the persisted view section (GUID strings, mode ids — no absolute paths, no index positions). ## Documented caveat Offlined FX **re-instantiate** when a track returns to the active mode. Stateful plugins (convolution, loaded samplers, tail-holding effects) re-initialize on return — possible load hitch, un-persisted internal state lost. Accepted cost of the CPU reclaim; surface it at the toggle affordance (tooltip). ## Module architecture (preserve the pure/shell split) Pure (no REAPER types, unit-tested — the mirror of `bank_model`): - `view_mode_model` — mode registry (id/name/ordinal; Arrange + Design seeded); membership index (`track GUID → { mode ids }` + per-track show-both flag; add / remove / retag / query); **folder-tree-aware** visibility derivation (given the current parent↔child tree supplied by the shell + the active mode, compute the visible set); the **parking/restore planner** (given active mode + snapshot record, emit the exact (track, flag, value) operation lists for park and restore — where the restore invariant is enforced); JSON round-trip of modes + membership + show-both + snapshots + active mode. REAPER-facing: - `view` shell — reads `I_FOLDERDEPTH` across the track list to build the parent↔child tree and feeds it to `view_mode_model`; applies the planner's operations via `SetMediaTrackInfo_Value` (`B_SHOWINTCP` / `B_SHOWINMIXER` / `B_MAINSEND` / `I_FXEN`) and `TrackFX_GetCount` + per-FX `TrackFX_SetOffline`; snapshots prior flag values before parking; resolves GUIDs via `GetTrackGUID` / `guidToString` / `stringToGuid`. Never touches master visibility, never touches `B_MUTE` / `I_SOLO`. - `persist` (slice) — serialize/deserialize the view section into the `"reasampler"` namespace alongside the bank; on project open, rebuild the tree and reapply the active mode. - `actions` (entries) — toggle active mode; activate mode: Arrange / Design; tag/untag selected tracks → mode; show-both for selected tracks. Registered with the `command_id` / `gaccel` / `hookcommand` pattern; toggle + mode-jumps MIDI-bindable. - UI (in the ReaSampler / bank_panel window) — a **segmented mode switch** (`[ Arrange | Design ]`) in the window header, active segment lit; small per-mode membership count; the offlined-FX caveat as a tooltip. Tag/untag acts on the current REAPER track selection, not a per-track widget. ## Show-both semantics A **per-track "pin visible across modes"** flag that **re-enables processing** whenever shown. A show-both leaf appears in every mode's visible set and is **never parked** — its driven flags stay at snapshot/restored values, FX online, in the mix. ("Show but keep parked" is not offered — a visible-but-silent-and-offline track is clutter with a thumbnail.) Stored on the membership record; persists; togglable per selection. ## REAPER API surface (verify all signatures) - Visibility/routing/FX flags via `GetMediaTrackInfo_Value` (snapshot) / `SetMediaTrackInfo_Value` (apply): `B_SHOWINTCP`, `B_SHOWINMIXER`, `B_MAINSEND`, `I_FXEN`. (Note: brief cited `B_SHOWINMCP`; verified SDK name is `B_SHOWINMIXER`.) - Per-FX offline: `TrackFX_GetCount` + `TrackFX_SetOffline(track, fx, offline)`. - Folder tree: read `I_FOLDERDEPTH` per track to derive parent↔child structure. - GUID keying: `GetTrackGUID`, `guidToString`, `stringToGuid`. - Persistence: `SetProjExtState` / `GetProjExtState` under `"reasampler"` (shared with the bank index — one blob, two logical sections). - Wrap flag mutations in `Undo_BeginBlock2` / `Undo_EndBlock2` as appropriate. ## Non-goals / guardrails - **No second canvas.** Do not build a parallel arrange surface — reject any such path in review. - **Never touch mute/solo.** Any code path reading/writing `B_MUTE` / `I_SOLO` is a bug. - **Every leaf is managed.** The mode system owns all leaf tracks: an untagged leaf is an Arrange member, and when the active mode is not Arrange it is fully parked and snapshot-restored exactly like a tagged leaf out of its mode. **show-both** is the only way to opt a leaf out of parking. (Parents stay visibility-only, master is never touched — see below.) - **Restore from snapshot, never to a default.** No hardcoded "on" restores. - **Verify API names** against the SDK header before use. ## Open questions to resolve during build - Snapshot durability across a save-while-parked (persisted here by decision; the lean alternative is force-restore-to-Arrange on save — see product notes item 4). - Reconcile behavior when a tagged leaf is deleted or a folder restructured while 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.