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.
89 KiB
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 #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 B1–B4): 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: L1–L7 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 S1–S18 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· (frames−start)) 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 Fritsch–Carlson 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 0–127 × 0–1 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 S1–S6 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_edituses param-domain schematic scaling (8px minimum node separation enforced by the forward map inenvelope_overlay), all nodes clamped in-canvas. Previously only a subset was editable. - Gap-free waveform render:
columnMinMaxmoved topeaks;waveformColumnCountmoved tocomponent_geometry; dock panel and VST editor now share one gap-free per-column min/max algorithm viadraw_kit::drawWaveform. - Radial Knob primitive:
param_slidergained aKnobcontrol kind (7→5 o'clock arc, needle, vertical-drag) — foundation for the later Wave B control deck. - Zone-bleed fix 3a:
reconcileSingleCaptureZonesadded tosample_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_coregains user-parameterized voice count (1–32, default 16),VoiceModePoly/Mono (last-note held-note stack,MonoTriggerRetrigger/Legato toggle), an isolatedPreviewCard(dedicated preview voice, never steals from/into the MIDI pool), two-tier panic (CC123 release / CC120 immediate hard-stop incl. Trigger one-shots). Processor sumsPreviewCardalongside 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).ComponentStateenvelope 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::startgained atakeoverDeclickparameter (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.
ChannelModebecomes decode-only (downmix vs dual-mono on WAV decode); channel mode auto-defaults from the loaded capture's channel count via purechannelModeFor()unless the user has explicitly toggled it (explicit toggle latches the preference).ComponentStatev8→v9:channelModeExplicitbool 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.vstpresetfile whose 'Comp' chunk is the serialized component state, applied viaTrackFX_SetPreset. FX hotspot now prefix-matchestcp.fx*/mcp.fx*/fx_*(embed strip tokens excluded).reasampler_uid.hsplit out ofreasampler_vst.has an SDK-free header soinstrument_dropcan derive the class-ID hex string without the VST3 SDK. - Drag-out arm fix:
SetCapturemoved to the drag-arm branch ofhandleClickinbank_panelso 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_shiftnow 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 C1–C8 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.
PreviewCardretired. Preview now injects a synthetic note-on at the loaded capture's root note into the mainVoiceEngine(the same path host MIDI uses), so preview obeys polyphony/mono/voice-stealing/envelopes.sampler_coreno longer has aPreviewCard; 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.
ComponentStatebumped v9→v10 with aSampleRefstable: 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'sSampleRefs. 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
~322–369): 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 1–3 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)
parseIntnarrowing hardening.bank_model.cpp'sparseIntcastsint64_t → intviastatic_castwithout a range check; integers that fit in int64 but exceedINT_MAXare 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 (S1–S18 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 S1–S6 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 0–1 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_shiftif the hand-rolled OLA onset latency or warble proves musically unacceptable; samePitchEngine::Preservecontract. 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_sliderslider 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 (L1–L7); 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 1–4 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 (S10–S13) findings
Daniel DAW-tested the S1–S6 instrument and the verdict was that it worked but the UX was unacceptable — "this is supposed to be better than ReaSamplOMatic5000" (RS5K). The S1–S6 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 (L1–L7 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.