Files
reasampler/CONTEXT.md
T
daniel 2d040d6896 Docs: reflect Phase D1 landing
Rename view_model -> view_mode_model across Design View spec docs;
archive D1 to COMPLETED.md; add view_mode_model_tests to CLAUDE.md
targets table and pure-core list.
2026-07-22 20:45:06 -04:00

17 KiB
Raw Blame History

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 M0M11 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 appears in — and is never parked in — every mode any of its descendant leaves belongs to. Rule of thumb: tag leaves; parents follow. 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 — and only on tracks it tagged. 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.
  • Never touch untagged tracks. The tool acts only on tracks it tagged (plus derived parents' visibility); it never parks or restores a track it doesn't own.
  • 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).