# 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 whole chain is printed — voice, master gain, then the limiter, in the processor's
  own order.** `bake_render.cpp`'s master stage carries the argument. The limiter is printed
  only when it is ENGAGED; bypassed, `renderBake` never constructs one and the result is the
  pre-limiter render frame for frame. The lookahead is compensated inside the render — the
  buffers carry an extra flush window and the capture is read past it — so an engaged bake
  under the ceiling is bit-identical to a bypassed one, not the same audio 2 ms late.
- **A printed capture replayed through an engaged limiter is limited TWICE — a NAMED
  boundary, not a bug**, and the same shape as the automation-lane limitation below. The
  reset is what normally prevents it (`limiterEnabled` is not on the survive list, so a bake
  hands the enable back off), and at unity the second pass has nothing to take: every sample
  of the printed file is already at or under the ceiling, and the limiter reduces only where
  its detector reads ABOVE it — which after a bake means its inter-sample estimate alone. Dial
  the enable back on over raised gain, though, and the capture is limited on top of limiting
  that is already in its samples. Not detectable from inside the instrument and not corrected
  there; the user's remedy is to leave the enable where the bake put it.
- **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.
- **A host automation lane outranks the reset, and the bake cannot clear it — a NAMED
  limitation, not a bug.** Every reset-class value that is also an exposed VST3 parameter is
  now notified to the host (the reset writes through `setInstrumentParams`, which is the one
  notification funnel), so the host's DISPLAY follows the reset. A lane, however, lives in the
  host's project data: if a reset-class parameter carries one, the host replays its curve onto
  audio that already has that processing baked in — double processing, and the "sounds as the
  dialled instrument sounded just before the click" claim does not hold in that case. There is
  no detection available: `IAutomationState` reports the host's automation mode for the whole
  plug-in, not per parameter, so both "refuse the bake" and "reset only the un-automated ones"
  are unbuildable rather than merely unattractive. The user's remedy is to remove the lane.

## 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 and then the master stage, 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.
