docs: close Γ-F6 and resequence Phase Γ into three waves
Dynamic latency ships as ruled; the restart cost is re-attributed to our own setActive and filed in TODO. The reflow splits canvas (W1-T4) from arrangement (W3-T1); preserve-time-stretch moves to W1-T5.
This commit is contained in:
+401
-156
@@ -41,13 +41,14 @@ full narrative. **Reload tier = Grouping B** (continuous knobs live: filter cuto
|
||||
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). **Phase Γ opened five [Daniel]-class forks (Γ-F1…Γ-F5) and all five are ruled** (Daniel,
|
||||
2026-08-01) — the rulings are folded into the tracks below and indexed in
|
||||
`docs/product/instrument-control-surface.md` §8.1. **One new fork, Γ-F6, is open**: it was
|
||||
surfaced by the Γ-F2 ruling out of the vendored VST3 SDK, and Daniel has not seen it. It sits
|
||||
on Γ-W1-T2, is **not blocking** — it is gated on a DAW measurement that track performs first
|
||||
— and it has a pre-agreed fallback. No other track in this plan carries an unanswered
|
||||
**[Daniel]**-class question.
|
||||
change, then advancing at 1/newDuration). **Phase Γ opened six [Daniel]-class forks
|
||||
(Γ-F1…Γ-F6) and all six are ruled** (Daniel, 2026-08-01) — the rulings are folded into the
|
||||
tracks below and indexed in `docs/product/instrument-control-surface.md` §8. **Γ-F6 closed
|
||||
with a correction to the analysis, not merely a ruling**: dynamic reported latency is routine
|
||||
for VST3 instruments and REAPER handles it as a matter of course; what makes the mandated
|
||||
restart expensive *here* is self-inflicted (`setActive(true)` calls `reloadInstrument`), so
|
||||
the cost is ours to reduce and the reduction is filed in `docs/TODO.md` rather than designed
|
||||
around. **No track in this plan carries an unanswered [Daniel]-class question.**
|
||||
|
||||
### Flagged for awareness — not blocking, but decision-grade
|
||||
|
||||
@@ -652,8 +653,8 @@ the fork rulings are in **`docs/product/instrument-control-surface.md`**. Read
|
||||
layout table) and §7 (collisions) before dispatching any track here — every number in this
|
||||
phase is derived there, and `docs/TODO.md`'s old deck-rework geometry is superseded.
|
||||
|
||||
**Fork state.** The phase's five original forks are **all ruled** (Daniel, 2026-08-01) —
|
||||
indexed at spec §8.1, folded into the tracks below:
|
||||
**Fork state — all six forks are ruled; nothing in this phase awaits Daniel.** Indexed at
|
||||
spec §8, folded into the tracks below:
|
||||
- **Γ-F1** — `kEditorMinHeight` stays **680**.
|
||||
- **Γ-F2** — the limiter has **lookahead with DYNAMIC reported latency** (zero when off,
|
||||
the lookahead when on, reported to host PDC). *This inverted the product recommendation;*
|
||||
@@ -664,11 +665,22 @@ indexed at spec §8.1, folded into the tracks below:
|
||||
in a deck. W2-T2's scope grows accordingly — spec §6.4.
|
||||
- **Γ-F5** — MASTER's reserved slot is **one** cell. The 90 px headroom argument behind
|
||||
that is spec §1.6 and governs every future control addition.
|
||||
- **Γ-F6** — **ship dynamic latency as ruled.** The `restartComponent(kLatencyChanged)`
|
||||
deactivate/reactivate the SDK mandates is accepted: *"the limiter will either be on or off
|
||||
on its instance, toggling during playback is not a use case."* No constant-latency
|
||||
fallback, no measurement gate. *This ruling also corrected the analysis* — spec §3.1.1.
|
||||
|
||||
**One fork remains open — Γ-F6, on W1-T2** (spec §8.2): the VST3 SDK defines
|
||||
`restartComponent(kLatencyChanged)` as a host **deactivate/reactivate**, and this plugin's
|
||||
`setActive` is destructive in both directions. Not blocking; gated on a DAW measurement W1-T2
|
||||
performs first, with constant reported latency as the pre-agreed fallback.
|
||||
**What the Γ-F6 ruling changed in the analysis, not just in the plan.** Dynamic latency
|
||||
reporting is **routine** for VST3 instruments and REAPER handles it as a matter of course;
|
||||
the SDK's deactivate/reactivate requirement (`pluginterfaces/vst/ivsteditcontroller.h:105-108`)
|
||||
is the normal contract, not an exotic one. What makes the cycle expensive **here** is entirely
|
||||
our own doing: `ReaSamplerProcessor::setActive(true)` calls `reloadInstrument()` — a bridge
|
||||
read plus a full WAV re-decode (`reasampler_processor.cpp:89-97`) — where a typical plugin's
|
||||
`setActive` only allocates and frees buffers, and the deactivate side's freeing of
|
||||
`live_`/`draining_`/graveyard (`:98-107`) is likewise our own design. **The cost is therefore
|
||||
ours to reduce if it ever matters, and the reduction is decoupling reload from activation —
|
||||
not abandoning dynamic latency.** That improvement is filed as a `docs/TODO.md` entry with its
|
||||
trigger condition; it is not scheduled in this phase.
|
||||
|
||||
**Sequencing against Phase Ξ — Γ runs BEFORE Ξ-W2.** Two reasons, both the same shape as
|
||||
Ξ's own stated gate:
|
||||
@@ -681,12 +693,43 @@ performs first, with constant reported latency as the pre-agreed fallback.
|
||||
payload, but Ξ-W3's programmed-signal persistence will, and two phases contending for the
|
||||
ladder is exactly the fight Θ's organizing constraint calls out.
|
||||
|
||||
**The organizing constraint.** Four surfaces are single-writer and dictate the wave shape:
|
||||
**The organizing constraint.** Five surfaces are single-writer and dictate the wave shape:
|
||||
`ui/deck_values.cpp` (the taper law, then the two new controls), `editor_paint_waveform.cpp`
|
||||
(the contour trace, then the loop marks), the deck descriptors in `ui/deck_groups.cpp` +
|
||||
`ui/knob_deck.cpp` (the new group, then the reflow), and the params-payload ladder. **Every
|
||||
wave boundary below is one of those collisions**, not a preference. Where a wave has more
|
||||
than one track, the tracks are disjoint by surface.
|
||||
(the contour trace, then the loop marks), `ui/deck_groups.cpp` (the row predicate, then the
|
||||
PITCH/RATE descriptor, then the reflow's row consumption — three tracks, three waves),
|
||||
`engine/voice.cpp` (the Preserve read path, then the rate compounding into it), and the
|
||||
params-payload ladder. **Every wave boundary below is one of those collisions**, not a
|
||||
preference. Where a wave has more than one track, the tracks are disjoint by surface.
|
||||
|
||||
**Resequenced 2026-08-01 (Daniel), two changes.** The prior four-wave shape put the reflow at
|
||||
W3 and the Preserve stretcher at W4; both moved.
|
||||
|
||||
1. **The reflow is split, canvas from arrangement.** The window floor and the width budget it
|
||||
is derived from land **early** (Γ-W1-T4), so every other UI track in the phase is drawn,
|
||||
tested and judged at the final 1190 × 680 window instead of at a size a later wave changes
|
||||
under it. The two-row *arrangement* stays late (Γ-W3-T1), because it can only be measured
|
||||
once the final PITCH/RATE and MASTER descriptors exist. The seam is stated at Γ-W1-T4.
|
||||
2. **`preserve-time-stretch` moved W4 → W1-T5.** It is the longest pole in the phase and has
|
||||
**zero dependency on any UI work** — a pure `core/instrument/engine/` module. Scheduling it
|
||||
last was a scheduling error. Consequence: it is no longer Rate's *successor* but its
|
||||
**prerequisite**, which retires the interim resample-and-cancel stand-in entirely — see
|
||||
Γ-W2-T1.
|
||||
|
||||
Net: four waves become three, and both of the phase's DSP unknowns (the limiter, the
|
||||
stretcher) are exposed in wave 1 rather than one of them landing last.
|
||||
|
||||
**The params-payload ladder is unchanged by the resequencing, and re-checked against the new
|
||||
membership.** Exactly one bump per wave, owned by exactly one track: **W1-T2 owns v14** (the
|
||||
limiter flag), **W2-T1 owns v15** (rate + pitch offset), and **every other track in the phase
|
||||
owns no rung** — W1-T4 changes no persisted field, W1-T5 adds no parameter, W2-T2's loop
|
||||
enable maps onto the already-persisted `SampleLoop::hasLoop`, and W3-T1 is layout only. The
|
||||
ordering still works because v14 lands a whole wave before v15, and neither of the two tracks
|
||||
that moved touches the codec.
|
||||
|
||||
**The editor's deck is knowingly mis-composed between Γ-W1-T4 and Γ-W3-T1, and that is not a
|
||||
defect report.** Raising the floor without the reflow leaves the greedy whole-group wrap
|
||||
packing two ragged left-aligned rows with categorically wrong membership. Γ-W1-T4 states the
|
||||
exact interim layout; do not "fix" it in a track that does not own it.
|
||||
|
||||
**Phase-wide acceptance criteria** (in addition to the ones stated at the top of this file):
|
||||
- **Bypassed means byte-identical.** With the limiter off, the per-sample output path is
|
||||
@@ -700,12 +743,14 @@ than one track, the tracks are disjoint by surface.
|
||||
- **`kVelocityPitchRangeSemitones` / `kPitchDepthMaxSemis` (24.0) does not move.** It is
|
||||
load-bearing in the v12 wire format. The new Pitch knob **reads** it; it does not mint a
|
||||
second ±24 constant.
|
||||
- **The window floor is 1190 × 680 and must not exceed 1280 × 720.** A track that pushes the
|
||||
floor past 1280 has failed, not overrun. **`kEditorMinHeight` stays 680** (Γ-F1). The
|
||||
remaining **90 px of width headroom is the budget for the life of this layout** — one deck
|
||||
cell is 60 px, so there is room for exactly one more, once. Spec §1.6 states the ledger;
|
||||
read it before adding any control. Chrome-row additions are a **separate purse** (they are
|
||||
paid for out of the title slot, not the floor) and must not be charged against this one.
|
||||
- **The window floor is 1190 × 680 and must not exceed 1280 × 720.** **Γ-W1-T4 sets it, in
|
||||
wave 1; no other track in the phase may move it**, and from that point every track is
|
||||
authored and judged at it. A track that pushes the floor past 1280 has failed, not overrun.
|
||||
**`kEditorMinHeight` stays 680** (Γ-F1). The remaining **90 px of width headroom is the
|
||||
budget for the life of this layout** — one deck cell is 60 px, so there is room for exactly
|
||||
one more, once. Spec §1.6 states the ledger; read it before adding any control. Chrome-row
|
||||
additions are a **separate purse** (they are paid for out of the title slot, not the floor)
|
||||
and must not be charged against this one.
|
||||
- **Reported latency is zero unless the limiter is on.** `getLatencySamples()` returns 0 with
|
||||
the limiter bypassed, in every track and at every point in the phase. Only W1-T2 may
|
||||
introduce a non-zero value, and only under the limiter-on condition.
|
||||
@@ -716,9 +761,32 @@ than one track, the tracks are disjoint by surface.
|
||||
|
||||
### Γ-W1 — Foundations
|
||||
|
||||
**Depends on:** nothing in this phase. All three tracks are disjoint by surface — T1 owns
|
||||
editor input and the value/taper layer, T2 owns the engine and processor, T3 owns the
|
||||
waveform painter's envelope trace. None touches the deck descriptors.
|
||||
**Depends on:** nothing in this phase. **Five tracks, disjoint by surface** — re-verified
|
||||
against this membership rather than carried over from the four-wave shape:
|
||||
|
||||
| Track | Owns |
|
||||
|---|---|
|
||||
| **T1** `knob-interaction-law` | `ui/deck_values`, `ui/param_slider`, the three `shell/instrument/editor_input_*` drag paths, the shared modifier helper in `editor_internal.h` |
|
||||
| **T2** `master-bus-audio` | new pure `engine/limiter` + `engine/meter_ballistics`, `shell/instrument/reasampler_processor` + `processor_state`, `map/component_state_io` + `params_payload` (**v14**) |
|
||||
| **T3** `contour-trace-curves` | `shell/instrument/editor_paint_waveform.cpp`'s staged trace + a **pure** tessellation helper |
|
||||
| **T4** `editor-floor-and-row-law` | `ui/sample_bands.h` (the floor), `ui/knob_deck.h` (budget constants + two invalidated header notes), `ui/deck_groups` (the row predicate **only**), five test fixtures |
|
||||
| **T5** `preserve-time-stretch` | `engine/pitch_shift` + a new pure stretcher module, `engine/voice.{h,cpp}`'s Preserve read path |
|
||||
|
||||
**One shared file in the wave, named rather than discovered at merge:
|
||||
`src/core/instrument/engine/CMakeLists.txt`.** T2 declares two new pure libraries and their
|
||||
test targets there; T5 declares one. Both are append-only additions in separate blocks —
|
||||
**textual merge adjacency, not semantic contention.** Whichever lands second rebases.
|
||||
|
||||
**Two near-misses that are avoided by construction, and must stay avoided.** (a) T3's
|
||||
tessellation helper **lands pure** (`ui/envelope_overlay` or a new pure module), *not* in
|
||||
`editor_internal.h`, which T1 is editing — this also satisfies the phase's geometry-stays-pure
|
||||
criterion, so it costs nothing. (b) T4 touches `deck_groups` but adds **only** the new row
|
||||
predicate; it does not touch `sampleDeckGroups`, which W2-T1 and W3-T1 own in later waves, and
|
||||
it does not touch `deck_values`, which is T1's.
|
||||
|
||||
**Both of the phase's DSP unknowns are in this wave** — T2's limiter and T5's stretcher. That
|
||||
is deliberate: they are the two tracks whose gate can fail, and failing in wave 1 is
|
||||
recoverable in a way that failing in the last wave is not.
|
||||
|
||||
#### Γ-W1-T1 — `knob-interaction-law`
|
||||
|
||||
@@ -793,7 +861,8 @@ plugin's first latency reporting** — no editor drawing. Landing the audio ahea
|
||||
what lets Γ-W3 draw against real published state instead of a stub.
|
||||
|
||||
**Spec:** `docs/product/instrument-control-surface.md` §3.1, **§3.1.1 (latency — read this
|
||||
first)**, §3.2–3.3, §3.5, §7.10, §8.2.
|
||||
first; it was rewritten when Γ-F6 closed, so an older reading of it is wrong)**, §3.2–3.3,
|
||||
§3.5, §7.10, §7.11, §8.2.
|
||||
|
||||
**Surface boundary — owns:** a new pure limiter module and a new pure meter-ballistics
|
||||
module under `core/instrument/engine/` (each with its own `<module>_tests` target),
|
||||
@@ -816,11 +885,20 @@ module under `core/instrument/engine/` (each with its own `<module>_tests` targe
|
||||
there is no `getLatencySamples` override, no `kLatencyChanged`, and no `restartComponent`
|
||||
call site anywhere in `src/`; the plugin ships the SDK default of 0. This track introduces
|
||||
the plugin's first latency reporting.
|
||||
- **The restart is the risky part and it is fenced.** The SDK defines `kLatencyChanged` as a
|
||||
host **deactivate/reactivate** (`pluginterfaces/vst/ivsteditcontroller.h:105-108`), and this
|
||||
plugin's `setActive` is destructive in both directions (`reasampler_processor.cpp:85-109`) —
|
||||
deactivate frees every sounding voice, reactivate re-decodes the WAV. Four requirements, all
|
||||
acceptance criteria:
|
||||
- **The restart is routine; the fencing is against a standing scar, not against the flag.**
|
||||
Dynamic latency reporting is ordinary VST3-instrument behaviour and REAPER handles it as a
|
||||
matter of course. The SDK's deactivate/reactivate requirement
|
||||
(`pluginterfaces/vst/ivsteditcontroller.h:105-108`) is the normal contract. **What makes the
|
||||
cycle expensive here is this plugin's own `setActive`** — reactivate calls
|
||||
`reloadInstrument()`, a bridge read plus a full WAV re-decode
|
||||
(`reasampler_processor.cpp:89-97`), where a typical plugin only allocates buffers; deactivate
|
||||
frees `live_`/`draining_`/graveyard (`:98-107`) for a documented reason (ghost sustained
|
||||
voices). **Γ-F6 is ruled: ship it — the toggle is a patch-design gesture, not a
|
||||
during-playback one.** Do **not** build a constant-reported-latency fallback and do **not**
|
||||
gate the deliverable on a measurement. The reduction of that self-inflicted cost is filed in
|
||||
`docs/TODO.md` ("Decouple the instrument reload from VST3 activation") with its trigger
|
||||
condition; it is out of scope here. The four requirements below survive as engineering
|
||||
hygiene against the `kIoChanged` scar, and all four are acceptance criteria:
|
||||
1. **Verify the whole call sequence against the vendored Steinberg SDK** before writing it,
|
||||
including the ordering rule that the new latency is what `getLatencySamples` returns
|
||||
*after* `setActive(true)` — so **the reported value must derive from persisted state, not
|
||||
@@ -839,11 +917,17 @@ module under `core/instrument/engine/` (each with its own `<module>_tests` targe
|
||||
- **Flipping the toggle during playback: apply immediately, do NOT defer to a transport
|
||||
boundary** (product ruling, spec §3.1.1). A deferred restart leaves the plugin misaligned by
|
||||
the lookahead with no visible cue, which is worse than a visible interruption; and the host,
|
||||
not the plugin, schedules the deactivate/reactivate anyway. Two mitigations are in scope:
|
||||
the engage/disengage is covered by a short (≤ 10 ms) equal-gain crossfade so **the plugin
|
||||
emits no discontinuity of its own**, and the limiter enable is classified **not automatable**
|
||||
(`docs/product/parameter-automation.md` §3.8) so nothing can flip it at rate. It is also
|
||||
**not** the plugin's `kIsBypass` parameter.
|
||||
not the plugin, schedules the deactivate/reactivate anyway. **Daniel has accepted the
|
||||
interruption outright** (Γ-F6) — it is not a case to design for. Two things remain in scope,
|
||||
and neither is a mitigation for it:
|
||||
- **A short (≤ 10 ms) equal-gain crossfade over the engage/disengage.** Kept as a *quality*
|
||||
measure, not a mitigation: a limiter engaging is a gain-path change, and this codebase
|
||||
already ramps every gain-path change (`kGainRampSeconds`, `ValueRamp`). It also earns its
|
||||
keep independently of the restart, because **we do not control when the host acts on the
|
||||
request** — our own transition must be clean in the window before it does.
|
||||
- **The limiter enable is classified NOT automatable**
|
||||
(`docs/product/parameter-automation.md` §3.8) so nothing can flip it at rate. It is also
|
||||
**not** the plugin's `kIsBypass` parameter.
|
||||
- **Per block the processor publishes, as relaxed atomics:** per-channel peak `max|x|`, a
|
||||
latched clip flag, and the block's maximum gain reduction. **No dB conversion, no
|
||||
ballistics, no hold timers on the audio thread** — the UI converts and runs ballistics from
|
||||
@@ -877,23 +961,18 @@ module under `core/instrument/engine/` (each with its own `<module>_tests` targe
|
||||
on a rendered signal, not judged by ear.
|
||||
|
||||
**Open questions.**
|
||||
- **[Daniel] fork Γ-F6 — is the deactivate/reactivate cost acceptable?** (spec §8.2.) **Not
|
||||
blocking, and this is the track's FIRST deliverable:** a verification spike in REAPER —
|
||||
flip the limiter with notes held, during playback and while stopped, and **record what
|
||||
actually happens** (do notes cut? is the re-decode perceptible? does transport hiccup?).
|
||||
The SDK mandates the deactivate/reactivate; what REAPER does with it is DAW-verifiable only.
|
||||
Ship the ruled dynamic-latency design regardless; if the observed behaviour is as ugly as
|
||||
the SDK's worst case allows, the **pre-agreed fallback is constant reported latency** (the
|
||||
delay line engaged whether or not the limiter is on), which needs one word from Daniel and
|
||||
**no DSP change** — only the latency-reporting predicate moves. Report the measurement to
|
||||
Daniel with a recommendation; do not choose the fallback unilaterally.
|
||||
- **No [Daniel] questions. Fork Γ-F6 is ruled** — dynamic latency ships as specced, the
|
||||
deactivate/reactivate is accepted, and there is no fallback design and no measurement gate.
|
||||
Do not reintroduce either; the constant-reported-latency option is closed, not shelved.
|
||||
- **[verify]** `temp_cortex/` has already been assessed and **rejected** (spec §3.5) — do not
|
||||
re-litigate it, and do not transplant from it.
|
||||
- **[verify]** whether `setActive(true)`'s `reloadInstrument()` can reuse the already-decoded
|
||||
`SampleData` on a latency-restart reactivate rather than re-reading the bridge and
|
||||
re-decoding the WAV. If it cannot cheaply, that is an **accepted cost to be recorded**, not
|
||||
a reason to restructure `setActive` — its destructive shape is deliberate and its reasoning
|
||||
(ghost sustained voices on reactivate) is documented at the call site.
|
||||
- **[record, not a gate]** While the limiter is in REAPER under your hand, note what the
|
||||
restart actually costs — do notes cut, is the re-decode perceptible, does transport hiccup —
|
||||
and record it in this track's review. It is **not** a gate on shipping and no outcome changes
|
||||
the design; it is the trigger-condition evidence for the `docs/TODO.md` entry "Decouple the
|
||||
instrument reload from VST3 activation," which is where that cost gets reduced if it ever
|
||||
matters. Do **not** restructure `setActive` here: its destructive shape is deliberate and its
|
||||
reasoning (ghost sustained voices on reactivate) is documented at the call site.
|
||||
|
||||
#### Γ-W1-T3 — `contour-trace-curves`
|
||||
|
||||
@@ -927,24 +1006,217 @@ diverge; tessellation approach is the engineer's call.
|
||||
are honoured unchanged. The spline overlay's own trace is untouched.
|
||||
- **Audio is unchanged** — this is a drawing defect only; a regression baseline proves it.
|
||||
|
||||
#### Γ-W1-T4 — `editor-floor-and-row-law`
|
||||
|
||||
**Goal.** Commit the **canvas** — the window floor, the width budget it is derived from, and
|
||||
the row every deck group belongs to — so every other UI track in the phase is drawn, tested and
|
||||
judged at the final window size. The **arrangement** inside that canvas is Γ-W3-T1's.
|
||||
|
||||
**Spec:** `docs/product/instrument-control-surface.md` §1.1 (the two categories), §1.2 (the
|
||||
floor arithmetic block), §1.6 (the headroom ledger), §7.1 and §7.4 (the two invalidated
|
||||
`knob_deck.h` notes).
|
||||
|
||||
**Surface boundary — owns:** `core/instrument/ui/sample_bands.h` (`kEditorMinWidth`),
|
||||
`core/instrument/ui/knob_deck.h` (the declared budget constants and the two invalidated header
|
||||
notes), `core/instrument/ui/deck_groups.{h,cpp}` (**the new row predicate only**), and the five
|
||||
test fixtures that read the floor — `test_sample_bands.cpp`, `test_deck_groups.cpp`,
|
||||
`test_knob_deck.cpp`, `test_sample_chrome.cpp`, `test_keyboard_strip.cpp`. **Does not own**
|
||||
`layoutDeck` / `deckRowCount` / `deckHeight` behaviour, the justification law, any descriptor,
|
||||
MASTER's inventory or interior, any painter, or any parameter. It changes **no drawing code at
|
||||
all.**
|
||||
|
||||
**Behavior — what it commits.**
|
||||
- **`kEditorMinWidth` 980 → 1190. `kEditorMinHeight` stays 680** (Γ-F1).
|
||||
- **Three declared budget constants in `knob_deck.h`:** the row block both rows will justify
|
||||
inside (**1020**), the right-anchored spanning deck's reserved width (**MASTER 142**), and
|
||||
the ceiling (**1280**). These are *declarations of budget*, not measurements — nothing
|
||||
computes them from a descriptor, and Γ-W3-T1's job is to prove its content fits inside them.
|
||||
- **The floor is derived, not asserted as a literal.** `1020 + kDeckGroupGap(12) + 142 +
|
||||
2·kPad(8) = 1190`. `kEditorMinWidth` stays a literal in `sample_bands.h` — **do not add an
|
||||
include edge from `sample_bands` to `knob_deck`**, which would invert the allocator's
|
||||
deliberate independence from the deck (it takes `deckHeight` as a *parameter* for exactly
|
||||
that reason). The identity is asserted in `test_deck_groups.cpp`, which already includes
|
||||
both headers. This is the Θ-W6-T1 derived-floor precedent, landed once and never rewritten.
|
||||
- **Row membership becomes a property of the group id:** `DeckRow { Sound, Contour, Spanning }`
|
||||
+ `deckRowFor(DeckGroupId)` in `deck_groups`, an **exhaustive switch** on the
|
||||
`isLiveDeckParam` discipline, so a future group is a compile error rather than a silent
|
||||
default. Partition: **Sound** = PITCH/RATE, FILTER, VELOCITY, VOICE; **Contour** = PITCH ENV,
|
||||
FILTER ENV, AMP ENVELOPE; **Spanning** = MASTER. **Nothing consumes it until Γ-W3-T1** — that
|
||||
is the seam, and it is why the predicate is safe to land now: **membership is a property of
|
||||
the group, width is a property of the descriptor**, and only the widths are still moving.
|
||||
- **Γ adds no new deck group**, so no later track amends this predicate.
|
||||
|
||||
**The seam, stated as what this track can and cannot assert.**
|
||||
|
||||
*Can assert today:*
|
||||
- The derived floor identity above, and `kEditorMinWidth ≤ 1280` with **90 px** of headroom.
|
||||
- `kEditorMinHeight == 680`, asserted so no later track drifts Γ-F1's ruling.
|
||||
- `deckRowFor` is total over `DeckGroupId` and yields exactly the partition above.
|
||||
- **Row 2's natural width already fits the block, in both play modes:**
|
||||
252 + 312 + 312 = **876 ≤ 1020**, leaving both its gutters ≥ `kDeckGroupGap`. Mode-stable
|
||||
because FILTER ENV's and AMP's reserve slots hold them at 312 in Gate and Trigger alike.
|
||||
- **MASTER's reserve is not yet spent:** `deckGroupWidth(MASTER) == 72 ≤ 142`.
|
||||
- At the floor, deck band **216** and waveform band **358** — the reflow's 112 px arrives here,
|
||||
two waves early (see the interim layout below).
|
||||
|
||||
*Cannot assert yet, and must not force:*
|
||||
- **Row 1's natural width does not fit the block.** Today it is PITCH 150 + FILTER 524 +
|
||||
VELOCITY 192 + VOICE 164 = **1030**, against the 1020 block. The 50 px deficit is exactly
|
||||
what the two descriptor changes buy: PITCH → PITCH/RATE **+42** (Γ-W2-T1) and FILTER's
|
||||
`Band|Notch` moving to the caption corner **−92** (Γ-W3-T1), netting **980**. Record the
|
||||
target and the two contributions as a test comment; **assert the fit in Γ-W3-T1, and do not
|
||||
pre-empt either descriptor change to close it early.**
|
||||
- Gutter distribution, the filter tie-line at x = 636, flush outer edges, MASTER's interior and
|
||||
its meter — all Γ-W3-T1. Every one of them measures a descriptor that does not exist yet.
|
||||
|
||||
**The interim editor, stated exactly so it is not filed as a defect.** At the new floor,
|
||||
`availWidth = 1190 − 2·kPad = 1174`, and the **unchanged** greedy whole-group wrap packs:
|
||||
|
||||
```
|
||||
row 1 PITCH 150 · PITCH ENV 252 · FILTER 524 = 950 used, 224 px ragged right
|
||||
row 2 FILTER ENV 312 · AMP 312 · VELOCITY 192 ·
|
||||
VOICE 164 · MASTER 72 = 1100 used, 74 px ragged right
|
||||
```
|
||||
|
||||
**Two rows, not three** — so the deck band is already 216 and the waveform already draws at its
|
||||
final 358 px, in both Gate and Trigger. After Γ-W2-T1 lands PITCH/RATE the pack is row 1 = 992,
|
||||
row 2 unchanged; still two rows. The composition is wrong in exactly the way the reflow exists
|
||||
to fix — PITCH ENV sits up with the sound decks, VOICE and MASTER sit down with the envelopes,
|
||||
MASTER is still a single-height 72 px box, and both rows are left-packed with dead space at the
|
||||
right. **Worse than today in composition, better in proportion.** That is the accepted
|
||||
transitional state for the rest of the phase.
|
||||
|
||||
**Do not convert the two-row interim into a claim.** It is a coincidence of the greedy wrap at
|
||||
exactly this width, not a guarantee — which is precisely why Γ-W3-T1's criterion is "two rows
|
||||
**by construction**, asserted against the group inventory, not observed as a wrap outcome."
|
||||
`testDeckFitsInsideTheEnforcedMinimumWindow` currently asserts `deckRowCount == 3`; relax it to
|
||||
an **upper bound** (`<= 2`), which is a real regression canary throughout the interim and is
|
||||
subsumed by Γ-W3-T1's exact claim. An exact `== 2` here is acceptable only with a comment
|
||||
naming it as a wrap outcome the reflow replaces.
|
||||
|
||||
**Acceptance criteria.**
|
||||
- **The floor is 1190 × 680, reached by a derived test over the three budget constants**, not
|
||||
by a literal — and the derivation is the one Γ-W3-T1 later reads rather than a second copy.
|
||||
- **Headroom is exactly 90 px** against the 1280 ceiling, asserted.
|
||||
- `deckRowFor` is exhaustive over `DeckGroupId`; adding a group without classifying it fails to
|
||||
compile.
|
||||
- Row 2's natural width and MASTER's unspent reserve are asserted, in **both** play modes.
|
||||
- **All five floor-reading test fixtures pass at the new floor** — including the chrome row,
|
||||
whose title slot gets *more* room at 1190, not less.
|
||||
- **No drawing code changes, no descriptor changes, no parameter changes, no audio change.**
|
||||
A regression baseline proves the last of those trivially.
|
||||
- The two invalidated `knob_deck.h` notes (§7.1's fourteen-pixel headroom figure, §7.4's
|
||||
cells-and-floor pairing) are **re-derived against the new floor, not deleted** — §7.4's
|
||||
restatement is *the deck's cell metrics AND its group/row composition both drive
|
||||
`kEditorMinWidth`; none of the three may move alone.*
|
||||
|
||||
**Open questions.** **No [Daniel] questions.** **[propose at review]** whether the three budget
|
||||
constants belong in `knob_deck.h` (the deck owns the row block and the spanning-deck reserve)
|
||||
or in `sample_bands.h` (the allocator owns the floor they derive). The plan's lean is
|
||||
`knob_deck.h` with the identity in the test, because it adds no include edge; either is
|
||||
defensible, but the *derivation must live in exactly one place*.
|
||||
|
||||
#### Γ-W1-T5 — `preserve-time-stretch`
|
||||
|
||||
**Goal.** A real pitch-preserving time-stretcher for Preserve mode, written from established
|
||||
state-of-the-art literature — landed **before** the control that drives it, so Rate ships onto a
|
||||
finished engine rather than onto a disposable stand-in.
|
||||
|
||||
**Spec:** `docs/product/instrument-control-surface.md` §2.5.
|
||||
|
||||
**Moved from Γ-W4-T1 (Daniel, 2026-08-01).** It is the longest pole in the phase and has zero
|
||||
dependency on any UI work. **The consequence is the interesting one: it inverts the
|
||||
relationship with Rate.** Under the old order the stretcher was Rate's quality upgrade and
|
||||
Γ-W2-T1 shipped an interim resample-and-cancel path to make Rate complete on day one; under
|
||||
this order the stretcher is Rate's **prerequisite** and **the interim path is not built at
|
||||
all.** Skipping a stand-in that was only ever going to be deleted is the win; see Γ-W2-T1's
|
||||
named contingency for what happens if this track's gate slips.
|
||||
|
||||
**Precedent for landing a DSP module ahead of its consumer:** Θ-W1-T3 (`filter-dsp-port`)
|
||||
landed the filter DSP as a standalone pure module a wave before Θ-W2-T1 wired it into the voice
|
||||
path, for the same reason — the unknown is the DSP, not the wiring.
|
||||
|
||||
**Surface boundary — owns:** `core/instrument/engine/pitch_shift` and whatever new pure module
|
||||
the stretcher needs (each with its own `<module>_tests` target), plus `voice.{h,cpp}`'s Preserve
|
||||
read path. **Does not own** any parameter, any UI, the varispeed path, or the deck. It adds no
|
||||
`ComponentState` field and takes **no rung of the payload ladder**.
|
||||
|
||||
**Behavior and constraints.** The algorithm is **the engineer's call under a
|
||||
measure-and-report gate — this plan deliberately names none.** The constraints:
|
||||
- **The stretch ratio is an argument, not a parameter.** Nothing publishes a non-unity ratio
|
||||
until Γ-W2-T1's Rate knob does. Until then the Preserve read path runs at ratio 1.0 and must
|
||||
be **bit-identical to the shipped Preserve read** — a stronger and cheaper regression gate
|
||||
than the old plan's A/B-against-an-interim-path, because the baseline is a build that exists.
|
||||
- **CPU stance (Daniel, verbatim intent):** *"we should be efficient but accept the cost of
|
||||
high-quality algorithm choices. It's 2026, most people's computers can handle audio with
|
||||
ease. Just don't be wasteful."*
|
||||
- **RT-safe:** no allocation, no I/O, no lock in `process()`; buffers sized at voice
|
||||
allocation or at the off-audio-thread reload, on `pitch_shift`'s existing pre-warm
|
||||
precedent.
|
||||
- **Per-voice state, holding up at the 32-voice ceiling.** The gate is 32 simultaneous
|
||||
Preserve voices at 50 % and 200 %, not one voice at 100 %.
|
||||
- **No new third-party dependency** (`pitch_shift`'s standing property).
|
||||
- **No dispatch on the per-sample path** — concrete, inlineable types; no `IStretcher`.
|
||||
- **Onset behaviour is a regression surface.** GA2 eliminated Preserve's ~25 ms onset latency
|
||||
by priming the ring with the actual upcoming source. **A stretcher that reintroduces an
|
||||
onset delay or a first-frame smear is a regression, not a trade-off.**
|
||||
|
||||
**Acceptance criteria.**
|
||||
- **Ratio 1.0 with no shift is bit-identical to the shipped Preserve read**, asserted by a
|
||||
regression baseline — the null case, and the criterion that makes landing this ahead of Rate
|
||||
safe.
|
||||
- Preserve speaks on frame 0 — no added onset latency, no first-frame smear, in any
|
||||
ratio/shift combination.
|
||||
- No audible metallic or phasey artefacting on sustained tonal material at ±6 st and
|
||||
75–133 % ratio; transient material at 50 % / 200 % is no worse smeared than **varispeed
|
||||
playback at the equivalent ratio** — the honest "what does preserving pitch cost" reference,
|
||||
and the one that needs **no disposable implementation built to serve the comparison.**
|
||||
- The Gate sustain-loop contract is unchanged: **loop the source, shift the output** — loop
|
||||
points remain source-frame facts.
|
||||
- **Measure and report before the algorithm is final:** per-voice CPU at 32 voices, added
|
||||
latency (must be zero at the onset), and A/B recordings on three material classes (one-shot,
|
||||
tonal sustain, full-mix bounce). Report to Daniel; the choice is not final until he has heard
|
||||
the A/Bs.
|
||||
|
||||
**Open questions.** **[propose, with a measurement step]** the algorithm family itself.
|
||||
**[verify]** that `core/instrument/CLAUDE.md`'s *"`WDL_Resampler` is not a Preserve engine —
|
||||
never wire it as the duration-preserving path"* is honoured: under Preserve, Rate legitimately
|
||||
changes duration, so a resampled read is an explicit duration control — but the
|
||||
*pitch-preserving* mechanism must not be a resampler. **No [Daniel] questions.**
|
||||
|
||||
---
|
||||
|
||||
### Γ-W2 — New controls, and the overlay's marks
|
||||
|
||||
**Depends on Γ-W1 for:** T1 depends on W1-T1 — Rate and Pitch must be authored into the
|
||||
finished taper/modifier law, not retro-fitted into it (and the semitone taper must exist
|
||||
before a second semitone knob does). T2 depends on W1-T3 — both write
|
||||
`editor_paint_waveform.cpp`, and running them together is a merge fight in one file.
|
||||
**Depends on Γ-W1 for — four dependencies, two of them new:**
|
||||
1. **T1 ← W1-T1 (taper law).** Rate and Pitch must be authored into the finished
|
||||
taper/modifier law, not retro-fitted into it, and the semitone taper must exist before a
|
||||
second semitone knob does.
|
||||
2. **T1 ← W1-T5 (the stretcher) — NEW, and the reason the interim path is gone.** Preserve
|
||||
Rate has no engine without it. Under the prior four-wave order this dependency ran the other
|
||||
way and was paid for with a disposable resample-and-cancel stand-in; the resequencing
|
||||
inverts it. **Rate must not ship before its Preserve engine.**
|
||||
3. **T2 ← W1-T3 (the contour trace).** Both write `editor_paint_waveform.cpp`; running them
|
||||
together is a merge fight in one file.
|
||||
4. **T2 ← W1-T1 and W1-T4, weakly.** W1-T1 also edits `editor_input_waveform.cpp` (the
|
||||
modifier read), which T2 rewrites for marker hit-test routing — serial, so not a conflict,
|
||||
but T2 rebases onto it. And T2's "the enable costs no window width" criterion is now
|
||||
asserted against **W1-T4's** derived floor test rather than one this track has to write.
|
||||
|
||||
**Disjointness — restated after the Γ-F4 ruling, because it changed.** T1 owns the parameter
|
||||
model, the engine and the deck descriptors; T2 owns the waveform band's marks and their pure
|
||||
geometry **and now also the chrome row's loop enable**. The two are disjoint at the module
|
||||
level with **one named exception: `shell/instrument/editor_session.cpp`.** T1 may touch it for
|
||||
the third commit tier's routing; **T2 owns `pickedMarkers` and `applyMarkers` there and nothing
|
||||
else.** The partition is by function and the two do not overlap — this is a textual merge
|
||||
adjacency, not a semantic contention — but it is a shared file in a phase whose wave boundaries
|
||||
T1 additionally inherits `engine/voice.{h,cpp}` from W1-T5 — a **hand-off, not a conflict**:
|
||||
W1-T5 defines the Preserve ratio seam, and T1 feeds it. Serial across waves by construction.
|
||||
|
||||
**Disjointness — re-verified against this wave's membership, not carried over.** Both tracks
|
||||
stayed in W2 and nothing entered or left it, so the prior finding is re-checked and stands. T1
|
||||
owns the parameter model, the engine and the deck descriptors; T2 owns the waveform band's
|
||||
marks and their pure geometry **and the chrome row's loop enable**. The two are disjoint at the
|
||||
module level with **one named exception: `shell/instrument/editor_session.cpp`.** T1 may touch
|
||||
it for the third commit tier's routing; **T2 owns `pickedMarkers` and `applyMarkers` there and
|
||||
nothing else.** The partition is by function and the two do not overlap — **textual merge
|
||||
adjacency, not semantic contention** — but it is a shared file in a phase whose wave boundaries
|
||||
are otherwise single-writer surfaces, so it is stated rather than discovered at merge. Whichever
|
||||
track lands second rebases onto the first.
|
||||
track lands second rebases onto the first. No *new* in-wave adjacency was created by the
|
||||
resequencing: T2 touches neither `deck_values` nor `deck_groups` nor `voice`.
|
||||
|
||||
**The format ladder stays clean.** The loop enable maps onto the existing
|
||||
`SampleLoop::hasLoop`, which is already persisted and whose `start`/`end` are already written
|
||||
@@ -964,7 +1236,8 @@ existing Varisp|Presrv toggle, with both new controls wired through the engine.
|
||||
`core/instrument/engine/voice.{h,cpp}` (the compounding and the note-on latch),
|
||||
`core/instrument/ui/deck_groups` (the PITCH/RATE descriptor **and** the three-state live
|
||||
predicate), `core/instrument/ui/deck_values` (the two new bindings). **Does not own** the
|
||||
deck's row layout — that is Γ-W3-T1 — nor the real time-stretcher (Γ-W4-T1).
|
||||
deck's row layout — that is Γ-W3-T1 — nor the time-stretcher itself (Γ-W1-T5, already landed
|
||||
by the time this track runs).
|
||||
|
||||
**Behavior.**
|
||||
- **Rate: 50 %–200 %, default 100 % at true knob centre, exponential taper** — 50 % = −12 st,
|
||||
@@ -977,10 +1250,11 @@ deck's row layout — that is Γ-W3-T1 — nor the real time-stretcher (Γ-W4-T1
|
||||
read-increment multiply**; the rate offset applies to the varispeed pitch. Composes with
|
||||
the pitch envelope's existing per-frame `ratio_` multiply — **no new per-sample stage**.
|
||||
- **Preserve:** rate is an **absolute** value driving **duration only**; keytrack and pitch
|
||||
offset drive the pitch shifter. **Interim implementation:** a resampled read with the
|
||||
resulting pitch change cancelled in the existing SOLA shifter. This is an explicit duration
|
||||
control, **not** a covert Preserve path — `core/instrument/CLAUDE.md`'s "never wire
|
||||
`WDL_Resampler` as the duration-preserving path" is untouched. Quality is Γ-W4-T1's.
|
||||
offset drive the pitch shifter. **Rate drives the stretch ratio Γ-W1-T5's stretcher already
|
||||
consumes — there is no interim path.** The stretcher is this track's prerequisite, not its
|
||||
successor; the resample-and-cancel stand-in the prior plan carried is retired unbuilt (see
|
||||
Open questions for the contingency). `core/instrument/CLAUDE.md`'s "never wire
|
||||
`WDL_Resampler` as the duration-preserving path" is honoured by construction.
|
||||
- **Rate is latched at note-on**, delivered by a **third commit class**: published into the
|
||||
live block like any live parameter, read only by `snapLive`, never by `applyLive`.
|
||||
`isLiveDeckParam`/`liveCommitFor` widens from two states to three
|
||||
@@ -1005,7 +1279,9 @@ deck's row layout — that is Γ-W3-T1 — nor the real time-stretcher (Γ-W4-T1
|
||||
**Acceptance criteria.**
|
||||
- Rate at 50 % plays an octave down and half speed under Varispeed; at 200 %, an octave up and
|
||||
double speed. Under Preserve the same settings change duration only — pitch is unchanged
|
||||
within the shifter's tolerance.
|
||||
within the stretcher's tolerance. **Preserve Rate is a finished feature the day this lands**,
|
||||
because Γ-W1-T5 already shipped its engine; a degraded or inert Preserve Rate is a failed
|
||||
track, not an acceptable interim.
|
||||
- Rate at exactly 100 % and Pitch at exactly 0 st render **bit-identical** to the
|
||||
pre-change build, in both engines.
|
||||
- Shift-drag on Rate lands on whole semitones (so an octave and a fifth are reachable by
|
||||
@@ -1019,9 +1295,19 @@ deck's row layout — that is Γ-W3-T1 — nor the real time-stretcher (Γ-W4-T1
|
||||
compile error in `isLiveDeckParam`'s exhaustive switch until they are classified.
|
||||
- A v14 project reopens at rate 100 % / pitch 0 st and sounds identical.
|
||||
|
||||
**Open questions.** None **[Daniel]**. **[propose at review]** the exact interim
|
||||
resample+cancel composition and its clamp behaviour at the range extremes, which Γ-W4-T1 then
|
||||
replaces.
|
||||
**Open questions.**
|
||||
- **None [Daniel].**
|
||||
- **[propose at review]** Rate's clamp behaviour at the range extremes as it meets the
|
||||
stretcher's own ratio bounds — one clamp, resolved where the two meet, not two that can
|
||||
disagree.
|
||||
- **Named contingency, not a plan item, and not to be taken silently.** If Γ-W1-T5's
|
||||
measure-and-report gate has not passed when this track is ready to dispatch, the pre-agreed
|
||||
fallback is the **resample-and-cancel composition** spec §2.5 records — a resampled read with
|
||||
the resulting pitch change cancelled in the existing SOLA shifter — shipped as an interim
|
||||
Preserve path with the stretcher as its later quality upgrade, i.e. a return to the prior
|
||||
four-wave order. **Escalate to Daniel rather than taking it:** it revives a disposable
|
||||
implementation and re-opens the `WDL_Resampler` guardrail conversation, and the whole point of
|
||||
the resequencing was to avoid building it.
|
||||
|
||||
#### Γ-W2-T2 — `loop-crossfade-ux`
|
||||
|
||||
@@ -1167,12 +1453,17 @@ track changes drawing, hit-testing and one editor-state retention rule — no fo
|
||||
|
||||
**Depends on Γ-W2 for:** the PITCH/RATE descriptor (W2-T1) — the reflow measures the real
|
||||
three-cell group, and laying it out against a forecast of that group means re-measuring
|
||||
afterward. **Depends on Γ-W1 for:** W1-T2's published meter/GR/clip state, which MASTER's
|
||||
deck draws; drawing against a stub would mean building the meter twice.
|
||||
afterward. **This is the whole reason the arrangement is late**, and it is why the canvas was
|
||||
split out of it into W1-T4.
|
||||
|
||||
**One track.** The row law, the group inventory, the double-height deck and the window floor
|
||||
are one geometry decision spread over `knob_deck`, `deck_groups`, `sample_bands` and the deck
|
||||
painter. Splitting it would put two tracks in the same three pure modules.
|
||||
**Depends on Γ-W1 for:** W1-T2's published meter/GR/clip state, which MASTER's deck draws
|
||||
(drawing against a stub would mean building the meter twice), and W1-T4's floor, budget
|
||||
constants and row predicate, which this track **consumes rather than re-derives**.
|
||||
|
||||
**One track.** The row law, the group inventory and the double-height deck are one geometry
|
||||
decision spread over `knob_deck`, `deck_groups` and the deck painter. Splitting it would put two
|
||||
tracks in the same pure modules. **The window floor is no longer part of it** — W1-T4 set it
|
||||
two waves ago, and this track must not move it.
|
||||
|
||||
#### Γ-W3-T1 — `deck-reflow`
|
||||
|
||||
@@ -1184,10 +1475,12 @@ measured table **and §1.6, the headroom ledger**) and §3.2–3.3 (what MASTER
|
||||
the invariants this track invalidates or widens — read it before touching `knob_deck.h`.**
|
||||
|
||||
**Surface boundary — owns:** `core/instrument/ui/knob_deck` (the row law, the double-height
|
||||
group, the justification), `core/instrument/ui/deck_groups` (row assignment, FILTER's toggle
|
||||
move, MASTER's inventory), `core/instrument/ui/sample_bands` (`kEditorMinWidth`), and
|
||||
`shell/instrument/editor_paint_deck` (the MASTER meter/limiter/bubble draw). **Does not own**
|
||||
any parameter, the limiter DSP, or the waveform band.
|
||||
group, the justification — **consuming** W1-T4's budget constants, not restating them),
|
||||
`core/instrument/ui/deck_groups` (consumption of W1-T4's row predicate, FILTER's `Band|Notch`
|
||||
caption move, MASTER's inventory), and `shell/instrument/editor_paint_deck` (the MASTER
|
||||
meter/limiter/bubble draw). **Does not own** `kEditorMinWidth` or any budget constant — those
|
||||
are W1-T4's and are **read**, never moved — nor any parameter, the limiter DSP, or the
|
||||
waveform band.
|
||||
|
||||
**Behavior.**
|
||||
- **Row 1 (sound), one row, non-negotiable:** PITCH/RATE 192 · FILTER 432 · VELOCITY 192 ·
|
||||
@@ -1209,9 +1502,12 @@ any parameter, the limiter DSP, or the waveform band.
|
||||
That tie-line, row 2's equal gutters, and row 1's minimum gutter being exactly
|
||||
`kDeckGroupGap` all hold at 1020 and only at 1020 — **this is why the floor is 1190 and not
|
||||
1186.** Above the floor the tie-line drifts and that is accepted (spec §1.3).
|
||||
- **`kEditorMinWidth` 980 → 1190**; `kEditorMinHeight` **stays 680** (Γ-F1, ruled — the
|
||||
reflow's 112 px goes entirely to the waveform); deck band height 328 → **216**; waveform
|
||||
band at the floor 246 → **358**.
|
||||
- **The floor is already 1190 × 680 and the bands are already 216 / 358** — Γ-W1-T4 landed all
|
||||
four in wave 1, and the greedy wrap happened to reach two rows at that width. **This track
|
||||
changes none of those numbers; it makes them true by construction instead of by coincidence.**
|
||||
Row 1's natural width fits the block **only after this track's `Band|Notch` move**: 1030
|
||||
today, +42 from W2-T1's PITCH/RATE, −92 here, = **980**. That is this track's fit assertion
|
||||
and W1-T4 deliberately left it open.
|
||||
- **The 90 px of remaining headroom is the budget for the life of this layout**, and one deck
|
||||
cell is 60 px. **This is why MASTER's reserved slot is ONE cell** (Γ-F5, ruled): two would
|
||||
spend 60 of the 90 up front on a control nobody has named, leaving 30 — which would freeze
|
||||
@@ -1248,9 +1544,13 @@ any parameter, the limiter DSP, or the waveform band.
|
||||
312 in both modes — assert it).
|
||||
- Row 1 and row 2 are **flush left and flush right**; at the floor width the filter tie-line
|
||||
is exact (both edges at x = 636) and row 2's two gutters are equal.
|
||||
- **`kEditorMinWidth` is 1190 and the floor is ≤ 1280 × 720**, asserted by a derived test
|
||||
rather than a literal (the Θ-W6-T1 precedent).
|
||||
- The waveform band is **358 px at the floor**, and the deck band is 216.
|
||||
- **Row 1's natural width is 980 and fits the 1020 block** — the fit Γ-W1-T4 could not yet
|
||||
assert, closed here by the `Band|Notch` move.
|
||||
- **`kEditorMinWidth` is still 1190 and the floor is still ≤ 1280 × 720** — unchanged by this
|
||||
track, verified against Γ-W1-T4's derived test rather than a second copy of it.
|
||||
- The waveform band is **358 px at the floor**, and the deck band is 216 — **unchanged from the
|
||||
interim, now reached by construction**: `deckRowCount` at and above the floor is 2 because the
|
||||
row predicate says so, not because a wrap landed there. Assert against the group inventory.
|
||||
- MASTER's gain knob shares a knob baseline with FILTER's knobs; its reserved slot shares one
|
||||
with AMP ENVELOPE's.
|
||||
- The meter reads correctly in mono and stereo, the peak-hold tick holds 1.5 s, the clip cap
|
||||
@@ -1274,66 +1574,6 @@ any parameter, the limiter DSP, or the waveform band.
|
||||
|
||||
---
|
||||
|
||||
### Γ-W4 — The Preserve time-stretcher
|
||||
|
||||
**Depends on Γ-W2 for:** Rate existing as a shipped, working control on the interim
|
||||
resample+cancel path. Landing the stretcher against a working feature makes it a measurable
|
||||
quality upgrade with an A/B; landing them together would make one track responsible for both
|
||||
a feature and an algorithm.
|
||||
|
||||
**One track.** **This is the phase's largest unknown** — flagged the way Ξ-W2-T1's crossing
|
||||
was. If the measure-and-report gate fails, what gives is the *algorithm choice*, not Rate:
|
||||
the interim path already ships in W2-T1, so the fallback is "keep the interim path and record
|
||||
why," not "cut the feature."
|
||||
|
||||
#### Γ-W4-T1 — `preserve-time-stretch`
|
||||
|
||||
**Goal.** Replace the interim resample+cancel composition with a real pitch-preserving
|
||||
time-stretcher written from established state-of-the-art literature.
|
||||
|
||||
**Spec:** `docs/product/instrument-control-surface.md` §2.5.
|
||||
|
||||
**Surface boundary — owns:** `core/instrument/engine/pitch_shift` and whatever new pure
|
||||
module the stretcher needs, plus `voice.cpp`'s Preserve read path. **Does not own** any
|
||||
parameter, any UI, or the varispeed path.
|
||||
|
||||
**Behavior and constraints.** The algorithm is **the engineer's call under a
|
||||
measure-and-report gate — this plan deliberately names none.** The constraints:
|
||||
- **CPU stance (Daniel, verbatim intent):** *"we should be efficient but accept the cost of
|
||||
high-quality algorithm choices. It's 2026, most people's computers can handle audio with
|
||||
ease. Just don't be wasteful."*
|
||||
- **RT-safe:** no allocation, no I/O, no lock in `process()`; buffers sized at voice
|
||||
allocation or at the off-audio-thread reload, on `pitch_shift`'s existing pre-warm
|
||||
precedent.
|
||||
- **Per-voice state, holding up at the 32-voice ceiling.** The gate is 32 simultaneous
|
||||
Preserve voices at 50 % and 200 %, not one voice at 100 %.
|
||||
- **No new third-party dependency** (`pitch_shift`'s standing property).
|
||||
- **No dispatch on the per-sample path** — concrete, inlineable types; no `IStretcher`.
|
||||
- **Onset behaviour is a regression surface.** GA2 eliminated Preserve's ~25 ms onset latency
|
||||
by priming the ring with the actual upcoming source. **A stretcher that reintroduces an
|
||||
onset delay or a first-frame smear is a regression, not a trade-off.**
|
||||
|
||||
**Acceptance criteria.**
|
||||
- **Rate 100 % with no shift is bit-identical to the un-stretched read.**
|
||||
- Preserve speaks on frame 0 — no added onset latency, no first-frame smear, in any
|
||||
rate/shift combination.
|
||||
- No audible metallic or phasey artefacting on sustained tonal material at ±6 st and
|
||||
75–133 % rate; transient material at 50 % / 200 % is no worse smeared than the interim path.
|
||||
- The Gate sustain-loop contract is unchanged: **loop the source, shift the output** — loop
|
||||
points remain source-frame facts.
|
||||
- **Measure and report before the algorithm is final:** per-voice CPU at 32 voices, added
|
||||
latency (must be zero at the onset), and A/B recordings against the interim path on three
|
||||
material classes (one-shot, tonal sustain, full-mix bounce). Report to Daniel; the choice
|
||||
is not final until he has heard the A/Bs.
|
||||
|
||||
**Open questions.** **[propose, with a measurement step]** the algorithm family itself.
|
||||
**[verify]** that `core/instrument/CLAUDE.md`'s *"`WDL_Resampler` is not a Preserve engine —
|
||||
never wire it as the duration-preserving path"* is honoured: Rate legitimately changes
|
||||
duration under Preserve, so a resampled read is an explicit duration control, but the
|
||||
*pitch-preserving* mechanism must not be a resampler.
|
||||
|
||||
---
|
||||
|
||||
## Traceability — all seventeen items
|
||||
|
||||
The check that nothing was dropped. Every row points at a track that exists above.
|
||||
@@ -1373,8 +1613,8 @@ proof it exists to give.
|
||||
from `TODO-1.0.md`, and not a track this plan originally scoped. The second such track
|
||||
in this plan today; if others appear, they belong on this list rather than in the
|
||||
table.
|
||||
- **All of Phase Γ** (`pg-*`). Seven tracks from a direct interview with Daniel
|
||||
(2026-08-01), not from `TODO-1.0.md`. Listed here as a block rather than per track,
|
||||
- **All of Phase Γ** (`pg-*`). Eight tracks across three waves, from a direct interview with
|
||||
Daniel (2026-08-01), not from `TODO-1.0.md`. Listed here as a block rather than per track,
|
||||
because the whole phase is outside the source doc; the product reasoning lives in
|
||||
`docs/product/instrument-control-surface.md` and the automation scoping it defers in
|
||||
`docs/product/parameter-automation.md`. Γ-W3-T1 additionally **discharges** the
|
||||
@@ -1447,15 +1687,20 @@ Phase Ξ — The resample loop (W1 concurrency-safe with Θ from Θ-W
|
||||
T1 capture-signal-popup .................... 15 (popup)
|
||||
|
||||
Phase Γ — The instrument's control surface (none of the seventeen; runs before Ξ-W2)
|
||||
W1 Foundations [3 tracks, disjoint by surface]
|
||||
W1 Foundations [5 tracks, disjoint by surface]
|
||||
T1 knob-interaction-law ....... modifiers + ms/semitone tapers + reset bypass
|
||||
T2 master-bus-audio ........... limiter + meter ballistics + processor [payload v14]
|
||||
T2 master-bus-audio ........... limiter + meter ballistics + dynamic PDC [payload v14]
|
||||
T3 contour-trace-curves ....... staged traces draw curved, knot on its trace
|
||||
T4 editor-floor-and-row-law ... floor 1190x680 + budget constants + row predicate
|
||||
T5 preserve-time-stretch ...... real stretcher [measure-and-report gate]
|
||||
W2 New controls, and the overlay's marks [2 tracks]
|
||||
T1 pitch-rate-deck ............ Rate + Pitch, Varisp/Presrv compounding [payload v15]
|
||||
T2 loop-crossfade-ux .......... four-mark grammar; fade painted where it is heard
|
||||
W3 The reflow [1 track]
|
||||
T1 deck-reflow ................ two rows + double-height MASTER; floor 1190x680
|
||||
W4 Preserve time-stretch [1 track]
|
||||
T1 preserve-time-stretch ...... real stretcher [measure-and-report gate]
|
||||
T1 deck-reflow ................ two rows + double-height MASTER, by construction
|
||||
|
||||
Resequenced 2026-08-01: the reflow split canvas (W1-T4) from arrangement (W3-T1), and
|
||||
preserve-time-stretch moved W4 -> W1-T5, which retires Rate's interim stand-in.
|
||||
Shared files, named: engine/CMakeLists.txt (W1-T2 | W1-T5) and editor_session.cpp
|
||||
(W2-T1 | W2-T2) — both textual adjacency, not semantic contention.
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user