Bake window: derived note lengths carry exact durations, not ladder rungs — a long take is no longer cut at 384 beats

Hold keeps its picker. Also: one home for the %-fold, duration-ordered Hold travel, and a corrupt tail degrades to absent rather than fabricating one.
This commit is contained in:
2026-08-01 21:10:38 -04:00
parent 19aeb92775
commit 65f6070348
35 changed files with 772 additions and 226 deletions
+13 -15
View File
@@ -7,7 +7,7 @@
#include "core/instrument/engine/loop/loop_span.h" // resolveLoop (the one sustain-loop fold)
#include "core/instrument/engine/voice.h" // kDeclickFrames (the terminal ramp length)
#include "core/instrument/map/trigger_seam.h" // the one Trigger span formula + %-fold
#include "core/instrument/map/trigger_seam.h" // triggerPlayLength (the one span formula)
namespace reasampler::instrument::bake {
@@ -71,9 +71,8 @@ bool bakeWindowNeedsHold(const SampleData& dialed) {
}
NoteProgram defaultBakeProgram(const SampleData& dialed, int renderSampleRate,
note::Tempo tempo, note::Division hold,
note::Velocity velocity) {
NoteProgram p; // 1/4 straight, capture opening at note-on
note::Division hold, note::Velocity velocity) {
NoteProgram p; // a quarter note, capture opening at note-on
p.velocity = velocity;
if (renderSampleRate <= 0) return p;
const double rate = static_cast<double>(renderSampleRate);
@@ -86,25 +85,24 @@ NoteProgram defaultBakeProgram(const SampleData& dialed, int renderSampleRate,
double endOffsetSeconds = 0.0;
if (dialed.play.playMode == PlayMode::Trigger) {
// Trigger ignores note-off entirely: the sound ends when the read head reaches the
// play span's end, which has nothing to do with the note's length — so the end
// offset is whatever is left after the note, positive or negative.
const std::int64_t span = map::triggerPlayLength(
map::effectiveLengthFraction(dialed.play), frameCount, start);
endOffsetSeconds = static_cast<double>(span) / rate * stretch -
tempo.beatsToSeconds(note::divisionBeats(p.length));
// play span's end. The note is that span, so the window closes on the sound rather
// than on a length the voice never consulted.
const std::int64_t span =
map::triggerPlayLength(effectiveLengthFraction(dialed.play), frameCount, start);
p.length = note::lengthOfSeconds(static_cast<double>(span) / rate * stretch);
} else if (bakeWindowNeedsHold(dialed)) {
// The loop cycles for as long as the note is held, so the hold IS the length, and the
// release is the one stage that runs after note-off.
p.length = hold;
p.length = note::lengthOfDivision(hold);
endOffsetSeconds = releaseSeconds;
} else {
// Gate with no loop: the read head runs off the source and frees the voice whether or
// not the gate is still down, so the maximal sound is the whole post-start span held.
// Rounding the note UP to a programmable length that outlasts it costs nothing — the
// voice is already gone by then — while a shorter note releases mid-attack.
// Exact, not a ladder rung: a source longer than the ladder's top rung would otherwise
// take that rung and release mid-sound, and rounding up to one costs trailing silence
// on every other source.
const std::int64_t postStart = (std::max)(std::int64_t{0}, frameCount - start);
const double exhaustSeconds = static_cast<double>(postStart) / rate * stretch;
p.length = note::shortestDivisionAtLeast(tempo.secondsToBeats(exhaustSeconds));
p.length = note::lengthOfSeconds(static_cast<double>(postStart) / rate * stretch);
endOffsetSeconds = releaseSeconds;
}
// The voice rings its last output out over kDeclickFrames instead of hard-cutting it, and