instrument: fix AHD node-tracking/tie-break/live-latch defects and close staged-envelope-curve test gaps
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
#include "core/util/clamp01.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath> // std::fabs
|
||||
#include <cstdlib> // std::abs
|
||||
|
||||
namespace reasampler::instrument::ui {
|
||||
@@ -91,15 +92,20 @@ SegmentLevels segmentLevels(const StageEnvelope& env, EnvNode knot) {
|
||||
}
|
||||
|
||||
// A knot drag: the grab-time mid-level shifted by the pixel delta, read back through
|
||||
// curve_law's inverse. Both directions go through the ONE law, which is why the knot and the
|
||||
// inner dial cannot express different exponents.
|
||||
// curve_law's inverse (curve_law.h owns why the knot and the inner dial share this one law).
|
||||
double curveFromKnotDrag(const StageEnvelope& grabEnv, EnvNode knot, double grabExponent,
|
||||
const Rect& area, int dyPixels) {
|
||||
const SegmentLevels seg = segmentLevels(grabEnv, knot);
|
||||
if (!seg.ok) return grabExponent;
|
||||
const double grabLevel = seg.start + (seg.end - seg.start) * curveMidLevel(grabExponent);
|
||||
const double span = seg.end - seg.start;
|
||||
// segmentLevels only rejects an EXACTLY level segment; a near-level one (e.g. sustain
|
||||
// 0.99) still passes with a tiny divisor here, so one pixel of drag can swing `u` by
|
||||
// ~1.0 and saturate the exponent. Floor the magnitude at a couple of pixels' worth of
|
||||
// level travel — a segment thinner than that is visually a no-op drag anyway.
|
||||
if (std::fabs(span) < 2.0 * levelPerPixel(area)) return grabExponent;
|
||||
const double grabLevel = seg.start + span * curveMidLevel(grabExponent);
|
||||
const double newLevel = grabLevel - static_cast<double>(dyPixels) * levelPerPixel(area);
|
||||
return curveFromMidLevel((newLevel - seg.start) / (seg.end - seg.start));
|
||||
return curveFromMidLevel((newLevel - seg.start) / span);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -196,10 +202,21 @@ StageEnvelope resolveNodeDrag(const StageEnvelope& grabEnv, EnvNode node, const
|
||||
out.holdFraction = clamp01((s.hold + dSec) / rem);
|
||||
break;
|
||||
}
|
||||
case EnvNode::DecayEnd:
|
||||
out.decaySeconds =
|
||||
std::clamp(grabEnv.decaySeconds + dSec, 0.0, bounds.maxDecaySeconds);
|
||||
case EnvNode::DecayEnd: {
|
||||
// DecayEnd is DRAWN at t0 + total, and total = attack + decay + (span-attack-decay)
|
||||
// * holdFraction, so d(total)/d(decay) = 1 - holdFraction: Hold eats a holdFraction
|
||||
// share of whatever decay gives up. Scaling by 1/(1-frac) makes the drawn endpoint
|
||||
// track the cursor 1:1, matching every other node. At frac == 1.0 (the Trigger
|
||||
// default) Hold consumes the WHOLE remainder regardless of decay's value, so the
|
||||
// derivative is exactly 0 — no scale recovers motion there, and decaySeconds is left
|
||||
// unchanged rather than divided by zero.
|
||||
const double denom = 1.0 - clamp01(grabEnv.holdFraction);
|
||||
if (denom > 1e-9) {
|
||||
out.decaySeconds =
|
||||
std::clamp(grabEnv.decaySeconds + dSec / denom, 0.0, bounds.maxDecaySeconds);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EnvNode::AttackCurve:
|
||||
out.attackCurve =
|
||||
curveFromKnotDrag(grabEnv, node, grabEnv.attackCurve, rect, dyPixels);
|
||||
|
||||
Reference in New Issue
Block a user