fix: pin the waveform arbitration in a testable predicate, close round-4 review minors

Extracts resolveWaveformClaim (core/instrument/ui/spline_edit) so the shell's node/tab/marker click resolution is unit-tested directly, not just its input geometry; folds the staged-envelope node into it; fixes comment accuracy, a cost regression, and test fidelity issues.
This commit is contained in:
2026-08-01 00:17:06 -04:00
parent 757e1585d6
commit aedcc6976c
10 changed files with 316 additions and 179 deletions
+28
View File
@@ -363,6 +363,33 @@ static void testFlatZeroFinalSegmentStillFreesTheVoiceEarly() {
CHECK(!v.soundingNote()); // a genuine permanent terminus, not a mid-contour dip
}
// The timing, not just the fact: the case above is trivially "early" (a wholly-flat contour
// frees on frame 0), which can't distinguish "frees early" from "frees at the right frame." This
// fixture's final segment starts MID-sample, so the free must land there, not at frame 0 and not
// at the sample's natural end. The breakpoint (phase 0.5495) is deliberately off every sampled
// frame's exact phase (k/1000), so no sampled frame lands on the segment boundary itself and
// which segment "owns" that frame is never ambiguous.
static void testFlatZeroFinalSegmentFreesTheVoiceWhereItBeginsNotAtFrameZero() {
const double breakpointPhase = 0.5495;
const VelocityCurve contour = VelocityCurve::fromPoints(
{{xAt(0.0), 1.0}, {xAt(breakpointPhase), 0.0}, {xAt(1.0), 0.0}}, CurveDomain::Unipolar);
const std::size_t frames = 1000;
const SampleData s = splineAmpSample(frames, contour);
Voice v;
v.start(60, 100, s);
// Frames 0..549 (phase < breakpoint) sit on the declining first segment: still sounding.
for (std::size_t i = 0; i < 550; ++i) {
v.renderFrame();
CHECK(v.soundingNote());
}
// Frame 550 (phase 0.55) is the first sampled frame past the breakpoint, on the flat-zero
// final segment — this is where the early-free fires.
const double y550 = v.renderFrame();
CHECK(near(y550, 0.0, 1e-9));
CHECK(!v.soundingNote());
}
// --- 9. A fresh spline EG opens on the smooth y = 1 - x ----------------------
static void testAFreshSplineEgDefaultsToTheSmoothDownwardSlope() {
@@ -532,6 +559,7 @@ int main() {
testAContourReplaysProportionallyOnADifferentLengthSample();
testTwoPointContourRisingFromZeroSoundsForItsFullSpan();
testFlatZeroFinalSegmentStillFreesTheVoiceEarly();
testFlatZeroFinalSegmentFreesTheVoiceWhereItBeginsNotAtFrameZero();
testAFreshSplineEgDefaultsToTheSmoothDownwardSlope();
testGateIsUnavailableWhileASplineEgIsActiveAndReturnsAfterwards();
testEnforceGateUnavailableWhileDrawnForcesTriggerOnBothRepresentations();