Commit the authoritative build spec that the docs reference, and add /.claude/worktrees/ to .gitignore ahead of milestone worktrees.
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— theSamplemetadata struct and theBankIndex(add / remove / query / tier moves / dedup-by-hash) plus JSON serialize/deserialize to astd::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— theICaptureBackendinterface plusOfflineRenderBackendandRealtimeRecordBackend. Input: aCaptureRequest(source mode, time range, wet/dry, tail, SR/bit-depth/channels, output path). Output: a finished file + a populatedSamplehanded tobank_model.insert— placement viaInsertMedia; 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 inmpe_view.cpp.persist— project ext state <->bank_modelJSON; 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 (thecommand_id+gaccel+hookcommandpattern frommain.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_SETTINGSfor source = master mix / selected tracks / selected items / time selection, wet vs dry) andGetSetProjectInfo_String(RENDER_FILE,RENDER_PATTERN,RENDER_FORMAT). - Trigger a no-dialog render via the appropriate render action /
RENDER_SETTINGSbit. 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. VerifyI_RECMODE/I_RECINPUTvalues 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 inUndo_BeginBlock2/Undo_EndBlock2.
Persistence & paths:
SetProjExtState/GetProjExtState(namespace e.g."reasampler") for the index JSON. Resolve project folder viaEnumProjects/GetProjectPathEx; store the bank under a project-relative subfolder; keep only relative paths in the index.
Build order (each milestone independently testable)
bank_model+ JSON round-trip + unit tests. (pure — no REAPER)peaks+ unit tests. (pure)- 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) persist: write the index to proj ext state, reload on project open; confirm it survives Save / Save As. (bank travels with the .rpp)bank_panel: docked grid with thumbnails, audition, selection.insert: "insert selected sample at edit cursor" action viaInsertMedia.- Capture action family: master / selected tracks / selected items / razor area, each with wet-dry and tail options, all registered as bindable actions.
RealtimeRecordBackendbehind the same interface.- Slots: "capture to slot N" / "insert slot N", MIDI-bindable (MPC-style).
- Provenance + "re-capture from source"; null-test verify action.
- 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.