Files
reasampler/COMPLETED.md
T
daniel cf871e195f Archive Milestone 5; correct M4 identity note to GUID-primary
Move completed M5 (bank_panel: thumbnails, audition, multi-select, keyboard)
to COMPLETED.md and close the thumbnail-cache open question; reword the M4
Save-As identity note to the final GUID-primary/pointer-secondary design.
2026-07-23 05:03:40 -04:00

13 KiB

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.

  • Delete src/mpe_model.{h,cpp} and src/mpe_view.{h,cpp}; remove tests/test_mpe_model.cpp.
  • 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.
  • Replace the pure mpe_model static lib + mpe_model_tests executable with bank_model (pure static lib) + bank_model_tests; keep the CTest wiring.
  • 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.
  • 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).
  • 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).

  • 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.
  • BankIndex: ordered collection keyed by id; add / remove / query.
  • Hash lookup for dedup-by-content-hash.
  • Tier model (scratch | archive) + tier-move + tier filtering; scratch marked auto-prunable.
  • JSON serialize/deserialize to/from std::string.
  • 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).

  • Min/max bin computation from interleaved PCM given a target bin count.
  • Multi-channel handling (per-channel envelope; no silent fold).
  • 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).

  • ICaptureBackend interface + CaptureRequest (source mode, time range, wet/dry, tail, SR/bit-depth/channels, output path). CONTEXT.md §capture.
  • OfflineRenderBackend: drive GetSetProjectInfo render settings + GetSetProjectInfo_String file/pattern/format; verify every flag against vendor/reaper-sdk/sdk/reaper_plugin_functions.h.
  • Resolve the no-dialog render command/flag on the current REAPER build (open question) and confirm it runs headless.
  • Populate a Sample from the finished file; hand to bank_model; console-log.
  • 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).

  • SetProjExtState / GetProjExtState under namespace "reasampler".
  • Bank-folder resolution from the current project path (EnumProjects / GetProjectPathEx); store under a project-relative subfolder.
  • 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.

  • Docked window + LICE grid render loop.
  • Thumbnail draw from peaks bins.
  • Audition (play selected sample) + stop.
  • 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.

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.

  • Mode registry: ordered (id, display name, ordinal); Arrange + Design seeded; add/query more modes (prove N-mode, not binary).
  • Membership index: GUID → { mode ids } + per-track show-both flag; add / remove / retag / query; untagged = Arrange.
  • Folder-tree-aware visibility derivation: given a supplied parent↔child tree + active mode, compute the visible set (active leaves, derived-visible parents, show-both leaves, master always in).
  • Parking/restore planner: emit exact (track, flag, value) op-lists for park and restore from active mode + snapshot record.
  • JSON round-trip: modes + membership + show-both + snapshots + active mode.
  • Tests: N-mode add/query; parent follows tagged leaf (multi-mode parent); restore-round-trip returns snapshot values (never hardcoded "on"); show-both leaf never parked; unknown/stale GUID tolerated; JSON lossless.

D2 — view shell (apply flags in the DAW)

Goal: Read the folder tree and drive REAPER flags per the planner. CONTEXT.md §Design View (view shell, REAPER API surface). Verify (in DAW): Toggling active mode hides + parks inactive leaves (B_SHOWINTCP/B_SHOWINMIXER/B_MAINSEND/I_FXEN + per-FX offline) and restores active ones from snapshot. Master untouched. B_MUTE/I_SOLO untouched. Untagged tracks untouched. Parents follow their tagged descendants.

  • Build parent↔child tree from I_FOLDERDEPTH; feed to view_mode_model.
  • Snapshot prior flag values (GetMediaTrackInfo_Value) before parking.
  • Apply park/restore ops (SetMediaTrackInfo_Value for the four flags; TrackFX_GetCount + per-FX TrackFX_SetOffline). Verify flag names/signatures.
  • GUID resolution: GetTrackGUID / guidToString / stringToGuid (never index).
  • Review gate: no path touches master visibility or B_MUTE/I_SOLO, or any untagged track's owned flags.

D3 — persist slice (view state ↔ project ext state)

Goal: Serialize the view section into the "reasampler" namespace alongside the bank; reapply the active mode on project open. CONTEXT.md §Design View (persist). Verify (in DAW): Membership + active mode + snapshots survive Save / Save As / close+reopen; on open, the active mode's visibility + processing is reapplied. Saved-while-parked project restores parked tracks from persisted snapshots (not to a guessed "on").

  • Serialize/deserialize the view section under "reasampler" (shared blob, distinct section from the bank index).
  • Reapply active mode on project open (rebuild tree, run the planner).
  • Confirm survival across Save / Save As; snapshot durability across save-while-parked.

D4 — actions

Goal: Bindable action set for the mode workflow. CONTEXT.md §Design View (actions). Verify (in DAW): Each action registered (bindable in Actions list); toggle + mode-jumps MIDI-bindable; tag/untag acts on the current track selection.

  • Toggle active mode (cycle; extensible to cycle-all for >2 modes).
  • Activate mode: Arrange / Activate mode: Design (direct jumps).
  • Tag selected tracks → Design / → Arrange; Untag selected (= → Arrange).
  • Show-both for selected tracks (toggle).
  • Register each (command_id/gaccel/hookcommand); toggle + jumps MIDI-bindable.

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.