fix: close review findings on spline EGs — engine, codec, and popup/overlay UI grammar

Live pitch depth, Gate/Spline enable-rule agreement, inert kTrigLength, NaN wire guards, hard-flag-tail corruption no longer wipes the record, RT/cold spline tie-break, retired alt-click, marker-shadow fix, plus new test coverage.
This commit is contained in:
2026-07-31 22:27:41 -04:00
parent e44bd42dd9
commit 1c774226d3
22 changed files with 414 additions and 88 deletions
+29 -6
View File
@@ -128,7 +128,11 @@ void readCurveTail(ByteReader& r, VelocityCurve& curve,
for (std::uint32_t i = 0; i < ptCount && r.ok; ++i) {
const double vel = bitsToDouble(r.u64());
const double value = bitsToDouble(r.u64());
pts.push_back(VelocityPoint{vel, value});
// A NaN velocity breaks fromPoints' stable_sort (not a strict weak ordering with NaN
// present); a NaN value reaches the RT eval's multiply. Same non-finite-falls-back-to-0
// guard as every other wire double this codec reads.
pts.push_back(VelocityPoint{std::isfinite(vel) ? vel : 0.0,
std::isfinite(value) ? value : 0.0});
}
if (r.ok) {
curve = reasampler::instrument::engine::VelocityCurve::fromPoints(std::move(pts), domain);
@@ -149,24 +153,43 @@ void readSplineEnv(ByteReader& r, SplineEnv& s) {
const double x = bitsToDouble(r.u64());
const double y = bitsToDouble(r.u64());
const bool hard = (r.u8() != 0);
pts.push_back(VelocityPoint{x, y, hard});
// Same NaN guard as readCurveTail: an x NaN breaks fromPoints' sort, a y NaN reaches
// SplineCursor::eval's multiply into the per-sample amp gain.
pts.push_back(VelocityPoint{std::isfinite(x) ? x : 0.0, std::isfinite(y) ? y : 0.0, hard});
}
if (!r.ok) return;
s.mode = spline ? EnvMode::Spline : EnvMode::Staged;
if (pts.size() < 2) {
// fromPoints' own sub-2-point fallback is flat()/zero() by DOMAIN — the neutral velocity
// curve response (a full-open gate). A spline EG's documented neutral is y = 1 - x
// instead, so a malformed/short block substitutes that rather than fromPoints' default.
s.contour = VelocityCurve::rampDown();
return;
}
s.contour = VelocityCurve::fromPoints(std::move(pts),
reasampler::instrument::engine::CurveDomain::Unipolar);
}
// Apply a hard-flag tail to an already-read velocity curve. A count that disagrees with the
// curve fromPoints actually produced is dropped rather than applied to shifted knots.
// curve fromPoints actually produced — including an out-of-bounds or truncated one — is
// dropped rather than applied to shifted knots, and the whole params record parsed ahead of
// this tail survives (component_state_io.h's documented promise): if THIS call is what tripped
// r.ok (a truncated count field), it is revived before returning. An r.ok already false on
// entry (an earlier, unrelated field genuinely truncated) is left alone — that failure is not
// this tail's to forgive.
void readHardFlags(ByteReader& r, VelocityCurve& curve) {
const bool enteredOk = r.ok;
const std::uint32_t count = r.u32();
if (!r.ok) {
if (enteredOk) r.ok = true; // a truncated count field: nothing to apply
return;
}
const std::size_t remaining = r.bytes.size() > r.pos ? r.bytes.size() - r.pos : 0;
if (count > remaining) { r.ok = false; return; }
if (count > remaining) return; // bound-and-skip: cannot safely reserve/read this many
std::vector<std::uint8_t> flags;
flags.reserve(count);
for (std::uint32_t i = 0; i < count && r.ok; ++i) flags.push_back(r.u8());
if (!r.ok || flags.size() != curve.size()) return;
for (std::uint32_t i = 0; i < count; ++i) flags.push_back(r.u8());
if (flags.size() != curve.size()) return;
for (std::size_t i = 0; i < flags.size(); ++i) curve.setHard(i, flags[i] != 0);
}
+1 -1
View File
@@ -259,7 +259,7 @@ PlayParams resolvePlay(const PlaySeconds& stored, int sampleRate) {
// The three drawn contours are normalized over the sample's own length, so no rate resolves
// them — they carry through verbatim, which is also what makes a different-length capture
// replay the same shape proportionally.
out.ampSpline = stored.ampSpline;
out.ampSpline = stored.ampSpline;
out.pitchSpline = stored.pitchSpline;
out.filterSpline = stored.filterSpline;
// Gate is unavailable while any EG is drawn — see splineActive (play_params.h) for why.