502f3c9e30
Phantom symbols commitMapAndReload, AhdsrParams, kReaSamplerProcessorUIDBeta replaced with what shipped. Corrects two over-claims and un-ticks the D5 tooltip, which was never built.
2618 lines
174 KiB
Markdown
2618 lines
174 KiB
Markdown
# COMPLETED.md — ReaSampler landed milestones
|
||
|
||
Completed milestone entries removed from `PLAN.md`. Each entry preserves its
|
||
original Goal, Verify, and checklist points with boxes marked done.
|
||
|
||
---
|
||
|
||
## Milestone 0 — Transition scaffold: reaper_mpeview → ReaSampler
|
||
**Goal:** Retire the MPE scaffold and stand up the sampler's pure core in its
|
||
place, preserving the pure-core / REAPER-shell split.
|
||
**Verify:** `cmake -B build -S .` configures clean; `cmake --build build` builds
|
||
the renamed extension target and the pure-core test target; `ctest --test-dir
|
||
build` is green with the new `bank_model` + `peaks` suites present.
|
||
|
||
- [x] Delete `src/mpe_model.{h,cpp}` and `src/mpe_view.{h,cpp}`; remove
|
||
`tests/test_mpe_model.cpp`.
|
||
- [x] Rename the CMake `project()` and the extension MODULE target from
|
||
`reaper_mpeview` to `reaper_reasampler` (binary `OUTPUT_NAME` likewise);
|
||
update `PREFIX ""` / platform SUFFIX blocks to the new target name.
|
||
- [x] Replace the pure `mpe_model` static lib + `mpe_model_tests` executable with
|
||
`bank_model` (pure static lib) + `bank_model_tests`; keep the CTest wiring.
|
||
- [x] Repoint `src/main.cpp`: drop the `mpe_view.h` include and all `MpeView_*`
|
||
calls (toggle / IsOpen / OnTimer / Cleanup); stub the extension entry so it
|
||
loads, logs to console, and registers nothing MPE-specific. The
|
||
`command_id` / `gaccel` / `hookcommand` registration *pattern* is preserved for
|
||
reuse (CLAUDE.md §REAPER extension contract) — the MPE action string is removed.
|
||
- [x] Choose and record the persistent action-id prefix for the sampler family
|
||
(replaces `CEREBELLUM_MPEVIEW_TOGGLE`); this string is forever-stable once
|
||
shipped (CLAUDE.md §action registration).
|
||
- [x] Refresh `README.md` layout/next-step sections to the sampler module set.
|
||
(Landed-work reflection is doc-keeper's; this point exists so the stale MPE
|
||
README does not mislead the first implementer.)
|
||
|
||
---
|
||
|
||
## Milestone 1 — bank_model + JSON round-trip (pure)
|
||
**Goal:** The `Sample` metadata struct and `BankIndex` (add / remove / query /
|
||
tier moves / dedup-by-hash) with JSON serialize/deserialize to `std::string`.
|
||
CONTEXT-ARCHIVE.md §Data model, §Module architecture.
|
||
**Verify:** CTest green. Round-trip is lossless (deserialize(serialize(x)) == x)
|
||
across all fields; dedup-by-hash and tier filtering asserted; **relative paths
|
||
only** invariant enforced at the model boundary (no absolute path accepted/stored).
|
||
|
||
- [x] Define `Sample` with the full field set (id, display name, relative path,
|
||
source mode, source range in project time + PPQ, track GUID(s), wet/dry,
|
||
channels, SR, length sec + beats, capture tempo, optional key, peak/RMS/LUFS,
|
||
clip flag, tier, content hash, provenance, created ts). CONTEXT-ARCHIVE.md §Data model.
|
||
- [x] `BankIndex`: ordered collection keyed by id; add / remove / query.
|
||
- [x] Hash lookup for dedup-by-content-hash.
|
||
- [x] Tier model (scratch | archive) + tier-move + tier filtering; scratch marked
|
||
auto-prunable.
|
||
- [x] JSON serialize/deserialize to/from `std::string`.
|
||
- [x] Tests: full-field round-trip lossless; dedup collapses equal-hash adds;
|
||
tier filter/move correct; relative-path invariant rejects absolute paths;
|
||
empty-index and malformed-JSON edge cases.
|
||
|
||
---
|
||
|
||
## Milestone 2 — peaks (pure)
|
||
**Goal:** Compute waveform min/max bins from raw PCM, dependency-free (not
|
||
REAPER's peak API). CONTEXT-ARCHIVE.md §Module architecture, CONTEXT.md §Non-goals.
|
||
**Verify:** CTest green. Fed a known signal (full-scale sine, ramp), asserted
|
||
min/max envelope per bin matches expected within tolerance; channel count
|
||
preserved; bin count honored for arbitrary sample lengths (incl. remainder bin).
|
||
|
||
- [x] Min/max bin computation from interleaved PCM given a target bin count.
|
||
- [x] Multi-channel handling (per-channel envelope; no silent fold).
|
||
- [x] Tests: sine envelope ≈ ±amplitude; ramp envelope monotonic; DC/silence →
|
||
zero envelope; short-buffer and non-divisible-length edge cases.
|
||
|
||
---
|
||
|
||
## Milestone 3 — Offline capture spike (REAPER shell)
|
||
**Goal:** Offline-render the time-selection master mix to a wav in the project
|
||
bank folder, add a `Sample`, log it. The render-driving spike. CONTEXT-ARCHIVE.md
|
||
§REAPER API surface (offline render), Build order 3.
|
||
**Verify (in DAW):** Render runs via `Main_OnCommand(42230)` ("Render using most
|
||
recent settings") — REAPER always shows its offline-render progress window; no
|
||
stock/header-documented fully-headless path exists. File lands in the
|
||
project-relative bank folder at **32-bit float WAV** at project rate (lossless,
|
||
dither-free → enables bit-identical/null-test). A `Sample` is added to the
|
||
in-memory `BankIndex`. Non-destructive. Unsaved-project state triggers a
|
||
Save-As prompt; capture is refused if the user cancels (no default-location
|
||
fallback).
|
||
**Bit-identical repeats:** two identical requests produce byte-identical files.
|
||
**Exact bounds:** rendered length matches the requested range (no rounding, no
|
||
added silence without an explicit tail).
|
||
|
||
- [x] `ICaptureBackend` interface + `CaptureRequest` (source mode, time range,
|
||
wet/dry, tail, SR/bit-depth/channels, output path). CONTEXT-ARCHIVE.md §capture.
|
||
- [x] `OfflineRenderBackend`: drive `GetSetProjectInfo` render settings +
|
||
`GetSetProjectInfo_String` file/pattern/format; **verify every flag against
|
||
`vendor/reaper-sdk/sdk/reaper_plugin_functions.h`.**
|
||
- [x] Resolve the no-dialog render command/flag on the current REAPER build
|
||
(open question) and confirm it runs headless.
|
||
- [x] Populate a `Sample` from the finished file; hand to `bank_model`; console-log.
|
||
- [x] Verify bit-identical repeats and exact-bounds by hand on a known range.
|
||
|
||
---
|
||
|
||
## Milestone 4 — persist (index ↔ project ext state)
|
||
**Goal:** Write the `BankIndex` JSON to project ext state, reload on project open;
|
||
project-relative path resolution. CONTEXT-ARCHIVE.md §persist, §Persistence & paths.
|
||
**Verify (in DAW):** Index survives Save / Save As / close+reopen; **bank travels
|
||
with the .rpp**; **relative paths only** in the persisted index (Save As to a new
|
||
folder still resolves the bank).
|
||
|
||
- [x] `SetProjExtState` / `GetProjExtState` under namespace `"reasampler"`.
|
||
- [x] Bank-folder resolution from the current project path
|
||
(`EnumProjects` / `GetProjectPathEx`); store under a project-relative subfolder.
|
||
- [x] Reload-on-open; confirm survival across Save / Save As.
|
||
|
||
**Notes/decisions:**
|
||
- Storage: `SetProjExtState` / `GetProjExtState`, namespace `"reasampler"`, keys `bank_index` (serialized JSON) and `project_guid`; relative paths only in the persisted index.
|
||
- Project identity: keyed off a **minted GUID** stored in ext state (REAPER exposes no native per-project GUID), not the raw `ReaProject*` — a recycled pointer cannot misread a project switch as a Save-As.
|
||
- Save-As: **copy** semantics (Daniel's decision) — the `reasampler_bank/` folder is copied under the new `.rpp`; the old project's bank stays intact. Every ext-state write calls `MarkProjectDirty` so captures/GUID changes flush on the normal save.
|
||
- Save-As collision — fixed (DAW-verified): project identity is **GUID-primary** — the stored per-project GUID is the identity of record; a different stored GUID always means a different project (Load its bank), immune to REAPER recycling `ReaProject*` addresses across close/open. The `ReaProject*` pointer is a secondary signal that disambiguates the same-GUID case only: a different object with the same GUID = a Save-As fork (Load + re-GUID to diverge); the same object with the same GUID + a new path = a genuine Save-As in progress (relocate bank). This replaced two earlier iterations: GUID-only (mis-detected forks sharing a copied GUID) and pointer-primary (mis-detected reopen/new-project because it ignored the GUID on address recycling). Non-destructive preserved.
|
||
|
||
---
|
||
|
||
## Milestone 5 — bank_panel (docked grid)
|
||
**Goal:** Docked LICE-drawn grid: thumbnails (from `peaks`), audition,
|
||
multi-select, keyboard navigation. Reuses the docking setup from the retired
|
||
`mpe_view.cpp`. CONTEXT-ARCHIVE.md §bank_panel.
|
||
**Verify (in DAW):** Grid docks; thumbnails render from computed peaks; audition
|
||
plays selected sample; multi-select + keyboard nav work.
|
||
|
||
- [x] Docked window + LICE grid render loop.
|
||
- [x] Thumbnail draw from `peaks` bins.
|
||
- [x] Audition (play selected sample) + stop.
|
||
- [x] Multi-select + keyboard navigation.
|
||
|
||
**Notes/decisions:**
|
||
- Thumbnail cache: in-memory recompute keyed by `(sampleId, drawWidth, bankGeneration)`; peak bins are NOT persisted alongside the index. Cache is discarded on bank change and rebuilt on next draw. (Closes the PLAN "thumbnail cache" open question.)
|
||
- Audition: stock `PlayPreview` / `StopPreview` API, read-only — display + select + audition only, never inserts into the arrange. Single stop-funnel ensures a leak-free preview lifecycle. Flagged undocumented assumption: `StopPreview` detaches the source before returning; mitigated by the single-funnel design. Escalation path if a runtime pop appears: switch to `StartPreviewFade` + deferred free.
|
||
|
||
---
|
||
|
||
## Milestone 6 — insert (placement)
|
||
**Goal:** "Insert selected sample at edit cursor" via `InsertMedia`. CONTEXT-ARCHIVE.md
|
||
§insert, Build order 6.
|
||
**Verify (in DAW):** Selected sample inserts at the edit cursor wrapped in
|
||
`Undo_BeginBlock2` / `Undo_EndBlock2`; conform-to-tempo is an explicit flag —
|
||
**no silent time-stretch** when off.
|
||
|
||
- [x] `InsertMedia(path, mode)` at edit cursor (verify mode bits against SDK).
|
||
- [x] Conform-to-project-tempo vs literal as an explicit flag (never silent).
|
||
- [x] Undo-block wrapping.
|
||
|
||
**Notes/decisions:**
|
||
- Placement target: inserts the focused bank sample onto the **currently selected track(s) at the edit cursor** (Daniel's directive — not a new track). Uses `InsertMedia` base mode 0; for multiple selected tracks the sample is placed on each at the same cursor position, then the original track selection and edit-cursor position are restored — non-destructive to editing state. The entire operation is one undo block.
|
||
- No silent time-stretch: the `&4` stretch-to-time-selection bit is never set; a pure `insert_plan` test asserts this across all mode combinations. Conform-to-project-tempo is an explicit separate action (`&8`), never on the default path.
|
||
- Non-destructive to the bank: insert only adds arrange items — no bank/file/ext-state writes.
|
||
|
||
---
|
||
|
||
## D1 — view_mode_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-ARCHIVE.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.
|
||
|
||
- [x] Mode registry: ordered (id, display name, ordinal); Arrange + Design seeded;
|
||
add/query more modes (prove N-mode, not binary).
|
||
- [x] Membership index: `GUID → { mode ids }` + per-track show-both flag;
|
||
add / remove / retag / query; untagged = Arrange.
|
||
- [x] 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).
|
||
- [x] Parking/restore planner: emit exact (track, flag, value) op-lists for park and
|
||
restore from active mode + snapshot record.
|
||
- [x] JSON round-trip: modes + membership + show-both + snapshots + active mode.
|
||
- [x] 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-ARCHIVE.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.
|
||
|
||
- [x] Build parent↔child tree from `I_FOLDERDEPTH`; feed to `view_mode_model`.
|
||
- [x] Snapshot prior flag values (`GetMediaTrackInfo_Value`) before parking.
|
||
- [x] Apply park/restore ops (`SetMediaTrackInfo_Value` for the four flags;
|
||
`TrackFX_GetCount` + per-FX `TrackFX_SetOffline`). Verify flag names/signatures.
|
||
- [x] GUID resolution: `GetTrackGUID` / `guidToString` / `stringToGuid` (never index).
|
||
- [x] 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-ARCHIVE.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").
|
||
|
||
- [x] Serialize/deserialize the view section under `"reasampler"` (shared blob,
|
||
distinct section from the bank index).
|
||
- [x] Reapply active mode on project open (rebuild tree, run the planner).
|
||
- [x] Confirm survival across Save / Save As; snapshot durability across
|
||
save-while-parked.
|
||
|
||
---
|
||
|
||
## D4 — actions
|
||
**Goal:** Bindable action set for the mode workflow. CONTEXT-ARCHIVE.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.
|
||
|
||
- [x] Toggle active mode (cycle; extensible to cycle-all for >2 modes).
|
||
- [x] Activate mode: Arrange / Activate mode: Design (direct jumps).
|
||
- [x] Tag selected tracks → Design / → Arrange; Untag selected (= → Arrange).
|
||
- [x] Show-both for selected tracks (toggle).
|
||
- [x] Register each (`command_id`/`gaccel`/`hookcommand`); toggle + jumps MIDI-bindable.
|
||
|
||
**Notes/decisions:**
|
||
- New `src/actions.{h,cpp}` — the Design View action family registered via the
|
||
`command_id`/`gaccel`/`hookcommand` contract in `main.cpp`; MIDI-bindable.
|
||
- New `src/track_guid.{h,cpp}` — shared `MediaTrack*` → canonical GUID-string
|
||
formatter; used by both the view shell and the actions layer (single source of
|
||
truth for membership keys).
|
||
- Wiring: actions drive the D2 view shell and D3-persisted model; saved active mode
|
||
is reapplied on project load via a load-signal seam in `persist` (`loadFromProject`
|
||
raises it; `main.cpp`'s timer drains it) — `persist` stays model-only.
|
||
- A pure `nextModeId` free function added to `view_mode_model` (the N-mode cycle
|
||
decision behind "toggle"), unit-tested in the existing `view_mode_model_tests`.
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
- [x] Segmented mode switch `[ Arrange | Design ]` in the window header; active lit.
|
||
- [x] Wire the switch to the toggle/activate actions from D4.
|
||
- [x] Per-mode membership count display.
|
||
- [ ] Offlined-FX re-init caveat as a tooltip on the switch. (Not built — no trace in source; did not survive the Phase L panel redesign.)
|
||
|
||
**Notes/decisions:**
|
||
- New PURE module `src/mode_switch.{h,cpp}` — REAPER-free layout math for the
|
||
Design View mode switch: divides a header rectangle into N equal segments (one per
|
||
registered mode) and hit-tests a point to a segment. Unit-tested via a new
|
||
`mode_switch_tests` CTest target. Mirror of `bank_grid`.
|
||
- `src/bank_panel.cpp` — segmented `[ Arrange | Design ]` control drawn in the panel
|
||
header (one lit segment per registered mode, click activates that mode via
|
||
`view::applyMode`), with the grid offset below the header.
|
||
|
||
---
|
||
|
||
## D2-W1 — view_mode_model lane extension (pure)
|
||
**Goal:** Extend D1's pure planner to item level for the two-canvas sub-phase:
|
||
lane↔mode mapping, a managed-vs-manual lane-ownership index, managed-only item-lane
|
||
ops in the toggle planner, the "which lanes may this toggle touch" query, the
|
||
auto-tag decision (manual-lane items exempt; pre-existing ⇒ Arrange), and JSON
|
||
round-trip of the lane index. REAPER-free, unit-tested; mirror of D1. CONTEXT.md
|
||
§Two-canvas sub-phase (Module architecture — pure).
|
||
**Verify:** CTest green; D1 behavior and tests unchanged.
|
||
|
||
- [x] Lane↔mode mapping: which lane maps to which mode, which `C_LANEPLAYS` value
|
||
per mode.
|
||
- [x] Lane-ownership index: per (track GUID, lane) managed-which-mode vs manual;
|
||
managed-only item-lane op family alongside the existing track-flag op family.
|
||
- [x] "Which lanes may this toggle touch" query (managed only) — planner emits lane
|
||
ops for managed lanes only, never for manual lanes.
|
||
- [x] Auto-tag decision (pure): new track/item GUIDs + active mode ⇒ membership
|
||
writes; manual-lane items exempt; pre-existing ⇒ Arrange.
|
||
- [x] JSON round-trip of the lane-ownership index.
|
||
- [x] Tests: managed/manual partition; toggle-touches-managed-only; auto-tag
|
||
exemption for manual-lane items; JSON lossless; D1 behavior/tests unchanged.
|
||
|
||
---
|
||
|
||
## D2-W2 — shell: lane application + new-content detection
|
||
**Goal:** The view shell applies the planner's managed-lane ops in the DAW and the
|
||
bank_panel timer detects new content and auto-tags it to the active mode. Resolves
|
||
the two flagged implementation design points (I_FIXEDLANE reorder/renumber
|
||
fragility; the auto-tag / manual-lane detection heuristic). See CONTEXT.md
|
||
§Two-canvas sub-phase (Module architecture — shell; New-content detection).
|
||
**Verify (in DAW):** Toggling a mode shows + plays only the active mode's managed
|
||
lane, hides + silences the inactive-mode lane, and **never touches a manual lane**
|
||
(its `C_LANEPLAYS` stays exactly as the user set it); new content created while a
|
||
mode is active is tagged to that mode; pre-existing content stays Arrange (no
|
||
mass-tag on the first poll after open).
|
||
**Depends on:** D2-W1.
|
||
|
||
- [x] Apply managed-lane ops in the view shell (`I_FREEMODE`/`I_FIXEDLANE`/
|
||
`C_LANEPLAYS` via the item/track info setters; `UpdateTimeline()` after `I_FREEMODE`);
|
||
**managed lanes only, never manual**. `B_FIXEDLANE_HIDDEN` is read-only per the SDK and
|
||
is not written — lane visibility follows from `C_LANEPLAYS`.
|
||
Verify every flag name/signature against the SDK header.
|
||
- [x] New-content detection on the bank_panel timer: diff the live track/item GUID
|
||
set against the previous poll; tag any GUID new since the last poll to the
|
||
then-active mode, with a **first-poll-after-open guard** (pre-existing ⇒ Arrange,
|
||
no mass-tag) and the **manual-lane exemption** (items in a manual lane not tagged).
|
||
- [x] Resolve the manual-lane detection heuristic (which new items are exempt) and
|
||
the `I_FIXEDLANE` lane-identity fragility (index survival across lane
|
||
reorder/renumber/deletion) — the two open design points from CONTEXT.md.
|
||
- [x] D2-W1 review polish: document the one-managed-lane-per-mode-per-track
|
||
exclusivity assumption in `laneModeState` (comment / debug-guard); clarify the
|
||
`serialize()` one-line style note; optional round-trip tests for the
|
||
last-writer-wins lane-replace contract.
|
||
|
||
**Notes/decisions:**
|
||
- Two new pure modules added with unit tests: `guid_diff` (diffs live track/item
|
||
GUID sets between polls) and `lane_keys` (manages lane identity via durable
|
||
`P_LANENAME` rather than the renumber-prone `I_FIXEDLANE` ordinal, reconciled each
|
||
apply — the resolution to the lane-identity fragility design point). CTest green.
|
||
- **Manual-lane protection:** a single pure predicate `isOnManualLane` is the
|
||
exclusive gate; manual lanes — including REAPER's default unnamed fixed lanes —
|
||
are provably never driven or auto-tagged.
|
||
- **Track-level auto-tag and park behavior is live.** Item-lane show/hide is
|
||
correctly structured but is a provable no-op on real projects until D2-W3 mints
|
||
the `reasampler:`-prefixed named lanes. End-to-end DAW verification of item-lane
|
||
show/hide is sequenced after D2-W3 for this reason.
|
||
- W1 review polish was folded in during this wave.
|
||
|
||
---
|
||
|
||
## Milestone 7 — capture action family
|
||
**Goal:** Bindable capture actions for master / selected tracks / selected items /
|
||
razor area, each with wet-dry + tail options. CONTEXT-ARCHIVE.md §actions, Build order 7.
|
||
**Verify (in DAW):** Each action registered (bindable in Actions list), routes to
|
||
the offline backend, and honors wet/dry + tail. **Load-bearing principle:** none
|
||
auto-inserts into the arrange.
|
||
|
||
- [x] Source resolvers: master mix, selected tracks, selected items, razor area
|
||
(`GetSet_LoopTimeRange`, `P_RAZOREDITS`, `CountSelectedMediaItems`, etc.).
|
||
- [x] Register each as a bindable action (`command_id`/`gaccel`/`hookcommand`).
|
||
- [x] Wet-dry + tail options per action.
|
||
- [x] Review gate: confirm no capture path touches the timeline.
|
||
|
||
**Notes/decisions:**
|
||
- Four **wet** bindable capture actions — master mix, selected tracks, selected items, razor area — registered under the `CEREBELLUM_REASAMPLER_CAPTURE_*` command-id prefix. Each routes to `OfflineRenderBackend` (RENDER_* snapshot/restore, dither/normalize off, 32-bit float), produces a `Sample`, adds it to the bank, persists, and calls `MarkProjectDirty`. The M3 spike action was retired.
|
||
- **Wet-only decision (Daniel):** REAPER offline render has no true pre-FX "dry" bit — the only wet/dry-adjacent lever (`&8192` pre-fader stems) is post-FX. Approximate-dry action variants were removed rather than ship a "dry" that isn't. `CaptureRequest.wetDry` is retained as the seam for true dry (M10).
|
||
- Pure `render_settings` module maps source mode → RENDER_SETTINGS bits and parses `P_RAZOREDITS` (union of track-audio areas), unit-tested. No capture path inserts into the arrange (load-bearing gate); non-destructive (selection/razor read-only).
|
||
|
||
**Superseded / reworked (post-landing):**
|
||
- The four wet source-mode actions (master/tracks/items/razor) were replaced by **three FX-scope actions** — `capture item`, `capture track`, `capture master` — with range (razor-else-time-selection) inferred orthogonally. This fixed the defect where item captures were rendered through the parent FX chain.
|
||
- **FX-scope semantics (initial rework):** item = item/take FX only; track = item FX + the selected track's own track FX; master = full chain. For item/track, the out-of-scope chain (ancestors + master, plus the item's own track for item scope) is neutralized during the render.
|
||
- **Master scope subsequently removed:** capture is now **two scopes — item and track only**. `CAPTURE_MASTER` and `CAPTURE_MASTER_REALTIME` are retired (to capture the master, render a track instead). The master track is still neutralized as out-of-scope chain for both item and track captures; it is a bypass target, not a capture scope. The realtime backend taps the selected track (track scope only; item realtime deferred).
|
||
- **`FxBypassGuard` (RAII):** snapshot → neutralize (FX bypassed via `I_FXEN`; gain zeroed via `D_VOL`; pan/width/pan-law/mode set to unity via `D_PAN`/`D_WIDTH`/`D_PANLAW`/`I_PANMODE`) → render → restore. Non-destructive. This guard is the reusable mechanism M8 (realtime backend) and M10 (null-test / true dry) build on.
|
||
|
||
---
|
||
|
||
## Milestone 8 — RealtimeRecordBackend
|
||
**Goal:** Realtime record behind the same `ICaptureBackend`, producing identical
|
||
bank entries. CONTEXT-ARCHIVE.md §capture (realtime), CONTEXT.md §Precision invariants.
|
||
**Verify (in DAW):** Hidden temp track taps each selected track's own post-fader
|
||
output via a `CreateTrackSend`; recorded file moves into the bank; **non-destructive**
|
||
— temp track (and its sends) removed cleanly, every snapshotted track arm, time
|
||
selection, and edit cursor restored unchanged on every terminal path.
|
||
|
||
- [x] Track-scope tap: a `CreateTrackSend(source, temp)` from each selected track
|
||
into a hidden temp track (`B_MAINSEND=0`, hidden from TCP/mixer). The temp records
|
||
its own post-fader output — capturing each source track's output **after its own FX
|
||
and fader, before the parent/folder/master sums it** — chain-independent by
|
||
construction. No `FxBypassGuard` needed or used. Multiple selected tracks sum in the
|
||
temp track (matching offline track scope). Item realtime deferred (`UnsupportedMode`).
|
||
No track selected → refused.
|
||
- [x] Timer-driven async state machine (`begin`/`tick`/`abort` driven by `OnTimer`,
|
||
non-blocking — REAPER's UI stays responsive across the record). `begin()` validates,
|
||
snapshots all state, creates the temp track, routes the tap, arms, calls
|
||
`CSurf_OnRecord`, and **returns immediately**. `tick()` (called from `OnTimer`)
|
||
reads the transport via `GetPlayStateEx`/`GetPlayPositionEx` scoped to the record's
|
||
own `ReaProject*` (project-switch safe), advances the pure `advanceRecordPhase`
|
||
state machine, and on a terminal verdict stops + finalizes/restores. `abort()` is
|
||
the force-terminate path for shutdown and project switch.
|
||
- [x] `RealtimeCaptureState` snapshot + idempotent restore: snapshots cursor,
|
||
time selection, and every other track's `I_RECARM`; restore() is latched
|
||
(`restored_` flag) and safe to call from whichever terminal path fires first.
|
||
Terminal paths: normal completion, manual stop, error, second-capture reject,
|
||
project switch (project-scoped `OnStopButtonEx(proj_)`, never the global
|
||
`CSurf_OnStop`), **project close** (guarded by `ValidatePtr2(nullptr, proj_,
|
||
"ReaProject*")` — a closed project calls `dropWithoutRestore()` rather than
|
||
touching freed pointers), and extension unload.
|
||
- [x] `Finalizing` flush-wait before file move: after the transport stops,
|
||
`tick()` waits for the recorded file size to be positive and stable across a tick
|
||
before calling `finalizeRecording` (file is no longer being written by REAPER's
|
||
audio thread). A wall-clock ceiling (steady-clock, independent of the play cursor)
|
||
bounds both the total record duration and the flush wait separately.
|
||
- [x] Move recorded source into bank: `recordedFilePath` discovers the take's source
|
||
file from the temp track's first media item; `finalizeRecording` moves it into the
|
||
bank folder (cross-volume fallback: copy+remove); populates a `Sample` via
|
||
`sampleFromRecordedCapture`; clean teardown via `restore()` deletes the temp track
|
||
(which REAPER uses to automatically remove every send routed into it).
|
||
- [x] Dialog-free; realtime is inherently non-deterministic (documented, not asserted
|
||
bit-identical); saved-project gate (refuses + prompts Save-As if unsaved, matching
|
||
offline). Bindable **cancel** action registered. Master scope removed entirely —
|
||
to capture the master, render a track.
|
||
|
||
**Notes/decisions:**
|
||
- **Track scope only this increment.** Item realtime is deferred: item scope needs
|
||
per-item take isolation on top of the track-output tap — a separate increment.
|
||
- **TAP vs. FxBypassGuard.** The `CreateTrackSend` defaults to post-fader
|
||
(`I_SENDMODE=0`) with full-stereo (`I_SRCCHAN` default): post-fader taps the source
|
||
track after its own FX and fader/pan, before the parent sums it. The parent chain
|
||
downstream of that branch is not in the tapped path at all — so there is nothing to
|
||
neutralize and `FxBypassGuard` (which mutates the live chain, altering the user's
|
||
monitoring) is deliberately not used. This also fixed the earlier silent-file bug
|
||
from the spike, which sent FROM the master INTO a temp track (a feedback loop REAPER
|
||
refuses, recording silence). A regular track→track send has no feedback.
|
||
- **Project-close guard.** `abort()` gates every REAPER call on
|
||
`ValidatePtr2(nullptr, proj_, "ReaProject*")`. A closed project already reclaimed
|
||
its temp track, arms, and transport — `dropWithoutRestore()` latches `restored_`
|
||
and clears `temp_` / `armSnaps_` without touching any REAPER pointer.
|
||
- **No undo block.** The transient mutations (temp track, sends, arm, transport) are
|
||
fully reversed by `restore()`; surfacing them as an undo point would pollute the
|
||
user's history with an internal scaffold they cannot meaningfully undo.
|
||
|
||
---
|
||
|
||
## T1 — offline tail: auto (default) + manual override
|
||
**Goal:** Preserve decay tails on offline captures. **Auto**: render an 8 s-capped
|
||
tail, then auto-trim trailing silence to -72 dB via a **surgical** `RENDER_NORMALIZE`
|
||
(only the trim-end bit, `32768`) + a derived `RENDER_TRIMEND` amplitude ratio.
|
||
**Manual**: a fixed tail length clamped to the 8 s cap, no trim. **None** (default):
|
||
exact bounds, byte-identical to the pre-tail capture. See
|
||
`docs/product/capture-tail.md` §The offline path.
|
||
**Verify (in DAW):** A range ending mid-reverb + Auto tail ends at the -72 dB decay
|
||
point (not a hard 8 s, not the range end); a non-decaying signal caps at range + 8 s;
|
||
**two identical Auto requests are byte-identical** (deterministic trim); a
|
||
TailMode::None capture is byte-identical to the pre-tail exact-bounds capture;
|
||
Manual(N ms) yields range + N ms untrimmed, with N clamped to 8000;
|
||
`ScopedRenderSettings` restores `RENDER_NORMALIZE` and every touched setting on every
|
||
path.
|
||
|
||
- [x] Pure layer (`render_settings.{h,cpp}`): named constants `kAutoTrimThresholdDb`
|
||
(-72) + derived `RENDER_TRIMEND` amplitude ratio via `autoTrimEndRatio()` (≈
|
||
0.00025119 for -72 dB, computed as `10^(dB/20)` — `std::pow` is not `constexpr`
|
||
before C++26 so this is a function, not a constant), `kMaxTailSeconds`/`kMaxTailMs`
|
||
(8 s); `TailMode { None, Auto, Manual }` enum; `TailRenderSettings` struct
|
||
(tailFlag/tailMs/normalize/trimEnd); `tailRenderSettingsFor(mode, manualTailMs)`
|
||
mapping (None = kTailFlagNone + kNormalizeDisableAll; Auto = kTailFlagCustomBounds
|
||
+ kMaxTailMs + kNormalizeTrimEnd (32768) + autoTrimEndRatio(); Manual =
|
||
kTailFlagCustomBounds + clamped ms + kNormalizeDisableAll); unit-tested.
|
||
- [x] Wire the mapping into `OfflineRenderBackend` (`capture.cpp`): drives tail +
|
||
surgical-normalize (Auto) / disable-all (Manual/None) via `GetSetProjectInfo`;
|
||
`ScopedRenderSettings` snapshots and restores `RENDER_TRIMEND` alongside the
|
||
existing `RENDER_*` set. `RENDER_TAILFLAG = kTailFlagCustomBounds` (1) for Auto
|
||
and Manual — custom bounds is the always-applicable tail bit for offline captures.
|
||
- [x] `CaptureRequest` three-state tail contract (None/Auto/Manual(ms)); default
|
||
None (exact bounds, null-test-safe). The earlier `renderTail` bool/`tailMs` pair
|
||
was superseded.
|
||
- [x] Exposure: a **docked-panel footer toggle** (label "Tail: Off" / "Tail: Auto" /
|
||
"Tail: Manual", cycles on click via `cycleTailMode`) in `bank_panel.cpp`, backed
|
||
by the pure `tail_control` module (`TailSetting`, `cycleTailMode`,
|
||
`clampManualMs`, `tailToggleLabel` — unit-tested). Default `TailMode::None`.
|
||
`CAPTURE_ITEM` and `CAPTURE_TRACK` read the panel setting at fire time — **no
|
||
per-action tail variants shipped** (the "…with tail" variants were dropped in
|
||
favour of the toggle; null-test/verify captures use None explicitly).
|
||
- [x] DAW-confirm: `RENDER_TRIMEND` amplitude curve (0.00025119 ≈ -72 dB); trim-end-only
|
||
normalize (32768) does not engage fades/normalize/pad; trim never eats pre-`ENDPOS`
|
||
body. (See spec §Open questions / DAW-confirm.)
|
||
|
||
**Notes/decisions:**
|
||
- **Surgical normalize.** `kNormalizeTrimEnd = 32768` sets only the trim-ending-silence
|
||
bit; every other postprocessing bit is clear. A fixed-threshold trailing-silence trim
|
||
scales and fades nothing, so two identical Auto requests trim at the identical sample
|
||
→ bit-identical repeats hold (spec §surgical normalize).
|
||
- **`kNormalizeDisableAll = (4 << 16) = 262144`.** Used for None and Manual — the
|
||
same disable-all value the pre-tail exact-bounds capture used.
|
||
- **`tail_control` pure module** (`src/tail_control.{h,cpp}`): REAPER-free logic for
|
||
the panel toggle. `kDefaultManualTailMs = 2000.0` (2 s). Fine-adjust UI (scroll-wheel
|
||
in 250 ms steps) and per-project persistence landed as T1-followons (see below).
|
||
- **Follow-ons resolved:** Manual fine-adjust UI and per-project persistence of the
|
||
toggle landed as T1-followons. T2 realtime tail landed separately.
|
||
|
||
---
|
||
|
||
## T2 — realtime tail (follow-on to T1)
|
||
**Goal:** The parallel tail path for the M8 realtime backend, which does not drive
|
||
`RENDER_*`: record an 8 s-capped tail window past the range end, then **trim in a
|
||
PCM decay-scan** to the -72 dB point (Manual = record fixed tail, skip the scan).
|
||
See `docs/product/capture-tail.md` §The realtime path.
|
||
**Verify (in DAW):** A realtime Auto capture of a decaying source records ≥ the range
|
||
then trims at the -72 dB decay point (± inherent realtime tolerance); realtime tail is
|
||
**not** asserted bit-identical (documented non-determinism).
|
||
**Depends on:** T1, M8.
|
||
|
||
- [x] Record `[start, end + clamp(tail, 8 s)]` (extend the record time selection in
|
||
`capture_realtime.cpp`); Manual skips the scan, Auto proceeds to it.
|
||
- [x] Pure decay-scan helper alongside `peaks`: `lastFrameAboveThreshold(interleaved,
|
||
channels, frames, linearThreshold) -> frameIndex` (backward scan, per-frame max-abs
|
||
across channels, no fold); unit-tested with a synthetic decaying ramp. (Spec §realtime
|
||
path option (a) — recommended over bending `computeEnvelope`.)
|
||
- [x] Realtime shell: read the recorded wav PCM into a float buffer, find the trim
|
||
frame, rewrite the file truncated (new I/O the backend does not do today).
|
||
|
||
**Notes/decisions:**
|
||
- New pure module `wav_trim` (`src/wav_trim.{h,cpp}`): 32-bit-float WAV parse + header-aware
|
||
truncate plan (RIFF/data size rewrite). Rejects WAVE_FORMAT_EXTENSIBLE with non-float
|
||
SubFormat GUID. Depends on `peaks` for the `AudioSample` float alias. Unit-tested via a
|
||
new `wav_trim_tests` CTest target.
|
||
- `peaks` gained `lastFrameAboveThreshold` (backward PCM scan, per-frame max-abs across
|
||
channels, no fold) for the Auto decay scan.
|
||
- **Auto/Manual/Off semantics.** Auto: records `[start, end + 8 s cap]`, scans backward
|
||
for the last frame above -72 dBFS, truncates the WAV header-aware at that frame. Manual:
|
||
records `[start, end + fixed tail]`, skips the scan. Off: byte-identical to the pre-tail
|
||
exact-bounds capture.
|
||
- **Realtime tail is non-deterministic by design** (inherent to the realtime backend).
|
||
Bit-identical repeats are not asserted for the realtime path; this is documented, not a defect.
|
||
|
||
---
|
||
|
||
## T1-followons — Manual fine-adjust UI + per-project tail persistence
|
||
**Goal:** Close the two follow-ons deferred at T1 landing: (1) scroll-wheel fine-adjust
|
||
of the Manual tail length in the panel footer; (2) the tail setting (mode + Manual length)
|
||
persists per-project inside the `.rpp` rather than resetting on extension unload.
|
||
**Verify (in DAW):** Scroll-wheel over the footer adjusts Manual length in 250 ms steps,
|
||
clamped 0–8 s; the label reads "Tail: Manual X.Xs" (one decimal) in Manual mode; footer
|
||
click still cycles Off → Auto → Manual. The tail setting survives Save / close+reopen;
|
||
projects with no stored key fall back to Off / 2 s.
|
||
**Depends on:** T1.
|
||
|
||
- [x] `adjustManualMs(current, notches, stepMs)` pure helper in `tail_control` (per-notch
|
||
±`kManualStepMs` = 250 ms, clamped [0, `kMaxTailMs`]); unit-tested.
|
||
- [x] `tailToggleLabel` updated: Manual mode appends the clamped length in seconds to one
|
||
decimal, e.g. `"Tail: Manual 2.0s"`; unit-tested at boundary lengths.
|
||
- [x] Panel footer scroll-wheel handler calls `adjustManualMs` and repaints; click handler
|
||
unchanged (still cycles mode via `cycleTailMode`).
|
||
- [x] `serializeTailSetting` / `deserializeTailSetting` pure round-trip (mode + manualMs)
|
||
added to `tail_control`; unit-tested including `std::nullopt` on malformed input.
|
||
- [x] `TailSetting tail_` promoted into `ReaSamplerSession` (peer to `bank_` and `view_`);
|
||
`persist` serializes it under the forever-stable key `"tail_setting"` (namespace
|
||
`"reasampler"`) on save and reloads it on project open. Absent key → default Off / 2 s
|
||
(graceful for older/unsaved projects).
|
||
- [x] Changing the toggle marks the project dirty and commits the value to ext state;
|
||
`bankPanelTailSetting()` reads through the session (not a panel-local copy).
|
||
|
||
**Notes/decisions:**
|
||
- `kManualStepMs = 250.0` — Daniel-set coarse-but-precise step; one wheel notch = ± 250 ms.
|
||
- Label format: `"Tail: Manual 2.0s"` (one decimal, `s` suffix) — format pinned by unit tests.
|
||
- Default fallback on absent/malformed key: `TailSetting { TailMode::None, kDefaultManualTailMs }`
|
||
(Off mode, 2 s stored length) — graceful for projects saved before this feature shipped.
|
||
- `kProjExtTailKey = "tail_setting"` is forever-stable (changing it would orphan saved choices,
|
||
falling back to the default — graceful but lossy).
|
||
|
||
---
|
||
|
||
## D2-W3-A — lane minting + item→lane assignment + persist round-trip
|
||
**Goal:** The functional core that makes item-lanes appear: a pure `planLaneMinting`
|
||
decision (which tracks hold >1 mode's content, which managed lane each item lands on)
|
||
plus the shell apply path in `view.cpp` — enables fixed-lane mode, mints one managed
|
||
`reasampler:<mode>`-named lane per involved mode, assigns each item (including
|
||
pre-existing) to its mode's lane, and drives per-lane play state, all under one undo
|
||
block, triggered off the auto-tag detection tick. Reconciles the lane-ownership index
|
||
from durable lane names on project load before active-mode visibility is reapplied.
|
||
The lane-ownership index persists inside the `"reasampler"` `view_state` blob (rides
|
||
in `ViewModeModel::serialize()` / `deserialize()`).
|
||
**Verify:** CTest green (14/14). Pure decision unit-tested in `view_mode_model_tests`.
|
||
**DAW verification pending** (Daniel testing on dev): two behaviors are
|
||
REAPER-runtime-only — whether lane names stick when written on the same tick the track
|
||
flips to fixed-lane mode, and whether the leftover empty default lane 0 is silent.
|
||
**Depends on:** D2-W2.
|
||
|
||
- [x] Pure `planLaneMinting` decision (`view_mode_model.{h,cpp}`): for each reported
|
||
track, collect the distinct modes of managed-eligible items; if < 2 modes, no split
|
||
(D1 whole-track parking still separates stances); if ≥ 2 modes, emit one
|
||
`TrackSplit`, one `LaneMint` per involved mode (durable key = `laneNameForMode(mode)`,
|
||
owned by that mode), and one `LaneAssign` per managed-eligible item — including
|
||
pre-existing items, so a track that just gained a second mode retroactively lanes all
|
||
its content. Manual-lane items (`onManualLane = true`) are exempt at the source:
|
||
never counted, never reassigned, never minted-over.
|
||
- [x] Shell apply path `applyMintPlan` in `view.cpp`: enables `I_FREEMODE` = fixed
|
||
lanes, grows `I_NUMFIXEDLANES` (never shrinks — user's manual lanes are never
|
||
deleted), stamps each managed lane's durable name via `P_LANENAME`, records
|
||
ownership in the model (`lanes().setManaged`), assigns each item to its mode's lane
|
||
via `I_FIXEDLANE` resolved from the durable key. Returns `changed` so the Undo block
|
||
is only kept when state actually changed (idempotent re-runs produce no undo point).
|
||
- [x] Per-lane play state driven immediately after minting: `planToggle` lane ops
|
||
applied via `applyLaneOps` so the freshly-minted lanes take the correct
|
||
`C_LANEPLAYS` state for the active mode without a full `applyMode` re-run (which
|
||
would re-park/restore whole tracks — not correct for a minting tick).
|
||
- [x] `mintManagedLanes` entry point in `view.cpp`: reads live track/item picture via
|
||
`readLaneTracks`, calls `planLaneMinting`, wraps the apply in one Undo block labelled
|
||
`"ReaSampler: separate cross-mode content into lanes"`, calls `UpdateTimeline()` +
|
||
`UpdateArrange()` after a fixed-lane mode change.
|
||
- [x] `reconcileManagedLanes` in `view.cpp`: on project load, reads every fixed-lane
|
||
track's `P_LANENAME` values; for each name carrying the managed prefix, records the
|
||
lane as managed-for-its-mode in the ownership index — pure read of REAPER state, no
|
||
lane created or renamed. Called from `main.cpp`'s load path before `applyMode`.
|
||
- [x] Lane-ownership index persists via `ViewModeModel::serialize()` /
|
||
`deserialize()` — the `LaneOwnershipIndex` is a member of `ViewModeModel` and
|
||
round-trips inside the `"reasampler"` `view_state` key alongside modes, membership,
|
||
snapshots, and active mode. No new persistence key required.
|
||
- [x] Detection tick integration: `mintManagedLanes` is called from the `bank_panel`
|
||
timer after the auto-tag pass, so a newly-tagged multi-mode track is split into lanes
|
||
on the same tick the content is detected.
|
||
|
||
**Notes/decisions:**
|
||
- **Single-mode-track rule:** a track carrying content of only ONE mode is not split —
|
||
D1's whole-track parking continues to separate its stance from the other mode without
|
||
lane overhead. The lane-split only engages when a track genuinely holds ≥ 2 modes'
|
||
content.
|
||
- **Manual-lane invariant upheld at the source:** `planLaneMinting` never receives
|
||
manual-lane items as split candidates. The shell's `readLaneTracks` marks items on
|
||
manual lanes `onManualLane = true`; the pure decision skips them entirely. Managed
|
||
lanes are always appended (tail ordinals), never overwriting a user's existing lanes.
|
||
- **Idempotency:** re-reporting an already-split track produces the same plan; the
|
||
shell's ensure/assign writes are no-ops when state already matches. The Undo block is
|
||
closed with no label (discarded by REAPER) when the plan is non-empty but every write
|
||
was already satisfied, so no phantom undo points accumulate.
|
||
- **Review passed** with no Critical or Major findings.
|
||
|
||
---
|
||
|
||
## D2-W3-B — item-level mode actions + W3-A polish
|
||
**Goal:** Item-level lane/mode-management actions mirroring the track-level Design
|
||
View tag family (bindable in the Actions list), plus the three code-review polish
|
||
items carried from D2-W3-A. The persist slice and lane-ownership index round-trip
|
||
were completed in D2-W3-A; this wave closes the remaining action surface and
|
||
cleans up the implementation.
|
||
See CONTEXT-ARCHIVE.md §Two-canvas sub-phase (Module architecture — persistence).
|
||
**Verify (in DAW):** Item mode actions registered and MIDI-bindable in the Actions
|
||
list; re-drive mint/apply so each item lands on its mode's managed lane; manual-lane
|
||
items exempt; one undo block per action. ctest 14/14 green.
|
||
**Depends on:** D2-W3-A.
|
||
|
||
- [x] "Move selected items → Design" action (`CEREBELLUM_REASAMPLER_VIEW_` family):
|
||
retags selected items' membership to Design mode, re-drives the existing mint/apply
|
||
so each item lands on its mode's managed lane; manual-lane items exempt; one undo
|
||
block.
|
||
- [x] "Move selected items → Arrange" action: retags selected items' membership to
|
||
Arrange mode, re-drives mint/apply; manual-lane items exempt; one undo block.
|
||
- [x] "Untag selected items" action: removes selected items' membership, re-drives
|
||
mint/apply; manual-lane items exempt; one undo block.
|
||
- [x] All three registered (`command_id`/`gaccel`/`hookcommand`); MIDI-bindable.
|
||
- [x] W3-A polish — simplified `applyMintPlan`'s redundant `I_NUMFIXEDLANES` re-read:
|
||
single grow-and-track pass removes the second `GetMediaTrackInfo_Value` call inside
|
||
the mint loop.
|
||
- [x] W3-A polish — extracted shared item-read seam (`src/item_read.{h,cpp}`):
|
||
removes duplicated `itemGuid`/`itemLaneName` read logic from `view.cpp` and
|
||
`bank_panel.cpp`.
|
||
- [x] W3-A polish — added reconcile guard in `reconcileManagedLanes`: skips lanes
|
||
encoding an unregistered mode id (log and skip rather than silently recording an
|
||
orphaned ownership entry).
|
||
|
||
**Notes/decisions:**
|
||
- ctest 14/14 green; review passed with no Critical or Major findings.
|
||
- **Panel UI indicator explicitly deferred** (Daniel's decision): a per-track
|
||
lane-split marker has no natural cheap home in the bank panel; the mode switch
|
||
already shows the active mode. Preserved as a deferred/backlog note in PLAN.md
|
||
Phase D2 — not silently dropped.
|
||
|
||
---
|
||
|
||
# Phase V — Versioning & release
|
||
|
||
> **New pillar, own lettered namespace.** Version scheme + beta side-channel.
|
||
> Namespaced **`V` (Versioning)** alongside `M`/`D`/`B`/`R` — a distinct concern
|
||
> (build identity + channel isolation) that touches CMake, `main.cpp`'s
|
||
> forever-stable command-id contract, and the `"reasampler"` ext-state. Product
|
||
> framing + full option analysis: `docs/product/versioning-and-release.md`.
|
||
> Deploy/CD wiring (two named artifacts per platform) hands off to dev-ops.
|
||
|
||
## V1/V3 — app_version module: version constant, ext-state stamp, show-version action
|
||
**Goal:** Pure `app_version` module — single-source semver from CMake
|
||
`REASAMPLER_VERSION "0.9.01"` via `configure_file` → `version_generated.h`;
|
||
ext-state writing-version stamp under key `"version"` riding
|
||
`saveToActiveProject()`; absent stamp = silent pre-versioning; on-demand
|
||
`"ReaSampler: show version"` action (no startup print). New CTest target
|
||
`app_version_tests`.
|
||
**Verify:** CTest green. Stamp written under `"version"` key on every
|
||
`saveToActiveProject()` call. Absent key classifies as `PreVersioning` (silent).
|
||
Show-version action fires on demand only.
|
||
|
||
- [x] `app_version` pure module (`src/app_version.{h,cpp}`): exports the CMake
|
||
version string constant (`appVersion()`), the ext-state stamp value
|
||
(`stampVersion()` — numeric triple only, no channel suffix), `parseVersion`,
|
||
`versionLess`, `classifyWritingVersion` (empty → `PreVersioning`; unparseable →
|
||
`Unknown`; well-formed → `Stamped`). No REAPER types; standard library only.
|
||
- [x] `configure_file` wires `REASAMPLER_VERSION` (the one CMake variable) +
|
||
`REASAMPLER_CHANNEL_IS_BETA` into `version_generated.h` in the build tree;
|
||
`app_version` reads from there — one edit re-threads the version string through
|
||
every consumer.
|
||
- [x] Writing-version stamp: `persist` calls `SetProjExtState` under
|
||
`kProjExtVersionKey` (`"version"`) with `stampVersion()` inside
|
||
`saveToActiveProject()` on every save. Absent key on load → `PreVersioning`
|
||
(silent; graceful for pre-versioning projects).
|
||
- [x] On-demand show-version action (`channelCommandId("SHOW_VERSION")` /
|
||
`channelActionName("show version")`): prints the CMake-sourced `appVersion()`
|
||
string to the console when fired. **No unconditional startup print** (no version
|
||
line added to the extension load message).
|
||
- [x] `app_version_tests` CTest target: version parse/compare/classify round-trip;
|
||
`PreVersioning` on empty; `Unknown` on malformed; `Stamped` on well-formed;
|
||
`versionLess` numeric ordering (10 > 9, not lexicographic).
|
||
|
||
## V4 — beta-in-isolation: fully isolated coexisting binary via compile-time channel flag
|
||
**Goal:** Compile-time channel flag `-DREASAMPLER_CHANNEL=beta` → fully isolated
|
||
`reaper_reasampler_beta` binary: ext-state namespace `reasampler_beta`, FOREVER-STABLE
|
||
command-id prefix `CEREBELLUM_REASAMPLER_BETA_`, `"ReaSampler beta: "` action names,
|
||
channel-qualified dock title/ident, `0.9.01-beta` display render, bank-panel footer
|
||
version/channel readout. Stable build byte-identical to prior identity.
|
||
**Verify (in DAW):** Both binaries load simultaneously in one REAPER via the startup
|
||
dlopen. Stable produces no change to any existing action id, ext-state key, or panel
|
||
string. Beta reads/writes only `"reasampler_beta"` namespace; its actions carry
|
||
`CEREBELLUM_REASAMPLER_BETA_` prefix; its panel shows `0.9.01-beta`. No shared-state
|
||
collision path between channels.
|
||
|
||
- [x] `app_version` extended as the single source of truth for channel identity (V4):
|
||
`channel()`, `isBeta()`, `extStateNamespace()`, `commandIdPrefix()`,
|
||
`actionDisplayPrefix()`, `binaryName()`, `dockTitle()`, `dockIdent()` — all derived
|
||
from the one `REASAMPLER_CHANNEL_IS_BETA` bit. Stable values byte-identical to
|
||
pre-V4 build.
|
||
- [x] `channelCommandId(suffix)` / `channelActionName(phrase)` composition helpers:
|
||
every action-registering shell funnels through these so no shell re-implements the
|
||
channel-qualified concatenation. FOREVER-STABLE per channel.
|
||
- [x] `configure_file` threads `REASAMPLER_CHANNEL_IS_BETA` (0 for the default build,
|
||
1 for `-DREASAMPLER_CHANNEL=beta`) alongside the version string. Beta binary name,
|
||
namespace, prefix, and display suffix all derive from this one bit.
|
||
- [x] All shells (`main.cpp`, `actions.cpp`, `bank_panel.cpp`, `persist.cpp`) updated
|
||
to compose ids/names via `channelCommandId`/`channelActionName` and read
|
||
`extStateNamespace()` — no scattered `#ifdef` forks in the shells.
|
||
- [x] Bank-panel footer version/channel readout: displays `appVersion()` (stable:
|
||
`"0.9.01"`, beta: `"0.9.01-beta"`).
|
||
- [x] The lane-name `reasampler:` prefix is deliberately NOT channel-qualified (shared
|
||
naming convention; ownership isolated by namespace).
|
||
- [x] Stable build: byte-identical to pre-V4 identity on every string that was
|
||
previously shipped.
|
||
|
||
**Notes/decisions:**
|
||
- The stamp value (`stampVersion()`) is the numeric triple only on BOTH channels —
|
||
no `-beta` suffix in the stamp. The channel is carried by the isolated namespace
|
||
(`extStateNamespace()`), not baked into the stamp, so the stamp parses as `Stamped`
|
||
on read-back and stable's stamp is byte-identical regardless of channel build.
|
||
- Two permanent commitments accepted: a second forever-stable command-id prefix
|
||
(`CEREBELLUM_REASAMPLER_BETA_`) and a second ext-state namespace
|
||
(`"reasampler_beta"`). Beta keybindings are a distinct forever-family from stable's.
|
||
- Isolation semantics (accepted, not a bug): a channel reads/writes only its own
|
||
namespace — a stable project looks empty/default when opened in beta, and vice versa.
|
||
No cross-namespace read, migration, or fallback.
|
||
- Deploy implication (dev-ops hand-off): two named artifacts per platform
|
||
(`reaper_reasampler` + `reaper_reasampler_beta`), built by toggling
|
||
`-DREASAMPLER_CHANNEL`.
|
||
|
||
---
|
||
|
||
# Phase B — Multi-bank (parallel to the M0–M11 capture roadmap and Phase D)
|
||
|
||
> **Separate phase namespace.** The M-numbers belong to the capture pillar
|
||
> (M0–M11); the D-letters belong to Design View. Multi-bank is a third orthogonal
|
||
> pillar — generalizing the single bank into a pool + named banks — so it takes its
|
||
> own **lettered** namespace (B1, B2, …). "B" reads for **Banks** and, like Phase D,
|
||
> keeps the roadmaps from colliding on numbering: Phase B is not "the twelfth
|
||
> capture step," it is a different pillar. Authoritative spec: **CONTEXT.md
|
||
> §Multi-bank**. Product framing: `docs/product/multi-bank.md`.
|
||
|
||
## B1 — bank_book (pure)
|
||
**Goal:** REAPER-free bank registry wrapping N `BankIndex` instances: pool seeded +
|
||
privileged, create/rename/reorder/delete named banks, active-bank id, move/copy a
|
||
sample between banks, JSON round-trip + legacy-migration. The heart of the phase;
|
||
mirror of `bank_model` / `view_mode_model`; **`BankIndex` untouched (additive)**.
|
||
CONTEXT-ARCHIVE.md §Multi-bank (Module architecture — pure).
|
||
**Verify:** CTest green. Pool always present, un-deletable, un-renamable,
|
||
un-evacuable (rules rejected in-model). Active-bank defaults to pool. Move is
|
||
index-only (source loses entry, destination gains it) and observes destination
|
||
collapse-by-hash; copy leaves source intact. Delete drops member index entries.
|
||
Evacuate moves all members to the pool, leaving the bank empty. JSON round-trip
|
||
lossless across pool-as-bank-zero + named banks + per-bank indices + ordinals +
|
||
active id. Legacy `bank_index` JSON parses into `{ pool }` with zero named banks.
|
||
|
||
- [x] Bank registry: ordered `{ bank id, display name, ordinal, BankIndex }`; pool
|
||
seeded with fixed id + fixed name; create / rename / reorder / delete named banks
|
||
(delete drops the bank's member index entries).
|
||
- [x] Pool-privilege rules enforced in-model: reject delete-pool, reject
|
||
rename-pool, reject evacuate-pool, never allow zero banks.
|
||
- [x] Active-bank id (get/set; defaults to pool); resolve active bank's `BankIndex`.
|
||
- [x] Move sample between banks (index-only; destination collapse-by-hash observed;
|
||
source entry removed).
|
||
- [x] Copy sample between banks (index-only; source entry retained; destination
|
||
collapse-by-hash observed).
|
||
- [x] Evacuate bank: move every member to the pool (index-only; destination
|
||
collapse-by-hash observed), leaving the bank empty; pool cannot be evacuated.
|
||
- [x] JSON round-trip: pool-as-bank-zero inside the blob + named banks + per-bank
|
||
indices + ordinals + active id.
|
||
- [x] Legacy migration: a bare `bank_index` JSON promotes to the pool's index with
|
||
zero named banks (one-way, lossless; blob authoritative thereafter).
|
||
- [x] Tests: pool privileges (delete/rename/evacuate rejected); move
|
||
source-loses/dest-gains; copy source-retained; evacuate empties source into pool
|
||
with dest collapse; cross-bank same-hash coexistence; dest collapse on move into a
|
||
bank already holding the hash; JSON lossless; legacy migration.
|
||
|
||
> **Phase-B-wide undo (fork R-B, settled 2026-07-24 — batched REAPER undo points).**
|
||
> Every index verb across B1–B5 (create/rename/reorder/delete-bank, move, copy,
|
||
> evacuate, remove) wraps its bank/index mutation in a **batched REAPER undo point**
|
||
> (`Undo_BeginBlock` / `Undo_EndBlock`), so one bank operation is one Ctrl-Z. This is
|
||
> a cross-cutting decision that retro-touches B1–B4, not a B5-local one; the
|
||
> per-verb points above inherit it. **Must-verify before build:** confirm against
|
||
> `vendor/reaper-sdk` that `"reasampler"` ext-state mutations participate correctly
|
||
> in `Undo_BeginBlock`/`Undo_EndBlock` undo blocks — the whole approach depends on
|
||
> it. See CONTEXT.md §Sample removal (Guardrails) + product notes §Fork R-B.
|
||
|
||
**Notes/decisions (R-B — Phase-B-wide undo, landed):**
|
||
- Every bank index verb — bindable action AND panel gesture (menu/drag/Delete key) —
|
||
is one batched REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`,
|
||
`UNDO_STATE_MISCCFG`); ext-state participates in undo via `UNDO_STATE_MISCCFG`
|
||
("extensions!"), SDK-verified. A `projectconfig` hook
|
||
(`BeginLoadProjectState(isUndo)`) triggers a deferred session reload so Ctrl-Z/redo
|
||
visibly restores book/view/tail/manifest in-session. Rejected/no-op ops open no
|
||
undo point; unsaved-project ops discard the empty block.
|
||
|
||
---
|
||
|
||
## B-cap — owned-file manifest seam (capture writes; prune consumes in Phase R)
|
||
**Goal:** Capture writes each file it creates into an **owned-file manifest**
|
||
persisted in the `"reasampler"` ext-state, so Phase R prune can later distinguish
|
||
the bank system's own orphans from hand-dropped files. Consumed only in Phase R
|
||
(R1/R2) — landed early here because reconstructing the manifest retroactively is a
|
||
backfill cliff (fork R-D, settled 2026-07-24: *defer the feature, design the seam*).
|
||
CONTEXT.md §Prune (Settled decisions — orphan attribution) + product notes §Fork R-D.
|
||
**Verify:** every file the capture path creates is recorded in the owned-file
|
||
manifest; the manifest round-trips through the `"reasampler"` ext-state (Save / Save
|
||
As / reopen); relative-paths-only preserved. Prune's consumption of it is Phase R.
|
||
**Depends on:** the capture add-path (M7) + persist blob machinery (M4 / B2).
|
||
|
||
- [x] Capture records each created file into an owned-file manifest (the set of
|
||
files the book has created), persisted in the `"reasampler"` ext-state (sibling
|
||
`owned_files` key — persistence shape resolved at build time: sibling key, not
|
||
folded into the `banks` blob).
|
||
- [x] Manifest round-trips: survives Save / Save As / reopen via the M4 blob
|
||
machinery; relative-paths-only. (Consumed by Phase R R1/R2 — not consumed here.)
|
||
|
||
**Notes/decisions:**
|
||
- Pure `owned_manifest` module (`src/owned_manifest.{h,cpp}`): relative paths, dedup,
|
||
JSON round-trip. Deliberately decoupled from `bank_book` — tracks files created, not
|
||
index membership; sample-remove is not manifest-remove. Unit-tested via new
|
||
`owned_manifest_tests` CTest target. Persisted under the `"owned_files"` ext-state
|
||
key. Both capture commit paths (offline + realtime) record created files. Joins the
|
||
undo-reload set.
|
||
|
||
---
|
||
|
||
## B2 — persist slice (banks ↔ project ext state)
|
||
**Goal:** Serialize the book under the `banks` key in `"reasampler"` alongside the
|
||
existing sections, with the pool folded in as bank-zero; migrate a legacy
|
||
`bank_index` key into the pool on first load and retire the legacy key; reload-on-open
|
||
and Save-As survival via the existing M4 machinery. CONTEXT-ARCHIVE.md §Multi-bank (persist).
|
||
**Verify (in DAW):** Banks + named banks + active bank + all per-bank samples survive
|
||
Save / Save As / close+reopen; **relative paths only**; bank travels with the `.rpp`;
|
||
a project saved before this phase (legacy `bank_index` only) loads as pool + zero
|
||
named banks with no sample loss, and after save carries `banks` with no `bank_index`
|
||
written.
|
||
**Depends on:** B1. (Persistence-key fork settled — fork 1 (a): pool inside the
|
||
`banks` blob, legacy key retired after one-way migration.)
|
||
|
||
- [x] Serialize/deserialize the book under the `banks` key (pool-as-bank-zero inside
|
||
the blob; distinct section from `view_state`; no `bank_index` key written going
|
||
forward).
|
||
- [x] Legacy-migration path on load: absent `banks` + present `bank_index` → promote
|
||
into pool, mint the blob, treat blob as authoritative (legacy key retired).
|
||
- [x] Session exposes the book; the active bank's `BankIndex` is the capture add
|
||
target (route the M7 capture family through it — additive to M7, no M7 rewrite).
|
||
- [x] Confirm survival across Save / Save As; confirm legacy-project load path.
|
||
|
||
---
|
||
|
||
## B3 — actions
|
||
**Goal:** Bindable action set for the multi-bank workflow. CONTEXT-ARCHIVE.md §Multi-bank
|
||
(actions). **Verify (in DAW):** Each action registered (bindable in Actions list);
|
||
bank-activate + move/copy + evacuate MIDI-bindable; create/rename/delete/evacuate
|
||
drive the B1 model via the B2-persisted session.
|
||
**Depends on:** B1, B2.
|
||
|
||
- [x] Create bank / rename bank / delete bank (delete drops member index entries;
|
||
confirm-on-non-empty offered at the UI layer in B4).
|
||
- [x] Evacuate bank → pool (move all members back to the pool; refuses on the pool).
|
||
- [x] Activate bank (direct-by-id + cycle).
|
||
- [x] Move selected samples → bank / copy selected samples → bank (move is default).
|
||
- [x] Pool full-height / banks full-height toggles.
|
||
- [x] Register each (`command_id`/`gaccel`/`hookcommand`); bank-activate + move/copy
|
||
+ evacuate MIDI-bindable.
|
||
|
||
---
|
||
|
||
## B4 — bank_panel vertical split (UI)
|
||
**Goal:** The vertical-split bank window — pool on top, named-banks tab-page region
|
||
below, full-height toggles — extending the M5 docked grid. CONTEXT-ARCHIVE.md §Multi-bank
|
||
(bank_panel). **Verify (in DAW):** Pool grid renders on top; named-banks tab strip
|
||
below (empty when no named banks, one tab per named bank); active-bank **unmistakably**
|
||
indicated; both full-height toggles collapse the split correctly; sample move/copy
|
||
affordance works; non-empty delete confirms and offers evacuate; the Design View mode
|
||
switch in the header is unaffected.
|
||
**Depends on:** B1, B2, B3. (Tab rendering + move-affordance mechanics — fork 5 —
|
||
settled 2026-07-23: LICE-drawn tabs + both move affordances; see Phase B open questions
|
||
and product notes → *Fork 5 — settled*.)
|
||
|
||
- [x] Vertical split: pool grid region (top) + named-banks tab-page region (bottom).
|
||
- [x] Named-banks tab strip: **LICE-drawn** (matching the M5 grid + Design View
|
||
segmented switch, not SWELL-native — fork 5a); one tab per named bank; empty state
|
||
when none.
|
||
- [x] Tab-strip overflow/scroll affordance (fork 5a): scroll/chevron overflow shipped
|
||
with the strip.
|
||
- [x] Pool full-height / banks full-height toggle affordances wired to B3.
|
||
- [x] Active-bank indicator — **visually unmistakable** (settled constraint).
|
||
- [x] Create / rename / delete / activate / evacuate affordances driving B3 actions.
|
||
- [x] Delete confirms on a non-empty bank, naming the evacuate alternative.
|
||
- [x] Sample move affordance — **both** (fork 5b): a "move to bank" menu on the current
|
||
selection (bindable front-end for the B3 move action) **and** drag-between-regions.
|
||
Copy is the deliberate secondary act, offered on the menu.
|
||
- [x] Drag mis-drop mitigation (fork 5b): clear drop-target highlighting on the
|
||
destination region/tab during a drag.
|
||
|
||
**Notes/decisions:**
|
||
- New pure module `src/tab_strip.{h,cpp}`: named-banks tab-strip geometry (B4) —
|
||
strip rect + N tabs + scroll offset → per-tab rects (overflow-clipped), overflow
|
||
chevron reservation + maxScroll, and point → tab/chevron hit-test. Unit-tested via
|
||
`tab_strip_tests` CTest target. Mirror of `mode_switch`.
|
||
- Active-bank indicator placement (the open polish detail from the Phase B open
|
||
questions) was resolved at build time in the panel implementation.
|
||
- Post-landing: m11's console-chatter policy applied to Phase B messages (successes
|
||
silent, failures kept).
|
||
|
||
---
|
||
|
||
## B5 — sample-remove (the missing sample-level verb)
|
||
**Goal:** Drop an individual `Sample`'s index entry from a bank or the pool —
|
||
the sample-level companion to move/copy/evacuate/delete-bank. Index-only,
|
||
non-destructive to the file; exposes the `BankIndex::remove` primitive that
|
||
`bank_model` already has (wires it, does not add it). CONTEXT.md §Sample removal.
|
||
Product framing + open forks: `docs/product/removal-and-prune.md` §Sample-remove.
|
||
**Verify (in DAW):** Remove drops the selected sample's entry from the target
|
||
bank; a same-hash entry in another bank is untouched (no cross-bank dedup);
|
||
pool *contents* are removable while pool-container privileges hold; removing the
|
||
last index reference to a file leaves that file on disk (orphaned until prune —
|
||
never deleted by remove); non-destructive (index + ext-state only, no file, no
|
||
timeline item).
|
||
**Depends on:** B1, B2, B3 (action set), B4 (panel affordance).
|
||
|
||
- [x] Surface `BankIndex::remove` through `bank_book`: remove a `Sample` from a
|
||
bank's index; pool contents removable, pool-container privileges unchanged.
|
||
- [x] "Remove selected sample(s)" action (`command_id`/`gaccel`/`hookcommand`),
|
||
MIDI-bindable; carries a `scope: this-bank | all-banks` seam (fork R-A, settled
|
||
2026-07-24: **this-bank** is the default and only surfaced affordance; all-banks
|
||
stays a latent seam-only parameter, not shipped).
|
||
- [x] `bank_panel` remove affordance on the current selection (reuse M5 selection
|
||
model, as move/copy do).
|
||
- [x] Silent remove: no confirm dialog; recoverability via batched REAPER undo
|
||
(R-B) — one Ctrl-Z restores the index entry; files are never deleted by remove.
|
||
- [x] Tests: remove drops the target entry; same-hash entry in another bank
|
||
survives; remove-from-pool allowed; last-reference remove leaves an orphan (file
|
||
untouched); non-destructive (no file/timeline mutation).
|
||
|
||
**Notes/decisions (B5 forks — settled 2026-07-24):**
|
||
- **R-A — remove scope.** Settled: **this-bank**. Removes the entry from the bank in
|
||
view only; the `scope: this-bank | all-banks` seam stays in the action signature
|
||
but all-banks is a latent parameter, not a surfaced verb.
|
||
- `hashReferencedElsewhere` cross-bank reference query in `bank_book` retained as
|
||
a tested model API for Phase R prune; the remove shells no longer call it.
|
||
- Both `bank_panel` context menu ("Remove selected sample(s)") and Delete key
|
||
affordance wired; panel gesture is also one batched undo point (R-B applies).
|
||
|
||
---
|
||
|
||
## Milestone 10 — provenance (re-capture from source)
|
||
**Goal:** Populate `Sample.provenance` (parent sample id + a capture-recipe
|
||
fingerprint) on resample-from-sample, and ship a **"re-capture from source"**
|
||
action that regenerates a sample from its recorded source. Reconciled with the
|
||
dual-canvas (Phase D2) model. CONTEXT-ARCHIVE.md §Data model, §capture; product framing +
|
||
the settled reconciliation in `docs/product/provenance.md`.
|
||
**Verify (in DAW):** A sample resampled from a bank sample carries its parent id +
|
||
recipe fingerprint; "re-capture from source" regenerates the file into the bank
|
||
(never auto-inserting into the timeline — load-bearing principle); re-capture with
|
||
an unchanged source + request is byte-identical to the original (bit-identical
|
||
repeats); non-destructive to source items/tracks.
|
||
|
||
> **Reshaped from the old "provenance + null-test verify" M10.** **Cut (fixed by
|
||
> Daniel):** the null-test verification *action* and the true-pre-FX-dry *mechanism*
|
||
> the old note required — both dropped, see `docs/product/provenance.md` §What was
|
||
> cut. **Kept:** provenance + re-capture. The `Sample.provenance` struct and its JSON
|
||
> round-trip **already exist** (M1) — M10 populates and consumes the field, it does
|
||
> not add it. Fork picks settled by Daniel (2026-07-23): **P1=a thin fingerprint,
|
||
> P2=a bank-only re-capture**; P3/P4 moot under P2=a.
|
||
|
||
- [x] Populate `Sample.provenance` on resample-from-sample: `parentSampleId` (the
|
||
bank sample the capture derived from) + `fxChainSnapshot` as a **thin capture-recipe
|
||
fingerprint** (scope + source FX-chain identity/hash at capture time — a drift/repro
|
||
fingerprint, NOT a serialized pre-FX-dry chain to restore; P1=a settled).
|
||
- [x] "Re-capture from source" action (`RECAPTURE_FROM_SOURCE`, channel-composed):
|
||
regenerate a provenanced sample by re-running its recorded capture request against
|
||
the source's **current** state; update the bank file + Sample in place
|
||
(`BankIndex::updateInPlace` / `BankBook::updateSampleInPlace` — order-preserving,
|
||
id-stable; old file becomes a Phase R orphan). **Bank-only — never inserts/re-places
|
||
into the timeline** (load-bearing principle). Reports drift if the source changed
|
||
since capture.
|
||
- [x] Verify: re-capture of an unchanged source is byte-identical to the original
|
||
capture (bit-identical repeats); non-destructive (`FxBypassGuard` snapshot/restore
|
||
as M7); relative-paths-only preserved.
|
||
|
||
**Dual-canvas reconciliation (settled — `docs/product/provenance.md`):** With
|
||
bank-only re-capture (P2=a), provenance is **pure per-sample bank metadata**,
|
||
`bank_model` and `view_mode_model` **stay decoupled**, and M10 touches **no** canvas
|
||
code. Dual-canvas compliance is satisfied by staying on the right side of the
|
||
capture-never-places line — not by any new coupling. Forks P3 (canvas/lane memory in
|
||
provenance) and P4 (re-capture auto-tag interaction) were only live under
|
||
re-capture-and-replace (P2=b) and are **closed as moot**; if the user manually
|
||
re-places a regenerated sample, the existing D2 mode-aware placement rule governs.
|
||
|
||
**Notes/decisions:**
|
||
- Pure `provenance` module: `rsprov1` length-prefixed encoding; captures scope, exact
|
||
range, tail, rate/channels, track GUIDs, order-sensitive FX-chain identity;
|
||
parse/compare for drift detection. NOT a serialized chain to restore.
|
||
- `provenance_shell`: FX-chain identity queries via `TrackFX_*` / `TakeFX_*`;
|
||
source-item path collection; parent-detection inputs. Item scope fingerprints take
|
||
FX via `TakeFX_*`; track scope fingerprints track FX. Stamps `Sample.provenance`
|
||
when every resolving source item maps by exact normalized path (case-folded on
|
||
Windows) to exactly one bank sample — ambiguous/mixed cases conservatively record
|
||
nothing.
|
||
- **Cut items (correct per spec, not built):** the null-test verification action and
|
||
the pre-FX dry path. Both were explicitly removed at M10 reshaping
|
||
(`docs/product/provenance.md` §What was cut).
|
||
- New CTest target `provenance_tests`.
|
||
|
||
---
|
||
|
||
## Phase B open questions — all resolved
|
||
|
||
All five forks settled by Daniel (2026-07-23): persistence key = fold pool into `banks`,
|
||
retire legacy key (1a); delete drops members + add evacuate verb (2); move is the
|
||
default gesture (3); active-bank/shown-tab distinct with an unmistakable indicator (4);
|
||
LICE-drawn tabs + overflow, and both move affordances with drop-highlighting (5).
|
||
Active-bank indicator placement (the one residual polish detail) was resolved at
|
||
build time. B5 forks R-A and R-B settled 2026-07-24 (see B5 and B1 notes above).
|
||
Both in `docs/product/removal-and-prune.md` §Fork R-A / §Fork R-B.
|
||
|
||
---
|
||
|
||
# Phase R — Reclaim (file lifecycle: the prune path)
|
||
|
||
> **New pillar, own lettered namespace.** Prune is the file-lifecycle path the
|
||
> capture and multi-bank specs forward-reference throughout ("files persist on disk
|
||
> until prune") but that had no phase, module, or point. It is the **only** operation
|
||
> in ReaSampler that deletes bytes off disk. Namespaced **`R` (Reclaim)** alongside
|
||
> `M`/`D`/`B` because it is a distinct pillar — it serves *every* orphan-producing
|
||
> path (delete-bank, sample-remove B5, potentially M10 re-capture), not just
|
||
> Multi-bank, and it carries a new risk class (file deletion) with its own
|
||
> invariants. Authoritative spec: **CONTEXT.md §Prune — file-lifecycle spec**.
|
||
> Product framing + phase-placement justification + forks:
|
||
> `docs/product/removal-and-prune.md` §Prune.
|
||
>
|
||
> **Boundary (load-bearing):** *remove creates orphans; prune reclaims them.* No
|
||
> operation other than prune deletes a file; prune deletes only files no index
|
||
> references. A bank op that deletes a file is still a bug.
|
||
>
|
||
> **Depends on:** B1, B2 (needs the multi-bank book to union the referenced-set
|
||
> across all banks) and B5 conceptually (sample-remove is a primary orphan-producer,
|
||
> so remove-then-prune is the coherent pair — mirror of evacuate-then-delete). Does
|
||
> **not** depend on the B3/B4 UI.
|
||
|
||
## R1 — prune-reconcile core (pure)
|
||
**Goal:** REAPER-free, filesystem-free reconciler — given the files present in the
|
||
bank folder, the files referenced by the book (unioned across all banks, pool
|
||
included), and the **owned-file manifest** (fork R-D, written from capture onward by
|
||
B-cap), compute the orphan set `(owned ∩ present) − referenced`. The mirror of
|
||
`ViewModeModel::reconcile(liveGuids)`, one level down (files instead of GUIDs).
|
||
CONTEXT-ARCHIVE.md §Prune (Module architecture — pure).
|
||
**Verify:** CTest green. **Prune null test:** a folder whose every file is
|
||
referenced deletes nothing; prune returns exactly `(owned ∩ present) − referenced`
|
||
and nothing else. Referenced-set unioned across every bank (a file referenced by any
|
||
bank — including via a copy — is never an orphan); a present-but-not-owned file (a
|
||
hand-dropped file) is never an orphan.
|
||
|
||
- [x] Prune-reconcile pure function: `(present, referenced, owned) → orphans`,
|
||
computing `(owned ∩ present) − referenced`; referenced unioned across the whole
|
||
book (copies keep a file alive).
|
||
- [x] Tests: prune null test (all-referenced → empty); orphan = (owned∩present)−
|
||
referenced; a copied file referenced by a second bank survives; a present-but-
|
||
unowned (hand-dropped) file is never reclaimed; empty folder / empty book / empty
|
||
manifest edge cases.
|
||
|
||
**Notes/decisions:**
|
||
- New pure module `src/prune_reconcile.{h,cpp}`: exports `pruneOrphans(present,
|
||
referenced, owned)` (the safety-critical set algebra), `buildPruneReport`
|
||
(count/bytes/display-capped list, unit-testable), and `pruneDeletePlan` (the R3
|
||
confirm-time staleness intersection — `confirmed ∩ freshOrphans` in confirm order).
|
||
Exact-string path match throughout (no case-folding, no separator normalization).
|
||
`BankBook::referencedPaths()` additive const union query (all banks incl. pool,
|
||
de-duped) added to `bank_book`. New `prune_reconcile_tests` CTest target.
|
||
|
||
---
|
||
|
||
## R2 — prune shell + persist wiring (filesystem I/O, thin)
|
||
**Goal:** Enumerate the current project bank folder (M4 project-relative resolution),
|
||
supply the referenced-set and the **owned-file manifest** (from B-cap) from the
|
||
session, feed the pure core, and produce a dry-run manifest. No deletion in this
|
||
wave — the report path only. CONTEXT-ARCHIVE.md §Prune (persist / prune shell).
|
||
**Verify (in DAW):** Dry-run reports the orphan count + reclaimed size (+ file list
|
||
for a small set) against the resolved current bank folder; resolves paths the same
|
||
way the index does (survives a Save-As relocation); deletes nothing.
|
||
**Depends on:** R1, B1, B2.
|
||
|
||
- [x] Prune shell: enumerate the resolved current bank folder; feed the pure core.
|
||
- [x] Session supplies the referenced-set (union across the book) **and the
|
||
owned-file manifest** (written by B-cap); resolve the bank folder via the M4
|
||
project-relative machinery.
|
||
- [x] Dry-run manifest: orphan count + reclaimed size (+ files for a small set);
|
||
**no deletion in this wave.**
|
||
|
||
**Notes/decisions:**
|
||
- `ReaSamplerSession::pruneDryRun()` (read-only, non-throwing) enumerates the
|
||
resolved current bank folder, unioning `book().referencedPaths()` and
|
||
`owned().paths()`, feeds `pruneOrphans`, and calls `buildPruneReport` with a 64-file
|
||
display cap. Pure `bankRelativeForName` (`capture_paths`) normalizes the folder-
|
||
enumeration spelling to match the index convention so the pure core's exact-string
|
||
match lines up. Forever-stable `BANK_PRUNE_FOLDER` action registered (dry-run report
|
||
to console in R2; deletion wired in R3 behind the same action id).
|
||
|
||
---
|
||
|
||
## R3 — deletion + action (the destructive step, guarded)
|
||
**Goal:** The confirmed deletion step, the bindable "Prune bank folder" action, and
|
||
a `bank_panel` prune button: dry-run-first, confirm-with-manifest, then reclaim the
|
||
orphan set — via OS trash where portably available (fork R-C), else unlink.
|
||
CONTEXT.md §Prune (guardrails) + CONTEXT-ARCHIVE.md §Prune (API).
|
||
**Verify (in DAW):** "Prune bank folder" (action or panel button) reports first,
|
||
deletes only on explicit confirm, and reclaims exactly the orphan set — never a
|
||
referenced file, never a hand-dropped non-bank file; the referenced/owned-set safety
|
||
holds; deletions route to OS trash where available; non-bank and capture invariants
|
||
untouched.
|
||
**Depends on:** R2 (and B-cap's owned-file manifest). All forks settled 2026-07-24.
|
||
|
||
- [x] "Prune bank folder" action (`command_id`/`gaccel`/`hookcommand`),
|
||
dry-run-first, confirm-to-delete.
|
||
- [x] `bank_panel` prune button (fork R-E) that fires the "Prune bank folder"
|
||
action through the existing command-id contract — the panel affordance alongside
|
||
the bindable action; split: button hit-test/layout is pure (mirror of
|
||
`mode_switch`/`bank_grid`), draw + dispatch is bank_panel shell.
|
||
- [x] Deletion mechanism (fork R-C, settled trash-preferred): route to OS trash
|
||
where a portable move-to-trash is verified available, else unlink behind the
|
||
dry-run/confirm guardrail.
|
||
- [x] Orphan attribution (fork R-D, settled owned-file manifest): reclaim only
|
||
`(owned ∩ present) − referenced` — the bank system's own leavings, never a
|
||
hand-dropped folder file. (Manifest written by B-cap; consumed via R1/R2.)
|
||
|
||
**Notes/decisions:**
|
||
- Deletion is guarded: dry-run → REAPER `ShowMessageBox` confirm (count+bytes+files)
|
||
→ `pruneDeletePlan` staleness intersection (confirmed ∩ fresh pure-core output) →
|
||
delete exactly the plan. Zero ext-state writes, no undo point (file deletion is not
|
||
REAPER-undoable by design).
|
||
- **Windows:** routes to Recycle Bin via `SHFileOperationW` + `FOF_ALLOWUNDO`
|
||
(verified against SDK 10.0.26100). **macOS / Linux:** no portable SWELL trash
|
||
surface; falls back to `unlink` behind the dry-run/confirm guardrail.
|
||
- Manifest entries are deliberately NOT removed on deletion (the owned-file manifest
|
||
algebra self-cleans: a deleted file will drop from `present` on the next prune scan,
|
||
and `pruneOrphans` returns `(owned ∩ present) − referenced` — the absent file
|
||
contributes nothing regardless).
|
||
- New pure module `src/prune_button.{h,cpp}`: layout (`computePruneButton`) and
|
||
hit-test (`hitTestPruneButton`) for the footer prune button, right-anchored,
|
||
suppressed gracefully when the footer is too narrow. Mirror of `mode_switch` /
|
||
`tab_strip`. New `prune_button_tests` CTest target.
|
||
- `bank_panel` footer button dispatches `BANK_PRUNE_FOLDER` via `Main_OnCommand`
|
||
through the registered command id (the same action as the bindable menu entry —
|
||
no duplicate logic).
|
||
|
||
---
|
||
|
||
## Milestone 11 — polish (wave 1)
|
||
**Goal:** Batch capture (per selected item / per razor area), action trigger buttons
|
||
+ keybinding help labels, conform-on-insert. CONTEXT-ARCHIVE.md Build order 11.
|
||
**Verify (gates 23/23 both configs):** CTest green on new pure targets; each
|
||
in-panel action fires through the command-id contract without regressing precision
|
||
invariants.
|
||
|
||
- [x] **Batch capture** (`batch_capture` pure module + `CAPTURE_BATCH_ITEMS` /
|
||
`CAPTURE_BATCH_RAZOR` actions): plan source ranges → ordinal units; mixed-result
|
||
aggregation; actions fire one bank sample per selected item / per razor area;
|
||
transient per-unit selection with RAII restore; per-unit invariants + provenance;
|
||
single persist per batch; one summary line. `RunCapture` internals extracted to a
|
||
shared `captureAndIndexOne` helper (behavior identical). New CTest target
|
||
`batch_capture_tests`.
|
||
- [x] **Action trigger buttons + keybinding help labels** (`action_buttons` pure
|
||
module + bank_panel strip): strip layout/hit-test with min-width overflow-hiding
|
||
and label formatting with "(unbound)" fallback; struct `ActionButtonRect`; a 28px
|
||
LICE button strip in bank_panel between the split body and tail footer fires
|
||
capture item/track, realtime start/cancel, insert native/conform, re-capture-from-
|
||
source via `NamedCommandLookup` + `Main_OnCommand`; labels show live bindings via
|
||
`kbd_getTextFromCmd`. Coexists with Phase R's prune button (footer). New CTest
|
||
target `action_buttons_tests`.
|
||
- [x] **Conform-on-insert** — verified already shipped (both insert variants were
|
||
registered actions since the insert milestone); no new code. Closes as
|
||
verified-extant.
|
||
- ~~[ ] **Resample-and-mute-source** — Cut (fixed by Daniel, 2026-07-26).~~ Rationale:
|
||
the Design View mode projection (park/hide inactive-mode content) supersedes the
|
||
mute-after-capture workflow; a mute action would be redundant with the dual-canvas
|
||
architecture. Mirror of the null-test cut precedent ("Cut (fixed by Daniel)").
|
||
|
||
## Milestone 11 — polish (wave 2 / completion)
|
||
**Goal:** Native OS drag-out — the final M11 polish item. CONTEXT-ARCHIVE.md Build order 11,
|
||
§Non-goals (drag-out deferred to last).
|
||
**Verify (gates 24/24 both configs):** Drag-out places a valid file in the OS target
|
||
without regressing the precision invariants; copy-only semantics throughout (no source
|
||
deletion on drop); internal move/copy drag unchanged.
|
||
|
||
- [x] **`drag_out` pure module** (gesture-boundary decision): internal drag becomes
|
||
OS-bound when the pointer leaves the panel client rect; path-list assembly with
|
||
dedupe and missing-file skip. No REAPER types at the boundary. New CTest target
|
||
`drag_out_tests`.
|
||
- [x] **`drag_out_win` shell** (Windows): OLE `DoDragDrop` / `CF_HDROP`. Copy-only
|
||
structurally — `DROPEFFECT_MOVE` is not offered and no source-deletion path exists;
|
||
prune remains the sole file-deleter. macOS/Linux via `SWELL_InitiateDragDropOfFileList`
|
||
with a documented copy-semantics caveat (SWELL does not expose a drop-effect query).
|
||
- [x] `bank_panel` additive hook only — internal move/copy drag unchanged.
|
||
|
||
**Notes/decisions:**
|
||
- Copy-only is structural, not a policy flag: `DROPEFFECT_MOVE` is never offered on
|
||
Windows, so the OS never signals a move. The SWELL path cannot query drop-effect;
|
||
copy semantics are documented as a known caveat for macOS/Linux.
|
||
- Prune remains the sole authority for deleting files off disk; drag-out does not
|
||
remove the source file or any bank index entry.
|
||
- This wave completes **Milestone 11** in full.
|
||
|
||
## Phase R forks — settled 2026-07-24
|
||
- **Fork R-C — deletion mechanism.** Settled: **trash-preferred, unlink fallback.**
|
||
Route to OS trash where a portable move-to-trash is available (recoverable), else
|
||
unlink behind strong dry-run/confirm. Per-platform trash surface verified at build:
|
||
Windows `SHFileOperationW` + `FOF_ALLOWUNDO` (SDK 10.0.26100); macOS/Linux no
|
||
portable SWELL trash surface → unlink fallback. Folded into R3.
|
||
- **Fork R-D — orphan attribution.** Settled: **owned-file manifest**,
|
||
`(owned ∩ present) − referenced`; folder-sweep rejected as unsafe. **Seam lands
|
||
early** — the manifest is written from capture onward (new **B-cap** point in
|
||
Phase B), not reconstructed at prune time; R1/R2 consume it. Persistence shape
|
||
(sibling `"reasampler"` key vs. `banks` blob) resolved at build time: sibling
|
||
`"owned_files"` key.
|
||
- **Fork R-E — trigger.** Settled: **manual action + `bank_panel` button**,
|
||
dry-run-first, confirm-to-delete. No background sweep. The earlier optional
|
||
delete-time "…and prune now" convenience was **not** selected — out of scope.
|
||
Folded into R3.
|
||
|
||
Both docs of record: `docs/product/removal-and-prune.md` §Fork R-C/R-D/R-E and
|
||
CONTEXT.md §Prune (Settled forks).
|
||
|
||
---
|
||
|
||
# Phase L — Look-and-feel (system-wide visual design language)
|
||
|
||
> Separate phase namespace. Namespaced **`L` (Look-and-feel)**, orthogonal to and ungated
|
||
> by the M/D/B/R/V/S pillars. Authoritative spec: **CONTEXT.md §Phase L**. Product
|
||
> framing + settled decisions (DS-1/DS-2/DS-3): `docs/product/visual-design-language.md`.
|
||
|
||
> **New pillar, own lettered namespace, taken up by a parallel team.** Phase L is the
|
||
> whole-system look-and-feel effort: a shared LICE drawing kit and the surfaces that
|
||
> adopt it, so ReaSampler and ReaSampler 9000 shed the flat "temple os" drawing for a
|
||
> modern, sleek 2026 dark synth look. It answers Daniel's post-DAW-test verdict on the
|
||
> instrument ("this looks like temple os… the VST is dogshit… scope it for the whole
|
||
> system… does Cockos have a toolkit?"). Namespaced **`L` (Look-and-feel)** so it is
|
||
> orthogonal to and ungated by the M/D/B/R/V/S pillars — a parallel team owns it while
|
||
> Phase S feature work proceeds independently. Authoritative spec: **CONTEXT.md §Phase L
|
||
> — visual design language (design-system spec)**. Product framing, the settled decision
|
||
> record (DS-1/DS-2/DS-3 all SETTLED 2026-07-26), palette, and the three visual
|
||
> directions: `docs/product/visual-design-language.md`.
|
||
>
|
||
> **L1 (shared LICE drawing kit — the foundation), L2 (dock-panel layout redesign), L3
|
||
> (VST editor + embed-strip restyle), L4 (dock-panel button layout enhancement), L5
|
||
> (dock-panel button refinements), L6 (toolbar polish), and L7 (capture ordering, card
|
||
> metadata, and selection styling) have all landed** —
|
||
> `theme`/palette module, `component_geometry` geometry/hit-test helpers, `draw_kit` shell, GDI
|
||
> `DrawText` retirement in `bank_panel` (L1); `action_bar` pure task-grouped layout module, full
|
||
> M11-aware button inventory placed by task cluster, `bank_panel` redesigned through the L1 kit
|
||
> (L2); VST editor (`reasampler_editor.cpp`) + embed strip (`reasampler_embed.cpp`) restyled
|
||
> through the L1 kit — REAPER-grey neutrals + three pastel accents, pastel spectral keyboard
|
||
> strip + zone bars, hover/pressed/drag states, local `kCol*` forest-green palette retired (L3);
|
||
> three-zone layout (top capture/placement/maintenance toolbar, bottom Design-View toolbar,
|
||
> footer toggle + Tail button + Prune), `footer_bar` pure module, `ActionCluster::Tagging`/
|
||
> `Switching` in `action_bar` (L4); top-bar overflow menu (`overflow_menu` pure module),
|
||
> custom LICE-kit hover-delay tooltips (`tooltip` pure module), opposite-mode Item/Track tag
|
||
> buttons + Show Both, Toggle + Activate-Arrange/Design buttons removed, grouping spacing
|
||
> widened (`mode_enable` pure module) (L5); single-row button faces, keybinding in tooltip,
|
||
> Cancel RT moved to overflow, top-bar cluster order tidied (L6); per-bank `SlotMap`
|
||
> (id→slot) in `bank_book`, sparse-grid rendering, `card_drag` + `card_meta` pure modules,
|
||
> `captureTimeSigNum`/`captureTimeSigDenom` on `Sample`, tertiary-border selection (L7). See
|
||
> individual entries above. **Phase L is complete.**
|
||
>
|
||
> **This section is self-contained for a team without Phase S context.** Where a point
|
||
> touches a Phase S surface (the VST editor, the embed strip, the keyboard strip), the
|
||
> gate is stated explicitly so the team does not chase files that are not on dev yet.
|
||
>
|
||
> **Settled decisions (Daniel, 2026-07-26 — see `docs/product/visual-design-language.md`
|
||
> §6):**
|
||
> - **DS-1 — toolkit: LICE + WDL free game, no external frameworks.** Draw the modern
|
||
> look with LICE directly; reuse any useful WDL/vwnd piece (skin/image helpers, draw
|
||
> idioms, a control like the scroll listbox) where it beats re-deriving — "don't
|
||
> reinvent the wheel." Reject iPlug2 / JUCE / VSTGUI (external frameworks re-opening the
|
||
> settled bare-SDK+LICE build shape). Keep hit-test geometry in pure CTest-covered
|
||
> modules — do not import vwnd's retained-mode object model wholesale.
|
||
> - **DS-2 — visual direction: Direction B ("Neon Console") + Direction C's spectral
|
||
> keyboard strip. SETTLED 2026-07-26, REVISED 2026-07-26 (Daniel) — palette-only.**
|
||
> *Neutral surfaces (revised):* the neutral ladder moved **from near-black up into REAPER's
|
||
> mid-grey theme family** so the dock reads as part of REAPER, not a black slab — `bg/base`
|
||
> ≈ `#2b2b2b`, `bg/panel` ≈ `#333333`, `bg/cell` ≈ `#3a3a3a`, `line/hairline` ≈ `#4a4a4a`,
|
||
> `text/primary` ≈ `#dcdcdc`, `text/dim` ≈ `~#a0a0a0`+ (elevation-ladder discipline
|
||
> unchanged). *Accent layer (revised):* now a **three-accent pastel system** —
|
||
> `accent/primary` pastel lime green (live/active/selected), `accent/secondary` pastel teal
|
||
> + `accent/tertiary` pastel purple (categorical distinctions) — replacing the original
|
||
> single electric cyan. The spectral keyboard strip is a **pastel** sweep anchored on the
|
||
> three accents. *Tight WCAG pairs to re-verify against the grey ladder:* `text/dim`-on-grey
|
||
> (mid-grey-on-mid-grey, AA 4.5:1) and the three pastels-as-indicators on `bg/cell` (shrunk
|
||
> from ~15:1 to ~6:1–7:1). **A stylish/bundled-font upgrade was considered and DECLINED
|
||
> (Daniel):** no font bundling/redistribution — the kit keeps its current cached-font face,
|
||
> no new typeface. The kit palette stays abstract (roles, one constants block — three accent
|
||
> roles + grey neutrals), so the direction is a single-file change; final hex is locked
|
||
> against the theme WCAG tests within the pastel intent. Detail:
|
||
> `docs/product/visual-design-language.md` §2.1/§4/§6 + CONTEXT.md §Phase L.
|
||
> - **DS-3 — dock-panel scope: a thorough layout redesign, not a light re-skin.** L2 lays
|
||
> out the full button inventory (including M11's action-button additions) intuitively,
|
||
> uncluttered, and useful — then applies the kit. Sequenced after M11 merges.
|
||
|
||
## Phase L — sequencing
|
||
```
|
||
L1 (shared kit) ──► L2 (dock-panel layout redesign) [LANDED]
|
||
├─────────► L3 (VST editor + embed-strip restyle) [LANDED]
|
||
└─────────► L4 (dock-panel button layout enhancement) [LANDED]
|
||
└────► L5 (dock-panel button refinements; ungated, after L4) [LANDED]
|
||
└────► L6 (toolbar polish; ungated, after L5) [LANDED]
|
||
└────► L7 (capture ordering + card metadata + selection styling; ungated, after L6) [LANDED]
|
||
```
|
||
L1, L2, L3, L4, L5, L6, and L7 have all landed. **Phase L is complete.**
|
||
|
||
## Phase L — must-verify-before-build
|
||
- **LICE design-kit surfaces (L1)** — `LICE_GradRect`, `LICE_RoundRect`, AA
|
||
`LICE_Line`/`LICE_FLine`/`LICE_ThickFLine`/`LICE_Circle`/`LICE_FillCircle`/
|
||
`LICE_DrawCBezier`, `LICE_FillTriangle`/`FillTrapezoid`/`FillConvexPolygon`, and the
|
||
`LICE_CachedFont`/`LICE_IFont` font engine (`SetFromHFont`, AA `DrawText`, shadow/outline/
|
||
glow FX flags). Verified *present* in `vendor/WDL/WDL/lice/lice.h` + `lice_text.h`
|
||
(design-language doc §1.1); **confirm exact signatures + the `LICE_CachedFont`↔`HFONT`
|
||
lifecycle at build.**
|
||
- **WDL/vwnd reuse assessment (DS-1)** — at build time, evaluate whether a vwnd piece beats
|
||
re-deriving it: `virtwnd-slider.cpp` / `vwnd_slider_drawknobstack` as the slider/knob
|
||
drawing reference, `virtwnd-listbox.cpp` as a candidate scroll listbox, `virtwnd-controls.h`
|
||
for `WDL_STYLE_*` gradient hooks, `virtwnd-skin.h` for image-skin helpers. Reuse where
|
||
useful; keep hit-test geometry pure regardless.
|
||
- **M11 button inventory (L2)** — resolved at L2 build: inventory taken against dev after
|
||
M11 merged; all buttons placed by task cluster in the landed `action_bar` module.
|
||
- **L4 re-home surface (L4)** — resolved at L4 build: three-zone layout confirmed against the
|
||
post-palette-revision `bank_panel`; `action_bar` extended with `ActionCluster::Tagging` +
|
||
`Switching`; new pure `footer_bar` module covers footer layout/hit-test.
|
||
- **L5 refinement surface (L5)** — resolved at L5 build: `TrackPopupMenu` overflow menu
|
||
confirmed; tooltip mechanism resolved as **custom LICE-kit hover-delay tooltip** (`tooltip`
|
||
pure module; sourced from the registered action phrase, prefix stripped at draw time); item-move
|
||
and track-tag action ids confirmed; active-mode read confirmed via the same
|
||
`view().activeModeId()` the footer toggle uses.
|
||
- **L7 model + tempo surface (L7)** — **RESOLVED at build (L7 landed).** Forks F1/F2/F3 all
|
||
confirmed: `TimeMap_GetTimeSigAtTime` confirmed at build for the meter stamp; SWELL stock
|
||
cursors chosen for drop-result cues (Reorder→IDC_SIZEALL, Move→IDC_HAND, Copy→IDC_UPARROW,
|
||
Replace→IDC_SIZEWE); existing pool-privilege guard reused as-is for Alt-replace. Gap navigation
|
||
in the grid = skip gaps (arrow keys skip empty slots). See individual entries §L7 above.
|
||
|
||
## L1 — shared LICE drawing kit (the foundation)
|
||
**Goal:** Stand up the shared LICE-based drawing kit that every Phase L surface (L2 dock
|
||
panel, L3 VST editor) consumes — palette/theme module, pure component geometry/hit-test
|
||
helpers, LICE draw shell, and retirement of the GDI `DrawText` path in `bank_panel`.
|
||
CONTEXT-ARCHIVE.md §Phase L (Kit architecture). DS-1 (LICE + WDL, no external frameworks) and
|
||
DS-2 (Direction B Neon Console + Direction C spectral) are the governing settled decisions.
|
||
**Verify:** CTest green (`theme_tests`, `component_geometry_tests`). Each text-on-surface
|
||
pair in the palette clears its WCAG floor (tested). `bank_panel` text routes through
|
||
cached-font `text()` — GDI `DrawText` path retired. Double-buffer discipline preserved.
|
||
|
||
- [x] **`theme`/palette module** (`src/theme.{h,cpp}`): role→color mapping via one
|
||
constants block (DS-2 revised — REAPER-grey neutral ladder + three-accent pastel
|
||
system: `bg/base #2b2b2b` / `bg/panel #333333` / `bg/cell #3a3a3a` / `line/hairline
|
||
#4a4a4a` / `text/primary #dcdcdc` / `text/dim #a8a8a8`; `accent/primary` pastel lime
|
||
`#B0E098` = live/active/selected, `accent/secondary` pastel teal `#84D6D0` +
|
||
`accent/tertiary` pastel purple `#C2AAE8` = categorical distinctions); `Role` enum
|
||
carries the three accent roles; `roleColor`/`roleColorState` updated (Active/Dragging/
|
||
Focus → primary accent); `spectralColor` is a pastel three-stop sweep anchored on the
|
||
three accents (lime → teal → purple); WCAG contrast-floor helpers + tests (text/dim-on-grey
|
||
AA body; three pastels on bg/cell + bg/panel at the 3:1 floor); interaction-state color
|
||
model. Pure; no LICE types. New CTest target `theme_tests`.
|
||
- [x] **`component_geometry` module** (`src/component_geometry.{h,cpp}`): button/slider/
|
||
list-row geometry + hover hit-test. Pure; no LICE or REAPER types. New CTest target
|
||
`component_geometry_tests`.
|
||
- [x] **`draw_kit` shell** (`src/draw_kit.{h,cpp}`): LICE draw layer — `fillSurface`
|
||
(micro-gradient + inner highlight/shadow), `drawButton`/`drawSlider`/`drawListRow`/
|
||
`drawWaveform`, cached-font `text()` over four `LICE_CachedFont`s (kit-owned lifecycle),
|
||
full interaction-state model, double-buffer preserved.
|
||
- [x] **GDI `DrawText` retirement in `bank_panel`**: all panel text now routes through the
|
||
kit's cached-font `text()`; raw GDI `DrawText` path retired (the single biggest
|
||
"temple os → modern" lever).
|
||
|
||
---
|
||
|
||
## L2 — dock-panel layout redesign
|
||
**Goal:** Full layout redesign of the `bank_panel` dock window — task-grouped action bar,
|
||
full M11-aware button inventory placed by cluster, entire panel drawn through the L1 kit.
|
||
CONTEXT-ARCHIVE.md §Phase L (L2 scope). DS-3 (thorough layout redesign, not a light re-skin) is the
|
||
governing settled decision; sequenced after M11 merged.
|
||
**Verify:** CTest green (`action_bar_tests`). The panel renders in the settled B+spectral
|
||
language through the L1 kit — chrome, buttons, tabs, grid cells, dividers all by palette
|
||
role with hover on interactive elements; remaining GDI text retired; single
|
||
`KitColor→LICE_pixel` boundary via the kit's `toLice` (exposed in `draw_kit.h`). Prune
|
||
button remains footer-set-apart + `warn`-colored; grid stays the centerpiece.
|
||
|
||
- [x] **`action_bar` pure module** (`src/action_bar.{h,cpp}`, `tests/test_action_bar.cpp`,
|
||
CTest target `action_bar_tests`): task-grouped action-bar layout — clusters (Capture /
|
||
Placement / Maintenance), per-button label + keybinding micro sub-rects, whole-trailing-
|
||
button overflow, point→index hit-test. Pure; no LICE or REAPER types. Mirror of
|
||
`mode_switch`/`bank_grid`/`action_buttons`.
|
||
- [x] **`bank_panel` redesigned**: full M11-aware button inventory placed and grouped by
|
||
task cluster (Capture: capture item/track, batch items, batch razor, realtime; Placement:
|
||
insert + insert-conform; Maintenance: re-capture, cancel-realtime); prune remains footer-
|
||
set-apart + `warn`-colored; grid stays the centerpiece. Entire panel draws through the L1
|
||
kit (chrome, buttons, tabs, grid cells, dividers) by palette role with hover on
|
||
interactive elements; remaining GDI text retired; single `KitColor→LICE_pixel` boundary
|
||
via the kit's `toLice` (now exposed in `draw_kit.h`).
|
||
|
||
---
|
||
|
||
## L4 — dock-panel button layout enhancement
|
||
**Goal:** Re-home the `bank_panel`'s L2 button inventory around *frequency and intent* —
|
||
three-zone structure: top toolbar (capture + placement + maintenance), bottom toolbar (Design
|
||
View tagging + switching), footer (narrow mode toggle · Tail button · Prune). A layout
|
||
re-home of buttons that fire existing actions; no new actions, no capture/placement behavior
|
||
change, no touching the "capture ≠ placement" principle. Independent of L3.
|
||
CONTEXT-ARCHIVE.md §Phase L (L4 dock-panel button layout). Product framing:
|
||
`CONTEXT-ARCHIVE.md` §L4 dock-panel button layout.
|
||
**Verify (in DAW):** the top toolbar fires every capture + placement + maintenance action; the
|
||
bottom toolbar tags/untags selected tracks and switches/toggles Arrange/Design/show-both; the
|
||
footer shows a narrow `[Arrange|Design]` toggle at the left, a Tail button that cycles tail
|
||
on click with button states, and the Prune button set apart at the right in `warn`; every
|
||
surface draws through the L1 kit in the DS-2 grey+pastel palette; capture and placement still
|
||
never auto-insert (the buttons only fire the existing, unchanged actions).
|
||
|
||
- [x] **Top toolbar**: capture cluster (capture item, capture track, batch items, batch razor,
|
||
capture RT) + placement cluster (insert, insert-conform) + maintenance cluster (re-capture,
|
||
cancel-realtime) moved from the L2 bottom bar to a top toolbar via `action_bar` row layout;
|
||
buttons fire existing actions unchanged — no auto-insert.
|
||
- [x] **Bottom toolbar**: Design View action family as buttons (tag / untag selected for mode,
|
||
activate Arrange, activate Design, toggle active mode, show-both) via `action_bar`
|
||
`ActionCluster::Tagging` + `ActionCluster::Switching`; fires existing registered Design View
|
||
actions.
|
||
- [x] **Footer toggle**: `[Arrange|Design]` segmented toggle shrunk to fit-its-text width and
|
||
moved to the footer left of Prune; per-mode count as a compact adjacent label.
|
||
- [x] **Footer Tail button**: Tail click-zone converted to a proper kit button (rest/hover/
|
||
pressed states; click still cycles the tail setting).
|
||
- [x] **Footer order** (`[Arrange|Design]` · Tail · … · Prune `warn` set apart at the right);
|
||
pure footer-strip layout in new `footer_bar` module covered by CTest (`footer_bar_tests`).
|
||
|
||
**Notes/decisions:**
|
||
- **Maintenance cluster restored to top toolbar** (Daniel's directive during L4 build): the
|
||
initial L4 spec described the top toolbar as "capture + placement" only; the landed
|
||
implementation includes re-capture and cancel-realtime in a Maintenance cluster on the same
|
||
top toolbar. CLAUDE.md updated to reflect the actual layout.
|
||
- New pure module `src/footer_bar.{h,cpp}`: footer layout/hit-test (narrow mode toggle +
|
||
Tail button + Prune); no LICE or REAPER types. New CTest target `footer_bar_tests`.
|
||
- `action_bar` gained `ActionCluster::Tagging` and `ActionCluster::Switching` for the
|
||
bottom-toolbar Design View verb groups.
|
||
|
||
---
|
||
|
||
## L5 — dock-panel button refinements
|
||
**Goal:** Refine the L4 three-zone toolbar so the button faces read cleanly and group
|
||
legibly — an overflow menu for the rare capture variants, short faces with full-name
|
||
tooltips (no `ReaSampler:` prefix), an opposite-mode tag-button set, removal of the now-
|
||
redundant Toggle and Activate-Arrange/Design buttons, and semantic-grouping spacing.
|
||
**No new capture/placement behavior**; every button fires an existing registered action
|
||
(the "capture ≠ placement" principle is untouched). Ungated by Phase S; sequences after L4.
|
||
**Verify (in DAW):** the top bar shows only frequent capture/placement/maintenance buttons +
|
||
a right-anchored More (⋯) menu that fires Batch Items / Batch Razor / Capture RT; hover
|
||
tooltip shows the full action name with `ReaSampler:` prefix stripped; the bottom bar shows
|
||
four Item/Track × Arrange/Design tag buttons with only the opposite-mode pair live (disabled
|
||
pair visibly greyed via the kit `Disabled` state) and no Toggle button; cluster groups read
|
||
as groups. All buttons fire the same actions their keybindings do.
|
||
|
||
- [x] Top-toolbar overflow: Batch Items / Batch Razor / Capture RT pulled off the visible bar
|
||
into a right-anchored **More (⋯) menu button** (kit-drawn button + `TrackPopupMenu` popup);
|
||
each entry fires its existing command id. Pure layout owns the menu-button rect + hit-test
|
||
(`overflow_menu` pure module); the popup + dispatch is shell.
|
||
- [x] Short faces + drop `ReaSampler:` prefix on the button *face*; keep the keybinding micro
|
||
sub-row.
|
||
- [x] **Hover tooltip carrying the full action name (prefix stripped)** via a **custom LICE-kit
|
||
hover-delay tooltip** (`tooltip` pure module): sourced from the registered action phrase
|
||
(not `kbd_getTextFromCmd`); `ReaSampler:` prefix stripped at draw time; tooltip box width
|
||
clamped to the client so it never overhangs a narrow dock. The keybinding sub-row still uses
|
||
the live binding from `kbd_getTextFromCmd`.
|
||
- [x] Bottom-toolbar four tag buttons: **Item: Arrange / Item: Design / Track: Arrange /
|
||
Track: Design**, wired to the existing item-move (`VIEW_MOVE_ITEMS_ARRANGE` /
|
||
`VIEW_MOVE_ITEMS_DESIGN`) + track-tag (`VIEW_TAG_ARRANGE` / `VIEW_TAG_DESIGN`) actions.
|
||
- [x] **Opposite-mode enablement:** a button is live iff its target mode ≠ the active mode;
|
||
otherwise drawn `Disabled` (kit disabled state, `TextDim`) and its click is a no-op. Pure
|
||
predicate (`mode_enable` pure module) unit-tested; shell reads `view().activeModeId()` once
|
||
per draw and applies.
|
||
- [x] **Activate-Arrange / Activate-Design / Toggle buttons removed** from the bottom toolbar
|
||
(all three actions stay registered; footer toggle owns mode switching). **Show Both** kept
|
||
as a set-apart button on the bottom toolbar.
|
||
- [x] Semantic-grouping spacing widened: `clusterGap` 16→24 (`buttonGap` remains 4; 6:1 ratio)
|
||
on both toolbars so clusters read as groups.
|
||
|
||
**Notes/decisions:**
|
||
- **Activate-Arrange / Activate-Design / Toggle FORK resolved (Daniel):** all three removed
|
||
from the bottom toolbar. The footer `[Arrange|Design]` toggle is the single mode-switch
|
||
affordance; the bottom bar is tagging + Show Both only.
|
||
- **Tooltip mechanism resolved as custom LICE-kit hover-delay tooltip** (DS-1 "keep drawing in
|
||
the kit"): avoids attaching a SWELL tooltip control to non-child LICE rects. SWELL is the
|
||
Win32-emulation layer for macOS/Linux and is not the mechanism used here; on Windows the
|
||
path is native, and the chosen implementation is a custom kit-drawn tooltip.
|
||
- New pure modules: `src/overflow_menu.{h,cpp}` (menu-button geometry/reserve/hit-test),
|
||
`src/mode_enable.{h,cpp}` (opposite-mode enablement predicate), `src/tooltip.{h,cpp}`
|
||
(placement + prefix-strip). New CTest targets `overflow_menu_tests`, `mode_enable_tests`,
|
||
`tooltip_tests`.
|
||
|
||
---
|
||
|
||
## L6 — toolbar polish (in-DAW feedback refinement on L5)
|
||
**Goal:** Polish pass on the L5 dock-panel state based on Daniel's in-DAW feedback —
|
||
single-row button faces, keybinding surfaced in the hover tooltip, Cancel RT moved into
|
||
the overflow menu, and visible top-bar cluster order tidied. No new modules, no new test
|
||
targets, no capture/placement behavior change.
|
||
|
||
- [x] **Single-row button faces:** keybinding micro sub-row removed from `ActionBarSlot`
|
||
(and `bindingHeight`/`minSplitHeight` removed from `ActionBarSpec`); buttons now show
|
||
only the short label. Toolbar height 40→28 px.
|
||
- [x] **Keybinding in hover tooltip:** tooltip now renders "`phrase — binding`" when the
|
||
action is bound, bare phrase when unbound (live binding via `kbd_getTextFromCmd`).
|
||
- [x] **Cancel RT moved into overflow menu:** the More (⋯) popup now lists four entries —
|
||
Batch Items / Batch Razor / Capture RT / Cancel RT. The visible top bar no longer has a
|
||
Cancel RT button.
|
||
- [x] **Visible top-bar cluster order:** Capture Item · Capture Track · Re-capture · Insert ·
|
||
Insert Conform (cluster order: Capture → Maintenance → Placement).
|
||
|
||
**Notes/decisions:**
|
||
- Icons were considered and deferred (not implemented in this pass).
|
||
|
||
---
|
||
|
||
## L7 — capture ordering, card metadata, and selection styling
|
||
**Goal:** Three grid-facing improvements to the dock panel, drawn through the L1 kit in the
|
||
settled DS-2 palette. (1) **Persisted deterministic capture order + drag-drop reorder + sparse
|
||
placement:** each bank (and the pool) carries an explicit, persisted per-sample order via a
|
||
per-`Bank` id→slot `SlotMap` in `bank_book`; a card may sit in a slot that leaves earlier slots
|
||
empty (gaps preserved; trailing empty tail trimmed for scroll extent). (2) **Decorative metadata
|
||
over the peaks:** each card overlays capture length as **bars.beats.subdivisions (bottom-left)**
|
||
and **seconds.ms (bottom-right)** in the kit's micro / value-mono type class, `text/dim`. (3)
|
||
**Selection restyle:** a selected card drops the inverted accent-fill and instead draws the
|
||
*normal* cell + an **`accent/tertiary` (pastel purple `#C2AAE8`) border**. No new capture /
|
||
placement behavior; the "capture ≠ placement" principle is untouched.
|
||
|
||
- [x] **`SlotMap` in `bank_book`:** gap-preserving per-`Bank` id→slot map — persisted
|
||
deterministic display order; interior gaps preserved / trailing tail trimmed; JSON rides
|
||
inside the existing `"banks"` blob; pre-L7 migration seeds dense insertion order via
|
||
`reconcileSlots()` on the load path. `bank_model` / `Sample` untouched by position (position
|
||
is a per-bank display concern).
|
||
- [x] **`BankBook` mutators:** `reorderSample` (insert-before-shift; same-slot = no-op),
|
||
`replaceSample` (occupant index-removal via the standard remove path + pool-guard inheritance;
|
||
no-op on reject), `orderedSampleIds`, `reconcileSlots`. All gap-preserving; deterministic;
|
||
CTest-covered.
|
||
- [x] **`Sample` meter stamp (ONE sanctioned `Sample` change):** `captureTimeSigNum` /
|
||
`captureTimeSigDenom` added to `Sample` + JSON round-trip; stamped on both capture paths and
|
||
refreshed on re-capture via `TimeMap_GetTimeSigAtTime` (API confirmed at build). Old samples
|
||
with no stamp fall back gracefully (blank musical read-out).
|
||
- [x] **`card_drag` pure module:** gesture precedence — leave-client → OS drag-out;
|
||
other-bank → move/copy; same-bank → reorder / Alt-over-occupied → replace. Cursor-cue map
|
||
returned as the pure resolved gesture. Sparse `computeSlotRects` / `hitTestSlot`. SWELL stock
|
||
cursors chosen at build: Reorder→`IDC_SIZEALL`, Move→`IDC_HAND`, Copy→`IDC_UPARROW`,
|
||
Replace→`IDC_SIZEWE`. New CTest target `card_drag_tests`.
|
||
- [x] **`card_meta` pure module:** bars.beats.subdivisions from the stamped tempo + meter;
|
||
seconds.milliseconds rounded. Both blank when the sample is unstamped. New CTest target
|
||
`card_meta_tests`.
|
||
- [x] **`bank_panel` sparse-grid render:** grid renders in sparse slot order; decorative
|
||
empty-gap cells drawn for unoccupied slots; every cell↔sample consumer remapped to
|
||
id-based occupied-ordinal space (selection, keyboard nav — arrows skip gaps, audition,
|
||
multi-select, delete / re-capture resolution, drags).
|
||
- [x] **`bank_panel` drop dispatch:** reorder / Alt-replace / move-copy drop with one-Ctrl-Z
|
||
undo via the existing batched undo pattern; per-slot drop highlight (`accent/hot`, doubled
|
||
outline for replace); SWELL stock cursor cues via `SetCursor` per the pure resolved gesture.
|
||
- [x] **`bank_panel` metadata overlay:** bars.beats bottom-left (Micro), s.ms bottom-right
|
||
(ValueMono), `TextDim`, decorative / non-interactive.
|
||
- [x] **Selection restyle:** normal cell + `accent/tertiary` purple border; inversion removed;
|
||
focus ring distinct from the selection border.
|
||
|
||
**Notes/decisions:**
|
||
- **M9 overlap (awareness note — unchanged intent).** M9 (capture-to-slot-N / insert-slot-N,
|
||
MIDI-bindable, MPC-style) remains **explicitly deferred (Daniel, 2026-07-26).** The
|
||
interchangeable-slot substrate L7 builds still eases a future M9 revival but L7 adds **no**
|
||
slot-numbered capture/insert actions and **no** MIDI bindings. The "plain vs. M9-shaped"
|
||
sub-fork is closed: plain gap-preserving substrate (F2).
|
||
- **Build-time choices confirmed:** `TimeMap_GetTimeSigAtTime` confirmed at build for the
|
||
meter stamp; SWELL stock cursors chosen (no custom cursor load/synthesis required);
|
||
gap navigation = skip gaps (arrow keys skip empty slots); same-slot reorder = no-op.
|
||
- New CTest targets `card_drag_tests`, `card_meta_tests`.
|
||
|
||
---
|
||
|
||
## L3 — VST editor + embed-strip restyle
|
||
**Goal:** Bring the ReaSampler 9000 VST editor (`IPlugView` LICE surface,
|
||
`reasampler_editor.cpp`) and the S6 embed strip (`reasampler_embed.cpp`) up to the
|
||
settled-and-revised **B + three-accent pastel** look via the L1 kit: kit cached-font
|
||
`text()` (the kit's current face — no font change), kit component draws, the
|
||
**REAPER-grey neutrals** (`bg/base #2b2b2b` / `bg/panel #333333` / `bg/cell #3a3a3a`)
|
||
with the **three pastel accents** (primary lime / secondary teal / tertiary purple), the
|
||
**pastel spectral keyboard strip + zone bars** (active zone lifts to `accent/primary` +
|
||
a static glow) as the signature surface, and hover/pressed/drag interaction states
|
||
throughout. Merged as `c53683e`. **L3 was the last remaining Phase L point — Phase L
|
||
is now complete (L1, L2, L3, L4, L5, L6, L7 all landed).**
|
||
**Verify (in DAW):** the VST editor + embed strip render in the settled B + three-accent
|
||
pastel language through the L1 kit — kit AA cached-font text, gradient/rounded kit
|
||
components, the pastel spectral keyboard strip, working hover/pressed/drag; the VST3
|
||
class UID is unchanged (a visual refresh is not a compat event).
|
||
|
||
- [x] Route the VST editor's + embed strip's text through the kit's cached-font
|
||
`text()`; retire their raw GDI `DrawTextA` path (retired in both shells).
|
||
- [x] Retire the shells' **local pre-L1 palette** — the `kColBackground`/`kColCardBg`/
|
||
`kColThumb`/… forest-green-on-charcoal constants block in `reasampler_editor.cpp`
|
||
(and the mirrored constants in `reasampler_embed.cpp`) — and draw every surface
|
||
through the L1 `theme` roles. One kit, one look; two palettes collapsed to one.
|
||
- [x] Restyle the editor + embed components through the kit (capture-first browser
|
||
search/tabs/thumbnails, channel toggles, ADSR + pitch sliders, Varispeed/Preserve mode
|
||
toggles, zone bars, list rows, waveform + start/loop markers, segmented controls) in
|
||
the B (Neon Console) palette with hover/pressed/drag states throughout.
|
||
- [x] Apply **Direction C's pastel spectral treatment** to the keyboard strip + zone bars:
|
||
hue-mapped zones as a pastel sweep anchored on the three accents; active zone lifts to
|
||
`accent/primary` + a static glow (no animation); waveform + loop/start markers drawn
|
||
through `drawWaveform` + `warn`/accent marker roles; VST3 class UID unchanged.
|
||
|
||
**Notes/decisions:**
|
||
- **Full restyle, not a born-in-kit no-op.** The Phase S surfaces (`reasampler_editor.cpp`
|
||
+ `reasampler_embed.cpp`) arrived on dev drawing flat `LICE_FillRect` blocks + raw GDI
|
||
`DrawTextA`, off a local pre-L1 forest-green palette (`kColBackground` etc.) — the
|
||
coordination contract's "born in the kit" branch did not occur. L3 performed the full
|
||
restyle and reconciled the two palettes into one.
|
||
- **Shell-side only.** All `src/vst/` UI geometry modules (`editor_geometry`,
|
||
`keyboard_strip`, `waveform_view`, `capture_browser`, `param_slider`, `browser_scroll`,
|
||
`embed_strip`) are pure geometry/hit-test — zero LICE, zero draw. L3 touched only the
|
||
two draw shells; no geometry rework was needed.
|
||
- **Beta title band — textual-only distinction (Daniel, 2026-07-27).** The S18 beta
|
||
channel title band gets no distinct visual accent; L3 restyles it in the standard B
|
||
pastel palette and the beta-vs-stable distinction stays purely textual (the
|
||
channel-derived plugin name via `app_version`, as before). No channel-specific accent
|
||
color. Closes the one open fork from the L3 readiness review.
|
||
- **VST3 class UID unchanged.** A visual refresh is not a compat event; RT/`process`
|
||
path untouched; pure geometry modules remained pure throughout.
|
||
|
||
---
|
||
|
||
# Phase S — MIDI-playback instrument (native VST3 sampler; a second build artifact)
|
||
|
||
> **Landed on dev (merged 2026-07-27); DAW verification pending Daniel's smoke test.**
|
||
> S1–S18 are all on dev. The cross-artifact ingest relay (one S13 bullet) was explicitly
|
||
> DEGRADED and remains deferred in `PLAN.md`. Authoritative spec: **CONTEXT.md
|
||
> §MIDI-playback instrument — additive phase spec (Phase S)**. Product framing:
|
||
> `docs/product/midi-playback.md`.
|
||
|
||
---
|
||
|
||
## S3 — pure sampler core (voice engine / envelope / keymap / repitch)
|
||
**Goal:** The REAPER-free **and** VST3-free sampler core — voice allocation/polyphony,
|
||
amplitude envelope (ADSR), key→sample and velocity→sample mapping (the keymap),
|
||
repitch/interpolation from root note, keymap resolution — unit-tested in CTest against
|
||
known signals. **The heart of the phase (D3); the mirror of
|
||
`bank_model`/`peaks`/`view_mode_model`/`bank_book`; test it hard.** The core is
|
||
invariant under the build-shape choice — no VST3 or REAPER type at its boundary.
|
||
CONTEXT-ARCHIVE.md §Phase S (pure core, module architecture).
|
||
**Verify:** CTest green. Voice allocation is correct under polyphony (note-on/off,
|
||
voice stealing where bounded); ADSR shape asserted against a known signal (mirror of
|
||
`peaks`); repitch from root note produces the expected pitch ratio; keymap resolution
|
||
maps a (note, velocity) to the correct sample/zone; the core takes and returns only
|
||
plain data (no VST3/REAPER types) — enforced by the test target linking neither SDK.
|
||
**Depends on:** S2 (consumes `rootNote` / loop points as core inputs).
|
||
|
||
- [x] Voice engine: polyphonic voice allocation (note-on/off, bounded voice stealing),
|
||
per-voice state, mono-and-basic-polyphony sufficient for Tier 0.
|
||
- [x] Amplitude envelope (ADSR) math — asserted against a known signal.
|
||
- [x] Repitch/interpolation from root note (chromatic pitch ratio across the
|
||
keyboard); loop-point-aware sustain for held notes.
|
||
- [x] Keymap model + resolution: key ranges/zones (Tier-1 shape) and the
|
||
(note, velocity) → sample/zone query; Tier-0 chromatic-from-single-root as the
|
||
degenerate case.
|
||
- [x] Tests: voice allocation under polyphony + stealing; ADSR envelope shape;
|
||
repitch pitch-ratio correctness; keymap resolution (single-root chromatic + zoned);
|
||
core boundary is plain-data-only (no VST3/REAPER types).
|
||
|
||
---
|
||
|
||
## S4 — Tier 0: "the bank plays" (single sample, chromatic)
|
||
**Goal:** The honest MVP — one bank sample mapped chromatically across the keyboard
|
||
from its root note, basic polyphony, a simple amp envelope, velocity→volume. Wire the
|
||
S3 core into the S1 VST3 shell over the live-state seam (bridge-read bank + audio via
|
||
the M4 project-relative path machinery). Editor deferrable behind a parameters-only
|
||
default view. CONTEXT.md §Phase S (Tier 0, seams). **Delivers the core promise.**
|
||
**Verify (in DAW):** on an instrument track, the VST3 plays a chosen bank sample
|
||
MIDI-triggered, repitched chromatically from its root note, with basic polyphony,
|
||
an amp envelope, and velocity→volume; it reads the live `"reasampler"` bank via the
|
||
bridge and resolves the WAV audio the same project-relative way `persist` does;
|
||
following the active project works; it never captures and never inserts into the
|
||
arrange (read-only over the bank).
|
||
**Depends on:** S1, S2, S3.
|
||
|
||
- [x] VST3 `process` marshalling: read MIDI note-on/off/velocity off the event bus,
|
||
drive the S3 core, write per-voice audio to the output bus. (Block-granular event
|
||
timing at Tier 0; sample-accurate offset scheduling is a later tier.)
|
||
- [x] Live-state seam: read the bank index + selected sample's root note from
|
||
`"reasampler"` ext-state via the bridge; resolve the WAV audio path the M4
|
||
project-relative way (shared convention with `persist`, not re-implemented — the
|
||
parent-of-.rpp derivation is extracted to `capture_paths::projectDirOfRpp`, which both
|
||
`persist` and the bridge call). Bank JSON parsed via the shared `bank_book` path (the
|
||
spike string-scan reader retired); ext-state key names shared via pure `ext_keys.h`.
|
||
- [x] Sample selection UI (minimal, in the `IPlugView` LICE editor): a clickable list
|
||
of the bank's samples; the pick is the instance's own VST3 component state
|
||
(setState/getState), never written back to the bank.
|
||
- [x] Tier-0 playback: chromatic-from-root, basic polyphony (16 voices), amp envelope,
|
||
velocity→volume — plays in REAPER's routing/record/render path like any VSTi. Sample
|
||
load / decode / keymap build happen off the audio thread and hand to `process` via a
|
||
lock-free atomic pointer swap (graveyard-reclaim); `process` never allocates.
|
||
|
||
---
|
||
|
||
## S5 — Tier 1: "a keymap" (zoned multisamples, per-sample root notes)
|
||
**Goal:** Multiple bank samples zoned across the keyboard (key ranges), each with its
|
||
own root note — a captured *kit* (one-shots) or a *multisampled instrument* (same
|
||
instrument sampled at several pitches) plays correctly. One sample per key-region.
|
||
CONTEXT.md §Phase S (Tier 1). **Where the root-note + key-range seam fields earn
|
||
their place.**
|
||
**Verify (in DAW):** a keymap of several bank samples plays correctly zoned across
|
||
the keyboard, each repitched from its own root note within its range; a captured kit
|
||
and a multisampled instrument both play as expected; the keymap is authored in the
|
||
instrument (performance map) while root notes come from the bank intrinsics (S2);
|
||
editing the keymap does not touch the bank.
|
||
**Depends on:** S4.
|
||
|
||
- [x] Keymap editor in the `IPlugView` LICE editor: assign bank samples to key ranges
|
||
(low/high note per sample), each with its own root note (from S2 intrinsics,
|
||
overridable in the performance map).
|
||
- [x] Tier-1 playback: zoned resolution — a note picks its zone's sample and repitches
|
||
from that sample's root note; one sample per key-region.
|
||
- [x] Performance-map persistence: the keymap (zones, per-sample assignment) is the
|
||
instrument's own state — held in the instrument as VST3 component state (setState/getState)
|
||
per D-B's data-ownership split; the live `"reasampler"` seam is read-only (bank +
|
||
intrinsics in, nothing written back), never written back as a bank intrinsic.
|
||
|
||
---
|
||
|
||
## S6 — embedded TCP/MCP UI (D-D — scheduled in-phase, after the editor)
|
||
**Goal:** Render a compact keymap/level strip **inline in the track/mixer control
|
||
panel** via `reaper_plugin_fx_embed.h` (`IReaperUIEmbedInterface`) — the same
|
||
Cockos surface REAPER's own embedded FX use — so the instrument draws inline, not only
|
||
in its own window. Composes with the S1/S5 LICE editor path (same LICE-class drawing).
|
||
**Scheduled, not deferred (D-D settled 2026-07-26):** a real later point, sequenced
|
||
last because it is polish over a Tier-0 need — but on the roadmap. CONTEXT.md §Phase S
|
||
(embedded UI, D-D).
|
||
**Verify (in DAW):** the instrument draws a compact inline strip in the TCP/MCP (not
|
||
only its own editor window); the inline surface reflects and (where offered) edits the
|
||
keymap/levels; the embed lifecycle is clean (open/close/resize); the same LICE drawing
|
||
as the main editor is reused.
|
||
**Depends on:** S5 (composes over the existing LICE editor). **Must-verify before
|
||
build:** the `IReaperUIEmbedInterface` contract + embed message/lifecycle against
|
||
`vendor/reaper-sdk/sdk/reaper_plugin_fx_embed.h`.
|
||
|
||
- [x] Implement `IReaperUIEmbedInterface` on the VST3; draw a compact keymap/level
|
||
strip inline in the TCP/MCP using the same LICE surface as the editor.
|
||
- [x] Embed lifecycle (open/close/resize/hit-test inline) handled cleanly; reflects
|
||
the live keymap/levels.
|
||
|
||
---
|
||
|
||
## S7 — stereo channel mode (mono | stereo; core channel dimension + bus negotiation)
|
||
**Goal:** Give the instrument a per-instance **channel-mode toggle — 1 (mono) or 2
|
||
(stereo)** — that "works with the REAPER audio bus automatically." Mono keeps today's
|
||
downmix path; stereo grows the S3 core a **channel dimension** (2-channel sample data,
|
||
per-voice stereo render, stereo interp/loop) and negotiates the VST3 output bus so
|
||
mono/stereo just works in REAPER's routing. **This is an S3-core extension, not a shell
|
||
hack** — it touches the engine Daniel smoke-tests, so it sequences first after the
|
||
editor/embed work. CONTEXT.md §Phase S (channel mode, D-E). **Decided direction
|
||
(2026-07-26); leans below are build-time residuals, not open forks.**
|
||
**Verify (in DAW):** an instance set to stereo plays a stereo capture in true stereo,
|
||
its VST3 output bus negotiated to 2 channels via `setBusArrangements` so REAPER routes it
|
||
without manual channel wiring; an instance set to mono plays the existing downmix path; a
|
||
mono source in stereo mode plays dual-mono (centered); a stereo source in mono mode
|
||
downmixes (existing policy); the mode is per-instance state that survives project
|
||
save/reopen (component state, like the selected sample); the pure core's stereo render is
|
||
asserted against a known two-channel signal (mirror of `peaks`), and mono behavior is
|
||
unchanged (regression).
|
||
**Depends on:** S3 (extends the core), S4 (extends the process/bus shell). Independent of
|
||
S8/S9.
|
||
|
||
- [x] Core channel dimension (pure, S3 extension): `SampleData` carries 1- or 2-channel
|
||
decoded PCM (`frames` + optional length-matched `framesR`; `channelCount()`);
|
||
`Voice::renderFrameStereo` + a `VoiceEngine::render(left,right,n)` overload produce a
|
||
per-channel frame sharing one read head + one envelope tick; stereo linear interpolation +
|
||
loop read per channel. Mono stays the degenerate case (`renderFrame` reads channel 0 only,
|
||
byte-identical). Tests: stereo render asserted against a known 2-channel signal; dual-mono;
|
||
per-channel repitch + additive mix; mono render unchanged (regression) — sampler_core_tests.
|
||
- [x] Channel-mode toggle as per-instance state: `ChannelMode {Mono,Stereo}` in the
|
||
instrument's own component state (v4 = v3 + a channel-mode byte; setState/getState);
|
||
default mono. Cross-mode policy in `decodeChannels`: **mono source + stereo mode →
|
||
dual-mono**; **stereo source + mono mode → downmix** (existing decode-side policy). The
|
||
toggle lives in the instrument, never written to the bank (D-B). v1/v2/v3 blobs lift to v4
|
||
with mono default; round-trip + lift tests — sample_map_tests.
|
||
- [x] Shell: `decodeRelative` fills 1- or 2-channel `DecodedZonePcm` per the active mode
|
||
(source channel count from the WAV layout); the process path renders the host's negotiated
|
||
output channel count (stereo into ch0/ch1, mono into ch0) — RT discipline unchanged.
|
||
- [x] VST3 bus negotiation: `setBusArrangements` accepts only the mode's arrangement
|
||
(kMono/kStereo), else rejects (kResultFalse) but keeps a valid mode arrangement so
|
||
`getBusArrangement` (base default) reports it; a runtime mode change repoints the output bus
|
||
+ calls `restartComponent(kIoChanged)` so REAPER re-negotiates. **Verified** against the
|
||
vendored Steinberg SDK (`ivstaudioprocessor.h` contract, `vstsinglecomponenteffect.cpp`
|
||
base impl, `ivsteditcontroller.h` kIoChanged); see handoff notes.
|
||
|
||
---
|
||
|
||
## S8 — ingest through the bank (one gesture: capture/import into bank + assign to instance)
|
||
**Goal:** Loading a sample into the sampler is **one gesture** — capture/import-into-bank
|
||
**and** auto-assign to the active sampler instance. **The extension owns ingest** (it has
|
||
arrange access, media-explorer access, and drop-target surface on its own panels); the
|
||
instrument stays a **read-only bank consumer**. This lives in the *extension* codebase
|
||
(actions + bank_panel + capture/insert), routing through the existing capture add-path and
|
||
the live `"reasampler"` seam the instrument already reads. CONTEXT.md §Phase S
|
||
(ingest-through-bank contract). **Decided direction "option 1" (2026-07-26).**
|
||
**Verify (in DAW):** a one-click "capture selected item / time-selection into the bank and
|
||
assign to the active instance" action captures via the existing capture path (never
|
||
auto-inserting into the arrange — load-bearing principle intact) and the target instance
|
||
plays the new sample on its next reload; a Media Explorer file imports into the bank and
|
||
assigns the same way; a file dropped onto a ReaSampler panel surface ingests into the bank
|
||
and assigns; the instrument never captures or imports (read-only over the bank throughout).
|
||
**Depends on:** S4 (an instance to assign to), M7 capture add-path, B2 (active-bank add
|
||
target). Best paired with S9 so assignment refreshes hands-free; functional without it
|
||
(assign triggers a reload on the target instance directly).
|
||
|
||
- [ ] "Capture selected item / time-selection into bank + assign to active instance"
|
||
action (`command_id`/`gaccel`/`hookcommand`, MIDI-bindable): reuse the existing capture
|
||
request path (`CountSelectedMediaItems`/`GetSelectedMediaItem` + `GetSet_LoopTimeRange`
|
||
as the capture inputs), add the resulting `Sample` to the active bank, then assign its
|
||
id to the target instance. **Never inserts a timeline item** (capture/placement stay
|
||
separate — the assignment is a bank-index + instance-selection act, not a placement).
|
||
- [ ] Media Explorer import → bank → assign: read the Media Explorer's current selection
|
||
via `MediaExplorerGetLastPlayedFileInfo` (path + selection range), import the file into
|
||
the bank (existing import/capture add-path), assign to the target instance. **Honest SDK
|
||
limit (verified against the vendored headers):** the Media-Explorer surface is thin —
|
||
`OpenMediaExplorer` (open/select) + `MediaExplorerGetLastPlayedFileInfo` (read the *one*
|
||
last-played/selected file + its range) are the whole contract; there is **no**
|
||
enumerate-selected-files and **no** register-a-drop-handler-on-the-Media-Explorer API.
|
||
So ME import is *single-file, pull-on-action* (an action the user fires while a file is
|
||
selected in the ME), not a push/drop from inside the Media Explorer. **Spike:** confirm
|
||
`MediaExplorerGetLastPlayedFileInfo` returns a usable path+range for a merely-*selected*
|
||
(not-yet-played) file, or whether a play is required first.
|
||
- [ ] Drag-and-drop onto ReaSampler surfaces: accept an OS file drop onto the docked
|
||
`bank_panel` (and its bank/tab regions) → ingest into the bank → assign. **Honest SDK
|
||
limit (verified):** REAPER exposes **no** drag-drop registration API; drop handling is on
|
||
ReaSampler's *own* HWNDs via SWELL/Win32 (`WM_DROPFILES` / an `IDropTarget` on the panel
|
||
HWND), the same surface the panel already owns. **Assess-and-flag (spike, do not promise
|
||
here):** a drop *onto the VST3 editor window* — whether the `IPlugView` HWND can accept an
|
||
OS file drop and relay it to the extension as a bank-ingest request (the instrument does
|
||
**not** ingest; it forwards a request to the extension over an agreed seam). Reported
|
||
honestly as a spike because it crosses the two-artifact boundary and the relay mechanism
|
||
is unproven; if it proves gnarly, drop-onto-panel is the shipped path and drop-onto-editor
|
||
is deferred.
|
||
- [ ] "Assign to instance" seam: how the ingest action names the target instance and hands
|
||
it the new sample id. Lean (build-time residual, not a fork): the active/last-focused
|
||
instance is the target, discovered via the host context the bridge already resolves; the
|
||
assignment is the same instance-owned selection state S4 already persists, so a reload
|
||
picks it up. If the change-detection seam (S9) exists, assignment refreshes hands-free;
|
||
without it, the ingest action pokes the target instance's reload directly.
|
||
|
||
---
|
||
|
||
## S9 — bank-generation change-detection (recapture / ingest refreshes instances hands-free)
|
||
**Goal:** Because instances reference sample **ids**, a **recapture** (M10) landing under
|
||
the same id — or an **ingest** (S8) touching the active bank — should refresh playing
|
||
instances **hands-free**, without the user re-opening each editor. Add a **bank-generation
|
||
counter** to `"reasampler"` ext-state that the extension bumps on any bank-content
|
||
mutation, and that the instrument polls off the audio thread on a safe cadence, calling its
|
||
existing `reloadFromBank()` when the generation changes. CONTEXT.md §Phase S
|
||
(bank-generation seam). **Closes the missing change-detection trigger the recapture
|
||
auto-update story needs.**
|
||
**Verify (in DAW):** a recapture that regenerates a sample already assigned to a live
|
||
instance refreshes that instance's playback within a bounded cadence, no editor re-open; an
|
||
ingest (S8) that updates the active bank likewise refreshes assigned instances; the poll
|
||
runs off the audio thread (never in `process`) and triggers the existing off-thread reload
|
||
path; instances not referencing a changed sample do not audibly glitch (reload is atomic —
|
||
the S4 graveyard-reclaim handoff); a project with no generation stamp (pre-S9) defaults
|
||
cleanly (treated as generation 0; first bump refreshes).
|
||
**Depends on:** S4 (the off-thread `reloadFromBank` + atomic handoff this drives). Writer
|
||
side is extension-only and independent of S8; consumed by S8 and M10 recapture. Best landed
|
||
alongside S8.
|
||
|
||
- [x] Writer (extension): a monotonic **bank-generation counter** stamped into
|
||
`"reasampler"` ext-state (new `ext_keys.h` constant — forever-stable spelling), bumped
|
||
on every bank-content mutation that changes what an instance would play (capture add,
|
||
recapture-in-place, sample-remove, move/copy affecting the active bank). Additive to the
|
||
persist blob; defaults to 0 for projects saved before the stamp exists.
|
||
- [x] Reader (instrument): poll the generation over the bridge on a safe **off-audio-thread
|
||
cadence** (a UI/timer tick, not `process`), compare to the last-seen value, and call the
|
||
existing `reloadFromBank()` on change — reusing S4's atomic pointer-swap handoff so a
|
||
refresh mid-play does not glitch. No new audio-thread work; no allocation in `process`.
|
||
- [x] Cadence + coalescing: pick a poll interval that is responsive but cheap (build-time
|
||
residual — a low-frequency UI timer, coalescing multiple bumps between polls into one
|
||
reload). **Must-verify before build:** that a bridge ext-state read on the instrument's
|
||
UI/timer thread is safe against a concurrent extension write (the read already tolerates a
|
||
stale value by design — it reloads on the *next* poll; confirm no torn-read hazard for the
|
||
single integer generation key).
|
||
|
||
---
|
||
|
||
## S10 — capture-first editor: browser + guided single-capture setup ("ReaSampler 9000" UX overhaul, part 1)
|
||
**Goal (REVISED 2026-07-26 — workflow-first reframe, Daniel):** Rebuild the editor's
|
||
default face around the **primary flow = one capture, fast**, not a keymap. A giant list of
|
||
"item" blocks is visually useless; most instances play a *single capture*, and zones are a
|
||
nice-to-have. So the default view is a **capture browser** (scannable cards with peak
|
||
thumbnails, name, root/key badge; **bank filter**) feeding a **guided single-capture setup**
|
||
(root note, play-mode basics, level) — and the keyboard strip serves the *single-capture*
|
||
case first (shows where the capture sits / its root). **Time-to-first-note is the metric.**
|
||
Multi-zone keymap editing is **demoted to an opt-in "Zones" panel** (S10-Z below), not the
|
||
default. The keyboard-strip drag machinery is still built here, but in service of the
|
||
capture-first layout. All layout/hit-test math is **pure geometry** (new `keyboard_strip` +
|
||
a `capture_browser` layout module — mirrors of `mode_switch`/`editor_geometry`); the LICE
|
||
draw + drag-state machine is the editor shell. RT discipline untouched (edits commit
|
||
off-thread via `commitAndReload`); the instrument stays a **read-only bank consumer**.
|
||
CONTEXT-ARCHIVE.md §Phase S (ReaSampler 9000 UX — capture-first editor).
|
||
|
||
**Policy reversal — fresh instance is SILENT, nothing auto-selected (was S4).** The S4
|
||
"first sample plays" fallback is **removed**: on open with no stored selection, the
|
||
instrument plays **nothing** and the editor shows a clear **empty state** ("pick a capture")
|
||
— it does not auto-play sample #1. Retires the `selectSample` first-sample fallback
|
||
(`sample_map.cpp` "No stored id → fall back to the FIRST sample") and the processor's
|
||
Tier-0 fallback that resolved it; an empty stored id now resolves to silence. A capture is
|
||
loaded when the user picks one (or via S13 drop-to-load / S8 ingest). This is a deliberate
|
||
reversal of the S4 convenience default, not a regression.
|
||
|
||
**Verify (in DAW):** a fresh instance plays **nothing** and shows the "pick a capture"
|
||
empty state (no auto-play of sample #1); the capture browser draws **peak thumbnails**
|
||
(the `Sample` peaks bank_model already carries — same data the dock panel thumbnails use),
|
||
name, and a root/key badge where present, and is **filterable by bank** (bank_book named
|
||
banks); picking a capture loads it, shows it (waveform/peaks + its root on the keyboard
|
||
strip), and it plays repitched from its root; time-to-first-note is a pick-then-play, not a
|
||
list-scroll; the keyboard strip shows the single capture's root and is draggable to set it;
|
||
the pure geometry modules are CTest-green (browser card/grid layout + hit-test; strip
|
||
edge-grab/body-move/key→note) with no host types at their boundary; the ±1 nudge-button row
|
||
is gone.
|
||
**Depends on:** S4 (the selection state + reload path this reverses the fallback on), S1
|
||
(the LICE `IPlugView` drag/event routing — extends the click-only `wndProc` to
|
||
`WM_MOUSEMOVE`/`WM_LBUTTONUP`), S5 (the `PerformanceMap`/zone model the opt-in Zones panel
|
||
edits — but the default face does not require a keymap). **Adopts the Phase L kit when
|
||
available — not gated on Phase L.** S10 builds its browser cards + keyboard strip with the
|
||
current LICE drawing; when Phase L's L1 kit lands on `dev`, this surface adopts it (the one
|
||
source of drawing). The drag machine's `WM_MOUSEMOVE` tracking also lights the kit's
|
||
**hover** states at near-zero marginal cost once the kit is present.
|
||
|
||
- [ ] No-auto-select + empty state (the policy reversal): remove the `selectSample`
|
||
first-sample fallback (`sample_map.cpp`) and the processor's Tier-0 fallback that
|
||
consumed it — an empty stored selection resolves to **silence**, not sample #1. The
|
||
editor draws a clear **empty state** ("pick a capture" affordance) when nothing is
|
||
selected. Pure change is testable (empty id → `nullopt`); the empty-state draw is shell.
|
||
- [ ] Capture browser (pure layout + shell draw): grow `SampleChoice` to carry the
|
||
**peak thumbnail data** (from the `Sample` peaks bank_model already stores — the same
|
||
peaks the dock panel draws), the **root/key badge** (S2 `rootNote` intrinsic / the
|
||
optional musical key), and its bank. A new pure `capture_browser` module lays out
|
||
scannable **cards/rows** (card rect grid, thumbnail rect, hit-test a point → card) — no
|
||
host types at the boundary, unit-tested. The shell draws each card's peak thumbnail +
|
||
name + badge in LICE (house palette) and routes a click to select.
|
||
- [ ] Bank filter (pure + shell): a filter/tab strip over the browser that narrows the
|
||
drawn cards to a chosen bank_book bank (or "all"). Filter-tab layout + hit-test pure
|
||
(mirror of `mode_switch`); the active-filter state is transient UI state; the shell draws
|
||
the tabs and applies the filter to the card list. (Type-to-filter search folds in from
|
||
S12 — see S12's boundary note; a name-substring filter over the same card list.)
|
||
- [ ] Guided single-capture setup (the fast path): once a capture is picked, a prominent,
|
||
self-explanatory setup surface — **root note** (settable on the keyboard strip / typed),
|
||
**play-mode basics**, **level** — sized for the single-capture case, not a zone table.
|
||
Graphic and descriptive; the point is to get from pick → set → play with no hunting.
|
||
- [ ] Pure `keyboard_strip` geometry module (serves the single-capture case first): map a
|
||
MIDI key span across a strip width (128 keys → pixels, reusing the S6 `embed_strip`
|
||
key-span idiom); a **root marker** for the loaded capture; `pixel→note` and a `keyAtPoint`
|
||
for click-to-set-root; a drag-delta resolver `(grabbedField, startNote, dxPixels) →
|
||
newNote`; **per-zone bar rect** + **edge-grab hit regions** (resize handles vs. body
|
||
move-handle) for the opt-in Zones panel. No VST3/REAPER/LICE types at the boundary;
|
||
unit-tested (root marker, edge grabs, body-move delta, key mapping, clamps low≤high,
|
||
boundary rounding). Mirror of `mode_switch`/`editor_geometry`.
|
||
- [ ] Editor shell drag-state machine: `WM_LBUTTONDOWN` grabs a card / a key / a zone
|
||
edge-or-body, `WM_MOUSEMOVE` updates the in-flight edit against the pure resolver,
|
||
`WM_LBUTTONUP` commits via the existing `commitAndReload` (off-thread reload; RT path
|
||
untouched). Live visual feedback while dragging; a single undo-coherent edit on release.
|
||
|
||
### S10-Z — Zones panel (opt-in multi-zone keymap editing; demoted from the default face)
|
||
The multi-zone keymap editor is now an **opt-in view/panel** ("Zones" toggle), not the
|
||
default. It reuses the same `keyboard_strip` geometry and drag-state machine: each zone a
|
||
bar over the keys it covers; **drag an edge** → low/high note; **drag the bar body** → move
|
||
the zone (span preserved); **click a key** → set/relocate the zone's root. This is the
|
||
capability RS5K structurally lacks (multi-zone in one instrument), kept as a *nice-to-have*
|
||
per Daniel's hierarchy — "most of the time the zones won't be used." Add/select/delete a
|
||
zone; overlapping zones render legibly and resolve first-match. The seven ±1 nudge/delete
|
||
mini-buttons are retired everywhere; delete is one affordance (a small × on the bar or a
|
||
keystroke). The `zoneHitTest`/±1 nudge path in `editor_geometry` is retired (a numeric
|
||
fallback for accessibility is a build-time residual, not a fork).
|
||
**Verify (in DAW):** the Zones panel is reachable via an explicit toggle (default view is
|
||
the capture browser + single-capture setup, not this); a zone's range is set by **dragging
|
||
edges** (not nudge clicks); body-drag moves the span; click-a-key sets the root (audible on
|
||
the next held note); zone add/select/delete work; the ±1 nudge row is gone.
|
||
|
||
- [ ] "Zones" panel toggle (opt-in): the default editor face is the capture browser +
|
||
single-capture setup; a toggle reveals the multi-zone keymap editor. Toggle state is
|
||
transient UI state (or per-instance component state if it should persist — build-time
|
||
residual).
|
||
- [ ] Zone edit via the shared strip: draw the keyboard strip + zone bars in LICE, drive
|
||
the shared drag-state machine (edge = resize, body = move, key = root), commit via
|
||
`commitAndReload`. Zone add/select/delete as single affordances; ±1 nudge row gone.
|
||
|
||
---
|
||
|
||
## S11 — waveform view with draggable loop points (UX overhaul, part 2)
|
||
**Goal:** Give each sample/zone a **waveform display** with **draggable start/end/loop
|
||
markers** — the S2 loop-point intrinsics and the S5 performance map already carry the data;
|
||
today there is no way to *see* a sample or *set* its loop by eye. Selecting a zone (or a
|
||
bank sample) shows its waveform (peaks via the existing `peaks` module, fed the decoded
|
||
PCM the shell already loads); drag the **loop-start / loop-end** markers to set the sustain
|
||
loop, snapping to zero-crossings (the S2 spec's zero-crossing-aware requirement). Loop
|
||
points are a **performance-map override on the zone** where set, seeded from the bank
|
||
intrinsic (D-B split: the bank carries the file-fact default; the instrument's drag is the
|
||
performance choice). All marker/waveform layout + hit-test is pure geometry; peaks compute
|
||
reuses `peaks`; the draw + drag is the shell. CONTEXT-ARCHIVE.md §Phase S (ReaSampler 9000 UX —
|
||
waveform view).
|
||
**Verify (in DAW):** selecting a zone shows its sample's waveform; dragging the loop-start
|
||
and loop-end markers sets the sustain loop and a held note audibly loops that region;
|
||
markers snap to the nearest zero-crossing; a sample with no loop shows the "no loop" state
|
||
and a held note past the end goes silent (existing core behavior); the waveform peaks match
|
||
the audio (mirror of the `peaks` envelope assertion); the marker geometry module is
|
||
CTest-green (px↔frame mapping, marker grab regions, clamp start≤end).
|
||
**Depends on:** S2 (loop-point intrinsics), S3 (loop-aware sustain the markers drive), S5
|
||
(the zone the loop attaches to), S10 (shares the editor's drag-state machine + shell). The
|
||
zero-crossing snap is a small pure helper over the decoded PCM.
|
||
|
||
> **Boundary note (S10 reframe, 2026-07-26):** the waveform view is now **central to the
|
||
> single-capture fast path**, not just per-zone. Selecting a capture in S10's browser shows
|
||
> its waveform (this is "see it" in pick → see it → play it); the loop-marker drag here
|
||
> extends that same waveform surface. S11's waveform draw is the same one S10's picked-
|
||
> capture view uses — build it once, S10 shows it read-only for the single capture, S11 adds
|
||
> the draggable loop markers. No renumber; S11 stays the loop-editing point.
|
||
|
||
- [ ] Pure waveform/marker geometry: `frame↔pixel` mapping across the waveform rect, marker
|
||
x-position from a frame index, marker grab regions (start/end/loop-start/loop-end),
|
||
drag-delta `(grabbedMarker, dxPixels) → newFrame` with clamps (start≤end, in-bounds). A
|
||
**zero-crossing snap** helper: nearest sign-change frame to a target (pure, over the
|
||
decoded mono PCM). No host types; unit-tested.
|
||
- [ ] Waveform draw: compute peaks with the existing `peaks` module from the shell's already-
|
||
decoded PCM (no new decode path, no new WAV reader); draw the envelope in LICE in the
|
||
house style; draw the loop markers over it. Reuses the S10 drag-state machine.
|
||
- [ ] Loop-point edit → performance-map override: a dragged loop writes a per-zone loop
|
||
override (seeded from the S2 bank intrinsic, D-B), committed off-thread via
|
||
`commitAndReload`; the bank intrinsic is never written back (instrument is a read-only
|
||
bank consumer). Extends `PerformanceZone` with an optional loop override (additive, same
|
||
shape as `rootOverride`) + its component-state (de)serialize (version bump, back-compat
|
||
with S5's v2 map blob — a truncated/older blob defaults the override absent).
|
||
|
||
---
|
||
|
||
## S12 — editor scale + ergonomics (UX overhaul, part 3; scrollable/searchable list, direct entry)
|
||
**Goal:** Make the editor usable **at bank scale** and close the remaining RS5K-parity
|
||
gaps: the sample list **scrolls** (today a long bank's rows run off the panel with no way
|
||
to reach them) and has a **type-to-filter search**; add **direct numeric entry** for a
|
||
zone's low/high/root (a click-to-type field over the strip, for precision the drag can't
|
||
hit) and an **ADSR control** for the amp envelope (S3 already has the ADSR math; today it
|
||
is fixed — expose attack/decay/sustain/release as draggable sliders, per-instance state).
|
||
This is the "sensible list handling + direct manipulation of the parameters that exist"
|
||
tier. All slider/scroll/search-box layout + hit-test is pure geometry; the shell draws +
|
||
routes; ADSR/scroll/filter state is instrument-owned (component state / transient UI
|
||
state). CONTEXT-ARCHIVE.md §Phase S (ReaSampler 9000 UX — scale + ergonomics).
|
||
**Verify (in DAW):** a bank with more samples than fit **scrolls** (wheel + drag) and every
|
||
sample is reachable; typing filters the list to matching names; a zone's low/high/root can
|
||
be **typed** (not only dragged) via a click-to-edit field; the amp envelope's ADSR is
|
||
**adjustable** (four draggable controls) and the change is audible + persists across project
|
||
save/reopen (component state); the scroll/search/slider geometry is CTest-green.
|
||
**Depends on:** S10 (the editor shell + drag-state machine + the **capture browser** the
|
||
scroll/search now apply to), S3 (the `AdsrParams` the ADSR sliders drive — already wired
|
||
into the voice engine; today they are fixed defaults), S5 (the map the numeric fields edit).
|
||
|
||
> **Boundary note (S10 reframe, 2026-07-26):** the "sample list" S12 originally scrolled and
|
||
> searched **is now S10's capture browser** (cards with peak thumbnails, bank filter). What
|
||
> pulled INTO S10: the browser layout itself, the peak thumbnails, and the **bank filter**
|
||
> (a bank_book tab, distinct from name search). What stays in S12 and applies **to S10's
|
||
> browser**: (a) **scroll** for a bank longer than the panel, and (b) **type-to-filter
|
||
> search** (a name-substring narrow over the same cards, composing with S10's bank filter —
|
||
> bank filter picks the bank, search narrows within it). The scroll/search geometry is pure,
|
||
> layered over the `capture_browser` module S10 builds. Net: S12 = scroll + search over the
|
||
> S10 browser + numeric entry + ADSR; the browser *card* work is S10's. **S12 now also
|
||
> carries the S15/S16 control surfaces** (per-zone Gate/Trigger mode toggle, AHDSR hold
|
||
> control, Trigger %-length/fade controls, Varispeed/Preserve engine toggle, and the AD pitch
|
||
> envelope depth/shape controls) — deferred here from S15 and S16 per spec.
|
||
|
||
- [x] Scrollable, searchable capture browser: a scroll offset (wheel + scrollbar drag) so a
|
||
bank longer than the panel is fully reachable; a **type-to-filter search** that narrows
|
||
the drawn cards to matching display names, **composing with S10's bank filter** (bank
|
||
filter selects the bank; search narrows within it). Scroll/search layout + hit-test is
|
||
pure geometry (visible-card window, scrollbar thumb rect, search-box rect), layered over
|
||
S10's `capture_browser` module; filter/scroll state is transient UI state. **Landed:**
|
||
pure `browser_scroll` module (`browser_scroll_tests`) — scrollContentHeight / max / clamp,
|
||
visibleCardRange window, scrolledCardCellRect, scrollThumbRect + thumbDragToOffset inverse,
|
||
searchBoxRect, nameMatchesQuery + filterNameIndices. Editor shell wires wheel (`WM_MOUSEWHEEL`),
|
||
thumb-drag (`DragKind::kScrollThumb`), and the search box (`WM_CHAR` -> `onSearchChar`,
|
||
composed into `rebuildVisible`). Scroll/search are transient (never persisted).
|
||
- [x] Direct numeric entry for zone low/high/root: a click-to-edit field over the strip
|
||
(LICE text-entry idiom) so a precise note can be typed, not only dragged. Commits via
|
||
`commitAndReload` like every other edit. **Landed:** pure `note_entry` module
|
||
(`note_entry_tests`) — `parseNoteEntry` accepts a decimal integer OR a note name (C4==60),
|
||
clamps to [0,127], rejects garbage. Editor shell hosts three focusable fields (low/high/root)
|
||
on the Zones legend, committing on Enter through `commitAndReload`.
|
||
- [x] ADSR/AHDSR editor + S15/S16 control surfaces: draggable sliders over the S3 `AdsrParams`
|
||
(attack/**hold**/decay/sustain/release — hold is the S15 addition) plus the deferred S15/S16
|
||
controls — per-zone Gate|Trigger mode toggle, Trigger %-length/fade-in/fade-out, Varispeed|
|
||
Preserve engine toggle, and the AD pitch-envelope enable/attack/decay/±semitone depth. All
|
||
edit the SELECTED zone's `ZonePlayParams` (instrument-owned, D-B; never the bank), round-trip
|
||
through the existing v3 component-state blob (no new persistence — the S15/S16 payload already
|
||
landed in the core pass), and commit off-thread via `commitAndReload`. **Landed:** pure
|
||
`param_slider` module (`param_slider_tests`) — control-panel stack layout, toggle-segment
|
||
split + hit-test, slider value<->pixel round-trip + clamping, point->control routing. The
|
||
shell owns the control-id -> engine-param binding + the value DOMAIN mapping (frames/fraction/
|
||
semitones); the module stays engine-free.
|
||
|
||
**Notes/decisions:**
|
||
- **Wall-clock envelope times stored as rate-free SECONDS (zones payload v5)**, resolved to
|
||
frames at keymap build against the live project rate — no hardcoded sample rates anywhere in
|
||
`src/`. Daniel's standing ruling; enforced throughout the voice engine and verified at S12.
|
||
|
||
---
|
||
|
||
## S13 — drop-to-load (partial landing; relay deferred)
|
||
**Goal:** Make "load a sample into the sampler" **one gesture from the editor** via an
|
||
OS file drop onto the editor window. The cross-artifact relay was a spike — the
|
||
instrument's REAPER bridge (`reaper_bridge`) is deliberately READ-ONLY; the relay is
|
||
DEGRADED and deferred. The two landed items are the drop-accept surface and the UX degrade
|
||
path. The deferred relay item remains in `PLAN.md`. CONTEXT-ARCHIVE.md §Phase S (drop-to-load).
|
||
|
||
**Spike verdict (ps-w12, 2026-07-27): DEGRADED.** The relay would require (a) a new
|
||
instrument WRITE seam into ext-state and (b) an extension-side timer poller + claim/clear
|
||
nonce — the same cross-process handshake race the S17 spec rejected. The shipped ingest
|
||
gesture stays drop-onto-docked-panel (S8). The relay is a future wave when the design is
|
||
ready.
|
||
|
||
- [x] Editor-window drop target: accept `WM_DROPFILES`/`IDropTarget` on the editor child
|
||
HWND (the same SWELL/Win32 surface `bank_panel` owns), extracting the dropped file
|
||
path(s). Windows-only (D5). This is the *acceptance* half; the ingest is the extension's.
|
||
**Landed:** the editor child window calls `DragAcceptFiles(TRUE)` on attach and handles
|
||
`WM_DROPFILES` (`reasampler_editor.cpp`). Windows-only (D5).
|
||
- [ ] Cross-artifact ingest relay (the S8-flagged spike): **DEFERRED** — relay mechanism
|
||
proved load-bearing to redesign. Remains in `PLAN.md` §S13.
|
||
- [x] UX degrade path: when the relay is unavailable/unproven, the editor shows a clear
|
||
"drop files on the ReaSampler panel to add" affordance rather than silently swallowing the
|
||
drop — the shipped ingest gesture stays discoverable either way. **Landed:** the editor
|
||
ACCEPTS the drop and flashes a transient banner ("drop files onto the ReaSampler bank panel
|
||
to add them") that decays over a few sync ticks, plus a persistent affordance line in the
|
||
empty state ("drop a file onto the ReaSampler bank panel"). No file is ingested; NO timeline
|
||
item is ever inserted (the hard invariant — the editor only displays guidance).
|
||
|
||
---
|
||
|
||
## S15 — sampling modes: Trigger vs Gate (per-sample play-mode; core + editor)
|
||
**Goal:** Give each played sample a **play mode** — **Gate** (classic held note) or
|
||
**Trigger** (one-shot) — a per-sample/per-zone performance choice (D-B, instrument-owned).
|
||
**Gate** is today's behavior grown from ADSR to **AHDSR** (adds a Hold stage): note-on →
|
||
attack/hold/decay/sustain, note-off → release, sustain **loop points apply** (S11's
|
||
draggable loop UI is Gate-mode UI). **Trigger** is a one-shot drum-pad: note-on fires
|
||
playback of a defined **% of sample length** with a **fade-in** and **fade-out** ramp,
|
||
**ignores note-off**, and uses **no sustain loop**. **Both** modes carry a **modifiable
|
||
start point** (playback begins at an offset into the sample, not always frame 0). This is
|
||
an **S3-core extension** (the engine Daniel smoke-tests) plus editor surfacing — the mode +
|
||
its parameters are instrument performance-map state, never a bank fact. CONTEXT.md §Phase S
|
||
(Sampling modes — Trigger vs Gate). **Daniel's feature set is settled.**
|
||
**Verify (in DAW):** a sample in **Gate** mode plays held with the AHDSR envelope (hold
|
||
stage audible between attack and decay), releases on note-off, and loops its sustain region
|
||
if loop points are set; a sample in **Trigger** mode fires a fixed % of its length on
|
||
note-on with audible fade-in/out, **plays through to completion regardless of note-off**,
|
||
and never sustain-loops; the **start point** offsets playback in both modes (a note starts
|
||
partway into the sample); the mode + parameters are per-instance component state that
|
||
survive save/reopen; the pure core's Trigger envelope (fade-in → hold → fade-out over
|
||
%-length frames) and the AHDSR hold stage are asserted against known signals; existing
|
||
Gate/ADSR behavior is unchanged when hold=0 (regression).
|
||
**Depends on:** S3 (extends the envelope + voice read-position machinery), S5 (the
|
||
`PerformanceZone` the mode + params attach to), S11 (Gate loop-point UI; Trigger's waveform
|
||
shows start + %-length + fades on the same waveform surface). Independent of S7.
|
||
|
||
- [x] Core: `PlayMode { Gate, Trigger }` on the voice + the envelope split. **Gate**: `AdsrParams`
|
||
gains `holdFrames` in place (AHDSR hold stage between attack and decay; `holdFrames == 0` is
|
||
the exact pre-S15 ADSR — back-compat; no type rename). **Trigger** is a distinct envelope: play `[start,
|
||
start + lengthFraction·(frames−start))` with a **fade-in** ramp (0→1 over `fadeInFrames`)
|
||
and a **fade-out** ramp (1→0 over `fadeOutFrames` ending at the play-length end),
|
||
**ignoring note-off** (release is a no-op in Trigger). Fade curve default **equal-power**
|
||
(constant-power `sin`/`cos`, click-free on one-shots); pure, unit-tested against a known
|
||
signal.
|
||
- [x] Core: **modifiable start point** — the voice's initial `readPos_` is `startFrame`
|
||
(frame offset), applied in both modes; the existing per-frame `readPos_ += ratio_` read
|
||
and loop/interp machinery is otherwise unchanged. Clamp `0 ≤ startFrame < frames`.
|
||
- [x] Core: **% length → frames + fade mapping** for Trigger. `lengthFraction ∈ (0,1]`
|
||
resolves to `playEnd = start + round(lengthFraction·(frames − start))`; `fadeInFrames` /
|
||
`fadeOutFrames` clamp so their sum ≤ play length (fade-out anchored to `playEnd`). Note-off
|
||
in Trigger does nothing; the voice frees when `readPos_ ≥ playEnd`. **Choke on note-off is
|
||
NOT in scope** (fork S15-F1, held).
|
||
- [x] Parameter ownership (per-sample/per-zone, instrument-owned): the play mode + its
|
||
params (Gate: AHDSR; Trigger: %-length, fade-in, fade-out; both: start point) attach to
|
||
the **capture selection / zone**, stored in the **performance map** (D-B). Additive/
|
||
version-bumped component state; back-compat — a truncated/older blob defaults to **Gate**,
|
||
hold=0, start=0, no fades = exactly today's behavior.
|
||
- [x] Editor (S11 waveform surface, mode-aware): **Gate** shows draggable **start + loop
|
||
markers** (S11's loop UI); **Trigger** shows **start + %-length end + fade-in/out**
|
||
handles on the same waveform. A **mode toggle** per capture/zone in the guided setup
|
||
(S10) / Zones panel (S10-Z). Marker/handle geometry is pure; commits off-thread via
|
||
`commitAndReload`. The instrument stays a **read-only bank consumer**. **Editor control
|
||
surface deferred to S12 tier (spec-sanctioned).**
|
||
|
||
---
|
||
|
||
## S16 — pitch engine modes (Varispeed vs Preserve) + pitch envelope (per-voice)
|
||
**Goal:** Give the sampler **two pitch behaviors** and a pitch envelope that rides whichever
|
||
is chosen. **Varispeed** — resampling that couples pitch and duration (classic sampler /
|
||
RS5K default). **Preserve** — duration-preserving repitch, where a transposed note keeps
|
||
its original length. A **per-zone/per-capture pitch-engine mode**. On top of either engine
|
||
rides a per-voice **AD pitch envelope**, **off by default** — a short attack-decay pitch
|
||
modulation. Per-instance performance-map state (D-B). CONTEXT.md §Phase S (Pitch engine
|
||
modes + pitch envelope).
|
||
**Verify (in DAW):** Varispeed — a note an octave up plays half as long as the root note;
|
||
Preserve — a note an octave up plays at the same duration as the root note; pitch envelope
|
||
off (default) under either engine: no pitch modulation applied (regression); pitch envelope
|
||
on: an AD envelope makes a note start offset in pitch and glide to the zone's base pitch
|
||
over attack+decay; CPU stays within budget at polyphony cap.
|
||
**Depends on:** S3, S5, S15. Independent of S7.
|
||
|
||
- [x] Core: **pitch-engine mode on the voice/zone** — `PitchEngine { Varispeed, Preserve }`.
|
||
**Varispeed** = `readPos_ += ratio_` (today's path, pitch and duration coupled). **Preserve**
|
||
= duration-preserving: the read advances at the **source** rate while a pitch shifter
|
||
transposes the output. Mode is per-`PerformanceZone` performance state (D-B), additive/
|
||
version-bumped; absent/older blob → engine default. Pure where possible: Varispeed math and
|
||
duration-invariance contract unit-tested.
|
||
- [x] Core: **Preserve engine implementation** — hand-rolled pure OLA `pitch_shift` module
|
||
(house pattern — CTest-testable, no WDL/REAPER/VST3 type at the boundary). Pre-allocated,
|
||
no locks, no `process` allocation; pre-warmed at voice allocation. `pitch_shift_tests`
|
||
CTest target. WDL_SimplePitchShifter excluded by include-chain (windows.h); held as a
|
||
quality/latency swap alternative (`PitchEngine::Preserve` contract identical behind the seam).
|
||
- [x] Core: a per-voice **AD pitch envelope**, engine-aware — `PitchEnvParams { enabled=false,
|
||
int64 attackFrames, int64 decayFrames, double peakSemitones }`. Off by default (`enabled=false`
|
||
→ offset always 0). Under **Varispeed** the offset multiplies `ratio_`; under **Preserve**
|
||
the offset is **added to the shifter's shift amount**. Pure, unit-tested.
|
||
- [x] Parameter ownership + editor: pitch-engine mode + pitch envelope are per-zone
|
||
instrument performance-map state (D-B), additive/version-bumped. Editor exposure deferred
|
||
to S12 tier (spec-sanctioned). The instrument stays a **read-only bank consumer**.
|
||
|
||
**Notes/decisions:**
|
||
- **S16-F1 (default engine):** default is **Preserve** (Daniel's directive: "I want
|
||
duration-preserving repitching"). Per-zone toggle prominent and cheap to flip for
|
||
drum/one-shot zones that want Varispeed character.
|
||
- **S16-F2 (Preserve engine):** hand-rolled pure OLA `pitch_shift` module chosen over
|
||
`WDL_SimplePitchShifter` (excluded by include-chain). Both share the same
|
||
`PitchEngine::Preserve` contract; `WDL_SimplePitchShifter` is held as the quality/latency
|
||
swap (the HELD item in `PLAN.md`).
|
||
|
||
---
|
||
|
||
## S17 — drop-and-load: drag a capture onto a track's FX button → instantiate ReaSampler 9000 with the capture loaded
|
||
**Goal:** Turn a bank capture into a playable instrument in one gesture. While a capture
|
||
is dragged from the `bank_panel`, a track's TCP **FX button** lights as a drop zone,
|
||
and dropping there instantiates a **ReaSampler 9000** on that track with the dragged
|
||
capture **already loaded and selected** for playback. A third `DragGesture` —
|
||
`InstrumentDrop` — added to the `drag_out` pure module. Mechanism: `TrackFX_AddByName` +
|
||
VST3 component-state injection via `TrackFX_SetNamedConfigParm(..., "vst_chunk", blob)`.
|
||
The shared `instrument_drop` pure module constructs the blob via the instrument's own
|
||
`sample_map::serializeComponentState`/`deserializeComponentState` — one serializer called
|
||
from both artifacts, so the blob format cannot drift. CONTEXT-ARCHIVE.md §Phase S (drop-and-load).
|
||
**Verify (in DAW):** dragging a single capture from the dock over a track's FX button
|
||
highlights it; dropping instantiates ReaSampler 9000 on that track with the dragged
|
||
capture loaded, selected, and MIDI-playable immediately; OS drag-out to Explorer still works
|
||
unchanged; internal bank-to-bank drag still works unchanged; no media item ever inserted
|
||
into the arrange. DAW-verify: whether REAPER's `vst_chunk` write-parm expects the plugin's
|
||
raw IComponent-state bytes or wraps them in a REAPER container header.
|
||
**Depends on:** M11 (`drag_out` gesture machinery), Phase S S4, the load-capture seam
|
||
(VST3 component-state injection — the shared blob contract).
|
||
|
||
- [x] Extend the `drag_out` pure module with the third gesture: `DragGesture::InstrumentDrop`
|
||
when a drag armed with a **single** capture is over REAPER's UI outside the panel client
|
||
rect; `OsDrag` only when it has left REAPER entirely; `Internal`/`OsDrag`/`None` otherwise
|
||
unchanged. `DragState` gains two defaulted fields (`singleCapture`, `overReaperUi`); M11
|
||
callers filling only `{dragging, hasArmedSamples}` get byte-identical M11 behavior — the
|
||
existing tests are the non-regression proof. **OPEN QUESTION RESOLVED (multi-capture over
|
||
FX button): REJECT** — only `singleCapture` arms InstrumentDrop; multi-payload over
|
||
REAPER UI falls through to `OsDrag`.
|
||
- [x] Shell (extension): hover-track the pointer over REAPER's UI during the drag; resolve
|
||
the hovered track + its FX button via `GetThingFromPoint` (verified present; its info string
|
||
reports `"fx_chain"`/`"fx_N"` for the FX region); highlight it as a drop target; on release
|
||
drive the drop. FX HOTSPOT resolved — drop target is the FX region (info prefix `"fx_"`),
|
||
not home-grown geometry. `instrument_drop_win::resolveFxDropTarget` implements this.
|
||
**Landed in `src/instrument_drop_win.cpp`.**
|
||
- [x] Shell (extension): on drop, `TrackFX_AddByName(track, "VST3:" + app_version::vstPluginName(),
|
||
false, negative)` to always add a fresh instance; capture the returned FX index; invoke the
|
||
load-capture seam. Batched into one REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`).
|
||
NEVER inserts a timeline item. **Landed: `instrument_drop_win::performInstrumentDrop`.**
|
||
- [x] **ReaSampler 9000 load-capture seam:** the instrument's existing `setState`/`getState`
|
||
already round-trip the full `ComponentState` via
|
||
`sample_map::serializeComponentState`/`deserializeComponentState` (S10). The extension
|
||
REUSES that exact serializer through the new pure `instrument_drop` module
|
||
(`buildInstrumentDropChunk` → `serializeComponentState` → base64). The shared-writer
|
||
requirement is met STRUCTURALLY — the blob format cannot drift. Pure round-trip test decodes
|
||
back through the instrument's own reader and asserts the capture is selected.
|
||
**`instrument_drop` lives at `src/instrument_drop.{h,cpp}`** (extension-side pure module);
|
||
CTest target `instrument_drop_tests`.
|
||
- [x] Tests: gesture disambiguation (inside-panel / over-REAPER-UI / left-REAPER) across
|
||
single- and multi-capture payloads; M11 OS drag-out and internal bank-to-bank drag both
|
||
unchanged. `test_drag_out.cpp` adds the InstrumentDrop cases with M11 cases retained as
|
||
the non-regression guard; `test_instrument_drop.cpp` is the blob round-trip + base64 codec
|
||
coverage. FX hit resolution is REAPER-API-bound (`GetThingFromPoint`) — DAW-verified in the
|
||
shell, not pure-tested (noted honestly).
|
||
|
||
**Notes/decisions:**
|
||
- `resetDragState()` consolidation in `bank_panel` (prior drift cleaned up this wave).
|
||
- Copy-only / no-auto-insert / no-source-deletion invariants all hold: NEVER inserts a
|
||
timeline item; the bank file is not deleted; prune remains the sole file-deleter.
|
||
|
||
---
|
||
|
||
## S18 — VST3 channel isolation: a beta ReaSampler 9000 that pairs with the beta extension only
|
||
**Goal:** Extend Phase V's beta/stable channel split (V4) to the **ReaSampler 9000 VST3
|
||
instrument**, so a beta-built VST is a distinct plugin that pairs only with the beta
|
||
extension, and a stable VST pairs only with stable — installable side-by-side in one REAPER
|
||
with no collision. **One channel per binary; all identity derives from the ONE
|
||
`REASAMPLER_CHANNEL_IS_BETA` bit via `app_version`, no scattered `#ifdef`s.** Mirrors V4's
|
||
philosophy exactly. Two forever-stable VST3 class UIDs committed (the existing stable UID +
|
||
a new beta UID). CONTEXT.md §Phase S (VST3 channel identity — the UID-pair invariant).
|
||
**Verify (in DAW):** stable VST3 (`reasampler_9000.vst3`) and beta VST3
|
||
(`reasampler_9000_beta.vst3`) install side-by-side in one REAPER as distinct plugins;
|
||
a beta instance reads only the beta extension's banks; a stable instance reads only stable's;
|
||
save/reopen rebinds by the correct UID; nothing plays differently (identity/pairing wave only).
|
||
**Depends on:** V4 (`app_version` channel-identity single-source), S1 (VST3 factory identity).
|
||
|
||
- [x] **Beta VST3 class UID (the permanent commitment).** Second FOREVER-STABLE class UID
|
||
(`REASAMPLER_PROC_UID_BETA_1..4`) alongside the existing stable UID in `reasampler_vst.h`.
|
||
A `#if REASAMPLER_CHANNEL_IS_BETA` block selects `REASAMPLER_ACTIVE_UID_*`, which feeds the
|
||
single `kReaSamplerProcessorUID` — no separate beta-named constant. The channel bit selects
|
||
which UID the factory registers (`DEF_CLASS2`) — compile-time, one class per binary. Both UIDs
|
||
frozen forever.
|
||
- [x] **Channel-derived binary + display identity (no scattered `#ifdef`s).** Binary name:
|
||
CMake VST3 target `OUTPUT_NAME` forks by channel — `reasampler_9000` (stable) /
|
||
`reasampler_9000_beta` (beta) — via `REASAMPLER_VST_OUTPUT_NAME`. Display name: factory
|
||
`DEF_CLASS2` plug-in display string sourced from `app_version::vstPluginName()` — "ReaSampler
|
||
9000" / "ReaSampler 9000 beta". Editor title band + S6 embed-strip label channel-aware from
|
||
the same accessor.
|
||
- [x] **Factory vendor/version strings channel-aware** where V4 does the equivalent. Version
|
||
display carries the `-beta` render (`appVersion()` yields `"0.9.01-beta"` on beta).
|
||
- [x] **Pairing-surface invariant recorded** (no new code — a documented guarantee): channel
|
||
isolation is structural — all wire keys live under the channel-derived `kProjExtNamespace()`,
|
||
so a future wire key that forgets to isolate is impossible by construction. The invariant
|
||
is recorded in CONTEXT.md.
|
||
- [x] **DAW-verify contract (the acceptance gate, no unit test — identity is a shell fact).**
|
||
Both channels installed side-by-side: each browser sees only its channel's banks; a
|
||
project saved with a beta instance reopens rebinding to the beta VST and restores its state.
|
||
|
||
---
|
||
|
||
## S-VIEW-BUG-1 — drop-to-FX bug fix (Wave 1)
|
||
**Goal:** Dropping a capture onto a track's FX chain (TCP FX button or FX-chain window) must
|
||
instantiate + init ReaSampler 9000, not fall through to arrange-as-audio.
|
||
**Root cause (diagnosed and fixed in Wave 1):** the FX-hotspot classifier in the pure
|
||
`instrument_drop` module only matched FX-chain and floating-window hotspot strings (`fx_*`);
|
||
a drop on the TCP FX button (`tcp.fx`) or MCP FX button (`mcp.fx`) was not matched and fell
|
||
through to `OsDrag` (arrange-as-audio).
|
||
**Fix:** widened then narrowed the predicate in `instrument_drop` to match `fx_*` / `tcp.fx` /
|
||
`mcp.fx`; unit-tested in `instrument_drop_tests` at the boundary. The shell (`instrument_drop_win`)
|
||
calls the updated pure predicate unchanged.
|
||
**Verify (in DAW):** DAW-confirmation pending Daniel's post-merge smoke test — drop → a playing
|
||
instance on the track, one Ctrl-Z removes it.
|
||
|
||
- [x] Pure `instrument_drop` predicate widened: `infoNamesFxHotspot` now matches `fx_*` / `tcp.fx` /
|
||
`mcp.fx`; unit-tested at each matched and unmatched hotspot string.
|
||
- [x] Shell unchanged — calls the updated pure predicate.
|
||
|
||
---
|
||
|
||
## S-VIEW-SIZE-1 — 1080p default window size (Wave 1, interim)
|
||
**Goal:** The editor opens too small (`ViewRect(0,0,560,400)`); set a larger default sized for
|
||
the three-band Sample face on 1080p.
|
||
**What landed (interim):** default `ViewRect` bumped to **840×560** and a `checkSizeConstraint`
|
||
minimum floor added. The mechanism (`getSize`/`setRect`/`checkSizeConstraint`/`onSize`/`canResize`
|
||
in `vendor/vst3sdk/public.sdk/source/common/pluginview.h`) was verified correct at this point.
|
||
**NOTE:** Wave 2 (T-SHELL) will re-tune the final numbers to the three concrete Sample-face band
|
||
heights once the Sample view layout is built; 840×560 is the correct starting point, not the
|
||
final tuned value.
|
||
**Verify (in DAW):** opens at 840×560 showing more of the editor surface than before; cannot
|
||
shrink below the floor. Final tuning deferred to T-SHELL.
|
||
|
||
- [x] Default `ViewRect` bumped to 840×560.
|
||
- [x] `checkSizeConstraint` minimum floor enforced.
|
||
|
||
---
|
||
|
||
# Phase S — editor view-model redesign (three views: Sample / Browse / Zone)
|
||
|
||
> **Additive Phase S sub-phase (S-VIEW; Daniel, 2026-07-27, r9).** Merged to dev (Wave 1
|
||
> 2026-07-27, Wave 2 2026-07-27 — `Merge pS-w2-t1-shell` + `Merge pS-w2-t2-velcurve`,
|
||
> Wave 3 2026-07-27 — `Merge pS-w3-velcurve-ui`). Integrated suite green.
|
||
> **S-VIEW complete — all ten points landed.**
|
||
|
||
## S-VIEW-1 — three-view navigation model
|
||
**Goal:** Retire the flat Browser|Zones toggle; introduce Sample (home/default), Browse (modal
|
||
overlay over Sample, select+confirm), Zone (dedicated surface, own button). Empty state surfaces
|
||
Browse as the dominant call-to-action. Fresh instance stays silent (S10 reversal). Fork
|
||
S-VIEW-F3 SETTLED — full-window overlay: Browse renders as a full-window modal over Sample
|
||
(not a centered sheet). See CONTEXT.md §S-VIEW navigation contract.
|
||
|
||
- [x] Flat Browser|Zones toggle retired; three-view model: Sample home, Browse full-window modal
|
||
over Sample, Zone dedicated surface with its own button.
|
||
- [x] Empty state surfaces Browse as the dominant call-to-action; fresh instance is silent
|
||
(S10 reversal preserved).
|
||
- [x] S-VIEW-F3 settled and implemented: Browse is a full-window modal overlay over Sample.
|
||
|
||
---
|
||
|
||
## S-VIEW-2 — Sample view (the new main face)
|
||
**Goal:** Compose the home face: enlarged **hero waveform** with the S11 markers (moved from
|
||
Browse), a **fenced root affordance**, the **Mono/Stereo toggle** (moved from Browse), and the
|
||
**"Modes-and-down" control strip** (Mode / Pitch engine / AHDSR|Trigger / AD pitch env — moved
|
||
from Zone's param panel, single-capture one-zone storage per S15-F2). Reference grammar:
|
||
Simpler / Phase Plant (labelled value-strip under a hero waveform).
|
||
|
||
- [x] Hero waveform with S11 markers (start/loop start/loop end) moved to Sample face.
|
||
- [x] Fenced root affordance on the Sample face.
|
||
- [x] Mono/Stereo toggle moved from Browse to Sample.
|
||
- [x] "Modes-and-down" control strip (Mode / Pitch engine / AHDSR|Trigger / AD pitch env) moved
|
||
from Zone's param panel to the Sample face (single-capture one-zone storage per S15-F2).
|
||
|
||
---
|
||
|
||
## S-VIEW-3 — envelope overlay + draggable nodes
|
||
**Goal:** Draw the amp envelope (AHDSR for Gate, fade/%-length for Trigger) as a curve over the
|
||
Sample waveform at accurate wall-clock time. Pure `envelope_overlay` module (params + frame-length
|
||
→ polyline; unit-tested); shell traces it in an accent hue. Breakpoints are draggable handles
|
||
(S-VIEW-F2 SETTLED): X → segment time, Y → level on level-breakpoint nodes (sustain drags both
|
||
axes), monotonic-in-time + range-clamped. Pure `envelope_edit` module (node hit-test +
|
||
pixel-delta→clamped-param inverse map; mirror of `card_drag`; unit-tested). Both surfaces
|
||
read/write the same `PerformanceZone` envelope fields. Wave 1 landed the pure modules;
|
||
Wave 2 wired them in `reasampler_editor.cpp`. The Trigger frames↔fraction conversion was
|
||
extracted to a new pure unit-tested `trigger_seam` module (startFrame threaded correctly).
|
||
|
||
- [x] **Wave 1:** `envelope_overlay` + `envelope_edit` pure modules landed and unit-tested.
|
||
- [x] **Wave 2:** `reasampler_editor.cpp` traces the overlay and wires draggable node handles.
|
||
- [x] **Wave 2:** `trigger_seam` pure module — Trigger frames↔fraction converter (pack and unpack
|
||
directions, unit-tested); `startFrame` threaded correctly through the seam.
|
||
|
||
---
|
||
|
||
## S-VIEW-4 — preview-trigger + velocity knob
|
||
**Goal:** A button firing the sampler at the loaded capture's root note through the live voice
|
||
engine (off the audio-thread commit path — no MIDI controller needed) + an adjacent velocity
|
||
knob. Preview velocity PERSISTS (S-VIEW-F1 SETTLED): `previewVelocity` on `ComponentState` via
|
||
an envelope bump to v6 (`src/vst/sample_map.h`), round-tripped through `getState`/`setState`
|
||
over `IBStream`. Zones payload untouched; older blobs lift to a mid default. Wave 1 landed the
|
||
field; Wave 2 landed the preview button + velocity knob UI.
|
||
|
||
- [x] **Wave 1:** `previewVelocity` field on `ComponentState`, envelope v5→v6 bump, processor
|
||
`getState`/`setState` round-trip, clamped 1..127.
|
||
- [x] **Wave 2:** preview button + velocity knob UI in `reasampler_editor.cpp`; RT-safe
|
||
off-thread preview-note mailbox on the processor.
|
||
|
||
---
|
||
|
||
## S-VIEW-5 — Browse reduced to choosing (modal over Sample)
|
||
**Goal:** Keep search + bank tabs + captures grid + scroll + selection; add confirm/cancel
|
||
(double-click loads). Remove the large waveform preview, Mono/Stereo toggle, root keyboard-strip
|
||
(all moved to Sample), and the loop-point labels + track-root message (cut). Render as a
|
||
full-window modal overlay (S-VIEW-F3 SETTLED — implemented here and in S-VIEW-1).
|
||
|
||
- [x] Search + bank tabs + captures grid + scroll + selection retained.
|
||
- [x] Confirm/cancel affordance added; double-click loads.
|
||
- [x] Large waveform preview, Mono/Stereo toggle, root keyboard-strip, loop-point labels, and
|
||
track-root message removed from Browse.
|
||
- [x] Browse renders as a full-window modal overlay over Sample (S-VIEW-F3 settled and shipped).
|
||
|
||
---
|
||
|
||
## S-VIEW-6 — key-tracking parameter
|
||
**Goal:** Per-`PerformanceZone` scalar on keyboard pitch tracking around the root (100% = 12-tone-ET,
|
||
0% = no tracking, 200% = double). Additive/version-bumped, defaults 100% (bit-identical). Key-track
|
||
math in the pure sampler core (unit-tested note/root/keyTrack → ratio), applied in both Varispeed
|
||
and Preserve repitch. Surfaces as a control on the Zone param panel + Sample control strip.
|
||
Wave 1 landed the field/math/apply; Wave 2 landed the UI control.
|
||
|
||
- [x] **Wave 1:** per-`PerformanceZone` `keyTrack` field (zones payload v6, default 100%
|
||
bit-identical), pure ratio math, applied in both Varispeed and Preserve engines.
|
||
- [x] **Wave 2:** UI control on the Zone param panel + Sample control strip.
|
||
|
||
---
|
||
|
||
## S-VIEW-7 — piano-key pattern on the keyboard strip
|
||
**Goal:** Overlay the actual alternating white/black (bright/dark per palette) key pattern over
|
||
the pastel spectral fill so the strip reads as a keyboard. Pure `keyboard_strip` gains a
|
||
natural/accidental predicate (12-tone, unit-tested); shell draws the two-tone overlay. Shared
|
||
by the Zone strip + Sample root affordance. Wave 1 landed `isNaturalKey`; Wave 2 drew the overlay.
|
||
|
||
- [x] **Wave 1:** pure `isNaturalKey` predicate on `keyboard_strip`, unit-tested.
|
||
- [x] **Wave 2:** `reasampler_editor.cpp` draws the two-tone overlay over the spectral fill.
|
||
|
||
---
|
||
|
||
## S-VIEW-8 — Zone view retained + wired
|
||
**Goal:** Keep +Add Zone / Delete, the per-zone keyboard strip (now with the piano pattern), the
|
||
Low/High/Root numeric-entry legend, and the per-zone param panel; add the key-tracking control.
|
||
Nothing from today's Zones view dropped.
|
||
|
||
- [x] +Add Zone / Delete retained.
|
||
- [x] Per-zone keyboard strip with piano-key pattern.
|
||
- [x] Low/High/Root numeric-entry legend retained.
|
||
- [x] Per-zone param panel retained.
|
||
- [x] Key-tracking control added to Zone view.
|
||
|
||
---
|
||
|
||
## S-VIEW-9 — velocity→amp transfer curve (pure core + engine application)
|
||
**Goal:** New pure `velocity_curve` module — eval (monotone cubic Hermite spline,
|
||
Fritsch–Carlson slope limiting) + control-point editing (add/move/delete x-ordered +
|
||
box-clamped) + hit-test + pixel-delta→clamped-point inverse map; unit-tested at eval +
|
||
clamp/order boundaries. Additive `velocityCurve` field on `PerformanceZone` (instrument-owned,
|
||
D-B), zones-payload version axis bumped to **v7**; default = **flat y=1** (R10-F1 SETTLED —
|
||
Option A, Daniel 2026-07-27: "any velocity plays at full level"); older ≤v6 blobs lift to flat
|
||
y=1. Applied at `Voice::start()` — replaces `velocityGain_ = velocity / 127.0` with
|
||
`velocityGain_ = curve.eval(velocity)`, off the per-frame path (no new RT work).
|
||
**Default is a deliberate non-back-compat behavior change:** the prior engine used linear
|
||
`velocity/127`; existing zones' soft hits will play at full level after upgrade. Flagged and
|
||
accepted by Daniel.
|
||
|
||
- [x] Pure `velocity_curve` module: `eval(velocity 0–127)→amp 0–1` via Fritsch–Carlson monotone
|
||
cubic Hermite spline (no overshoot outside [0,1]; collinear points reduce to exact linear ramp).
|
||
- [x] Control-point editing: `addPoint`, `movePoint` (x-clamped between neighbours, endpoints
|
||
x-pinned), `deletePoint` (endpoints not deletable); `fromPoints` repair-on-deserialize.
|
||
- [x] Hit-test + inverse map: `pointAtPixel`, `resolvePointDrag` (pure; mirror of `envelope_edit`).
|
||
- [x] Additive `velocityCurve` on `PerformanceZone`, zones-payload v7; pre-v7 blobs lift to
|
||
flat y=1; `velocity_curve_tests` CTest target.
|
||
- [x] Applied at `Voice::start()` — `velocityGain_` now set from `curve.eval(velocity)`.
|
||
- [x] Default flat y=1 — back-compat caveat documented and accepted.
|
||
|
||
---
|
||
|
||
## S-VIEW-10 — velocity-curve editor UI (shell, Sample + Zone views)
|
||
**Goal:** Draggable transfer-curve editor rendered through the L1 kit in BOTH the Sample view (a
|
||
curve box beside the hero band) and the Zone per-zone param panel: the velocity→amp spline drawn
|
||
as the `eval` polyline over a 0–127 × 0–1 box, with draggable control points — add on empty-click
|
||
(inside the mapping box), move, Alt-click delete interior, drag-off-box delete with a warn-state
|
||
affordance. All coordinate math in the pure `velocity_curve` module (two new tested helpers
|
||
`pixelFromPoint`/`pointFromPixel`); reads/writes the existing per-`PerformanceZone`
|
||
`velocityCurve` field (zones payload v7 — no schema change). Mirrors the S-VIEW-3 envelope-node
|
||
interaction grammar (snapshot-at-grab → off-audio-thread commit). Shell: `reasampler_editor.cpp`.
|
||
|
||
- [x] `pixelFromPoint` / `pointFromPixel` helpers added to `velocity_curve` — coordinate mapping
|
||
between curve-point space (velocity 0–127, amp 0–1) and pixel box; unit-tested in
|
||
`velocity_curve_tests`.
|
||
- [x] Curve editor drawn in Sample view (curve box beside hero waveform band): `eval` polyline in
|
||
accent hue over 0–127 × 0–1 grid, draggable node markers per control point.
|
||
- [x] Add on empty-space click inside the box; move on drag; Alt-click to delete interior points;
|
||
drag-off-box delete with warn-state affordance.
|
||
- [x] Same editor in Zone per-zone param panel (one curve per zone, same interaction grammar).
|
||
- [x] Snapshot-at-grab → off-audio-thread commit, matching S-VIEW-3 envelope-node pattern.
|
||
- [x] Reads/writes existing `velocityCurve` on `PerformanceZone`; zones payload remains v7 — no
|
||
schema change.
|
||
|
||
---
|
||
|
||
## Phase S — product name (ReaSampler 9000)
|
||
The MIDI-playback instrument's product name is **ReaSampler 9000** (Daniel, 2026-07-26,
|
||
on DAW-testing the S1–S6 instrument). The extension remains **ReaSampler**; the instrument
|
||
is **ReaSampler 9000**. Framing + propagation surfaces:
|
||
`docs/product/midi-playback.md` §Product name.
|
||
|
||
- [x] Propagate the display name **ReaSampler 9000** across user-visible surfaces: the VST3
|
||
class **display name** string (in the factory registration), the `IPlugView` editor title
|
||
band (currently "ReaSampler Instrument"), the S6 embed-strip label, and the Phase S docs.
|
||
**Do NOT change the VST3 class UID** — instances in already-saved projects key off it; a
|
||
UID change orphans every existing instance.
|
||
- [x] **Rename the binary filename too (S-NAME-1 SETTLED, Daniel 2026-07-26):** rename the
|
||
built VST3 module (CMake `OUTPUT_NAME` / target artifact — e.g. `reasampler_9000.vst3`)
|
||
alongside the display strings, so the on-disk name matches the product name. Record the
|
||
full rename surface: **CMake output name** (the second VST3 target's artifact name), the
|
||
**factory vendor/name strings**, the **`IPlugView` editor title**, and the **S6 embed
|
||
label**. Do NOT touch the **VST3 class UID** (unchanged — the compat anchor).
|
||
|
||
---
|
||
|
||
## Phase S — S-VIEW-1 through S-VIEW-10 (rolled-up)
|
||
|
||
- [x] **S-VIEW-1 through S-VIEW-10** — all landed (Wave 1 core + Wave 2 shell + Wave 3 velocity-curve editor UI). See entries §S-VIEW-1 through §S-VIEW-10 above for the full entry set.
|
||
|
||
---
|
||
|
||
# Phase D2 — Two-canvas (item-level mode projection; additive to D1)
|
||
|
||
> **Design View sub-phase.** Extends D1's track-level mode projection to **item
|
||
> level** via REAPER 7 fixed lanes: on a track present in both stances, each mode
|
||
> owns a fixed lane — the active mode's lane shows and plays, the inactive mode's is
|
||
> hidden and silenced — so a Design take and an Arrange take can share the same
|
||
> track and time position without colliding on the view. Nothing in D1 changes.
|
||
> Runtime floor rises to **REAPER 7** for this sub-phase (no version-gate branch;
|
||
> below v7 it is simply unavailable). Authoritative spec: **CONTEXT.md §Two-canvas
|
||
> sub-phase (Phase D2 / Phase E)** and the surrounding §Design View — additive phase
|
||
> spec. Product framing: `docs/product/design-view.md` §Two-canvas direction.
|
||
>
|
||
> **D2-W1 (pure lane extension), D2-W2 (shell: lane application + new-content
|
||
> detection), D2-W3-A (lane minting + item→lane assignment + persist round-trip),
|
||
> and D2-W3-B (item-level mode actions + W3-A polish) have all landed** — see
|
||
> individual entries above. **Phase D2 is functionally complete.**
|
||
>
|
||
> **Deferred:** panel UI indicator for per-track lane/mode state (a per-track
|
||
> lane-split marker). The mode switch already shows the active mode; no natural
|
||
> cheap home for a per-track indicator was found in the bank panel. Explicitly
|
||
> deferred — not silently dropped. Can be picked up later if wanted.
|
||
|
||
---
|
||
|
||
## Milestone 9 — slots (MPC-style)
|
||
|
||
> **Abandoned (Daniel, 2026-07-27) — will not be built.**
|
||
|
||
**Goal:** "Capture to slot N" / "insert slot N", MIDI-bindable. CONTEXT-ARCHIVE.md
|
||
Build order 9.
|
||
**Verify (in DAW):** Slot capture and slot insert fire from MIDI bindings; slot
|
||
state persists via the index.
|
||
|
||
Slot model + slot↔sample assignment.
|
||
"Capture to slot N" / "insert slot N" actions, MIDI-bindable.
|