instrument: fix AHD node-tracking/tie-break/live-latch defects and close staged-envelope-curve test gaps

This commit is contained in:
2026-07-31 09:52:17 -04:00
parent d60ab1524a
commit 2fa1405b06
27 changed files with 360 additions and 86 deletions
+12 -5
View File
@@ -346,14 +346,14 @@ public:
// Trigger amp and filter envelopes. A zero/negative span finishes immediately.
void configure(std::int64_t spanFrames, const AhdParams& params) {
span_ = spanFrames > 0 ? spanFrames : 0;
fit(params);
fit(params, /*latchFinished=*/false); // a fresh note starts from a clean read
smooth_.clear();
}
// Peer of AdsrEnvelope::snapLive: a voice that has rendered nothing takes the new shape
// outright, with no step to absorb.
void snapLive(const AhdParams& params) {
fit(params);
fit(params, /*latchFinished=*/false);
smooth_.clear();
}
@@ -361,7 +361,10 @@ public:
// why this smooths rather than holding a normalized position.
void applyLive(double sourceOffset, const AhdParams& params) {
const double before = ahdLevelAt(sourceOffset, fit_, attackCurve_, decayCurve_);
fit(params);
// LATCHED: a voice already read past its fitted total must never resurge because a
// later live move reopened the total. Reachable on any active() voice, including one
// ringing out past its own end (voice.h) where tickAmplitude() still runs.
fit(params, /*latchFinished=*/true);
const double after = ahdLevelAt(sourceOffset, fit_, attackCurve_, decayCurve_);
if (after != before) smooth_.absorb(before - after);
}
@@ -381,11 +384,15 @@ public:
const AhdSpan& stages() const { return fit_; }
private:
void fit(const AhdParams& p) {
// `latchFinished`: once true, a re-fit can only ever KEEP finished_ true, never clear it —
// see applyLive above for why. configure()/snapLive() pass false: those are a fresh read
// (new note or a not-yet-rendered voice), which must compute finished_ from scratch.
void fit(const AhdParams& p, bool latchFinished) {
fit_ = fitAhd(span_, p);
attackCurve_ = p.attackCurve;
decayCurve_ = p.decayCurve;
finished_ = (fit_.total <= 0);
const bool empty = (fit_.total <= 0);
finished_ = latchFinished ? (finished_ || empty) : empty;
}
std::int64_t span_ = 0;