Fix spline-drag rollback, bake contour leak, held-note live miss, amp clamp

Editor close mid-drag now rolls back published contours like capture-loss does; bake detaches liveContours so a printed capture can't observe a live drag; a held note now receives a live publish that lands in the same block as a note-on; amp spline read clamped to [0,1].
This commit is contained in:
2026-09-25 13:59:37 -04:00
parent f7505d0e53
commit 4567744b8f
10 changed files with 109 additions and 18 deletions
+3 -2
View File
@@ -225,8 +225,9 @@ private:
// 128-point ceiling.
//
// Holds a RAW POINTER into the bound point array: the caller guarantees the array outlives the
// cursor, or rebinds it first. The voice binds against its SampleData, which has exactly that
// lifetime, or against its engine's live contour copy, which the engine rebinds before reusing.
// cursor, or rebinds it first. The voice binds against its SampleData (that lifetime), or
// against its engine's live contour copy — the double-buffer contract VoiceEngine::contours_
// owns (voice_engine.h) is what makes that second binding safe.
class SplineCursor {
public:
// Binds `c` if it has an evaluable segment; a shorter curve leaves the cursor inactive so
+2 -2
View File
@@ -335,8 +335,8 @@ void Voice::applyLive(const instrument::engine::LiveValues& live, bool snap) {
namespace {
// One EG's half of Voice::applyContours. `before` is read through the OLD binding, which is
// still intact: the engine only overwrites a buffer no active voice is bound to.
// One EG's half of Voice::applyContours. `before` is read through the OLD binding — still
// intact, by the double-buffer contract VoiceEngine::contours_ owns (voice_engine.h).
void rebindContour(SplineCursor& cursor, StepSmoother& smooth,
const instrument::engine::ContourPoints& pts, double phase, bool snap) {
if (!cursor.active()) return;
+6
View File
@@ -302,10 +302,16 @@ private:
// Early-free at a genuine permanent terminus (the spline analogue of a staged AHD's
// finished()) — onFinalSegment()/segmentEndValue()'s own doc comments own the why.
amp = readSpline(ampSplineCur_, ampSplineSmooth_);
// A swap-step absorbed by ampSplineSmooth_ can briefly push the sum outside [0,1]
// (a contour's own points are drawn in-range, but the smoother's addend isn't); the
// filter clamps its own read for the same reason (below). The finished-check reads
// the PRE-clamp value so a genuine zero-terminus isn't masked by the clamp itself.
if (amp == 0.0 && ampSplineCur_.onFinalSegment() &&
ampSplineCur_.segmentEndValue() == 0.0) {
amplitudeDone_ = true;
}
if (amp < 0.0) amp = 0.0;
else if (amp > 1.0) amp = 1.0;
} else if (playMode_ == PlayMode::Gate) {
amp = env_.tick();
if (env_.finished()) amplitudeDone_ = true;
+6 -2
View File
@@ -69,9 +69,13 @@ void VoiceEngine::applyContoursToActive() {
}
void VoiceEngine::startVoice(Voice& voice, int note, int velocity) {
// FIRST, so no sounding voice is left bound to a buffer the next read overwrites.
// FIRST, so no sounding voice is left bound to a buffer the next read overwrites. A held
// note sharing this generation with the note-on must see it too — refreshLive() alone
// would consume the generation without applying it, and since applyLiveToActive() only
// acts on a NEW generation, the held voice would then never see this publish at all
// (not merely late a block), only catching up whenever the next one arrives.
applyContoursToActive();
refreshLive();
applyLiveToActive();
// THE read of the note-on-latched commit class, and the only one: a published block outranks
// the snapshot's own copy (a live edit deliberately leaves that stale), and applyLive below
// touches none of these three — so a move reaches the next note and no sounding one.