Files
reasampler/CONTEXT.md
T
daniel d9081090fd docs(multi-bank): frame Phase B — pool + named banks spec
Add the multi-bank pillar as Phase B (lettered, parallel to M-line and
Phase D): CONTEXT.md §Multi-bank spec, PLAN.md Phase B block (B1 bank_book
pure / B2 persist / B3 actions / B4 panel split), and docs/product/multi-bank.md
framing. Additive only — BankIndex, the M0–M11 roadmap, and Phase D untouched.
2026-07-23 12:52:06 -04:00

30 KiB
Raw Blame History

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 M0M11 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 M0M11 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 M0M6 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 M0M11 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.