Files
reasampler/src/core/instrument/bake/CLAUDE.md
T

8.1 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. 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_plandefaultBakeProgram (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_renderBakeAudio 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_resetBakeReset 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.