Files
reasampler/docs/ARCHIVE.md
T
daniel 1f24c4b095 docs: 1.0 documentation restructure
Split root CLAUDE.md into 19 per-directory files scoped to their source area.
Roll v0 history into docs/ARCHIVE.md; retire CONTEXT.md, CONTEXT-ARCHIVE.md,
PLAN.md, COMPLETED.md. Move plan docs under docs/. Rescue 9 live deferrals
into docs/TODO.md.
2026-07-29 15:09:48 -04:00

1066 lines
89 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.
# ARCHIVE.md — ReaSampler pre-1.0 history
Rarely-read backup of completed version-0 work, rolled here when 0 → 1 closed (2026-07-29). Not a source of context for current work: current architecture lives in the per-directory `src/**/CLAUDE.md` files, current plans in `docs/`. Full uncompressed history is in git.
## Part 1 — Capture roadmap, Design View, versioning, multi-bank, reclaim
### Milestone 0 — Transition scaffold: reaper_mpeview → ReaSampler
Retired the MPE scaffold (`mpe_model`/`mpe_view`) and stood up the sampler's pure core (`bank_model`, `peaks`) in its place, preserving the pure-core/REAPER-shell split; extension renamed `reaper_reasampler`; forever-stable action-id prefix chosen for the sampler family.
### Milestone 1 — bank_model + JSON round-trip (pure)
`Sample` metadata struct + `BankIndex` (add/remove/query/tier moves/dedup-by-hash) with lossless JSON round-trip. Relative-paths-only enforced at the model boundary (absolute paths rejected).
### Milestone 2 — peaks (pure)
Waveform min/max bin computation from raw PCM, dependency-free of REAPER's own peak API; multi-channel envelopes preserved (no silent fold); remainder-bin and short-buffer edge cases covered.
### Milestone 3 — Offline capture spike (REAPER shell)
`OfflineRenderBackend` renders the time selection to a bank-relative 32-bit float WAV via `Main_OnCommand(42230)` (REAPER always shows its render-progress window — no headless path exists); populates a `Sample` into the in-memory `BankIndex`; non-destructive; unsaved-project state gates on Save-As (no default-location fallback).
### Milestone 4 — persist (index ↔ project ext state)
`SetProjExtState`/`GetProjExtState` under namespace `"reasampler"`, keys `bank_index` + `project_guid`. **Decision:** project identity is **GUID-primary** with `ReaProject*` as a secondary disambiguator only — this replaced two earlier iterations (GUID-only mis-detected forks sharing a copied GUID; pointer-primary mis-detected reopen/new-project on address recycling). Save-As is **copy** semantics — old project's bank stays intact.
### Milestone 5 — bank_panel (docked grid)
Docked LICE grid; thumbnails from `peaks` bins, in-memory cache keyed by `(sampleId, drawWidth, bankGeneration)`**not persisted**, discarded on bank change. Audition via stock `PlayPreview`/`StopPreview`, single-stop-funnel for leak-free lifecycle (read-only, never inserts into arrange). Multi-select + keyboard nav.
### Milestone 6 — insert (placement)
`InsertMedia` at the edit cursor onto the **currently selected track(s)** (Daniel's directive — not a new track), for multiple selected tracks placing on each then restoring original selection/cursor; one undo block. Conform-to-project-tempo is an explicit separate flag (`&8`) — the stretch-to-time-selection bit (`&4`) is never set on the default path (pure-tested). Non-destructive to the bank.
### D1 — view_mode_model (pure)
N-mode registry (Arrange + Design seeded) + GUID-keyed membership + folder-tree-aware visibility derivation + park/restore planner (restore returns captured values, never a hardcoded "on") + JSON round-trip.
### D2 — view shell
Drives REAPER flags per the D1 planner: `B_SHOWINTCP`/`B_SHOWINMIXER`/`B_MAINSEND`/`I_FXEN` + per-FX offline, snapshot-before-park via `GetMediaTrackInfo_Value`. Master and `B_MUTE`/`I_SOLO` never touched (review-gated).
### D3 — persist slice (view state ↔ project ext state)
View section (membership + active mode + snapshots) serialized under `"reasampler"` alongside the bank; active mode reapplied on project open, including saved-while-parked restore from persisted snapshots.
### D4 — actions
Bindable Design View action family (`src/actions.{h,cpp}`): toggle/activate mode, tag/untag selected tracks, show-both toggle. New `src/track_guid.{h,cpp}` shared GUID formatter. Saved active mode reapplied on load via a load-signal seam in `persist` (drained by `main.cpp`'s timer).
### D5 — in-window toggle affordance (UI)
Segmented `[ Arrange | Design ]` mode switch in the bank_panel header via new pure module `mode_switch` (mirror of `bank_grid`); per-mode membership count shown. **Not built:** the offlined-FX re-init tooltip caveat — no trace in source, did not survive the later Phase L panel redesign.
### D2-W1 — view_mode_model lane extension (pure)
Extended D1's planner to item level: lane↔mode mapping, managed-vs-manual lane-ownership index, managed-only lane ops, the auto-tag decision (manual-lane items exempt; pre-existing content ⇒ Arrange), JSON round-trip of the lane index.
### D2-W2 — shell: lane application + new-content detection
Applies managed-lane ops (`I_FREEMODE`/`I_FIXEDLANE`/`C_LANEPLAYS`) in the DAW, manual lanes never touched. New pure modules `guid_diff` (poll-to-poll GUID diffing) and `lane_keys` (durable `P_LANENAME`-based lane identity, resolving the `I_FIXEDLANE` renumber-fragility risk). New-content auto-tag runs on the bank_panel timer with a first-poll-after-open guard. **Noted limitation at landing:** item-lane show/hide was structurally correct but a provable no-op on real projects until D2-W3 minted `reasampler:`-prefixed named lanes; end-to-end DAW verification was sequenced after D2-W3 for that reason.
### Milestone 7 — capture action family
Bindable capture actions, source resolvers (master mix/selected tracks/selected items/razor area). **Wet-only decision (Daniel):** REAPER offline render has no true pre-FX dry bit, so approximate-dry action variants were dropped; `CaptureRequest.wetDry` retained as a seam for true dry (later Milestone 10). **Superseded post-landing:** the four wet source-mode actions were replaced by **three FX-scope actions** (item/track/master, later item/track only) with range inferred orthogonally (razor-else-time-selection) — this fixed item captures rendering through the parent FX chain. FX scope: item = item/take FX only; track = item FX + the track's own FX; the out-of-scope chain (ancestors + master) is neutralized to unity via the reusable `FxBypassGuard` RAII (snapshot → neutralize → render → restore). **Master scope was later removed entirely** — capture is item and track only; to capture the master, render a track instead.
### Milestone 8 — RealtimeRecordBackend
Track-scope tap via `CreateTrackSend` into a hidden temp track (post-fader, `B_MAINSEND=0`) — captures each source track's own post-FX/fader output, chain-independent by construction; no `FxBypassGuard` needed (fixed the spike's master→temp feedback-loop silent-file bug). Timer-driven async `begin`/`tick`/`abort` state machine, project-scoped transport reads, idempotent latched `restore()`. **Decisions:** track scope only this increment (item realtime deferred — needs per-item take isolation); project-close guarded via `ValidatePtr2` (`dropWithoutRestore()` for an already-closed project); no undo block (transient scaffold, fully reversed by `restore()`). Master scope removed entirely (mirrors Milestone 7).
### T1 — offline tail: auto (default) + manual override
Three-state `TailMode {None, Auto, Manual}`. Auto: 8s-capped tail render + surgical `RENDER_NORMALIZE` (trim-end-only bit, `32768`) trimmed to -72 dB via a derived `RENDER_TRIMEND` ratio — deterministic (bit-identical repeats hold because only trailing silence is trimmed, nothing scaled/faded). Manual: fixed tail clamped to 8s, no trim. None: byte-identical to pre-tail exact-bounds capture. Exposed as a docked-panel **footer toggle** (cycles Off→Auto→Manual) rather than per-action tail variants (the "…with tail" action variants were dropped).
### T2 — realtime tail (follow-on to T1)
Parallel tail path for the realtime backend (which doesn't drive `RENDER_*`): records an 8s-capped tail window past the range end, then trims via a PCM backward decay-scan to the -72 dB point (new pure module `wav_trim` for header-aware WAV truncation; `peaks` gained `lastFrameAboveThreshold`). Manual records a fixed tail and skips the scan. **Realtime tail is documented non-deterministic** — bit-identical repeats are not asserted for this path (by design, not a defect).
### T1-followons — Manual fine-adjust UI + per-project tail persistence
Scroll-wheel fine-adjust of Manual tail length in 250ms steps (`kManualStepMs`), clamped [0, 8s]; label format `"Tail: Manual X.Xs"`. Tail setting (mode + Manual length) promoted into `ReaSamplerSession` and persisted under forever-stable ext-state key `"tail_setting"`; absent key falls back to Off / 2s default.
### D2-W3-A — lane minting + item→lane assignment + persist round-trip
Pure `planLaneMinting`: a track with ≥2 modes' worth of managed-eligible content gets one durable `reasampler:<mode>`-named lane minted per involved mode and every managed item (including pre-existing) assigned to its mode's lane; manual-lane items exempt at the source. Shell `applyMintPlan`/`mintManagedLanes` grows `I_NUMFIXEDLANES` (never shrinks user lanes), one undo block, triggered off the auto-tag detection tick. `reconcileManagedLanes` rebuilds the lane-ownership index from durable `P_LANENAME` on project load. Lane-ownership index rides inside the existing `view_state` blob (no new persistence key). **At landing, two behaviors were flagged as REAPER-runtime-only / DAW-verification-pending:** whether lane names stick when written the same tick a track flips to fixed-lane mode, and whether the leftover empty default lane 0 is silent.
### D2-W3-B — item-level mode actions + W3-A polish
Item-level "move to Design"/"move to Arrange"/"untag" actions mirroring the track-level family, each re-driving mint/apply, manual-lane items exempt, one undo block each. Plus three carried polish items (redundant `I_NUMFIXEDLANES` re-read removed, shared `item_read` seam extracted, reconcile guard for unregistered mode ids). **Deferred (Daniel's decision):** a per-track lane-split UI indicator in the panel — no natural cheap home found; preserved as a backlog note in PLAN.md Phase D2, not silently dropped.
### Phase V — Versioning & release
#### V1/V3 — app_version module: version constant, ext-state stamp, show-version action
Pure `app_version` module: CMake-sourced semver (`REASAMPLER_VERSION`) via `configure_file``version_generated.h`; ext-state writing-version stamp (numeric triple only, no channel suffix) under key `"version"` written on every save; absent stamp classifies as silent `PreVersioning`; on-demand "show version" action (no startup print).
#### V4 — beta-in-isolation: fully isolated coexisting binary via compile-time channel flag
`-DREASAMPLER_CHANNEL=beta` produces a fully isolated `reaper_reasampler_beta` binary: its own ext-state namespace (`reasampler_beta`), forever-stable command-id prefix (`CEREBELLUM_REASAMPLER_BETA_`), `"ReaSampler beta: "` action names, channel-qualified dock ident, `-beta` version-display suffix. All channel identity derives from one `REASAMPLER_CHANNEL_IS_BETA` bit via `channelCommandId`/`channelActionName` composition helpers — no scattered `#ifdef`s in shells. Stable build is byte-identical to pre-V4 identity. **Accepted isolation semantics:** a channel reads/writes only its own namespace (no cross-channel migration/fallback); the `reasampler:` lane-name prefix is deliberately NOT channel-qualified.
### Phase B — Multi-bank
#### B1 — bank_book (pure)
REAPER-free bank registry wrapping N `BankIndex` instances (pool seeded + privileged: un-deletable/un-renamable/un-evacuable, never zero banks); create/rename/reorder/delete named banks; active-bank id; move/copy a sample between banks (destination collapse-by-hash observed); evacuate; JSON round-trip with legacy `bank_index` migration into the pool. `BankIndex` itself untouched (additive). **Fork R-B, settled 2026-07-24 (Phase-B-wide, retro-touches B1B4):** every bank index verb — action and panel gesture alike — wraps its mutation in a batched REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`, `UNDO_STATE_MISCCFG`, SDK-verified to participate in undo), with a `projectconfig` hook triggering deferred session reload so Ctrl-Z/redo visibly restores state in-session.
#### B-cap — owned-file manifest seam
Capture records each file it creates into an owned-file manifest (pure `owned_manifest` module), persisted under a sibling `"owned_files"` ext-state key, decoupled from `bank_book` (tracks creation, not index membership). Landed early — deferred to Phase R would have meant retroactively reconstructing the manifest, a backfill cliff (fork R-D).
#### B2 — persist slice (banks ↔ project ext state)
Book serialized under `banks` key (pool folded in as bank-zero), distinct from `view_state`; legacy `bank_index`-only projects migrate to pool + zero named banks on first load, with `banks` authoritative and no `bank_index` written thereafter.
#### B3 — actions
Bindable multi-bank action set: create/rename/delete/evacuate bank, activate (direct + cycle), move/copy selected samples to a bank, pool/banks full-height toggles.
#### B4 — bank_panel vertical split (UI)
Pool grid (top) + named-banks tab-page region (bottom); LICE-drawn tab strip (new pure module `tab_strip`, mirror of `mode_switch`) with overflow/scroll (fork 5a); both a "move to bank" menu and drag-between-regions for sample moves, copy as secondary act on the menu (fork 5b), with drop-target highlighting during drag. Delete confirms on non-empty banks, naming evacuate as the alternative. Active-bank indicator resolved at build time.
#### B5 — sample-remove (the missing sample-level verb)
Surfaces `BankIndex::remove` through `bank_book`: drops a sample's index entry from a bank/the pool, index-only, non-destructive to the file (orphaned until prune); pool contents removable while pool-container privileges hold; no confirm dialog (recoverable via the R-B batched undo). **Fork R-A, settled 2026-07-24:** remove scope is **this-bank only** by default and only surfaced affordance; an `all-banks` scope stays a latent, unshipped parameter in the action signature.
#### Milestone 10 — provenance (re-capture from source)
Reshaped from an earlier, broader M10. **Cut (Daniel):** the null-test verification action and the true pre-FX-dry mechanism — both dropped (`docs/product/provenance.md` §What was cut). **Kept, forks P1=a/P2=a settled 2026-07-23** (P3/P4 moot under P2=a): `Sample.provenance` populated on resample-from-sample with parent id + a **thin `rsprov1` capture-recipe fingerprint** (scope, exact range, tail, rate/channels, track GUIDs, FX-chain identity — a drift/repro fingerprint, explicitly NOT a serialized chain to restore); "re-capture from source" action regenerates the file in place from the source's current state (bank-only, never places into the timeline), reporting drift if the source changed. Because re-capture is bank-only, provenance stays pure per-sample bank metadata and `bank_model`/`view_mode_model` remain decoupled — dual-canvas compliance needed no new coupling.
#### Phase B open questions — all resolved
All five forks settled by Daniel 2026-07-23 (persistence-key fold, delete-drops-members + evacuate verb, move-as-default gesture, distinct active-bank/shown-tab with an indicator, LICE tabs + both move affordances); B5 forks R-A/R-B settled 2026-07-24. Recorded in `docs/product/removal-and-prune.md` §Fork R-A/R-B.
### Phase R — Reclaim (file lifecycle: the prune path)
> **Boundary (load-bearing):** remove creates orphans; prune reclaims them. No operation other than prune deletes a file; prune deletes only files no index references.
#### R1 — prune-reconcile core (pure)
`pruneOrphans(present, referenced, owned) → (owned ∩ present) referenced`, filesystem-free; referenced-set unioned across the whole book so a file kept alive by any bank (including via a copy) is never an orphan; a present-but-unowned (hand-dropped) file is never reclaimed.
#### R2 — prune shell + persist wiring (filesystem I/O, thin)
Enumerates the resolved current bank folder, unions `book().referencedPaths()` + the owned-file manifest, feeds the pure core, produces a dry-run report (count/bytes/file list). No deletion this wave. Forever-stable `BANK_PRUNE_FOLDER` action registered (dry-run only in R2).
#### R3 — deletion + action (the destructive step, guarded)
Dry-run → `ShowMessageBox` confirm → staleness-intersect (`pruneDeletePlan`: confirmed ∩ fresh pure-core output) → delete exactly that plan; zero ext-state writes, no undo point (file deletion is not REAPER-undoable). `bank_panel` prune button dispatches the same registered action (no duplicate logic). **Forks settled 2026-07-24:** R-C (deletion mechanism) — trash-preferred (Windows `SHFileOperationW`+`FOF_ALLOWUNDO`), unlink fallback where no portable trash surface exists (macOS/Linux); R-D (orphan attribution) — owned-file manifest, seam landed early at B-cap rather than reconstructed at prune time; R-E (trigger) — manual action + panel button only, dry-run-first, no background sweep, no "delete-and-prune-now" convenience shipped.
#### Milestone 11 — polish (wave 1)
Batch capture (`batch_capture` pure module, one sample per selected item/razor area, mixed-result aggregation, single persist per batch); action trigger buttons + keybinding-help labels (`action_buttons` pure module, live-binding labels via `kbd_getTextFromCmd`); conform-on-insert closed as already-shipped (verified-extant, no new code). **Cut (Daniel, 2026-07-26):** resample-and-mute-source — superseded by the Design View mode projection (park/hide inactive-mode content), which made a mute-after-capture workflow redundant.
#### Milestone 11 — polish (wave 2 / completion)
Native OS drag-out: pure `drag_out` module (gesture-boundary decision at panel-edge, path-list assembly with dedupe/missing-file skip) + `drag_out_win` shell (Windows OLE `DoDragDrop`/`CF_HDROP`; macOS/Linux via `SWELL_InitiateDragDropOfFileList`). Structurally copy-only — `DROPEFFECT_MOVE` never offered, no source-deletion path exists; prune remains the sole file-deletion authority. Completes Milestone 11.
## Part 2 — Look-and-feel, and the ReaSampler 9000 instrument
### Phase L — Look-and-feel (system-wide visual design language)
Own lettered namespace (`L`), orthogonal to and ungated by the M/D/B/R/V/S pillars — a
parallel team's whole-system look-and-feel effort answering Daniel's post-DAW-test verdict
("this looks like temple os… does Cockos have a toolkit?"). The then-authoritative spec was
CONTEXT.md §Phase L. Product framing + settled decisions: `docs/product/visual-design-language.md`.
**Settled decisions (Daniel, 2026-07-26):** **DS-1** — toolkit is LICE + WDL free game, no
external frameworks (iPlug2/JUCE/VSTGUI rejected); reuse a WDL/vwnd piece where it beats
re-deriving; keep hit-test geometry in pure CTest-covered modules. **DS-2** — visual
direction Direction B ("Neon Console") + Direction C's spectral keyboard strip, **REVISED
2026-07-26 (palette-only):** neutral ladder moved from near-black into REAPER's mid-grey
family (`bg/base #2b2b2b` / `bg/panel #333333` / `bg/cell #3a3a3a` / `line/hairline #4a4a4a`
/ `text/primary #dcdcdc` / `text/dim #a8a8a8`), and a single electric-cyan accent replaced by
a **three-accent pastel system** (`accent/primary` lime `#B0E098` = live/active/selected,
`accent/secondary` teal `#84D6D0`, `accent/tertiary` purple `#C2AAE8` = categorical
distinctions); a bundled-font upgrade was considered and **declined** (no font
redistribution). **DS-3** — dock-panel scope is a thorough layout redesign, not a light
re-skin; sequenced after M11 merged. **Phase L is complete: L1L7 and L3 all landed.**
#### L1 — shared LICE drawing kit (the foundation)
`theme`/palette module (role→color, the DS-2-revised grey ladder + three pastel accents,
WCAG contrast-floor helpers/tests, `theme_tests`); `component_geometry` (button/slider/
list-row geometry + hover hit-test, `component_geometry_tests`); `draw_kit` shell
(`fillSurface`, `drawButton`/`drawSlider`/`drawListRow`/`drawWaveform`, cached-font `text()`
over four `LICE_CachedFont`s, full interaction-state model, double-buffer preserved). GDI
`DrawText` retired from `bank_panel` — the single biggest "temple os → modern" lever.
#### L2 — dock-panel layout redesign
`action_bar` pure module (task-grouped clusters: Capture/Placement/Maintenance, label +
keybinding sub-rects, overflow, hit-test; `action_bar_tests`). `bank_panel` redesigned with
the full M11-aware button inventory placed by cluster, prune kept footer-set-apart +
`warn`-colored, entire panel drawn through the L1 kit with a single `KitColor→LICE_pixel`
boundary (`draw_kit`'s `toLice`).
#### L4 — dock-panel button layout enhancement
Three-zone re-home: top toolbar (capture/placement/maintenance — **Maintenance restored to
the top toolbar per Daniel's directive during build**, revising the original "capture +
placement only" spec), bottom toolbar (Design View tagging/switching), footer (narrow
`[Arrange|Design]` toggle · Tail button as a proper kit button · Prune set apart). New pure
`footer_bar` module (`footer_bar_tests`); `action_bar` gained `ActionCluster::Tagging`/
`Switching`. Pure re-home of existing actions — no new actions, no capture/placement change.
#### L5 — dock-panel button refinements
Rare capture variants (Batch Items/Batch Razor/Capture RT) pulled into a right-anchored
More (⋯) overflow menu (`overflow_menu` pure module + `TrackPopupMenu`); short button faces
with the `ReaSampler:` prefix dropped; a **custom LICE-kit hover-delay tooltip** (`tooltip`
pure module, sourced from the registered action phrase, not `kbd_getTextFromCmd`) chosen
over attaching a SWELL tooltip to non-child LICE rects; four Item/Track × Arrange/Design tag
buttons with opposite-mode-only enablement (`mode_enable` pure module); Activate-Arrange/
Activate-Design/Toggle buttons removed (footer toggle is the sole mode-switch affordance;
Show Both stays); cluster-gap spacing widened 16→24px.
#### L6 — toolbar polish (in-DAW feedback pass on L5)
Single-row button faces (keybinding sub-row removed, toolbar height 40→28px); keybinding
folded into the hover tooltip (`"phrase — binding"`); Cancel RT moved into the overflow menu;
visible top-bar cluster order tidied (Capture → Maintenance → Placement). Icons considered
and deferred.
#### L7 — capture ordering, card metadata, and selection styling
Persisted deterministic capture order via a per-`Bank` id→slot `SlotMap` in `bank_book`
(gap-preserving, JSON rides inside the existing `banks` blob, pre-L7 migration seeds dense
order); `BankBook` gains `reorderSample`/`replaceSample`/`orderedSampleIds`/`reconcileSlots`.
One sanctioned `Sample` change: `captureTimeSigNum`/`captureTimeSigDenom` stamped via
`TimeMap_GetTimeSigAtTime`. `card_drag` pure module (gesture precedence: leave-client→OS
drag-out, other-bank→move/copy, same-bank→reorder/Alt-replace; SWELL stock cursors per
gesture) and `card_meta` pure module (bars.beats.subdivisions + s.ms, blank when unstamped).
Sparse-grid render with gap cells; selection restyle drops inverted-fill for a normal cell +
`accent/tertiary` purple border. **M9** (capture-to-slot-N MIDI-bindable capture) stays
explicitly deferred — L7 eases a future revival but ships no slot-numbered actions or MIDI
bindings.
#### L3 — VST editor + embed-strip restyle
Brought `reasampler_editor.cpp` + `reasampler_embed.cpp` onto the L1 kit: cached-font
`text()` replacing raw GDI `DrawTextA`; the shells' **local pre-L1 palette** (forest-green
`kColBackground`/`kColCardBg`/`kColThumb`/… constants, arrived off-kit — the "born in the
kit" coordination contract did not occur) retired in favor of the L1 `theme` roles; capture
browser/toggles/ADSR+pitch sliders/zone bars/list rows/waveform all restyled through the kit;
pastel spectral keyboard strip + zone bars (active zone lifts to `accent/primary` + a static
glow, no animation). Beta title band stays a **textual-only** distinction (no channel-
specific accent color — closed the one open L3-readiness fork). VST3 class UID unchanged (a
visual refresh is not a compat event); all `src/vst/` geometry modules stayed pure — L3
touched only the two draw shells. **L3 was the last Phase L point — Phase L is complete.**
### Phase S — MIDI-playback instrument (native VST3 sampler; a second build artifact)
Landed on `dev` (merged 2026-07-27); **DAW verification pending Daniel's smoke test** at the
time of this record. The then-authoritative spec was CONTEXT.md §MIDI-playback instrument
(Phase S). Product framing: `docs/product/midi-playback.md`.
#### S3 — pure sampler core (voice engine / envelope / keymap / repitch)
The REAPER-free and VST3-free core — the heart of the phase, mirroring `bank_model`/`peaks`/
`view_mode_model`. Polyphonic voice allocation with bounded stealing; ADSR envelope math
asserted against a known signal; repitch/interpolation from root note with loop-point-aware
sustain; keymap model (key ranges/zones, (note,velocity)→sample query, chromatic-from-root
as the degenerate Tier-0 case). Core boundary enforced plain-data-only (test target links
neither SDK).
#### S4 — Tier 0: "the bank plays" (single sample, chromatic)
Wired the S3 core into the VST3 shell: `process` marshals MIDI note-on/off/velocity, drives
the core, writes to the output bus (block-granular timing at this tier). Live-state seam
reads the bank + selected sample's root note off the bridge, resolving WAV paths the same
project-relative way `persist` does (shared `capture_paths::projectDirOfRpp`; bank JSON
parsed via the shared `bank_book` path, not a spike string-scan). Minimal LICE sample-picker
list in the editor; the pick is instance-owned component state, never written to the bank.
Chromatic-from-root, 16-voice polyphony, amp envelope, velocity→volume; load/decode off the
audio thread via lock-free atomic pointer swap (graveyard-reclaim) — `process` never
allocates.
#### S5 — Tier 1: "a keymap" (zoned multisamples, per-sample root notes)
Keymap editor in the LICE editor assigns bank samples to key ranges, each with its own root
note (from S2 intrinsics, overridable in the performance map). Zoned playback resolution —
one sample per key-region, repitched from its own root. Keymap persisted as the instrument's
own VST3 component state (D-B split: bank carries file-fact defaults, instrument owns the
performance choice); the live bank seam stays read-only.
#### S6 — embedded TCP/MCP UI (D-D)
`IReaperUIEmbedInterface` implemented so a compact keymap/level strip draws inline in the
TCP/MCP, reusing the same LICE surface as the main editor; clean open/close/resize lifecycle.
Scheduled (not deferred) per D-D, sequenced last as polish over the Tier-0 need.
#### S7 — stereo channel mode (mono | stereo)
Per-instance `ChannelMode {Mono, Stereo}` as an S3-core extension (not a shell hack):
`SampleData` carries 1- or 2-channel decoded PCM, `Voice::renderFrameStereo` +
`VoiceEngine::render(left,right,n)` share one read head/envelope tick; mono stays the exact
degenerate case (byte-identical). Cross-mode decode policy: mono source + stereo mode →
dual-mono; stereo source + mono mode → downmix. Toggle lived in component state v4 (v3 +
one byte), never written to the bank. `setBusArrangements` accepted only the mode's
arrangement, with `restartComponent(kIoChanged)` on a runtime mode change — verified against
the vendored Steinberg SDK. **Superseded later:** the GA post-launch pass (recorded in Part 3
scope) deleted the dynamic mono↔stereo bus renegotiation in favor of a permanently stereo
output bus with `ChannelMode` becoming decode-only — do not read S7 as the final channel-mode
shape.
#### S8 — ingest through the bank (one gesture: capture/import into bank + assign)
Goal: the extension owns ingest (arrange/Media-Explorer/drop-target access); the instrument
stays a read-only bank consumer. Recorded **honest SDK limits** at spec time: Media Explorer
exposes only `OpenMediaExplorer` + `MediaExplorerGetLastPlayedFileInfo` — no enumerate-
selected-files, no drop-handler registration — so ME import is single-file, pull-on-action,
not push/drop; REAPER exposes no drag-drop registration API, so panel drop handling rides
ReaSampler's own HWND (`WM_DROPFILES`/`IDropTarget`), and a drop onto the VST3 editor window
was flagged as an unproven cross-artifact spike (see S13). **All four checklist items are
recorded unchecked (`[ ]`) in the source** despite the Phase S banner asserting S1S18 landed
on dev — no confirming text elsewhere in this range resolves the discrepancy (see Open
questions).
#### S9 — bank-generation change-detection
A monotonic bank-generation counter (new `ext_keys.h` constant, forever-stable spelling)
stamped into `"reasampler"` ext-state, bumped on any bank-content mutation that changes
instance playback (capture add, recapture-in-place, remove, move/copy). The instrument polls
it off the audio thread on a UI/timer cadence and calls `reloadFromBank()` on change, reusing
S4's atomic pointer-swap handoff (no `process`-thread work, no glitch). Pre-S9 projects
default to generation 0.
#### S10 — capture-first editor: browser + guided single-capture setup
**Policy reversal (revising S4):** the "first sample plays" fallback is removed — a fresh
instance with no stored selection plays **nothing** and the editor shows an explicit
"pick a capture" empty state; loading only happens on an explicit pick (or S13 drop / S8
ingest). Default editor face reframed (Daniel, 2026-07-26) as a **capture browser**
(peak-thumbnail cards, name, root/key badge, bank filter) feeding a guided single-capture
setup (root note, play-mode basics, level), demoting the full keymap editor to an opt-in
**S10-Z Zones panel**. New pure `keyboard_strip` + `capture_browser` modules planned (mirrors
of `mode_switch`/`editor_geometry`); drag-state machine on the LICE `IPlugView`. **All
checklist items for S10 (and its S10-Z sub-section) are recorded unchecked (`[ ]`) in the
source** — see Open questions; the ±1 nudge-button row and `zoneHitTest` were slated for
retirement here regardless of check-state.
#### S11 — waveform view with draggable loop points
Selecting a zone/capture shows its waveform (via the existing `peaks` module over the
already-decoded PCM); loop-start/loop-end markers drag to set the sustain loop, snapping to
the nearest zero-crossing; a sample with no loop shows a "no loop" state. Loop points are a
performance-map override on the zone, seeded from the S2 bank intrinsic (D-B split — the
bank fact is never written back). **Boundary note (S10 reframe):** this waveform draw is the
same one S10's picked-capture view uses, built once. **All S11 checklist items are recorded
unchecked (`[ ]`) in the source** — see Open questions.
#### S12 — editor scale + ergonomics
**Boundary note:** the "sample list" S12 was to scroll/search became S10's capture browser;
S12 narrowed to (a) scroll + type-to-filter search layered over that browser and (b) numeric
entry + ADSR, and additionally absorbed the S15/S16 control surfaces (deferred to here per
spec). **Landed (all three checklist items `[x]`):** pure `browser_scroll` module (scroll/
search geometry, `browser_scroll_tests`); pure `note_entry` module (`parseNoteEntry` accepts
decimal or note-name input, clamps to [0,127], `note_entry_tests`); pure `param_slider`
module (control-panel stack/toggle/slider geometry + point→control routing,
`param_slider_tests`) driving AHDSR (hold-stage addition) plus the S15/S16 controls (Gate/
Trigger toggle, Trigger %-length/fades, Varispeed/Preserve toggle, AD pitch-envelope
enable/attack/decay/depth) on the selected zone's `ZonePlayParams`, committed via
`commitAndReload`. **Standing ruling (Daniel), enforced here:** wall-clock envelope times are
stored as rate-free **seconds** (zones payload v5), resolved to frames at keymap build
against the live project rate — no hardcoded sample rates in `src/`.
#### S13 — drop-to-load (partial landing; relay deferred)
**Spike verdict (ps-w12, 2026-07-27): DEGRADED.** The cross-artifact relay would require a
new instrument WRITE seam into ext-state plus an extension-side timer-poller/claim-nonce
handshake — the same race the S17 spec rejected — so it was explicitly deferred (remains in
`PLAN.md` §S13); the shipped ingest gesture stays drop-onto-docked-panel (S8). **Landed:**
the editor child window accepts `WM_DROPFILES`/`IDropTarget` (Windows-only, D5) and, since no
relay exists, shows a transient banner + persistent empty-state affordance directing the user
to drop onto the ReaSampler bank panel instead — no file is ever ingested from the editor
drop, and no timeline item is ever inserted.
#### S15 — sampling modes: Trigger vs Gate
Per-sample/per-zone performance choice (D-B), an S3-core extension. **Gate** grows ADSR to
AHDSR (`holdFrames`, `==0` is exactly pre-S15 behavior — back-compat, no type rename),
sustain loop points apply. **Trigger** is a one-shot: plays `[start, start+lengthFraction·
(framesstart))` with equal-power fade-in/fade-out, ignores note-off entirely, no sustain
loop; choke-on-note-off explicitly out of scope (fork S15-F1, held). Both modes gain a
modifiable **start point** (playback offset). Mode + params live in the performance map;
truncated/older blobs default to Gate/hold=0/start=0 (today's behavior unchanged). Editor
control surface deferred to S12 (spec-sanctioned).
#### S16 — pitch engine modes (Varispeed vs Preserve) + pitch envelope
Per-zone `PitchEngine {Varispeed, Preserve}`. Varispeed = today's coupled pitch/duration
path. Preserve = duration-preserving repitch via a hand-rolled pure OLA `pitch_shift` module
(house pattern, `pitch_shift_tests`; `WDL_SimplePitchShifter` excluded by an include-chain
conflict with `windows.h`, held as a future quality/latency swap behind the same contract). A
per-voice AD pitch envelope (`enabled=false` default) multiplies the Varispeed ratio or adds
to the Preserve shift amount. **S16-F1: default engine is Preserve** (Daniel: "I want
duration-preserving repitching"), per-zone togglable back to Varispeed. Editor exposure
deferred to S12.
#### S17 — drop-and-load: drag a capture onto a track's FX button
Dragging a single capture over a track's TCP FX button lights it as a drop zone; dropping
instantiates ReaSampler 9000 on that track with the capture already loaded and selected.
Third `drag_out` gesture `InstrumentDrop` (only arms for a **single** capture — multi-payload
over REAPER UI falls through to `OsDrag`, per a resolved open question). Mechanism:
`TrackFX_AddByName` + a Steinberg-format `.vstpreset` built by the new pure `instrument_drop`
module from the instrument's own `sample_map::serializeComponentState` (one serializer, two
artifacts — the byte format cannot drift), applied via `TrackFX_SetPreset`. **The originally
planned `TrackFX_SetNamedConfigParm` "vst_chunk" write proved silently unappliable for VST3**
and was replaced by the `.vstpreset` + `TrackFX_SetPreset` path (GA DAW-fix pass, 2026-07-28).
FX-button hotspot resolved via `GetThingFromPoint` (`"fx_chain"`/`"fx_N"` info strings), not
home-grown geometry. Batched into one undo point; never inserts a timeline item.
**VERIFIED (GA DAW-fix pass, 2026-07-28).**
#### S18 — VST3 channel isolation (beta ReaSampler 9000 pairs only with beta extension)
Extends V4's beta/stable split to the VST3 instrument: a second FOREVER-STABLE class UID
(`REASAMPLER_PROC_UID_BETA_1..4`) alongside the existing stable UID, selected at compile time
by the single `REASAMPLER_CHANNEL_IS_BETA` bit (one class per binary, both UIDs frozen
forever); binary name forks via CMake `OUTPUT_NAME` (`reasampler_9000` / `reasampler_9000_beta`);
display name/version-string channel-aware via `app_version::vstPluginName()`. Pairing-surface
invariant recorded structurally (all wire keys live under the channel-derived
`kProjExtNamespace()`, so a key that forgets to isolate is impossible by construction) rather
than newly coded.
#### S-VIEW-BUG-1 — drop-to-FX bug fix (Wave 1)
Root cause: the pure `instrument_drop` FX-hotspot classifier matched only `fx_*` strings, so
a drop on the TCP (`tcp.fx`) or MCP (`mcp.fx`) FX button fell through to `OsDrag` (arrange-
as-audio) instead of instantiating the instrument. Fix: predicate widened to match `fx_*` /
`tcp.fx` / `mcp.fx`, unit-tested at each hotspot string; shell unchanged. **DAW-confirmation
recorded as pending** Daniel's post-merge smoke test at the time of this entry.
#### S-VIEW-SIZE-1 — 1080p default window size (Wave 1, interim)
Default editor `ViewRect` bumped from 560×400 to **840×560** with a `checkSizeConstraint`
minimum floor, verified against the vendored `pluginview.h` mechanism. **Explicitly interim**
— recorded as the correct starting point, not the final tuned value; final tuning deferred to
a later wave (T-SHELL) once the three concrete Sample-face band heights exist.
---
## Part 3 — Editor redesign, GA fixes, and the Phase Q structural reorganization
### Phase S — editor view-model redesign (three views: Sample / Browse / Zone)
Additive Phase S sub-phase (S-VIEW; Daniel, 2026-07-27, r9). Retired the flat Browser|Zones
toggle for a three-view model — Sample (home/default), Browse (full-window modal overlay over
Sample, S-VIEW-F3 settled), Zone (dedicated surface, own button). Landed across three waves
(pure modules → shell wiring → velocity-curve editor UI); **S-VIEW complete — all ten points
landed**, integrated suite green.
#### S-VIEW-1 — three-view navigation model
Flat toggle retired; three-view model landed. Empty state surfaces Browse as the dominant
call-to-action; fresh instance stays silent (S10 reversal preserved). S-VIEW-F3 settled and
implemented: Browse is a full-window modal overlay over Sample.
#### S-VIEW-2 — Sample view (the new main face)
Composed the home face: hero waveform with the S11 markers (moved from Browse), a fenced root
affordance, the Mono/Stereo toggle (moved from Browse), and the "modes-and-down" control strip
(Mode / Pitch engine / AHDSR|Trigger / AD pitch env, moved from Zone's param panel — single-
capture one-zone storage per S15-F2). Reference grammar: Simpler / Phase Plant.
#### S-VIEW-3 — envelope overlay + draggable nodes
Amp envelope (AHDSR for Gate, fade/%-length for Trigger) drawn as a curve over the Sample
waveform at accurate wall-clock time. New pure `envelope_overlay` (params+frame-length→polyline)
and `envelope_edit` (node hit-test + pixel-delta→clamped-param inverse map) modules, both
unit-tested. Breakpoints are draggable (S-VIEW-F2 settled): X→segment time, Y→level on
level-breakpoint nodes (sustain drags both axes), monotonic-in-time + range-clamped. The
Trigger frames↔fraction conversion was extracted to a new pure `trigger_seam` module.
#### S-VIEW-4 — preview-trigger + velocity knob
A button fires the sampler at the loaded capture's root note through the live voice engine
(off the audio-thread commit path) plus an adjacent velocity knob. Preview velocity PERSISTS
(S-VIEW-F1 settled): `previewVelocity` on `ComponentState`, envelope v5→v6, round-tripped via
`getState`/`setState`; zones payload untouched, older blobs lift to a mid default.
#### S-VIEW-5 — Browse reduced to choosing (modal over Sample)
Search + bank tabs + captures grid + scroll + selection retained; confirm/cancel added
(double-click loads). Large waveform preview, Mono/Stereo toggle, root keyboard-strip (all
moved to Sample), and loop-point labels + track-root message (cut) removed. Renders as a
full-window modal overlay (S-VIEW-F3 settled).
#### S-VIEW-6 — key-tracking parameter
Per-`PerformanceZone` scalar on keyboard pitch tracking around the root (100% = 12-tone-ET, 0%
= no tracking, 200% = double); additive, defaults 100% (bit-identical). Key-track math lives in
the pure sampler core, applied in both Varispeed and Preserve repitch; zones payload v6.
Surfaces on the Zone param panel + Sample control strip.
#### S-VIEW-7 — piano-key pattern on the keyboard strip
Alternating white/black (bright/dark per palette) key pattern overlaid on the pastel spectral
fill via a pure `isNaturalKey` predicate (12-tone, unit-tested) on `keyboard_strip`. Shared by
the Zone strip and Sample root affordance.
#### S-VIEW-8 — Zone view retained + wired
+Add Zone / Delete, the per-zone keyboard strip (piano pattern), the Low/High/Root numeric-entry
legend, and the per-zone param panel all retained; key-tracking control added. Nothing from the
prior Zones view dropped.
#### S-VIEW-9 — velocity→amp transfer curve (pure core + engine application)
New pure `velocity_curve` module: eval via a FritschCarlson monotone cubic Hermite spline (no
overshoot outside [0,1]) + control-point editing (add/move/delete x-ordered + box-clamped) +
hit-test + pixel-delta→clamped-point inverse map. Additive `velocityCurve` field on
`PerformanceZone`, zones-payload v7; default = **flat y=1** (R10-F1 settled, Option A, Daniel
2026-07-27: "any velocity plays at full level"); older ≤v6 blobs lift to flat y=1. Applied at
`Voice::start()`, replacing `velocityGain_ = velocity/127.0` with `curve.eval(velocity)`.
**Default is a deliberate non-back-compat behavior change** — existing zones' soft hits play at
full level after upgrade; flagged and accepted by Daniel.
#### S-VIEW-10 — velocity-curve editor UI (shell, Sample + Zone views)
Draggable transfer-curve editor rendered through the L1 kit in both the Sample view (curve box
beside the hero band) and the Zone per-zone param panel: `eval` polyline over a 0127 × 01 box
with draggable control points — add on empty-click, move, Alt-click delete interior, drag-off-box
delete with a warn-state affordance. Coordinate math added to `velocity_curve` (`pixelFromPoint`/
`pointFromPixel`); reads/writes the existing `velocityCurve` field — zones payload stays v7, no
schema change. Mirrors the S-VIEW-3 envelope-node interaction grammar (snapshot-at-grab →
off-audio-thread commit).
#### Phase S — product name (ReaSampler 9000)
The MIDI-playback instrument's product name is **ReaSampler 9000** (Daniel, 2026-07-26, on
DAW-testing the S1S6 instrument); the extension remains **ReaSampler**. Propagated across the
VST3 class display-name string, the `IPlugView` editor title band, the S6 embed-strip label, and
Phase S docs. **Do not change the VST3 class UID** — already-saved-project instances key off it.
**S-NAME-1 settled (Daniel 2026-07-26):** the built binary filename (CMake `OUTPUT_NAME`, e.g.
`reasampler_9000.vst3`) was renamed alongside the display strings so the on-disk name matches;
the VST3 class UID stayed unchanged as the compat anchor.
S-VIEW-1 through S-VIEW-10 rolled up as fully landed (Wave 1 core + Wave 2 shell + Wave 3
velocity-curve editor UI).
### Phase D2 — Two-canvas (item-level mode projection; additive to D1)
Extends D1's track-level mode projection to **item level** via REAPER 7 fixed lanes: on a track
present in both stances, each mode owns a fixed lane — the active mode's lane shows/plays, the
inactive mode's is hidden/silenced — so a Design take and an Arrange take share a track/time
position without colliding on the view. Nothing in D1 changes; runtime floor rises to **REAPER
7** for this sub-phase (no version-gate branch — simply unavailable below v7). D2-W1 (pure lane
extension), D2-W2 (shell: lane application + new-content detection), D2-W3-A (lane minting +
item→lane assignment + persist round-trip), and D2-W3-B (item-level mode actions + polish) have
all landed — **Phase D2 is functionally complete.** **Deferred, not dropped:** a per-track
lane-split marker in the panel UI — the mode switch already shows the active mode; no natural
cheap home for a per-track indicator was found.
### Milestone 9 — slots (MPC-style)
**Abandoned (Daniel, 2026-07-27) — will not be built.** Goal had been "capture to slot N" /
"insert slot N", MIDI-bindable, with slot state persisted via the index.
### Post-S-VIEW DAW-fix pass + voice-system redesign (2026-07-27)
Merged to dev; integrated suite 52/52 green. Found during Daniel's post-S-VIEW DAW testing
(fixes) and a subsequent voice-redesign directive — not PLAN-tracked.
- **Envelope node editing:** every Gate stage (A/H/D/S/R incl. a visible in-bounds Release) and
Trigger's zero-fade-out node are now fully grabbable in both modes; `envelope_edit` uses
param-domain schematic scaling (8px minimum node separation enforced by the forward map in
`envelope_overlay`), all nodes clamped in-canvas. Previously only a subset was editable.
- **Gap-free waveform render:** `columnMinMax` moved to `peaks`; `waveformColumnCount` moved to
`component_geometry`; dock panel and VST editor now share one gap-free per-column min/max
algorithm via `draw_kit::drawWaveform`.
- **Radial Knob primitive:** `param_slider` gained a `Knob` control kind (7→5 o'clock arc, needle,
vertical-drag) — foundation for the later Wave B control deck.
- **Zone-bleed fix 3a:** `reconcileSingleCaptureZones` added to `sample_map`, fixing stale
full-range zone shadowing so the engine plays the zone the editor draws; called on load
(`setState`), bank-assign, and capture-confirm.
- **Voice-system redesign (Daniel directive):** `sampler_core` gains user-parameterized voice
count (132, default 16), `VoiceMode` Poly/Mono (last-note held-note stack, `MonoTrigger`
Retrigger/Legato toggle), an isolated `PreviewCard` (dedicated preview voice, never steals
from/into the MIDI pool), two-tier panic (CC123 release / CC120 immediate hard-stop incl.
Trigger one-shots). Processor sums `PreviewCard` alongside engine+drain; `retireIdleDrain()`
retires fully-idle drain snapshots on the UI-timer cadence; voice-param setters rebuild the
engine via the drain-slot swap (no bank re-read, no WAV re-decode, ringing tails not cut).
`ComponentState` envelope bumped **v6→v7** (voiceCount/voiceMode/monoTrigger bytes); pre-v7
blobs lift to `{16, Poly, Retrigger}` — reproduces pre-Phase-S behavior exactly.
### FB1 — Sample-view recomposition + master gain (r11; 2026-07-27)
Merged to dev; integrated suite 55/55 green. Closes S-VIEW-11 + S-VIEW-12 from the Wave B plan
(S-VIEW-13 stays open, deferred to FB2). Recomposed the Sample face after Daniel's post-landing
DAW pass. Three new pure modules: `knob_deck` (group-box/caption-row/compact-toggle/knob-cell
geometry, deterministic whole-group wrap), `curve_popup` (centered-sheet geometry, width/height
clamps, outside-sheet dismissal test), `master_gain` (dB↔linear taper math for a new −∞…+24dB
post-mixer master gain, norm 0 = true silence, unity ≈0.714 normalized — shared by the editor
knob and the processor multiply so they cannot drift). Sample control strip rebuilt as five
fenced groups: AMP ENVELOPE (Gate|Trigger toggle), PITCH (Varisp|Preserve + Key Track), PITCH ENV
(Off|On), VOICE (Voices/Poly|Mono/Retrig|Legato), MASTER (Gain). Hero waveform now runs
full-width/elastic (840×620 default preserved, floor 150px — **R11-F1 settled** at build, no
window-size change required). New 28×28 curve preview button opens a `curve_popup`-geometry
centered sheet; right-click on a node deletes it (endpoint-guarded). New persisted
`masterGainLinear` field, `ComponentState` **v7→v8**, pre-v8 blobs lift to unity gain; processor
applies it as a per-sample ramp (no zipper noise).
**Decision:** persistence was **NOT zero-change**, contrary to the original r11 spec description
of a pure view recomposition — CONTEXT.md's "zero component-state change" clause was corrected to
record the v8 bump and the master gain field.
### FB2 — Zone-panel parity (r11; 2026-07-28)
Merged to dev; integrated suite 55/55 green. Closes S-VIEW-13 / R11-F2. Zone param panel rebuilt
on the same `knob_deck` + `curve_popup` grammar as the Sample face (FB1); `param_slider`'s linear
slider rows retired on the Zone panel — the `Knob` primitive is now the only live consumer of
that half of `param_slider`. Zone-authoring affordances (+Add Zone/Delete, per-zone piano-key
strip, Low/High/Root legend) preserved unchanged; VOICE and MASTER stay Sample-only
(per-instance). No new pure modules required. **This completes the r11 editor recomposition
(Phase S Wave B)** — S-VIEW-11/12/13 all landed, no open r11 or Wave B items remain in PLAN.md.
### GA post-launch DAW-fix pass (2026-07-28)
Merged to dev; integrated suite 55/55 green. Six fixes found during GA DAW testing, not
PLAN-tracked.
- **Preserve pitch engine rewritten to SOLA:** `pitch_shift`'s dual-tap OLA (taps hard-locked half
a window apart — fixed relative phase caused anti-phase cancellation on many source
frequencies, spectral garbage on repitched notes) replaced by **correlation-aligned SOLA
splices**: one active read tap chases the write head at the shift ratio, each splice jump
refined by a cross-correlation search so the new read point is waveform-aligned, then old and
new taps crossfaded with a raised-cosine amplitude-complementary fade. Clean pitch shift past
+24st; a repitched pure sine stays a single tone.
- **Voice takeover declick:** `Voice::start` gained a `takeoverDeclick` parameter (gated on the
envelope complement) removing the click at mono retrig/fallback and poly at-cap steal
boundaries. The "steal-all" DAW symptom was diagnosed as no-loop sample exhaustion under
Preserve (voices playing to silence, not being stolen) — the engine steals exactly one voice
per note-on as designed.
- **Stereo bus pin + channel-mode auto-default:** output bus made **permanently stereo** — the
dynamic mono↔stereo bus renegotiation (which hard-panned dual-mono) is deleted. `ChannelMode`
becomes decode-only (downmix vs dual-mono on WAV decode); channel mode auto-defaults from the
loaded capture's channel count via pure `channelModeFor()` unless the user has explicitly
toggled it (explicit toggle latches the preference). `ComponentState` **v8→v9**:
`channelModeExplicit` bool added; pre-v9 blobs treat the stored mode byte as an explicit
preference (no auto-override).
- **Drop-to-FX injection fix:** injection switched from `TrackFX_SetNamedConfigParm(...,
"vst_chunk", <base64>)` (silently unappliable for VST3 — REAPER's wrapper cannot accept
unframed component-state bytes) to a Steinberg-format `.vstpreset` file whose 'Comp' chunk is
the serialized component state, applied via `TrackFX_SetPreset`. FX hotspot now prefix-matches
`tcp.fx*`/`mcp.fx*`/`fx_*` (embed strip tokens excluded). `reasampler_uid.h` split out of
`reasampler_vst.h` as an SDK-free header so `instrument_drop` can derive the class-ID hex string
without the VST3 SDK.
- **Drag-out arm fix:** `SetCapture` moved to the drag-arm branch of `handleClick` in `bank_panel`
so a first straight-out drag (pointer leaving the client rect before a second click) correctly
arms and fires the OS drag-out.
- **Panel mode-toggle repaint:** the Design/Arrange mode-toggle actions now call
`bankPanelInvalidate()` after applying the mode switch, so the panel footer reflects a mode
change fired from the Actions list or a keybinding, not only a button click.
### GA2 fix pass (2026-07-28)
Merged to dev; integrated suite 55/55 green. Two fixes found during GA DAW testing, not
PLAN-tracked.
- **Preserve pitch engine — ring prime:** `pitch_shift` now primes its ring buffer with the
actual upcoming source at note-on (was zero-filled): gap-free frame-0 onset (the ~25ms Preserve
onset latency is gone — Preserve now speaks on frame 0, matching Varispeed), clean repitch
across the full C1C8 range, and a real-content-bounded tail (the prior last-window
tail-truncation that clipped the decay is gone).
- **Takeover declick rev-2/3 — bounded blend, supersedes the GA fix:** the declick mechanism is
now a bounded blend (`out*(1-w) + ref*w`, w decaying from 1.0 over the blend window) applied at
every takeover boundary — mono retrig/fallback, poly at-cap steal, and **preview re-trigger**.
This supersedes the GA pass's `(1-amp)` envelope-complement gate, which zeroed the compensation
on Trigger and zero-attack restarts, leaving an audible click. The preview card is now covered
by the same mechanism.
### Preview via real MIDI note path + self-contained playback (pS; 2026-07-28)
Merged to dev; integrated suite 55/55 green. Architecture corrections from DAW testing, not
PLAN-tracked.
- **`PreviewCard` retired.** Preview now injects a synthetic note-on at the loaded capture's root
note into the main `VoiceEngine` (the same path host MIDI uses), so preview obeys
polyphony/mono/voice-stealing/envelopes. `sampler_core` no longer has a `PreviewCard`; the
processor no longer sums a separate preview voice. The unity-Varispeed-bypass demotion (added
in GA2 for the PreviewCard) is removed — GA2's primed shifter speaks on frame 0 anyway.
- **Self-contained playback.** ReaSampler 9000 no longer depends on the extension being loaded to
play. `ComponentState` bumped v9→**v10** with a `SampleRefs` table: per referenced sample, the
instance owns a project-relative path + decode intrinsics (root note, loop points, channels,
displayName) — NOT a copy of the audio. `reloadInstrument()` decodes directly from those refs
(bank-free — plays with the extension absent). The bank/bridge is now a browser source only:
loading a capture copies its reference into the instance's `SampleRefs`. The reopen-heal
apparatus (retry timer, editor-gated poll-to-play) is removed. Pre-v10 blobs lift to empty refs
and re-save self-contained; a bounded legacy lift covers migration.
### Q-W0 fix-now remediations — closes Q-W0 (2026-07-28)
Merged to `phase-q` (commit `546927e`). Landed six Daniel-approved fix-now remediations from the
Q-W0 pre-restructure functional + DSP quality audit — T1-01 (linked-lag stereo SOLA + follower
self-heal fallback), T1-03 (playable-span prime bound), T1-09 (declick dead-state removal),
T2-01a (provenance wire-cursor hardening backport), T3-01 (rate-derived gain ramp), T3-03
(rate-derived fade ceiling) — plus seven review riders. **Closes Q-W0 entirely** (audit, triage,
sign-off, and remediation all complete) and opens Q-W1.
### Phase Q — 1.0 structural reorganization
Reorganized all of `src/` into `core/` (pure, subsystem-namespaced: `model`/`view`/`capture`/
`audio`/`ui`/`reclaim`/`version`/`json`/`util`/`wire`/`instrument/{engine,map,ui}`), `shell/`
(REAPER/host-facing: `capture`/`panel`/`view`/`persist`/`actions`/`instrument`/`bank_ops`), and
`app/` (`main.cpp`), splitting several god-modules along the way under a soft ~600-line-per-TU
ceiling. Suite reached 60/60 at Q-W1, 61/61 for the remaining waves.
#### Q-W1 — safe opener: `core/json` extraction + directory/namespace layout (2026-07-29)
Merged to `phase-q`, 60/60 green. Extracted pure `core/json` (`Reader`/`Writer`), deleting the
five hand-rolled JSON decoders (`bank_model`/`bank_book`/`view_mode_model`/`owned_manifest`/
`tail_control`) — round-trip byte-identical to before. Collapsed the wire-`Cursor` family into
`core/wire`; added a shared `readFileBytes` helper; extracted `slot_map` from `bank_book`.
Relocated ~50 clean modules into the `core/{model,view,capture,audio,ui,reclaim,version,json,
util,wire}` / `core/instrument/{engine,map,ui}` / `shell/{capture,panel,view,persist,actions,
instrument}` layout; `main.cpp` moved to `app/`. Unified `ui::Rect` + `contains()` (the
XYWH-vs-LTRB fork retired, `footer_bar.h`'s "NAME NOTE" collision workaround gone); `clamp01`
deduplicated. Naming riders: survivor parser minted as `json::Reader`/`Writer`; `BankIndex`
renamed `BankModel`. `reasampler_uid.h` relocated to `core/wire/`. Interim `core/namespaces.h`
shim added for six not-yet-split god TUs (each downstream split wave retires its own includes).
**Riders explicitly skipped/deferred:** T4-22 (`hitIndex` hit-test template), T4-06
(`view_mode_model` planner split), T4-09 (`view_lanes` split). **Open residual:** `bank_book.cpp`
still 737 LOC — the serialize/deserialize seam identified but blocked on a `nameKey` linkage
design decision, escalated to Daniel and pending as of 2026-07-29 (resolved in Q-W5).
#### Q-W2 — split `bank_panel.cpp` (2026-07-29)
Merged (`pq-w2-panel`), 61/61 green, reviewed-approved. Split the largest extension god-module
(3459 LOC at the Q-W0 census, 8+ responsibilities) into eight TUs under `shell/panel/`:
`panel_render`, `panel_thumbnails`, `panel_audition` (direct call-through, never virtual —
preview idle path unchanged), `panel_input`, `panel_bank_ops`, `panel_window`, plus two new seams
(Q-5 settled reshape, Daniel 2026-07-28) `panel_layout` (toolbar/footer/menu rects + row/cluster
builders + region geometry) and `panel_drag` (the card-drag/hover state machine, mirroring the
pure `card_drag`) — without which `panel_render` (~700) and `panel_input` (~800) would have
shipped over the ceiling. `bank_panel.h` split alongside (Interface Segregation).
`panel_bank_ops` becomes the single home for bank-CRUD verbs that Q-W4 dedupes `actions.cpp`
against. **Recorded ceiling overages (reviewer-endorsed, comment-volume driven, non-comment lines
~322369):** `panel_input.cpp` 636, `panel_render.cpp` 613, `panel_state.h` 608 — no honest seam
remained, bisection rejected. **Review note for Q-W4:** `panel_bank_ops`'s verbs still embedded
prompts/panel-state nudges; Q-W4's dedupe needed promptless inner verbs, not a call-site swap.
**DAW-smoke-tested and passing (Daniel, 2026-07-29); the full panel-parity verification batch
was not executed.**
#### Q-W2v — split the VST god-modules (2026-07-29)
Merged (`pq-w2v-vst`), 61/61 green, reviewed-approved. Closed the audit's structural scope gap —
the VST artifact's god-modules had no owning wave. Split `reasampler_editor.cpp` (3065 LOC,
largest file in the repo) into eight TUs along the Sample/Browse/Zone face axis: `editor_session`,
`editor_controls`, `editor_layout` (pure-candidate hoist into the existing pure home,
`editor_geometry` — discharges T2-06's stranded-layout-math finding), `editor_paint_sample`,
`editor_paint_browse_zone`, `editor_input_sample`, `editor_input_browse_zone`, `editor_platform`.
Split `reasampler_processor.cpp` (1164 LOC) into `processor_state` / `processor_reload` /
lifecycle+`process()` (kept whole, no virtual seam added to the atomic-pointer-swap reload
pattern). Split `sample_map` into resolution core vs the `component_state_io` binary codec (+
header split, T4-13 ≡ T2-07) — the extension's preset-blob path stops linking the whole voice
engine to serialize one blob. **`sampler_core.cpp` stays whole (968 LOC) — a documented,
deliberate exception to the ~600 ceiling** (per-voice-per-sample envelope ticks need same-TU
inlining to let the compiler inline the stack; no LTO in the build; a by-class split would blow
out the dispatch); its header splits into `zone_params.h` + `sampler_core.h`. The `core/wire` LE
byte-codec template (`putLE`/`readLE`) lands here with its biggest consumer. The `src/vst/`
directory is gone — all VST sources now live under `core/instrument/` and `shell/instrument/`.
**Deferred/known:** `component_state_io.h` still transitively includes `sample_map.h`→
`sampler_core.h`; the `engine` namespace is deferred (`sampler_core` stays flat `reasampler`);
capture-side LE rewires left for the capture family. **DAW-smoke-tested and passing (Daniel, 2026-07-29); the full editor/processor-parity
verification batch was not executed.**
#### Q-W3 — split `main.cpp` (2026-07-29)
Merged (`pq-w3-main`), 61/61 green, reviewed-approved. Reduced `main.cpp` (1897 LOC at the Q-W0
census) to API pointers + `ReaperPluginEntry` + dispatch by hoisting four TUs (T4-02 reshape,
settled with Q-5, Daniel 2026-07-28 — the planned three left `capture_orchestrator` over the
ceiling): `capture_orchestrator`, `capture_batch` (fourth hoist, landing `capture_orchestrator`
~450), `scope_resolve`, `realtime_lifecycle`. `FxBypassGuard` moved out but stays a stack RAII
object (precision-critical); the realtime idle tick stays a single pointer test. Q-W0 riders
landed in this wave: **`ICaptureBackend` deleted** (T4-26) — one deriver, zero polymorphic call
sites; `OfflineRenderBackend` becomes concrete; the CLAUDE.md/CONTEXT "two backends behind one
interface" description corrected in the same commit. Shared `stampCaptureSample` capture-epilogue
dedupe (T2-09); `makeUniqueTag` fixed with a per-session monotonic counter (T1-11 — same-second
batch captures previously collided silently). Naming rider (Q-9, settled — Daniel 2026-07-28):
pure module takes the stem `capture_realtime`, shell takes the suffix (mirroring
`drag_out`/`drag_out_win`) — renamed from `realtime_record`; `capture_realtime_finalize` split in
the same surgery. WAV/RIFF consolidation rider (audit §4e, settled): one pure `wav_codec` owner
(walker+layout+build+patch) absorbing `ingest.cpp`'s WAV/PCM build helpers (ingest drops to ~500,
gains a test target). `wav_codec_tests` replaces `wav_trim_tests`; `capture_realtime_tests`
replaces `realtime_record_tests`. **Known open:** `wav_trim.h`'s transitional forwarding shim
still has three live includers (`sample_map.h`, `editor_session.cpp`, `processor_reload.cpp`);
`ingest.cpp` trimmed to 567 LOC but keeps the `namespaces.h` shim. **DAW-smoke-tested and passing (Daniel, 2026-07-29); the full
null-test/bit-identical-repeats/capture≠placement verification batch was not executed.**
#### Q-W4 — split `actions.cpp` + dedupe bank verbs against `panel_bank_ops` (2026-07-29)
Merged (`pq-w4-actions`), 61/61 green, reviewed-approved. Split the two unrelated command-id
families in `actions.cpp` (1016 LOC at the Q-W0 census — T4-03, no reshape needed) into
`design_view_actions`, `bank_actions`, `prune_action` (keeping the `doBankPruneFolder` deletion
authority contract intact, routing to `persist`'s `prune_fs` after Q-W5). Deduped
`actions.cpp`'s own `promptText`/`mintBankId` and bank verbs against the Q-W2 `panel_bank_ops`
single owner — bank verbs reshaped to **promptless inner verbs** (one mutation home, two UX
skins: panel and actions each keep their exact prior UX); `promptText` renamed
`promptBankName`; `persistBankOp`/`persistBook` gain null-session guards. `prune_action` verified
a clean deletion-authority isolate (no `Undo_*`, no ext-state writes). Command-id
suffixes/display phrases verified byte-identical in review (FOREVER-STABLE). **Review 🟡
(resolved in Q-W6):** two session pointers / a null-session-as-model-rejection misreport
(unreachable today). **DAW-smoke-tested and passing (Daniel, 2026-07-29); the full per-wave
verification batch was not executed.**
#### Q-W5 — split `persist.cpp` (isolate the single file-deletion authority into `prune_fs`) (2026-07-29)
Merged (`pq-w5-persist`), 61/61 green, reviewed-approved. Split `persist.cpp` (852 LOC at the
Q-W0 census, 5 responsibilities) into `session` (lifecycle+poll, `BeginLoadProjectState` reload
hook), `ext_state_io` (ext-state ↔ JSON serialization bridge + GUID minting + folder relocation),
and **`prune_fs`** (prune scanning + `deleteOrphanFile` via `SHFileOperationW`) — the split
**concentrates** the byte-deleting authority into one obvious module (verified tree-wide as
exactly one anonymous-namespace function), never spreading it. Q-W0 rider (T2-04, settled):
generalized the `GetProjExtState` grow-loop retry policy into a header-only template, rewiring all
three hand-rolled copies (`usage_scan`'s start cap raised 4KB→64KB, allocation-only, verified
equivalent; the grow-loop gains a defensive NUL). **Resolves the Q-W1 open residual:** the
`bank_book_json` split lands via a private static `nameKey` (Daniel-approved option a) —
`bank_book.cpp` is now ~462 LOC. `persist.h` kept as a compat umbrella across the in-flight waves
(retired in Q-W6). **DAW-smoke-tested and passing (Daniel, 2026-07-29); the full
save/load/undo-reload/relocation/prune verification batch was not executed.**
#### Q-W6 — OCP registration table + residual fat-header (I) splits (2026-07-29)
Merged to `phase-q`, 61/61 green, reviewed-approved. Replaced the ~350-line hand-written
non-table action registration blocks (isolated in `app/main.cpp` after Q-W3) with a data-driven
`ActionTableRow` registration table (flat function-pointer dispatch, no `std::function`/virtual);
unload mirror-unregisters from the same table; `main.cpp` shrinks 653→404. Capture rows derive
their suffix+phrase from the pure `captureActionTable()` (parallel-list risk gone by
construction). FOREVER-STABLE suffixes/phrases/retired-ids verified byte-identical row-by-row in
review. Split residual fat headers: `persist.h` umbrella retired (13 callers repointed);
`capture.h`'s realtime seam moved to `capture_realtime_shell.h`; the `wav_trim.h` shim + its
INTERFACE target deleted. Phase-end cleanup riders: `bankOp*` verbs + `persistBankOp` lifted to
new `shell/bank_ops` taking `ReaSamplerSession&` (dissolves the Q-W4 🟡 review note); **`core/
namespaces.h` DELETED** (the interim Q-W1 shim's contract fulfilled, ~26 includers rewired); the
grow-loop rehomed to `core/wire/ext_state_read.h`; a stale-comment sweep
(`persist.cpp`/`bank_panel.cpp` refs); CLAUDE.md's persist/bank_book/actions/wav_codec bullets
corrected in-wave. **Review-noted follow-on, not landed:** extending the table pattern to the
design_view/bank/ingest families' hand-registration; `view_mode_model.h` (748 LOC) remains the
largest header, its planner split stays optional/deferred. **DAW-smoke-tested and passing (Daniel,
2026-07-29); the full per-wave verification batch was not executed.**
**All seven Phase Q waves (Q-W0 through Q-W6) are recorded as structurally complete on
`phase-q`.** Every wave from Q-W2 onward originally carried the same recurring caveat verbatim:
in-DAW behavioral-parity verification checked off as PENDING/deferred by design. **Daniel has
since DAW-smoke-tested Phase Q and confirmed it passing (2026-07-29); the full per-wave
verification batch described above was not executed.**
---
## Part 4 — The version-0 plan of record
Distilled from `PLAN.md` (the tracking checklist itself, not `COMPLETED.md`) at the 0→1 roll.
Parts 13 above narrate what landed, drawn from `COMPLETED.md`; Part 4 preserves the plan-of-record
layer — open questions, forks, and settled/recommended decisions — as it stood in `PLAN.md`,
including entries that were still live deferrals rather than finished history at roll time (see
the roll report for the full enumeration of those).
### Open questions carried from CONTEXT.md (still open at the 1.0 roll)
- **`parseInt` narrowing hardening.** `bank_model.cpp`'s `parseInt` casts `int64_t → int` via
`static_cast` without a range check; integers that fit in int64 but exceed `INT_MAX` are
implementation-defined. Hardening candidate — add a bounds check before the cast when
integer-field validation is next in scope.
- **Capture send/routing isolation.** The FX-scope capture neutralizes out-of-scope FX, gain, and
pan, but NOT aux **sends** — a downstream coloring send (e.g. folder → reverb track) still
routes and blends into an item/track capture past the intended isolation point. The hard part:
distinguishing source routing that must be preserved (a MIDI send whose destination synth IS an
item's true audio source) from coloring sends that must be excluded (folder → reverb). Repro
case and a likely snapshot/mute-sends approach are recorded in `PLAN.md`; no fix has landed.
### Phase D2 — two-canvas closure
D2 (item-level mode projection, additive to D1) is functionally complete — D2-W1, D2-W2, D2-W3-A,
D2-W3-B all landed. One item is explicitly **deferred, not dropped**: a per-track lane/mode-state
panel indicator. No natural cheap home was found in the bank panel; the mode switch already shows
the active mode. Can be picked up later if wanted.
### Phase S — MIDI-playback instrument (ReaSampler 9000)
Landed on dev 2026-07-27 (S1S18 plus the product-name/binary-rename work); `PLAN.md` records DAW
verification as pending Daniel's smoke test at that point. The then-authoritative spec was
CONTEXT.md §MIDI-playback instrument (Phase S); product framing `docs/product/midi-playback.md`.
**S13 — cross-artifact ingest relay: DEFERRED, not built.** Spike verdict (ps-w12, 2026-07-27):
DEGRADED. The instrument's REAPER bridge (`reaper_bridge`) is deliberately read-only; a relay
would need a new instrument WRITE seam into ext-state plus an extension-side timer poller
servicing a drop-ingest inbox key with a claim/clear nonce — the same cross-process handshake race
the S17 spec rejected for alternative (A). Both the read-only-instrument boundary and the new
poller are load-bearing design calls, so the relay is deferred to a future wave; the shipped
ingest gesture stays drop-onto-docked-panel (S8). The editor's degrade-path affordance ("drop
files onto the ReaSampler bank panel to add them") landed as part of Phase S.
**Product name.** ReaSampler 9000 (Daniel, 2026-07-26, on DAW-testing the S1S6 instrument). The
extension stays ReaSampler.
**Compat verification — must-DAW-verify before shipping the rename, unchecked in `PLAN.md`.** The
working assumption is that REAPER rebinds a saved instance by VST3 class UID, not module filename,
so renaming the module with an unchanged UID keeps saved projects working. This was **not
confirmed from source** — a web check surfaced a JUCE/VST3-replace-VST2 case suggesting the
binding may be more nuanced (possible FXID involvement) — treat as to-verify, not asserted fact.
Required DAW check: save a project with a ReaSampler 9000 instance under the old filename, rename
the module, reopen, confirm rebind + state restore. Fallback if REAPER keys partly on filename:
keep the current filename (display-strings-only rename).
**Held and optional-forever (noted, not specified — no PLAN points drawn up):**
- Tier 2 "expressive" (HELD) — velocity layers, round-robin, full ADSR, per-sample tuning/gain
trim, sustain loops; the next depth increment once Tier 01 proves the instrument belongs.
- Tier 3 "instrument polish" (optional-forever) — filters, filter/pitch envelopes, LFOs, per-voice
pan, choke groups, a modest FX slot; a direction to leave room for, never a commitment. S16
landed the *pitch* envelope + Varispeed/Preserve pitch-engine mode early (Daniel's directive),
so the Tier-3 line now means the *filter* envelope + LFO remainder.
- Sinc Varispeed-quality upgrade (HELD — WDL_Resampler) — beats the 2-point linear interp for
Varispeed base-repitch quality; an optional per-voice quality toggle, RT-suitable but heavier;
a Varispeed-quality option only, never a Preserve engine.
- WDL_SimplePitchShifter swap (HELD — fork S16-F2 route a) — drop-in swap for `pitch_shift` if the
hand-rolled OLA onset latency or warble proves musically unacceptable; same
`PitchEngine::Preserve` contract. WDL excluded from the shipped build by include-chain
(windows.h).
- Trigger choke-on-note-off (HELD — fork S15-F1) — a future option for Trigger mode to cut on
note-off or a same-group re-trigger (hi-hat open/closed); deliberately out of S15 scope.
**Editor view-model redesign (S-VIEW, three views: Sample / Browse / Zone) — landed.**
Re-partitioned the editor from a two-view toggle into a three-view model with the loaded sample as
home (Sample default face, Browse a modal picker over Sample, Zone a dedicated keymap surface);
added key-tracking, preview velocity, and the r10 velocity→amp transfer curve, plus an envelope
overlay, real piano-key pattern, and velocity-curve editor components. Built directly on the
post-L3 look-and-feel as its baseline — no separate restyle-after pass. All r9/r10 forks settled
2026-07-27: S-VIEW-F1 (preview velocity persists via envelope-v6 `ComponentState`) and S-VIEW-F2
(envelope nodes draggable via `envelope_edit`) folded into S-VIEW-4/S-VIEW-3; R10-F1 (flat y=1
default) folded into S-VIEW-9; S-VIEW-F3 (full-window overlay) implemented as Browse rendering as
a full-window modal over Sample.
**Wave B — Sample-face recomposition (r11, 2026-07-27) — landed.** All linear sliders replaced by
radial knobs in a fenced knob deck; mode toggles compact, not full-width; the inline
velocity-curve box replaced by a miniature curve-preview button + full-size popup (right-click
deletes a node); hero waveform full-width. S-VIEW-11/12/13 landed as FB1 (knob deck + master gain
+ curve popup + full-width hero, merged 2026-07-27, suite 55/55) and FB2 (Zone-panel parity,
merged 2026-07-28, suite 55/55). Phase S editor Wave B (r11) is recorded complete in `PLAN.md`.
Forks settled: R11-F1 (hero height vs. default window) at FB1 build — elastic hero, 840×620
default kept; R11-F2 (Zone-panel parity) at FB2 build — knob deck + curve popup adopted on the
Zone panel, `param_slider` slider rows retired there.
### Phase Q — Quality (1.0 structural reorganization): the decision record
(The wave-by-wave landing narrative is recorded in Part 3 above, distilled from `COMPLETED.md`.
What follows is `PLAN.md`'s own decision/fork record — the settled and recommended calls that
shaped the reorg — kept here because it does not otherwise survive verbatim in the landed-narrative
form.)
Phase Q was named the last structural pillar — namespaced `Q` (Quality; M/D/B/R/V/S/L all already
taken) — and framed as a pure structural refactor: no feature, no behavior change, the test suite
passing unchanged as the proof of correctness. Product framing, the Vital-grounded target shape,
the grep-verified SOLID audit, and the fork record (Q-1..Q-6) live in
`docs/product/code-organization.md`.
**The gate.** Phase Q was gated on the tree being otherwise quiescent — Daniel's plain readiness
target: "when Phase S and L3 are finished." Satisfied 2026-07-27: Phase S merged; Phase L complete
(L1L7); D2 functionally complete with its one deferred item (per-track lane indicator) explicitly
not blocking; M9 (slots) abandoned 2026-07-27 (Daniel) and will not be reactivated — named in the
gate only so that reactivating D2's deferred indicator would re-arm quiescence.
**Q-W0 (pre-restructure functional + DSP audit) ran first**, per Daniel's ask, complementary to
the grep-verified SOLID/naming audit already grounding Q-W1..Q-W6. Four parallel tracks (T1 DSP,
T2 architecture, T3 env-coupled constants, T4 sizing/placement) produced 59 findings
(`docs/product/code-quality-audit.md` + `docs/product/audit-notes/`); Daniel approved every
disposition 2026-07-28. The Q-11 question (pitch-technique replacement) was answered by the audit
itself: the SOLA pitch engine is sound — no technique replacement warranted; every pitch finding is
a bounded in-technique fix or a documented operating limit.
**Settled decisions:** Q-1 phase name/id family (`Q`, `Q-W0..Q-W6` + `Q-W2v`) settled this-doc.
Q-10 audit-report home = a committed doc (`docs/product/code-quality-audit.md`), not a tracked
issue list. Q-11 = defer to findings (a bounded OLA fix weighed before a technique replacement,
which would be a Daniel decision at triage time, not automatic). Q-5 settled to seams-by-
responsibility with the T4 seam lists adopted (`bank_panel` 6→8 seams, `capture_orchestrator`
further split with `capture_batch`); the ~600-line ceiling is an acceptance criterion on every
split wave, not a bisection target. Q-6 settled in scope, last wave. Q-8 settled both renames
(`BankIndex`→`BankModel`; JSON parser → `json::Reader`/`Writer`) plus, from the audit,
`ICaptureBackend` deleted in Q-W3 (one deriver, zero polymorphic call sites — the CLAUDE.md/
CONTEXT correction rides Q-W3 itself). Q-9 settled: align to stem `capture_realtime`, shell
suffixed, during W3. VST placement (audit §4a) settled: `core/instrument/{engine,map,ui}` +
`shell/instrument/` under the single `core/`/`shell/` top split. WAV/RIFF consolidation
(audit §4e) settled as a Q-W3 rider: one pure `wav_codec` owner (walker+layout+build+patch).
Q-W2v scheduling (audit §4f) settled: parallel with Q-W2 (different artifact, zero file overlap).
**Recommended-and-adopted (Q-2..Q-9, `docs/product/code-organization.md` §6):** Q-2 JSON
extraction in scope and first; Q-3 `core/`/`shell/`/`app/` top-split with subsystem dirs beneath
(over pure-Vital subsystem-first — makes the pure/shell invariant structural); Q-4 sub-namespace
matches sub-directory; Q-7 naming rides the relocation waves, no dedicated naming wave (forced
once Q-3/Q-4 settle).
**Hard constraint — zero runtime cost.** No added virtual dispatch, no header→TU indirection, no
changed call/inline or branch shape on the three hot paths: `peaks` envelope compute,
audition/preview, the realtime-capture idle tick. An acceptance criterion on every point.
**Structural heuristics (acceptance criteria on every wave):** (1) more directories a must, more
files good, ~600-line ceiling as the bar with the audit's named seams as method — a documented
hot-path exception (`sampler_core.cpp`) is legitimate, silent overshoot is not; (2) templates
earned for compile-time dedup (the LE byte codec) but not for name-only unification (the rect
family stays one concrete `ui::Rect`, no template); (3) SOLID is great but saved CPU is better —
no dispatch-stack blowouts anywhere, prefer static polymorphism where types are compile-time-known.
**Sequencing (as planned; all landed 2026-07-29):** GATE → Q-W0 → Q-W1 (safe opener) →
{Q-W2 → Q-W4; Q-W2v parallel with Q-W2 (zero file overlap); Q-W3 → Q-W6; Q-W5 best after Q-W4}.
Big-bang was rejected; every wave independently landable and CTest-green throughout.
**Must-verify-before-build checklist** (all satisfied by the 2026-07-29 landing): Q-W0 closed
before any structural point (triage + Daniel sign-off on every disposition); ~600-line ceiling per
split TU with `sampler_core.cpp` the sole documented exception; no dispatch-stack blowouts
anywhere; hot-path call/inline shape unchanged (`computeEnvelope` stays free-function, audition
stays direct call-through, idle tick stays a single pointer test); command-id/display-string/
ext-state-namespace/VST3-UID contracts left byte-identical by any rename; name-collision sweep
resolved before W1; naming changes zero-behavior-change and off the wire; the GATE re-confirmed
against dev before W1.
---
## Part 5 — Spec provenance (pre-build reasoning)
Distilled from `CONTEXT-ARCHIVE.md` ("ReaSampler spec provenance", 1230 lines) ahead of its
deletion. That file held build-detail sections moved verbatim out of `CONTEXT.md` once each
phase landed, in original document order — each section was the unedited original text, kept
for provenance rather than as a living reference. Most of it was pre-implementation scaffolding
whose value fully expired once the code shipped, so it is summarized rather than transcribed
here: recurring per-phase **"Module architecture (preserve the pure/shell split)"** sketches
(superseded by the nineteen per-directory `CLAUDE.md` files the shipped tree now carries),
recurring **"REAPER API surface (verify all signatures)"** / **"REAPER / Steinberg API surface"**
/ **"LICE / WDL API surface"** checklists (superseded by root `CLAUDE.md`'s standing instruction
to verify every cited API name against `vendor/reaper-sdk/sdk/reaper_plugin_functions.h`), a
**"Data model (sketch — refine in code)"** for the original single-artifact tool (the shipped
`Sample`/`BankModel` shapes are the truth now), and a numbered **"Build order"** for that same
original tool. These appeared, in the same shape, ahead of the Phase B (original capture tool),
D1/D2 (Design View), M (multi-bank), R (removal-and-prune), and D3 (VST3 spike) build-detail
sections, plus a "Kit architecture (the pure/shell split)" sketch ahead of Phase L — all dropped
for the same reason. What follows preserves only the design reasoning and settled constraints
that do not survive verbatim in `CLAUDE.md`, `CONTEXT.md`, or Parts 14 above.
### WDL pitch/resample surface — the S16 viability assessment
A prior sweep had dismissed `WDL_SimplePitchShifter` (`vendor/WDL/WDL/simple_pitchshift.h`) as
"wrong tool" because it is duration-preserving — but Daniel's directive made duration-preserving
*the requirement* for the Preserve pitch mode, so the dismissal was replaced with a real
viability read of the header. Findings: the vendored WDL tree's *entire* pitch/resample surface
is exactly two headers, `resample.h` (`WDL_Resampler`, a sinc/linear resampler — held as an
optional Varispeed-quality upgrade only, since a resampler couples duration) and
`simple_pitchshift.h` (`WDL_SimplePitchShifter`, time-domain OLA, duration-preserving — the S16
Preserve-engine candidate under fork S16-F2 route (a)); there is no elastique-class
formant-preserving shifter anywhere in the tree, and REAPER's own elastique is a separate
zplane license unavailable to the project without a new third-party dependency (JUCE/rubberband/
signalsmith), which was not proposed. `WDL_SimplePitchShifter` was assessed RT-viable *with a
pre-warm discipline* (run silence through once at voice-allocation so its OLA ring and queue
reach steady state before the audio thread ever calls it) but with a real cost: an inherent
~half-window onset latency (~25 ms at a 50 ms window) and basic ("SimpleWindowed"-class) quality
with audible warble on large transpositions and no formant preservation. The recommendation was
route (a) (`WDL_SimplePitchShifter`, low-cost) or route (b) (a hand-rolled pure `pitch_shift`
module) as the Preserve engine, with the pitch-envelope modulation hand-rolled over whichever
engine won. **What shipped:** a hand-rolled correlation-aligned SOLA `pitch_shift` (route (b)) —
see `CLAUDE.md`'s GA post-launch DAW-fix pass — with the WDL swap recorded as a still-HELD
fallback in `PLAN.md`'s carried-forward list (Part 4 above) should the hand-rolled engine's
latency/warble ever prove unacceptable.
### ReaSampler 9000 — the UX overhaul (S10S13) findings
Daniel DAW-tested the S1S6 instrument and the verdict was that it *worked* but the UX was
unacceptable — "this is supposed to be better than ReaSamplOMatic5000" (RS5K). The S1S6 editor
was spike-grade: a clickable sample list, zone rows each carrying seven tiny ±1 nudge/delete
mini-buttons, text-only labels, no keyboard visualization, no waveform, no drag interaction, no
scrolling for long lists — setting a zone from C1 to C4 by clicking "+" thirty-six times was the
headline catastrophe. The overhaul was then **reframed around the actual workflow, not a
keymap** (a revision superseding the original keymap-first S10 plan): most instances play a
*single* capture, so the metric became **time-to-first-note**, not zone-table completeness. The
settled hierarchy: (1) primary flow is one capture, fast; (2) a **fresh instance is silent** —
nothing auto-selected, no auto-play of sample #1 (a deliberate reversal of the earlier S4
"first sample plays" convenience, recorded as a reversal, not a regression); (3) a **capture
browser** (scannable cards with peak thumbnails, name, root/key badge, bank-filterable) replaces
the "giant list of item blocks" anti-pattern; (4) graphic, descriptive controls with a guided
single-capture setup fast path; (5) multi-zone keymap editing is **demoted to an opt-in "Zones"
panel** — "most of the time the zones won't be used." **What "better than RS5K" meant,
concretely:** match RS5K's genuine strengths (drag-a-file-on-it loading, a visual note-range
control, a draggable waveform with loop markers, ADSR sliders) while exploiting its real
weaknesses — RS5K is one-sample-per-instance (forces track sprawl) and has no multi-zone view in
a single instance, where ReaSampler 9000 is multi-zone-in-one-instrument by design with an
opt-in Zones panel RS5K structurally lacks. **Sequencing rationale:** S10 (the browser + guided
setup + silent-by-default fix) was recommended first because it carries the *entire* felt UX
wound and the time-to-first-note metric, ahead of both S11/S12 (which lean on S10's browser) and
ahead of S7 (stereo capture) — "it works, the UX is awful" points at the editor as the live
wound, not the engine, even though no hard dependency runs either way.
### Drop-and-load — dragging a capture onto a track's FX button (S17 spec)
The gesture: while a capture is dragged out of the bank panel, a track's TCP FX button becomes a
drop zone; dropping there instantiates a ReaSampler 9000 on that track with the dragged capture
already loaded and selected — placement-of-the-player, the third integration gesture alongside
capture and placement-into-arrange. It needed a genuinely new drag mode because the existing
`CF_HDROP` OS-drag path (M11) cannot carry it — REAPER's FX button is not a native
drop-target-that-instantiates-a-plugin-with-a-file — so the extension itself has to hover-track
the pointer over REAPER's own UI and drive the insert. The `drag_out` pure module was to gain a
third `DragGesture`, `InstrumentDrop`, disambiguated from `Internal` and `OsDrag` purely by
pointer position plus a shell-supplied "is this REAPER's own UI" predicate — mirroring how the
existing gesture decision stayed pure over a shell-supplied rect. **The load-capture seam was
the hard, load-bearing part:** the Phase S spec at the time gave the instrument only a read seam
over the bank, with no entry point for an external actor to say "load *this* capture." Two
mechanisms were weighed: **(A, rejected)** a fresh-instance ext-state handshake (a "pending load"
hint keyed to the target track/FX, claimed and cleared by the new instance on init) — rejected
because the claim/clear race needs a cross-process handshake to get right; **(B, settled)**
direct VST3 component-state injection immediately after `TrackFX_AddByName` returns the new FX
index, writing the instance's own serialized chunk (with the capture pre-selected) via
`TrackFX_SetNamedConfigParm(track, fx, "vst_chunk", <base64 blob>)` — deterministic, no shared
state, no handshake, at the cost of making the component-state blob format a hard cross-artifact
contract between the extension and the instrument. **Deviation from the landed code:** per root
`CLAUDE.md`'s `instrument_drop_win` entry, the shipped mechanism is not the spec's settled (B) —
the `TrackFX_SetNamedConfigParm` "vst_chunk" write was found **silently unappliable for VST3** at
build time, so the shipped shell instead applies the dragged capture's state via a transient
`.vstpreset` image + `TrackFX_SetPreset`, all-or-nothing with a `TrackFX_Delete` rollback on
failure. The gesture-disambiguation contract (position, not a mode toggle; M11's OS-drag and the
internal bank-to-bank drag byte-for-byte unchanged) and the invariant framing (a deliberate
placement act, no auto-capture, no timeline insert, no private sample copy — the instrument
reads the one authoritative bank by reference) carried through unchanged.
### L2/L4/L5 dock-panel layout contracts — the reasoning
**L2** was scoped as a *layout design*, not a skin pass, because M11 added a real button
inventory (action-trigger buttons + keybinding-help labels) that had to be placed without
crowding the grid — the mandate was to group by *task* (capture / organize / reclaim / view),
not by build phase, and to sequence L2 after M11 merged so it inventoried the buttons actually
landed. **L4** re-homed that inventory around *frequency and intent*, shipping no new action and
changing no capture/placement behavior: capture + placement + maintenance moved to a **top**
toolbar (the eye's first landing, matching the tool's purpose); Design View tagging/switching
took the vacated **bottom** toolbar; the footer got a narrowed `[Arrange|Design]` toggle, a
proper Tail button, and Prune — ordered left (benign/frequent: view toggle, tail length) to right
(destructive/rare: Prune, isolated and `warn`-colored) so a mis-click near the left is cheap and
the one destructive control stays spatially and chromatically distinct. **L5** was a third
refinement pass, again shipping no new action: the rarer capture variants (Batch Items/Razor,
Capture RT, Cancel RT) moved into a right-anchored "⋯ More" overflow menu; button *faces* stayed
short labels with a hover tooltip carrying the full action name (`ReaSampler:` prefix stripped
for display, not for the registered gaccel name) and its keybinding; the Tag Design/Untag pair
became **four opposite-mode-only** buttons (Item/Track × Arrange/Design) over the *already
existing* `doTag`/`doUntag`/`doMoveItems` action families — pure layout + enablement wiring, not
new feature work — with a pure enablement rule (a button is live iff its target mode differs
from the active mode, read from the same source the footer toggle uses); `VIEW_TOGGLE_MODE`,
`VIEW_ACTIVATE_ARRANGE`, and `VIEW_ACTIVATE_DESIGN` were dropped from the bottom toolbar (actions
stay registered, FOREVER-STABLE ids unchanged) since the footer toggle already covers switching,
while `VIEW_SHOW_BOTH` was kept as the cross-mode "pin visible everywhere" escape hatch the
toggle doesn't cover.
### The L3 gate + Phase S coordination contract
The originally-anticipated coordination contract had two branches for how the VST3 editor/embed
shells would pick up the L1 kit: "born in the kit" (built against it from the start) or "L3
restyles them" (built plain, then brought onto the kit). In practice Phase S's editor and embed
shells arrived on `dev` drawing flat `LICE_FillRect` blocks and raw GDI `DrawTextA` off a local
pre-L1 forest-green palette — the "born in the kit" branch did not occur — so L3 performed a full
restyle of both shells through the L1 kit instead, landing the settled-and-revised
grey-neutral-plus-three-accent-pastel treatment (with a pastel spectral keyboard strip as the
signature surface) and routing text through the kit's cached-font `text()`. This resolved the
Phase Q gate condition ("Phase S + L3 merged to dev") and closed out Phase L (L1L7 complete).
The VST3 class UID was left unchanged — a visual restyle is explicitly not a compat event.
### Open questions the source left unresolved
Two items were still open at the point their sections were written and are not known to have
been revisited since: **multi-capture drag over an FX button** (S17) — reject, or load the
first / a keymap of all — and whether the drop zone has to be the **FX button specifically** vs.
anywhere on the target track's TCP. Neither is known to block anything currently planned; noted
here rather than silently dropped.