From 6fcb81166858808548f74315eb0adcd86e462eac Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Tue, 21 Jul 2026 21:34:27 -0400 Subject: [PATCH] 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. --- .gitignore | 8 +++ CONTEXT.md | 164 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 .gitignore create mode 100644 CONTEXT.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73a674c --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/build/ +/.claude/worktrees/ +*.dll +*.dylib +*.so +*.o +*.obj +.DS_Store diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..eaf7d57 --- /dev/null +++ b/CONTEXT.md @@ -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.