# 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.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.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.md §Module architecture, §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.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.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.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.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.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.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.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.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.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. - [x] Offlined-FX re-init caveat as a tooltip on the switch. **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. --- ## Milestone 7 — capture action family **Goal:** Bindable capture actions for master / selected tracks / selected items / razor area, each with wet-dry + tail options. CONTEXT.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.