Merge dev into phase-g: Phase Ε/Ρ and the 1.5.0 bump meet Phase Gamma's instrument work; 120/120 green
The per-directory CLAUDE.md count is re-derived at twenty-seven rather than carried from either side. The "Decouple the instrument reload from VST3 activation" TODO entry does not survive: Γ-W3-T1 landed it, and COMPLETED.md carries the discharge.
This commit is contained in:
+299
-4
@@ -854,9 +854,28 @@ own refs table. Pre-existing — `bakeWindowNeedsHold` is only a new *consumer*
|
||||
|
||||
Seven tracks across three waves, code-complete, reviewed, remediated, and integrated on
|
||||
this branch: 89/89 tests passing, a clean build. Phase Ψ came from a direct list of
|
||||
seven defects and refinements (Daniel, 2026-08-01) rather than a backing product doc —
|
||||
see `docs/PLAN.md`'s Phase Ψ section for the Ψ.1–Ψ.7 provenance list this phase traces
|
||||
back to.
|
||||
seven defects and refinements (Daniel, 2026-08-01) rather than a backing product doc, so
|
||||
the list is carried here verbatim rather than by reference, now that `docs/PLAN.md`'s
|
||||
Phase Ψ section (its original home) is retired:
|
||||
|
||||
> **Ψ.1** — item and track captures should be named (labeled) after their source track
|
||||
> name, plus a discriminator (date, etc.); currently they aren't named anything useful.
|
||||
> **Ψ.2** — Design vs. Arrange modes: any SOLO state in one mode is cached and removed
|
||||
> when switching to another mode, disjoining the solo surfaces.
|
||||
> **Ψ.3** — the Design/Arrange active-mode toggle is gated if playback is running; only
|
||||
> allow the switch when the project is not playing.
|
||||
> **Ψ.4** — the action that moves a Media Explorer item to a new ReaSampler on the
|
||||
> selected track is not in the Media Explorer action category, so it cannot be added to
|
||||
> the Media Explorer toolbar; fix this.
|
||||
> **Ψ.5** — drag-and-drop targets are inexact: sometimes dropping into the arrange
|
||||
> doesn't work, sometimes dropping into the FX area doesn't work; dragging between banks
|
||||
> is fine.
|
||||
> **Ψ.6** — capture into mono: if the left and right channels of a new capture are
|
||||
> bit-identical, collapse to mono — one channel of data, mono arrange items, ReaSamplers
|
||||
> load in mono mode.
|
||||
> **Ψ.7** — capture item / capture track with a small time selection on a large item
|
||||
> captures the entire item, not the selection/razor; make capture regions consistent and
|
||||
> correct.
|
||||
|
||||
**Ψ-W1-T1 — `capture-range-exactness`.** A ranged item capture now renders the
|
||||
requested window instead of the whole item, by re-sourcing through the selected-tracks
|
||||
@@ -920,7 +939,7 @@ has been confirmed in a running REAPER. Several rest on a **shared unverified
|
||||
inference** about how REAPER's selected-tracks render source interacts with custom
|
||||
time bounds — and Ψ-W3's refusal now rests on it too, meaning if the inference is
|
||||
wrong that refusal costs a working capture. Each track's DAW-verification obligation
|
||||
is recorded in `docs/PLAN.md`'s Phase Ψ section; `docs/verify-track-scope-multitrack.md`
|
||||
is recorded in `docs/VERIFICATION.md`; `docs/verify-track-scope-multitrack.md`
|
||||
is a new standalone verification script on this branch, for Ψ-W3-T1's multi-track
|
||||
refusal specifically. No human has observed any of these seven behaviors in a DAW.
|
||||
|
||||
@@ -1365,3 +1384,279 @@ formatted string, whether REAPER's MIDI learn actually covers the un-shipped `IM
|
||||
case, the three migration round trips (a pre-parameter project, a save/reopen in an older
|
||||
binary, automation drawn and replayed), whether an offline render replays automation, and
|
||||
whether REAPER restores instance state through `setState` rather than `setComponentState`.
|
||||
|
||||
### Phase Ρ — Render in place: a track's output to a new sibling, source to the bench
|
||||
|
||||
One wave, one track (Ρ-W1-T1 `render-in-place`), code-complete, reviewed, remediated, and
|
||||
merged to `dev` as `b400384`: 91/91 tests passing, a clean build. Phase Ρ came from a
|
||||
direct request (Daniel, 2026-08-02) rather than a backing product doc list — see
|
||||
`docs/product/render-in-place.md` for the framing and its three [Daniel]-class forks
|
||||
(Ρ-F1/F2/F3), all ruled the day the phase was framed.
|
||||
|
||||
**Ρ-W1-T1 — `render-in-place`.** One bindable action, `RENDER_TRACK_IN_PLACE`, renders
|
||||
the selected track's output over the current range to the project's recording path —
|
||||
never the bank — places it as an item on a brand-new sibling track at the exact unsnapped
|
||||
render position, clones the source's colour and its name with an idempotent `Capture `
|
||||
prefix, moves the source track to Design mode, and puts the result track into Arrange
|
||||
unconditionally (the Ρ-F2 ruling). New `src/shell/capture/render_in_place.{h,cpp}`.
|
||||
Extended `core/capture/track_topology` (`siblingPlacement`), `core/capture/capture_name`
|
||||
(`captureTrackName`), `core/capture/capture_paths` (`RenderPaths`/`deriveRenderPaths`,
|
||||
with `deriveBankPaths` re-expressed over it). A `CaptureDestination` enum was added to
|
||||
`CaptureRequest`; `render_bounds_gate` became destination-aware. A filter added to
|
||||
`panel_input::detectNewContent`, one `ActionTableRow` in `src/app/main.cpp`. All four
|
||||
invariant amendments the plan required (`src/shell/capture/CLAUDE.md`,
|
||||
`src/shell/actions/CLAUDE.md`, root `CLAUDE.md` §"The load-bearing principle",
|
||||
`src/core/view/CLAUDE.md`) landed inline with the track.
|
||||
|
||||
**Four deviations worth recording:**
|
||||
|
||||
1. **The `activeModeId` acceptance criterion was met in spirit, not to the letter.** The
|
||||
criterion said `activeModeId()` must appear only in the `applyMode` reapply. The
|
||||
implementer added the spec-recommended one-line Design-fired `ShowConsoleMsg`, which
|
||||
requires reading the active mode, and hoisted that read into a single named local
|
||||
shared by the message condition and the reapply. All three `tag()` calls still take
|
||||
literal mode ids, so the ruling the criterion protects (Ρ-F2) holds. Review accepted
|
||||
this explicitly.
|
||||
2. **The `panel_input` edit was larger than the spec's estimate** — the spec budgeted
|
||||
"two lines only"; the landed change is six lines plus an `<algorithm>` include and
|
||||
dropping a `const`, still confined to `detectNewContent`.
|
||||
3. **`TrackList_AdjustWindows(false)` was included preemptively** where the spec had
|
||||
asked to `[verify — DAW]` whether it is needed. Consequence worth recording: the DAW
|
||||
check can no longer distinguish, so answering that question now requires commenting
|
||||
the call out locally.
|
||||
4. **A behavioural change beyond Ρ's stated scope**, surfaced in review and judged an
|
||||
improvement: a track restored by undo now keeps its original mode instead of being
|
||||
re-tagged to the active mode. Its reach is narrower than it sounds —
|
||||
`ViewModeModel::reconcile` prunes records for GUIDs that have gone away, so a track
|
||||
absent across a reconcile pass still falls back to the old behaviour.
|
||||
|
||||
Both **[propose at review]** items resolved to the plan's own recommendations: the
|
||||
Design-fired console message was added (yes), and no master-track refusal was added
|
||||
(no — `ResolveScopeSource` already refuses a master-only selection).
|
||||
|
||||
**The entire DAW-verification obligation remains outstanding.** The null test on Ρ's
|
||||
own output, the three folder cases, collapsed-mono placement and summing, both mode
|
||||
transitions waited out past a panel timer tick, undo, name/colour clone, and
|
||||
`GetProjectPathEx` against a non-default recording path — none of it is unit-testable
|
||||
and none has been run.
|
||||
|
||||
### The offline-render millisecond floor — located and closed (ad-hoc)
|
||||
|
||||
Closes the `docs/TODO.md` entry of the same name. REAPER's offline render was
|
||||
intermittently refusing an otherwise-valid capture whenever the requested window's end
|
||||
carried a sub-millisecond remainder — breaking root `CLAUDE.md`'s "exact bounds — no
|
||||
rounding of the requested range" precision invariant.
|
||||
|
||||
**Located, not inferred: the floor lives in the `RENDER_BOUNDSFLAG=0` custom-time-bounds
|
||||
field, not in REAPER's render engine.** Switching the offline render to
|
||||
`RENDER_BOUNDSFLAG=2` (the project's own time selection, driven through
|
||||
`GetSet_LoopTimeRange`) escapes it entirely. Confirmed by two live 48 kHz
|
||||
`TailMode::None` DAW renders, both landing exactly 97627 frames against the window's own
|
||||
count: the first started at the on-grid `0s` and tested only the END edge (a floored end
|
||||
would have printed 97584 — 43 frames short); the second, the decisive run, started at
|
||||
`2.0338983050847457s` and ended at `4.0677966101694913s`, both edges off the
|
||||
millisecond grid, and no millisecond-floored model of either edge alone or both together
|
||||
reproduces 97627. No compensation, trimming, or extraction was needed.
|
||||
`src/core/capture/render_settings.h`'s `kRenderBoundsTimeSelection` is now the one
|
||||
narrative home for the mechanism and the measurement; time selection is the only bounds
|
||||
mode the offline render reaches.
|
||||
|
||||
**Two hypotheses this track's originating entry previously carried are disproven, not
|
||||
merely superseded** — both predicted a shortfall tracking the render's CONTENT: that the
|
||||
render bounds itself to the media it can see, and that a trailing-silence trim fires
|
||||
despite `RENDER_NORMALIZE`. The measured cause tracks the WINDOW instead — the exact
|
||||
millisecond-floored count, independent of what the material does. Recorded so neither is
|
||||
re-proposed without a fresh observation.
|
||||
|
||||
**Scaffolding removed.** The experiment's apparatus — a two-position
|
||||
`RenderBoundsChannel` type, a console verdict line, and a three-checkpoint
|
||||
`RENDER_STARTPOS`/`RENDER_ENDPOS` read-back probe — is deleted now that the mechanism is
|
||||
settled.
|
||||
|
||||
**Left open, filed to `docs/TODO.md`:** `renderHonoredBounds`'s one-frame tolerance
|
||||
remains empirical, not proven; and `TailMode::Auto`/`Manual` have no automatic bounds
|
||||
observation at all — `render_bounds_gate` judges `TailMode::None` only, so both modes are
|
||||
fixed by inference (same bounds path, same floor) rather than by measurement, and only
|
||||
the 0-byte gate covers them until a DAW check closes it.
|
||||
|
||||
### Ε-W1 — The contract, the filesystem, and the ledger's new kind
|
||||
|
||||
Phase Ε's first wave: the `.rsbank` package contract, the filesystem/dialog seam
|
||||
behind it, and a new tracking-ledger origin kind for package-sourced files — three
|
||||
tracks, disjoint by directory, dispatched in parallel.
|
||||
|
||||
**Ε-W1-T1 — `package-format`.** The pure `src/core/package/` codec for the
|
||||
hand-rolled `RSBK` container (Ε-F1, ruled — no ZIP, no compressor, no link edge to
|
||||
`vendor/WDL/WDL/zlib/`): a fixed little-endian header carrying two version
|
||||
integers — `formatVersion` (what the writer emitted) and `minReaderVersion` (the
|
||||
oldest reader that can read it safely) — a length-prefixed JSON manifest, and
|
||||
payloads concatenated in manifest order. `classifyPackageVersion` answers
|
||||
`Readable`/`TooNew`/`Malformed`; a `TooNew` header refuses whole, producing no
|
||||
manifest, so the refusal can still name the writer's semver rather than
|
||||
half-succeeding. Landed as three modules: `package_format` (the contract, the
|
||||
version ladder, and three name-validation rules — `isValidEntryName`,
|
||||
`sameEntryName`'s ASCII-case fold, `isValidNestedSamplePath`), `package_manifest`
|
||||
(the manifest model + JSON codec, carrying the bank's `slot_map` and a whole-file
|
||||
`hashBytes` digest per entry — deliberately not `hashWavContent`, which skips
|
||||
chunks and so cannot answer "did these bytes survive"), and `bank_package`
|
||||
(framing/layout arithmetic: `encodePackage`/`decodePackage`/`requiredPrefixSize`,
|
||||
never holding or hashing a payload itself). Hostile input is refused, never UB,
|
||||
at every byte offset.
|
||||
|
||||
**Ε-W1-T2 — `package-fs-shell`.** `src/shell/package/`: streaming, atomic package
|
||||
filesystem I/O (`package_io`'s `PackageFileWriter`/`PackageFileReader`, at most one
|
||||
entry's payload materialized at a time, backed by a `.rsbanktmp` sibling that
|
||||
reaches the destination only through a `commit()` rename — process-crash atomic,
|
||||
not power-loss atomic, deliberately, since an `fsync` over a whole sample bank is a
|
||||
real stall) and the rollback journal (`package_rollback`'s `LandedFileJournal`,
|
||||
citing the `prune_fs.cpp` carve-out rather than restating it, disarmed only after
|
||||
the caller's own write has returned success). `package_pickers` rides REAPER's own
|
||||
`GetUserFileName` for both directions, as specified (mode 1 import, mode 0 export)
|
||||
— the plan's "REAPER has no save picker" finding was a regex miss in the original
|
||||
research, not a real gap, so there was no asymmetric-picker deviation to land: no
|
||||
SWELL `BrowseForSaveFile`, no Win32 `GetSaveFileNameW`, no `GetUserFileNameForRead`
|
||||
(the SDK header marks it superseded). REAPER owning the dialog on every platform is
|
||||
why there's no platform split; that's separate from `main.cpp` already aborting
|
||||
extension load if any needed API pointer fails to resolve, which is why no fallback
|
||||
path is needed. Both pickers are `[verify — DAW]`, never exercised in a live REAPER
|
||||
session.
|
||||
|
||||
**Ε-W1-T3 — `import-origin-kind`.** `OriginKind::PackageImport` appended to the
|
||||
tracking ledger as value 5 — package-sourced vs `Ingest`'s user-picked. Append-only,
|
||||
per `core/tracking/CLAUDE.md`'s persisted-integer rule; an unrecognized kind
|
||||
degrades to `Unknown` rather than failing the parse, and `kLedgerVersion` stays at
|
||||
2 — a vocabulary addition, not a document-version bump. No decision surface
|
||||
changed: `pruneProtection`'s output is unaffected for every existing kind.
|
||||
|
||||
### Ε-W2 — The two verbs
|
||||
|
||||
Two tracks landed on Ε-W1's contract: a bank leaves the project as one `.rsbank`
|
||||
file, or the export refuses and says why; a `.rsbank` becomes a **new** bank,
|
||||
completely or not at all. Both tracks were code-reviewed and remediated before
|
||||
merging; the merged tree (Ε-W1 + Ε-W2) builds clean and passes 100/100 tests.
|
||||
|
||||
**Ε-W2-T1 — `bank-export`.** New `core/package/export_plan` (pure: which entries,
|
||||
what names, what is missing, and therefore whether the export may proceed — verdict
|
||||
`Ready`/`Incomplete`/`Refused`) and `shell/package/export_bank` (the promptless
|
||||
verb, in three composable public steps — `surveyBankExport`, `digestSources`,
|
||||
`writePackageFile` — arriving with a **const** `ReaSamplerSession&`, so "writes no
|
||||
ext state, opens no undo point, never bumps the generation" holds by the type
|
||||
rather than by memory), plus `shell/actions/package_export_action`, one
|
||||
`main.cpp` action-table row, and one panel bank-menu row. Nothing is re-encoded;
|
||||
payloads are copied and hashed. The exported unit is one bank — the pool included,
|
||||
since the pool is structurally one `BankIndex` among many — and whole-book export
|
||||
stays out of scope for the phase. Both open questions were answered at review:
|
||||
affordance ships as **both** the bindable action and the panel row, and the
|
||||
default file name derives from the bank's display name through
|
||||
`capture_paths::sanitizeStem`.
|
||||
|
||||
**Ε-W2-T2 — `bank-import`.** New `core/package/import_plan` (pure: the id remap
|
||||
table, the parent remap, the per-entry land/skip-already-present/rename
|
||||
disposition, and the destination bank's display name after `BankBook`'s own
|
||||
uniqueness fold — reached through a new additive `BankBook::uniqueDisplayName`
|
||||
member, the only `core/model/` edit in the phase), and on the shell side a
|
||||
REAPER-free `import_landing` (decode, verify every payload's `hashBytes` digest
|
||||
against the manifest BEFORE the bank folder is created, then land through the
|
||||
rollback journal) plus a REAPER-facing `import_bank` (the only piece touching the
|
||||
extension's project state — the undo-batched persist and the generation bump),
|
||||
`shell/actions/package_import_action`, the panel's `.rsbank` drop route, one
|
||||
`main.cpp` row, one panel menu row, and a new `src/core/util/ascii_ws.h`. The
|
||||
tracking-ledger guard runs before the file picker opens (Ε-F3, ruled: refuse
|
||||
outright on `Unreadable`/`FutureVersion`, no confirm-and-proceed); the version
|
||||
gate runs before any byte is written; all four collision classes — sample id, file
|
||||
name, content hash, bank display name — are answered explicitly, with the
|
||||
display-name collision auto-suffixed and never prompted (Ε-F2, ruled: always a new
|
||||
bank, never a merge); birth records land via
|
||||
`recordCreated(sample, OriginKind::PackageImport)` in the same straight-line block
|
||||
as the index add; the index mutation is one Ctrl-Z, and the landed files'
|
||||
survival as orphans until the next prune is stated in the user-facing summary, not
|
||||
left implicit. **Beyond spec:** `import_plan`'s `spelledLikeABankFile` mints a
|
||||
fresh name even absent a collision, whenever the package's own entry name isn't
|
||||
spelled the way `deriveBankPaths` would spell it — counted separately from a
|
||||
genuine folder-name collision (`sanitizeRenameCount` vs `collisionRenameCount`) so
|
||||
a hostile or foreign-spelled entry name (e.g. an unexpected extension) always
|
||||
lands sanitized rather than verbatim.
|
||||
|
||||
### Ε-W3 — The compatibility fixtures
|
||||
|
||||
The phase's third and final wave, and with it Phase Ε's implementation is complete: the
|
||||
version-compatibility policy stated in `docs/product/bank-package.md` is now a property
|
||||
proven against frozen bytes rather than an assertion in a doc.
|
||||
|
||||
**Ε-W3-T1 — `package-compat-fixtures`.** A new checked-in corpus of 23 frozen `.rsbank`
|
||||
fixtures under `tests/fixtures/package_compat/` — one v1 package written by the shipping
|
||||
build (`1.4.0`), a synthetic additive-forward package (`formatVersion` 2 /
|
||||
`minReaderVersion` 1) carrying three keys this build has never heard of, a synthetic
|
||||
structural-refusal package (2/2), nine truncations (one per distinct decode failure
|
||||
site, including one cut at `additive_forward.rsbank`'s own payload boundary), and eleven
|
||||
hostile-name packages (six bad entry names, five bad nested `relativePath` values) —
|
||||
every payload a single 300-byte 16-bit mono WAV, ~15 KB for the whole corpus. Two new
|
||||
test targets decode and exercise it: `package_compat_tests` (frozen bytes decode to
|
||||
exactly what the shipping build wrote, the additive fixture reads with every unknown key
|
||||
skipped, every truncation classifies `Malformed` and never `TooNew`, every hostile name
|
||||
is refused before any planner runs) and `package_round_trip_tests` (the same corpus
|
||||
driven through the actual verbs — export → import → export over `v1_shipping.rsbank`
|
||||
yields byte-identical payloads, and every refusal fixture refuses the whole import with
|
||||
nothing landed and nothing in the index). A new repo-root `.gitattributes` (`*.rsbank
|
||||
binary`) is load-bearing, not decoration: under `core.autocrlf = true`, git's NUL-sniffing
|
||||
heuristic would text-classify a future short, ASCII-heavy fixture and CRLF-mangle it on a
|
||||
Windows checkout, silently breaking the frozen-bytes premise the whole corpus rests on. A
|
||||
standalone DAW verification script, `docs/verify-package-transfer.md`, covers the one
|
||||
claim no unit test can make — a real cross-machine transfer, including the too-new
|
||||
refusal, the truncated-download refusal, and mid-payload corruption, each read off as an
|
||||
exact message string. **Open question resolved:** the recommendation (one-sample
|
||||
packages, a few hundred bytes of payload each) was followed — the corpus holds
|
||||
one-sample packages with a 300-byte payload each. **Deviation from spec:** the plan
|
||||
called for a truncation cut mid-layout; RSBK stores no layout section (the layout is
|
||||
derived from the manifest's entries, not stored as its own section), so the fixture that
|
||||
exercises "the manifest parses, the layout computes, the exact-size proof fails" lands at
|
||||
the payload boundary instead. No production module was touched — the wave adds test-tree
|
||||
files, the corpus, its README, the verification script, and one path variable in the root
|
||||
`CMakeLists.txt`.
|
||||
|
||||
### Resample-bake mono collapse — closes the `docs/TODO.md` deferral (ad-hoc)
|
||||
|
||||
`prepareLanding` (`src/shell/capture/bake_landing.cpp`) now applies the shared lossless
|
||||
mono collapse to the staged buffer — via a new thin wrapper `applyMonoCollapse` in
|
||||
`src/core/capture/wav_codec.cpp` — before the hash and the channel-count read, so the
|
||||
hash, the entry, and the written file all come from one collapsed buffer. A dead-center
|
||||
(dual-mono) bake now lands as a 1-channel file exactly as a dead-center offline capture
|
||||
already does; a true-stereo bake is byte-identical to before, asserted on bytes and on
|
||||
hash.
|
||||
|
||||
**The blocker this deferral originally cited has cleared.** `bake_land.cpp` was
|
||||
another team's freshly-landed remediation surface at the time; that remediation has
|
||||
since landed, which is what made taking this item this wave safe.
|
||||
|
||||
**Consequences accepted, not avoided:**
|
||||
- A dead-center bake's **content identity moves** — the hash now covers the collapsed
|
||||
bytes, so a dual-mono bake will not hash-dedup against a stereo twin already in the
|
||||
bank, and its derived file name changes. This was already documented as accepted for
|
||||
the other capture paths in `src/core/capture/CLAUDE.md`; the bake path now inherits
|
||||
it rather than being an exception.
|
||||
- `BakeOutcome::channelCount` now answers 1 for a dead-center bake, which flips the
|
||||
instrument's channel-mode auto-default to Mono. Safe: the audio is identical either
|
||||
way when the source was dead-center, and the consuming site was already written
|
||||
anticipating that value.
|
||||
|
||||
### Design View FX-GUID keying for `restoreFxOffline` — closes the `docs/TODO.md` deferral (ad-hoc)
|
||||
|
||||
`restoreFxOffline` (`src/shell/view/view.cpp`) now returns each parked track's per-FX
|
||||
offline state to the plugin it was captured from, keyed by the FX's own GUID
|
||||
(`TrackFX_GetFXGUID`) rather than its slot index. New pure module
|
||||
`src/core/view/fx_offline.{h,cpp}` holds the keying types (`FxKeying`: Identity/Slot),
|
||||
the per-FX snapshot/plan types, and `resolveFxRestore`, which matches each captured
|
||||
state against the chain as it stands at restore time. `view_state` gained a v2 schema
|
||||
that writes the identity array beside the v1 slot array, so an older build reading a
|
||||
v2 blob keeps the behaviour it had rather than losing every FX state.
|
||||
|
||||
**Consequences accepted, not avoided:**
|
||||
- A dropped FX (one whose captured identity is no longer live in the chain at restore
|
||||
time) is left **offline**, as park left it, with its snapshot already cleared — the
|
||||
console report names the drop and the recovery.
|
||||
- The console report uses the quiet `!SHOW:` form on every path, so it never
|
||||
force-opens the console window.
|
||||
|
||||
**Left open, filed to `docs/TODO.md`:** FX-GUID stability itself — whether
|
||||
`TrackFX_GetFXGUID` survives a chain reorder while parked — is unverified in the DAW
|
||||
(SWS issue #802 names a specific way it might not hold).
|
||||
|
||||
+32
-1527
File diff suppressed because it is too large
Load Diff
+171
-87
@@ -110,20 +110,6 @@ Forward-looking follow-ups. Deferred by decision, not oversight — each entry r
|
||||
|
||||
**Done looks like.** Not stated in PLAN.md.
|
||||
|
||||
## FX-GUID keying for `restoreFxOffline` (Design View park/restore)
|
||||
|
||||
**Context.** CONTEXT.md's "Open questions to resolve during build" (Design View section): the bulk of reconcile residuals shipped (`ViewModeModel::reconcile(liveGuids)` prunes orphaned snapshots on every toggle/load; folder restructure is self-healing because the tree is rebuilt each toggle; membership is intentionally kept so undo-delete preserves the tag). Two sub-items were left deferred out of that; this is the first.
|
||||
|
||||
**The wart.** `restoreFxOffline` currently restores per-FX offline state by slot index. If the FX chain is reshuffled while a track is parked, restore lands on whatever plugin now occupies that slot rather than the plugin it was originally captured from.
|
||||
|
||||
**Intended fix.** FX-GUID keying — key the per-FX offline snapshot entries by FX identity rather than slot index.
|
||||
|
||||
**The constraint the fix MUST handle.** The keying change requires a snapshot-schema migration; CONTEXT.md names this alongside the keying change as the reason the fix was deferred rather than folded into the reconcile-residuals work.
|
||||
|
||||
**Priority / risk.** Not stated in the source.
|
||||
|
||||
**Done looks like.** Not stated in the source beyond the fix description above.
|
||||
|
||||
## Dormant membership entries in persisted `view_state`
|
||||
|
||||
**Context.** CONTEXT.md's "Open questions to resolve during build" (Design View section), the second of the two sub-items left deferred after the reconcile-residuals ship described above.
|
||||
@@ -318,6 +304,15 @@ buffer.
|
||||
shared with every other caller in `core/capture/wav_codec`; a fix must not change
|
||||
those callers' contract or add a second WAV-building code path to maintain.
|
||||
|
||||
**Re-confirmed still accurate (2026-08-02), after the mono-collapse landing touched
|
||||
`wav_codec` adjacent to this site.** `applyMonoCollapse` operates on the staged bytes in
|
||||
`bake_landing.cpp`'s `prepareLanding`, upstream of and unrelated to `runBake`'s
|
||||
`std::vector<double>` copy in `instrument_bake.cpp`; `buildFloat32Wav`'s signature is
|
||||
unchanged. The wart stands exactly as described above.
|
||||
|
||||
**Current blocker.** Not taken this wave because `instrument_bake.cpp` is being edited
|
||||
by a live VST3-parameter track.
|
||||
|
||||
**Priority / risk.** Low / deferred. Logged at Ξ-W2-T1's review; correctness is
|
||||
unaffected, only peak memory on a large bake.
|
||||
|
||||
@@ -507,7 +502,7 @@ needs no live REAPER process to exercise `rec->Register(...)` calls. Once
|
||||
**The constraint the fix MUST handle.** The extraction alone buys nothing:
|
||||
`action_registry` has no test target today either, so lifting `ingestHandleSectionCommand`
|
||||
into it without also standing up the test target just relocates the untested code. The
|
||||
same follow-up could collapse `ingest.cpp:466-472`'s hand-rolled `command_id`+`gaccel`
|
||||
same follow-up could collapse `ingest.cpp`'s `ingestRegisterActions` hand-rolled `command_id`+`gaccel`
|
||||
pair onto `action_registry::registerAction`, which already does exactly that dance for
|
||||
the Q-W6 table.
|
||||
|
||||
@@ -542,7 +537,7 @@ track", nothing to do and the inference is retired into fact. If it comes back "
|
||||
summed file", the refusal is over-strict for the TRACK scope and should be narrowed back
|
||||
— and the ITEM-scope half is then an OPEN question, not settled: a full-extent item
|
||||
capture already sums a multi-track item selection via `&32|single-file`
|
||||
(`tests/test_render_settings.cpp:262`), so if `&128` also sums, a ranged item capture
|
||||
(`test_render_settings.cpp`'s `testMultiTrackStemRenderIsNamedForRefusal`), so if `&128` also sums, a ranged item capture
|
||||
routed through it sums too, and keeping the item refusal in that branch would make item
|
||||
scope inconsistent with itself across the range boundary (full-extent sums, ranged
|
||||
refuses, same scope). Whether that inconsistency is acceptable or the item refusal should
|
||||
@@ -622,44 +617,6 @@ select/move the neighbour, or capture at track scope instead.
|
||||
non-isolation as an oversight and re-propose closing it against the recipe's stated
|
||||
tracks-and-range-only shape.
|
||||
|
||||
## Resample-bake landings don't apply the lossless mono collapse to a dual-mono render
|
||||
|
||||
**Context (surfaced by Ψ-W2-T2, mono-collapse).** The collapse (`collapseCapturedFileToMono`
|
||||
/ `core/capture/wav_codec::collapseToMono`) ships for every extension capture path —
|
||||
offline, realtime, batch, recapture — but not for `bake_land.cpp`'s landing, the
|
||||
resample bake's `prepareLanding` / `commitLanding` pair. A dead-center instrument render (the common case
|
||||
that motivated Ψ.6 in the first place) is exactly the dual-mono shape the predicate
|
||||
collapses, so an un-collapsed bake keeps paying for the second channel it doesn't need.
|
||||
|
||||
**Not deferred for the reason once given.** `prepareLanding` reads the staged file into
|
||||
`prep.bytes` once, parses its layout, hashes it and derives the channel count from that
|
||||
same one buffer, and `commitLanding` writes that buffer — so collapsing it right after the
|
||||
layout parse would keep the hash, the channel count, and the written file consistent by
|
||||
construction; there is no ordering hazard here to defer around.
|
||||
|
||||
**The real reason.** `bake_land.cpp` is Phase Ξ's freshly-landed surface
|
||||
(Ξ-W2-T1, the resample bake chain) and another team is actively remediating it. Landing
|
||||
a mutation there now would cross tracks mid-remediation for no urgent gain — the mono
|
||||
propagation this item would add is a size win, not a correctness one.
|
||||
|
||||
**A mono capture already propagates through the bake for free**, so this item is scoped
|
||||
to the dual-mono-*render* case only: `runBake` / `instrument_bake.cpp` already renders
|
||||
however many channels the dialed sound has, and `bake_render.cpp:38` reads
|
||||
`sample.channelCount()` off that render rather than hardcoding 2 — a mono-programmed
|
||||
sound already bakes to a mono file today, with no change needed.
|
||||
|
||||
**Intended fix.** Once `bake_land.cpp` is quiet, call `collapseToMono` on `prep.bytes` in
|
||||
`prepareLanding` right after the layout parse and before the hash, matching the
|
||||
offline/realtime insertion point (post-parse, pre-identity-read).
|
||||
|
||||
**Priority / risk.** Low — a size optimization on an already-correct path, not a
|
||||
precision-invariant gap; the bake's dual-mono case still lands as a valid (if larger)
|
||||
stereo file today.
|
||||
|
||||
**Done looks like.** A dead-center instrument bake lands as a 1-channel file with
|
||||
`Sample::channelCount` matching, the same way an offline dead-center capture does; a
|
||||
true-stereo bake is byte-identical to today's output.
|
||||
|
||||
## A 0-byte render can still pass every gate under Auto/Manual tail (closed)
|
||||
|
||||
**Context (surfaced by Ψ-W3 review).** `OfflineRenderBackend::capture`'s exists-check
|
||||
@@ -682,37 +639,106 @@ refusal reuses `CaptureStatus::BoundsMismatch` rather than minting its own statu
|
||||
earlier note here preferred a distinct status, and that preference is unresolved, not
|
||||
withdrawn.
|
||||
|
||||
## An offline capture can be refused for a short render — root cause open
|
||||
## `renderHonoredBounds`'s one-frame tolerance is empirical, not proven
|
||||
|
||||
**Symptom (live, 2026-08-02).** A capture over [0.000000s, 4.067797s) at 48 kHz was
|
||||
refused: `Render produced 195216 frames but the requested range is 195254`. 38 frames
|
||||
short — 38x the gate's one-frame tolerance, so the tolerance is not what refused it.
|
||||
**Context.** The millisecond-floor defect that motivated this gate is closed
|
||||
(`docs/COMPLETED.md`), but the gate itself — `render_window.h`'s
|
||||
`renderHonoredBounds` — carries a one-frame tolerance that carried through the fix
|
||||
unchanged and was never itself proven.
|
||||
|
||||
**Hypothesis A — the render bounds itself to the media it can see.** REAPER's
|
||||
selected-items render source (`&32`) derives its bounds from the selected items' own
|
||||
extents (`src/core/capture/CLAUDE.md` §Gotchas — itself an inference from an observed
|
||||
defect, not a header fact). If a time-bounded selected-tracks render (`&128`) does the
|
||||
same thing against content extent, a range running past the end of its material comes up
|
||||
exactly as short as the material is.
|
||||
**The wart.** A renderer that resolves the window's two edges by DIFFERENT
|
||||
conventions can sit two frames from `frameCountFor`'s answer on a
|
||||
correctly-honored render. That cannot account for the 8- and 38-frame shortfalls
|
||||
the floor produced (`docs/COMPLETED.md`), so it was not the cause of those
|
||||
refusals — but it means a future one- or two-frame refusal may be the gate's own
|
||||
edge convention rather than a real defect.
|
||||
|
||||
**Hypothesis B — a trailing-silence trim fires anyway.** `TailMode::None` sets
|
||||
`RENDER_NORMALIZE = &(4<<16)` (disable all postprocessing) and `RENDER_TRIMEND = 0`. If
|
||||
REAPER trims regardless of that bit, a range whose material decays before its end loses
|
||||
exactly the decayed frames.
|
||||
**Intended fix.** Not proposed. Widening the tolerance is a precision-invariant
|
||||
decision, not a bug fix, and was deliberately not taken on speculation.
|
||||
|
||||
**Not excluded — the gate itself.** `renderHonoredBounds`' one-frame tolerance is
|
||||
empirical, not proven (`src/core/capture/render_window.h`): a renderer that resolves the
|
||||
window's two edges by DIFFERENT conventions can sit two frames from `frameCountFor`'s
|
||||
answer on a correctly-honored render. That cannot account for 38 frames, so it is not
|
||||
this refusal — but it means a future one- or two-frame refusal may be ours, which is why
|
||||
the tolerance was not widened on speculation. Widening it is a precision-invariant
|
||||
decision, not a bug fix.
|
||||
**Priority / risk.** Low. Nothing to date implicates the tolerance itself;
|
||||
recorded so a future narrow refusal is investigated rather than assumed to be
|
||||
the same floor.
|
||||
|
||||
**How it gets decided.** `docs/VERIFICATION.md` §Capture range and bounds, the three
|
||||
numbered blocker steps: step 1 separates A's `&32` path from the shared `&128` path (and
|
||||
says how to tell when it failed to), step 2 asks whether the render is short at all, step
|
||||
3 reads the retained refused render to place the missing frames. Nothing here should be
|
||||
"fixed" before that comes back.
|
||||
**Done looks like.** Either the tolerance is confirmed correct by a DAW
|
||||
observation that isolates edge-convention behavior from bounds-floor behavior,
|
||||
or it is widened with the reasoning recorded.
|
||||
|
||||
## `TailMode::Auto` and `Manual` have no automatic bounds observation
|
||||
|
||||
**Context.** `render_bounds_gate.cpp`'s `checkRenderedBounds` returns early for
|
||||
anything but `TailMode::None`, so the millisecond-floor fix (`docs/COMPLETED.md`)
|
||||
was measured only against `TailMode::None` — Auto and Manual were never
|
||||
observed, before the fix or after it.
|
||||
|
||||
**The wart.** The inference that Auto/Manual are fixed too is sound — same
|
||||
bounds path, same floor, same fix — but it is an inference, not a measurement.
|
||||
`checkRenderedFileNotEmpty` runs on every tail mode and still catches a 0-byte
|
||||
render, but that is the ONLY automatic bounds signal Auto/Manual get; a
|
||||
floored or otherwise short-but-nonzero render under either mode would land as
|
||||
`Ok` with nothing to catch it.
|
||||
|
||||
**Intended fix.** Not a code change — a DAW observation. `docs/VERIFICATION.md`'s
|
||||
"Capture range and bounds" section already carries the manual check: repeat an
|
||||
off-grid-start capture at Manual over a source loud to the window's end and
|
||||
check the landed frame count against window + `tailMs`; Auto can't be checked
|
||||
by count (it trims trailing silence) and needs the null test by ear/inversion
|
||||
instead.
|
||||
|
||||
**Priority / risk.** Low. Both modes share the same bounds path as the
|
||||
now-fixed `TailMode::None`, so nothing suggests they still floor — but nothing
|
||||
confirms it either.
|
||||
|
||||
**Done looks like.** A DAW-observed Auto and Manual capture, each landing the
|
||||
window as requested, closes the inference into fact — or surfaces a
|
||||
mode-specific divergence this entry does not currently know about.
|
||||
|
||||
## Floor, ceil and round are not the identity on a millisecond grid point in binary double (caution, not an open question)
|
||||
|
||||
A discarded compensation design for the millisecond-floor defect
|
||||
(`docs/COMPLETED.md`) rested on the premise that a grid-aligned value survives a
|
||||
bare floor/ceil/round unchanged. That is false in binary double: `1.007 * 1000
|
||||
== 1006.9999999999999` (floors to 1006, not 1007), and `4.068 * 1000 ==
|
||||
4067.9999999999995` (floors to 4067, not 4068). The compensation this premise
|
||||
would have supported is no longer needed — the fix moved the render to a bounds
|
||||
mode that does not floor at all — so this is not a live open question. Recorded
|
||||
because it would bite any future millisecond-grid arithmetic that assumes an
|
||||
on-grid value is safe from a bare floor: `render_window.h`'s own
|
||||
`isOnMillisecondGrid`/`msFlooredEndFrameCount` already carry the nanosecond
|
||||
tolerance that handles it correctly on this codebase's side of the boundary; the
|
||||
trap is for whoever writes the next piece of grid arithmetic without that guard.
|
||||
|
||||
## `capture.cpp` is over the ~600-line ceiling — the seam is identified, taking it is blocked
|
||||
|
||||
**Context.** Removing the settled bounds experiment's instrumentation (the console
|
||||
verdict and the three-checkpoint `RENDER_STARTPOS`/`ENDPOS` read-back) brought the file
|
||||
from 697 to **620 measured lines**, against root `CLAUDE.md`'s ~600-line ceiling. The
|
||||
seam that entry originally named is gone with the instrumentation; nothing left in the
|
||||
file is bisectable without cutting load-bearing why.
|
||||
|
||||
**The remaining seam is a real responsibility boundary**, and the file header already
|
||||
names it as two things: `OfflineRenderBackend::capture` (the offline render driver)
|
||||
versus the four helpers BOTH backends share — `makeUniqueTag`, `captureNameFor`,
|
||||
`collapseCapturedFileToMono`, `stampCaptureSample` — consumed by `capture_batch`,
|
||||
`capture_orchestrator`, `capture_realtime_shell`, `capture_realtime_finalize` and
|
||||
`render_in_place`. Lifting those four into their own TU takes the driver under the
|
||||
ceiling and gives the cross-backend steps their own home.
|
||||
|
||||
**Why not taken.** `src/shell/capture/` has no `CMakeLists.txt` of its own — its sources
|
||||
are listed in `src/app/CMakeLists.txt`, so a new TU needs an edit there. Forcing the
|
||||
four helpers into an existing TU instead (orchestrator, realtime finalize) would put
|
||||
them in a wrong home to dodge one build-file line, which is worse than the overshoot.
|
||||
|
||||
## bext TimeReference read-back is not a floor detector (dead end, recorded so it is not re-litigated)
|
||||
|
||||
Idea considered and dropped: read a captured file's `BWF:TimeReference` tag back as
|
||||
independent evidence on the START-edge millisecond-floor question above. `WDL/metadata.h`'s
|
||||
`WriteMetadataPrefPos` only writes it past its `prefpos > 0.0` guard (`:1301`) — that guard
|
||||
alone is enough to rule the approach out. One nuance worth recording separately: the
|
||||
millisecond quantization at `:1382-1383` (`AddMexMetadata`'s `ParseUInt64(val)/1000.0`)
|
||||
belongs to the MEX caller, not proven to be `WriteMetadataPrefPos`'s own behavior or the
|
||||
renderer's direct call into it — so even without the guard, a floored bext tag would show
|
||||
that MEX quantizes, not that the render engine does.
|
||||
|
||||
## Split `render_bounds_gate` on the verdict/message vs. filesystem seam
|
||||
|
||||
@@ -744,8 +770,8 @@ the two callers' plumbing.
|
||||
|
||||
**Context.** `saveToActiveProject()` returns false for exactly two reasons — no active
|
||||
project, or an unsaved one — and in both cases NOTHING was written. Four capture sites
|
||||
discard that return outright: `capture_orchestrator.cpp:343`, `capture_batch.cpp:266` and
|
||||
`:333`, and `realtime_lifecycle.cpp:39`.
|
||||
discard that return outright: `capture_orchestrator.cpp`'s `RunCapture`, `capture_batch.cpp`'s
|
||||
`RunBatchCaptureItems` and `RunBatchCaptureRazor`, and `realtime_lifecycle.cpp`'s `CommitRealtimeResult`.
|
||||
|
||||
**The wart.** A capture on an unsaved project renders the file into the bank folder, adds
|
||||
the `Sample` to the in-memory book, records a birth record in memory — and loses all three
|
||||
@@ -766,7 +792,7 @@ and the choice between those two is recorded rather than implicit.
|
||||
|
||||
## `panel_input`'s wheel handler persists the whole book per wheel message
|
||||
|
||||
**Context.** `panel_input.cpp:450` — `handleWheel` calls `markTailDirty()` on every wheel
|
||||
**Context.** `panel_input.cpp` — `handleWheel` calls `markTailDirty()` on every wheel
|
||||
message that actually moves `manualMs`, while the pointer is over the footer in Manual
|
||||
mode. (It coalesces sub-notch deltas within ONE message and no-ops at a bound, so the
|
||||
count is wheel messages that changed the value, not raw notches.)
|
||||
@@ -785,10 +811,10 @@ the value that lands is the gesture's final one.
|
||||
|
||||
## `RunCaptureItemAssign`'s undo point does not follow the pattern its comment claims
|
||||
|
||||
**Context.** `capture_orchestrator.cpp:364-365` states that the action follows the bank-op
|
||||
family's discard-on-unsaved pattern.
|
||||
**Context.** `capture_orchestrator.cpp`'s `RunCaptureItemAssign` states that the action follows
|
||||
the bank-op family's discard-on-unsaved pattern.
|
||||
|
||||
**The wart.** It does not: `:382-383` records the undo point unconditionally whenever
|
||||
**The wart.** It does not: `RunCaptureItemAssign` records the undo point unconditionally whenever
|
||||
`sampleId` is non-empty, and never consults the persist's return at all. So on an unsaved
|
||||
project it records an undo point for ext-state that was never written — the empty
|
||||
no-effect entry `persistBankOp`'s guardrail exists to avoid. The comment describes the
|
||||
@@ -803,7 +829,8 @@ the unsaved-project case one way.
|
||||
|
||||
## `core/tracking/CLAUDE.md`'s untracked-file enumeration says "reaches the `.rpp`" too loosely
|
||||
|
||||
**Context.** `src/core/tracking/CLAUDE.md:24-31` enumerates how a created file can stay
|
||||
**Context.** `src/core/tracking/CLAUDE.md` §"Invariants" — "No silent gaps — in memory at
|
||||
creation, on disk at the next save" — enumerates how a created file can stay
|
||||
untracked, and describes the ledger as reaching the `.rpp` at the following
|
||||
`saveToActiveProject()`.
|
||||
|
||||
@@ -837,3 +864,60 @@ in the `.rpp`", and does not gain a second home for the distinction.
|
||||
**Priority / risk.** Low. Nothing here is load-bearing on the frozen contract: the id table, the plain ranges and the norm↔plain laws are all decided and tested without a host.
|
||||
|
||||
**Done looks like.** Each of the four exercised once in REAPER, with the unit-rendering answer recorded and, if it went the other way, the one-line formatter change made.
|
||||
|
||||
## `view_mode_model.cpp` is over the ~600-line structural bar, and `view.cpp` is close behind
|
||||
|
||||
**Context (surfaced by the FX-GUID keying track).** Root `CLAUDE.md`'s structural
|
||||
heuristics put an ~600-line ceiling on any one file, with a documented responsibility
|
||||
seam as the required method for splitting it, not an arbitrary bisection.
|
||||
`src/core/view/view_mode_model.cpp` measures **815 lines** (verified this pass),
|
||||
up from 715 before the FX-GUID keying track's v2 schema addition made it worse.
|
||||
|
||||
**The named seam.** The JSON codec — `serialize()`/`deserialize()` — wants its own
|
||||
`view_state_codec` TU in `src/core/view/`.
|
||||
|
||||
**Why it was deferred, and this reasoning should survive.** `serialize()` is a
|
||||
`ViewModeModel` member and `deserialize()` a static factory (confirmed:
|
||||
`std::string ViewModeModel::serialize() const` and
|
||||
`std::optional<ViewModeModel> ViewModeModel::deserialize(const std::string&)`), both
|
||||
reaching private state — so extraction needs either a friend declaration or a new
|
||||
public accessor surface. Doing that in the same commit that changed the byte format
|
||||
the golden test literals pin would roll a format change and a codec extraction
|
||||
together, which is the riskier order.
|
||||
|
||||
**Also over the bar, blocked differently.** `src/shell/view/view.cpp` measures
|
||||
**642 lines** (verified this pass). Its seam is blocked not by a private-state/friend
|
||||
question but by a build file another team owns: `src/shell/view/` has no
|
||||
`CMakeLists.txt` of its own today.
|
||||
|
||||
**Priority / risk.** Not stated.
|
||||
|
||||
**Done looks like.** `view_mode_model.cpp`'s JSON codec is extracted into its own
|
||||
`view_state_codec` TU (with the friend/accessor question resolved deliberately, not
|
||||
sidestepped), dropping the file under the ~600-line ceiling; `view.cpp`'s own path is
|
||||
unblocked once the build-file ownership question is resolved.
|
||||
|
||||
## FX-GUID stability for `restoreFxOffline` is unverified in the DAW
|
||||
|
||||
**Context.** The Design View park/restore FX keying (`restoreFxOffline`,
|
||||
`src/shell/view/view.cpp`) rests on `TrackFX_GetFXGUID` returning an identity that
|
||||
survives a chain reorder while a track is parked. SWS issue #802 reports that after
|
||||
`SNM_MoveOrRemoveTrackFX` reorders a chain, the FXID lines do not follow the plugin
|
||||
(`SNM_PreObjectState()` → `RemoveAllIds()`) — if that still holds, an SWS-driven
|
||||
reorder while parked produces wrong-plugin restores or mass drops, which is the exact
|
||||
operation this keying targets.
|
||||
|
||||
**What must be checked.** Native drag-reorder, an SWS move, save/reload, and two live
|
||||
instances of the same plugin.
|
||||
|
||||
**Already flagged in code — this entry is the tracked home, not a restatement.**
|
||||
There is a `[verify — DAW]` marker at `fxGuidString` in `src/shell/view/view.cpp` and
|
||||
a note in `src/shell/view/CLAUDE.md`'s Gotchas; point at them rather than restating
|
||||
them in full.
|
||||
|
||||
**Priority / risk.** Not stated.
|
||||
|
||||
**Done looks like.** Native reorder, SWS reorder, save/reload, and a
|
||||
two-instance-of-the-same-plugin case are each observed in a live REAPER session, and
|
||||
either the identity is confirmed to survive all four, or a degradation is found and
|
||||
the keying is amended.
|
||||
|
||||
+146
-53
@@ -1,83 +1,176 @@
|
||||
# DAW verification — post-1.0 work on `dev`
|
||||
|
||||
Checks for Θ, Ξ, and Ψ work that no unit test can close. Build **Release**, install into
|
||||
Checks for Θ, Ξ, Ψ, Ε, and Ρ work that no unit test can close. Build **Release**, install into
|
||||
`UserPlugins/`, restart REAPER. Panel tail toggle = **None**, project rate 48000, unless a check says otherwise.
|
||||
|
||||
## Precision invariants
|
||||
|
||||
- [ ] Dry offline item capture of a 2 s range, re-inserted at its source position, inverted against the source — reads silence (`CLAUDE.md:207`)
|
||||
- [ ] Run the identical offline capture request twice — the two files are byte-identical on disk (`CLAUDE.md:208`)
|
||||
- [ ] After any capture, source items and tracks are unchanged: fader, pan, mute, FX bypass, selection (`CLAUDE.md:209`)
|
||||
- [ ] After a realtime capture, the temp track is gone and every source track's routing is back as it was (`CLAUDE.md:209`)
|
||||
- [ ] Capture 10.000–12.000 s — card reads 2.000 s / 96000 frames, no leading or trailing silence (`CLAUDE.md:210`)
|
||||
- [ ] With an FX on the source track: item scope does NOT carry it, track scope does (`CLAUDE.md:212`)
|
||||
- [ ] Track scope on a child track with FX, gain, and pan set on the parent and master — neither colors the capture (`CLAUDE.md:212`)
|
||||
- [ ] Save, move the whole project folder elsewhere, reopen — every card still resolves and auditions (`CLAUDE.md:211`)
|
||||
- [ ] Dry offline item capture of a 2 s range, re-inserted at its source position, inverted against the source — reads silence (`CLAUDE.md` §"Precision invariants" — "Null test")
|
||||
- [ ] Run the identical offline capture request twice — the two files are byte-identical on disk (`CLAUDE.md` §"Precision invariants" — "Bit-identical repeats")
|
||||
- [ ] After any capture, source items and tracks are unchanged: fader, pan, mute, FX bypass, selection (`CLAUDE.md` §"Precision invariants" — "Non-destructive")
|
||||
- [ ] After a realtime capture, the temp track is gone and every source track's routing is back as it was (`CLAUDE.md` §"Precision invariants" — "Non-destructive")
|
||||
- [ ] Capture 10.000–12.000 s — card reads 2.000 s / 96000 frames, no leading or trailing silence (`CLAUDE.md` §"Precision invariants" — "Exact bounds")
|
||||
- [ ] With an FX on the source track: item scope does NOT carry it, track scope does (`CLAUDE.md` §"Precision invariants" — "Capture FX scope")
|
||||
- [ ] Track scope on a child track with FX, gain, and pan set on the parent and master — neither colors the capture (`CLAUDE.md` §"Precision invariants" — "Capture FX scope")
|
||||
- [ ] Save, move the whole project folder elsewhere, reopen — every card still resolves and auditions (`CLAUDE.md` §"Precision invariants" — "Relative paths only")
|
||||
|
||||
## The decisive observation
|
||||
|
||||
- [ ] **Run first.** `docs/verify-track-scope-multitrack.md` §3 by hand, and count the files REAPER writes (`docs/TODO.md:552`, `PLAN.md:2134`, `PLAN.md:2238`)
|
||||
- [ ] Two files confirms Ψ-W1-T1 and Ψ-W3-T1 at once; **one summed file invalidates both** — stop and report, the refusal is costing a capture 1.0.0 accepted (`PLAN.md:2239`, `docs/COMPLETED.md:884`)
|
||||
- [ ] Then walk the rest of `docs/verify-track-scope-multitrack.md` (§1–§2, §4–§7) for the multi-track refusal itself (`PLAN.md:2240`)
|
||||
- [ ] **Run first.** `docs/verify-track-scope-multitrack.md` §3 by hand, and count the files REAPER writes (`docs/TODO.md` §"The `&128` multi-track output shape is still DAW-unobserved", `docs/COMPLETED.md` §"Ψ-W3-T1", `docs/COMPLETED.md` §"None of the seven is DAW-verified")
|
||||
- [ ] Two files confirms Ψ-W1-T1 and Ψ-W3-T1 at once; **one summed file invalidates both** — stop and report, the refusal is costing a capture 1.0.0 accepted (`docs/COMPLETED.md` §"None of the seven is DAW-verified")
|
||||
- [ ] Then walk the rest of `docs/verify-track-scope-multitrack.md` (§1–§2, §4–§7) for the multi-track refusal itself (`docs/COMPLETED.md` §"Ψ-W3-T1")
|
||||
|
||||
## Capture range and bounds
|
||||
|
||||
- [ ] Over an item much longer than the selection: item scope × time selection, and item scope × razor — each lands exactly the window, not the whole item (`PLAN.md:2136`)
|
||||
- [ ] Same source: track scope × time selection, and track scope × razor — same exact window (`PLAN.md:2136`)
|
||||
- [ ] One razor-union case (two disjoint areas, one track) — lands the requested window, no `ReaSampler capture failed:` line (`PLAN.md:2137`)
|
||||
- [ ] Capture an item whose extent already equals the window — still lands, unchanged (the byte-identity regression floor) (`docs/COMPLETED.md:829`)
|
||||
- [ ] **Open blocker — root cause unknown; the three steps below are the experiment** (both live hypotheses and what is NOT yet excluded: `docs/TODO.md` §An offline capture can be refused for a short render). A live capture over [0.000000s, 4.067797s) was refused 38 frames short (195216 of 195254 at 48 kHz). Set View → time unit to Samples first
|
||||
- [ ] **Step 1 — does the shortfall follow the render source?** This only tests anything if item scope actually reaches REAPER's selected-items render, and it does that ONLY when the selected items' extent already equals the requested window (`itemExtentPrintsWindow`, `src/core/capture/render_window.h`); otherwise item scope re-sources through the items' own tracks — the same source track scope uses, so the two runs would test one thing twice. So: snap the time selection to the item's exact start and end, run **item** scope, then **track** scope over the identical range. Report both `Render source:` lines and both frame counts. **If both lines read `selected tracks via master`, the item path was NOT exercised** — the extents did not match; re-snap and repeat before concluding anything. **A landing `&32` run here is not evidence `&32` honours custom bounds** — at a window snapped to the item's own extent, a render that honours the window and one that bounds itself to media content print IDENTICAL frames, so this step cannot tell those two apart; it only tells you which render source is in play. **If neither run refuses at this snapped range, the blocker did not reproduce here** — this range does not recreate the original refusal, which ran past the end of its media; move to step 2, which does
|
||||
- [ ] **Step 2 — full-length or short?** Extend the same range ~1 s past the end of all media, **track** scope. Landing with the full range (no refusal, the card reads the extended length) rules out BOTH a trailing-silence trim and a content-extent bound at once — but it also means there is no refused render for step 3 to read; re-run the ORIGINAL refusing range ([0.000000s, 4.067797s), track scope) to produce one before continuing. A short render does NOT tell the two hypotheses apart: a trim firing despite `RENDER_NORMALIZE &(4<<16)` and a render bounding itself to content extent produce the same count — and that render IS the one step 3 reads. Report which happened, then run step 3
|
||||
- [ ] **Step 3 — where are the missing frames?** Reads the short render from step 2 (or, if step 2 landed, the fresh refused render from re-running the original range per step 2's note) — not anything step 1 may have left behind, since a correctly-snapped step 1 should not have refused at all. A refused render is kept deliberately, not deleted: it is moved to `<project folder>/reasampler_refused/`. **Follow the path in the refusal line, not this sentence** — if the move itself failed the file stays in the bank folder, unindexed, and the line says which happened. Filenames carry a timestamp/counter but no scope marker, so if more than one file has landed in `reasampler_refused/` by now, the one from step 2 is the most recently written one — or empty the folder before running step 2 so there is only one candidate. Insert it against the source over the same range and report whether the head aligns. Frames missing from the TAIL with an aligned head fits either a tail trim or a content-extent bound; a head offset fits neither and is a start-position defect. Also report whether the media under the range ends before the range does. Delete `reasampler_refused/` when done — nothing in the bank references it
|
||||
- [ ] Over an item much longer than the selection: item scope × time selection, and item scope × razor — each lands exactly the window, not the whole item (`docs/COMPLETED.md` §"Ψ-W1-T1")
|
||||
- [ ] Same source: track scope × time selection, and track scope × razor — same exact window (`docs/COMPLETED.md` §"Ψ-W1-T1")
|
||||
- [ ] One razor-union case (two disjoint areas, one track) — lands the requested window, no `ReaSampler capture failed:` line (`docs/COMPLETED.md` §"Ψ-W1-T1")
|
||||
- [ ] Capture an item whose extent already equals the window — still lands, unchanged (the byte-identity regression floor) (`docs/COMPLETED.md` §"Ψ-W1-T1")
|
||||
- [ ] **The millisecond floor — SETTLED, nothing to re-run for `TailMode::None`.** The floor lives in the custom-time-bounds field (`RENDER_BOUNDSFLAG=0`), not in the render engine. Two live 48 kHz `TailMode::None` renders on `RENDER_BOUNDSFLAG=2` (time selection, handed over via `GetSet_LoopTimeRange`) came back exact — 97627 frames against 97627 — the second over a window whose START carried a sub-millisecond remainder, with no floored model of that window able to reproduce the count. Time selection is now the only bounds mode a capture can reach; the console verdict line and the `RENDER_STARTPOS`/`ENDPOS` read-back probe that answered this are gone. Full observation: `src/core/capture/render_settings.h`'s `kRenderBoundsTimeSelection`
|
||||
- [ ] **Still open — Auto and Manual tail.** `checkRenderedBounds` judges `TailMode::None` only (Auto/Manual add frames by design), so the settled result covers those two by INFERENCE, not observation, and the inference rests on an unverified PREMISE too: that the (retired) floor applied to the bounds identically across all three tail modes, and that all three now hand the window over the same way. Neither is measured — both live short renders that settled the bounds mode were `TailMode::None`; no Auto or Manual capture has been observed at all. **On Auto/Manual, the ONLY automatic check left is the 0-byte gate (`checkRenderedFileNotEmpty`)** — there is no automatic bounds signal for those two modes at all until this bullet is closed by hand. What would establish it: repeat an off-grid-start capture at **Manual** over a source that is loud right to the window's end, and check the landed file's frames against window + `tailMs` — a floored edge shows up in that count. **Auto** cannot be checked by count (it trims trailing silence), so it needs the null test by ear/inversion against the source instead
|
||||
- [ ] `[verify — DAW]` A tail is assumed to render PAST the window end — the SDK header (`:3048`) confirms only that `RENDER_TAILMS` is a length in ms, not that it extends past the end. If that assumption is wrong, a tail capture is silently SHORTER than its window with no detector at all. Report whether either tail capture comes up short against the source
|
||||
- [ ] A refused render is kept for diagnosis at `<project folder>/reasampler_refused/` (the refusal line names the path; a failed move leaves it unindexed in the bank folder and says so). Delete the folder when done — nothing in the bank references it
|
||||
- [ ] **If a capture is refused for a short render**, report the refusal line verbatim. A message naming `floored to the millisecond` means the floor is back on a mode measured escaping it; a shortfall of one or two frames with no such sentence may be the gate's own edge-convention tolerance rather than the render (`render_window.h`'s `renderHonoredBounds`)
|
||||
|
||||
## Names and channels
|
||||
|
||||
- [ ] Capture from a named track — the card reads `<Track> MM-DD HHMM`; capture again the same minute and the second carries an ordinal (`PLAN.md:2199`)
|
||||
- [ ] Capture from an unnamed track, and from a multi-item selection — both readable, `+N` present on the multi (`PLAN.md:2199`, `docs/COMPLETED.md:862`)
|
||||
- [ ] Load a named capture into ReaSampler 9000 — the same name shows there (`PLAN.md:2200`)
|
||||
- [ ] The card label stays legible over its scrim at every card size (`docs/COMPLETED.md:863`)
|
||||
- [ ] Capture a dead-center mono source — the `.wav` is roughly half the size of the equivalent stereo capture (`PLAN.md:2216`)
|
||||
- [ ] Insert that collapsed file on a stereo track and null it against the source — confirms REAPER sums a 1-channel item at unity (`PLAN.md:2218`)
|
||||
- [ ] Capture a true-stereo source — stays 2-channel, and both it and the collapsed file load into the instrument correctly (`PLAN.md:2217`)
|
||||
- [ ] Capture from a named track — the card reads `<Track> MM-DD HHMM`; capture again the same minute and the second carries an ordinal (`docs/COMPLETED.md` §"Ψ-W2-T1")
|
||||
- [ ] Capture from an unnamed track, and from a multi-item selection — both readable, `+N` present on the multi (`docs/COMPLETED.md` §"Ψ-W2-T1")
|
||||
- [ ] Load a named capture into ReaSampler 9000 — the same name shows there (`docs/COMPLETED.md` §"Ψ-W2-T1")
|
||||
- [ ] The card label stays legible over its scrim at every card size (`docs/COMPLETED.md` §"Ψ-W2-T1")
|
||||
- [ ] Capture a dead-center mono source — the `.wav` is roughly half the size of the equivalent stereo capture (`docs/COMPLETED.md` §"Ψ-W2-T2")
|
||||
- [ ] Insert that collapsed file on a stereo track and null it against the source — confirms REAPER sums a 1-channel item at unity (`docs/COMPLETED.md` §"Ψ-W2-T2")
|
||||
- [ ] Capture a true-stereo source — stays 2-channel, and both it and the collapsed file load into the instrument correctly (`docs/COMPLETED.md` §"Ψ-W2-T2")
|
||||
|
||||
## Mode switching
|
||||
|
||||
- [ ] Solo tracks in Arrange, switch to Design, solo different tracks, switch back — each mode restores its own solo set verbatim (`PLAN.md:2151`)
|
||||
- [ ] Attempt a mode switch while the transport is playing, then while recording — both refuse, visibly (`PLAN.md:2151`)
|
||||
- [ ] Click the footer mode segment, save, reopen the project — the mode persisted (`PLAN.md:2152`, `docs/COMPLETED.md:841`)
|
||||
- [ ] Solo tracks in Arrange, switch to Design, solo different tracks, switch back — each mode restores its own solo set verbatim (`docs/COMPLETED.md` §"Ψ-W1-T2")
|
||||
- [ ] Attempt a mode switch while the transport is playing, then while recording — both refuse, visibly (`docs/COMPLETED.md` §"Ψ-W1-T2")
|
||||
- [ ] Click the footer mode segment, save, reopen the project — the mode persisted (`docs/COMPLETED.md` §"Ψ-W1-T2")
|
||||
|
||||
## Actions and drops
|
||||
|
||||
- [ ] Add the import action to a Media Explorer toolbar and fire it from there — it imports (`PLAN.md:2162`)
|
||||
- [ ] Fire the existing Main-section import binding — still works (`PLAN.md:2163`)
|
||||
- [ ] Unload/reload (restart REAPER) — no duplicate Media Explorer entry in the action list (`PLAN.md:2164`)
|
||||
- [ ] Drag one card across the arrange, over an FX window, over the TCP/MCP and back — cue changes per surface, every transition reverses (`PLAN.md:2176`, `PLAN.md:2177`)
|
||||
- [ ] Drag fast, and drag onto a narrow TCP — target class still resolves; no release anywhere in REAPER is a silent no-op (`PLAN.md:2178`, `docs/COMPLETED.md:858`)
|
||||
- [ ] Drop a single card into the arrange — an item lands at the pointer's track and time (`PLAN.md:2174`)
|
||||
- [ ] Drag-out to an external app twenty-plus times in a row — audio arrives every time; this is a soak, a single pass is not a gate (`docs/COMPLETED.md:109`)
|
||||
- [ ] Drop a capture onto an FX container — the instrument loads with that capture (`docs/COMPLETED.md:110`)
|
||||
- [ ] Add the import action to a Media Explorer toolbar and fire it from there — it imports (`docs/COMPLETED.md` §"Ψ-W1-T3")
|
||||
- [ ] Fire the existing Main-section import binding — still works (`docs/COMPLETED.md` §"Ψ-W1-T3")
|
||||
- [ ] Unload/reload (restart REAPER) — no duplicate Media Explorer entry in the action list (`docs/COMPLETED.md` §"Ψ-W1-T3")
|
||||
- [ ] Drag one card across the arrange, over an FX window, over the TCP/MCP and back — cue changes per surface, every transition reverses (`docs/COMPLETED.md` §"Ψ-W1-T4")
|
||||
- [ ] Drag fast, and drag onto a narrow TCP — target class still resolves; no release anywhere in REAPER is a silent no-op (`docs/COMPLETED.md` §"Ψ-W1-T4")
|
||||
- [ ] Drop a single card into the arrange — an item lands at the pointer's track and time (`docs/COMPLETED.md` §"Ψ-W1-T4")
|
||||
- [ ] Drag-out to an external app twenty-plus times in a row — audio arrives every time; this is a soak, a single pass is not a gate (`docs/COMPLETED.md` §"Θ-W1-T2" — "Neither acceptance criterion has actually been met yet")
|
||||
- [ ] Drop a capture onto an FX container — the instrument loads with that capture (`docs/COMPLETED.md` §"Θ-W1-T2" — "Neither acceptance criterion has actually been met yet")
|
||||
|
||||
## Bank packages
|
||||
|
||||
- [ ] **Run in full.** `docs/verify-package-transfer.md` — the whole cross-machine
|
||||
export/import round trip: writes-one-file, the transfer itself, re-importing the
|
||||
same file never overwrites, the round trip back to the source, the too-new /
|
||||
truncated / mid-payload-corruption refusals (each an exact string), the
|
||||
unsaved-project refusals, and drag-and-drop (`docs/COMPLETED.md` §"Ε-W3-T1")
|
||||
- [ ] Force a degraded tracking ledger and confirm the import refuses **before the
|
||||
file picker opens**: save a project with a bank, close REAPER, edit the saved
|
||||
`.rpp`'s `owned_files` ext-state value inside its `<REASAMPLER ...>` block — corrupt
|
||||
the JSON for the `Unreadable` case, or bump `"v":2` to `"v":3` for the
|
||||
`FutureVersion` case — reopen the project, then run *ReaSampler: import bank
|
||||
package (.rsbank)*. Read off: the console prints the ledger-refusal block and no
|
||||
file dialog ever appears (`origin_ledger.h`'s `LedgerStatus` and `ledgerDegraded`,
|
||||
`package_import_action.cpp`'s `ledgerPermits`)
|
||||
- [ ] Export dialog: type a destination name with no extension, then again over a
|
||||
name that already carries a different one (e.g. `mybank.bak`) — read off whether
|
||||
`GetUserFileName` appended `.rsbank` itself or ReaSampler's own re-append produced
|
||||
the double-extension result (`mybank.bak.rsbank`) the code expects
|
||||
(`src/shell/package/CLAUDE.md` §"Gotchas" — "The re-append is suffix-blind")
|
||||
- [ ] Both the export and the import file dialogs open in front of REAPER's main
|
||||
window, not behind it — `GetUserFileName` takes no owner window
|
||||
(`src/shell/package/CLAUDE.md` §"Gotchas" — "`GetUserFileName` also takes no owner window")
|
||||
- [ ] With a ReaSampler 9000 instance's editor open on the destination project
|
||||
(Browse view visible), import a `.rsbank` from the docked panel — the browser
|
||||
reflects the new bank without closing or reopening the editor (the bank-generation
|
||||
bump, `session.h`'s `bumpBankGeneration`, polled by the instrument at
|
||||
`processor_reload.cpp`'s `pollBankSync`)
|
||||
- [ ] Drag two or more `.rsbank` files onto the docked panel in one drop — each lands
|
||||
as its OWN new bank, never merged into one, and if the tracking ledger is degraded
|
||||
the refusal prints ONCE for the whole drop rather than once per file
|
||||
(`panel_window.cpp`'s `handleDropFiles`)
|
||||
- [ ] Kill REAPER (or the process) partway through an import so a partial bank file
|
||||
is stranded under its real name in the bank folder, then re-run the same import
|
||||
into the same project — read off what happens. Whether the import verb should
|
||||
pre-clean that stale debris is an open question, not yet decided
|
||||
(`src/shell/package/CLAUDE.md` §"Gotchas" — "A crash mid-export strands the `.rsbanktmp` sibling")
|
||||
|
||||
## The resample bake
|
||||
|
||||
- [ ] Bake a dialed sound — the banked file sounds like what the editor was playing (`docs/COMPLETED.md:737`)
|
||||
- [ ] Bake the result twice more — iteration composes, nothing is lost per pass (`docs/COMPLETED.md:737`)
|
||||
- [ ] Save and reopen after a bake — the instance still points at the baked capture (`docs/COMPLETED.md:737`)
|
||||
- [ ] Confirm no bake put an item in the arrange, and the superseded file is still on disk (`docs/COMPLETED.md:737`)
|
||||
- [ ] Bake from an instance in a background project tab — refuses rather than writing into the wrong bank (`docs/COMPLETED.md:725`)
|
||||
- [ ] Load the VST with the extension not installed — the resample affordance reads unavailable, not silently lossy (`docs/COMPLETED.md:731`)
|
||||
- [ ] After a bake: instance is in Trigger with start point reset, channel mode and preview velocity survived (`docs/COMPLETED.md:714`, `docs/COMPLETED.md:720`)
|
||||
- [ ] Gate mode + active sustain loop — "Bake Hold" appears within ~500 ms, its label fits its cell, its travel is duration-ordered (`docs/COMPLETED.md:806`)
|
||||
- [ ] Bake a dialed sound — the banked file sounds like what the editor was playing (`docs/COMPLETED.md` §"Ξ-W2-T1" — "Not demonstrated")
|
||||
- [ ] Bake the result twice more — iteration composes, nothing is lost per pass (`docs/COMPLETED.md` §"Ξ-W2-T1" — "Not demonstrated")
|
||||
- [ ] Save and reopen after a bake — the instance still points at the baked capture (`docs/COMPLETED.md` §"Ξ-W2-T1" — "Not demonstrated")
|
||||
- [ ] Confirm no bake put an item in the arrange, and the superseded file is still on disk (`docs/COMPLETED.md` §"Ξ-W2-T1" — "Not demonstrated")
|
||||
- [ ] Bake from an instance in a background project tab — refuses rather than writing into the wrong bank (`docs/COMPLETED.md` §"Ξ-W2-T1" — "Two behaviors worth recording")
|
||||
- [ ] Load the VST with the extension not installed — the resample affordance reads unavailable, not silently lossy (`docs/COMPLETED.md` §"Ξ-W2-T1" — "Two behaviors worth recording")
|
||||
- [ ] After a bake: instance is in Trigger with start point reset, channel mode and preview velocity survived (`docs/COMPLETED.md` §"Ξ-W2-T1" — "Reset-scope classifications made at review")
|
||||
- [ ] Gate mode + active sustain loop — "Bake Hold" appears within ~500 ms, its label fits its cell, its travel is duration-ordered (`docs/COMPLETED.md` §"Ξ-W3-T1")
|
||||
|
||||
## Render in place
|
||||
|
||||
- [ ] Fire *Render track in place* over a track with a range selected — solo the source
|
||||
and the new sibling track, invert one track's polarity, and confirm silence. This is
|
||||
Ρ's own trust anchor: the placement is the null test performed automatically
|
||||
(`docs/product/render-in-place.md` §"DAW-verification obligations",
|
||||
`docs/COMPLETED.md` §"Phase Ρ — Render in place")
|
||||
- [ ] Render in place from three source positions in turn — a normal mid-folder track, a
|
||||
track that is last in its folder, and a folder-parent track — each time confirm the new
|
||||
sibling track lands at the same nesting level as the source and that the folder bus
|
||||
feeds (or bypasses) it correctly. Then, to settle whether `TrackList_AdjustWindows(false)`
|
||||
is actually needed: comment out that call in `render_in_place.cpp`'s
|
||||
`RunRenderTrackInPlace` (it was added preemptively, answering a question the spec had
|
||||
left open rather than one the code confirmed), rebuild, and repeat the folder-parent
|
||||
case — if nesting still displays correctly with the call removed, it can be dropped in a
|
||||
follow-up (`docs/product/render-in-place.md` §"DAW-verification obligations")
|
||||
- [ ] Render in place from a dead-centre (channel-identical) source — confirm the placed
|
||||
item is mono, and confirm it plays back at the same perceived level the stereo source
|
||||
did before the render. This is root `CLAUDE.md`'s existing mono-summing
|
||||
`[verify — DAW]`, promoted to load-bearing because Ρ is the first path that places a
|
||||
collapsed render into the mix automatically (`docs/product/render-in-place.md`
|
||||
§"DAW-verification obligations")
|
||||
- [ ] Fire Render track in place once from Arrange and once from Design. From Arrange:
|
||||
confirm the source track parks and the result track is visible and in the mix. From
|
||||
Design: confirm the source stays on the bench, the result track is parked too, then
|
||||
switch to Arrange and confirm the result track appears in the source's place. **In
|
||||
both cases wait out at least one panel timer tick before checking membership** — that
|
||||
is the check that catches a missing explicit-tag-wins filter or an untagged item,
|
||||
either of which silently reverses the ruling that the result track is always an
|
||||
Arrange member (`docs/product/render-in-place.md` §"Mode transitions — the source
|
||||
parks, the result goes to Arrange")
|
||||
- [ ] Render in place, then press Ctrl-Z once — confirm the new track and its item are
|
||||
both gone, the source track's folder depth is restored, the rendered file itself is
|
||||
still on disk, and the source track is still tagged Design (`docs/product/render-in-place.md`
|
||||
§"DAW-verification obligations")
|
||||
- [ ] Render in place from a named source, then run it again over the resulting (already
|
||||
`Capture `-prefixed) track — confirm the name does not stack a second prefix, and
|
||||
confirm the new sibling's colour matches the source's (the colour clone has no unit
|
||||
coverage at all). Repeat once from an unnamed source and confirm the result reads
|
||||
`Capture Track N` (`docs/product/render-in-place.md` §"DAW-verification obligations";
|
||||
the name-composition logic itself — apart from the live colour clone and the real
|
||||
`GetTrackName`/`P_NAME` round trip — is unit-tested in `tests/test_capture_name.cpp`)
|
||||
- [ ] Save a project into a folder whose recording path is set away from the default
|
||||
(Project Settings → Media → Path), then Render track in place — confirm the rendered
|
||||
file lands in that configured recording path, not the project folder itself
|
||||
(`render_in_place.cpp`'s `RunRenderTrackInPlace`, `GetProjectPathEx`)
|
||||
- [ ] Render in place, save the project, and reopen it — confirm the result track (which
|
||||
carries an explicit `kArrangeModeId` membership record, unlike the shipped
|
||||
tag-selected-tracks action which never writes one) behaves identically, in every
|
||||
mode-switch and visibility check, to an ordinary untagged Arrange track. The JSON
|
||||
round-trip itself is unit-tested (`tests/test_view_mode_model.cpp`); this is the
|
||||
live-view half that isn't (`docs/product/render-in-place.md` §"Mode transitions — the
|
||||
source parks, the result goes to Arrange")
|
||||
|
||||
## Instrument migration
|
||||
|
||||
- [ ] Open a project saved before the zone retirement — the instance reopens on its first zone and sounds the same (`docs/COMPLETED.md:85`)
|
||||
- [ ] Such an instance with implicit channel mode + a stereo capture reopens **Stereo** — confirm that is acceptable by ear (`docs/COMPLETED.md:78`)
|
||||
- [ ] Open a project saved before the zone retirement — the instance reopens on its first zone and sounds the same (`docs/COMPLETED.md` §"Θ-W1-T1")
|
||||
- [ ] Such an instance with implicit channel mode + a stereo capture reopens **Stereo** — confirm that is acceptable by ear (`docs/COMPLETED.md` §"Θ-W1-T1")
|
||||
|
||||
## Look and feel
|
||||
|
||||
- [ ] Sign off by eye in a live editor window: knob arcs, needles, envelope splines, waveform outline (`docs/COMPLETED.md:669`, `PLAN.md:375`)
|
||||
- [ ] Same pass for legibility: text sizes, arc weight, and whether the waveform stroke thickens the docked panel (`docs/COMPLETED.md:613`, `PLAN.md:348`)
|
||||
- [ ] Piano strip at the 840 px default — keys tile uniformly, the 37 px end gutters read as acceptable (`docs/COMPLETED.md:219`)
|
||||
- [ ] Resize the editor across several widths — gutters stay symmetric, no key width jumps (`docs/COMPLETED.md:217`)
|
||||
- [ ] Set host/OS scaling to 150% then 200% — record how the strip and the AA strokes actually look (`docs/TODO.md:418`, `docs/COMPLETED.md:226`)
|
||||
- [ ] Sign off by eye in a live editor window: knob arcs, needles, envelope splines, waveform outline (`docs/COMPLETED.md` §"Θ-W7-T1", `docs/COMPLETED.md` §"Θ-W6-T1" — "Antialiasing pass")
|
||||
- [ ] Same pass for legibility: text sizes, arc weight, and whether the waveform stroke thickens the docked panel (`docs/COMPLETED.md` §"Θ-W6-T1", `docs/COMPLETED.md` §"Θ-W6-T1" — "Sizing")
|
||||
- [ ] Piano strip at the 840 px default — keys tile uniformly, the 37 px end gutters read as acceptable (`docs/COMPLETED.md` §"Θ-W2-T3")
|
||||
- [ ] Resize the editor across several widths — gutters stay symmetric, no key width jumps (`docs/COMPLETED.md` §"Θ-W2-T3")
|
||||
- [ ] Set host/OS scaling to 150% then 200% — record how the strip and the AA strokes actually look (`docs/TODO.md` §"High-DPI host scaling is unverified (distinct from the antialiasing audit)", `docs/COMPLETED.md` §"Θ-W2-T3" — "Width uniformity is guaranteed in client pixels only")
|
||||
|
||||
@@ -22,9 +22,9 @@ stated; contradict it in review with an argument, not a preference.
|
||||
|
||||
**A bank package is one file that carries one bank — its audio and its index —
|
||||
out of a project and into another.** Today a bank is per-project by construction:
|
||||
the audio sits in `<projectDir>/reasampler_bank/` (`core/capture/capture_paths.h:16`,
|
||||
the audio sits in `<projectDir>/reasampler_bank/` (`core/capture/capture_paths.h`'s
|
||||
`kBankSubfolder`) and the index that gives that audio meaning lives in the `.rpp`'s
|
||||
project ext state under the `"reasampler"` namespace (`src/ext_keys.h:25`,
|
||||
project ext state under the `"reasampler"` namespace (`src/ext_keys.h`'s
|
||||
`kProjExtBanksKey`). The two travel together with the project and nowhere else.
|
||||
Export writes both halves into a single `.rsbank` file; import lands them into
|
||||
another project's bank folder and index.
|
||||
@@ -116,7 +116,7 @@ cases, and still no compression.
|
||||
**The two costs, accepted with the ruling.** (1) The package is **opaque without our
|
||||
tool** — no unzip-and-look support path. (2) We own the hostile-input hardening of our
|
||||
own parser, to the discipline `bank_model::deserialize` and `parseLedger` already
|
||||
carry — *error signaled, never UB* (`bank_model.h:204-206`). Both are priced in; a
|
||||
carry — *error signaled, never UB* (`bank_model.h`'s `BankModel::deserialize`). Both are priced in; a
|
||||
later "let's make it inspectable" impulse is a new phase's argument, not this one's.
|
||||
|
||||
**This was a one-way door and it is now shut** — packages are in users' hands the day
|
||||
@@ -133,28 +133,29 @@ moves from here.
|
||||
The repo already carries **two** versioning mechanisms, and they answer different
|
||||
questions:
|
||||
|
||||
1. **A blob-schema ladder.** `src/core/tracking/origin_ledger.cpp:8-30` states the
|
||||
ladder for the `owned_files` blob in a header comment (v1 legacy path-only, v2
|
||||
1. **A blob-schema ladder.** `src/core/tracking/origin_ledger.cpp`'s version-ladder
|
||||
header comment states the
|
||||
ladder for the `owned_files` blob (v1 legacy path-only, v2
|
||||
current), pins `constexpr int kLedgerVersion = 2`, and — the load-bearing part —
|
||||
**reads and validates `"v"`, not merely writes it**: "A version above
|
||||
`kLedgerVersion` is therefore its own degraded status, never a Loaded ledger"
|
||||
(`:20-21`). The parse outcome is a three-way `Ok` / `Malformed` / `FutureVersion`
|
||||
(`:171`, `:185`), deliberately distinguished so the operator gets the right
|
||||
(the same comment). The parse outcome is a three-way `Ok` / `Malformed` / `FutureVersion`
|
||||
(`origin_ledger.cpp`'s `ParseOutcome` enum and `parseStored`), deliberately distinguished so the operator gets the right
|
||||
recovery advice. A *field-vocabulary* gap behaves oppositely: an unrecognized
|
||||
`OriginKind` integer degrades to `Unknown` rather than failing the parse
|
||||
(`:32-40`), because "a vocabulary gap must not halt the prune"
|
||||
(`src/core/tracking/CLAUDE.md:82-86`).
|
||||
2. **An app writing-version stamp.** `src/core/version/app_version.h:165-186` —
|
||||
(`origin_ledger.cpp`'s `kindFromInt`), because "a vocabulary gap must not halt the prune"
|
||||
(`src/core/tracking/CLAUDE.md` §"Gotchas").
|
||||
2. **An app writing-version stamp.** `src/core/version/app_version.h`'s
|
||||
`WritingVersion` with `PreVersioning` / `Unknown` / `Stamped`, classified by
|
||||
`classifyWritingVersion`, stamped into project ext state by
|
||||
`src/shell/persist/ext_state_io.cpp:172-176` using `stampVersion()` (the numeric
|
||||
`ReaSamplerSession::saveToActiveProject` (`src/shell/persist/ext_state_io.cpp`) using `stampVersion()` (the numeric
|
||||
triple only, no channel suffix). It is informational: an absent stamp is "not an
|
||||
error and not a warning" (`app_version.h:166-168`).
|
||||
error and not a warning" (`app_version.h`'s `WritingVersion` comment, the `PreVersioning` case).
|
||||
|
||||
**An observation worth recording, not a defect to fix here:** `BankBook` writes
|
||||
`"version": 1` into the banks blob (`src/core/model/bank_book_json.cpp:37`) but its
|
||||
`"version": 1` into the banks blob (`src/core/model/bank_book_json.cpp`'s `BankBook::serialize`) but its
|
||||
parser skips the key along with every other unknown one
|
||||
(`bank_book_json.cpp:182` — `if (!r.skipValue()) return false; // version, or unknown`).
|
||||
(`bank_book_json.cpp`'s `parseBook` — `if (!r.skipValue()) return false; // version, or unknown`).
|
||||
The book's version field is therefore **decorative today** — written, never read,
|
||||
never gating. The ledger's is the precedent to extend; the book's is the precedent
|
||||
not to repeat.
|
||||
@@ -175,10 +176,10 @@ advise (an integer tells a user nothing about which build to install).
|
||||
|
||||
A single ladder has one bad property: **every change strands every older reader,
|
||||
even a purely additive one.** That is not hypothetical here — look at what `Sample`
|
||||
has actually accumulated: `rootNote` and `loop` (`bank_model.h:112-122`,
|
||||
has actually accumulated: `rootNote` and `loop` (`bank_model.h`'s `Sample::rootNote` / `Sample::loop`,
|
||||
"additive like `provenance`. Both default cleanly empty"), `captureTimeSigNum` /
|
||||
`captureTimeSigDenom` (`:103-108`, "0/0 means UNSTAMPED"), `channelCount`
|
||||
(`:91-96`, "0 = unknown — a pre-field entry"). Every one of those was additive with
|
||||
`captureTimeSigDenom` (`Sample::captureTimeSigNum` / `Sample::captureTimeSigDenom`, "0/0 means UNSTAMPED"), `channelCount`
|
||||
(`Sample::channelCount`, "0 = unknown — a pre-field entry"). Every one of those was additive with
|
||||
a defined absent-value. Under a single ladder, each would have blocked older readers
|
||||
for no reason.
|
||||
|
||||
@@ -197,17 +198,17 @@ message text and the log.
|
||||
|
||||
**One change class that looks additive and is not: a new enum value.**
|
||||
`BankModel::deserialize` *rejects* an out-of-range `SourceMode` or `Tier` rather than
|
||||
degrading it (`bank_model.cpp:232-239`, `:339-346`), and every enum a package carries
|
||||
degrading it (`bank_model.cpp`'s `parseSample` — the `sourceMode` and `tier` branches), and every enum a package carries
|
||||
rides inside the nested `BankModel` blob. So growing either vocabulary is
|
||||
**structural** and bumps `minReaderVersion` too. This is wider than packages and
|
||||
predates them: `BankModel::deserialize` is also the live project ext-state parser
|
||||
(`bank_book_json.cpp:99`), so appending a `SourceMode` value already strands an older
|
||||
(`bank_book_json.cpp`'s `parseBank`), so appending a `SourceMode` value already strands an older
|
||||
build opening a newer project's `.rpp`. Phase Ε inherits that property; it did not
|
||||
cause it, and changing it — degrade-to-`Unknown` at those two sites, the way
|
||||
`BakeStatus` already does — is a change to the model layer, not a package concern. It
|
||||
leaves the argument above untouched: the four fields that motivated the two-integer
|
||||
design are *fields*, and `parseSample`'s `skipValue()` fallback
|
||||
(`bank_model.cpp:370-372`), plus the manifest parsers' equivalent at each level, still
|
||||
(`bank_model.cpp`), plus the manifest parsers' equivalent at each level, still
|
||||
carries them forward.
|
||||
|
||||
This is a borrowed pattern, not an invention: Matroska's `EBMLVersion` /
|
||||
@@ -216,7 +217,7 @@ This is a borrowed pattern, not an invention: Matroska's `EBMLVersion` /
|
||||
must understand to read me." It costs one extra integer and one writer discipline —
|
||||
*decide honestly whether your change is additive* — and that discipline is exactly
|
||||
the one `origin_ledger` already enforces on `OriginKind`
|
||||
(`src/core/tracking/CLAUDE.md:82-83`: "PERSISTED INTEGERS — never renumber, only
|
||||
(`src/core/tracking/CLAUDE.md` §"Gotchas": "PERSISTED INTEGERS — never renumber, only
|
||||
append").
|
||||
|
||||
### Both directions, concretely
|
||||
@@ -225,12 +226,12 @@ append").
|
||||
Every reader reads every `minReaderVersion <= kPackageFormatVersion`. Absent manifest
|
||||
keys take their defined defaults, exactly as `Sample`'s additive fields already do,
|
||||
and exactly as `origin_ledger` lifts a v1 path-only blob into v2 records with kind
|
||||
`Unknown` and empty ids (`origin_ledger.cpp:14-16`). Unrecognized manifest keys are
|
||||
`Unknown` and empty ids (`origin_ledger.cpp`'s version-ladder header comment). Unrecognized manifest keys are
|
||||
skipped, which is already how every parser in this repo behaves
|
||||
(`bank_book_json.cpp:182`). Unrecognized enum integers (the manifest's own —
|
||||
(`bank_book_json.cpp`'s `parseBook`). Unrecognized enum integers (the manifest's own —
|
||||
`BankModel`'s nested ones reject) degrade to their defined `Unknown`-equivalent,
|
||||
never to the numeric default and never to a parse failure —
|
||||
`bake_wire`'s rule verbatim (`src/core/wire/CLAUDE.md:83`: "an unrecognized value
|
||||
`bake_wire`'s rule verbatim (`src/core/wire/CLAUDE.md` §"Modules", the `bake_wire` bullet: "an unrecognized value
|
||||
decodes as `Failed` rather than as the numeric default `Ok`").
|
||||
**The user sees:** a normal import summary. Optionally a single console line naming
|
||||
the older writer version. No dialog, no warning, no ceremony — a supported case is
|
||||
@@ -240,7 +241,7 @@ not an incident.
|
||||
written.** `minReaderVersion > kPackageFormatVersion` is a hard stop, before a single
|
||||
byte is written to the bank folder and before the index is touched. This is exactly
|
||||
`LedgerStatus::FutureVersion`'s treatment, and for the same reason stated at
|
||||
`origin_ledger.cpp:18-21`: parsing an unknown shape by old rules "would yield a
|
||||
`origin_ledger.cpp`'s version-ladder header comment: parsing an unknown shape by old rules "would yield a
|
||||
plausible-but-partial" result, and a partial bank is worse than no bank.
|
||||
**The user sees** a message box (`ShowMessageBox`, verified —
|
||||
`vendor/reaper-sdk/sdk/reaper_plugin_functions.h:6546`,
|
||||
@@ -273,13 +274,13 @@ will not find out which twelve of forty samples were dropped until they need one
|
||||
`bank_book_json` precedent applied outward: the book writer "emits the bank
|
||||
envelope … plus a raw `index` member whose value is the `BankModel` blob verbatim,
|
||||
so per-bank sample serialization stays owned by `bank_model` and is not duplicated
|
||||
here" (`bank_book_json.cpp:15-20`). The package does the same, so a future `Sample`
|
||||
here" (`bank_book_json.cpp`'s file-header comment). The package does the same, so a future `Sample`
|
||||
field reaches packages for free and the shape has exactly one owner.
|
||||
- **Per entry, additionally:** the payload's **bare file name** inside the package,
|
||||
its byte length, and a whole-file `hashBytes` digest
|
||||
(`core/capture/wav_codec.h:143` — FNV-1a 64-bit over raw bytes, 16-char lowercase
|
||||
(`core/capture/wav_codec.h`'s `hashBytes` — FNV-1a 64-bit over raw bytes, 16-char lowercase
|
||||
hex). Note carefully: `hashBytes`, **not** `hashWavContent`. The latter deliberately
|
||||
skips non-`fmt `/`data` chunks (`wav_codec.h:145-151`), which is right for dedup
|
||||
skips non-`fmt `/`data` chunks (`wav_codec.h`'s `hashWavContent`), which is right for dedup
|
||||
identity and wrong for "did these bytes survive the trip." Both hashes are already
|
||||
in the codebase; the package needs the raw one for integrity and carries the
|
||||
`Sample`'s existing `contentHash` for dedup, and they are different fields
|
||||
@@ -306,14 +307,14 @@ guarantee for this feature; overselling it would be the error.
|
||||
one genuinely security-shaped surface this feature has.
|
||||
- **The origin ledger.** The ledger is *this project's* record of files *it*
|
||||
created, and it is the authority prune's protected set is computed from
|
||||
(`src/core/tracking/CLAUDE.md:5-13`). Importing foreign ownership records would
|
||||
(`src/core/tracking/CLAUDE.md` §"Scope"). Importing foreign ownership records would
|
||||
assert this project's authority over another project's history. Instead the
|
||||
importer writes **its own** birth records for the files it lands, at the moment it
|
||||
lands them, through the one writer (`ReaSamplerSession::recordCreated`,
|
||||
`src/shell/persist/session.h:95` — it already takes an `OriginKind`). Without that,
|
||||
`src/shell/persist/session.h` — it already takes an `OriginKind`). Without that,
|
||||
every imported file would be "foreign, therefore never reclaimed"
|
||||
(`core/tracking/CLAUDE.md:24-31`) and a user's bank folder would grow forever.
|
||||
- **Live-instance usage records** (`rsusage_*`, `src/ext_keys.h:65`). Per-instance
|
||||
(`core/tracking/CLAUDE.md` §"Invariants", the "No silent gaps" bullet) and a user's bank folder would grow forever.
|
||||
- **Live-instance usage records** (`rsusage_*`, `src/ext_keys.h`'s `kProjExtUsageKeyPrefix`). Per-instance
|
||||
runtime state of a specific project's specific FX instances. Meaningless elsewhere.
|
||||
- **Project state that is not bank state:** which bank was active, the Design View
|
||||
mode model (`view_state`), the tail setting, the project GUID, the bank-generation
|
||||
@@ -342,11 +343,11 @@ Four distinct collisions hide under the word "collision," and they need four
|
||||
different answers.
|
||||
|
||||
1. **Sample id.** Ids are minted as `"cap-" + uniqueTag + "-" + fileName`
|
||||
(`src/shell/capture/capture.cpp:567`) and `"imp-" + …`
|
||||
(`src/shell/actions/ingest.cpp:269`) — unique within a project, **not** globally.
|
||||
(`src/shell/capture/capture.cpp`'s `OfflineRenderBackend::capture`) and `"imp-" + …`
|
||||
(`src/shell/actions/ingest.cpp`'s `importFileIntoActiveBank`) — unique within a project, **not** globally.
|
||||
Re-importing a package into the project it came from would collide.
|
||||
**Answer: remint every sample id on import**, under its own prefix, and remap
|
||||
`Provenance::parentSampleId` (`bank_model.h:45-50`) through the same map — to the
|
||||
`Provenance::parentSampleId` (`bank_model.h`'s `Provenance` struct) through the same map — to the
|
||||
reminted parent if that parent came in the same package, cleared otherwise. A
|
||||
foreign id never enters the destination index. This also makes "import the same
|
||||
package twice" a clean, duplicative, correct operation rather than an undefined
|
||||
@@ -354,18 +355,18 @@ different answers.
|
||||
2. **File name in the destination bank folder.** **Never overwrite.** Overwriting
|
||||
would destroy an existing capture, and only prune touches existing bank bytes.
|
||||
Mint a fresh unique name through the existing `deriveBankPaths` +
|
||||
unique-tag machinery (`core/capture/capture_paths.h:42`), automatically, no
|
||||
unique-tag machinery (`core/capture/capture_paths.h`'s `deriveBankPaths`), automatically, no
|
||||
prompt, and report the count in the summary.
|
||||
3. **Content hash.** `BankModel::add` collapses an equal-`contentHash` add onto the
|
||||
existing entry (`bank_model.h:144-147`, `AddResult::Collapsed`). Desirable — but
|
||||
existing entry (`bank_model.h`'s `AddResult::Collapsed`). Desirable — but
|
||||
if the file was already written to disk before the collapse, it becomes an
|
||||
instant orphan. **Answer: check the destination bank's `findByHash` BEFORE writing
|
||||
the payload**; on a hit, skip the write entirely and report "N already present."
|
||||
This is the one place the import must consult the model before touching the
|
||||
filesystem, and it is a concrete acceptance criterion rather than an optimization.
|
||||
4. **Bank display name.** `bank_book` enforces unique display names, trimmed and
|
||||
case-insensitive ASCII (`src/core/model/CLAUDE.md:22-26`; `createBank`'s own
|
||||
contract at `bank_book.h:92-96` — *"Drums"/"drums"/" Drums " collide, including
|
||||
case-insensitive ASCII (`src/core/model/CLAUDE.md` §"Invariants", the "Bank identity, movement, dedup" bullet; `createBank`'s own
|
||||
contract at `bank_book.h` — *"Drums"/"drums"/" Drums " collide, including
|
||||
against the pool's "Pool"*), so `createBank("Drums")` into a project that already
|
||||
has "Drums" returns `false` with no mutation. **Answer: an automatic numeric
|
||||
suffix, specified below.** No prompt, no overwrite, no refusal.
|
||||
@@ -381,7 +382,7 @@ trim, the seed is the literal `Imported bank`.
|
||||
|
||||
**The probe.** Let `seed` be that string and `fold(x)` be `BankBook`'s own uniqueness
|
||||
key — strip leading/trailing ASCII whitespace, lower-case ASCII letters
|
||||
(`bank_book.h:252-258`). Take the **first** name in this sequence whose fold is not
|
||||
(`bank_book.h`'s `BankBook::nameKey`). Take the **first** name in this sequence whose fold is not
|
||||
already carried by a bank in the destination book:
|
||||
|
||||
seed, seed + " 2", seed + " 3", seed + " 4", …
|
||||
@@ -410,7 +411,7 @@ implementations diverge:**
|
||||
`B + 1` candidates is free by pigeonhole, so no cap is needed and none should be
|
||||
added.
|
||||
4. **The fold has exactly one home.** `import_plan` must **not** re-implement
|
||||
`nameKey` — `bank_book.h:252-258` says in as many words that a drifted second copy
|
||||
`nameKey` — `bank_book.h`'s `BankBook::nameKey` says in as many words that a drifted second copy
|
||||
would let the uniqueness invariant be violated. The probe therefore runs behind
|
||||
`BankBook`'s own folding, which means Ε-W2-T2 adds **one additive public `const`
|
||||
member** to `BankBook` (recommended: `std::string uniqueDisplayName(const
|
||||
@@ -422,7 +423,7 @@ name. Sample ids are reminted by collision rule 1 regardless of whether a name
|
||||
collision occurred, and the two mechanisms are independent. **`Sample` display names
|
||||
are never suffixed** — two banks may legitimately hold a sample called `"Kick"`, and
|
||||
`resample_name`'s own contract already states that sample display names are not unique
|
||||
(`resample_name.h:13-16`). Bank-folder file names are handled by collision rule 2 and
|
||||
(`resample_name.h`'s `nextIterationName`). Bank-folder file names are handled by collision rule 2 and
|
||||
are unaffected by the bank's name. `slot_map` positions ride along unchanged.
|
||||
|
||||
**The pool case is guaranteed, not hypothetical.** Exporting the pool is in scope (the
|
||||
@@ -456,7 +457,7 @@ settled on: report before acting, and never leave a half-state that looks whole.
|
||||
| Destination package file exists | export | Platform save dialog's own overwrite confirm | Native dialog |
|
||||
| Write fails partway | export | Temp file in the destination directory, atomic rename only on complete success | Console error; no `.rsbank` left behind. A truncated package must never exist |
|
||||
| `minReaderVersion` above this build | import | Refuse whole. Nothing written, index untouched | The three-part message box above (package needs / this build reads / what to install) |
|
||||
| Malformed or truncated container | import | Refuse whole. Reported **distinctly from** the version case | "This file is not a readable bank package (corrupt or truncated)." The distinction matters: the two have opposite recoveries — one is "install a newer build," the other is "get an intact copy." `origin_ledger.cpp:178-185` makes exactly this distinction for exactly this reason |
|
||||
| Malformed or truncated container | import | Refuse whole. Reported **distinctly from** the version case | "This file is not a readable bank package (corrupt or truncated)." The distinction matters: the two have opposite recoveries — one is "install a newer build," the other is "get an intact copy." `origin_ledger.cpp`'s `parseStored` makes exactly this distinction for exactly this reason |
|
||||
| Entry name contains a path separator, `..`, or is absolute | import | Refuse whole, before any write | "This package is not well-formed." Hostile input, not user error — no need to elaborate |
|
||||
| Payload hash mismatch on any entry | import | Refuse whole, before landing anything | "This bank package is damaged (entry `<name>` failed its integrity check). Nothing was imported." |
|
||||
| A write fails mid-import (disk full, permission) | import | Roll back: delete the files **this import wrote** and abandon the index mutation | "Import failed and was rolled back. Nothing was added." |
|
||||
@@ -467,7 +468,7 @@ settled on: report before acting, and never leave a half-state that looks whole.
|
||||
|
||||
**On the rollback, and why it is not an invariant breach.** Prune is the single
|
||||
exclusive file-deletion authority, with exactly one carve-out, stated in one place —
|
||||
`src/shell/persist/prune_fs.cpp:5-11`: "a shell removing a file it wrote itself
|
||||
`src/shell/persist/prune_fs.cpp`'s file-header comment: "a shell removing a file it wrote itself
|
||||
moments earlier and that no index ever referenced is self-cleanup, not authority
|
||||
over user data … the discriminator is 'did this call create it, and did anything ever
|
||||
reference it', not where it sits." An import rollback fits that discriminator
|
||||
@@ -477,11 +478,11 @@ restate it, or a reviewer will correctly read the rollback as a breach.
|
||||
|
||||
**On undo.** The index side of an import is one Ctrl-Z, through the same
|
||||
`persistBankOp` undo batching every bank verb already uses
|
||||
(`src/shell/bank_ops/CLAUDE.md:29-31`; `Undo_BeginBlock2` / `Undo_EndBlock2` verified
|
||||
(`src/shell/bank_ops/CLAUDE.md` §"Invariants", the "One bank operation is one Ctrl-Z" bullet; `Undo_BeginBlock2` / `Undo_EndBlock2` verified
|
||||
at `reaper_plugin_functions.h:7758` and `:7806`). **Undo does not un-write the
|
||||
files** — they remain on disk, referenced by no index, until a prune reclaims them.
|
||||
That is the same designed orphaned-until-prune window a non-empty bank delete already
|
||||
produces (`src/core/model/CLAUDE.md:38-40`). Say it out loud in the spec; do not let
|
||||
produces (`src/core/model/CLAUDE.md` §"Invariants", the "Bank identity, movement, dedup" bullet). Say it out loud in the spec; do not let
|
||||
a user infer that Ctrl-Z cleans the folder.
|
||||
|
||||
### Import under a degraded tracking ledger (Ε-F3, RULED: refuse)
|
||||
@@ -494,7 +495,7 @@ reasoning that carried it is recorded below rather than re-argued.
|
||||
|
||||
**The trigger, exactly.** The guard fires when `tracking::ledgerDegraded(status)` holds
|
||||
for the project's loaded ledger status — that is, `LedgerStatus::Unreadable` or
|
||||
`LedgerStatus::FutureVersion` (`src/core/tracking/origin_ledger.h:94`, `:100-101`).
|
||||
`LedgerStatus::FutureVersion` (`src/core/tracking/origin_ledger.h`'s `LedgerStatus` and `ledgerDegraded`).
|
||||
`Fresh` (absent key — a legitimate new project) and `Loaded` both proceed normally.
|
||||
|
||||
**Two things the guard is deliberately NOT keyed on:**
|
||||
@@ -513,7 +514,7 @@ package is read, before any allocation. Making the user find and pick a file we
|
||||
already decided to refuse is the wrong order.
|
||||
|
||||
**What the user sees.** A console block through `ShowConsoleMsg`, mirroring prune's
|
||||
abort (`src/shell/actions/prune_action.cpp:30-69`) in structure and in tone, because a
|
||||
abort (`src/shell/actions/prune_action.cpp`'s `doBankPruneFolder` — the `blockedByTracking` console block) in structure and in tone, because a
|
||||
user who has hit prune's block should recognise this one. Every recovery line names
|
||||
**this build's** ext-state namespace via `version::extStateNamespace()` — the
|
||||
beta/stable trap prune already documents, where a beta user handed the stable spelling
|
||||
@@ -545,7 +546,7 @@ clears the wrong key and is still blocked. Two cases, exactly one of which fires
|
||||
> them could be given a birth record, and every one would be permanently unreclaimable.
|
||||
|
||||
**The recovery path.** The status is written only by `loadFromProject`, so it is sticky
|
||||
for the session (`src/shell/persist/CLAUDE.md:39-46`): repair or clear the key
|
||||
for the session (`src/shell/persist/CLAUDE.md` §"Invariants", the "A ledger this build cannot read is degraded" bullet): repair or clear the key
|
||||
(malformed case only), or install the newer build (future-version case), **reopen the
|
||||
project**, then import again. The package needs no re-export, and nothing about the
|
||||
destination project was changed by the refusal.
|
||||
@@ -557,7 +558,7 @@ likely to want to. Only the landing side refuses.
|
||||
|
||||
**Why the ruling went this way.** The rejected option — allow the import behind an
|
||||
up-front confirm — matched the accepted residual already stated at
|
||||
`core/tracking/CLAUDE.md:24-31`, where a capture made during a degraded session is
|
||||
`core/tracking/CLAUDE.md` §"Invariants" (the "No silent gaps" bullet), where a capture made during a degraded session is
|
||||
recorded in memory but not persisted and degrades to foreign. The argument that carried
|
||||
is **scale**: that residual contemplates *one* untracked capture, and a bulk import can
|
||||
strand two hundred files in a single gesture. Same mechanism, different animal. A
|
||||
@@ -611,7 +612,7 @@ Two new directories, following the split the whole repo turns on.
|
||||
and `classifyPackageVersion(formatVersion, minReader) -> Readable | TooNew |
|
||||
Malformed`. The ladder lives with the framing because the ladder *is* the framing's
|
||||
contract, and it gets a header-comment ladder written the way
|
||||
`origin_ledger.cpp:8-21` writes one.
|
||||
`origin_ledger.cpp`'s version-ladder header comment writes one.
|
||||
- `package_manifest` — the manifest model and its JSON codec, nesting `BankModel`'s
|
||||
own blob verbatim.
|
||||
- `bank_package` — header encode / prefix decode / entry layout, composing the two
|
||||
@@ -643,21 +644,21 @@ responsibility seam, which is what the structural heuristic asks for.
|
||||
`mode=1` an existing one (import's source). `extension_list` takes the
|
||||
`'ReaSampler banks|*.rsbank|All files|*.*'` form. `GetUserFileNameForRead` is
|
||||
explicitly "Superseded, see GetUserFileName" (`:3796`) and is not used. No fallback
|
||||
is needed: `src/app/main.cpp:15` defines `REAPERAPI_IMPLEMENT` without
|
||||
is needed: `src/app/main.cpp`'s `#define REAPERAPI_IMPLEMENT` appears without
|
||||
`REAPERAPI_MINIMAL`, so the resolver walks the full table (`GetUserFileName` at
|
||||
`:9084`), and `main.cpp:292-293` refuses to load the extension if any one function
|
||||
`:9084`), and `REAPER_PLUGIN_ENTRYPOINT`'s `REAPERAPI_LoadAPI` check refuses to load the extension if any one function
|
||||
fails to resolve — so no REAPER build that loads us can lack it.
|
||||
- `export_bank` / `import_bank` — the promptless verbs, mirroring
|
||||
`src/shell/bank_ops/`'s pattern exactly: take a `ReaSamplerSession&`, do the work,
|
||||
return an outcome, **no prompts and no message boxes**. The bindable action and the
|
||||
panel menu item are then thin skins over one verb apiece, so the logic has one home
|
||||
(`src/shell/bank_ops/CLAUDE.md:1-12`).
|
||||
(`src/shell/bank_ops/CLAUDE.md` §"Scope").
|
||||
|
||||
**The dependency-shape criterion, stated because the brief demands it.** The pure
|
||||
planners take **explicit value inputs** — the decoded manifest, the destination
|
||||
`BankBook`, the set of file names present in the bank folder — never a session handle,
|
||||
never a service container, never a "pass me the thing that has everything." The shell
|
||||
*gathers*; the core *decides*. That is the same shape `src/shell/persist/CLAUDE.md:11`
|
||||
*gathers*; the core *decides*. That is the same shape `src/shell/persist/CLAUDE.md` §"Scope"
|
||||
already states ("it gathers rather than decides"). If a circular dependency shows up
|
||||
during the build, the fix is a service split or a thin interface at the seam — never
|
||||
threading an extra parameter through a chain of constructors, and never handing a
|
||||
@@ -690,12 +691,12 @@ constructors.
|
||||
freshly-generated pair.
|
||||
- **Bank generation.** Import mutates bank content that live ReaSampler 9000
|
||||
instances may play, so it must `bumpBankGeneration()`
|
||||
(`src/shell/persist/session.h:108`, whose own comment says call sites "err toward
|
||||
(`src/shell/persist/session.h`'s `ReaSamplerSession::bumpBankGeneration`, whose own comment says call sites "err toward
|
||||
bumping"). Export mutates nothing and must bump nothing, write no ext state, and
|
||||
open no undo point.
|
||||
- **Beta/stable channel isolation.** Packages are channel-**agnostic** and this is
|
||||
deliberate. Channel isolation exists so a beta cannot rewrite a stable project's
|
||||
ext state (`app_version.h:73-76`); a package is a file the user moves by hand, not
|
||||
ext state (`app_version.h`'s `extStateNamespace` — the ISOLATION comment); a package is a file the user moves by hand, not
|
||||
ambient project state, so there is no isolation property to preserve. A beta build
|
||||
and a stable build at the same package format read each other's packages, and that
|
||||
is the useful behaviour. The version ladder — not the channel — is what gates.
|
||||
@@ -725,6 +726,30 @@ contemplates one untracked capture, an import strands hundreds).
|
||||
|
||||
---
|
||||
|
||||
## Implementation decisions — Ε-W2-T1
|
||||
|
||||
Not [Daniel]-class forks — both were `[propose at review]` calls in `docs/PLAN.md`'s
|
||||
Ε-W2-T1 track, answered at implementation review rather than by Daniel, and recorded
|
||||
here per this phase's own convention for keeping such answers where the design lives
|
||||
rather than only in the track's own now-stale open-questions line.
|
||||
|
||||
- **Affordance: both the bindable action and the panel row.** The action targets the
|
||||
**active** bank and is the only spelling that can reach the **pool** (the panel's
|
||||
`showTabMenu` returns early on `isPool()` — a named-bank-tab context menu has no tab
|
||||
to right-click for the pool), while the exported unit's own definition above includes
|
||||
the pool. The panel row is the direct gesture on a specific named bank. Neither
|
||||
subsumes the other.
|
||||
- **Default file name: the bank's display name**, sanitized through
|
||||
`capture_paths::sanitizeStem`, seeded into `<projectDir>/<stem>.rsbank`. A
|
||||
project-derived name was the rejected alternative: three banks exported from one
|
||||
project must produce three distinguishable files, and a project-derived name
|
||||
collides on the second export. Known wart, worth recording rather than hiding:
|
||||
`sanitizeStem` collapses an all-non-ASCII display name to the literal `capture`, so
|
||||
two such banks still collide — the existing rename verb is the recovery, same as the
|
||||
import-side auto-suffix collisions above.
|
||||
|
||||
---
|
||||
|
||||
## Non-goals and guardrails
|
||||
|
||||
- **No auto-insertion of imported audio into the arrange.** Same rule as capture.
|
||||
|
||||
@@ -53,12 +53,20 @@ snapshot/restore, forces dither and all normalize-postprocessing off, and render
|
||||
32-bit float. The tail wires into that existing path — no new render trigger, no
|
||||
new backend.
|
||||
|
||||
### Bounds are always custom — so the tail bit is always `&1`
|
||||
### Bounds are always the time selection — so the tail bit is always `&4`
|
||||
|
||||
The backend renders with `RENDER_BOUNDSFLAG = 0` (custom time bounds) for **every**
|
||||
scope and every range type: it sets `RENDER_STARTPOS` / `RENDER_ENDPOS` explicitly
|
||||
from the request's exact seconds (`capture.cpp` ~L352–354). It does **not** use the
|
||||
time-selection / selected-items / regions bounds modes.
|
||||
The backend renders with `RENDER_BOUNDSFLAG = 2` (time selection) for **every**
|
||||
scope and every range type: it writes the request's exact seconds into the
|
||||
project's own time selection via `GetSet_LoopTimeRange` (`capture.cpp` ~L470–477;
|
||||
`RENDER_STARTPOS`/`RENDER_ENDPOS` are also written, as a defensive no-op for a
|
||||
mode-0-only field, but the window itself travels in the time selection). It does
|
||||
**not** use the custom-time-bounds mode (`RENDER_BOUNDSFLAG = 0`) — that mode was
|
||||
tried and retired: DAW observation showed REAPER resolving a custom-bounds window
|
||||
on a whole-millisecond grid AT RENDER TIME, flooring the end and rendering exactly
|
||||
the floored frame count, which silently broke the exact-bounds precision
|
||||
invariant. The time-selection mode does not floor the window. (The one narrative
|
||||
home for that finding is `render_settings.h`'s `kRenderBoundsTimeSelection`; this
|
||||
doc points there rather than retelling it.)
|
||||
|
||||
`RENDER_TAILFLAG` is a bitmask keyed to the **bounds mode**, not the capture range
|
||||
type (header line 3047):
|
||||
@@ -69,18 +77,20 @@ RENDER_TAILFLAG : &1=custom time bounds, &2=entire project, &4=time selection,
|
||||
&32=selected project markers/regions
|
||||
```
|
||||
|
||||
Because we always render in custom-time-bounds mode, **the only tail bit that ever
|
||||
applies is `&1`**. There is no per-range-type tail-flag decision to make — a razor
|
||||
capture, a time-selection capture, and an item capture are all custom-bounds
|
||||
renders under the hood, so all three take `RENDER_TAILFLAG = 1`.
|
||||
Because we always render in time-selection mode, **the only tail bit that ever
|
||||
applies is `&4`**. There is no per-range-type tail-flag decision to make — a razor
|
||||
capture, a time-selection capture, and an item capture are all time-selection-bounds
|
||||
renders under the hood, so all three take `RENDER_TAILFLAG = 4`.
|
||||
|
||||
> **Correction to the framing brief.** The brief asked us to pick a
|
||||
> `RENDER_TAILFLAG` bit *per capture range type* (time selection vs. razor vs. item)
|
||||
> and flagged `&32` as "markers/regions." The header (line 3047) says `&32` =
|
||||
> *selected project regions* and `&8` = *all markers/regions* — but neither matters:
|
||||
> our renders are all `RENDER_BOUNDSFLAG = 0`, so the tail bit is `&1` unconditionally.
|
||||
> The existing `kTailFlagCustomBounds = 1.0` constant in `capture.cpp` (~L80) is
|
||||
> already correct; the field wiring is what's missing.
|
||||
> our renders are all `RENDER_BOUNDSFLAG = 2`, so the tail bit is `&4` unconditionally.
|
||||
> The existing `kTailFlagTimeSelection = 4` constant in
|
||||
> `src/core/capture/render_settings.h` (the bounds mode's own bit, per bounds mode —
|
||||
> header line 3047) is already correct — it was right from the start; the wording
|
||||
> above it (which had assumed a custom-bounds render) was what was wrong.
|
||||
|
||||
### Mode 1 — Automatic (default): generous tail + auto-trim to -72 dB
|
||||
|
||||
@@ -88,7 +98,7 @@ Set, in addition to the exact `STARTPOS`/`ENDPOS` already driven:
|
||||
|
||||
| Setting | Value | Meaning / header ref |
|
||||
|---|---|---|
|
||||
| `RENDER_TAILFLAG` | `1` | apply tail for custom time bounds (line 3047, `&1`) |
|
||||
| `RENDER_TAILFLAG` | `4` | apply tail for time selection (line 3047, `&4`) |
|
||||
| `RENDER_TAILMS` | `8000` | the 8 s cap, in ms (line 3048) |
|
||||
| `RENDER_NORMALIZE` | `32768` | **only** the trim-ending-silence bit (line 3051, `&32768`) |
|
||||
| `RENDER_TRIMEND` | `≈ 0.000251` | -72 dB threshold (line 3062; scaling below) |
|
||||
@@ -156,7 +166,7 @@ The existing (currently unwired) `CaptureRequest.renderTail` / `tailMs` fields
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| `RENDER_TAILFLAG` | `1` |
|
||||
| `RENDER_TAILFLAG` | `4` |
|
||||
| `RENDER_TAILMS` | `request.tailMs` (clamped to the 8 s cap — see below) |
|
||||
| `RENDER_NORMALIZE` | `262144` (`kNormalizeDisableAll`, unchanged) |
|
||||
| `RENDER_TRIMEND` | not set / irrelevant (trim bit is clear) |
|
||||
@@ -177,9 +187,9 @@ adds a third state, so the wiring is a small enum, not a bool:
|
||||
- **None** (default for null-test / verify captures, and the current two-scope
|
||||
action defaults): `RENDER_TAILFLAG = 0`, `RENDER_TAILMS = 0`, normalize =
|
||||
disable-all. Exact bounds. Byte-identical to today.
|
||||
- **Auto** (the new user-facing default for tail-on captures): tailFlag `1`,
|
||||
- **Auto** (the new user-facing default for tail-on captures): tailFlag `4`,
|
||||
tailMs `8000`, normalize `32768` (surgical trim), trimEnd `0.00025119`.
|
||||
- **Manual(ms)**: tailFlag `1`, tailMs `clamp(ms, 8000)`, normalize `262144`
|
||||
- **Manual(ms)**: tailFlag `4`, tailMs `clamp(ms, 8000)`, normalize `262144`
|
||||
(disable-all), no trim.
|
||||
|
||||
Recommended shape: replace `bool renderTail` with a `TailMode { None, Auto,
|
||||
|
||||
@@ -226,25 +226,25 @@ them through the reorg, not to change them:
|
||||
These are the naming equivalent of the JSON-`Parser` DRY violation — concrete hazards, not taste:
|
||||
|
||||
1. **Four hand-rolled `Parser` classes, one name.** `class Parser` is defined **four times** —
|
||||
`bank_model.cpp:306`, `bank_book.cpp:663`, `owned_manifest.cpp:107`, `view_mode_model.cpp:654`.
|
||||
`bank_model.cpp`, `bank_book.cpp`, `owned_manifest.cpp`, `view_mode_model.cpp`.
|
||||
Q-W1 already deletes three of them by extracting `core/json`; the naming rule is that the
|
||||
survivor is **`json::Parser`** (or a more specific `json::Reader`/`json::Writer` pair — see
|
||||
Q-8), never a bare `Parser` in flat scope.
|
||||
2. **`FooterRect` and `ButtonRect` are shared across pure UI modules — and the codebase already
|
||||
*knows* it.** `struct FooterRect` and `struct ButtonRect` are defined in `prune_button.h`
|
||||
(lines 32, 46) and **reused** by `footer_bar.h`, which carries an explicit in-file "NAME NOTE"
|
||||
(`footer_bar.h:27–34`) documenting that `ButtonRect / FooterRect / SegmentRect / ActionBarRect /
|
||||
and **reused** by `footer_bar.h`, which carries an explicit in-file "NAME NOTE"
|
||||
(`footer_bar.h`) documenting that `ButtonRect / FooterRect / SegmentRect / ActionBarRect /
|
||||
KitBox / KitButtonBox` are "already owned in this namespace" and that new types must carry a
|
||||
`FooterBar*` prefix to avoid collision. That comment is a smell made visible: the flat
|
||||
`reasampler::` namespace forces every pure-UI author to hand-check for name collisions before
|
||||
minting a type. This is the single strongest in-codebase argument for the Q-4 sub-namespaces —
|
||||
under `reasampler::ui` these shared rect types get one clear owner and the hand-checking stops.
|
||||
3. **`Sample` (`bank_model.h:69`, the bank metadata struct) vs `AudioSample` (the `peaks` float
|
||||
3. **`Sample` (`bank_model.h`'s `Sample` struct, the bank metadata struct) vs `AudioSample` (the `peaks` float
|
||||
alias).** Already flagged in §2.4/Q-4; verified — `Sample` is the model record, `AudioSample`
|
||||
is a raw PCM float. Under `model::Sample` vs `audio::AudioSample` the collision risk is gone,
|
||||
but the *names* still read oddly side by side (a `Sample` that is metadata, an `AudioSample`
|
||||
that is one float). Noted; the namespace split is the required fix, a rename is optional (Q-8).
|
||||
4. **`Selection` (`bank_grid.h:112`) and `CellRect` (`bank_grid.h:23`) are generic names in a
|
||||
4. **`Selection` (`bank_grid.h`'s `Selection` struct) and `CellRect` (`bank_grid.h`'s `CellRect`) are generic names in a
|
||||
flat namespace.** `Selection` in particular is the kind of name a newcomer cannot place without
|
||||
opening the file. `ui::Selection` / `ui::CellRect` resolve it structurally; no rename needed
|
||||
beyond the namespace.
|
||||
@@ -255,23 +255,23 @@ Here the names are legal and non-colliding but do not read on one principle —
|
||||
at" gap:
|
||||
|
||||
1. **The model-family suffixes disagree: `_model` vs `_book` vs `Index`.** Verified: the pure model
|
||||
modules are `bank_model.{h,cpp}` (owning `class BankIndex`, `bank_model.h:132`), `bank_book.{h,cpp}`
|
||||
(owning `class BankBook`, `bank_book.h:208`), `view_mode_model.{h,cpp}` (owning `class ViewModeModel`,
|
||||
`view_mode_model.h:376`), `owned_manifest.{h,cpp}` (owning `class OwnedFileManifest`,
|
||||
`owned_manifest.h:52`). Four modules, four different file↔class naming relationships:
|
||||
modules are `bank_model.{h,cpp}` (owning `class BankIndex`, `bank_model.h`), `bank_book.{h,cpp}`
|
||||
(owning `class BankBook`, `bank_book.h`'s `BankBook`), `view_mode_model.{h,cpp}` (owning `class ViewModeModel`,
|
||||
`view_mode_model.h`'s `ViewModeModel`), `owned_manifest.{h,cpp}` (owning `class OwnedFileManifest`,
|
||||
`owned_manifest.h`). Four modules, four different file↔class naming relationships:
|
||||
`bank_model`→`BankIndex` (file says "model," class says "index"), `bank_book`→`BankBook`
|
||||
(file = class), `view_mode_model`→`ViewModeModel` (file = class), `owned_manifest`→`OwnedFileManifest`
|
||||
(file ≈ class, but the class adds "File"). The `bank_model`/`BankIndex` mismatch is the worst:
|
||||
the file name and its primary class name share no word. This is a genuine legibility wart — the
|
||||
fix is a *rename decision* (Q-8), not something the directory move alone resolves.
|
||||
2. **The `bank_book` "wraps `bank_model`" relationship is invisible in the names.** `BankBook`
|
||||
(`bank_book.h:208`) is a registry of `Bank` (`bank_book.h:147`), each wrapping a `BankIndex`
|
||||
(`bank_model.h:132`). The names `Book` → `Bank` → `Index` do not read as a containment hierarchy;
|
||||
(`bank_book.h`'s `BankBook`) is a registry of `Bank` (`bank_book.h`'s `Bank` struct), each wrapping a `BankIndex`
|
||||
(`bank_model.h`). The names `Book` → `Bank` → `Index` do not read as a containment hierarchy;
|
||||
a reader has to learn it. (Not necessarily worth a rename — "book of banks" is evocative — but
|
||||
it is the kind of call Q-8 should make deliberately, not by accident.)
|
||||
3. **`realtime_record.h` (pure) vs `capture_realtime.cpp` (shell) — the word order flips.** Verified:
|
||||
the pure realtime module is `realtime_record.{h}` (owning `RecordModePlan`/`RecordPhase`/
|
||||
`RecordTickInputs`, `realtime_record.h:57–173`) while its shell is `capture_realtime.cpp`. So the
|
||||
`RecordTickInputs`, `core/capture/capture_realtime.h`) while its shell is `capture_realtime.cpp`. So the
|
||||
pure core is `realtime_record` but the shell is `capture_realtime` — the two halves of one feature
|
||||
are named on inverted word order (`realtime_record` vs `capture_realtime`). Compare the *clean*
|
||||
shell-pair convention elsewhere: `drag_out` (pure) ↔ `drag_out_win` (shell) — same stem, suffix
|
||||
@@ -279,7 +279,7 @@ at" gap:
|
||||
naming-drift instance in the tree (Q-9).
|
||||
4. **`capture.{h,cpp}` is the *offline* backend shell, but the name claims all of capture.**
|
||||
Verified: `capture.h` declares `ICaptureBackend`, `OfflineRenderBackend`, **and**
|
||||
`RealtimeRecordBackend` (`capture.h:112,124,201`), while the realtime *implementation* lives in
|
||||
`RealtimeRecordBackend` (`capture.h`'s `OfflineRenderBackend`), while the realtime *implementation* lives in
|
||||
`capture_realtime.cpp` and its pure planner in `realtime_record.h`. So `capture` is really
|
||||
"capture interface + offline backend," a fat header (the §2.3 Interface-Segregation concern) whose
|
||||
name oversells its scope. Its Q-W3 hoist (`capture_orchestrator`/`scope_resolve`) is the moment
|
||||
@@ -290,11 +290,11 @@ at" gap:
|
||||
Swept for names a newcomer couldn't decode; the tree is mostly clean here (a credit to it). Two
|
||||
minor notes:
|
||||
|
||||
- **`guid_diff` / `GuidBaseline` (`guid_diff.h:40`)** — "GUID diff" is decodable in context (it
|
||||
- **`guid_diff` / `GuidBaseline` (`guid_diff.h`'s `GuidBaseline`)** — "GUID diff" is decodable in context (it
|
||||
diffs the live track/item GUID set between polls) but `GuidBaseline` reads more clearly as "the
|
||||
previous-poll snapshot" than the module name suggests. Low priority; leave unless its `core/view`
|
||||
relocation invites it.
|
||||
- **`MinMax` (`peaks.h:30`), `KitBox` (`component_geometry.h:28`)** — terse but correct and local;
|
||||
- **`MinMax` (`peaks.h`'s `MinMax`), `KitBox` (`component_geometry.h`'s `KitBox`)** — terse but correct and local;
|
||||
no change. Named here only to record they were swept and cleared.
|
||||
|
||||
### 2b.5 What the naming audit does NOT touch (hard boundary)
|
||||
|
||||
@@ -25,7 +25,8 @@ with its reasoning stated; contradict it in review with an argument, not a prefe
|
||||
|
||||
**Slug convention.** `Λ → l`, so tracks dispatch into `pl-w<wave>-t<track>-<slug>` —
|
||||
Λ-W2-T1 into `pl-w2-t1-linux-compile-blockers`. The two audits already ran under
|
||||
`pl-w1-t1-build-toolchain-audit` and `pl-w1-t2-source-runtime-audit`. **`docs/PLAN.md:33–36`
|
||||
`pl-w1-t1-build-toolchain-audit` and `pl-w1-t2-source-runtime-audit`. **`docs/PLAN.md`'s
|
||||
"Worktree slug convention" paragraph
|
||||
lists the transliterations for Θ / Ξ / Γ / Ψ / Ε / Ρ and does not yet carry Λ — adding it
|
||||
is a plan-doc edit this phase's PLAN.md entry must make.**
|
||||
|
||||
@@ -77,21 +78,21 @@ this doc re-read against the tree while writing.
|
||||
**The extension is close — two one-line compile blockers stand between the tree and a
|
||||
GCC/Clang build.**
|
||||
|
||||
- `src/shell/panel/draw_kit.cpp:73` passes `DEFAULT_PITCH | FF_DONTCARE` to `CreateFont`.
|
||||
- `src/shell/panel/draw_kit.cpp`'s `loadFont` passes `DEFAULT_PITCH | FF_DONTCARE` to `CreateFont`.
|
||||
`FF_DONTCARE` has zero occurrences anywhere in `vendor/WDL/` (L2-01); the file is not
|
||||
platform-guarded, only its include is (`:70–77`). `draw_kit` links into both modules, so
|
||||
platform-guarded, only its include is (`loadFont`'s whole body). `draw_kit` links into both modules, so
|
||||
nothing builds.
|
||||
- `src/shell/actions/instrument_drop_win.cpp:59` calls `GetCurrentProcessId()` inside
|
||||
`writeTempPreset` with no platform branch anywhere in the TU. SWELL exports
|
||||
- `src/shell/actions/instrument_drop_win.cpp`'s `writeTempPreset` calls `GetCurrentProcessId()`
|
||||
with no platform branch anywhere in the TU. SWELL exports
|
||||
`GetCurrentThreadId` and not this (L2-02). The PID exists only to keep two concurrent
|
||||
REAPER instances from colliding in the shared temp dir; the atomic counter at `:52`
|
||||
REAPER instances from colliding in the shared temp dir; the atomic counter in the same function
|
||||
already carries the intra-process half.
|
||||
|
||||
**`core/` is genuinely pure, and it was verified rather than assumed.** Every `#include`
|
||||
under `src/core/**` is a `core/` sibling, one of 26 standard headers, or the generated
|
||||
`version_generated.h` — zero REAPER, SWELL, WDL, LICE, VST3 or `windows.h` (T2 §1.1). The
|
||||
whole directory contains nine preprocessor conditional lines, exactly one of which is a
|
||||
platform fork, and that one (`capture_paths.cpp:18–20`, the Windows case-fold) is *correct*
|
||||
platform fork, and that one (`capture_paths.cpp`'s `normalizeSlashes`, the Windows case-fold) is *correct*
|
||||
for Linux with both branches already asserted by `tests/test_capture_paths.cpp`. All 91
|
||||
test TUs under `tests/` are platform-neutral.
|
||||
|
||||
@@ -100,24 +101,25 @@ double-buffered LICE `WM_PAINT`, mouse/wheel/capture, `WM_CAPTURECHANGED` rollba
|
||||
seven cursors, menus, the keyboard accelerator path, modifier reads, tooltips, drag-out and
|
||||
`DragQueryFile`/`DragFinish` were each checked by name against `swell-functions.h` /
|
||||
`swell-types.h` and are present (T2 §1.5). The Windows-only escapes are three:
|
||||
`DragAcceptFiles` (`panel_window.cpp:148–150`, `#ifdef _WIN32`), `SHFileOperationW`
|
||||
`DragAcceptFiles` (`panel_window.cpp`'s `openPanel`, under `#ifdef _WIN32`), `SHFileOperationW`
|
||||
(prune), and OLE `DoDragDrop` (drag-out) — each already carrying a non-Windows branch or a
|
||||
documented reason it does not. **This is the audits' single most load-bearing finding.**
|
||||
|
||||
**The dock panel will not appear until the dialog-resource question is answered.**
|
||||
`panel_window.cpp:135` is `CreateDialogParam(g_hInst, MAKEINTRESOURCE(IDD_BANK_PANEL), …)`,
|
||||
`panel_window.cpp`'s `openPanel` calls `CreateDialogParam(g_hInst, MAKEINTRESOURCE(IDD_BANK_PANEL), …)`,
|
||||
which SWELL resolves out of a per-module registry populated by a **resgen-generated source
|
||||
file that is not in the Linux target**: `src/app/CMakeLists.txt:97` has the `target_sources`
|
||||
line commented out (and `:86` for macOS). The registry head stays null, `SWELL_CreateDialog`
|
||||
returns null, `panel_window.cpp:137` returns, and the toggle action is a silent no-op with
|
||||
file that is not in the Linux target**: `src/app/CMakeLists.txt`'s `else()` (Linux) branch has the `target_sources`
|
||||
line commented out (and the `elseif(APPLE)` branch's own copy for macOS). The registry head stays null, `SWELL_CreateDialog`
|
||||
returns null, `openPanel`'s `if (!g_panel.hwnd) return;` guard returns, and the toggle action is a silent no-op with
|
||||
no console line and no Actions-list checkmark (Λ-01, L2-06). Three defects stack in the
|
||||
commented-out instructions themselves: the script named at `:96` (`mac_resgen.php`) does
|
||||
commented-out instructions themselves: the script named in the `elseif(APPLE)`/`else()` branches' comment lines (`mac_resgen.php`) does
|
||||
not exist, the output filename is wrong, and the output is an `#include`-only artifact that
|
||||
cannot be a `target_sources` entry at all (Λ-01). Λ-F2 decides the route.
|
||||
|
||||
**The instrument's Linux editor is a from-scratch X11 job, and today the target does not
|
||||
configure at all.** `src/shell/instrument/CMakeLists.txt:9` is
|
||||
`if(WIN32 AND EXISTS "${VST3_SDK}/…/pluginfactory.cpp")` — a conjunction, so a Linux
|
||||
configure at all.** `src/shell/instrument/CMakeLists.txt`'s
|
||||
`if(WIN32 AND EXISTS "${VST3_SDK}/…/pluginfactory.cpp")` gate is
|
||||
a conjunction, so a Linux
|
||||
configure silently omits `reasampler_vst` even with the submodule slice fully initialised.
|
||||
Beyond the gate: the wrong module entry point is compiled, the artifact is a file where
|
||||
Linux wants a directory bundle, nothing hands a VST3 plugin the SWELL function table, and
|
||||
@@ -127,7 +129,7 @@ drawing survives a window-system change intact. It is the window and event plumb
|
||||
is entirely absent.
|
||||
|
||||
**Nothing about the build is optimized, and the documented ship command is a no-op on
|
||||
Linux.** Root `CMakeLists.txt:28–30` is the complete list of language settings — there is
|
||||
Linux.** Root `CMakeLists.txt`'s `set(CMAKE_CXX_STANDARD ...)`/`set(CMAKE_CXX_STANDARD_REQUIRED ...)`/`set(CMAKE_POSITION_INDEPENDENT_CODE ...)` block is the complete list of language settings — there is
|
||||
no `CMAKE_BUILD_TYPE`, no `CMAKE_CXX_FLAGS`, no IPO/LTO, and no `target_compile_options`
|
||||
anywhere in the tree. `--config Release` is accepted and ignored by Ninja and Make, so the
|
||||
README's ship incantation produces a binary with no `-O` flag at all, on a tree whose
|
||||
@@ -214,7 +216,7 @@ meaning the same edit covers both. **Those edits still get made in their shared
|
||||
noted as shared** — a `#ifdef _WIN32` / `#else` that is right for both costs nothing extra
|
||||
and does not require a mac. What is out is: macOS as a phase deliverable, any macOS
|
||||
verification, the `swell-modstub.mm`-under-a-CXX-only-`project()` question (root
|
||||
`CMakeLists.txt:26` is `LANGUAGES CXX`), signing and notarization, and the macOS-only
|
||||
`CMakeLists.txt`'s `project(...)` call is `LANGUAGES CXX`), signing and notarization, and the macOS-only
|
||||
half of L2-11 (`normalizeSlashes` under-folds on case-insensitive APFS — a real pre-existing
|
||||
defect this phase surfaces and does not own).
|
||||
|
||||
@@ -283,7 +285,7 @@ Route B3a (Λ-D2) is the ruled route. It must be reached without the stub's own
|
||||
|
||||
1. **Never define `SWELL_LOAD_SWELL_DYLIB`.** Compile
|
||||
`swell-modstub-generic.cpp` in its default branch — the same branch the extension already
|
||||
uses (`src/app/CMakeLists.txt:91–92`) — which exports `SWELL_dllMain(hInst, callMode,
|
||||
uses (`src/app/CMakeLists.txt`'s `else()` (Linux) branch's `target_sources`/`target_compile_definitions` pair) — which exports `SWELL_dllMain(hInst, callMode,
|
||||
GetFunc)` (`:135`) and calls `doinit` on the pointer it is handed. The whole file is
|
||||
inside `#ifdef SWELL_PROVIDED_BY_APP` (`:21`), so the VST3 target must define that
|
||||
symbol too; today it does not.
|
||||
@@ -422,8 +424,8 @@ audits and verified in Λ-W3; T4 is verifiable on the current box.
|
||||
**Goal.** The extension compiles and links under GCC/Clang, and when it refuses to load it
|
||||
says why instead of vanishing.
|
||||
|
||||
**Surface boundary — owns:** `src/shell/panel/draw_kit.cpp` (`loadFont`, `:70–77`),
|
||||
`src/shell/actions/instrument_drop_win.cpp` (`writeTempPreset`, `:50–61`), `src/app/main.cpp`
|
||||
**Surface boundary — owns:** `src/shell/panel/draw_kit.cpp` (`loadFont`),
|
||||
`src/shell/actions/instrument_drop_win.cpp` (`writeTempPreset`), `src/app/main.cpp`
|
||||
(the `REAPERAPI_LoadAPI` failure branch only). **Does not own:** any `CMakeLists.txt`,
|
||||
`panel_window.cpp`, or any `core/` file.
|
||||
|
||||
@@ -432,7 +434,7 @@ says why instead of vanishing.
|
||||
**Do not add `windows.h`** (L2-01's stated direction). The family bits are advisory to
|
||||
Windows' font mapper and meaningless to fontconfig.
|
||||
- Replace `GetCurrentProcessId()` with a platform-neutral uniqueness source behind a guard;
|
||||
the atomic counter at `:52` already carries the intra-process half (L2-02).
|
||||
the atomic counter in `writeTempPreset` already carries the intra-process half (L2-02).
|
||||
- On the load-failure branch, either switch `main.cpp` to `REAPERAPI_MINIMAL` plus an
|
||||
explicit `WANT` list — the pattern `panel_window.cpp` and `panel_audition.cpp` already
|
||||
use — or keep the full load and print the failure count via
|
||||
@@ -507,8 +509,8 @@ only; the `WIN32` gate is Λ-W6-T1's), `README.md` and root `CLAUDE.md` §"Build
|
||||
**Goal.** The docked bank panel opens on Linux, and if it ever fails to, it says so.
|
||||
|
||||
**Surface boundary — owns:** `src/shell/panel/panel_window.cpp` (the `CreateDialogParam`
|
||||
call at `:135–137`, the dialog proc's platform contract, the drop-accept opt-in at
|
||||
`:145–150`), `src/resource.rc`, `src/resource.h`, and — **under the resgen route only** —
|
||||
call and its `if (!g_panel.hwnd) return;` guard, both in `openPanel`, the dialog proc's platform contract, the drop-accept opt-in
|
||||
also in `openPanel`), `src/resource.rc`, `src/resource.h`, and — **under the resgen route only** —
|
||||
one `target_sources` line in `src/app/CMakeLists.txt`'s `else()` branch plus a new
|
||||
include-shim TU. **Does not own:** any other panel TU, `draw_kit`, or any CMake target
|
||||
property.
|
||||
@@ -685,7 +687,7 @@ as the Linux defaults, following SWELL's own no-fontconfig fallback list
|
||||
(LiberationSans/DejaVuSans, LiberationMono/DejaVuSansMono) as precedent. **One code path**,
|
||||
shared with macOS's eventual San Francisco/Menlo (Λ-D4: made in shared form, not verified).
|
||||
|
||||
The subtlety worth carrying into the work: `draw_kit.cpp:74`'s `if (!hf) return` guard does
|
||||
The subtlety worth carrying into the work: `draw_kit.cpp`'s `loadFont`'s `if (!hf) return` guard does
|
||||
**not** catch the failure mode here. SWELL's `CreateFont` always returns a non-null handle
|
||||
even when the face never resolved — the failure is recorded internally as a null
|
||||
`typedata`, not as a null return. So a wrong or missing face is not observable at the call
|
||||
@@ -706,7 +708,7 @@ cosmetic or a readability regression. **Discharges:** L2-09.
|
||||
**Goal.** The build's source list stops relying on every TU's own `#ifdef` discipline, and
|
||||
the invariants Linux weakens are stated where a reviewer will read them.
|
||||
|
||||
**Surface boundary — owns:** the `target_sources` list in `src/app/CMakeLists.txt:8–51`
|
||||
**Surface boundary — owns:** the `add_library(reaper_reasampler MODULE ...)` source list in `src/app/CMakeLists.txt`
|
||||
(the *list*; the property blocks are Λ-W2-T2's), any new platform-sibling TU the sweep
|
||||
showed was needed, `src/shell/actions/drag_out_win.h`'s invariant comment, and the
|
||||
corresponding `src/shell/**/CLAUDE.md` invariant passages. **Does not own:** any behaviour
|
||||
@@ -718,7 +720,7 @@ change in a shipped code path.
|
||||
`arrange_drop_win.cpp` and `instrument_drop_win.cpp` are `_win`-suffixed for the surface
|
||||
they serve, not for a platform dependency, and the audits found them portable by
|
||||
inspection — confirm against the actual compile rather than re-inspecting.
|
||||
- **L2-10** — make `drag_out_win.h:7–11` the doc a Linux reviewer is pointed at, and treat
|
||||
- **L2-10** — make `drag_out_win.h`'s file-header invariant comment the doc a Linux reviewer is pointed at, and treat
|
||||
"MOVE is structurally impossible" as a Windows-scoped claim. **The wording is Λ-F3's
|
||||
ruling**; the edit is this track's regardless of which way it goes.
|
||||
- **L2-11** — no Linux action. If the predicate is touched at all it becomes
|
||||
@@ -808,7 +810,7 @@ modstub TU), and the three D5 passages in `src/core/instrument/CLAUDE.md`,
|
||||
includes `<windows.h>` with no `SMTG_OS_*` guard; `linuxmain.cpp` exports `ModuleEntry`
|
||||
and `ModuleExit`, **both mandatory** — the SDK's own loader refuses the module without
|
||||
either. Both files are already vendored; this is a source swap plus a platform `if()`.
|
||||
- **Split the `WIN32 AND EXISTS` conjunction** at `src/shell/instrument/CMakeLists.txt:9`.
|
||||
- **Split the `WIN32 AND EXISTS` conjunction** in `src/shell/instrument/CMakeLists.txt`'s `if(WIN32 AND EXISTS ...)` gate.
|
||||
The `EXISTS` half stays (a fresh clone with no VST3 slice must still configure); the
|
||||
`WIN32` half becomes a Windows-or-Linux predicate.
|
||||
- **B2** — the artifact becomes a directory:
|
||||
@@ -1001,7 +1003,7 @@ the strict reading:
|
||||
| File | Tracks | Nature |
|
||||
|---|---|---|
|
||||
| `src/app/CMakeLists.txt` | Λ-W2-T2 (property + platform blocks), Λ-W2-T3 (one `target_sources` line, **resgen route only**), Λ-W4-T3 (the source list), Λ-W5-T1 (the `install()` rule) | Four disjoint regions of one file. Λ-W2-T2 and Λ-W2-T3 are the only pair in the same wave; one line each. |
|
||||
| `src/shell/panel/draw_kit.cpp` | Λ-W2-T1 (`loadFont`'s `CreateFont` args, `:73`), Λ-W4-T2 (the five call sites, `:154–158`) | Different waves. |
|
||||
| `src/shell/panel/draw_kit.cpp` | Λ-W2-T1 (`loadFont`'s `CreateFont` args), Λ-W4-T2 (`kitFontsInit`'s five `loadFont` call sites) | Different waves. |
|
||||
| `src/shell/instrument/CMakeLists.txt` | Λ-W2-T2 (thread linkage), Λ-W6-T1 (gate, entry point, bundle, install), Λ-W7-T1 (one added TU) | Different waves. |
|
||||
| `src/shell/panel/panel_window.cpp` | Λ-W2-T3 alone | **Deliberately not split.** The L2-06 diagnostic and the Λ-01 resource route are the same function; under the resource-id-0 route they are the same *line*. Splitting them would be semantic contention. |
|
||||
| `src/shell/instrument/editor_platform.cpp` | Λ-W6-T2 (the refusal branch), Λ-W8-T1 (the real branch) | Different waves; the second replaces the first's computation without touching its call sites. |
|
||||
@@ -1125,7 +1127,7 @@ carried. Nothing is gated on it before Λ-W5.
|
||||
0, which creates an opaque child window, provided a `WNDPROC` returning `LRESULT` (cast to
|
||||
`DLGPROC`) is passed instead of a real `DLGPROC`. The implementation confirms both halves —
|
||||
`swell-dlg-generic.cpp` skips the resource lookup entirely when `resid` is 0. And
|
||||
`IDD_BANK_PANEL` is precisely the case it was written for: `src/resource.rc:18–22` is a
|
||||
`IDD_BANK_PANEL` is precisely the case it was written for: `src/resource.rc`'s `IDD_BANK_PANEL` dialog block is a
|
||||
`WS_CHILD` dialog with an empty `BEGIN`/`END` body and zero controls, whose own header
|
||||
comment says "the bank_panel shell owns every pixel and draws the sample grid with LICE in
|
||||
`WM_PAINT`". **Taking this route deletes the entire resgen pipeline from the non-Windows
|
||||
|
||||
@@ -1117,7 +1117,7 @@ Addendum is the *why*; those are the *what/how*.
|
||||
|
||||
**Framing.** Folds one more control into the S-VIEW redesign: a **visual velocity → amp
|
||||
transfer-curve editor**. Today the engine maps velocity to gain *linearly* (`velocityGain_ =
|
||||
velocity / 127.0`, `sampler_core.cpp:261`), applied once at note-on in `Voice::start()`. Daniel
|
||||
velocity / 127.0`, `Voice::start()`), applied once at note-on in `Voice::start()`. Daniel
|
||||
wants that mapping to become an **editable transfer curve** — a bezier from a default flat line to
|
||||
an arbitrary multi-point curve — so velocity dynamics are fully shapeable per sound.
|
||||
|
||||
@@ -1181,7 +1181,7 @@ a LICE shell that draws handles and routes the mouse).
|
||||
evaluation is called at note-on, not per frame (see call 4).
|
||||
|
||||
4. **Voice-engine application point → `Voice::start()`, replacing the linear `velocity/127`.**
|
||||
Confirmed from source: `sampler_core.cpp:261` computes `velocityGain_ = velocity / 127.0` **once
|
||||
Confirmed from source: `Voice::start()` computes `velocityGain_ = velocity / 127.0` **once
|
||||
at note-on** inside `Voice::start()`; the per-frame render path (`advanceFrame`, line 408:
|
||||
`gain = amp * velocityGain_`) then just multiplies the cached scalar. So the transfer curve
|
||||
slots in at exactly one line: `velocityGain_ = curve.eval(velocity)` at note-on — **off the
|
||||
|
||||
@@ -199,7 +199,7 @@ it — three small pure additions and one bounded seam:
|
||||
|
||||
1. **A render destination that is not the bank.** `OfflineRenderBackend::capture`
|
||||
derives its output path from `deriveBankPaths(projectDir, …)` unconditionally
|
||||
(`capture.cpp:417`) and points `RENDER_FILE` at the bank folder. Nothing about
|
||||
(`capture.cpp`'s `OfflineRenderBackend::capture`) and points `RENDER_FILE` at the bank folder. Nothing about
|
||||
that is parameterized. The alternative — render into the bank and then move the
|
||||
file out — was rejected: it puts a transient, unindexed, unowned file inside the
|
||||
folder prune enumerates, which is exactly the file class the ownership rule exists
|
||||
|
||||
@@ -45,7 +45,7 @@ below:
|
||||
|
||||
Two sharp edges follow directly and recur throughout this note:
|
||||
|
||||
- **The `STABLE_FOREVER_STRING` command-id contract** (CLAUDE.md; `main.cpp:41`,
|
||||
- **The `STABLE_FOREVER_STRING` command-id contract** (CLAUDE.md; `app_version.h`'s `commandIdPrefix()`,
|
||||
prefix `CEREBELLUM_REASAMPLER_`). Command-id strings are minted once and **never
|
||||
changed after shipping** — user keybindings key off them. Two coexisting binaries
|
||||
that register the *same* id strings collide in REAPER's Actions list.
|
||||
@@ -67,9 +67,9 @@ allowed to touch.
|
||||
|
||||
## What we have today
|
||||
|
||||
- No version anywhere. `CMakeLists.txt:2` is `project(reaper_reasampler LANGUAGES
|
||||
- No version anywhere. `CMakeLists.txt` is `project(reaper_reasampler LANGUAGES
|
||||
CXX)` — no `VERSION`. The binary announces itself only as `"ReaSampler loaded.\n"`
|
||||
to the console (`main.cpp:960`). There is no number a user, a bug report, or a
|
||||
to the console (`main.cpp`). There is no number a user, a bug report, or a
|
||||
future migration can key off.
|
||||
- The natural user-visible readout already exists: the docked LICE bank panel, and
|
||||
the console (`ShowConsoleMsg`). A version has cheap homes; none is wired.
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# DAW verification — bank-package transfer across machines
|
||||
|
||||
What a DAW pass must establish for `.rsbank` export and import, and the exact strings or
|
||||
counts to read off. The unit corpus (`tests/fixtures/package_compat/`) already proves the
|
||||
version ladder, the truncation verdicts and the hostile-name refusals against frozen
|
||||
bytes. **Nothing below is covered by it**: every cell here depends on a real REAPER
|
||||
session, a real file dialog, or a genuine second machine.
|
||||
|
||||
**Build to use.** Release, installed into `UserPlugins/`, REAPER restarted — extensions
|
||||
load at startup only. Note the version the *About*/version action reports; §5 needs it.
|
||||
|
||||
**Machines to use.** Two: **A** (the source) and **B** (the destination). B must be a
|
||||
different machine, or at minimum a different user account with its own REAPER resource
|
||||
path and its own projects folder — the point is that no absolute path from A can resolve
|
||||
on B. A USB stick, a network share, or a cloud folder are all acceptable transports.
|
||||
|
||||
**Projects to use.** On A: one **saved** project with a bank holding at least **three**
|
||||
samples, at least one of them audibly distinct from the others, and at least one whose
|
||||
display name carries a non-ASCII character (e.g. `Café hit`). On B: one **saved**,
|
||||
otherwise empty project.
|
||||
|
||||
---
|
||||
|
||||
## 1. Export writes one file and touches nothing else
|
||||
|
||||
On A, right-click the bank's header in the docked panel → **Export as package...** (or
|
||||
run *ReaSampler: export active bank as package*). Accept the suggested file name.
|
||||
|
||||
Read off:
|
||||
|
||||
- The console shows `ReaSampler export: wrote 3 entry/entries (N bytes) to <path>`, with
|
||||
the entry count matching the bank.
|
||||
- A single `.rsbank` file exists at that path. **No `.rsbanktmp` sibling remains** — a
|
||||
leftover temp file means the atomic rename did not complete.
|
||||
- The bank's card count, the bank folder's file count, and the project's dirty flag are
|
||||
all **unchanged**. An export writes no ext state and opens no undo point, so REAPER
|
||||
must not consider the project modified by it alone.
|
||||
- Nothing was added to the arrange view.
|
||||
|
||||
## 2. The transfer itself — the claim no unit test can make
|
||||
|
||||
Copy the `.rsbank` to B by whatever transport you chose. Do **not** copy the project, the
|
||||
bank folder, or anything else.
|
||||
|
||||
On B, open the empty saved project. Panel bank menu → **Import bank package...** (or run
|
||||
*ReaSampler: import bank package (.rsbank)*), and choose the transferred file.
|
||||
|
||||
Read off:
|
||||
|
||||
- A message box: `Imported 3 sample(s) into a new bank: "<bank name>".`
|
||||
- The console block repeats that line and ends with `One undo removes the imported bank
|
||||
and its entries. It does NOT delete the imported files ...`.
|
||||
- The panel shows a **new** bank with the same display name and the same number of cards,
|
||||
**in the same order** as on A.
|
||||
- B's bank folder holds three new files. The non-ASCII display name from A renders
|
||||
correctly on the card — a mangled name here means the UTF-8 path/name conversion broke
|
||||
in transit.
|
||||
- **Audition each card.** They must sound like their counterparts on A. This is the whole
|
||||
claim: the audio survived a machine boundary with no shared path.
|
||||
- Press **Ctrl-Z once**. The imported bank and its entries disappear in one step. The
|
||||
three files remain in B's bank folder (that is stated in the console block above, and is
|
||||
the designed behaviour — a prune reclaims them). Redo to continue.
|
||||
|
||||
## 3. Re-importing the same package never overwrites
|
||||
|
||||
Still on B, import the **same** file a second time.
|
||||
|
||||
Read off:
|
||||
|
||||
- A second new bank appears, named with a suffix (`<bank name> 2`), and the box's
|
||||
`(a bank named "<bank name>" already exists in this project)` clause appears in the
|
||||
console block.
|
||||
- B's bank folder now holds **six** files, not three. The console reports
|
||||
`3 file(s) landed under a freshly minted name (the package's own name was already taken
|
||||
in the bank folder). An existing bank file is never overwritten.`
|
||||
- The first imported bank's cards still audition correctly — nothing was replaced under it.
|
||||
|
||||
## 4. Round trip back to the source
|
||||
|
||||
On B, export the imported bank (§1) to a second `.rsbank`. Carry it back to A and import
|
||||
it into A's original project.
|
||||
|
||||
Read off:
|
||||
|
||||
- The import succeeds and lands as a new bank beside the original.
|
||||
- The original bank on A is untouched: same card count, same names, same audio.
|
||||
- Compare the two `.rsbank` files' **sizes**. They will usually differ — entry names,
|
||||
sample ids and the export timestamp are all legitimately re-minted across a trip. The
|
||||
payload bytes are what must survive, and that half is closed by
|
||||
`tests/test_package_round_trip.cpp` against frozen bytes; do **not** treat a size
|
||||
difference here as a defect.
|
||||
|
||||
## 5. The too-new refusal, with the message read verbatim
|
||||
|
||||
This is the direction a user hits when a collaborator is ahead of them, and the message is
|
||||
the only actionable output. Produce it by hand:
|
||||
|
||||
1. Copy the `.rsbank` from §1 to a scratch name.
|
||||
2. Open the copy in a hex editor. Bytes 0–3 are `RSBK`; bytes 4–7 are `formatVersion`
|
||||
little-endian; bytes **8–11** are `minReaderVersion` little-endian.
|
||||
3. Change byte **8** from `01` to `02`, and byte **4** from `01` to `02` (a writer cannot
|
||||
require a reader newer than the format it wrote — leaving `formatVersion` at 1 makes
|
||||
the file incoherent and it will be refused as malformed instead, which is a different
|
||||
cell). Save.
|
||||
4. Import the edited copy.
|
||||
|
||||
Read off — the message box, all four lines:
|
||||
|
||||
```
|
||||
Cannot import this bank package.
|
||||
It was written by ReaSampler <the version noted at the top> and needs package format 2 or newer.
|
||||
This build (<the same version>) reads package format 1.
|
||||
Nothing was imported. Install ReaSampler <the same version> or newer and try again.
|
||||
```
|
||||
|
||||
- The writer version named is the one **this** build stamped in §1 (the hex edit does not
|
||||
touch the semver), so the second and fourth lines will name your own version. That is
|
||||
expected — what is being verified is that all three facts are present and the box
|
||||
appears at all.
|
||||
- **No** new bank, **no** new files in the bank folder, **no** undo point.
|
||||
|
||||
## 6. The truncated-download refusal is a different message
|
||||
|
||||
Copy the §1 package again and delete the last few hundred bytes (any hex editor, or
|
||||
`head -c` / `fsutil` — the exact count does not matter as long as the file is shorter).
|
||||
Import it.
|
||||
|
||||
Read off:
|
||||
|
||||
- The message box reads exactly: `This file is not a readable bank package (corrupt or
|
||||
truncated). Nothing was imported.`
|
||||
- It is **not** the §5 message. Crossing these two is the failure this cell exists to
|
||||
catch — "install a newer build" does not fix a partial download.
|
||||
- No new bank, no new files.
|
||||
|
||||
## 7. Corruption in the middle is caught before anything lands
|
||||
|
||||
Copy the §1 package again and flip a single byte **well past the halfway point** (inside a
|
||||
payload, not the header). Import it.
|
||||
|
||||
Read off:
|
||||
|
||||
- The message box names the offending entry:
|
||||
`This bank package is damaged (entry "<name>" failed its integrity check). Nothing was
|
||||
imported.`
|
||||
- The bank folder gained **no** files at all — not even the entries before the damaged
|
||||
one. Verification runs to completion before the first write, so a damaged package costs
|
||||
no rollback.
|
||||
|
||||
## 8. The unsaved-project refusals
|
||||
|
||||
- On B, File → New Project (do not save). Try to import. Read off:
|
||||
`Save the project before importing a bank package -- an unsaved project has no bank
|
||||
folder to import into.` The file picker must **not** have opened first.
|
||||
- On A, in an unsaved project with no bank, try to export. Read off the console:
|
||||
`ReaSampler export: save the project first -- an unsaved project has no bank folder to
|
||||
read from.`
|
||||
|
||||
## 9. Drag-and-drop reaches the same verb
|
||||
|
||||
On B, drag a `.rsbank` from the file manager onto the docked ReaSampler panel.
|
||||
|
||||
Read off: the same import box as §2, and the same new bank. A `.rsbank` is a whole bank,
|
||||
not audio — it must never land as an item in the arrange view.
|
||||
|
||||
---
|
||||
|
||||
## Recording the result
|
||||
|
||||
For each section, record **pass**, **fail with the string actually seen**, or **not
|
||||
exercised**. §2 and §4 are the load-bearing ones: they are the only cells in this document
|
||||
that involve a real machine boundary, and no unit test can stand in for them.
|
||||
Reference in New Issue
Block a user