# 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.** `renderBake` takes its `SampleData` BY VALUE and detaches `SampleData::live` before constructing a `VoiceEngine` for 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 at `noteOffFrame` so 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.cpp` carries the argument for the gain, and `bake_reset.h` records where the printed master stage stops. - **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 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.** `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 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.cpp` carries the argument at the assignment. - **`kStageTimeMaxSeconds` (the stage-time ceiling `param_taper` owns) 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`, and `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 both the parameter set and the post-mixer master gain. ## Gotchas - **`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 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 (`playbackStretch` argues 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.