Note the M7 four-mode -> three-FX-scope rework in COMPLETED.md; add the capture FX-scope precision invariant to CLAUDE.md; record the send/routing isolation TODO (coloring sends bleed into item captures) in PLAN.md open questions.
18 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}andsrc/mpe_view.{h,cpp}; removetests/test_mpe_model.cpp. - Rename the CMake
project()and the extension MODULE target fromreaper_mpeviewtoreaper_reasampler(binaryOUTPUT_NAMElikewise); updatePREFIX ""/ platform SUFFIX blocks to the new target name. - Replace the pure
mpe_modelstatic lib +mpe_model_testsexecutable withbank_model(pure static lib) +bank_model_tests; keep the CTest wiring. - Repoint
src/main.cpp: drop thempe_view.hinclude and allMpeView_*calls (toggle / IsOpen / OnTimer / Cleanup); stub the extension entry so it loads, logs to console, and registers nothing MPE-specific. Thecommand_id/gaccel/hookcommandregistration 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.mdlayout/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
Samplewith 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).
ICaptureBackendinterface +CaptureRequest(source mode, time range, wet/dry, tail, SR/bit-depth/channels, output path). CONTEXT.md §capture.OfflineRenderBackend: driveGetSetProjectInforender settings +GetSetProjectInfo_Stringfile/pattern/format; verify every flag againstvendor/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
Samplefrom the finished file; hand tobank_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/GetProjExtStateunder 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", keysbank_index(serialized JSON) andproject_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 callsMarkProjectDirtyso 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. TheReaProject*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
peaksbins. - 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/StopPreviewAPI, read-only — display + select + audition only, never inserts into the arrange. Single stop-funnel ensures a leak-free preview lifecycle. Flagged undocumented assumption:StopPreviewdetaches the source before returning; mitigated by the single-funnel design. Escalation path if a runtime pop appears: switch toStartPreviewFade+ 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.
InsertMedia(path, mode)at edit cursor (verify mode bits against SDK).- Conform-to-project-tempo vs literal as an explicit flag (never silent).
- 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
InsertMediabase 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
&4stretch-to-time-selection bit is never set; a pureinsert_plantest 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.
- 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 toview_mode_model. - Snapshot prior flag values (
GetMediaTrackInfo_Value) before parking. - Apply park/restore ops (
SetMediaTrackInfo_Valuefor the four flags;TrackFX_GetCount+ per-FXTrackFX_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 thecommand_id/gaccel/hookcommandcontract inmain.cpp; MIDI-bindable. - New
src/track_guid.{h,cpp}— sharedMediaTrack*→ 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(loadFromProjectraises it;main.cpp's timer drains it) —persiststays model-only. - A pure
nextModeIdfree function added toview_mode_model(the N-mode cycle decision behind "toggle"), unit-tested in the existingview_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.
- Segmented mode switch
[ Arrange | Design ]in the window header; active lit. - Wire the switch to the toggle/activate actions from D4.
- Per-mode membership count display.
- Offlined-FX re-init caveat as a tooltip on the switch.
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 newmode_switch_testsCTest target. Mirror ofbank_grid. src/bank_panel.cpp— segmented[ Arrange | Design ]control drawn in the panel header (one lit segment per registered mode, click activates that mode viaview::applyMode), with the grid offset below the header.
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.
- Source resolvers: master mix, selected tracks, selected items, razor area
(
GetSet_LoopTimeRange,P_RAZOREDITS,CountSelectedMediaItems, etc.). - Register each as a bindable action (
command_id/gaccel/hookcommand). - Wet-dry + tail options per action.
- 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 toOfflineRenderBackend(RENDER_* snapshot/restore, dither/normalize off, 32-bit float), produces aSample, adds it to the bank, persists, and callsMarkProjectDirty. 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 (
&8192pre-fader stems) is post-FX. Approximate-dry action variants were removed rather than ship a "dry" that isn't.CaptureRequest.wetDryis retained as the seam for true dry (M10). - Pure
render_settingsmodule maps source mode → RENDER_SETTINGS bits and parsesP_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: 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.
FxBypassGuard(RAII): snapshot → neutralize (FX bypassed viaI_FXEN; gain zeroed viaD_VOL; pan/width/pan-law/mode set to unity viaD_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.