bd5ca2f861
Move completed M6 (insert placement onto selected tracks at edit cursor) from PLAN.md to COMPLETED.md with a decisions note.
239 lines
15 KiB
Markdown
239 lines
15 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.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`.
|