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

89 KiB
Raw Blame History

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_fileversion_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 #ifdefs 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_CachedFonts, 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.hsampler_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 (BankIndexBankModel; 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.