docs: close out Ξ-W1-T1, Ξ-W1-T2 and Θ-W3-T1 into COMPLETED; map the note directory; document the Release build
This commit is contained in:
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
**ReaSampler** is a per-project audio sample-bank capture tool that builds two artifacts: the REAPER extension (`reaper_reasampler`) and **ReaSampler 9000**, a Windows-only VST3 sampler instrument (`reasampler_9000.vst3`, `core/instrument/` + `shell/instrument/`, second CMake target `reasampler_vst`, gated on the vendored `vendor/vst3sdk` submodule slice). The pure-testable-core / REAPER-facing-shell discipline is preserved throughout: `core/` never includes REAPER or VST3 SDK types, `shell/` is where those hosts are actually touched, `app/` is the extension entry point. Every REAPER API name cited in project docs is correct-by-intent; verify argument order, types, and flag values against `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use.
|
||||
|
||||
Per-module detail — what each file owns, its invariants — lives in the twenty-one per-directory `src/**/CLAUDE.md` files; see the compact map in "Architecture: the load-bearing split" below to find the right one. Landed-phase history lives in `docs/ARCHIVE.md`; current work lives in `docs/COMPLETED.md`, `docs/TODO.md`, and `docs/TODO-1.0.md` — see "Project docs" below.
|
||||
Per-module detail — what each file owns, its invariants — lives in the twenty-two per-directory `src/**/CLAUDE.md` files; see the compact map in "Architecture: the load-bearing split" below to find the right one. Landed-phase history lives in `docs/ARCHIVE.md`; current work lives in `docs/COMPLETED.md`, `docs/TODO.md`, and `docs/TODO-1.0.md` — see "Project docs" below.
|
||||
|
||||
## Settled decisions
|
||||
|
||||
@@ -46,6 +46,16 @@ On a multi-config generator (Visual Studio, Xcode) the bare `ctest` command abov
|
||||
reports every test as "Not Run" — add `-C Debug` (or whichever config was built) to
|
||||
resolve the test executables. Single-config generators (Ninja, Make) need no such flag.
|
||||
|
||||
On a multi-config generator, `cmake --build build` with no `--config` builds **Debug** —
|
||||
there is no `CMAKE_BUILD_TYPE`, no `CMAKE_CXX_FLAGS`, and no IPO/LTO setting anywhere in
|
||||
the build, so nothing is optimized or inlined at that default. The performance
|
||||
guardrails and structural heuristics below (header-inline hot paths, "no LTO
|
||||
configured") presume an **optimizing** build. Shipping, installing, or judging
|
||||
performance requires the Release config explicitly:
|
||||
|
||||
cmake --build build --config Release
|
||||
ctest --test-dir build -C Release
|
||||
|
||||
Every pure module has a corresponding `<module>_tests` executable target that runs without REAPER or a DAW. Targets are declared per directory: each `src/**/CMakeLists.txt` owns its own libraries and their test targets, added via `add_subdirectory` from the root, which keeps only repo-global settings (version, channel, vendor paths). `cmake/reasampler_targets.cmake` holds the two shared declaration helpers. The two loadable-module targets are `reaper_reasampler` (the REAPER extension `.dll`/`.dylib`/`.so`) and `reasampler_vst` (the VST3 instrument; Windows-only, omitted if the `vendor/vst3sdk` slice is absent). The `sample_usage_tests` executable target runs the pure unit tests for `sample_usage` (no REAPER, no DAW).
|
||||
|
||||
### Beta channel build
|
||||
@@ -69,19 +79,20 @@ Add the generated file to the appropriate `APPLE` / Linux `target_sources` block
|
||||
|
||||
### Install / reload
|
||||
|
||||
There is no hot-reload. Copy the built binary into REAPER's `UserPlugins/` folder (Options → Show REAPER resource path) and restart REAPER. Extensions load at startup only.
|
||||
There is no hot-reload. Copy the **Release** build's binary (`build/Release/` on a multi-config generator — not the default `Debug/` output) into REAPER's `UserPlugins/` folder (Options → Show REAPER resource path) and restart REAPER. Extensions load at startup only.
|
||||
|
||||
## Architecture: the load-bearing split
|
||||
|
||||
`core/` holds pure, unit-testable logic — no REAPER or VST3 SDK types, each with a corresponding `<module>_tests` target that runs without a DAW. `shell/` holds the REAPER/host-facing shells — where those SDK types are actually touched. `app/` is the extension entry point. Each of the twenty-one directories below carries its own `CLAUDE.md` with the full module list and that area's invariants — open the relevant one for detail; this file states only repo-wide truth.
|
||||
`core/` holds pure, unit-testable logic — no REAPER or VST3 SDK types, each with a corresponding `<module>_tests` target that runs without a DAW. `shell/` holds the REAPER/host-facing shells — where those SDK types are actually touched. `app/` is the extension entry point. Each of the twenty-two directories below carries its own `CLAUDE.md` with the full module list and that area's invariants — open the relevant one for detail; this file states only repo-wide truth.
|
||||
|
||||
| Directory | Scope |
|
||||
|---|---|
|
||||
| `src/app/` | REAPER extension entry point |
|
||||
| `src/core/audio/` | pure audio-data math |
|
||||
| `src/core/capture/` | pure logic behind the capture pillar |
|
||||
| `src/core/instrument/` | pure VST3-instrument core (engine / map / ui) |
|
||||
| `src/core/instrument/` | pure VST3-instrument core (engine / map / note / ui) |
|
||||
| `src/core/instrument/engine/filter/` | pure per-voice resonant TPT/SVF filter (HP→BP→LP / HP→notch→LP morph, drive stage), run by each `Voice` between the pitch and amp stages |
|
||||
| `src/core/instrument/note/` | the programmed capture-signal model — musical divisions, tempo resolution, anchored offsets |
|
||||
| `src/core/json/` | the hand-rolled JSON lexical layer |
|
||||
| `src/core/model/` | the pure bank/sample index and its multi-bank container |
|
||||
| `src/core/reclaim/` | pure prune orphan computation |
|
||||
@@ -104,7 +115,7 @@ There is no hot-reload. Copy the built binary into REAPER's `UserPlugins/` folde
|
||||
The top-level split is by the pure/shell discipline: `core/` never includes REAPER or VST3 SDK
|
||||
types; `shell/` is where those host types are actually touched — the discriminator is "may this
|
||||
file touch a host type, REAPER *or* VST3 SDK." Subsystem directories sit beneath `core/` (see the
|
||||
table above); `core/instrument/` further subdivides into `engine/` / `map/` / `ui/`. Namespaces
|
||||
table above); `core/instrument/` further subdivides into `engine/` / `map/` / `note/` / `ui/`. Namespaces
|
||||
mirror directories — `reasampler::<subsystem>` for `core/`, house style for `shell/`. `app/` holds
|
||||
`main.cpp` only: API-pointer ownership, `ReaperPluginEntry`, and dispatch.
|
||||
|
||||
|
||||
@@ -228,3 +228,88 @@ uniform key widths, note-name tooltips, root displayed and settable.
|
||||
carries a "confirm the fix survives DPI scaling" item and now genuinely inherits it.
|
||||
- A stale-hover latch was fixed across **all** drag kinds and both drag-termination
|
||||
paths, wider than the strip work that surfaced it.
|
||||
|
||||
### Θ-W3-T1 — live-parameter-delivery
|
||||
|
||||
Continuous playback controls are now delivered live to sounding voices instead of being
|
||||
latched at note-on. New `src/core/instrument/engine/live_params.{h,cpp}` holds a
|
||||
seqlock-published `LiveValues` block owned at **processor-instance scope**, above
|
||||
`LoadedInstrument`, so `live_` and `draining_` observe the same one (a drain-slot voice
|
||||
tracks the knob, which is the desired behavior). `foldLive(const PlayParams&)` is the
|
||||
single derivation from the value type; `PlayParams` stays a plain copyable value type.
|
||||
|
||||
**Daniel's two decisions, both implemented:**
|
||||
- **Reload tier = Grouping B.** Continuous knobs live (filter cutoff/Q/morph/drive/mod
|
||||
amount/key-track; every stage time and level on all three envelopes). Root note, loop
|
||||
span, and start frame still trigger a full reload.
|
||||
- **Mid-stage rule = candidate (iv), hold normalized stage position.** φ =
|
||||
elapsed/duration held fixed across a duration change, then advancing at
|
||||
1/newDuration — expressed over normalized position specifically so Θ-W3-T2's
|
||||
per-segment curve exponent composes with it.
|
||||
|
||||
**Deviations from spec:**
|
||||
- **Trigger's %-length and fades are NOT live** — they are baked into `SampleData` at
|
||||
build, so reload is the only tier that can deliver them. Consequence: a Trigger-mode
|
||||
instance gets zero live amp delivery until Θ-W3-T2 folds the fade pair into the AHD.
|
||||
The five non-live exclusions (`kKeyTrack`, `kFilterVel`, `kTrigLength`,
|
||||
`kTrigFadeIn`, `kTrigFadeOut`) are documented in `src/core/instrument/ui/deck_groups.h`,
|
||||
now their single home.
|
||||
- Open question 3 resolved as **F2 + seqlock**; open question 4 (sub-block resolution)
|
||||
was not built but not foreclosed — the writer interface assumes no UI thread; open
|
||||
question 5 verified — a live edit still persists, `commitLive` keeps the
|
||||
`setInstrumentParams` write.
|
||||
- A filter envelope only advances while its depth is non-zero (the exact-skip at
|
||||
`modAmount == 0`), which is what keeps the at-rest path byte-identical.
|
||||
|
||||
### Ξ-W1-T1 — tracking-consolidation
|
||||
|
||||
Consolidates the provenance/usage territory into one system: the retired
|
||||
`owned_manifest` gives way to a new `src/core/tracking/` directory holding
|
||||
`origin_ledger` (the record family — `OriginRecord`/`OriginKind`, the insertion-ordered
|
||||
`OriginLedger`, its JSON codec, and the `Fresh`/`Loaded`/`Unreadable`/`FutureVersion`
|
||||
load classification) and `tracking_authority` (the one decision surface:
|
||||
`pruneProtection` and `tiedUsageExists`). Both prune's protected set and the resample's
|
||||
replace-vs-add decision are computed from one borrowed `TrackingState`, so the two
|
||||
safety-critical consumers cannot drift apart. `isAbsolutePath` was hoisted out to a new
|
||||
`src/core/util/relative_path.h`, shared with `bank_model`'s `Sample.relativePath`.
|
||||
|
||||
**Deviations from spec:**
|
||||
- The deferred persisted-instance-identity fix was **not** folded in — open question 5
|
||||
resolved as "restate the deferral." `docs/TODO.md` already carries the sharpened
|
||||
rationale (the session-epoch candidate and its sibling-drop flaw); not duplicated here.
|
||||
- `sample_usage` deliberately **stays in `core/wire`** — the consolidation is of the
|
||||
*decisions*, not the codecs.
|
||||
- A realtime record interrupted by a project switch strands an untracked WAV in the old
|
||||
project's bank folder. Resolved as document-don't-delete (prune is the exclusive
|
||||
deletion authority); `docs/TODO.md` carries the entry.
|
||||
- `PruneReport` fields were renamed; a malformed ledger is now reported as a distinct
|
||||
blocker with its own recovery instructions.
|
||||
|
||||
### Ξ-W1-T2 — note-program-model
|
||||
|
||||
Lands the programmed-capture-signal model as a new pure module directory,
|
||||
`src/core/instrument/note/` — a fourth peer of `engine/`/`map/`/`ui/` under
|
||||
`core/instrument/` — holding `musical_division` (the 1/64–64/1 ladder with
|
||||
dotted/triplet multipliers, the 39-entry picker order), `tempo` (validated BPM plus
|
||||
every beats↔seconds↔ms conversion), and `note_program` (`Velocity`, the denominated
|
||||
`OffsetAmount`, the anchored `StartOffset`/`EndOffset`, the `NoteProgram` record, and
|
||||
`resolveNote`).
|
||||
|
||||
**Deviations / resolutions from spec:**
|
||||
- Open question "negative offsets" resolved: both directions are legal and the sign is
|
||||
uniform (positive is later in time); only an *inverted* window is refused, reported
|
||||
via `ResolvedNote::windowCollapsed`.
|
||||
- Open question "denomination seam" confirmed: note length is musical-division-only;
|
||||
the ms/beats duality belongs to the offsets alone. An offset stores the denomination
|
||||
it was **entered in**, deriving the other view on demand, so a beats offset follows a
|
||||
tempo change and a ms offset holds still.
|
||||
- Module name/location resolved as `src/core/instrument/note/` — three modules, not
|
||||
one, with the layering enforced by the CMake link line.
|
||||
- **Beyond spec:** every value type closes its domain at construction behind a single
|
||||
normalizing door (`makeDivision`, `offsetOf`, `Tempo::fromBpm`, `Velocity::of`), with
|
||||
private value constructors. Consequence: `resolveNote` needs no failure path and
|
||||
`ResolvedNote` no validity flag, because every returned field is finite for every
|
||||
constructible program and tempo. Junk detection is relocated to the future codec,
|
||||
which sees both the bytes it read and the value construction produced. `NoteProgram`
|
||||
deliberately carries no MIDI note number — render pitch is deferred to Ξ-W2 as an
|
||||
additive field.
|
||||
|
||||
+62
-449
@@ -28,21 +28,19 @@ transliterate: **Θ → `th`**, **Ξ → `xi`**. So Θ-W1-T1 dispatches into
|
||||
|
||||
## Decision state
|
||||
|
||||
**Two questions in this plan await a Daniel decision, and both are in Θ-W3-T1.**
|
||||
Everything carried forward from `TODO-1.0.md` is still classified **[verify]**
|
||||
(answerable by reading code or running the DAW) or **[propose]** (a design call made at
|
||||
implementation review with a proposal, not a Daniel call); that classification is
|
||||
preserved per question, attached to the track that will answer it.
|
||||
Everything carried forward from `TODO-1.0.md` is classified **[verify]** (answerable by
|
||||
reading code or running the DAW) or **[propose]** (a design call made at implementation
|
||||
review with a proposal, not a Daniel call); that classification is preserved per
|
||||
question, attached to the track that will answer it.
|
||||
|
||||
Θ-W3-T1 (`live-parameter-delivery`) is the exception, and it is an honest one: it is not
|
||||
from `TODO-1.0.md` — it arose from Θ-W2-T1's implementation review — and it carries two
|
||||
genuine **[Daniel]** questions that no amount of code-reading answers. **Which edits stop
|
||||
triggering an instrument reload** is an edit-model call about the product's shape, and
|
||||
**what a stage-time change does to a voice already inside that stage** is sound-defining.
|
||||
Both are stated in that track with candidates laid out and neither pre-picked. The track
|
||||
should not be dispatched at full scope until they are answered; a reduced-scope fallback
|
||||
is named there so the track is not hard-blocked if Daniel would rather rule after seeing
|
||||
the mechanism work.
|
||||
Θ-W3-T1's two genuine **[Daniel]** questions — which no amount of code-reading could
|
||||
answer — are both ruled on and the track has landed; see `docs/COMPLETED.md` for the
|
||||
full narrative. **Reload tier = Grouping B** (continuous knobs live: filter cutoff/Q/
|
||||
morph/drive/mod amount/key-track, every envelope stage time and level; root note, loop
|
||||
span, and start frame still trigger a full reload). **Mid-stage rule = candidate (iv),
|
||||
hold normalized stage position** (φ = elapsed/duration held fixed across a duration
|
||||
change, then advancing at 1/newDuration). No other track in this plan currently carries
|
||||
an unanswered **[Daniel]**-class question.
|
||||
|
||||
### Flagged for awareness — not blocking, but decision-grade
|
||||
|
||||
@@ -55,19 +53,6 @@ the mechanism work.
|
||||
deferred relay shape; see that track. Consequence to hold: if every candidate fails
|
||||
verification, item 15's "one click from inside the VST" framing is what gives, not
|
||||
the read-only invariant — the fallback is a bindable extension-side action.
|
||||
2. **Item 17's "100% robust" and `docs/TODO.md`'s deferred persisted-instance-identity
|
||||
fix sit one day apart and point opposite ways.** On 2026-07-28 Daniel accepted
|
||||
shipping the safe-but-incomplete usage tracking and deferring the persisted-nonce
|
||||
fix; on 2026-07-29 he required the tracking be "consolidated and made 100% robust."
|
||||
My reading: those do not conflict — "robust" is a *safety* strength claim (no
|
||||
destructive act follows from ambiguity), while the deferred wart is a *completeness*
|
||||
one (prune stops reclaiming after a reopen; the bank folder grows, nothing is lost).
|
||||
Ξ-W1-T1 carries this as a [propose] question: fold the deferred fix in, or restate
|
||||
the deferral explicitly in `docs/TODO.md` terms. I am flagging it because the
|
||||
consolidation is the natural moment to do it, not because it blocks.
|
||||
3. **Phase Ξ Wave 1 is concurrency-safe with Phase Θ** (from Θ-W2 onward). See
|
||||
"Running Θ and Ξ concurrently" below. Whether to spend a specialist that way is a
|
||||
scheduling call, not a plan decision.
|
||||
|
||||
## Phase-wide acceptance criteria
|
||||
|
||||
@@ -204,10 +189,12 @@ rework. T1 first means every parameter T2 introduces is authored into the delive
|
||||
mechanism from the start. The cost of the ordering is honest and worth stating: T2 is the
|
||||
wave's user-visible payload and T1 delays it by one track.
|
||||
|
||||
T1 must specify its mid-stage rule in terms that survive T2 — see that track's open
|
||||
question 2, whose candidates are deliberately expressed over *normalized stage position*
|
||||
rather than over output level, because T2's exponent is a pure map of that position and
|
||||
must compose with the rule rather than invalidate it.
|
||||
T1 has landed and specified its mid-stage rule in terms that survive T2: **hold
|
||||
normalized stage position** — φ = elapsed/duration held fixed across a duration change,
|
||||
then advancing at 1/newDuration (Daniel's pick among the candidates T1 laid out; see
|
||||
`docs/COMPLETED.md`). It is expressed over *normalized stage position* rather than over
|
||||
output level specifically so T2's exponent is a pure map of that position and composes
|
||||
with the rule rather than invalidating it.
|
||||
|
||||
Beyond the ordering, the W3 collision is the phase's densest, which is why neither track
|
||||
splits further: the envelope parameter model, the per-sample envelope evaluation, the
|
||||
@@ -219,247 +206,30 @@ forward/inverse map pair. The serialization is the correct answer.
|
||||
|
||||
#### Θ-W3-T1 — `live-parameter-delivery`
|
||||
|
||||
**Goal.** Retire note-on latching as the delivery model for continuous playback controls:
|
||||
a knob moved while a note is sounding changes *that* note, not merely the next one.
|
||||
Establishes the live-parameter block and its publish path, splits the editor's commit
|
||||
routing so a parameter edit stops rebuilding the instrument, and makes reachable a
|
||||
property the filter DSP was deliberately built with and that nothing currently exercises.
|
||||
|
||||
**Consolidates:** nothing from `TODO-1.0.md`. This track carries no item number — it
|
||||
arose from Θ-W2-T1's implementation review. See "Work in this plan that is not one of the
|
||||
seventeen" below the traceability table.
|
||||
|
||||
**Origin — Daniel's ruling, recorded verbatim** (2026-07-30, on the filter's latched
|
||||
cutoff):
|
||||
|
||||
> *"hell no, I was going to bring that up for the other envelopes. We must live compute,
|
||||
> latching the parameters at note on is not acceptable. long term these will be
|
||||
> automatable parameters."*
|
||||
|
||||
The ruling rejects the **precedent, not one instance of it.** Every playback parameter is
|
||||
latched into the `Voice` at note-on today — the filter (Θ-W2-T1), the amp AHDSR, and the
|
||||
pitch envelope alike. It also silently defeats a property that was built on purpose:
|
||||
`src/core/instrument/engine/filter/CLAUDE.md` records that `prepare()` does **not** clear
|
||||
state precisely so a live parameter move glides rather than clicks. Nothing reaches it.
|
||||
|
||||
**The architectural finding — read this before scoping the track.** A staff-engineer
|
||||
attempted the live-cutoff fix inside Θ-W2-T1 and correctly stopped. The fix is **not
|
||||
local to the engine:**
|
||||
|
||||
- An editor knob release calls `commitAndReload()` → `reloadInstrument()`, which re-reads
|
||||
the bridge, **re-decodes the WAV from disk**, builds a fresh `SampleData` +
|
||||
`VoiceEngine`, and atomic-swaps it into `live_`.
|
||||
- The displaced instrument moves to `draining_`, where already-sounding voices keep
|
||||
rendering **their own frozen `SampleData`**. New note-ons route only to `live_`.
|
||||
- Therefore a `Voice` that read `sample_->play` per frame instead of latching would
|
||||
**still** not move a sustaining note: that note lives in a different snapshot, and that
|
||||
snapshot is by design never updated again.
|
||||
|
||||
Two consequences bind the whole track. The live-parameter block's ownership must sit
|
||||
**above** `LoadedInstrument` so `live_` and `draining_` observe the same one — a block
|
||||
owned by a snapshot reproduces the defect exactly. And the set of edits that trigger a
|
||||
reload has to shrink; that is the open product question below.
|
||||
|
||||
**Three commit tiers already exist. The work is reassigning edits across them, not
|
||||
inventing a taxonomy.**
|
||||
|
||||
1. **Full reload** — `commitAndReload()` → `reloadInstrument()`: bridge read, WAV
|
||||
re-decode, fresh engine, snapshot swap. Today: every parameter edit.
|
||||
2. **Engine rebuild** — `rebuildVoiceEngine()`: rebuilds around the already-decoded
|
||||
`SampleData`, no disk, same drain-slot swap. Today: voice count, voice mode, mono
|
||||
trigger.
|
||||
3. **Live** — `masterGain_`: a lock-free atomic the audio thread reads per block and
|
||||
ramps toward per sample (`gainCurrent_` / `gainRampStep_`), no rebuild, no snapshot.
|
||||
Today: master gain alone.
|
||||
|
||||
Tier 3 is the shape this track generalizes, and it is a **house precedent already
|
||||
shipped and already zipper-free** — not a pattern imported from elsewhere. Read it before
|
||||
designing the block.
|
||||
|
||||
**Behavior — what must become live.** Every continuous user-facing playback control:
|
||||
|
||||
- **Filter** — cutoff, Q, morph, drive, mod amount, key-track.
|
||||
- **All three envelopes** — every stage time and every stage level, on the amp AHDSR, the
|
||||
pitch envelope, and the filter AHDSR alike.
|
||||
|
||||
**Behavior — what stays latched per note, where making it live is a bug.** These are
|
||||
facts about the note event, not controls, and a design that "generalizes" them into the
|
||||
live block has misread the ruling:
|
||||
|
||||
- **Velocity**, and everything derived from it — in particular the velocity-curve
|
||||
evaluation result (`velocityGain_`, `filterVelOffset_`), which is evaluated once in
|
||||
`Voice::start` on purpose.
|
||||
- **The note number**, and the pitch ratio derived from it against the root note
|
||||
(`baseRatio_`). Moving a root-note or key-track control must not retune a sounding
|
||||
note; the legato `retune()` path is the only sanctioned mid-note pitch move.
|
||||
- **The sample/capture identity and its decoded PCM.** Loading a different capture is a
|
||||
new sound, not a parameter change.
|
||||
|
||||
**Behavior — the live block.** Requirements, stated as constraints rather than as a
|
||||
chosen design:
|
||||
|
||||
- **Ownership outlives every `LoadedInstrument`.** One block per processor instance,
|
||||
observed identically by `live_` and `draining_`. A drain-slot voice therefore keeps
|
||||
tracking the knob — that is the *desired* behavior (it is the note the user is
|
||||
hearing), and it should be stated so nobody later "fixes" it.
|
||||
- **`PlayParams` stays a plain copyable value type.** `component_state_io`, `sample_map`,
|
||||
and the editor all pass it by value; atomics cannot simply be pushed into it.
|
||||
- **No lock on the audio path**, and **no torn read**: the editor writes on the UI
|
||||
thread while `process()` reads on the audio thread, so observing half of one edit and
|
||||
half of another mid-block is a real hazard the design must close, not a theoretical one.
|
||||
- **No constructor growth, and no dependency grab-bag.** An `IServiceProvider`-shaped
|
||||
parameter object threaded into `VoiceEngine`/`Voice` constructors is a smell and is
|
||||
rejected. One candidate that satisfies both: a single `const LiveParams*` field on
|
||||
`SampleData`, defaulting to `nullptr` so the bare core behaves exactly as today
|
||||
(latched, byte-identical, existing tests untouched) and the shell sets it on the path
|
||||
`SampleData` already travels. That is a [propose]-class mechanism note, not a mandate.
|
||||
|
||||
**Behavior — the per-frame constraint (Daniel's standing non-negotiable).** Root
|
||||
`CLAUDE.md`'s performance guardrails and structural heuristic 3 both bind here:
|
||||
|
||||
- **No allocation and no virtual dispatch on the per-voice-per-sample path.** A per-frame
|
||||
copy of a params struct is a violation; so is an added header→TU indirection.
|
||||
`envelopes.h`'s evaluators and `Voice::advanceFrame` are header-inline by RT
|
||||
constraint (no LTO configured) — concrete, no common base, no virtual `tick()`.
|
||||
- **Observation happens at block boundaries, not per frame.** The voice already caches
|
||||
its filter parameters in members (`filterCutoffNorm_`, `filterModAmount_`,
|
||||
`filterKeyTrack_`, …); the minimal live implementation refreshes those cached members
|
||||
at a block boundary instead of only at note-on, leaving the per-sample shape unchanged.
|
||||
At 512 frames / 48 kHz that is ~93 Hz of control resolution — ample for a hand on a
|
||||
knob, and the coarse floor for automation later.
|
||||
- **Smoothing is required, and it is what makes the block-rate step inaudible.** A
|
||||
block-rate jump in a base value is a step; `masterGain_`'s per-sample linear ramp
|
||||
toward the target is the in-house answer. **The ramp must terminate exactly**, not
|
||||
asymptotically: `tickFilterCutoff`'s two exact skips compare the value itself, so a
|
||||
never-quite-arriving one-pole would pin the filter on the always-re-solve path forever.
|
||||
Cost while a cutoff ramp is running is known and affordable — 15.5 ns/frame/voice
|
||||
measured in `engine/filter/CLAUDE.md`, ~1.2% of one core at 16 voices — and it returns
|
||||
to the skip path when the ramp completes.
|
||||
|
||||
**Acceptance criteria.**
|
||||
|
||||
- With a note held, sweeping filter cutoff, Q, morph, drive, mod amount, or key-track
|
||||
audibly moves *that* note. Same for every stage time and stage level on all three
|
||||
envelopes.
|
||||
- A knob move on a live parameter performs **no** WAV re-decode and **no** snapshot
|
||||
rebuild — verified against `reloadInstrument`, not by ear alone.
|
||||
- A note sounding out of the **drain slot** responds to a live parameter move identically
|
||||
to a `live_` voice.
|
||||
- **No click, no step, no zipper** on any live parameter move, at any block size —
|
||||
including a full-range cutoff sweep at maximum Q, and a sustain-level change on a note
|
||||
held in Sustain.
|
||||
- **The per-sample path costs nothing at rest.** With no live value changed, the render
|
||||
is unchanged in call/inline shape from today; all live observation is at block
|
||||
boundaries. No allocation, no lock, no virtual call is added on the audio thread.
|
||||
- The audio thread never observes a partially-applied edit within one block.
|
||||
- `PlayParams` is still a plain copyable value type, still passed by value by
|
||||
`component_state_io`, `sample_map`, and the editor.
|
||||
- **Velocity, the velocity-curve result, the note number, the pitch ratio, and the
|
||||
decoded PCM are still latched at note-on.** Dragging a velocity-curve point does not
|
||||
retune or re-gain a sounding note.
|
||||
- **With no live block attached the core is byte-identical to today** — `sampler_core`'s
|
||||
existing regression tests pass unchanged, including the bare-engine baselines.
|
||||
- **Migration:** a project saved before this change reopens sounding identical. Persisted
|
||||
values are unchanged; `ComponentState` gains no version bump unless the chosen
|
||||
representation forces one, and if it does, the bump is additive and pre-existing blobs
|
||||
lift with no audible change.
|
||||
- The filter DSP's glide property is finally exercised by a test: a cutoff move across a
|
||||
`prepare()` without a `reset()` produces no output discontinuity.
|
||||
|
||||
**Open questions.**
|
||||
|
||||
1. **Which edits stop triggering a reload? [Daniel]** Sample/capture selection genuinely
|
||||
needs a reload; filter cutoff plainly does not. The line between them is an edit-model
|
||||
decision, not an implementation one. Three candidate groupings, in widening order:
|
||||
- **Grouping C — continuous controls only.** Only knob-valued continuous controls go
|
||||
live (filter cutoff/Q/morph/drive/mod/key-track, envelope times and levels). Every
|
||||
discrete toggle — play mode, pitch engine, filter law, filter enable, channel mode,
|
||||
velocity curve — keeps reloading. Narrowest blast radius, smallest verification
|
||||
surface, and it fully satisfies the ruling as stated.
|
||||
- **Grouping B — capture-anchored edits reload.** C, plus: the three capture-anchored
|
||||
overrides (root note, loop span, start frame) also reload, because they name
|
||||
positions in the decoded PCM and `loadSelection` already treats them as one family.
|
||||
Everything else goes live or drops to the tier-2 rebuild.
|
||||
- **Grouping A — only the sound source reloads.** Reload if and only if the identity
|
||||
of the decoded audio changes: capture selection and channel mode (a decode policy).
|
||||
Everything else is live or a rebuild. Widest live surface, largest verification
|
||||
surface, and the one that most nearly matches "these will be automatable
|
||||
parameters."
|
||||
No pick is made here. **Proposed fallback if Daniel would rather rule after seeing the
|
||||
mechanism run:** build the mechanism and ship Grouping C, leaving B and A reachable as
|
||||
later reassignments rather than rework — the tier a given edit sits in is a routing
|
||||
decision at the editor's commit site, not a property of the block. That is a scope
|
||||
proposal, not a decision taken.
|
||||
2. **What does a stage-time change do to a voice already inside that stage? [Daniel]**
|
||||
Sound-defining, and there is no single right answer. Bounded by one non-negotiable:
|
||||
**it must not click.** A discontinuity in output level on a parameter move is a
|
||||
defect, and a sustain level changed while a voice is held must glide, not step. The
|
||||
candidates, with the click property of each named:
|
||||
- **(i) Jump / recompute from absolute elapsed.** Keep `framesInStage_`, divide by the
|
||||
new duration. This is what today's code would do if params were simply swapped —
|
||||
`level_ = framesInStage_ / attackFrames`. Steps the level discontinuously. Fails the
|
||||
non-negotiable unaided.
|
||||
- **(ii) Clamp.** As (i), but a stage whose new duration is already exceeded completes
|
||||
immediately. Still steps.
|
||||
- **(iii) Re-derive rate from the current level.** Hold the level, recompute the
|
||||
per-frame advance so the stage completes at the new duration measured *from now*.
|
||||
Continuous by construction. Costs: the stage's total time becomes elapsed +
|
||||
remaining rather than the dialed value, and it needs a rate representation. This is
|
||||
what a hardware EG with a rate DAC does.
|
||||
- **(iv) Hold normalized stage position.** Keep phase φ = elapsed / duration fixed
|
||||
across the change, then advance at 1/newDuration. Continuous in level (φ unchanged →
|
||||
level unchanged) and the remaining stage takes its share of the dialed duration.
|
||||
- **(v) Track absolute elapsed against the new duration.** Identical to (i); listed so
|
||||
it is visibly not a distinct third option.
|
||||
- **(vi) Keep (i) and smooth the envelope output.** Accept the recompute and put a
|
||||
short declick ramp on the envelope's output level — the codebase already owns a
|
||||
bounded-blend declick primitive (`kDeclickDecay`, `seedDeclick`) built for exactly
|
||||
this class of step.
|
||||
Only **(iii)** and **(iv)** are continuous without added machinery, and both compose
|
||||
with T2's curve exponent because both are expressed over normalized position, which
|
||||
the exponent is a pure map of. **(vi)** is the option that makes the cheap rule
|
||||
acceptable, at the cost of a second smoother. Sustain is a separate sub-case under
|
||||
every candidate: it is a *level*, not a timed stage, so a live sustain edit is a direct
|
||||
level step and needs the ramp regardless of which rule wins.
|
||||
3. **Representation: atomics in the value type, or a second live representation?
|
||||
[propose]** Two forks, both with real costs:
|
||||
- **F1 — atomics in the value type.** Push atomics into the live-relevant fields of
|
||||
`PlayParams` / `FilterParams` / `AdsrParams`. One model, no mapping to drift.
|
||||
Blast radius is wide and probably disqualifying: atomics are non-copyable, and
|
||||
`PlayParams` is passed by value by `component_state_io`, `sample_map`, and the
|
||||
editor — the plain-copyable-value-type constraint above is a hard bound on how far
|
||||
this fork can go.
|
||||
- **F2 — a second live-parameter representation.** A parallel block the audio thread
|
||||
reads, published by the UI thread. Keeps the value type plain; the cost is that the
|
||||
model exists twice and the mapping between them can drift. Mitigation to require if
|
||||
this fork wins: derive the live block from `PlayParams` through exactly **one**
|
||||
explicit fold function, so there is a single writer and a single site to keep in
|
||||
step. Three publication mechanisms sit under F2, and the choice among them is the
|
||||
torn-read answer: a **seqlock** (odd/even generation counter, audio thread copies
|
||||
the block once per block and retries on a torn read — the standard single-writer
|
||||
lock-free pattern); **per-parameter atomics** (simplest, exactly what `masterGain_`
|
||||
does today, but offers no coherence *across* parameters — a set like an envelope's
|
||||
A/H/D/S/R can be observed mid-edit); or a **double-buffered block published by
|
||||
atomic pointer swap** (coherent, no retry, needs a reclaim rule — and the processor
|
||||
already owns that muscle in `live_`/`draining_`/`graveyard_`).
|
||||
4. **Does the block need sample-accurate (sub-block) resolution now? [propose]** Daniel's
|
||||
ruling names automation as the long-term destination, and VST3 delivers parameter
|
||||
changes on the audio thread with sample offsets inside `ProcessData` — so per-block is
|
||||
the coarse floor, not the ceiling. Building sub-block splitting now is speculative;
|
||||
*foreclosing* it is the failure mode. The requirement this track must carry either
|
||||
way: **the block's writer interface must not assume a UI thread**, so the audio
|
||||
thread's own parameter-change queue can drive it later without a redesign.
|
||||
5. **Does a live edit still persist immediately? [verify]** Today `commitAndReload` also
|
||||
writes the edited set into `params_`, which is what `getState` serializes. A live path
|
||||
must keep making that UI-thread write so a saved project carries the edit — but that
|
||||
write is no longer the audio thread's source. Confirm by reading that nothing else
|
||||
depends on the reload as its persistence trigger.
|
||||
**Θ-W3-T1 has landed** — see `docs/COMPLETED.md` for the full narrative. Continuous
|
||||
playback controls (filter cutoff/Q/morph/drive/mod amount/key-track; every stage time
|
||||
and level on all three envelopes) are now delivered live to sounding voices via a
|
||||
seqlock-published `LiveValues` block owned at processor-instance scope, above
|
||||
`LoadedInstrument`, so a drain-slot voice keeps tracking the knob (the desired
|
||||
behavior). `foldLive(const PlayParams&)` is the single derivation from the value type.
|
||||
Both of Daniel's ruling questions are answered and implemented: **reload tier =
|
||||
Grouping B** (root note, loop span, and start frame still reload; everything else
|
||||
continuous goes live), and **mid-stage rule = candidate (iv), hold normalized stage
|
||||
position** (φ = elapsed/duration held fixed across a duration change, then advancing
|
||||
at 1/newDuration) — expressed over normalized position specifically so T2's
|
||||
per-segment curve exponent, below, composes with it. **Trigger's %-length and fades
|
||||
are NOT live** — they are baked into `SampleData` at build, so a Trigger-mode instance
|
||||
gets zero live amp delivery until T2 folds the fade pair into the AHD. The five
|
||||
non-live exclusions (`kKeyTrack`, `kFilterVel`, `kTrigLength`, `kTrigFadeIn`,
|
||||
`kTrigFadeOut`) are documented in `src/core/instrument/ui/deck_groups.h`, now their
|
||||
single home.
|
||||
|
||||
#### Θ-W3-T2 — `staged-envelope-curves`
|
||||
|
||||
**Prerequisite: Θ-W3-T1 must land first** — see the wave preamble. Both tracks write
|
||||
**Prerequisite: Θ-W3-T1 has landed** — see `docs/COMPLETED.md`. Both tracks write
|
||||
`envelopes.h`'s stage math, and this track's curve exponent must be authored as a map of
|
||||
whatever normalized stage position T1's mid-stage rule establishes.
|
||||
the normalized stage position T1's mid-stage rule holds fixed (φ = elapsed/duration,
|
||||
candidate (iv)).
|
||||
|
||||
**Goal.** Grow the envelope-overlay editor from an amp-only fixture into the shared
|
||||
graphical surface for every envelope, give every envelope shapeable segments, fix the
|
||||
@@ -944,174 +714,35 @@ schedule preference; the feature is not expressible.
|
||||
|
||||
### Ξ-W1 — Consolidated tracking, and the programmed-note model
|
||||
|
||||
**Depends on:** nothing in this phase. **Concurrency-safe with Phase Θ from Θ-W2 onward** —
|
||||
see the note at the end of this phase.
|
||||
|
||||
**Two tracks, in priority order.** Disjoint: T1 is entirely extension-side record-keeping;
|
||||
T2 is a new pure module with no existing call site.
|
||||
|
||||
#### Ξ-W1-T1 — `tracking-consolidation`
|
||||
|
||||
**Goal.** Make the provenance/usage territory **one system, 100% robust** — recipe
|
||||
provenance, live-instance usage, and recapture lineage as facets of the same
|
||||
record-keeping, answering both safety-critical consumers from one place.
|
||||
|
||||
**Consolidates item 17.**
|
||||
|
||||
**Surface boundary — owns:** `core/model/provenance`, `core/model/owned_manifest`,
|
||||
`core/wire/sample_usage`, `core/reclaim/prune_reconcile`, `shell/persist/`
|
||||
(`usage_scan`, `prune_fs`, `ext_state_io`'s manifest/provenance keys),
|
||||
`shell/actions/prune_action`. Instrument-side touch is limited to the usage-publish block
|
||||
at the tail of `reloadInstrument` in `shell/instrument/processor_reload.cpp` — **this is
|
||||
the one file shared with Phase Θ; if run concurrently, this track owns that block and
|
||||
Θ-W1-T1 does not touch it.**
|
||||
|
||||
**Behavior.**
|
||||
- **One system, not three mechanisms.** Today the territory holds two separately-grown
|
||||
mechanisms plus one new demand:
|
||||
1. the **capture-recipe fingerprint** — a thin reproducibility record of how a capture
|
||||
was made, deliberately not a restorable chain, conservatively recording nothing when
|
||||
the situation is ambiguous;
|
||||
2. the **instance-usage tracking** — each live instance declares the captures it holds,
|
||||
so prune can never delete a capture a live instance is using, with a fail-safe stance
|
||||
that unreadable usage state halts prune entirely;
|
||||
3. item 15's demand for **recapture lineage** — records tying usages of a capture to it
|
||||
"by the provenance/recaptureing system," deciding replace-vs-add at bake time.
|
||||
These stop being separate ad-hoc mechanisms. Every consumer — the prune's protection
|
||||
decision, the resample's replace-vs-add decision, and any future lineage reader — is
|
||||
answered from the one system.
|
||||
- **What "100% robust" observably means.** Daniel stated the strength, not the mechanics.
|
||||
Three observable implications follow from the stakes, and no further specifics are
|
||||
invented:
|
||||
- **No silent gaps.** Every capture the system itself creates is tracked from the moment
|
||||
of its creation; a recapture carries its lineage **from birth, never backfilled**.
|
||||
There is no window in which a system-created file exists untracked.
|
||||
- **Fail-safe on unreadable or ambiguous state.** Tracking state that cannot be read
|
||||
never yields the destructive answer: the prune deletes nothing (today's settled stance
|
||||
— preserved and generalized, not relaxed), and the resample never takes the replace
|
||||
branch on unreadable lineage.
|
||||
- **Consumers cannot disagree.** The prune's protection answer and the resample's
|
||||
tied-usage answer are different questions with different universes — item 15 settles
|
||||
that the replace-vs-add universe is **narrower** than the prune-protection universe —
|
||||
but both are computed from the same records, so they cannot drift apart.
|
||||
- **Existing guarantees are the floor.** Consolidation must not weaken anything settled:
|
||||
prune still deletes only the system's own orphans and never a referenced or live-held
|
||||
capture; the fail-safe abort on unreadable usage state survives; the recipe
|
||||
fingerprint's record-nothing-when-ambiguous conservatism survives **for the recipe
|
||||
half**. The lineage half is the one place that conservatism is foreclosed — item 15's
|
||||
replace-vs-add must be computable, so a recapture's lineage record is mandatory.
|
||||
- **Why this is safety-critical, stated once:** this territory gates the system's only
|
||||
file-deletion authority (prune) and its only capture-replacement act (resample) — the
|
||||
two places where a tracking error loses a user's audio or sound.
|
||||
|
||||
**Acceptance criteria.**
|
||||
- **One consolidated tracking system answers both safety-critical consumers:** the prune's
|
||||
protected set and the resample's replace-vs-add decision are each computed from it, per
|
||||
their own settled rules; **no separate ad-hoc tracking mechanism remains in the
|
||||
territory.**
|
||||
- **No silent gaps:** a recapture created by item 15's bake is tracked from the instant it
|
||||
exists — a bake followed immediately by a prune, or by a second bake, behaves correctly
|
||||
with no window in which the recapture is untracked or its lineage absent. (Testable
|
||||
ahead of item 15 by simulating a system-created file through the same path.)
|
||||
- **Fail-safe throughout:** with tracking state made unreadable, the prune deletes nothing
|
||||
(and reports what blocked it, per today's behavior) and the resample never takes the
|
||||
replace branch; **no destructive act follows from ambiguity, anywhere in the territory.**
|
||||
- **Every protection settled today holds undiminished after consolidation:** a capture
|
||||
held by a live instance cannot be pruned; files the system did not create are
|
||||
untouchable; unreadable usage state still halts the prune.
|
||||
- **A pre-existing bank lifts into the consolidated system with no loss of protection and
|
||||
no spurious lineage**, and never-recorded remains distinguishable from unreadable.
|
||||
- **Item 15's other-references case is decidable:** for any capture, "does provenance-tied
|
||||
usage exist" has a definite yes/no answer.
|
||||
- The pure fold decisions stay pure and unit-tested without a DAW; the REAPER/filesystem
|
||||
half stays in `shell/persist`.
|
||||
|
||||
**Open questions.** None awaits a Daniel decision — the requirement and its strength are
|
||||
his; the shape is review work.
|
||||
- **The consolidated shape [propose].** What "one system" concretely is — one record
|
||||
family, one authority, how the three facets relate — is design work proposed at review.
|
||||
- **The lineage record [propose, jointly with item 15's naming-and-lineage question].**
|
||||
What constitutes a "usage tie," when it is written, whether it is ever severed, and
|
||||
whether iteration lineage is user-readable from the bank.
|
||||
- **The recipe-fingerprint half of a recapture [propose].** A resample's recipe is the
|
||||
instrument's own settings, not a track chain; whether the fingerprint records a
|
||||
resample-shaped recipe — and what its conservatism means there — is proposed at review.
|
||||
(The lineage half has no record-nothing option; the recipe half may keep one.)
|
||||
- **Never-recorded vs. unreadable [propose].** Pre-existing captures predate lineage
|
||||
records, and the two absences demand opposite treatment: never-recorded means no tied
|
||||
usage exists (replace is legitimate); unreadable means fail-safe. How the consolidated
|
||||
system distinguishes them — and how pre-existing banks lift in without weakening any
|
||||
protection they enjoy today — is proposed at review.
|
||||
- **NEW — does this absorb the deferred persisted-instance-identity fix? [propose].**
|
||||
`docs/TODO.md` carries "Persist ReaSampler 9000 instance identity to let prune reclaim
|
||||
de-referenced captures after reopen," deferred 2026-07-28 as low-risk (safe, but the
|
||||
bank folder grows unbounded after a reopen). "Consolidated and made 100% robust"
|
||||
arrived the next day. My reading is that they do not conflict — robustness is a *safety*
|
||||
claim, the wart is a *completeness* one — but the consolidation is the natural moment to
|
||||
revisit it, and the constraint it must handle is unchanged (a persisted identity is
|
||||
inherited by a Ctrl+D in-place duplicate; a divergent clone must still be detected and
|
||||
protected fail-safe without reintroducing the sibling-drop bug). **Fold it in or
|
||||
explicitly restate the deferral at review; do not leave it ambiguous.**
|
||||
|
||||
---
|
||||
**Landed** — see `docs/COMPLETED.md` for the full narrative. The provenance/usage
|
||||
territory is now one system: a new `src/core/tracking/` directory holds `origin_ledger`
|
||||
(the record family — `OriginRecord`/`OriginKind`, the insertion-ordered `OriginLedger`,
|
||||
its JSON codec, and the `Fresh`/`Loaded`/`Unreadable`/`FutureVersion` load
|
||||
classification) and `tracking_authority` (the one decision surface: `pruneProtection`
|
||||
and `tiedUsageExists`), retiring `core/model/owned_manifest`. Both prune's protected set
|
||||
and the resample's replace-vs-add decision are computed from one borrowed
|
||||
`TrackingState`, so the two safety-critical consumers cannot drift apart.
|
||||
`sample_usage` deliberately **stays in `core/wire`** — the consolidation is of the
|
||||
decisions, not the codecs. The deferred persisted-instance-identity fix was **not**
|
||||
folded in — the deferral is restated in `docs/TODO.md`, its one home. This track
|
||||
landed `OriginRecord`'s birth-time `parentSampleId` chain as the lineage mechanism, but
|
||||
**Ξ-W2-T1's "naming and lineage" open question (jointly held with this track) is
|
||||
unaffected and still theirs to close** — display naming and user-readable iteration
|
||||
lineage were not decided here.
|
||||
|
||||
#### Ξ-W1-T2 — `note-program-model`
|
||||
|
||||
**Goal.** Land the programmed-capture-signal model as a pure, tested module: musical
|
||||
divisions, tempo resolution, and offset anchoring — the arithmetic both the bake and the
|
||||
popup will read.
|
||||
|
||||
**Consolidates item 15 (the capture-signal model; the bake is Ξ-W2-T1 and the popup UI is
|
||||
Ξ-W3-T1).**
|
||||
|
||||
**Surface boundary — owns:** a new pure module (`core/instrument/map/note_program` or
|
||||
similar, name [propose]) plus its tests target and the CMake row. New files only; no
|
||||
existing file edited except `CMakeLists.txt`. Disjoint from T1 by construction.
|
||||
|
||||
**Behavior.**
|
||||
- **Note length is a musical division, not a free duration.** Chosen from divisions
|
||||
spanning **1/64th through 64/1, with dotted and triplet multipliers** (Daniel's
|
||||
examples: `1/8.`, `1/4t`, `1/16`, `4/1`).
|
||||
- **Beats resolve against the project tempo under the cursor.** A beat-denominated value —
|
||||
the note-length division always, the offsets when expressed in beats — resolves to time
|
||||
against "the project tempo under the item cursor" (Daniel's phrase; read plainly: the
|
||||
tempo in effect at the project's cursor position when the preview or bake runs). The
|
||||
module takes the tempo as a parameter; **reading it from REAPER is the shell's job**,
|
||||
not this module's.
|
||||
- **Offsets anchor to note-on and note-off.** The start offset is relative to the
|
||||
programmed note's **note-on**; the end offset is relative to its **note-off**.
|
||||
- **Offsets are expressed in ms AND in beats** — the two denominations are two views of
|
||||
one stored value, and the conversion is this module's. Note length is
|
||||
musical-division-only.
|
||||
- **Velocity is explicit** — a plain 1–127 value carried in the same record. Material
|
||||
because the velocity transfer curves modulate amp (and, post-Θ, pitch and filter) at
|
||||
that velocity.
|
||||
- **The record is one struct**, round-trippable, that the bake renders from and the popup
|
||||
edits. One source of truth: **preview and bake cannot diverge** because they read the
|
||||
same record through the same resolver.
|
||||
|
||||
**Acceptance criteria.**
|
||||
- The division set spans 1/64 through 64/1 inclusive, with dotted and triplet variants,
|
||||
and each resolves to the correct duration at a given tempo — asserted at the extremes
|
||||
and at Daniel's four named examples.
|
||||
- The same division yields a correspondingly different duration when the supplied tempo
|
||||
differs (tests assert the proportionality, not a hardcoded rate).
|
||||
- Offsets round-trip losslessly between ms and beats at a given tempo.
|
||||
- Anchoring is explicit in the type: a start offset is note-on-relative and an end offset
|
||||
is note-off-relative, and the resolved window is computed from both plus the note length.
|
||||
- **No hardcoded sample rate and no hardcoded tempo** anywhere in the module (the standing
|
||||
rate-free-seconds ruling).
|
||||
- Builds and tests without REAPER, VST3, or a DAW.
|
||||
|
||||
**Open questions.**
|
||||
- **Are negative offsets meaningful? [propose]** Unchanged from the source doc. A negative
|
||||
start offset (capture beginning before note-on) is plausible; a negative end offset
|
||||
(truncating before note-off) is also plausible. Propose the answer with the type.
|
||||
- **The denomination seam [propose].** The first answer round expressed the offsets "in ms
|
||||
AND in beats" while note length is musical-division-only. The plain reading is that the
|
||||
ms/beats duality applies **to the offsets only**. If an ms display or entry for note
|
||||
length seems wanted at implementation, propose it at review rather than assuming either
|
||||
way.
|
||||
**Landed** — see `docs/COMPLETED.md` for the full narrative. The programmed-capture-
|
||||
signal model is a new pure module directory, `src/core/instrument/note/` — a fourth
|
||||
peer of `engine/`/`map/`/`ui/` under `core/instrument/` — holding `musical_division`,
|
||||
`tempo`, and `note_program` (`Velocity`, the denominated `OffsetAmount`, the anchored
|
||||
`StartOffset`/`EndOffset`, `NoteProgram`, `resolveNote`). **Both open questions below
|
||||
are answered, for Ξ-W3-T1:** negative offsets are legal in both directions (sign
|
||||
uniform, positive is later in time; only an inverted window is refused, reported via
|
||||
`ResolvedNote::windowCollapsed`), and the denomination seam is confirmed — note length
|
||||
stays musical-division-only, and an offset stores the denomination it was entered in.
|
||||
|
||||
---
|
||||
|
||||
@@ -1360,24 +991,6 @@ must be closed.
|
||||
|
||||
---
|
||||
|
||||
### Running Θ and Ξ concurrently
|
||||
|
||||
**Ξ-W1 is concurrency-safe with Phase Θ from Θ-W2 onward.** Ξ-W1-T1 is extension-side
|
||||
record-keeping (`core/model`, `core/wire`, `core/reclaim`, `shell/persist`,
|
||||
`shell/actions`); Ξ-W1-T2 is a new pure module. Neither is on Θ's critical path.
|
||||
|
||||
**Two conditions.**
|
||||
1. **Not during Θ-W1.** Θ-W1-T1 re-seams the whole instrument, including
|
||||
`processor_reload.cpp`, which is the one file Ξ-W1-T1 also touches (the usage-publish
|
||||
block). Wait for Θ-W1 to land.
|
||||
2. **Ξ-W1-T1 owns the usage-publish block** for the duration; no Θ track edits it.
|
||||
|
||||
**Recommendation:** run Ξ-W1 concurrently if a specialist is spare — it ships prune
|
||||
robustness early and removes the largest dependency from Ξ-W2's critical path. Otherwise
|
||||
sequence it after Θ. This is a scheduling call, not a plan decision.
|
||||
|
||||
---
|
||||
|
||||
## Traceability — all seventeen items
|
||||
|
||||
The check that nothing was dropped. Every row points at a track that exists above.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# src/core/instrument — pure VST3-instrument core (engine / map / ui)
|
||||
# src/core/instrument — pure VST3-instrument core (engine / map / note / ui)
|
||||
|
||||
## Scope
|
||||
|
||||
The ReaSampler 9000 instrument's pure, REAPER-free, VST3-free, unit-tested core, in three
|
||||
The ReaSampler 9000 instrument's pure, REAPER-free, VST3-free, unit-tested core, in four
|
||||
subdirectories:
|
||||
|
||||
- **`engine/`** — the polyphonic voice engine, the one set of play params, pitch shifting,
|
||||
@@ -11,6 +11,9 @@ subdirectories:
|
||||
`ComponentState` codec, and the small pure helpers the engine/shell share
|
||||
(bank-generation sync, bridge-read marshalling, note-name parsing, Trigger
|
||||
frame↔fraction conversion).
|
||||
- **`note/`** — the programmed capture-signal model: musical-division note length, tempo
|
||||
resolution, and anchored start/end offsets — the one record and resolver a
|
||||
capture-signal popup and the offline bake read from, so they cannot diverge.
|
||||
- **`ui/`** — pure editor geometry/hit-test modules (the band-stack allocator and its band
|
||||
interiors, waveform, keyboard strip, capture browser, param controls, envelope
|
||||
overlay/edit). These are geometry-and-math only; the LICE draw + REAPER/VST3 plumbing is
|
||||
|
||||
Reference in New Issue
Block a user