fix: close round-3 review findings — smallest-target-first, residue test fix, extraction

Waveform overlay now resolves node/tab/marker click collisions by target area instead of check order; residue test now uses a distinguishing fixture; Gate-unavailable-while-drawn logic extracted to one pure helper shared by resolvePlay and applyControl.
This commit is contained in:
2026-07-31 23:44:05 -04:00
parent c3d67bc3da
commit 757e1585d6
11 changed files with 255 additions and 87 deletions
+42
View File
@@ -344,6 +344,25 @@ static void testTwoPointContourRisingFromZeroSoundsForItsFullSpan() {
CHECK(v.soundingNote()); // still sounding at the midpoint, rising toward 1
}
// The positive direction of the fix above: a contour whose final segment is flat at 0 (here,
// the whole two-point span) DOES free the voice early, on its very first tick. Nothing exercises
// this without it — a future tightening of the gate (e.g. requiring more than onFinalSegment() +
// segmentEndValue()) could silently turn the early-free off, which is a performance regression
// (a ringing but silent voice) rather than an audible one, so nothing else would catch it.
static void testFlatZeroFinalSegmentStillFreesTheVoiceEarly() {
const VelocityCurve contour =
VelocityCurve::fromPoints({{kVelMin, 0.0}, {kVelMax, 0.0}}, CurveDomain::Unipolar);
const std::size_t frames = 1000;
const SampleData s = splineAmpSample(frames, contour);
Voice v;
v.start(60, 100, s);
CHECK(v.soundingNote()); // fresh note: sounding before anything is rendered
const double y0 = v.renderFrame();
CHECK(near(y0, 0.0, 1e-9));
CHECK(!v.soundingNote()); // a genuine permanent terminus, not a mid-contour dip
}
// --- 9. A fresh spline EG opens on the smooth y = 1 - x ----------------------
static void testAFreshSplineEgDefaultsToTheSmoothDownwardSlope() {
@@ -424,6 +443,27 @@ static void testGateIsUnavailableWhileASplineEgIsActiveAndReturnsAfterwards() {
CHECK(!deckKnobInert(DeckParam::kFilterModAmt, gates));
}
// enforceGateUnavailableWhileDrawn (play_params.h) is the ONE enforcement resolvePlay and the
// editor's applyControl both call — resolvePlay's own coverage above only exercises it through
// the frames mirror; pin it directly over BOTH representations it is shared between, closing the
// coverage gap the extraction was for (applyControl has no shell test target of its own).
static void testEnforceGateUnavailableWhileDrawnForcesTriggerOnBothRepresentations() {
PlaySeconds seconds;
seconds.playMode = PlayMode::Gate;
enforceGateUnavailableWhileDrawn(seconds);
CHECK(seconds.playMode == PlayMode::Gate); // not splineActive -> untouched
seconds.ampSpline.mode = EnvMode::Spline;
enforceGateUnavailableWhileDrawn(seconds);
CHECK(seconds.playMode == PlayMode::Trigger);
PlayParams frames;
frames.playMode = PlayMode::Gate;
frames.filter.enabled = true;
frames.filterSpline.mode = EnvMode::Spline;
enforceGateUnavailableWhileDrawn(frames);
CHECK(frames.playMode == PlayMode::Trigger);
}
// --- 11. The velocity->amp curve is the same grammar -------------------------
static void testTheVelocityAmpCurveGainsTheToggleAndKeepsItsDelete() {
@@ -491,8 +531,10 @@ int main() {
testAV12PayloadLoadsWithoutLoss();
testAContourReplaysProportionallyOnADifferentLengthSample();
testTwoPointContourRisingFromZeroSoundsForItsFullSpan();
testFlatZeroFinalSegmentStillFreesTheVoiceEarly();
testAFreshSplineEgDefaultsToTheSmoothDownwardSlope();
testGateIsUnavailableWhileASplineEgIsActiveAndReturnsAfterwards();
testEnforceGateUnavailableWhileDrawnForcesTriggerOnBothRepresentations();
testTheVelocityAmpCurveGainsTheToggleAndKeepsItsDelete();
testSplineCursorBinarySearchAgreesWithTheColdReaderOnAJumpingRead();
if (g_fail == 0) std::printf("spline_egs: all tests passed\n");