// bake_plan — the programmed note resolved against a concrete sample rate: the frames the // offline pass renders, the slice of them the capture keeps, and the two event frames. // // Separate from bake_render because the plan is what a preview and a bake must agree on; // the render is only one consumer of it. #pragma once #include #include #include "core/instrument/engine/play_params.h" // SampleData (the dialed sound) #include "core/instrument/note/note_program.h" namespace reasampler::instrument::bake { // The render's frame ceiling, refused like any other degenerate window. A legal offset // magnitude reaches ~11.6 days, and renderBake allocates two channel buffers plus an // interleaved one from the window — an unbounded one is a bad_alloc inside a UI tick, not a // long bake. ~5.5 minutes at 48 kHz, past any musical programmed note. inline constexpr std::int64_t kMaxBakeFrames = 16'000'000; // Whether the window needs a user-supplied hold. A Gate voice over an ACTIVE sustain loop // sounds for as long as it is held, by definition — there is no intrinsic end to derive, and // this is the ONLY case in which there isn't. Answered by the engine's own loop fold, so the // control that collects the hold cannot appear for a loop the voice would refuse. bool bakeWindowNeedsHold(PlayMode mode, const SampleLoop& loop, std::int64_t crossfadeFrames, std::int64_t frameCount); bool bakeWindowNeedsHold(const SampleData& dialed); // The bake's programmed note, DERIVED from the dialed sound at `renderSampleRate` (the rate // the bake renders at, which is what the engine's frame counts are consumed against): // // Trigger — the note IS the play span (note-off is ignored anyway), stretched by the // slowest read the dialed voice can reach: Rate under BOTH engines, plus the // deepest downward pitch offset under Varispeed. // Gate, loop — `hold` is the note length; the end offset is the release. // Gate, no loop— the read head runs off the source and frees the voice whatever the gate is // doing, so the note is the whole post-start span, stretched the same way. // // Both derived lengths are EXACT durations, not ladder rungs: a source longer than the // ladder's top rung has no rung that covers it, and quantizing up to one overshoots every // other source (see note/CLAUDE.md). `hold` alone stays musical — it is a picker. // // Every case is padded by the voice's terminal declick ramp (kDeclickFrames): trailing // silence is free, and closing the window on the frame the ramp starts is a hard cut. // `hold` is read only in the Gate-with-loop case; `velocity` is the velocity the note fires // at, and it feeds the Varispeed half of that stretch as well as the render. // // Takes no tempo: nothing derived here is beat-denominated. The one field that is — `hold` — // meets the tempo in resolveNote, with the rest of the program's beat-denominated fields. note::NoteProgram defaultBakeProgram(const SampleData& dialed, int renderSampleRate, note::Division hold, note::Velocity velocity); // The render window in frames. TWO domains meet here: `totalFrames` is the captured FILE's // length, everything else counts RENDER frames from whichever comes first, note-on or the // capture opening. A positive start offset (legal — it trims the attack) puts note-on at // render frame 0 and the file's frame 0 `leadInFrames` later; a negative one does the // reverse, and the file opens on silence before the note. Either event frame may sit past // the render, which then closes before the note ever fires — a legal empty capture. struct BakePlan { std::int64_t totalFrames = 0; // frames in the captured file std::int64_t leadInFrames = 0; // rendered ahead of the file's frame 0, then discarded std::int64_t noteOnFrame = 0; // both in render frames std::int64_t noteOffFrame = 0; // The capture's root: rendering AT root is what makes the root survivable, which is // why the root parameter is the one processing control a bake does not reset. int note = 60; int velocity = 100; int sampleRate = 0; std::int64_t renderFrames() const { return leadInFrames + totalFrames; } }; // Why a window was refused. The two are different user problems and read as different // sentences: an empty window is a programming mistake, a window past the ceiling is a legal // dialed sound that simply cannot be held in one pass. enum class BakeRefusal : std::uint8_t { None, EmptyWindow, // collapsed, a non-positive rate, or a window that rounds to no frames PastFrameCeiling, // representable but longer than kMaxBakeFrames }; // The one `ResolvedNote` + rate -> frames resolution. A degenerate or unholdable window is // refused rather than rendered; `refusal` is None iff `plan` holds one. `rootNote` and the // resolved velocity are clamped into MIDI range. struct PlannedBake { std::optional plan; BakeRefusal refusal = BakeRefusal::None; }; PlannedBake planBake(const note::ResolvedNote& resolved, int sampleRate, int rootNote); } // namespace reasampler::instrument::bake