# ReaSampler — implementation briefing A native C++ REAPER extension that captures any arbitrary audio source into a per-project **sample bank** (cached files + a docked grid), decoupled from the arrange view, with keyboard/MIDI-bindable capture and placement. Built as a precision tool: deterministic, non-destructive, no clutter. This document is the spec. Read the existing scaffold first (`src/main.cpp` is the REAPER<->extension contract; the pure/testable-core split in `src/mpe_model.*` is the pattern to preserve — the MPE model is being replaced, the *discipline* is not). Verify every REAPER API name and signature against `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use — the API names in this brief are correct-by-intent but treat them as hints, not gospel, and check argument order/types. ## The load-bearing principle **Capture and placement are separate acts.** Capturing audio writes a file to the bank and adds an index entry — it NEVER puts an item in the arrange. Placement is a distinct, on-demand action (insert / drag). Any code path that auto-inserts a capture into the timeline violates the entire point of the tool and must be rejected in review. This single rule is why the tool exists. ## Settled decisions - **Capture modes:** offline render (deterministic, the default) AND realtime record (for hardware / performed FX). Both sit behind one capture interface and produce identical bank entries. - **Bank scope:** per-project, travels with the `.rpp`. Files live in a project-relative subfolder; the index persists in project ext state. No absolute paths anywhere in the index. - **Material:** must handle full-mix/stem bounces, chops/one-shots, and single-cycle/wavetable grabs equally. That means exact sample-accurate bounds, explicit tail control, channel-count preservation, and loop/zero-crossing handling all matter from day one. ## Module architecture Preserve the scaffold's split: pure, REAPER-free logic in one set of files (unit-tested outside the DAW via the existing `tests/` + CTest harness), REAPER- facing shells in another. Pure (no REAPER types, fully unit-tested): - `bank_model` — the `Sample` metadata struct and the `BankIndex` (add / remove / query / tier moves / dedup-by-hash) plus JSON serialize/deserialize to a `std::string`. This is the heart; test it hard. - `peaks` — compute waveform min/max bins from raw PCM. Feed it a known signal (sine, ramp) and assert the envelope. We compute our own thumbnails from the captured file rather than depending on REAPER's peak API — we own the file format, so this is simpler, testable, and dependency-free. REAPER-facing: - `capture` — the `ICaptureBackend` interface plus `OfflineRenderBackend` and `RealtimeRecordBackend`. Input: a `CaptureRequest` (source mode, time range, wet/dry, tail, SR/bit-depth/channels, output path). Output: a finished file + a populated `Sample` handed to `bank_model`. - `insert` — placement via `InsertMedia`; conform-to-project-tempo vs literal, as an explicit flag (never silent stretching). - `bank_panel` — the docked grid: LICE-drawn thumbnails, audition, multi-select, keyboard navigation. Reuses the docking setup already in `mpe_view.cpp`. - `persist` — project ext state <-> `bank_model` JSON; project-relative path resolution (resolve bank folder from the current project path). - `actions` — registers the capture/placement/slot action family and routes each to the modules above (the `command_id` + `gaccel` + `hookcommand` pattern from `main.cpp`). ## Data model (sketch — refine in code) `Sample`: id, display name, relative file path, source mode, source range (start/ end in project time + PPQ), track GUID(s) if applicable, wet/dry, channel count, sample rate, length (seconds + musical/beats), capture tempo, optional key, peak/RMS/LUFS, clip flag, tier (scratch | archive), content hash, provenance (parent sample id + FX-chain snapshot string, when resampled from another sample), created timestamp. `BankIndex`: ordered collection of `Sample`, keyed by id; hash lookup for dedup; tier filtering; JSON round-trip. Scratch tier is auto-prunable; archive is kept. ## REAPER API surface (verify all signatures) Offline render (the crux — prototype this first): - Drive render settings with `GetSetProjectInfo` (`RENDER_BOUNDSFLAG`, `RENDER_STARTPOS`, `RENDER_ENDPOS`, `RENDER_TAILFLAG`, `RENDER_TAILMS`, `RENDER_SRATE`, `RENDER_CHANNELS`, `RENDER_SETTINGS` for source = master mix / selected tracks / selected items / time selection, wet vs dry) and `GetSetProjectInfo_String` (`RENDER_FILE`, `RENDER_PATTERN`, `RENDER_FORMAT`). - Trigger a no-dialog render via the appropriate render action / `RENDER_SETTINGS` bit. Confirm the exact command id and the "render without opening dialog" flag against current REAPER — do not assume; test that it runs headless. - Determinism is a hard requirement: two identical requests must produce bit-identical files (enables the null test below). Realtime record: - Standard "resample track" recipe: create a hidden track, set its record mode to record-output (latency-compensated) or route the source to it via a send, arm (`I_RECARM`), `CSurf_OnRecord`, run for the range, `CSurf_OnStop`, then move the recorded source file into the bank and delete the temp track. Verify `I_RECMODE` / `I_RECINPUT` values for output-recording. Sources & metadata: - Time selection: `GetSet_LoopTimeRange`. Razor edits: `GetSetMediaTrackInfo_String(track, "P_RAZOREDITS", ...)`. Tempo: `Master_GetTempo` / `TimeMap2_timeToBeats` / `GetProjectTimeSignature2`. Selected items/tracks: `CountSelectedMediaItems` / `GetSelectedTrack`. Placement: - `InsertMedia(path, mode)` at edit cursor / new track / replace selection (verify mode bits). `SetEditCurPos`. Wrap edits in `Undo_BeginBlock2` / `Undo_EndBlock2`. Persistence & paths: - `SetProjExtState` / `GetProjExtState` (namespace e.g. `"reasampler"`) for the index JSON. Resolve project folder via `EnumProjects` / `GetProjectPathEx`; store the bank under a project-relative subfolder; keep only relative paths in the index. ## Build order (each milestone independently testable) 1. `bank_model` + JSON round-trip + unit tests. (pure — no REAPER) 2. `peaks` + unit tests. (pure) 3. Offline capture of the time-selection master mix to a wav in the project bank folder; add a `Sample`; log it to the console. (the render-driving spike) 4. `persist`: write the index to proj ext state, reload on project open; confirm it survives Save / Save As. (bank travels with the .rpp) 5. `bank_panel`: docked grid with thumbnails, audition, selection. 6. `insert`: "insert selected sample at edit cursor" action via `InsertMedia`. 7. Capture action family: master / selected tracks / selected items / razor area, each with wet-dry and tail options, all registered as bindable actions. 8. `RealtimeRecordBackend` behind the same interface. 9. Slots: "capture to slot N" / "insert slot N", MIDI-bindable (MPC-style). 10. Provenance + "re-capture from source"; null-test verify action. 11. Polish: batch capture (per selected item / per razor area), resample-and-mute- source, conform-on-insert, drag-out to OS. ## Precision invariants (enforce, test) - **Null test:** a dry offline capture of a range, re-inserted at its source position, nulls to silence against the source. Ship this as a verification action; it is the tool's trust anchor. - **Bit-identical repeats:** identical offline requests produce identical files. - **Non-destructive:** capture never mutates source items or tracks (realtime's temp track is created and removed cleanly; source routing is restored). - **Exact bounds:** no rounding of the requested range; no added silence unless a tail is explicitly requested; channel count preserved (no silent stereo fold). - **Relative paths only** in the persisted index. ## Non-goals / guardrails - No auto-insertion of captures into the arrange (see the load-bearing principle). - Native OS drag-out is deferred to the final milestone — `InsertMedia`-driven placement is the primary path and must work first. - Do not depend on REAPER's peak API for thumbnails; compute from the captured file. - Do not silently time-stretch on insert; conform is opt-in. - Do not trust this brief's API names blindly — verify against the SDK header. ## Open questions to resolve during build - Exact no-dialog render command/flag on the current REAPER build. - Realtime record routing that captures wet master output without altering the user's monitoring. - Audio format for the bank (wav bit depth default; allow float for wavetable fidelity). - Thumbnail cache: recompute vs store peak bins alongside the index. --- # Design View — additive phase spec > **Additive section.** This is a standalone phase parallel to — not part of — the > M0–M11 capture roadmap above. Nothing above changes. Product framing (workflow > narrative, screenset differentiation, N-mode reasoning, design-direction calls) > lives in `docs/product/design-view.md`; this section is the authoritative > technical spec, matching the house style of the capture spec. Same standing > discipline applies: **verify every REAPER API name/signature against > `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use.** ## What it is A **track-visibility-plus-processing "mode" system**, toggled from the ReaSampler window. Tracks used purely for sound design (scratch oscillators, FX mangling, resampling sources) are tagged into **Design** mode; the arrangement's real tracks are **Arrange** mode (the default). Toggling to a mode **hides and disables** the tracks that don't belong to it. Workflow value: mental separation + clutter elimination — be in Design view, resample into the bank, flip to Arrange, place it. **This is not a separate canvas.** REAPER has exactly one arrange timeline. Design View is the *same timeline* with a curated, filtered track set and the inactive tracks parked — not a second surface, window, or duplicated project. It is the visibility/processing analog of the capture pillar's load-bearing rule: **designing and arranging are separate stances on one timeline**, and the tool enforces the separation **without ever destroying the user's real state.** ## Settled decisions - **Membership.** Default = Arrange; every untagged leaf belongs to it. Leaves **opt in** to Design (or any mode). No track appears in two modes at once except (a) via an explicit **show-both** toggle, or (b) parent/folder derivation. - **Parents are derived, never tagged.** A parent/folder track is visible in mode M if **either** (a) any descendant leaf is visible in M (descendant-derived), **or** (b) the parent belongs to M by its **own membership** — and an untagged parent is an Arrange member by default. So an untagged folder carrying its own FX/media above all-Design leaves shows in **both** Arrange (its own default) and Design (derived from its children). A parent is **never parked** in any mode it is visible in. Rule of thumb: **tag leaves; parents follow** — the common case, since a content-bearing folder still surfaces wherever its own membership places it. Master track is always visible and never touched. - **N-mode model, two-mode UI.** The data model carries arbitrarily many modes; the UI ships **Arrange** + **Design**. A mode is (stable id, display name, ordinal). Arrange is special only as the default home for untagged leaves. - **Parking a track** (inactive-mode leaf): `B_SHOWINTCP=0`, `B_SHOWINMIXER=0` (hide both panels), `B_MAINSEND=0` (out of mix), `I_FXEN=0` (FX bypassed), and `TrackFX_SetOffline(track, fx, true)` for **each** FX (reclaim CPU). Full CPU-park is the deliberate choice over mix-removal-only. - **Never touches `B_MUTE` / `I_SOLO`.** The tool owns only visibility, `B_MAINSEND`, `I_FXEN`, and per-FX offline — on every managed leaf, tagged or untagged. User mute/solo survives every toggle untouched. - **Persistence.** Membership index + last-active mode + per-track flag snapshots ride in the existing `"reasampler"` project ext-state namespace and travel with the `.rpp`. On project open, reapply the active mode's visibility + processing. ## Precision invariants (enforce, test) - **Non-destructive restore.** For every flag the tool drives, snapshot the prior value **before** parking; on toggle-back restore **from the snapshot**, never to a hardcoded "on." Round-trip (snapshot → park → restore) returns every driven flag to its captured value. This is the phase's trust anchor — the analog of the capture null test — and is enforced in the pure layer. - **Mute/solo untouched.** No toggle ever reads or writes `B_MUTE` / `I_SOLO`. - **Master untouched.** The tool never drives the master track's visibility flags (the SDK forbids `B_SHOWINTCP`/`B_SHOWINMIXER` on master; the invariant agrees). - **GUID-keyed, reorder-safe.** Membership keys on track GUID (`GetTrackGUID`), never track index; tolerates unknown/stale GUIDs (prune on reconcile). - **Relative/portable state only** in the persisted view section (GUID strings, mode ids — no absolute paths, no index positions). ## Documented caveat Offlined FX **re-instantiate** when a track returns to the active mode. Stateful plugins (convolution, loaded samplers, tail-holding effects) re-initialize on return — possible load hitch, un-persisted internal state lost. Accepted cost of the CPU reclaim; surface it at the toggle affordance (tooltip). ## Module architecture (preserve the pure/shell split) Pure (no REAPER types, unit-tested — the mirror of `bank_model`): - `view_mode_model` — mode registry (id/name/ordinal; Arrange + Design seeded); membership index (`track GUID → { mode ids }` + per-track show-both flag; add / remove / retag / query); **folder-tree-aware** visibility derivation (given the current parent↔child tree supplied by the shell + the active mode, compute the visible set); the **parking/restore planner** (given active mode + snapshot record, emit the exact (track, flag, value) operation lists for park and restore — where the restore invariant is enforced); JSON round-trip of modes + membership + show-both + snapshots + active mode. REAPER-facing: - `view` shell — reads `I_FOLDERDEPTH` across the track list to build the parent↔child tree and feeds it to `view_mode_model`; applies the planner's operations via `SetMediaTrackInfo_Value` (`B_SHOWINTCP` / `B_SHOWINMIXER` / `B_MAINSEND` / `I_FXEN`) and `TrackFX_GetCount` + per-FX `TrackFX_SetOffline`; snapshots prior flag values before parking; resolves GUIDs via `GetTrackGUID` / `guidToString` / `stringToGuid`. Never touches master visibility, never touches `B_MUTE` / `I_SOLO`. - `persist` (slice) — serialize/deserialize the view section into the `"reasampler"` namespace alongside the bank; on project open, rebuild the tree and reapply the active mode. - `actions` (entries) — toggle active mode; activate mode: Arrange / Design; tag/untag selected tracks → mode; show-both for selected tracks. Registered with the `command_id` / `gaccel` / `hookcommand` pattern; toggle + mode-jumps MIDI-bindable. - UI (in the ReaSampler / bank_panel window) — a **segmented mode switch** (`[ Arrange | Design ]`) in the window header, active segment lit; small per-mode membership count; the offlined-FX caveat as a tooltip. Tag/untag acts on the current REAPER track selection, not a per-track widget. ## Show-both semantics A **per-track "pin visible across modes"** flag that **re-enables processing** whenever shown. A show-both leaf appears in every mode's visible set and is **never parked** — its driven flags stay at snapshot/restored values, FX online, in the mix. ("Show but keep parked" is not offered — a visible-but-silent-and-offline track is clutter with a thumbnail.) Stored on the membership record; persists; togglable per selection. ## REAPER API surface (verify all signatures) - Visibility/routing/FX flags via `GetMediaTrackInfo_Value` (snapshot) / `SetMediaTrackInfo_Value` (apply): `B_SHOWINTCP`, `B_SHOWINMIXER`, `B_MAINSEND`, `I_FXEN`. (Note: brief cited `B_SHOWINMCP`; verified SDK name is `B_SHOWINMIXER`.) - Per-FX offline: `TrackFX_GetCount` + `TrackFX_SetOffline(track, fx, offline)`. - Folder tree: read `I_FOLDERDEPTH` per track to derive parent↔child structure. - GUID keying: `GetTrackGUID`, `guidToString`, `stringToGuid`. - Persistence: `SetProjExtState` / `GetProjExtState` under `"reasampler"` (shared with the bank index — one blob, two logical sections). - Wrap flag mutations in `Undo_BeginBlock2` / `Undo_EndBlock2` as appropriate. ## Non-goals / guardrails - **No second canvas.** Do not build a parallel arrange surface — reject any such path in review. - **Never touch mute/solo.** Any code path reading/writing `B_MUTE` / `I_SOLO` is a bug. - **Every leaf is managed.** The mode system owns all leaf tracks: an untagged leaf is an Arrange member, and when the active mode is not Arrange it is fully parked and snapshot-restored exactly like a tagged leaf out of its mode. **show-both** is the only way to opt a leaf out of parking. (Parents stay visibility-only, master is never touched — see below.) - **Restore from snapshot, never to a default.** No hardcoded "on" restores. - **Verify API names** against the SDK header before use. ## Open questions to resolve during build - Snapshot durability across a save-while-parked (persisted here by decision; the lean alternative is force-restore-to-Arrange on save — see product notes item 4). - Reconcile behavior when a tagged leaf is deleted or a folder restructured while parked (ignore-and-prune stale GUIDs on next toggle/open). - Interaction with the user having a screenset active (Design View drives the same flags a screenset recall would; last writer wins — confirm no surprising fight).