Files
reasampler/src/core/instrument/bake/CLAUDE.md
T
2026-08-02 13:52:32 -04:00

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. 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_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, 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.