docs: spec Phase Ψ — the extension trust pass, seven items across two waves, six tracks, three scheduled invariant amendments
This commit is contained in:
+748
-5
@@ -4,7 +4,10 @@ The post-1.0 roadmap. Seventeen queued items consolidated into overlapping areas
|
|||||||
sequenced into a Phase → Wave → Track hierarchy that implementation specialists can be
|
sequenced into a Phase → Wave → Track hierarchy that implementation specialists can be
|
||||||
dispatched against directly — **plus Phase Γ**, which did not come from those seventeen
|
dispatched against directly — **plus Phase Γ**, which did not come from those seventeen
|
||||||
(it came from a direct interview, 2026-08-01) and is scoped in
|
(it came from a direct interview, 2026-08-01) and is scoped in
|
||||||
`docs/product/instrument-control-surface.md`.
|
`docs/product/instrument-control-surface.md`, **and Phase Ψ**, which likewise did not
|
||||||
|
come from the seventeen: it came from a direct list of seven defects and refinements
|
||||||
|
(Daniel, 2026-08-01) and is specified inline in its own section below — there is no
|
||||||
|
backing product doc for it.
|
||||||
|
|
||||||
## What this doc is, and how it relates to the others
|
## What this doc is, and how it relates to the others
|
||||||
|
|
||||||
@@ -25,8 +28,9 @@ dispatched against directly — **plus Phase Γ**, which did not come from those
|
|||||||
cited section rather than reading a file whole.
|
cited section rather than reading a file whole.
|
||||||
|
|
||||||
**Worktree slug convention:** `p<phase>-w<wave>-t<track>-<slug>`. Greek phase letters
|
**Worktree slug convention:** `p<phase>-w<wave>-t<track>-<slug>`. Greek phase letters
|
||||||
transliterate: **Θ → `th`**, **Ξ → `xi`**, **Γ → `g`**. So Θ-W1-T1 dispatches into
|
transliterate: **Θ → `th`**, **Ξ → `xi`**, **Γ → `g`**, **Ψ → `psi`**. So Θ-W1-T1
|
||||||
`pth-w1-t1-zone-retirement` and Γ-W1-T1 into `pg-w1-t1-knob-interaction-law`.
|
dispatches into `pth-w1-t1-zone-retirement`, Γ-W1-T1 into `pg-w1-t1-knob-interaction-law`,
|
||||||
|
and Ψ-W1-T1 into `ppsi-w1-t1-capture-range-exactness`.
|
||||||
|
|
||||||
## Decision state
|
## Decision state
|
||||||
|
|
||||||
@@ -113,8 +117,11 @@ in Γ-W4-T1 and changes no wave boundary.
|
|||||||
|
|
||||||
## Phase-wide acceptance criteria
|
## Phase-wide acceptance criteria
|
||||||
|
|
||||||
These bind every track in all three phases and are stated once here rather than repeated
|
These bind every track in all four phases and are stated once here rather than repeated
|
||||||
per track. **Phase Γ adds a set of its own**, stated in its phase header.
|
per track. **Phase Γ adds a set of its own**, stated in its phase header. **Phase Ψ adds
|
||||||
|
none** — the structural heuristics, performance guardrails, and product invariants below
|
||||||
|
bind it exactly as written (its phase header states its performance posture against the
|
||||||
|
named hot paths).
|
||||||
|
|
||||||
### Structural (root `CLAUDE.md`, Daniel 2026-07-28)
|
### Structural (root `CLAUDE.md`, Daniel 2026-07-28)
|
||||||
|
|
||||||
@@ -2029,6 +2036,719 @@ value semantics, any deck geometry, or the bake's reset *membership* (W3-T2's).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Phase Ψ — The extension trust pass: exact bounds, disjoint solo surfaces, reachable actions, honest drops, real names, true mono
|
||||||
|
|
||||||
|
**Ships:** capture ranges that mean what was asked — a time selection or razor area over
|
||||||
|
a longer item captures the selection, both scopes; per-mode solo surfaces that cache,
|
||||||
|
clear, and restore across the Design/Arrange switch, with the switch itself refused while
|
||||||
|
the transport runs; the Media-Explorer import action registered into the Media Explorer
|
||||||
|
action section so it can live on that toolbar; a drag-out gesture law that resolves its
|
||||||
|
target from what is actually under the cursor, continuously and reversibly, with no
|
||||||
|
silent no-op release anywhere; captures labeled after their source track instead of the
|
||||||
|
literal `"item"`/`"track"`, surfaced on the panel card; and lossless mono collapse for
|
||||||
|
new captures whose channels are bit-identical.
|
||||||
|
|
||||||
|
**Consolidates: none of the seventeen.** Phase Ψ came from a direct list of seven defects
|
||||||
|
and refinements (Daniel, 2026-08-01). There is no backing product doc — the design
|
||||||
|
content lives inline in the tracks below, and the seven are recorded here as the phase's
|
||||||
|
provenance, cited throughout as Ψ.1–Ψ.7:
|
||||||
|
|
||||||
|
> **Ψ.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.
|
||||||
|
|
||||||
|
**Where the seven land:**
|
||||||
|
|
||||||
|
| Ψ-item | Track | Worktree slug |
|
||||||
|
|---|---|---|
|
||||||
|
| Ψ.7 | Ψ-W1-T1 | `ppsi-w1-t1-capture-range-exactness` |
|
||||||
|
| Ψ.2 + Ψ.3 | Ψ-W1-T2 | `ppsi-w1-t2-mode-switch-discipline` |
|
||||||
|
| Ψ.4 | Ψ-W1-T3 | `ppsi-w1-t3-media-explorer-section` |
|
||||||
|
| Ψ.5 | Ψ-W1-T4 | `ppsi-w1-t4-drop-target-resolution` |
|
||||||
|
| Ψ.1 | Ψ-W2-T1 | `ppsi-w2-t1-capture-naming` |
|
||||||
|
| Ψ.6 | Ψ-W2-T2 | `ppsi-w2-t2-mono-collapse` |
|
||||||
|
|
||||||
|
Ψ.2 and Ψ.3 share one track deliberately: they share one chokepoint — `applyMode`, the
|
||||||
|
sole mode mutator (`shell/view/view.cpp:380-469`) — and one discriminator
|
||||||
|
(`targetModeId != model.activeModeId()`, the test that distinguishes a real switch from a
|
||||||
|
reapply). Splitting them is two tracks fighting over the same function.
|
||||||
|
|
||||||
|
**Concurrency with Γ and Ξ.** Phase Ψ is extension-side. Γ and Ξ-W3 live in
|
||||||
|
`core/instrument/` + `shell/instrument/`; the surfaces are disjoint with ONE named
|
||||||
|
exception — Ψ-W2-T2 touches `shell/instrument/processor_reload.cpp` for a stale comment
|
||||||
|
and an index/file consistency check, and that touch is bounded to the minimum in its
|
||||||
|
surface boundary precisely because Γ is live in that directory.
|
||||||
|
|
||||||
|
**Three invariant amendments are DELIVERABLES of this phase, not asides.** Each is
|
||||||
|
scheduled in — and an acceptance criterion of — its owning track. This spec schedules
|
||||||
|
them; the implementing track performs them:
|
||||||
|
|
||||||
|
1. **The never-touch-solo rule** (Ψ-W1-T2): `src/shell/view/CLAUDE.md:14-17`,
|
||||||
|
`src/core/view/CLAUDE.md:10`, and `docs/product/design-view.md:160-165` + `:588-592`.
|
||||||
|
Ψ.2 requires writing `I_SOLO`; the amended form is stated in the track. A track that
|
||||||
|
lands solo writes without the amendment reads as an invariant breach in review.
|
||||||
|
2. **The action-registration contract** (Ψ-W1-T3): root `CLAUDE.md`
|
||||||
|
§"REAPER extension contract" documents only the main-section 4-step pattern; the
|
||||||
|
second, non-main mechanism (`custom_action` + `hookcommand2` + `-custom_action`
|
||||||
|
mirror) must be added beside it.
|
||||||
|
3. **The channel-count-preserved rule** (Ψ-W2-T2): root `CLAUDE.md:208` — "channel count
|
||||||
|
preserved (no silent stereo fold)" — is contradicted in text (not in spirit) by a
|
||||||
|
lossless bit-identical collapse, and was already untrue in the other direction (a mono
|
||||||
|
source renders at `RENDER_CHANNELS=2` today). The amended form is stated in the track.
|
||||||
|
|
||||||
|
**Performance posture.** Every Ψ surface is cold — per-capture, per-click,
|
||||||
|
per-mode-switch, per-mouse-move. None of the named hot paths (peaks envelope compute,
|
||||||
|
audition, realtime-capture tick, instrument `process()`) is touched. Two disciplines
|
||||||
|
carry anyway: the realtime tick's single-pointer-test idle fast path is untouched by
|
||||||
|
Ψ-W1-T1's render work, and Ψ-W1-T4 keeps the inside-client drag path free of SDK
|
||||||
|
hit-tests, exactly as today (`panel_drag.cpp:273-274` — "costs nothing on the common
|
||||||
|
internal-drag path").
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ψ-W1 — Exact bounds, disciplined switches, reachable actions, resolved drops
|
||||||
|
|
||||||
|
**Depends on:** nothing in this phase. **Four tracks, disjoint by surface:**
|
||||||
|
|
||||||
|
| Track | Owns |
|
||||||
|
|---|---|
|
||||||
|
| **T1** `capture-range-exactness` | `core/capture/render_settings`, `shell/capture/capture.cpp`'s render-configuration block, `shell/capture/scope_resolve` (source-mode selection only), `tests/test_render_settings.cpp` |
|
||||||
|
| **T2** `mode-switch-discipline` | `core/view/view_mode_model` (solo cache), `shell/view/view.cpp` (`applyMode` seams + WANT block), `shell/actions/design_view_actions.cpp` (refusal feedback), `shell/panel/panel_input.cpp` **footer mode-segment block only** (`:300-309`), the mode segment's disabled state in `core/ui/footer_bar` + `shell/panel/panel_render.cpp` |
|
||||||
|
| **T3** `media-explorer-section` | `shell/actions/ingest.cpp` (register/dispatch/unregister), **the registration block in `src/app/main.cpp`** (the `hookcommand2` hook + unload mirror), the root-`CLAUDE.md` contract amendment |
|
||||||
|
| **T4** `drop-target-resolution` | `core/ui/drag_out`, `core/wire/instrument_drop`, `shell/panel/panel_drag.cpp`, `shell/actions/instrument_drop_win.cpp`, `shell/actions/drag_out_win.cpp` (hand-off timing), `tests/test_drag_out.cpp` |
|
||||||
|
|
||||||
|
**Why these four are parallel.** T1 is capture core/shell; T2 is view core/shell plus one
|
||||||
|
fenced block of `panel_input.cpp`; T3 is action registration; T4 is the drag chain. No
|
||||||
|
two tracks own the same function anywhere.
|
||||||
|
|
||||||
|
**Two shared-file adjacencies, named rather than discovered at merge.**
|
||||||
|
(a) `shell/panel/panel_input.cpp`: T2 owns the footer mode-segment block (`:300-309`)
|
||||||
|
only; T4 may touch the drag-arm block (`:382-402`) only. Separate blocks — textual
|
||||||
|
adjacency, not semantic contention; whichever lands second rebases.
|
||||||
|
(b) **`src/app/main.cpp` is T3's exclusively.** T4 must not touch `main.cpp` — its whole
|
||||||
|
redesign lives in the drag chain and registers nothing. T2 also does not touch it: the
|
||||||
|
playback gate lives inside `applyMode` conditioned on the real-switch discriminator, so
|
||||||
|
`main.cpp:173`'s project-load reapply passes through unchanged.
|
||||||
|
|
||||||
|
#### Ψ-W1-T1 — `capture-range-exactness`
|
||||||
|
|
||||||
|
**Goal.** A capture over a time selection or razor area on a source item substantially
|
||||||
|
longer than the selection produces exactly the requested range — both scopes, no
|
||||||
|
whole-item widening. (Ψ.7)
|
||||||
|
|
||||||
|
**What is already correct, so the fix does not wander.** Range resolution is correct end
|
||||||
|
to end: `scope_resolve.cpp:100-112` (`resolveRange`) → razor union
|
||||||
|
(`render_settings.cpp:135-170`) or `GetSet_LoopTimeRange` (`scope_resolve.cpp:35-40`),
|
||||||
|
passed verbatim (`capture_orchestrator.cpp:221-222`), landed as
|
||||||
|
`RENDER_BOUNDSFLAG=0` + exact `RENDER_STARTPOS`/`RENDER_ENDPOS`
|
||||||
|
(`capture.cpp:320-322`). There is no item-bounds fallback in `RunCapture`. **The widening
|
||||||
|
decision point is the source-mode bit**: item scope maps to
|
||||||
|
`kRenderSelItems (&32) | kRenderSingleFile` (`render_settings.cpp:82-86`), and the repo's
|
||||||
|
own comments treat `&32` as item-extent-driven (`capture_batch.cpp:64-66`, `:98-99` —
|
||||||
|
the entire reason batch transiently selects one item per render). The working inference —
|
||||||
|
**unverified without a DAW** — is that REAPER's "selected media items" render source
|
||||||
|
overrides the custom time bounds. Track scope uses `&128` (selected tracks via master), a
|
||||||
|
normal time-bounded render, and **no code path was found that widens it**.
|
||||||
|
|
||||||
|
**Behavior.**
|
||||||
|
- **The track opens with a DAW repro matrix, before any code change** `[verify]`:
|
||||||
|
item scope × {time selection, razor} and track scope × {time selection, razor}, over a
|
||||||
|
source item substantially longer than the selection, **tail mode None**. This confirms
|
||||||
|
the `&32`-overrides-bounds inference, and disambiguates Ψ.7's "item/track both" claim —
|
||||||
|
the candidates are: the reporter saw only the item scope; or a non-`None` panel tail
|
||||||
|
mode was active (`capture_orchestrator.cpp:217`; Auto adds an 8 s window,
|
||||||
|
`render_settings.cpp:28-37`). If track scope reproduces with tail None, the analysis
|
||||||
|
above is wrong and the track says so before proceeding.
|
||||||
|
- **The fix architecture is a design call at implementation review** `[propose]`, with
|
||||||
|
the two candidates named now:
|
||||||
|
- **(a) Re-source the ranged item capture.** When item scope carries an explicit range,
|
||||||
|
render time-bounded (e.g. selected-track source `&128` over the item's own track,
|
||||||
|
with the item-scope FX-bypass plan unchanged — it already neutralizes the track's own
|
||||||
|
track-FX chain and everything above). Semantic edge to resolve: another item on the
|
||||||
|
same track overlapping the range would now be audible in the capture, where `&32`
|
||||||
|
excluded it.
|
||||||
|
- **(b) Render-then-trim.** Keep `&32` (only the selected item's audio, REAPER's own
|
||||||
|
semantics) and trim the rendered file to the requested range afterward with the pure
|
||||||
|
`wav_codec` tools, sample-exactly (offset = requested start − rendered start at the
|
||||||
|
file's sample rate). Precedent: `trimAutoTailInPlace`
|
||||||
|
(`capture_realtime_finalize.cpp:55-125`). Cost: renders more than needed; requires
|
||||||
|
knowing the rendered file's start time exactly.
|
||||||
|
Whichever candidate lands, the choice is judged against the acceptance criteria below,
|
||||||
|
not against convenience.
|
||||||
|
- **Ripple surfaces if the item-scope source mode changes**, enumerated so none is
|
||||||
|
discovered late: `capture_batch.cpp:98-99`'s select-one mechanism, item-scope FX
|
||||||
|
fingerprinting reading `CountSelectedMediaItems` (`scope_resolve.cpp:199-207`), and
|
||||||
|
`core/model/provenance` recipes storing `sourceMode` as an int
|
||||||
|
(`scope_resolve.cpp:186`) — a recorded recipe must replay to the same audio.
|
||||||
|
- **Batch semantics are deliberately different and stay so:** `RunBatchCaptureItems` is
|
||||||
|
one sample per item at item extent (`capture_batch.cpp:177-179`) — that is its meaning,
|
||||||
|
not this defect. Stated so nobody "fixes" it.
|
||||||
|
|
||||||
|
**Acceptance criteria.**
|
||||||
|
- **The bounds equality, stated as a number:** the captured file's frame count equals the
|
||||||
|
requested range's duration at the project sample rate — exactly, no rounding — for
|
||||||
|
BOTH scopes, with both a time selection and a razor area, over a source item
|
||||||
|
substantially longer than the selection, tail None. `[verify — DAW]`
|
||||||
|
- The null test holds for a ranged item capture: re-inserted at the range start, it nulls
|
||||||
|
against the source over the range. `[verify — DAW]`
|
||||||
|
- Bit-identical repeats hold for the ranged capture.
|
||||||
|
- Full-item captures (no time selection/razor, or bounds equal to the item) are
|
||||||
|
byte-identical to today's output.
|
||||||
|
- Recapture-from-source of a ranged capture reproduces it (the provenance recipe stays
|
||||||
|
truthful under the chosen fix).
|
||||||
|
- `tests/test_render_settings.cpp` pins the chosen mapping (today `:44-47` asserts only
|
||||||
|
that the `&32` bit is chosen — never its bounds interaction); any new pure trim/window
|
||||||
|
arithmetic lands in `core/capture` with unit tests.
|
||||||
|
- **DAW-verification obligation:** Daniel observes the four-cell matrix above, plus one
|
||||||
|
razor-union case (two disjoint razor areas — the union rule at
|
||||||
|
`render_settings.cpp:135-170` is bounds-driven and must not regress).
|
||||||
|
- Root `CLAUDE.md:208`'s **"Exact bounds"** invariant needs no amendment — this track
|
||||||
|
makes the code honor it; the invariant was always the spec.
|
||||||
|
|
||||||
|
**Open questions.** `[verify]` the override inference and the track-scope repro (the
|
||||||
|
matrix above). `[propose]` fix candidate (a) vs (b), including (a)'s overlapping-item
|
||||||
|
semantic edge and (b)'s rendered-start-time derivation.
|
||||||
|
|
||||||
|
#### Ψ-W1-T2 — `mode-switch-discipline`
|
||||||
|
|
||||||
|
**Goal.** Switching the active mode caches and clears the outgoing mode's solo state and
|
||||||
|
restores the incoming mode's — disjoint solo surfaces per mode — and the switch itself is
|
||||||
|
refused, visibly, while the transport is playing or recording. (Ψ.2 + Ψ.3)
|
||||||
|
|
||||||
|
**The invariant amendment comes first, because without it this track is a breach.** The
|
||||||
|
never-touch-solo rule is stated three times: `src/shell/view/CLAUDE.md:14-17` ("Never
|
||||||
|
touches master or `B_MUTE`/`I_SOLO`… User mute/solo survives every toggle untouched"),
|
||||||
|
`src/core/view/CLAUDE.md:10`, and `docs/product/design-view.md:160-165` (framed as the
|
||||||
|
analog of capture's non-destructive invariant) + `:588-592`. **The amended form:** the
|
||||||
|
tool never *loses* the user's solo state — solo is cached per mode on a real switch and
|
||||||
|
restored verbatim on return, the snapshot sense of non-destructive, exactly as the
|
||||||
|
park/restore snapshots already treat visibility and FX state. `B_MUTE` stays untouched
|
||||||
|
absolutely; the master track stays untouched absolutely (and survives for free —
|
||||||
|
`handleByGuid` is built from `GetTrack`'s index space, which excludes master,
|
||||||
|
`view.cpp:90-91`). All three files are amended by this track.
|
||||||
|
|
||||||
|
**Surface boundary — owns:** `core/view/view_mode_model` (the solo cache: storage,
|
||||||
|
serialize/deserialize, reconcile participation — mirroring the existing `snapshots_` map,
|
||||||
|
`view_mode_model.h:284-358`), `shell/view/view.cpp` (the `applyMode` seams and its
|
||||||
|
`REAPERAPI_WANT` block, `:21-41`), `shell/actions/design_view_actions.cpp` (refusal
|
||||||
|
feedback on the action path), `shell/panel/panel_input.cpp`'s footer mode-segment block
|
||||||
|
ONLY (`:300-309`), the segment's disabled state in `core/ui/footer_bar` (pure predicate,
|
||||||
|
transport bool passed IN from the shell) and its paint in `shell/panel/panel_render.cpp`
|
||||||
|
(`:110-125` region), the three invariant-amendment files, `tests/test_view_mode_model.cpp`.
|
||||||
|
**Must not touch:** `panel_drag.cpp` (T4's), `src/app/main.cpp` (the gate's discriminator
|
||||||
|
makes the project-load reapply pass through unchanged), `B_MUTE`, master, and the
|
||||||
|
park/restore planner's existing semantics.
|
||||||
|
|
||||||
|
**Behavior.**
|
||||||
|
- **The discriminator is the hinge for both halves.** `applyMode` is also the reapply
|
||||||
|
path — called with `targetModeId == activeModeId()` from `reapplyActiveMode`
|
||||||
|
(`design_view_actions.cpp:106-108`, reached from tag/untag/show-both at
|
||||||
|
`:187,197,206`) and from project load (`main.cpp:173`). A real switch is
|
||||||
|
`targetModeId != model.activeModeId()`. **Only a real switch caches/clears/restores
|
||||||
|
solo, and only a real switch is playback-gated** — reapply during playback (tagging,
|
||||||
|
project load) keeps working, and there is no cache/clear/restore flicker on reapply.
|
||||||
|
- **Solo cache semantics.** On a real switch: read `I_SOLO` for every track in the live
|
||||||
|
enumeration (`handleByGuid`, keyed by GUID — all non-master tracks, not just managed
|
||||||
|
leaves: a mode's solo surface is the whole project as seen in that mode); store the
|
||||||
|
non-zero values into the model keyed by the OUTGOING mode id (`model.activeModeId()`
|
||||||
|
is still the outgoing mode at the capture seam); write `I_SOLO = 0` on every track that
|
||||||
|
had it set; after `setActiveMode` (`view.cpp:455`), restore the INCOMING mode's cached
|
||||||
|
entries verbatim and consume them (clear-on-restore, mirroring
|
||||||
|
`storeSnapshot`/`clearSnapshot`). **Values are cached and restored verbatim as the raw
|
||||||
|
`I_SOLO` int — never collapsed to a boolean** (solo-in-place and safe-solo variants
|
||||||
|
survive the round trip).
|
||||||
|
- **Seams in `applyMode`:** capture after `handleByGuid` is populated (`:386`) and before
|
||||||
|
`Undo_BeginBlock2` (`:399`); the clear and restore writes both ride the existing single
|
||||||
|
undo block (clear at its head, restore after `setActiveMode` `:455` and before
|
||||||
|
`TrackList_AdjustWindows` `:460`) — one mode toggle stays one Ctrl-Z.
|
||||||
|
- **Storage is pure, shell touches only the API pair.** The cache lives in
|
||||||
|
`ViewModeModel` beside `snapshots_` with the same `deserialize(serialize(x)) == x`
|
||||||
|
contract (`view_mode_model.h:347-350`); only the
|
||||||
|
`GetMediaTrackInfo_Value`/`SetMediaTrackInfo_Value` pair lives in `view.cpp`, exactly
|
||||||
|
as `snapshotTrack` (`:121-134`) does. `core/` includes no REAPER headers.
|
||||||
|
- **Reconcile participation — decided: the solo cache prunes on dead GUIDs**, like
|
||||||
|
snapshots and unlike membership (`view.cpp:389-395`). The hazard it removes: a stale
|
||||||
|
entry restoring onto a reused GUID. A pruned entry is simply lost solo state for a
|
||||||
|
track that no longer exists — correct.
|
||||||
|
- **Persistence rides the one blob.** `ViewModeModel::serialize()` →
|
||||||
|
`SetProjExtState(proj, "reasampler", "view_state")` (`ext_state_io.cpp:156-158`, key
|
||||||
|
`ext_keys.h:31`). `parseModel` skips unknown keys (`view_mode_model.cpp:603`), so the
|
||||||
|
new key is backward-safe: an older build reading newer state drops the solo cache
|
||||||
|
silently and nothing else breaks. Stated, accepted.
|
||||||
|
- **The playback gate.** Inside `applyMode`, when the discriminator says real switch and
|
||||||
|
`GetPlayStateEx(proj) & (1|4)` (bit 1 playing, bit 4 recording — the precedent is
|
||||||
|
`capture_realtime_shell.cpp:284`, project-scoped `*Ex` variants deliberately,
|
||||||
|
comment `:192`), refuse before touching anything: return `false`, no partial apply
|
||||||
|
(the same fail-closed shape as the mode-exists guard at `:381`). Add
|
||||||
|
`REAPERAPI_WANT_GetPlayStateEx` to `view.cpp`'s WANT block.
|
||||||
|
- **The refusal is visible, not a silent no-op.** The footer `[Arrange|Design]` segment
|
||||||
|
renders a disabled state while the transport runs, following the one existing disabled
|
||||||
|
precedent end to end: pure predicate (`mode_enable`-style, transport bool passed IN so
|
||||||
|
`core/ui` stays REAPER-free) → layout/paint state → no-op click. The action path's
|
||||||
|
refusal (`doToggleMode`/`doActivateMode`) reads `applyMode`'s `false` and skips
|
||||||
|
persist/invalidate.
|
||||||
|
- **The panel's direct-call divergence is closed in this track — decided.** The footer
|
||||||
|
segment click calls `applyMode` directly (`panel_input.cpp:300-309`, passing `nullptr`
|
||||||
|
project), skipping the `persistViewState()` + `bankPanelInvalidate()` the action path
|
||||||
|
does (`design_view_actions.cpp:165-180`) — a pre-existing defect: a panel-initiated
|
||||||
|
switch does not persist. The segment click now routes through `Main_OnCommand` of the
|
||||||
|
activate actions, exactly as the bottom-bar tag buttons already do
|
||||||
|
(`panel_input.cpp:74-75`). One path, one gate, one persist.
|
||||||
|
|
||||||
|
**Acceptance criteria.**
|
||||||
|
- **The disjoint-surface matrix, DAW-observed:** solo two tracks in Arrange; switch to
|
||||||
|
Design → no track is soloed; solo a third track in Design; switch back → exactly the
|
||||||
|
two Arrange solos are restored (raw `I_SOLO` values verbatim, including solo-in-place)
|
||||||
|
and the Design solo is gone; switch forward again → the Design solo is restored.
|
||||||
|
- Reapply paths (tag/untag/show-both, project load) neither clear nor flash solo state —
|
||||||
|
before or after this track, during playback or stopped.
|
||||||
|
- With the transport playing or recording: the switch is refused on EVERY path (action,
|
||||||
|
panel segment), the segment visibly renders disabled, and project state is untouched by
|
||||||
|
the refused attempt. Stopped → the switch works. Recording gates identically to
|
||||||
|
playing.
|
||||||
|
- A panel-initiated switch now persists (the divergence closure): switch via the footer
|
||||||
|
segment, save, reload → the project reopens in the switched mode.
|
||||||
|
- Solo cache round-trips save/reload: save mid-disjunction, reload → the inactive mode's
|
||||||
|
cached solos restore on the next switch.
|
||||||
|
- One mode toggle remains one Ctrl-Z; `B_MUTE` is never written; master is never touched.
|
||||||
|
- The three invariant-amendment files land amended, in this track.
|
||||||
|
- Pure model changes covered in `tests/test_view_mode_model.cpp` (store/consume,
|
||||||
|
serialize round-trip, reconcile pruning); the enable predicate is pure and tested.
|
||||||
|
- **DAW-verification obligation:** Daniel runs the solo matrix, the playback-gated
|
||||||
|
refusal (playing AND recording), and the panel-persist case.
|
||||||
|
|
||||||
|
**Open questions.** `[verify]` `I_SOLO` is settable via `SetMediaTrackInfo_Value` and its
|
||||||
|
value domain — against `vendor/reaper-sdk/sdk/reaper_plugin_functions.h`, per root
|
||||||
|
`CLAUDE.md`'s API-verification rule (settability is inference today). `[verify]` adding
|
||||||
|
`REAPERAPI_WANT_GetPlayStateEx` to `view.cpp` alone compiles (`main.cpp:15` does not
|
||||||
|
define `REAPERAPI_MINIMAL`, so it should — inference, one compile answers it).
|
||||||
|
`[propose]` the repaint trigger for the disabled segment (the panel must notice transport
|
||||||
|
transitions; lean: poll play state in the panel's existing timer tick and invalidate on
|
||||||
|
change). `[propose]` restore-onto-parked-track edge: a track soloed in mode B, then
|
||||||
|
re-tagged so it is parked when B next activates, would on verbatim restore solo a
|
||||||
|
silent track and mute the mix — lean: restore skips tracks parked in the incoming mode
|
||||||
|
and drops those entries, with the alternative (retain until the track re-enters) named
|
||||||
|
and rejected as zombie state.
|
||||||
|
|
||||||
|
#### Ψ-W1-T3 — `media-explorer-section`
|
||||||
|
|
||||||
|
**Goal.** The Media-Explorer import action appears in the Media Explorer action section,
|
||||||
|
so it can be bound and placed on the Media Explorer toolbar. (Ψ.4)
|
||||||
|
|
||||||
|
**Why it lands in Main today (SDK-verified):** the action registers via `"gaccel"`
|
||||||
|
(`ingest.cpp:457-468`), and `gaccel_register_t` registers into the main keyboard section
|
||||||
|
only — the struct has no section field (`vendor/reaper-sdk/sdk/reaper_plugin.h:1106-1117`).
|
||||||
|
**The supported mechanism (SDK-verified):** `custom_action_register_t`
|
||||||
|
(`reaper_plugin.h:1090-1103`) — `{uniqueSectionId, idStr, name, extra}`, with **Media
|
||||||
|
Explorer = section 32063** (`:1099`, corroborated `reaper_plugin_functions.h:3467`);
|
||||||
|
`Register("custom_action", &ca)` returns the command id or 0; `idStr` must be unique
|
||||||
|
**across all sections** (`:1100`). Dispatch for non-main sections MUST come from
|
||||||
|
`"hookcommand2"` (`reaper_plugin.h:212-221`) — `"hookcommand"` runs only for the main
|
||||||
|
section (`:200-205`), and `main.cpp:309` registers only `"hookcommand"` today.
|
||||||
|
`custom_action_register_t` carries no `ACCEL`, so the non-Main entry ships no default
|
||||||
|
keybinding — acceptable; toolbar placement is the ask.
|
||||||
|
|
||||||
|
**Settled, not a fork: the action registers in BOTH Main and Media Explorer.** Ψ.4 asks
|
||||||
|
for reach, not relocation, and a bare move would orphan any existing Main-section
|
||||||
|
keybinding — a half-fix. Because `idStr` is unique across all sections, the Media
|
||||||
|
Explorer registration **mints a second FOREVER-STABLE command-id suffix:**
|
||||||
|
**`INGEST_IMPORT_MEDIA_EXPLORER_MX`**, composed per channel via `channelCommandId` like
|
||||||
|
every other id (`core/version/app_version.cpp:43-47,84-86`) — a new permanent entry in
|
||||||
|
each channel's id family, never to change after shipping. The existing
|
||||||
|
`INGEST_IMPORT_MEDIA_EXPLORER` string is preserved unchanged for the Main entry, so root
|
||||||
|
`CLAUDE.md:180-183`'s never-change contract is honored literally. Both registrations
|
||||||
|
share the one handler (`doImportFromMediaExplorer`, `ingest.cpp:317-395`).
|
||||||
|
|
||||||
|
**Surface boundary — owns:** `shell/actions/ingest.cpp` (the second registration, the
|
||||||
|
`hookcommand2` claim, the `-custom_action` unload mirror beside the existing mirrors at
|
||||||
|
`:476-481`), **the registration block in `src/app/main.cpp`** (registering the
|
||||||
|
`"hookcommand2"` hook beside `"hookcommand"` at `:309`, and its unload mirror at the
|
||||||
|
`rec == nullptr` path), and the root-`CLAUDE.md` §"REAPER extension contract" amendment
|
||||||
|
(the second registration mechanism, documented beside the 4-step main-section pattern).
|
||||||
|
**Must not touch:** any other action family's registration, `panel_*`, `capture*`. T4
|
||||||
|
must not touch `main.cpp`; this track is why.
|
||||||
|
|
||||||
|
**Behavior.**
|
||||||
|
- Main-section registration byte-identical to today: same id string, same gaccel, same
|
||||||
|
keybindings surviving.
|
||||||
|
- Second registration: `custom_action_register_t{32063, channelCommandId(new suffix),
|
||||||
|
channelActionName(same display phrase — decided: identical phrase in both sections,
|
||||||
|
for findability), nullptr}` via `Register("custom_action", ...)`. A 0 return is
|
||||||
|
tolerated gracefully (the Main entry still works; a console note, not a failure).
|
||||||
|
- `hookcommand2` claims ONLY the new MX command id and returns `false` otherwise —
|
||||||
|
`hookcommand2` fires for every section including Main, and the Main id stays claimed by
|
||||||
|
the existing `hookcommand` path (`ingest.cpp:470-474` via `main.cpp:222-230`); the two
|
||||||
|
ids differ, so no double dispatch is possible. Stated as a criterion, not an accident.
|
||||||
|
- Unload mirrors everything: `-custom_action` with the same struct/pointer, alongside the
|
||||||
|
existing `-gaccel`/`-command_id`.
|
||||||
|
- Beta channel forks both ids automatically through `channelCommandId` — each channel
|
||||||
|
gains exactly one new permanent id.
|
||||||
|
|
||||||
|
**Acceptance criteria.**
|
||||||
|
- The Actions list, section "Media Explorer", shows the channel-prefixed action; it can
|
||||||
|
be added to the Media Explorer toolbar; firing it from that toolbar imports the last
|
||||||
|
played Media Explorer file into a ReaSampler on the selected track — identical behavior
|
||||||
|
to the Main-section entry. `[verify — DAW]`
|
||||||
|
- The Main-section entry is unchanged: same command id, existing keybindings intact.
|
||||||
|
- Extension unload unregisters both entries cleanly; reload re-registers both.
|
||||||
|
- The new suffix `INGEST_IMPORT_MEDIA_EXPLORER_MX` is recorded as FOREVER-STABLE, per
|
||||||
|
channel, in the same breath as the registration lands.
|
||||||
|
- Root `CLAUDE.md`'s "REAPER extension contract" section gains the non-main mechanism
|
||||||
|
(`custom_action` / `hookcommand2` / `-custom_action` mirror) — a deliverable of this
|
||||||
|
track.
|
||||||
|
- **DAW-verification obligation:** Daniel adds the action to the Media Explorer toolbar
|
||||||
|
and imports from it; also confirms the Main-section binding still fires.
|
||||||
|
|
||||||
|
**Open questions.** `[verify]` re-verify `custom_action_register_t` and `hookcommand2`
|
||||||
|
argument order/types against the SDK headers at implementation time (root `CLAUDE.md`
|
||||||
|
rule; the section ids and struct shapes above are already header-verified, the rule
|
||||||
|
applies regardless). None classified `[propose]` — the ruling above is settled.
|
||||||
|
|
||||||
|
#### Ψ-W1-T4 — `drop-target-resolution`
|
||||||
|
|
||||||
|
**Goal.** Dragging cards out of the panel resolves its target from what is actually under
|
||||||
|
the cursor — continuously, reversibly, with a defined outcome and a visible cue for every
|
||||||
|
surface, and no silent no-op release anywhere. (Ψ.5)
|
||||||
|
|
||||||
|
**The root causes, so the redesign is judged against them.** The current chain locks a
|
||||||
|
drag's fate at its first processed move outside the client rect
|
||||||
|
(`core/ui/drag_out.cpp:18-26`): a single-card drag over ANY REAPER surface —
|
||||||
|
`GetThingFromPoint` returns non-empty info for the arrange too
|
||||||
|
(`instrument_drop_win.cpp:84-86`) — locks to `InstrumentDrop` and returns before the OS
|
||||||
|
hand-off is ever evaluated (`panel_drag.cpp:285-294`); over the arrange the info string
|
||||||
|
fails the FX-hotspot rule, so release falls to a **silent no-op** (root cause A — the
|
||||||
|
verified headline: single-card arrange drops do nothing, while multi-card drags skip the
|
||||||
|
FX resolve (`:276`) and work via OS drag, which is exactly Ψ.5's "sometimes"). The
|
||||||
|
`OsDrag` branch is irreversible — `ReleaseCapture` + `resetDragState` + modal
|
||||||
|
`DoDragDrop` (`:299-335`) — and `WM_MOUSEMOVE` coalescing makes the deciding exit pixel
|
||||||
|
vary with drag speed (root cause B). The FX "hotspot" is only the FX-button glyph family
|
||||||
|
(`fx_*`/`tcp.fx*`/`mcp.fx*`, `core/wire/instrument_drop.cpp:90-95`) — TCP body/name, or
|
||||||
|
a TCP too narrow to draw the button, yields `"tcp"` and a cue-less no-op (root cause C).
|
||||||
|
Multi-card drags can never instrument-drop (root cause D). One unresolvable hand-off
|
||||||
|
evaluation sets `dragOsHandoffBlocked` for the remainder of the drag (`:320`, cleared
|
||||||
|
only in `resetDragState` — root cause E). Coordinate spaces and DPI are NOT causes
|
||||||
|
(verified); bank-to-bank never leaves the client and is CTest-covered
|
||||||
|
(`tests/test_drag_out.cpp:33-112`) — consistent with "dragging between banks seems
|
||||||
|
fine", and it must stay byte-identical.
|
||||||
|
|
||||||
|
**The redesign is a gesture law, not a patch.** The law: **the target class is resolved
|
||||||
|
from what is under the cursor on every move, every class transition is reversible until
|
||||||
|
release or until the pointer leaves REAPER entirely, and the OS hand-off is reserved for
|
||||||
|
leaving REAPER** — every REAPER-internal target is executed natively on release. The
|
||||||
|
minimal patch (re-evaluating the hotspot per move but keeping the early irreversible
|
||||||
|
`OsDrag` fork) is named and rejected: it re-creates root cause B — a drag that crosses
|
||||||
|
the arrange en route to an FX button would still lose the instrument drop forever.
|
||||||
|
|
||||||
|
**The class enumeration and its defined outcomes (single-card / multi-card):**
|
||||||
|
|
||||||
|
| Under the cursor | Single card | Multi card |
|
||||||
|
|---|---|---|
|
||||||
|
| Inside the panel client | Internal drag (unchanged, byte-identical) | Internal drag (unchanged) |
|
||||||
|
| FX hotspot (`fx_*` window, or track panel — see below) | **Instrument drop** (existing `.vstpreset` path) | **Refuse-with-cue** (no instrument drop for a multi payload — root cause D becomes a defined, cued outcome) |
|
||||||
|
| TCP/MCP, whole panel (not just the FX glyph; `*.fxembed` still excluded) | **Instrument drop — decided:** the whole track panel is the hotspot. Root cause C IS the FX-glyph-only rule; a capture dropped on a track's panel means "sampler on this track." The rejected alternative — TCP drop = item at edit cursor — is silent-placement-adjacent and has no time coordinate; the arrange owns placement. | Refuse-with-cue |
|
||||||
|
| Arrange | **Timeline item at the pointer's track/time** — native insertion on release | Native insertion, shape at review (see open questions) |
|
||||||
|
| Elsewhere in REAPER (ruler, transport, docker chrome) | Refuse-with-cue | Refuse-with-cue |
|
||||||
|
| Outside REAPER entirely | OS drag-out (`DoDragDrop`, CF_HDROP, copy-only — unchanged mechanics, new trigger condition) | OS drag-out |
|
||||||
|
|
||||||
|
- **Every release either performs the resolved action or visibly refuses** — the cue
|
||||||
|
(cursor via the existing `applyDragCursor` precedent, plus the panel's drag paint
|
||||||
|
state) tracks the resolved class continuously, so "will this work" is visible before
|
||||||
|
release, and no cell in the matrix is a silent nothing.
|
||||||
|
- **The blocked flag is retired as a drag-lifetime latch** (root cause E): an
|
||||||
|
unresolvable evaluation refuses that evaluation, not the rest of the drag.
|
||||||
|
- **The arrange outcome does not violate the load-bearing principle, and the spec says so
|
||||||
|
where a reviewer will look:** the item is placed because the USER dragged it to that
|
||||||
|
spot — user-initiated placement, the same class of act as `RunInsertSelected`, not a
|
||||||
|
capture auto-insert. Root `CLAUDE.md`'s "capture and placement are separate acts" is
|
||||||
|
about capture never placing; a deliberate drop is placement on demand.
|
||||||
|
- **The pure decision stays pure.** The class enumeration, its precedence, and the
|
||||||
|
single/multi split land in `core/ui/drag_out` (the law) +
|
||||||
|
`core/wire/instrument_drop` (the info-string classification), CTest-covered over the
|
||||||
|
full matrix; the shell reads live pointer/window state and executes outcomes, exactly
|
||||||
|
the discipline `card_drag` already follows.
|
||||||
|
- **Hot-path discipline:** the inside-client path stays free of SDK hit-tests
|
||||||
|
(`GetThingFromPoint` is evaluated only outside the client rect, as today —
|
||||||
|
`panel_drag.cpp:273-274`); per-move resolution outside the client now runs for multi
|
||||||
|
payloads too, which is new but still per-mouse-move cold.
|
||||||
|
|
||||||
|
**Surface boundary — owns:** `core/ui/drag_out` (the gesture law, rewritten),
|
||||||
|
`core/wire/instrument_drop` (classification), `shell/panel/panel_drag.cpp` (routing +
|
||||||
|
release dispatch), `shell/actions/instrument_drop_win.cpp` (hit-test shell),
|
||||||
|
`shell/actions/drag_out_win.cpp` (hand-off trigger timing), `tests/test_drag_out.cpp`.
|
||||||
|
May touch `panel_input.cpp`'s drag-arm block (`:382-402`) only — T2's footer block is
|
||||||
|
fenced. **Must not touch:** `src/app/main.cpp` (T3's — this track registers nothing),
|
||||||
|
`core/ui/card_drag` and the in-grid reorder/replace semantics (bank-to-bank stays
|
||||||
|
byte-identical), the capture pillar, the insert action's own semantics.
|
||||||
|
|
||||||
|
**Acceptance criteria.**
|
||||||
|
- The full matrix above, DAW-observed, single AND multi: every cell produces its defined
|
||||||
|
outcome with its cue, and in particular — a single-card drop onto the arrange lands an
|
||||||
|
item at the pointer's track and time (the headline defect); a single-card drop onto a
|
||||||
|
track panel (not just the FX glyph) loads the instrument; a multi-card drop onto the
|
||||||
|
arrange still lands items; every refuse cell visibly refuses.
|
||||||
|
- **Reversibility:** leave the client, cross the arrange, reach an FX window →
|
||||||
|
instrument drop still available; return into the panel client → internal drag resumes.
|
||||||
|
One unresolvable evaluation does not change any later evaluation's outcome.
|
||||||
|
- **Drag speed does not change outcomes:** fast flicks and slow drags to the same release
|
||||||
|
point resolve identically (the coalescing hazard is designed out with the early lock,
|
||||||
|
not mitigated).
|
||||||
|
- Bank-to-bank drags are byte-identical to today; `tests/test_drag_out.cpp:33-112` (or
|
||||||
|
their successors) stay green with unchanged semantics.
|
||||||
|
- The pure law's CTest matrix covers every class × single/multi combination, including
|
||||||
|
the `*.fxembed` exclusions and the null-track/non-empty-info cases the SDK documents
|
||||||
|
for `GetThingFromPoint` (`reaper_plugin_functions.h:3440-3446`).
|
||||||
|
- No new SDK hit-test inside the client rect.
|
||||||
|
- **DAW-verification obligation:** Daniel runs the matrix, explicitly including the
|
||||||
|
narrow-TCP case (root cause C's trigger), fast-flick drags, and a drag that crosses the
|
||||||
|
arrange before reaching an FX window.
|
||||||
|
|
||||||
|
**Open questions.** `[verify]` pointer→(track, time) for the native arrange insertion —
|
||||||
|
candidate API `GetSet_ArrangeView2` (pixel-column → time mapping) plus
|
||||||
|
`GetThingFromPoint`'s track; verify signatures and behavior against the SDK header
|
||||||
|
before building on them; if no exact pixel→time mapping exists, the arrange outcome's
|
||||||
|
mechanism (not its existence) is re-proposed. `[verify]` what REAPER does with a CF_HDROP
|
||||||
|
drop re-entering its windows during our `DoDragDrop` (the accepted residual once the
|
||||||
|
pointer has left REAPER and returns mid-modal-loop — confirm it is REAPER's own file
|
||||||
|
import, then state it). `[propose]` the multi-card arrange insertion shape (lean: one
|
||||||
|
item per file mirroring REAPER's own multi-file drop convention; alternative: retain the
|
||||||
|
OS hand-off for the multi-arrange cell only — costs a second delivery mechanism and a
|
||||||
|
modal fork, named to be rejected on uniformity unless the native shape fails
|
||||||
|
verification). `[propose]` the exact refuse-with-cue rendering (cursor-only vs cursor +
|
||||||
|
panel status text).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ψ-W2 — Names and channels, over the settled render block
|
||||||
|
|
||||||
|
**Depends on Ψ-W1 — specifically Ψ-W1-T1**, which rewrites the capture
|
||||||
|
render-configuration block in `shell/capture/capture.cpp` and
|
||||||
|
`core/capture/render_settings.cpp`; both W2 tracks edit adjacent regions of those same
|
||||||
|
TUs, so W2 dispatches only after W1-T1 is on `dev`. (T2–T4 of W1 gate nothing here; the
|
||||||
|
wave boundary is the file collision, not a semantic dependency.)
|
||||||
|
|
||||||
|
**Two tracks, parallel, disjoint by field family:** T1 owns the naming fields
|
||||||
|
(`baseName` / `uniqueTag` / `displayName` / `capture_paths`), T2 owns the channel fields
|
||||||
|
(`channelCount` / PCM / `wav_codec`). **The known collision seam, named now:** both
|
||||||
|
tracks touch `shell/capture/capture.cpp` and
|
||||||
|
`shell/capture/capture_realtime_finalize.cpp` at adjacent lines. In `capture.cpp`, T1
|
||||||
|
owns the tag/paths mint (`:310-314`) and the `Sample` id/label lines (`:398-402`); T2
|
||||||
|
owns `RENDER_CHANNELS` (`:353-354`), the new post-render collapse step inserted between
|
||||||
|
the exists-check (`:390-395`) and the `Sample` population (`:398`), and
|
||||||
|
`stampCaptureSample`'s channel echo (`:207`). In `capture_realtime_finalize.cpp`, T1 owns
|
||||||
|
the label site (`:195`); T2 owns the layout-parse/channel lines (`:188-198`) and the
|
||||||
|
collapse insertion. Adjacent lines, disjoint fields — textual merge adjacency, not
|
||||||
|
semantic contention; whichever lands second rebases.
|
||||||
|
|
||||||
|
#### Ψ-W2-T1 — `capture-naming`
|
||||||
|
|
||||||
|
**Goal.** Captures are labeled after their source track's name plus a discriminator, on
|
||||||
|
every mint site — and the name is visible where the user looks. (Ψ.1)
|
||||||
|
|
||||||
|
**Today, and why it reads as broken:** every offline capture's `displayName` is the
|
||||||
|
literal `"item"` or `"track"` (`CaptureActionDef` → `render_settings.cpp:175-183` →
|
||||||
|
`capture.cpp:402`), the filename is `"item_<epoch>-<counter>.wav"`
|
||||||
|
(`makeUniqueTag`, `capture.cpp:187-199`; `deriveBankPaths`,
|
||||||
|
`core/capture/capture_paths.cpp:48-72`) — and **the source track name is read nowhere in
|
||||||
|
the tree** (repo-wide: zero hits for `P_NAME`/`GetTrackName`; the capture path holds
|
||||||
|
`MediaTrack*` and GUIDs but never a name). Worse, **the label barely surfaces:** the
|
||||||
|
panel card draws no name at all (`panel_render.cpp:23-63` — waveform, bars.beats,
|
||||||
|
seconds.ms only); the sample name appears only inside the VST3 editor. A good name
|
||||||
|
nobody can see is a half-fix, so this track also surfaces it.
|
||||||
|
|
||||||
|
**Surface boundary — owns:** `shell/capture/scope_resolve` (the ONE place already
|
||||||
|
walking source tracks before the FX-bypass guard — gains the name read, plumbed through
|
||||||
|
`ResolvedSource`, `scope_resolve.h:33`), `shell/capture/capture.h` (`CaptureRequest`),
|
||||||
|
`capture_orchestrator.cpp`'s naming lines (`:229`, `:405`), `capture.cpp`'s mint/label
|
||||||
|
lines (`:310-314`, `:398-402`), `capture_batch.cpp`'s mint sites (`:220`, `:284`,
|
||||||
|
`:395`), the realtime label chain (`capture_realtime_shell.cpp:309-310`,
|
||||||
|
`capture_realtime_finalize.cpp:195`, `core/capture/capture_realtime.cpp:31-32`),
|
||||||
|
`core/capture/render_settings` only for `CaptureActionDef::baseName` semantics
|
||||||
|
(`render_settings.h:170-175` — the literals become fallbacks), and the card name
|
||||||
|
surface: pure geometry in `core/ui` (`bank_grid` / `card_meta`) + its draw in
|
||||||
|
`shell/panel/panel_render.cpp`. **Must not touch:** `channelCount`/PCM/`wav_codec`
|
||||||
|
(T2's), the render-configuration block W1-T1 landed (read-only here),
|
||||||
|
`core/capture/capture_paths`'s purity — it stays REAPER-free path arithmetic with **no
|
||||||
|
filesystem access** (`core/capture/CLAUDE.md`); any collision handling stays
|
||||||
|
`makeUniqueTag`'s, shell-side.
|
||||||
|
|
||||||
|
**Behavior.**
|
||||||
|
- **The name is resolved shell-side in `scope_resolve`,** before the FX-bypass guard,
|
||||||
|
alongside the GUID walk it already does (`scope_resolve.cpp:53-72`, `:114-127`):
|
||||||
|
track scope → the selected track's name; item scope → the selected item's owning
|
||||||
|
track's name. `[verify]` the read API (`GetSetMediaTrackInfo_String` with `"P_NAME"`
|
||||||
|
vs `GetTrackName`) against the SDK header before use.
|
||||||
|
- **Label composition:** `displayName = "<TrackName> <discriminator>"`. The
|
||||||
|
discriminator's exact format is `[propose]` (lean: a compact date-time derived from the
|
||||||
|
capture's own `createdTimestamp`, e.g. `Bass 08-01 1432` — Daniel's "date, etc."
|
||||||
|
names it); `displayName` remains explicitly NOT unique, per the existing rule
|
||||||
|
(`core/model/CLAUDE.md` §`resample_name`) — the discriminator serves legibility, not
|
||||||
|
uniqueness.
|
||||||
|
- **The filename becomes meaningful too:** `baseName` = the sanitized track name, so the
|
||||||
|
stem is `"Bass_<tag>.wav"` via the existing `sanitizeStem` + `makeUniqueTag` pipeline —
|
||||||
|
stem uniqueness remains entirely `makeUniqueTag`'s job, unchanged. A name the sanitizer
|
||||||
|
reduces to nothing (non-ASCII) falls back to `"capture"` for the stem
|
||||||
|
(`capture_paths.cpp:24-46`'s existing rule) while `displayName` keeps the real name.
|
||||||
|
- **Fallback for an unnamed track:** deterministic, matching REAPER's own display
|
||||||
|
convention (`"Track N"` by index at capture time) — never the old literals on the
|
||||||
|
interactive paths.
|
||||||
|
- **All five mint sites covered, each stated:** offline (above); **realtime** — same
|
||||||
|
resolution at `begin` time (the action has a selected track); **batch** — per-unit
|
||||||
|
owning-track name, the batch ordinal retained as an additional discriminator
|
||||||
|
(`"Bass 1"`, `"Bass 2"`… exact composition settled with the discriminator format);
|
||||||
|
**recapture** — decided: preserves the existing entry's `displayName` (it regenerates
|
||||||
|
an entry, it does not mint a new identity; only the file stem re-mints);
|
||||||
|
**ingest** — unchanged (already meaningful: the source file's stem,
|
||||||
|
`ingest.cpp:249-266`); **bake** — unchanged (`model::nextIterationName`, the
|
||||||
|
`Kick r2` precedent). **The bake's naming-and-lineage open question is Ξ's, jointly
|
||||||
|
held with Ξ-W1-T1, and this track does not close or touch it.**
|
||||||
|
- **The card shows the name — decided in-scope:** one truncated name line on the panel
|
||||||
|
card, geometry pure (`bank_grid`/`card_meta`, CTest-covered), drawn by palette role
|
||||||
|
through the kit. Truncation is display-only; the stored value is never shortened. If
|
||||||
|
review finds the card too crowded at the smallest cell size, the fallback is
|
||||||
|
name-in-tooltip — `[propose]` at review, with on-card as the lean.
|
||||||
|
- **Existing bank entries are untouched** — new captures only; no retroactive rename.
|
||||||
|
- `displayName` already flows to the instrument (component-state copy + one-way refresh,
|
||||||
|
`component_state_io.cpp:74-88`, `sample_map.cpp:96`) — no instrument-side work.
|
||||||
|
|
||||||
|
**Acceptance criteria.**
|
||||||
|
- An item capture from a track named `Bass` yields `displayName` `Bass <discriminator>`
|
||||||
|
(format as settled at review) and a file `Bass_<tag>.wav`; a track capture likewise;
|
||||||
|
the literals `"item"`/`"track"` never appear as labels on any interactive capture path
|
||||||
|
again. `[verify — DAW]`
|
||||||
|
- Realtime and batch captures follow the same scheme (batch retains per-unit ordinals);
|
||||||
|
recapture preserves the entry's existing name; ingest and bake naming are unchanged.
|
||||||
|
- An unnamed source track produces the deterministic `Track N` fallback.
|
||||||
|
- The panel card shows the name, truncated to its cell, pure-geometry tested; the
|
||||||
|
instrument's browse list shows the same name with no mechanism change.
|
||||||
|
- File-stem uniqueness is still solely `makeUniqueTag`'s; `capture_paths` gains no
|
||||||
|
filesystem access; existing entries' labels are byte-identical after the track lands.
|
||||||
|
- **DAW-verification obligation:** Daniel captures from a named track, an unnamed track,
|
||||||
|
and a multi-item selection; sees the labels on the card and in the instrument.
|
||||||
|
|
||||||
|
**Open questions.** `[verify]` the track-name read API against the SDK header.
|
||||||
|
`[propose]` the discriminator format (lean above). `[propose]` multi-track item
|
||||||
|
selections (items spanning several tracks in one capture — lean: first source track's
|
||||||
|
name plus a `+N` marker; alternatives: joined names, or the scope literal as fallback).
|
||||||
|
`[propose]` card-name fallback to tooltip only if the card proves too crowded.
|
||||||
|
|
||||||
|
#### Ψ-W2-T2 — `mono-collapse`
|
||||||
|
|
||||||
|
**Goal.** A new capture whose channels are bit-identical is collapsed losslessly to one
|
||||||
|
channel — one channel of data on disk, a mono arrange item on insert, the instrument
|
||||||
|
loading in Mono mode. (Ψ.6)
|
||||||
|
|
||||||
|
**The invariant amendment is part of this track.** Root `CLAUDE.md:208` — "channel count
|
||||||
|
preserved (no silent stereo fold)" — is honored in spirit (the parenthetical forbids a
|
||||||
|
LOSSY fold; this collapse is lossless by predicate) but contradicted in text. The
|
||||||
|
amendment: channel count is preserved except that bit-identical channels may collapse
|
||||||
|
losslessly to mono; a lossy fold remains forbidden. Noted in the amendment: the old text
|
||||||
|
was already untrue in the other direction — a mono source renders at `RENDER_CHANNELS=2`
|
||||||
|
today (`capture_orchestrator.cpp:227`, hardcoded and never measured).
|
||||||
|
|
||||||
|
**Surface boundary — owns:** `core/capture/wav_codec` (the pure bit-identity predicate +
|
||||||
|
collapse plan, with `wav_codec` unit tests), `shell/capture/capture.cpp` (the post-render
|
||||||
|
collapse step between the exists-check `:390-395` and the `Sample` population `:398`,
|
||||||
|
plus `stampCaptureSample`'s channel echo `:207` — derive from the FILE, the
|
||||||
|
`bake_land.cpp:105,131,177` / `ingest.cpp:199,268` precedent),
|
||||||
|
`shell/capture/capture_realtime_finalize.cpp` (the collapse on the realtime path, and
|
||||||
|
the same echo fix — it parses the layout at `:188-198` and STILL echoes the request
|
||||||
|
value), the root-`CLAUDE.md:208` amendment, and — **minimally, because Phase Γ is live in
|
||||||
|
`shell/instrument/`** — `shell/instrument/processor_reload.cpp` only for the stale
|
||||||
|
"always 2 for extension captures" comment (`:64-77` vs `:150-157`) and any index/file
|
||||||
|
consistency fix that falls out; nothing else instrument-side. **Must not touch:** the
|
||||||
|
naming fields (T1's), `capture_paths`, the panel, the instrument's channel-mode logic
|
||||||
|
(`sample_map.cpp:58-62` already maps `channelCount == 1` → Mono, explicit user toggle
|
||||||
|
winning — no change needed).
|
||||||
|
|
||||||
|
**Behavior.**
|
||||||
|
- **The predicate is pure and bit-exact:** all channels bit-identical per frame (float
|
||||||
|
bit patterns, never epsilon) → collapse to 1 channel. Generalized to N channels
|
||||||
|
(all-identical → mono; no partial collapse, e.g. never 4→2) — N-channel captures are
|
||||||
|
not reachable today (`channelCount` hardcoded 2), so the generalization is
|
||||||
|
future-proofing the predicate, stated as such.
|
||||||
|
- **The collapse mirrors the one existing rewrite precedent** — `trimAutoTailInPlace`
|
||||||
|
(`capture_realtime_finalize.cpp:55-125`): `parseWavLayout` → `extractFloatFrames` →
|
||||||
|
`buildFloat32Wav(1, …)` → truncating rewrite, all with the existing `wav_codec` tools
|
||||||
|
(`wav_codec.h:48-89` — `buildFloat32Wav` already takes arbitrary `nch`). There is no
|
||||||
|
PCM assembly on the offline path today (REAPER writes the file; the extension only
|
||||||
|
stats and hashes it) — this step is the first, and it lives shell-side at the named
|
||||||
|
insertion point with its plan pure.
|
||||||
|
- **Scope — decided: every extension capture path** — offline, realtime, batch,
|
||||||
|
recapture (batch and recapture route through the same backends). **Ingest is
|
||||||
|
excluded** — imported files are the user's bytes, not our capture; rewriting them is a
|
||||||
|
mutation this tool has no license for. **Unconditional, no user opt-out** — Ψ.6 asks
|
||||||
|
for the behavior, not a preference; dual-mono stereo carries zero information the mono
|
||||||
|
file lacks. **New captures only, never retroactive** — existing bank entries and files
|
||||||
|
are untouched.
|
||||||
|
- **The file and the index value are written together — the stated hazard.** A collapsed
|
||||||
|
file with `Sample::channelCount` left at 2 still plays (the instrument's
|
||||||
|
`extractChannel` clamps out-of-range to the last channel, `sample_map.cpp:174-186`,
|
||||||
|
yielding dual-mono) but the mono/stereo toggle and the waveform lane count would read
|
||||||
|
Stereo — so the criterion is equality with the file's `fmt` on every path, not absence
|
||||||
|
of crashes.
|
||||||
|
- **Insert needs no change, stated so nobody invents work:** `insert.cpp:129-152` passes
|
||||||
|
only a path to `InsertMedia`; REAPER derives the item's channel count from the file —
|
||||||
|
a 1-channel WAV yields a mono item for free.
|
||||||
|
- **Behavior change, stated and accepted:** `hashWavContent` covers the `fmt ` body +
|
||||||
|
`data` payload, so a collapsed capture will NOT hash-dedup against a pre-existing
|
||||||
|
stereo twin of the same audio (`bank_model.h:143-148`). Accepted — the predicate is
|
||||||
|
deterministic, so repeats of the same request still dedup against each other.
|
||||||
|
- **Bit-identical repeats survive:** a deterministic predicate over deterministic bytes;
|
||||||
|
identical requests still produce identical files (now identically-collapsed ones).
|
||||||
|
|
||||||
|
**Acceptance criteria.**
|
||||||
|
- A capture of dead-center mono content yields a 1-channel float32 WAV whose PCM is
|
||||||
|
bit-identical to either source channel and whose frame count is unchanged — losslessness
|
||||||
|
asserted by `extractFloatFrames` equality in the pure tests, observed in the DAW on the
|
||||||
|
real path. `[verify — DAW]`
|
||||||
|
- A capture with ANY differing sample pair is byte-identical to today's 2-channel output
|
||||||
|
— the not-collapsed path is unchanged, the same discipline as "bypassed means
|
||||||
|
byte-identical."
|
||||||
|
- `Sample::channelCount` equals the produced file's `fmt` channel count on EVERY capture
|
||||||
|
path — including realtime, whose finalize currently parses the layout and echoes the
|
||||||
|
request anyway; that defect is fixed here.
|
||||||
|
- Inserting a collapsed capture yields a mono arrange item; the instrument loads it in
|
||||||
|
Mono channel mode with a single waveform lane, explicit user toggle still winning.
|
||||||
|
`[verify — DAW]`
|
||||||
|
- The null test holds for a collapsed capture re-inserted at its source position.
|
||||||
|
`[verify — DAW: REAPER's mono-item-on-stereo-track summing at unity is the thing to
|
||||||
|
confirm]`
|
||||||
|
- Bit-identical repeats hold across the collapse; realtime and batch behave identically
|
||||||
|
to offline; ingest is demonstrably untouched (an imported dual-mono file stays stereo).
|
||||||
|
- The predicate + collapse plan are pure with `wav_codec` test coverage, including the
|
||||||
|
N-channel all-identical case and the one-sample-differs case.
|
||||||
|
- Root `CLAUDE.md:208` lands amended, in this track; the `processor_reload.cpp` touch is
|
||||||
|
the comment + consistency fix only.
|
||||||
|
- **DAW-verification obligation:** Daniel captures a dead-center source and a true-stereo
|
||||||
|
source, inserts both, loads both into the instrument, and runs the null test on the
|
||||||
|
collapsed one.
|
||||||
|
|
||||||
|
**Open questions.** `[propose]` whether bake landings collapse too (lean YES — the bake
|
||||||
|
"writes a file plus an index entry, like every other capture", `bake_land` already
|
||||||
|
derives channel count from the file, and a dead-center instrument render is exactly the
|
||||||
|
dual-mono case; it is `[propose]` rather than decided only because `bake_land` is Ξ-W2's
|
||||||
|
freshly-landed surface and the collapse there should be confirmed against what actually
|
||||||
|
shipped). `[verify]` REAPER's mono-item summing for the null test (above). Decided, not
|
||||||
|
open: unconditional; new-captures-only; ingest excluded; N-generalized predicate; no
|
||||||
|
partial collapse.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Traceability — all seventeen items
|
## Traceability — all seventeen items
|
||||||
|
|
||||||
The check that nothing was dropped. Every row points at a track that exists above.
|
The check that nothing was dropped. Every row points at a track that exists above.
|
||||||
@@ -2077,6 +2797,11 @@ proof it exists to give.
|
|||||||
deck-rework entry (whose original "one row of taller decks with within-deck stacking" shape
|
deck-rework entry (whose original "one row of taller decks with within-deck stacking" shape
|
||||||
Daniel explicitly superseded), and **Γ-W1-T1 discharges "Raise the stage-time ceiling above
|
Daniel explicitly superseded), and **Γ-W1-T1 discharges "Raise the stage-time ceiling above
|
||||||
2 s"** (Γ-F3 reversed).
|
2 s"** (Γ-F3 reversed).
|
||||||
|
- **All of Phase Ψ** (`ppsi-*`). **Six tracks across two waves**, from a direct list of
|
||||||
|
seven defects and refinements (Daniel, 2026-08-01), not from `TODO-1.0.md`. Listed here
|
||||||
|
as a block, like Γ; unlike Γ it has no backing product doc — the seven are recorded
|
||||||
|
verbatim in the phase header as its provenance (Ψ.1–Ψ.7), and the design content lives
|
||||||
|
inline in its tracks.
|
||||||
- **Γ-W3-T2 `bake-reset-amendment` is a CORRECTION, not a feature**, and belongs on this list
|
- **Γ-W3-T2 `bake-reset-amendment` is a CORRECTION, not a feature**, and belongs on this list
|
||||||
for a different reason from the others: it exists only because Ξ-W2-T1 shipped ahead of this
|
for a different reason from the others: it exists only because Ξ-W2-T1 shipped ahead of this
|
||||||
plan's sequencing claim. If more corrections of this shape appear, they belong here rather
|
plan's sequencing claim. If more corrections of this shape appear, they belong here rather
|
||||||
@@ -2176,4 +2901,22 @@ Phase Γ — The instrument's control surface (none of the seventeen; ends
|
|||||||
(W1-T1 | W1-T3), editor_session.cpp (W2-T1 | W2-T2) — all textual adjacency, not
|
(W1-T1 | W1-T3), editor_session.cpp (W2-T1 | W2-T2) — all textual adjacency, not
|
||||||
semantic contention. W3-T2's disjointness from W3-T1 is CONDITIONAL: confirm it against
|
semantic contention. W3-T2's disjointness from W3-T1 is CONDITIONAL: confirm it against
|
||||||
what Xi-W2-T1 shipped, and serialize behind T1 if it does not hold.
|
what Xi-W2-T1 shipped, and serialize behind T1 if it does not hold.
|
||||||
|
|
||||||
|
Phase Psi — The extension trust pass (none of the seventeen; a direct list of seven)
|
||||||
|
W1 Exact bounds, disciplined switches, reachable actions, resolved drops [4 tracks]
|
||||||
|
T1 capture-range-exactness ..... Psi.7 [opens with a DAW repro matrix]
|
||||||
|
T2 mode-switch-discipline ...... Psi.2 + Psi.3 [one chokepoint: applyMode]
|
||||||
|
T3 media-explorer-section ...... Psi.4 [BOTH sections; new FOREVER-STABLE id]
|
||||||
|
T4 drop-target-resolution ...... Psi.5 [gesture law, not a patch]
|
||||||
|
W2 Names and channels, over the settled render block [gated on W1-T1's render block]
|
||||||
|
T1 capture-naming .............. Psi.1 [+ the card shows the name]
|
||||||
|
T2 mono-collapse ............... Psi.6 [lossless only; ingest excluded]
|
||||||
|
|
||||||
|
Three invariant amendments are track deliverables: never-touch-solo (W1-T2, three
|
||||||
|
files), the action-registration contract (W1-T3, root CLAUDE.md), channel-count-
|
||||||
|
preserved (W2-T2, root CLAUDE.md:208).
|
||||||
|
Shared files, named: panel_input.cpp (W1-T2 footer block | W1-T4 drag-arm block);
|
||||||
|
capture.cpp + capture_realtime_finalize.cpp (W2-T1 naming lines | W2-T2 channel
|
||||||
|
lines) — all textual adjacency, not semantic contention. main.cpp is W1-T3's
|
||||||
|
exclusively.
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user