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

165 lines
8.6 KiB
Markdown

# 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.