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:
+146
@@ -162,3 +162,149 @@ Persistence & paths:
|
|||||||
- Audio format for the bank (wav bit depth default; allow float for wavetable
|
- Audio format for the bank (wav bit depth default; allow float for wavetable
|
||||||
fidelity).
|
fidelity).
|
||||||
- Thumbnail cache: recompute vs store peak bins alongside the index.
|
- 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 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).
|
||||||
|
|||||||
@@ -126,3 +126,101 @@ landed milestone.
|
|||||||
`int64_t → int` via `static_cast` without a range check; integers that fit
|
`int64_t → int` via `static_cast` without a range check; integers that fit
|
||||||
in int64 but exceed `INT_MAX` are implementation-defined. Hardening candidate
|
in int64 but exceed `INT_MAX` are implementation-defined. Hardening candidate
|
||||||
— add bounds check before the cast when integer-field validation is in scope.
|
— add bounds check before the cast when integer-field validation is in scope.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase D — Design View (parallel to the M0–M11 capture roadmap)
|
||||||
|
|
||||||
|
> **Separate phase namespace.** The M-numbers belong to the capture pillar
|
||||||
|
> (M0–M11, 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)
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user