1490c25250
Dials and asserts pitch-env/filter-env/trigEnv fields the sweep previously skipped in both fixtures; no reset behavior changed.
6.0 KiB
6.0 KiB
src/core/instrument/bake — the resample bake's pure half
Scope
The offline pass that turns the dialed instrument into a file, and the reset that hands the
instrument back neutral afterwards. A fifth peer of engine/ / map/ / note/ / ui/
under core/instrument/, pure by the same rule — no REAPER types, no VST3 types, no host.
It is neither engine (it owns no voice), mapping (it resolves no capture), nor note (it holds no program): it is the composition of the three into one render, plus the one decision about what the render made obsolete.
Invariants
- The bake renders on its OWN engine, never the live one.
renderBaketakes itsSampleDataBY VALUE and detachesSampleData::livebefore constructing aVoiceEnginefor the render alone. Two consequences, both load-bearing: the audio thread's live block can neither be observed nor disturbed by a bake, and a repeated bake of one dialed sound is byte-identical because nothing outside the passed value can vary between runs. - The window bounds the render; the envelope does not. Termination is structural — the
loop runs to
BakePlan::renderFrames()and stops. That is why a Gate bake with a sustain loop active terminates: the gate is released atnoteOffFrameso the tail is real, but even a pathological envelope cannot run past the window. - The voice chain and master gain are printed; the limiter is not. The gain multiply in
bake_render.cppcarries the argument for the gain, andbake_reset.hrecords where the printed master stage stops. - A degenerate or unholdable window is refused, not rendered.
planBakerefuses a collapsed window, a non-positive rate, a window that rounds to no frames, and one pastkMaxBakeFrames— an unbounded window is abad_allocinside a UI tick, and the seconds→frames narrowing is undefined long before the allocation would fail. The refusal carries aBakeRefusalnaming 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.
bakeWindowNeedsHoldis 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 errs outward — the
Varispeed bound takes the deepest reachable offset the voice can play, 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. What it does NOT mean is quantizing: a derived length is an exact duration and a finite ladder cannot express one (note/CLAUDE.md) — rounding up to a rung truncated any source past the top rung, which is the failure this asymmetry exists to prevent. - The reset's survive list is written out; everything else defaults.
resetAfterBakestarts 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 under-resetting applies the same processing twice while over-resetting costs a re-dial. A new mapping fact must be added to the copy list explicitly. - Play mode resets to TRIGGER, not to the value struct's Gate default — the one
classification this track made against the ratified rule rather than reading off it.
bake_reset.cppcarries the argument at the assignment. kStageTimeMaxSeconds(the stage-time ceilingparam_taperowns) is not a reset-list candidate at all — it bounds a knob's taper, is never itself a dialed value, and so has no disposition to classify against the ratified reset rule.
Modules
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, andplanBake, the oneResolvedNote+ rate -> frames resolution, answering aPlannedBake.bake_render—BakeAudioandrenderBake: the programmed note through the sample's own voice path, summed into an interleaved buffer at the source's own channel count.bake_reset—BakeResetandresetAfterBake: the ratified reset scope, answered for both the parameter set and the post-mixer master gain.
Gotchas
BakePlanspeaks 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.hsays which field is in which; do not read them as one clock.defaultBakeProgram's read-rate bound is an upper bound, not a model. Anything that slows the read makes the head take longer to cross its span, so the window is scaled by the slowest read the voice can reach — a shallower excursion leaves trailing silence in the file. Rate is a term of it under BOTH engines and the deepest downward pitch offset under Varispeed alone (playbackStretchargues each); both the Trigger span and the Gate exhaustion length take the product, and the Gate-with-loop branch takes neither.- 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.