docs: collapse Phase Ψ's landed track specs, and record Ψ-W3 — the wave a review finding opened
This commit is contained in:
+114
-605
@@ -2083,6 +2083,19 @@ provenance, cited throughout as Ψ.1–Ψ.7:
|
|||||||
| Ψ.1 | Ψ-W2-T1 | `ppsi-w2-t1-capture-naming` |
|
| Ψ.1 | Ψ-W2-T1 | `ppsi-w2-t1-capture-naming` |
|
||||||
| Ψ.6 | Ψ-W2-T2 | `ppsi-w2-t2-mono-collapse` |
|
| Ψ.6 | Ψ-W2-T2 | `ppsi-w2-t2-mono-collapse` |
|
||||||
|
|
||||||
|
**Ψ-W3 is not one of the seven** — opened mid-phase, after Ψ-W2's review surfaced that
|
||||||
|
the track scope carried the same multi-track stem-collapse hole Ψ-W1-T1 had just closed
|
||||||
|
for item scope. It consolidates none of the original seven and carries no `Ψ-item` row
|
||||||
|
above; see Ψ-W3 below.
|
||||||
|
|
||||||
|
**All three waves have landed — Phase Ψ is complete.** W1 and W2 each carry their own
|
||||||
|
landed notes below; W3 carries its own too. See `docs/COMPLETED.md` for every track's
|
||||||
|
full narrative. **None of the seven tracks is DAW-verified** — all are code-complete and
|
||||||
|
unit-tested, several resting on a shared unverified inference about how REAPER's
|
||||||
|
selected-tracks render source interacts with custom time bounds, which Ψ-W3-T1's
|
||||||
|
refusal now also rests on; each track's DAW-verification obligation is restated inline
|
||||||
|
below.
|
||||||
|
|
||||||
Ψ.2 and Ψ.3 share one track deliberately: they share one chokepoint — `applyMode`, the
|
Ψ.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
|
sole mode mutator (`shell/view/view.cpp:380-469`) — and one discriminator
|
||||||
(`targetModeId != model.activeModeId()`, the test that distinguishes a real switch from a
|
(`targetModeId != model.activeModeId()`, the test that distinguishes a real switch from a
|
||||||
@@ -2094,9 +2107,8 @@ exception — Ψ-W2-T2 touches `shell/instrument/processor_reload.cpp` for a sta
|
|||||||
and an index/file consistency check, and that touch is bounded to the minimum in its
|
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.
|
surface boundary precisely because Γ is live in that directory.
|
||||||
|
|
||||||
**Three invariant amendments are DELIVERABLES of this phase, not asides.** Each is
|
**Three invariant amendments were DELIVERABLES of this phase, not asides.** Each landed
|
||||||
scheduled in — and an acceptance criterion of — its owning track. This spec schedules
|
in its owning track, as an acceptance criterion of that track:
|
||||||
them; the implementing track performs them:
|
|
||||||
|
|
||||||
1. **The never-touch-solo rule** (Ψ-W1-T2): `src/shell/view/CLAUDE.md:14-17`,
|
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`.
|
`src/core/view/CLAUDE.md:10`, and `docs/product/design-view.md:160-165` + `:588-592`.
|
||||||
@@ -2123,416 +2135,68 @@ internal-drag path").
|
|||||||
|
|
||||||
### Ψ-W1 — Exact bounds, disciplined switches, reachable actions, resolved drops
|
### Ψ-W1 — Exact bounds, disciplined switches, reachable actions, resolved drops
|
||||||
|
|
||||||
**Depends on:** nothing in this phase. **Four tracks, disjoint by surface:**
|
**Depends on:** nothing in this phase.
|
||||||
|
|
||||||
| 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` — **plus, as landed:** the new pure `core/capture/render_window` + `core/capture/track_topology`, the new shells `shell/capture/render_selection` + `shell/capture/render_isolation`, and `renderOffline`'s guard block in `shell/capture/capture_orchestrator.cpp` (the one seam a fresh capture and a recipe replay both cross, so the refusal and the transient guards had to live there rather than in `scope_resolve`) |
|
|
||||||
| **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`
|
#### Ψ-W1-T1 — `capture-range-exactness`
|
||||||
|
|
||||||
**Goal.** A capture over a time selection or razor area on a source item substantially
|
**Landed** — see `docs/COMPLETED.md` for the full narrative. A ranged item capture now
|
||||||
longer than the selection produces exactly the requested range — both scopes, no
|
renders exactly the requested window instead of the whole item, by re-sourcing through
|
||||||
whole-item widening. (Ψ.7)
|
the selected-tracks render when the item extent does not already print the window.
|
||||||
|
**Deviation:** the spec named two candidate architectures (re-source vs. render-then-
|
||||||
**What is already correct, so the fix does not wander.** Range resolution is correct end
|
trim); the engineer shipped a conditional form of the re-source candidate — the
|
||||||
to end: `scope_resolve.cpp:100-112` (`resolveRange`) → razor union
|
full-extent case runs literally unchanged code, keeping the byte-identity regression
|
||||||
(`render_settings.cpp:135-170`) or `GetSet_LoopTimeRange` (`scope_resolve.cpp:35-40`),
|
floor structural and the fix cheap to revert if the override inference proves wrong.
|
||||||
passed verbatim (`capture_orchestrator.cpp:221-222`), landed as
|
Also landed: a transient isolation guard (cutting `B_MAINSEND` on direct folder
|
||||||
`RENDER_BOUNDSFLAG=0` + exact `RENDER_STARTPOS`/`RENDER_ENDPOS`
|
children, muting receives) so an item capture stays true to item scope, and a
|
||||||
(`capture.cpp:320-322`). There is no item-bounds fallback in `RunCapture`. **The widening
|
post-render frame-count gate (±1 tolerance, tail-None only) that refuses and self-cleans
|
||||||
decision point is the source-mode bit**: item scope maps to
|
a widened render. New modules `core/capture/render_window`, `core/capture/track_topology`,
|
||||||
`kRenderSelItems (&32) | kRenderSingleFile` (`render_settings.cpp:82-86`), and the repo's
|
`shell/capture/render_selection`, `shell/capture/render_isolation`. The whole fix rests
|
||||||
own comments treat `&32` as item-extent-driven (`capture_batch.cpp:64-66`, `:98-99` —
|
on the unverified inference that REAPER's selected-tracks render source overrides
|
||||||
the entire reason batch transiently selects one item per render). The working inference —
|
custom time bounds — Ψ-W3-T1 (below) now also depends on it. **DAW-verification
|
||||||
**unverified without a DAW** — is that REAPER's "selected media items" render source
|
obligation, unmet:** the four-cell scope × selection-type matrix over a source item
|
||||||
overrides the custom time bounds. Track scope uses `&128` (selected tracks via master), a
|
substantially longer than the selection (tail None), plus one razor-union case — none of
|
||||||
normal time-bounded render, and **no code path was found that widens it**.
|
it run in a live REAPER session yet.
|
||||||
|
|
||||||
**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`
|
#### Ψ-W1-T2 — `mode-switch-discipline`
|
||||||
|
|
||||||
**Goal.** Switching the active mode caches and clears the outgoing mode's solo state and
|
**Landed** — see `docs/COMPLETED.md` for the full narrative. Per-mode SOLO surfaces now
|
||||||
restores the incoming mode's — disjoint solo surfaces per mode — and the switch itself is
|
cache, clear, and restore across a Design/Arrange switch, with the switch itself
|
||||||
refused, visibly, while the transport is playing or recording. (Ψ.2 + Ψ.3)
|
refused, visibly, while the transport is playing or recording. The never-touch-solo
|
||||||
|
invariant — stated three times (`src/shell/view/CLAUDE.md`, `src/core/view/CLAUDE.md`,
|
||||||
**The invariant amendment comes first, because without it this track is a breach.** The
|
`docs/product/design-view.md`) — is amended, in this track, to the snapshot sense of
|
||||||
never-touch-solo rule is stated three times: `src/shell/view/CLAUDE.md:14-17` ("Never
|
non-destructive: solo is cached per mode on a real switch and restored verbatim, not
|
||||||
touches master or `B_MUTE`/`I_SOLO`… User mute/solo survives every toggle untouched"),
|
left untouched absolutely the way `B_MUTE` and the master track are. Also closed: a
|
||||||
`src/core/view/CLAUDE.md:10`, and `docs/product/design-view.md:160-165` (framed as the
|
pre-existing bug where a footer mode-segment click never persisted view state. New
|
||||||
analog of capture's non-destructive invariant) + `:588-592`. **The amended form:** the
|
`core/view/solo_cache`, `shell/view/view_solo`. **DAW-verification obligation, unmet:**
|
||||||
tool never *loses* the user's solo state — solo is cached per mode on a real switch and
|
the disjoint-surface solo matrix, the playback-gated refusal (playing and recording),
|
||||||
restored verbatim on return, the snapshot sense of non-destructive, exactly as the
|
and the panel-persist case — none run in a live REAPER session yet.
|
||||||
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`
|
#### Ψ-W1-T3 — `media-explorer-section`
|
||||||
|
|
||||||
**Goal.** The Media-Explorer import action appears in the Media Explorer action section,
|
**Landed** — see `docs/COMPLETED.md` for the full narrative. The Media Explorer import
|
||||||
so it can be bound and placed on the Media Explorer toolbar. (Ψ.4)
|
action now registers into REAPER's Media Explorer action section (32063) via
|
||||||
|
`custom_action` + `hookcommand2`, alongside its existing Main-section entry so existing
|
||||||
**Why it lands in Main today (SDK-verified):** the action registers via `"gaccel"`
|
keybindings survive — a second FOREVER-STABLE id, `INGEST_IMPORT_MEDIA_EXPLORER_MX`,
|
||||||
(`ingest.cpp:457-468`), and `gaccel_register_t` registers into the main keyboard section
|
minted per channel. Root `CLAUDE.md`'s "REAPER extension contract" is amended, in this
|
||||||
only — the struct has no section field (`vendor/reaper-sdk/sdk/reaper_plugin.h:1106-1117`).
|
track, with the second, non-main registration mechanism beside the original four-step
|
||||||
**The supported mechanism (SDK-verified):** `custom_action_register_t`
|
main-section pattern. **DAW-verification obligation, unmet:** adding the action to the
|
||||||
(`reaper_plugin.h:1090-1103`) — `{uniqueSectionId, idStr, name, extra}`, with **Media
|
Media Explorer toolbar and firing it from there; confirming the Main-section binding
|
||||||
Explorer = section 32063** (`:1099`, corroborated `reaper_plugin_functions.h:3467`);
|
still fires; and confirming unload/reload does not leak a duplicate Media Explorer entry
|
||||||
`Register("custom_action", &ca)` returns the command id or 0; `idStr` must be unique
|
(the `-custom_action` unload mirror is unconfirmed against the SDK header) — none run in
|
||||||
**across all sections** (`:1100`). Dispatch for non-main sections MUST come from
|
a live REAPER session yet.
|
||||||
`"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. Also: unload,
|
|
||||||
reload, confirm no duplicate Media Explorer entry — the `-custom_action` unload mirror
|
|
||||||
is unconfirmed against the SDK header (root `CLAUDE.md` §"REAPER extension contract",
|
|
||||||
`[verify — DAW]`), and if unsupported the entry leaks across a reload.
|
|
||||||
|
|
||||||
**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`
|
#### Ψ-W1-T4 — `drop-target-resolution`
|
||||||
|
|
||||||
**Goal.** Dragging cards out of the panel resolves its target from what is actually under
|
**Landed** — see `docs/COMPLETED.md` for the full narrative. The drag-out gesture is now
|
||||||
the cursor — continuously, reversibly, with a defined outcome and a visible cue for every
|
a per-move, stateless law: target class resolves from what is under the cursor on every
|
||||||
surface, and no silent no-op release anywhere. (Ψ.5)
|
move, every class transition is reversible until release or until the pointer leaves
|
||||||
|
REAPER, and the OS hand-off is reserved for leaving REAPER entirely — the whole TCP/MCP
|
||||||
**The root causes, so the redesign is judged against them.** The current chain locks a
|
is now an instrument-drop hotspot, and a single-card arrange drop lands a timeline item
|
||||||
drag's fate at its first processed move outside the client rect
|
at the pointer's track and time. New `shell/actions/arrange_drop_win`.
|
||||||
(`core/ui/drag_out.cpp:18-26`): a single-card drag over ANY REAPER surface —
|
**DAW-verification obligation, unmet:** the full target-class matrix (single- and
|
||||||
`GetThingFromPoint` returns non-empty info for the arrange too
|
multi-card), reversibility across a drag that crosses the arrange en route to an FX
|
||||||
(`instrument_drop_win.cpp:84-86`) — locks to `InstrumentDrop` and returns before the OS
|
window, drag-speed independence, and the narrow-TCP case — none run in a live REAPER
|
||||||
hand-off is ever evaluated (`panel_drag.cpp:285-294`); over the arrange the info string
|
session yet.
|
||||||
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).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -2544,215 +2208,58 @@ render-configuration block in `shell/capture/capture.cpp` and
|
|||||||
TUs, so W2 dispatches only after W1-T1 is on `dev`. (T2–T4 of W1 gate nothing here; the
|
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.)
|
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`
|
#### Ψ-W2-T1 — `capture-naming`
|
||||||
|
|
||||||
**Goal.** Captures are labeled after their source track's name plus a discriminator, on
|
**Landed** — see `docs/COMPLETED.md` for the full narrative. Captures are now named
|
||||||
every mint site — and the name is visible where the user looks. (Ψ.1)
|
after their source track's name plus a discriminator
|
||||||
|
(`<Track> [+N] [#ordinal] MM-DD HHMM`) at every interactive mint site, with the name
|
||||||
**Today, and why it reads as broken:** every offline capture's `displayName` is the
|
shown on the panel card over a scrim clearing the 4.5:1 contrast floor. New
|
||||||
literal `"item"` or `"track"` (`CaptureActionDef` → `render_settings.cpp:175-183` →
|
`core/capture/capture_name`. Recapture, ingest, and the bake deliberately keep their own
|
||||||
`capture.cpp:402`), the filename is `"item_<epoch>-<counter>.wav"`
|
naming — the bake's naming-and-lineage open question stays Ξ-W1-T1's/Ξ-W2-T1's to close,
|
||||||
(`makeUniqueTag`, `capture.cpp:187-199`; `deriveBankPaths`,
|
untouched here. **DAW-verification obligation, unmet:** capturing from a named track, an
|
||||||
`core/capture/capture_paths.cpp:48-72`) — and **the source track name is read nowhere in
|
unnamed track, and a multi-item selection, and confirming the labels show on the card
|
||||||
the tree** (repo-wide: zero hits for `P_NAME`/`GetTrackName`; the capture path holds
|
and in the instrument — none run in a live REAPER session yet.
|
||||||
`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`
|
#### Ψ-W2-T2 — `mono-collapse`
|
||||||
|
|
||||||
**Goal.** A new capture whose channels are bit-identical is collapsed losslessly to one
|
**Landed** — see `docs/COMPLETED.md` for the full narrative. A capture whose channels
|
||||||
channel — one channel of data on disk, a mono arrange item on insert, the instrument
|
are bit-identical now collapses losslessly to one mono channel, written via temp file
|
||||||
loading in Mono mode. (Ψ.6)
|
plus atomic rename, with `Sample::channelCount` measured off the landed file rather than
|
||||||
|
echoed from the request on every capture path — including realtime, which previously
|
||||||
|
parsed the layout and echoed the request anyway. Root `CLAUDE.md:208`'s
|
||||||
|
channel-count-preserved invariant is amended, in this track, to the landed wording:
|
||||||
|
channel count preserved, except that bit-identical channels may collapse losslessly to
|
||||||
|
mono; a lossy fold remains forbidden. The `shell/instrument/processor_reload.cpp` touch
|
||||||
|
stayed to the minimum named in its surface boundary (Phase Γ was live in that
|
||||||
|
directory). **Open, not closed here:** whether bake landings collapse too — `[propose]`,
|
||||||
|
leaning yes, still deferred to be confirmed against what Ξ-W2-T1 actually shipped.
|
||||||
|
**DAW-verification obligation, unmet:** capturing a dead-center mono source and a
|
||||||
|
true-stereo source, inserting both, loading both into the instrument, and running the
|
||||||
|
null test on the collapsed one (REAPER's mono-item-on-stereo-track summing at unity is
|
||||||
|
the specific thing to confirm) — none run in a live REAPER session yet.
|
||||||
|
|
||||||
**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). `[verify — DAW]`
|
|
||||||
"lossless" is proven at the file-bytes level; it is not the same claim as the null
|
|
||||||
test's playback-chain property (whether REAPER sums a 1-channel item on a stereo track
|
|
||||||
at the same unity gain as a dual-mono 2-channel item) — see the acceptance criteria's
|
|
||||||
own `[verify — DAW]` on that bullet below.
|
|
||||||
|
|
||||||
**Surface boundary — owns:** `core/capture/wav_codec` (the pure bit-identity predicate +
|
### Ψ-W3 — Closing the track-scope stem-collapse hole
|
||||||
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.**
|
**Depends on Ψ-W2 for:** existing at all — this wave did not exist when the phase was
|
||||||
- **The predicate is pure and bit-exact:** all channels bit-identical per frame (float
|
scoped. Daniel opened it after Ψ-W2's review surfaced that the track scope carried the
|
||||||
bit patterns, never epsilon) → collapse to 1 channel. Generalized to N channels
|
same multi-track stem-collapse hole Ψ-W1-T1 had just closed for item scope.
|
||||||
(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.**
|
**One track. Consolidates none of the seven** — it came from a review finding, not from
|
||||||
- A capture of dead-center mono content yields a 1-channel float32 WAV whose PCM is
|
Ψ.1–Ψ.7.
|
||||||
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
|
#### Ψ-W3-T1 — `track-scope-range`
|
||||||
"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
|
**Landed** — see `docs/COMPLETED.md` for the full narrative. Any multi-track
|
||||||
dual-mono case; it is `[propose]` rather than decided only because `bake_land` is Ξ-W2's
|
selected-tracks render now refuses, in both scopes, keyed on the render *source* rather
|
||||||
freshly-landed surface and the collapse there should be confirmed against what actually
|
than the capture scope — closing the hole Ψ-W1-T1 left open for track scope. Realtime
|
||||||
shipped). `[verify]` REAPER's mono-item summing for the null test (above). Decided, not
|
deliberately diverges and was left untouched, because it sums correctly. The refusal
|
||||||
open: unconditional; new-captures-only; ingest excluded; N-generalized predicate; no
|
rests on the same unverified inference Ψ-W1-T1 rests on — that REAPER's selected-tracks
|
||||||
partial collapse.
|
render source overrides custom time bounds — so if that inference is wrong, this refusal
|
||||||
|
costs a working capture. `docs/verify-track-scope-multitrack.md` is a new standalone
|
||||||
|
verification script on this branch, for this track's multi-track refusal specifically.
|
||||||
|
**DAW-verification obligation, unmet:** confirmed nowhere in a live REAPER session yet.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -2804,11 +2311,13 @@ 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
|
- **All of Phase Ψ** (`ppsi-*`). **Seven tracks across three waves**, from a direct list
|
||||||
seven defects and refinements (Daniel, 2026-08-01), not from `TODO-1.0.md`. Listed here
|
of seven defects and refinements (Daniel, 2026-08-01), not from `TODO-1.0.md`. Listed
|
||||||
as a block, like Γ; unlike Γ it has no backing product doc — the seven are recorded
|
here as a block, like Γ; unlike Γ it has no backing product doc — the seven are
|
||||||
verbatim in the phase header as its provenance (Ψ.1–Ψ.7), and the design content lives
|
recorded verbatim in the phase header as its provenance (Ψ.1–Ψ.7), and the design
|
||||||
inline in its tracks.
|
content lives inline in its tracks. The seventh track, Ψ-W3-T1, is not one of the
|
||||||
|
seven defects/refinements itself — it came from a review finding mid-phase; see the
|
||||||
|
Phase Ψ section for detail.
|
||||||
- **Γ-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
|
||||||
|
|||||||
Reference in New Issue
Block a user