instrument: fix AHD node-tracking/tie-break/live-latch defects and close staged-envelope-curve test gaps

This commit is contained in:
2026-07-31 09:52:17 -04:00
parent d60ab1524a
commit 2fa1405b06
27 changed files with 360 additions and 86 deletions
+15 -12
View File
@@ -1068,21 +1068,24 @@ static void testTriggerLengthWithStart() {
}
// --- Trigger fade-in / fade-out ramp shape (equal-power default). ---
static void testTriggerFadeShape() {
// 100 frames, 100% length, fadeIn 20, fadeOut 20. Head ramps 0->1, tail ramps 1->0, unity
// between. Equal-power: sin/cos ramps, monotonic, endpoints ~0 and ~1.
// 100 frames, 100% length, attack 20 / decay 20, holdFraction 1.0 — triggerSample() sets no
// curve exponent, so both stages default to util::kCurveNeutral (1.0): the ramps are LINEAR,
// not the retired fade pair's equal-power sin/cos. Asserted against the closed form rather
// than monotonicity alone — a monotonicity-only check is blind to exactly this shape change.
static void testTriggerAhdFadeShape() {
SampleData km = (triggerSample(100, 1.0, 20, 20));
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
std::vector<AudioSample> out;
eng.render(out, 120);
CHECK(approx(out[0], 0.0, 1e-3)); // fade-in starts at 0
// Fade-in monotonic non-decreasing.
for (std::size_t i = 1; i < 20; ++i) CHECK(out[i] >= out[i - 1] - 1e-4);
// Unity plateau in the middle.
for (std::size_t i = 25; i < 75; ++i) CHECK(approx(out[i], 1.0, 1e-3));
// Fade-out monotonic non-increasing over [80,100).
for (std::size_t i = 81; i < 100; ++i) CHECK(out[i] <= out[i - 1] + 1e-4);
for (std::size_t i = 0; i < 20; ++i) {
CHECK(approx(out[i], static_cast<double>(i) / 20.0, 1e-3));
}
// Hold plateau at unity.
for (std::size_t i = 20; i < 80; ++i) CHECK(approx(out[i], 1.0, 1e-3));
for (std::size_t i = 80; i < 100; ++i) {
CHECK(approx(out[i], 1.0 - static_cast<double>(i - 80) / 20.0, 1e-3));
}
// Past playEnd = silence.
for (std::size_t i = 100; i < 120; ++i) CHECK(approx(out[i], 0.0, 1e-6));
}
@@ -1099,7 +1102,7 @@ static void testTriggerEdgeCases() {
for (float v : out) CHECK(approx(v, 0.0, 1e-6));
CHECK(eng.activeVoiceCount() == 0);
}
// Fades that sum beyond the play length are clamped (no crash, no negative gain, amp in [0,1]).
// AHD attack + decay beyond the play length are fitted by fitAhd, not overflowed (no crash, no negative gain, amp in [0,1]).
{
// 40 frames, 100% -> playLen 40; fadeIn 30 + fadeOut 30 = 60 > 40 -> clamped.
SampleData km = (triggerSample(40, 1.0, 30, 30));
@@ -2578,7 +2581,7 @@ int main() {
testAhdsrHoldZeroEqualsAdsr();
testTriggerLengthFractionFrames();
testTriggerLengthWithStart();
testTriggerFadeShape();
testTriggerAhdFadeShape();
testTriggerEdgeCases();
testTriggerIgnoresNoteOff();