fix: close review findings on the velocity-curve deck and bipolar curves

Cancels the curve-node drag whenever the popup closes so Esc mid-drag can't alias the amp curve; generalizes CurveTarget routing to one switch; fixes stale/overstated comments; clamps a pre-v12 depth fold; adds deck-inertness and filter-fold test coverage.
This commit is contained in:
2026-07-31 19:46:37 -04:00
parent 9d38f87a2d
commit cfb53aade3
22 changed files with 223 additions and 99 deletions
+11 -8
View File
@@ -90,18 +90,21 @@ namespace reasampler::instrument::map {
// prefix and lifts to 0 — the hard seam it always played.
//
// v12 (CURRENT WRITE FORMAT) is v11 PLUS the velocity->PITCH transfer curve (count + points,
// the same shape as v7's), appended after the loop crossfade. It also RE-INTERPRETS two frozen
// slots inside the v9 filter tail — the byte shape is untouched, only the meaning at v12+:
// the same shape as v7's), appended after the loop crossfade. Its y is a normalized fraction
// of kVelocityPitchRangeSemitones (play_params.h) — a full-scale constant that lives OUTSIDE
// this frozen ladder, so retuning it re-tunes every saved v12 project's pitch-curve throw. It
// also RE-INTERPRETS two frozen slots inside the v9 filter tail — the byte shape is untouched,
// only the meaning at v12+:
// * the filter's velocity curve is now BIPOLAR [-1,+1] and is the whole velocity->cutoff
// amount, not a [0,1] shape scaled by a separate depth;
// * the retired filter velAmount slot is written as a constant 1.0 and ignored on read.
// PRE-v12 LIFT: the stored [0,1] filter curve has every knot's y multiplied by that blob's
// velAmount and is re-read as bipolar. eval is homogeneous in y, so the lifted curve evaluates
// to exactly velAmount * oldCurve(v) — the product the voice used to compute per note — and a
// pre-v12 project sounds identical. A pre-v12 blob carries no pitch curve at all and lifts to
// the bipolar flat-at-zero default, which transposes nothing. A DOWNGRADE to a pre-v12 binary
// reads the constant 1.0 depth against a curve whose negative half clamps away, so it
// reproduces the curve's positive half only.
// velAmount and is re-read as bipolar. eval is homogeneous in y to within double rounding (see
// velocity_curve.h), so the lifted curve evaluates to velAmount * oldCurve(v) — the product the
// voice used to compute per note — and a pre-v12 project sounds identical. A pre-v12 blob
// carries no pitch curve at all and lifts to the bipolar flat-at-zero default, which transposes
// nothing. A DOWNGRADE to a pre-v12 binary reads the constant 1.0 depth against a curve whose
// negative half clamps away, so it reproduces the curve's positive half only.
//
// The two int64 slots the v5 play tail spends on the RETIRED Trigger fade pair are frozen in
// shape and still read: a pre-v10 blob's fade-in/fade-out become the Trigger AHD that replaced
+6 -1
View File
@@ -140,8 +140,13 @@ void readFilterTail(ByteReader& r, InstrumentParams& p, bool preVelocityVersion)
f.env.decaySeconds = bitsToDouble(r.u64());
f.env.sustainLevel = bitsToDouble(r.u64());
f.env.releaseSeconds = bitsToDouble(r.u64());
// velAmount feeds a MULTIPLIER on the stored curve's knots (below), not a param the engine
// clamps on its own — the UI never dials it outside [-1,1] (deckBipolarFromNorm), so a
// corrupt-but-finite blob value outside that range must clamp here rather than silently
// scaling the lifted curve past what fromPoints' own [-1,1] box-clamp would then truncate.
const double velFold =
preVelocityVersion ? (std::isfinite(velAmount) ? velAmount : 0.0) : 1.0;
preVelocityVersion ? std::clamp(std::isfinite(velAmount) ? velAmount : 0.0, -1.0, 1.0)
: 1.0;
readCurveTail(r, f.velocityCurve, reasampler::instrument::engine::CurveDomain::Bipolar,
velFold);
}