Files
reasampler/src/core/instrument/bake/bake_plan.h
T

95 lines
5.1 KiB
C++

// 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 <cstdint>
#include <optional>
#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<BakePlan> plan;
BakeRefusal refusal = BakeRefusal::None;
};
PlannedBake planBake(const note::ResolvedNote& resolved, int sampleRate, int rootNote);
} // namespace reasampler::instrument::bake