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
+21 -5
View File
@@ -195,13 +195,29 @@ bool splineActive(const Play& p) {
(p.filter.enabled && p.filterSpline.mode == EnvMode::Spline);
}
// The one enforcement of splineActive's rule (see its doc above). Header-inline and
// allocation-free: play_params.h sits on the per-voice-per-sample include path. Both callers —
// resolvePlay (sample_map.cpp) and the editor's applyControl — route through here, so the two
// cannot drift apart.
// The mode the engine will actually run, and the one home of splineActive's rule (see its doc
// above). Header-inline and allocation-free: play_params.h sits on the per-voice-per-sample
// include path. Every caller — resolvePlay (sample_map.cpp), the editor's applyControl, and
// the editor's read-only predicates — routes through one of these two, so none of them can
// drift into a second reading of the fields.
template <class Play>
PlayMode effectivePlayMode(const Play& p) {
return splineActive(p) ? PlayMode::Trigger : p.playMode;
}
template <class Play>
void enforceGateUnavailableWhileDrawn(Play& p) {
if (splineActive(p)) p.playMode = PlayMode::Trigger;
p.playMode = effectivePlayMode(p);
}
// The %-length the voice ACTUALLY plays. Same rule family, same reason it is templated: a drawn
// contour is a pure time function over the full sample length, so any active spline EG folds
// the fraction to 1.0 while the stored knob goes inert — but the stored value survives, so a
// pre-spline setting is still there to be read. Every consumer of the Trigger span must fold it
// here or it silently plays/draws/bakes a fraction of the take.
template <class Play>
double effectiveLengthFraction(const Play& p) {
return splineActive(p) ? 1.0 : p.trigger.lengthFraction;
}
// [start, end) frames, half-open. A zero-length loop (start == end) is the "no sustain loop"
+7 -8
View File
@@ -6,8 +6,6 @@
#include <algorithm>
#include "core/instrument/map/trigger_seam.h" // effectiveLengthFraction (the one %-length rule)
namespace reasampler {
void Voice::presizePreserveShifters(std::int64_t windowFrames) {
@@ -103,12 +101,13 @@ void Voice::start(int note, int velocity, const SampleData& sample, bool declick
env_.noteOn();
playEnd_ = 0; // unused in Gate
} else {
// Trigger: play [start, playEnd) where playEnd = start + round(frac*(frames-start)).
// The spline fold lives in effectiveLengthFraction (trigger_seam.h), which the bake's
// window derivation reads too — a second copy of it here is what let a stored-but-inert
// %-knob shorten the bake while the voice played the whole take.
double frac = instrument::map::effectiveLengthFraction(p);
if (frac <= 0.0) frac = 0.0; // %=0 -> zero play length (finishes immediately)
// Trigger: play [start, playEnd) where playEnd = start + round(frac*(frames-start))
// map/trigger_seam.h's formula, evaluated inline because the engine does not depend on
// map/. The spline fold is effectiveLengthFraction (play_params.h); a second copy of it
// here is what let a stored-but-inert %-knob shorten the bake while the voice played
// the whole take.
double frac = effectiveLengthFraction(p);
if (!(frac > 0.0)) frac = 0.0; // %=0 (or a corrupt NaN) -> finishes immediately
if (frac > 1.0) frac = 1.0;
std::int64_t playLen = static_cast<std::int64_t>(
static_cast<double>(postStart) * frac + 0.5); // round