Files
reasampler/CONTEXT.md
T
daniel 2d040d6896 Docs: reflect Phase D1 landing
Rename view_model -> view_mode_model across Design View spec docs;
archive D1 to COMPLETED.md; add view_mode_model_tests to CLAUDE.md
targets table and pure-core list.
2026-07-22 20:45:06 -04:00

311 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
---
# Design View — additive phase spec
> **Additive section.** This is a standalone phase parallel to — not part of — the
> M0M11 capture roadmap above. Nothing above changes. Product framing (workflow
> narrative, screenset differentiation, N-mode reasoning, design-direction calls)
> lives in `docs/product/design-view.md`; this section is the authoritative
> technical spec, matching the house style of the capture spec. Same standing
> discipline applies: **verify every REAPER API name/signature against
> `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use.**
## What it is
A **track-visibility-plus-processing "mode" system**, toggled from the ReaSampler
window. Tracks used purely for sound design (scratch oscillators, FX mangling,
resampling sources) are tagged into **Design** mode; the arrangement's real tracks
are **Arrange** mode (the default). Toggling to a mode **hides and disables** the
tracks that don't belong to it. Workflow value: mental separation + clutter
elimination — be in Design view, resample into the bank, flip to Arrange, place it.
**This is not a separate canvas.** REAPER has exactly one arrange timeline. Design
View is the *same timeline* with a curated, filtered track set and the inactive
tracks parked — not a second surface, window, or duplicated project.
It is the visibility/processing analog of the capture pillar's load-bearing rule:
**designing and arranging are separate stances on one timeline**, and the tool
enforces the separation **without ever destroying the user's real state.**
## Settled decisions
- **Membership.** Default = Arrange; every untagged leaf belongs to it. Leaves
**opt in** to Design (or any mode). No track appears in two modes at once except
(a) via an explicit **show-both** toggle, or (b) parent/folder derivation.
- **Parents are derived, never tagged.** A parent appears in — and is never parked
in — every mode any of its descendant leaves belongs to. Rule of thumb: **tag
leaves; parents follow.** Master track is always visible and never touched.
- **N-mode model, two-mode UI.** The data model carries arbitrarily many modes; the
UI ships **Arrange** + **Design**. A mode is (stable id, display name, ordinal).
Arrange is special only as the default home for untagged leaves.
- **Parking a track** (inactive-mode leaf): `B_SHOWINTCP=0`, `B_SHOWINMIXER=0`
(hide both panels), `B_MAINSEND=0` (out of mix), `I_FXEN=0` (FX bypassed), and
`TrackFX_SetOffline(track, fx, true)` for **each** FX (reclaim CPU). Full CPU-park
is the deliberate choice over mix-removal-only.
- **Never touches `B_MUTE` / `I_SOLO`.** The tool owns only visibility,
`B_MAINSEND`, `I_FXEN`, and per-FX offline — and only on tracks it tagged. User
mute/solo survives every toggle untouched.
- **Persistence.** Membership index + last-active mode + per-track flag snapshots
ride in the existing `"reasampler"` project ext-state namespace and travel with
the `.rpp`. On project open, reapply the active mode's visibility + processing.
## Precision invariants (enforce, test)
- **Non-destructive restore.** For every flag the tool drives, snapshot the prior
value **before** parking; on toggle-back restore **from the snapshot**, never to a
hardcoded "on." Round-trip (snapshot → park → restore) returns every driven flag
to its captured value. This is the phase's trust anchor — the analog of the
capture null test — and is enforced in the pure layer.
- **Mute/solo untouched.** No toggle ever reads or writes `B_MUTE` / `I_SOLO`.
- **Master untouched.** The tool never drives the master track's visibility flags
(the SDK forbids `B_SHOWINTCP`/`B_SHOWINMIXER` on master; the invariant agrees).
- **GUID-keyed, reorder-safe.** Membership keys on track GUID (`GetTrackGUID`),
never track index; tolerates unknown/stale GUIDs (prune on reconcile).
- **Relative/portable state only** in the persisted view section (GUID strings, mode
ids — no absolute paths, no index positions).
## Documented caveat
Offlined FX **re-instantiate** when a track returns to the active mode. Stateful
plugins (convolution, loaded samplers, tail-holding effects) re-initialize on
return — possible load hitch, un-persisted internal state lost. Accepted cost of
the CPU reclaim; surface it at the toggle affordance (tooltip).
## Module architecture (preserve the pure/shell split)
Pure (no REAPER types, unit-tested — the mirror of `bank_model`):
- `view_mode_model` — mode registry (id/name/ordinal; Arrange + Design seeded);
membership index (`track GUID → { mode ids }` + per-track show-both flag; add /
remove / retag / query); **folder-tree-aware** visibility derivation (given the
current parent↔child tree supplied by the shell + the active mode, compute the
visible set); the **parking/restore planner** (given active mode + snapshot
record, emit the exact (track, flag, value) operation lists for park and
restore — where the restore invariant is enforced); JSON round-trip of modes +
membership + show-both + snapshots + active mode.
REAPER-facing:
- `view` shell — reads `I_FOLDERDEPTH` across the track list to build the
parent↔child tree and feeds it to `view_mode_model`; applies the planner's operations
via `SetMediaTrackInfo_Value` (`B_SHOWINTCP` / `B_SHOWINMIXER` / `B_MAINSEND` /
`I_FXEN`) and `TrackFX_GetCount` + per-FX `TrackFX_SetOffline`; snapshots prior
flag values before parking; resolves GUIDs via
`GetTrackGUID` / `guidToString` / `stringToGuid`. Never touches master
visibility, never touches `B_MUTE` / `I_SOLO`.
- `persist` (slice) — serialize/deserialize the view section into the
`"reasampler"` namespace alongside the bank; on project open, rebuild the tree
and reapply the active mode.
- `actions` (entries) — toggle active mode; activate mode: Arrange / Design;
tag/untag selected tracks → mode; show-both for selected tracks. Registered with
the `command_id` / `gaccel` / `hookcommand` pattern; toggle + mode-jumps
MIDI-bindable.
- UI (in the ReaSampler / bank_panel window) — a **segmented mode switch**
(`[ Arrange | Design ]`) in the window header, active segment lit; small per-mode
membership count; the offlined-FX caveat as a tooltip. Tag/untag acts on the
current REAPER track selection, not a per-track widget.
## Show-both semantics
A **per-track "pin visible across modes"** flag that **re-enables processing**
whenever shown. A show-both leaf appears in every mode's visible set and is **never
parked** — its driven flags stay at snapshot/restored values, FX online, in the
mix. ("Show but keep parked" is not offered — a visible-but-silent-and-offline
track is clutter with a thumbnail.) Stored on the membership record; persists;
togglable per selection.
## REAPER API surface (verify all signatures)
- Visibility/routing/FX flags via `GetMediaTrackInfo_Value` (snapshot) /
`SetMediaTrackInfo_Value` (apply): `B_SHOWINTCP`, `B_SHOWINMIXER`, `B_MAINSEND`,
`I_FXEN`. (Note: brief cited `B_SHOWINMCP`; verified SDK name is `B_SHOWINMIXER`.)
- Per-FX offline: `TrackFX_GetCount` + `TrackFX_SetOffline(track, fx, offline)`.
- Folder tree: read `I_FOLDERDEPTH` per track to derive parent↔child structure.
- GUID keying: `GetTrackGUID`, `guidToString`, `stringToGuid`.
- Persistence: `SetProjExtState` / `GetProjExtState` under `"reasampler"` (shared
with the bank index — one blob, two logical sections).
- Wrap flag mutations in `Undo_BeginBlock2` / `Undo_EndBlock2` as appropriate.
## Non-goals / guardrails
- **No second canvas.** Do not build a parallel arrange surface — reject any such
path in review.
- **Never touch mute/solo.** Any code path reading/writing `B_MUTE` / `I_SOLO` is a
bug.
- **Never touch untagged tracks.** The tool acts only on tracks it tagged (plus
derived parents' *visibility*); it never parks or restores a track it doesn't own.
- **Restore from snapshot, never to a default.** No hardcoded "on" restores.
- **Verify API names** against the SDK header before use.
## Open questions to resolve during build
- Snapshot durability across a save-while-parked (persisted here by decision; the
lean alternative is force-restore-to-Arrange on save — see product notes item 4).
- Reconcile behavior when a tagged leaf is deleted or a folder restructured while
parked (ignore-and-prune stale GUIDs on next toggle/open).
- Interaction with the user having a screenset active (Design View drives the same
flags a screenset recall would; last writer wins — confirm no surprising fight).