Bake window derives itself: %-knob fold, declick pad, Gate held to exhaustion, preview velocity; Hold is the one knob a loop needs

This commit is contained in:
2026-08-01 20:26:04 -04:00
parent d3894dae6d
commit 19aeb92775
36 changed files with 1040 additions and 274 deletions
+27 -12
View File
@@ -23,10 +23,23 @@ decision about what the render made obsolete.
even a pathological envelope cannot run past the window.
- **The whole signal chain is printed, master gain included** — the gain multiply in
`bake_render.cpp` carries the argument for why.
- **A degenerate or unholdable window is refused, not rendered.** `planBake` returns nullopt
for a collapsed window, a non-positive rate, a window that rounds to no frames, and one
past `kMaxBakeFrames` — an unbounded window is a `bad_alloc` inside a UI tick, and the
seconds→frames narrowing is undefined long before the allocation would fail.
- **A degenerate or unholdable window is refused, not rendered.** `planBake` refuses a
collapsed window, a non-positive rate, a window that rounds to no frames, and one past
`kMaxBakeFrames` — an unbounded window is a `bad_alloc` inside a UI tick, and the
seconds→frames narrowing is undefined long before the allocation would fail. The refusal
carries a `BakeRefusal` naming WHICH: past-the-ceiling is a real sound that will not fit,
which reads to the user as a different sentence than an empty window.
- **The window derives itself, and Hold is the one exception.** Trigger derives from the play
span; Gate over an active sustain loop takes the user's Hold, because a loop sounds for as
long as it is held and no derivation can supply a duration; Gate WITHOUT one derives from
source exhaustion, since the read head frees the voice whether or not the gate is down.
`bakeWindowNeedsHold` is the predicate, and it reads the ENGINE's loop fold rather than the
loop fields, so the control that collects Hold cannot appear for a loop the voice refuses.
- **Trailing silence is free; truncation is not.** Every derivation rounds outward — the Gate
note rounds UP to a programmable length that outlasts the source, the Varispeed bound takes
the deepest reachable offset, and every path is padded by the voice's terminal declick ramp
(`kDeclickFrames`, unconditionally — not branched on the pitch engine that has the ramp
today). Judge any change to this module against that asymmetry.
- **The reset's survive list is written out; everything else defaults.** `resetAfterBake`
starts from a default-constructed parameter set and copies back only the mapping facts.
A parameter added later therefore resets by default — the safe direction, since
@@ -38,10 +51,10 @@ decision about what the render made obsolete.
## Modules
- `bake_plan``defaultBakeProgram` (the program a bake uses until the capture-signal popup
ships; its end offset is DERIVED from the dialed sound, never constant), `BakePlan` (the
- `bake_plan``defaultBakeProgram` (the whole programmed note, DERIVED from the dialed
sound: its note length as well as its end offset), `bakeWindowNeedsHold`, `BakePlan` (the
render window, the captured slice of it, and the two event frames), `kMaxBakeFrames`, and
`planBake`, the one `ResolvedNote` + rate -> frames resolution.
`planBake`, the one `ResolvedNote` + rate -> frames resolution, answering a `PlannedBake`.
- `bake_render``BakeAudio` and `renderBake`: the programmed note through the sample's
own voice path, summed into an interleaved buffer at the source's own channel count.
- `bake_reset``BakeReset` and `resetAfterBake`: the ratified reset scope, answered for
@@ -52,10 +65,12 @@ decision about what the render made obsolete.
- **`BakePlan` speaks two frame domains** — the captured file's and the render's, which are
offset from each other whenever the note and the capture window do not start together.
`bake_plan.h` says which field is in which; do not read them as one clock.
- **`defaultBakeProgram`'s Trigger window bounds the Varispeed read stretch, it does not
model it.** A downward pitch offset makes the read head take longer to cross the play
span, so the window is scaled by the deepest downward offset the voice can reach — an
upper bound, so a shallower excursion leaves trailing silence in the file. The
capture-signal popup is where a user sets the window exactly.
- **`defaultBakeProgram`'s Varispeed bound is an upper bound, not a model.** A downward pitch
offset makes the read head take longer to cross its span, so the window is scaled by the
deepest downward offset the voice can reach — a shallower excursion leaves trailing silence
in the file. Both the Trigger span and the Gate exhaustion length take it.
- **The bake fires at the instance's PREVIEW velocity, not a constant.** Three velocity curves
are live, so the velocity is a property of the sound being printed and not a detail of the
render; it also feeds the Varispeed bound above (a velocity→pitch curve moves the window).
- The render's channel count is the loaded `SampleData`'s, which is already the instance's
channel-mode decision — a mono-mode instance bakes mono, and that is faithful, not a fold.