instrument: one staged-envelope system — per-segment curves, the sustain-less AHD, and a shared overlay for all three envelopes

Trigger's fade pair folds into the AHD (and goes live); the release anchors right;
Preserve rings its synthetic tail out instead of cutting it. Payload v10.
This commit is contained in:
2026-07-31 08:37:57 -04:00
parent 87d7ceb066
commit 13e8c5c4d9
51 changed files with 3406 additions and 1812 deletions
+55 -10
View File
@@ -26,27 +26,27 @@ static int g_fail = 0;
// toggle + row toggle), MASTER (1 cell, no toggle).
static std::vector<DeckGroupDesc> shellLikeDeck() {
std::vector<DeckGroupDesc> g;
g.push_back({0, 78, {100, 44}, {1, 2, 3, 4, 5}, {}});
g.push_back({1, 38, {101, 48}, {6}, {}});
g.push_back({2, 58, {102, 32}, {7, 8, 9}, {}});
g.push_back({3, 38, {103, 40}, {10}, {104, 44}});
g.push_back({4, 46, {}, {11}, {}});
g.push_back({0, 78, {}, {100, 44}, {1, 2, 3, 4, 5}, {}});
g.push_back({1, 38, {}, {101, 48}, {6}, {}});
g.push_back({2, 58, {}, {102, 32}, {7, 8, 9}, {}});
g.push_back({3, 38, {}, {103, 40}, {10}, {104, 44}});
g.push_back({4, 46, {}, {}, {11}, {}});
return g;
}
static void testGroupWidth() {
// Knob row dominates: 5 cells (240) > caption row (78 + 4 + 88 = 170) -> 240 + 2*6.
DeckGroupDesc amp{0, 78, {100, 44}, {1, 2, 3, 4, 5}, {}};
DeckGroupDesc amp{0, 78, {}, {100, 44}, {1, 2, 3, 4, 5}, {}};
CHECK(deckGroupWidth(amp) == 5 * kDeckCellW + 2 * kDeckGroupPadX);
// Caption row dominates: 38 + 4 + 96 = 138 > 48 -> 138 + 12.
DeckGroupDesc pitch{1, 38, {101, 48}, {6}, {}};
DeckGroupDesc pitch{1, 38, {}, {101, 48}, {6}, {}};
CHECK(deckGroupWidth(pitch) == 38 + kDeckToggleGap + 2 * 48 + 2 * kDeckGroupPadX);
// Row toggle counts into the knob row: 48 + 4 + 88 = 140 > caption 38+4+80=122.
DeckGroupDesc voice{3, 38, {103, 40}, {10}, {104, 44}};
DeckGroupDesc voice{3, 38, {}, {103, 40}, {10}, {104, 44}};
CHECK(deckGroupWidth(voice) ==
kDeckCellW + kDeckToggleGap + 2 * 44 + 2 * kDeckGroupPadX);
// No toggles: max(caption, cells) + padding.
DeckGroupDesc master{4, 46, {}, {11}, {}};
DeckGroupDesc master{4, 46, {}, {}, {11}, {}};
CHECK(deckGroupWidth(master) == kDeckCellW + 2 * kDeckGroupPadX);
}
@@ -148,7 +148,7 @@ static void testHitTest() {
// A blank cell (id -1) misses even though its rect exists.
std::vector<DeckGroupDesc> trig;
trig.push_back({0, 78, {100, 44}, {20, 21, 22, -1, -1}, {}});
trig.push_back({0, 78, {}, {100, 44}, {20, 21, 22, -1, -1}, {}});
const DeckLayout tl = layoutDeck(trig, 0, 0, 824);
const DeckCellLayout& blank = tl.groups[0].cells[4];
CHECK(blank.id == -1);
@@ -162,6 +162,49 @@ static void testHitTest() {
CHECK(h.kind == DeckHitKind::None);
}
// The corner radio widens the caption row, takes the far corner, and pushes the caption
// toggle left of itself — the three properties the overlay-select switch relies on.
static void testCaptionRadioGeometryAndHit() {
const DeckGroupDesc bare{7, 78, {}, {200, 44}, {1, 2}, {}};
const DeckGroupDesc withRadio{7, 78, {201}, {200, 44}, {1, 2}, {}};
// Caption row grows by exactly gap + radio; the knob row is unchanged, so a group whose
// caption row already dominated grows by that much.
CHECK(deckGroupWidth(withRadio) - deckGroupWidth(bare) ==
kDeckToggleGap + kDeckRadioSize);
std::vector<DeckGroupDesc> g{withRadio};
const DeckLayout dl = layoutDeck(g, 0, 0, 800);
const DeckGroupLayout& lay = dl.groups[0];
CHECK(lay.captionRadio.id == 201);
CHECK(lay.captionRadio.box.width == kDeckRadioSize);
// Far corner: flush with the group's inner right edge.
CHECK(lay.captionRadio.box.right() == lay.box.right() - kDeckGroupPadX);
// The toggle sits entirely left of the radio, and the caption text left of the toggle.
CHECK(lay.captionToggle.seg1.right() <= lay.captionRadio.box.x);
CHECK(lay.caption.right() <= lay.captionToggle.seg0.x);
const DeckHit h = hitTestDeck(dl, lay.captionRadio.box.x + 2, lay.captionRadio.box.y + 2);
CHECK(h.kind == DeckHitKind::CaptionRadio && h.id == 201);
}
// The inner dial is a concentric sub-region of the knob: a grab there still names the cell,
// with `inner` set, so a cell with no inner value simply ignores the flag.
static void testInnerDialHit() {
const std::vector<DeckGroupDesc> g = shellLikeDeck();
const DeckLayout dl = layoutDeck(g, 0, 0, 900);
const DeckCellLayout& c = dl.groups[0].cells[0];
CHECK(c.inner.width == kDeckInnerDialSize && c.inner.height == kDeckInnerDialSize);
// Concentric with the knob square.
CHECK(c.inner.x + c.inner.width / 2 == c.knob.x + c.knob.width / 2);
CHECK(c.inner.y + c.inner.height / 2 == c.knob.y + c.knob.height / 2);
DeckHit h = hitTestDeck(dl, c.inner.x + c.inner.width / 2, c.inner.y + c.inner.height / 2);
CHECK(h.kind == DeckHitKind::Knob && h.id == c.id && h.inner);
// A grab on the outer ring is the same cell WITHOUT the inner flag.
h = hitTestDeck(dl, c.knob.x + 1, c.knob.y + 1);
CHECK(h.kind == DeckHitKind::Knob && h.id == c.id && !h.inner);
}
static void testEmptyDeck() {
const std::vector<DeckGroupDesc> none;
CHECK(deckRowCount(none, 800) == 0);
@@ -176,6 +219,8 @@ int main() {
testFirstGroupAlwaysPlaces();
testGroupInnerGeometry();
testHitTest();
testCaptionRadioGeometryAndHit();
testInnerDialHit();
testEmptyDeck();
if (g_fail) {
std::printf("%d FAILURE(S)\n", g_fail);