Files
reasampler/CONTEXT.md
T
daniel 6fcb811668 Track CONTEXT.md spec and ignore worktrees dir
Commit the authoritative build spec that the docs reference, and add
/.claude/worktrees/ to .gitignore ahead of milestone worktrees.
2026-07-21 21:34:27 -04:00

8.6 KiB

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.