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.
This commit is contained in:
+164
@@ -0,0 +1,164 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user