2d040d6896
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.
311 lines
17 KiB
Markdown
311 lines
17 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.
|
||
|
||
---
|
||
|
||
# Design View — additive phase spec
|
||
|
||
> **Additive section.** This is a standalone phase parallel to — not part of — the
|
||
> M0–M11 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).
|