Add Design View phase spec (Phase D), parallel to capture roadmap

CONTEXT.md: additive Design View technical spec section.
PLAN.md: additive Phase D (D1-D5) milestone breakdown.
docs/product/design-view.md: product framing, N-mode reasoning,
screenset differentiation, design-direction calls.
This commit is contained in:
2026-07-22 19:55:23 -04:00
parent a9ba532f07
commit 06256f05e9
3 changed files with 580 additions and 0 deletions
+146
View File
@@ -162,3 +162,149 @@ Persistence & paths:
- 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 appears in — and is never parked
in — every mode any of its descendant leaves belongs to. Rule of thumb: **tag
leaves; parents follow.** 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 — and only on tracks it tagged. 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_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_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.
- **Never touch untagged tracks.** The tool acts only on tracks it tagged (plus
derived parents' *visibility*); it never parks or restores a track it doesn't own.
- **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).
+98
View File
@@ -126,3 +126,101 @@ landed milestone.
`int64_t → int` via `static_cast` without a range check; integers that fit
in int64 but exceed `INT_MAX` are implementation-defined. Hardening candidate
— add bounds check before the cast when integer-field validation is in scope.
---
# Phase D — Design View (parallel to the M0M11 capture roadmap)
> **Separate phase namespace.** The M-numbers belong to the capture pillar
> (M0M11, M4 in flight). Design View is an orthogonal feature on its own track,
> so it uses a **lettered** namespace (D1, D2, …) rather than extending the M-line.
> This keeps the two roadmaps from colliding on numbering and reads correctly:
> Phase D is not "the twelfth capture step," it's a different pillar. Authoritative
> spec: **CONTEXT.md §Design View**. Product framing: `docs/product/design-view.md`.
> When a point lands, doc-keeper moves it to `COMPLETED.md`.
## D1 — view_model (pure)
**Goal:** REAPER-free mode registry + membership index + folder-tree-aware
visibility derivation + parking/restore planner + JSON round-trip. The heart of the
phase; mirror of `bank_model`. CONTEXT.md §Design View (Module architecture — pure).
**Verify:** CTest green. N-mode model (not a boolean); Arrange + Design seeded.
Restore-planner round-trip (snapshot → park → restore) returns every driven flag to
its captured value. Parent-derivation correct against a supplied folder tree.
JSON round-trip lossless across modes + membership + show-both + snapshots + active
mode.
- [ ] Mode registry: ordered (id, display name, ordinal); Arrange + Design seeded;
add/query more modes (prove N-mode, not binary).
- [ ] Membership index: `GUID → { mode ids }` + per-track show-both flag;
add / remove / retag / query; untagged = Arrange.
- [ ] Folder-tree-aware visibility derivation: given a supplied parent↔child tree +
active mode, compute the visible set (active leaves, derived-visible parents,
show-both leaves, master always in).
- [ ] Parking/restore planner: emit exact (track, flag, value) op-lists for park and
restore from active mode + snapshot record.
- [ ] JSON round-trip: modes + membership + show-both + snapshots + active mode.
- [ ] Tests: N-mode add/query; parent follows tagged leaf (multi-mode parent);
restore-round-trip returns snapshot values (never hardcoded "on"); show-both leaf
never parked; unknown/stale GUID tolerated; JSON lossless.
## D2 — view shell (apply flags in the DAW)
**Goal:** Read the folder tree and drive REAPER flags per the planner.
CONTEXT.md §Design View (view shell, REAPER API surface).
**Verify (in DAW):** Toggling active mode hides + parks inactive leaves
(`B_SHOWINTCP`/`B_SHOWINMIXER`/`B_MAINSEND`/`I_FXEN` + per-FX offline) and restores
active ones from snapshot. **Master untouched. `B_MUTE`/`I_SOLO` untouched.**
Untagged tracks untouched. Parents follow their tagged descendants.
- [ ] Build parent↔child tree from `I_FOLDERDEPTH`; feed to `view_model`.
- [ ] Snapshot prior flag values (`GetMediaTrackInfo_Value`) before parking.
- [ ] Apply park/restore ops (`SetMediaTrackInfo_Value` for the four flags;
`TrackFX_GetCount` + per-FX `TrackFX_SetOffline`). Verify flag names/signatures.
- [ ] GUID resolution: `GetTrackGUID` / `guidToString` / `stringToGuid` (never index).
- [ ] Review gate: no path touches master visibility or `B_MUTE`/`I_SOLO`, or any
untagged track's owned flags.
## D3 — persist slice (view state ↔ project ext state)
**Goal:** Serialize the view section into the `"reasampler"` namespace alongside the
bank; reapply the active mode on project open. CONTEXT.md §Design View (persist).
**Verify (in DAW):** Membership + active mode + snapshots survive Save / Save As /
close+reopen; on open, the active mode's visibility + processing is reapplied.
Saved-while-parked project restores parked tracks from persisted snapshots (not to a
guessed "on").
- [ ] Serialize/deserialize the view section under `"reasampler"` (shared blob,
distinct section from the bank index).
- [ ] Reapply active mode on project open (rebuild tree, run the planner).
- [ ] Confirm survival across Save / Save As; snapshot durability across
save-while-parked.
## D4 — actions
**Goal:** Bindable action set for the mode workflow. CONTEXT.md §Design View
(actions). **Verify (in DAW):** Each action registered (bindable in Actions list);
toggle + mode-jumps MIDI-bindable; tag/untag acts on the current track selection.
- [ ] Toggle active mode (cycle; extensible to cycle-all for >2 modes).
- [ ] Activate mode: Arrange / Activate mode: Design (direct jumps).
- [ ] Tag selected tracks → Design / → Arrange; Untag selected (= → Arrange).
- [ ] Show-both for selected tracks (toggle).
- [ ] Register each (`command_id`/`gaccel`/`hookcommand`); toggle + jumps MIDI-bindable.
## D5 — in-window toggle affordance (UI)
**Goal:** The segmented mode switch in the ReaSampler / bank_panel window header.
CONTEXT.md §Design View (UI). **Verify (in DAW):** Segmented control shows current
mode (lit segment), one click flips modes via the D4 toggle action, per-mode
membership count visible, offlined-FX caveat surfaced as a tooltip.
- [ ] Segmented mode switch `[ Arrange | Design ]` in the window header; active lit.
- [ ] Wire the switch to the toggle/activate actions from D4.
- [ ] Per-mode membership count display.
- [ ] Offlined-FX re-init caveat as a tooltip on the switch.
## Phase D open questions
- **Snapshot durability across save-while-parked** — persisted here by decision;
lean alternative is force-restore-to-Arrange on save (product notes item 4).
(touches D3)
- **Reconcile on delete/restructure** — tagged leaf deleted or folder restructured
while parked leaves a stale GUID; ignore-and-prune on next toggle/open.
(touches D1/D2)
- **Screenset coexistence** — Design View drives the same flags a screenset recall
would; confirm last-writer-wins is not surprising. (touches D2)
+336
View File
@@ -0,0 +1,336 @@
# Design View — product notes
Framing, rationale, and design-direction calls behind the **Design View** phase.
The tickable spec lives in `PLAN.md` (Phase D) and the authoritative technical
detail in `CONTEXT.md` (§Design View). This doc holds the *why* — the workflow
narrative, the N-mode reasoning, the screenset differentiation, and the
design-direction recommendations — so those don't clutter the build docs.
Status: settled with Daniel (2026-07-22). Open items for Daniel are listed at the
bottom.
---
## What it is (and what it is not)
**Design View is a track-visibility-plus-processing "mode" system.** Tracks used
purely for sound design — scratch oscillators, FX-mangling chains, resampling
sources — get tagged into **Design** mode. The arrangement's real tracks stay in
**Arrange** mode (the default). Toggling to a mode *hides and disables* the tracks
that don't belong to it.
The value is **mental separation plus clutter elimination**, tuned to the resample
workflow: be in Design view, resample something into the bank, flip to Arrange,
place it. The design scaffolding never touches the arrangement while you're
arranging, and the arrangement never buries the design bench while you're
designing.
**It is not a second canvas.** REAPER has exactly one arrange timeline. Design
View does not create a parallel surface, a separate window, or a duplicate
project. It is the *same timeline* with a curated, filtered track set and the
inactive tracks parked. Anyone expecting a second arrange view will be
disappointed — and that expectation must be headed off in the spec, not
discovered in review.
This is the visibility/processing analog of the capture pillar's load-bearing
rule. The capture pillar says *capture and placement are separate acts*. Design
View says *designing and arranging are separate stances on one timeline* — and the
tool enforces the separation without ever destroying the user's real state.
---
## Why not REAPER screensets? (pre-answered)
REAPER already ships screensets, which save and recall track visibility. This is
deliberately different on three axes:
1. **Live membership, not a flat snapshot.** A screenset is a re-saved picture of
"these tracks visible, those hidden" — you re-capture it every time the track
set changes. Design View is a *membership index*: tag a track into a mode once,
and it is handled forever, including tracks added later. You maintain
membership, not snapshots.
2. **It disables processing, not just visibility.** Screensets hide tracks; the
hidden tracks still run their FX and still feed the mix. Design View parks the
inactive mode's tracks: out of the mix, FX bypassed, FX taken offline to
reclaim CPU. Flipping modes is a real load change, not a cosmetic one.
3. **It is integrated with the resample workflow.** The toggle lives in the
ReaSampler window, next to the bank, driven by the same actions that capture
and place. It is part of the tool's loop, not a general window-management
feature parked in a menu.
---
## N-mode reasoning (why the model is not a boolean)
Daniel's instruction was explicit: design the data model as **N-mode**, ship the
UI with two. "Design Mode, or any other future mode."
The cost of a boolean is a **backfill cliff**: the moment a third stance appears
("Mixdown" that hides everything but buses; "Print" that isolates a stem chain),
a boolean model forces a migration of the persisted index and a rewrite of every
call site that assumed two states. The cost of N-mode *now* is trivial: a mode is
an identifier, membership is a `GUID → set<ModeId>` relation instead of a
`GUID → bool`. The visibility/parking logic is already "is this track in the
active mode?" — which is a set-membership test regardless of how many modes exist.
So: **the model carries arbitrarily many modes; the UI exposes two.** Adding a
third mode later is a UI-and-naming task, not a data-migration task. This is the
defer-the-feature, design-the-seam principle applied.
A mode is a small record: a stable id, a display name, and an ordinal for tab
order. Two seed modes ship: **Arrange** (the implicit default; every untagged leaf
belongs to it) and **Design**. Arrange is special only in that it is the fallback
home for untagged leaves — structurally it is just another mode.
---
## Membership model (the settled rules, in prose)
- **Default is Arrange.** Every leaf track belongs to Arrange unless it opts in
elsewhere. Untagged = Arrange.
- **Leaves opt in.** You tag a *leaf* track into Design (or any mode). You never
tag a parent.
- **Parents are derived, never tagged.** A folder/parent track appears in — and is
never parked in — every mode that *any* of its descendant leaves belongs to. If
a folder holds two Design leaves and one Arrange leaf, the folder shows in both
modes. Rule of thumb: **tag leaves; parents follow.**
- **Master is always visible**, in every mode, and the tool never touches its
flags. (REAPER's own SDK forbids driving `B_SHOWINTCP`/`B_SHOWINMIXER` on the
master track — the invariant and the API agree.)
- **One mode at a time, with two exceptions:** a leaf can appear in more than one
mode only via (a) the explicit *show-both* toggle, or (b) the parent-derivation
rule above. Otherwise a leaf lives in exactly one mode.
The parent-derivation rule is why the pure model must be **folder-tree aware**. It
cannot answer "should this parent be visible in mode M?" from the membership index
alone — it needs the current parent↔child structure, which the shell reads from
REAPER's `I_FOLDERDEPTH` and feeds in. The tree is an *input* to the pure model's
visibility query, not something the model stores (folder structure is REAPER's
truth and changes underneath us).
---
## What a toggle actually does (the parking contract)
Toggling to mode M partitions tagged leaves into **active** (in M, or show-both)
and **inactive** (tagged into some other mode, not M). For each track:
**Active leaves** and **derived-visible parents** → restored to their snapshot
values (see restore contract) and shown.
**Inactive leaves** → parked:
- `B_SHOWINTCP = 0` and `B_SHOWINMIXER = 0` — hidden in both panels.
- `B_MAINSEND = 0` — removed from the mix (no audio to parent).
- `I_FXEN = 0` — FX chain bypassed.
- `TrackFX_SetOffline(track, fx, true)` for **each** FX — taken offline to reclaim
CPU.
Daniel chose the **full CPU-park** option over mix-removal-only, deliberately. The
point of Design mode is a heavy FX bench you don't want taxing the CPU while you
arrange, and vice versa. Half-parking (mix out, FX still resident) would leave the
tax in place.
**Documented caveat:** offlined FX re-instantiate when the track returns to the
active mode. Stateful plugins (convolution reverbs, samplers with loaded content,
anything holding a tail or a big buffer) re-initialize on return — there may be a
load hitch and any un-persisted internal state is lost. This is an accepted cost
of the CPU reclaim, not a bug. It must be documented at the toggle affordance so
the user isn't surprised.
**Never touched:** `B_MUTE` and `I_SOLO`. The tool owns visibility, `B_MAINSEND`,
`I_FXEN`, and FX-offline — nothing else — and only on tracks it has tagged. The
user's mute/solo survives every toggle, untouched. This is the exact analog of the
capture pillar's non-destructive invariant: **the tool never destroys the user's
real state to do its job.**
---
## Non-destructive restore contract (the testable invariant)
For every flag the tool drives on a track, it **snapshots the prior value before
parking** and **restores exactly on toggle-back** — restore *from the snapshot*,
never to a hardcoded "on." If the user had `B_MAINSEND = 0` on a track for their
own reasons before it was parked, it comes back as 0, not 1.
While a track is parked/hidden, its tool-owned flags (`B_SHOWINTCP`,
`B_SHOWINMIXER`, `B_MAINSEND`, `I_FXEN`, per-FX offline) are **tool-owned**: the
user is not expected to hand-edit a hidden track's routing mid-mode, and the
contract does not promise to merge concurrent hand-edits of a hidden track. The
snapshot is the source of truth for restore.
This is enforceable and testable in the pure layer: given a snapshot record and a
target mode, the model produces the exact set of (track, flag, value) restore
operations, and a round-trip (snapshot → park → restore) returns every driven flag
to its captured value. The shell's job is only to apply those operations to
REAPER; the *decision* of what to restore to is pure and unit-tested — mirror of
how `bank_model` owns the index logic and the shell only touches the DAW.
---
## Persistence
The mode-membership index and the last-active mode ride in the existing
`"reasampler"` project ext-state namespace and travel with the `.rpp`. On project
open, the tool reapplies the active mode's visibility + processing state. Same
namespace, same travel-with-the-project guarantee as the bank index — one
`"reasampler"` blob, two logical sections (bank + view).
Snapshots (prior flag values for currently-parked tracks) also persist: if a
project is saved while in Design mode with Arrange tracks parked, reopening it must
be able to restore those Arrange tracks correctly later. A saved-while-parked
project that lost its snapshots would restore parked tracks to a guessed "on" —
violating the restore contract across a save boundary.
---
## Design-direction recommendations (opinionated)
### Naming: keep "Design View" as the feature; name modes for *stances*, not *tracks*
The feature is **Design View**. The modes are **Arrange** and **Design** — verbs-
as-stances the user adopts, not labels for what's in them. "Arrange" and "Design"
read as *what you are doing right now*, which is the whole mental-separation pitch.
Resist "Main / Scratch" or "A / B" — they describe the tracks, not the stance, and
they don't scale to a named third mode. When a third mode arrives it gets a stance
name too ("Mixdown," "Print").
### The toggle affordance: a segmented mode switch, not a checkbox
Recommendation: a **segmented control** (pill/tab switch) in the ReaSampler window
header — `[ Arrange | Design ]` — with the active segment lit. Not a checkbox, not
a menu item as the primary affordance.
Why segmented over the alternatives:
- **vs. a checkbox** ("Design mode on/off"): a checkbox hardcodes the binary the
data model is explicitly built to avoid. A segmented control grows to
`[ Arrange | Design | Mixdown ]` with no redesign — the UI mirrors the N-mode
model. Borrowed from DAW/plugin A/B/C compare switches and browser device-toolbar
responsive-mode pickers, both of which are "pick one active view from a small
set."
- **vs. a menu**: a mode switch is a *frequent, glanceable* action in this
workflow — you flip it many times a session. It wants to be always-visible and
one-click, showing current state at rest. A menu hides both the state and the
switch.
The segment also carries the current-mode indicator "for free" (the lit segment is
the state), and it is the natural home for the offlined-FX caveat as a tooltip
("switching parks the other mode's FX offline; stateful plugins re-init on
return").
Actions (below) drive the same toggle for keyboard/MIDI binding; the segmented
control is the visible, discoverable surface over those actions.
### Tagging affordance: act on the track selection, not a per-track widget
Tag/untag operates on the **current REAPER track selection** via an action (and a
context entry), not a per-track button in ReaSampler's window. "Select your design
tracks, hit *Tag selected → Design*." This matches how a user thinks ("these
tracks are my design bench") and avoids ReaSampler having to render a full track
list mirror. The bank_panel window shows a small membership summary (counts per
mode) rather than a per-track control surface.
---
## Action set (proposed)
Stable command-id strings follow the sampler family prefix chosen in M0. Default
names below; all bindable, the toggle and mode-jumps also MIDI-bindable to suit the
performance-adjacent workflow.
- **Toggle active mode** — cycle Arrange ↔ Design (extensible to cycle-through-all
when >2 modes). The segmented control's click target.
- **Activate mode: Arrange** / **Activate mode: Design** — jump directly to a named
mode (distinct from cycle; needed once there are >2, and nicer for bindings).
- **Tag selected tracks → Design** / **Tag selected tracks → Arrange** — move the
current selection's leaves into a mode. (Arrange = untag from any other mode.)
- **Untag selected tracks** — return selection to the default (Arrange), explicit
alias of "Tag → Arrange" for discoverability.
- **Show both for selected tracks** (toggle) — see *show-both semantics* below.
### Show-both semantics (resolved)
**Show-both is a per-track "pin visible across modes" flag, and it re-enables
processing whenever the track is shown.** A show-both leaf:
- appears in **every** mode's visible set (like a derived parent, but by explicit
request on a leaf);
- is **never parked** — its driven flags stay at their snapshot/restored values in
all modes, FX online, in the mix.
Rationale: the use case is a track you need audible in *both* stances — a reference
oscillator you design against and also hear in the arrangement, or a master-bus FX
chain living on a normal track. "Show but keep parked" is not a real need (a track
you can see but that's silent and offline is just clutter with a thumbnail), so
show-both means show-*and-run*. It is the deliberate escape hatch from
one-mode-at-a-time, and it costs CPU by design — that's the user's explicit call
per track.
Show-both is stored on the membership record for the track, persists with the
index, and is togglable per selection.
---
## Module breakdown (pure / shell / persist / actions / UI)
Mirrors the capture pillar's split exactly.
**Pure `view_model` (REAPER-free, unit-tested — the mirror of `bank_model`):**
- Mode registry: ordered set of modes (id, display name, ordinal); Arrange + Design
seeded; add/query more.
- Membership index: `track GUID → { mode ids }` (normally one; multiple only via
show-both), plus the per-track show-both flag. add / remove / retag / query.
- Folder-tree-aware visibility derivation: given the current parent↔child tree
(supplied by the shell) and the active mode, compute the visible set — active
leaves, derived-visible parents, show-both leaves, master always in.
- Parking/restore planner: given the active mode + a snapshot record, emit the
exact (track, flag, value) operation lists for park and for restore. Pure and
fully testable — this is where the restore-contract 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; feeds
it to the pure model.
- Applies the planner's operations: `SetMediaTrackInfo_Value` for `B_SHOWINTCP` /
`B_SHOWINMIXER` / `B_MAINSEND` / `I_FXEN`; `TrackFX_GetCount` + loop
`TrackFX_SetOffline` per FX.
- Snapshots prior flag values before parking (reads the same flags it will drive).
- Resolves track GUIDs via `GetTrackGUID` / `guidToString` / `stringToGuid` for the
index; never uses track index (unstable across reorders).
- Never touches the master track's visibility flags; never touches `B_MUTE` /
`I_SOLO` on anything.
**`persist` slice:**
- Serialize/deserialize the view section (modes + membership + show-both + snapshots
+ active mode) into the `"reasampler"` ext-state namespace alongside the bank.
- On project open, rebuild the tree, reapply the active mode.
**`actions` entries:** the set listed above, registered with the
`command_id` / `gaccel` / `hookcommand` pattern; toggle + mode-jumps MIDI-bindable.
**UI (in the ReaSampler / bank_panel window):** the segmented mode switch in the
window header; a small per-mode membership count; the offlined-FX caveat as a
tooltip on the switch.
---
## Open items for Daniel
1. **Product-notes location.** This doc was created at `docs/product/` — the first
product-notes directory in the repo. If notes belong elsewhere (`notes/`,
`docs/design/`), say so and it moves. Flagging because establishing the
directory is a project convention, not mine to assume silently.
2. **Phase namespace.** Proposed **Phase D** (a lettered namespace) rather than
M12 — see PLAN.md rationale. Confirm you're happy with letters for parallel,
non-capture phases.
3. **SDK name correction.** The brief cited `B_SHOWINMCP`; the verified SDK flag
for mixer-panel visibility is **`B_SHOWINMIXER`** (SDK header line 2235). The
spec uses the verified name. Just flagging the discrepancy so it isn't a
surprise in review.
4. **Snapshot-across-save durability.** I ruled that per-track snapshots must
persist so a saved-while-parked project restores correctly. This adds snapshot
data to the persisted blob. If you'd rather keep the persisted state lean and
accept that saving-while-in-Design-mode is an edge case (e.g., force-restore-to-
Arrange on save), that's a viable smaller alternative — flag if you prefer it.
5. **Reorder/delete robustness (deferred, not blocking).** If a tagged leaf is
deleted or a folder is restructured while parked, the index holds a stale GUID.
The pure model should tolerate unknown GUIDs (ignore-and-prune on next reconcile)
— noted as a hardening item for the build, not a phase-defining decision.