95 lines
5.1 KiB
C++
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
|