PITCH/RATE deck: Rate and Pitch knobs compounded into one read increment, on a three-state commit predicate and payload v16
This commit is contained in:
+68
-24
@@ -412,10 +412,12 @@ static void testBipolarKnobLawRoundTripsAndIsExactAtCentre() {
|
||||
CHECK(deckNormFromBipolar(3.0) == 1.0);
|
||||
}
|
||||
|
||||
static void testEveryDeckControlIsClassifiedLiveOrReloading() {
|
||||
// The live set: the seven filter tone/modulation knobs, plus every stage time, stage level,
|
||||
// hold fraction and curve exponent on all three envelopes — in BOTH mode shapes.
|
||||
static void testEveryDeckControlIsClassifiedIntoOneOfTheThreeCommitTiers() {
|
||||
// The live set: the seven filter tone/modulation knobs, the baseline pitch offset, plus
|
||||
// every stage time, stage level, hold fraction and curve exponent on all three envelopes —
|
||||
// in BOTH mode shapes.
|
||||
const DeckParam live[] = {
|
||||
DeckParam::kPitch,
|
||||
DeckParam::kFilterMorph, DeckParam::kFilterCutoff, DeckParam::kFilterQ,
|
||||
DeckParam::kFilterDrive, DeckParam::kFilterModAmt, DeckParam::kFilterVel,
|
||||
DeckParam::kFilterKeyTrack,
|
||||
@@ -434,7 +436,14 @@ static void testEveryDeckControlIsClassifiedLiveOrReloading() {
|
||||
DeckParam::kFilterEnvReleaseCurve,
|
||||
DeckParam::kFilterTrigAttackCurve, DeckParam::kFilterTrigDecayCurve,
|
||||
};
|
||||
for (DeckParam p : live) CHECK(isLiveDeckParam(p));
|
||||
for (DeckParam p : live) CHECK(deckParamCommit(p) == LiveCommit::Live);
|
||||
|
||||
// The note-on-latched tier: published like a live control, read only at note-on. Asserted as
|
||||
// its OWN state rather than as "not Reload" — the whole point of widening the predicate is
|
||||
// that Rate must not fall back into either neighbour, and Γ-W4-T1 reads this classification
|
||||
// to decide what it exposes to the host.
|
||||
const DeckParam latched[] = {DeckParam::kRate};
|
||||
for (DeckParam p : latched) CHECK(deckParamCommit(p) == LiveCommit::NoteOnLatched);
|
||||
|
||||
// Everything else reloads or rebuilds; deck_groups.h is the home for why each exclusion
|
||||
// is excluded.
|
||||
@@ -448,15 +457,16 @@ static void testEveryDeckControlIsClassifiedLiveOrReloading() {
|
||||
DeckParam::kVoiceCount, DeckParam::kVoiceMode,
|
||||
DeckParam::kMonoTrigger, DeckParam::kMasterGain,
|
||||
};
|
||||
for (DeckParam p : reloads) CHECK(!isLiveDeckParam(p));
|
||||
for (DeckParam p : reloads) CHECK(deckParamCommit(p) == LiveCommit::Reload);
|
||||
|
||||
// COVERAGE, not cardinality: every id appears in EXACTLY ONE of the two lists. A sum check
|
||||
// COVERAGE, not cardinality: every id appears in EXACTLY ONE of the three lists. A sum check
|
||||
// would stay green if an edit duplicated one id and dropped another, leaving that one
|
||||
// unclassified.
|
||||
for (int i = 0; i < static_cast<int>(DeckParam::kCount); ++i) {
|
||||
const DeckParam p = static_cast<DeckParam>(i);
|
||||
int seen = 0;
|
||||
for (DeckParam q : live) if (q == p) ++seen;
|
||||
for (DeckParam q : latched) if (q == p) ++seen;
|
||||
for (DeckParam q : reloads) if (q == p) ++seen;
|
||||
if (seen != 1) std::printf(" (deck id %d classified %d times)\n", i, seen);
|
||||
CHECK(seen == 1);
|
||||
@@ -464,26 +474,34 @@ static void testEveryDeckControlIsClassifiedLiveOrReloading() {
|
||||
}
|
||||
|
||||
static void testOnlyALiveControlsDragTakesTheLiveTier() {
|
||||
// isLiveDeckParam alone is not what a user experiences — liveCommitFor is, at the editor's
|
||||
// deckParamCommit alone is not what a user experiences — liveCommitFor is, at the editor's
|
||||
// commit site. Inverting it has to FAIL a test rather than merely read wrong.
|
||||
CHECK(liveCommitFor(LiveDragKind::kDeckKnob, static_cast<int>(DeckParam::kFilterCutoff)));
|
||||
CHECK(liveCommitFor(LiveDragKind::kDeckKnob, static_cast<int>(DeckParam::kAttack)));
|
||||
const auto knob = [](DeckParam p) {
|
||||
return liveCommitFor(LiveDragKind::kDeckKnob, static_cast<int>(p));
|
||||
};
|
||||
CHECK(knob(DeckParam::kFilterCutoff) == LiveCommit::Live);
|
||||
CHECK(knob(DeckParam::kAttack) == LiveCommit::Live);
|
||||
CHECK(knob(DeckParam::kPitch) == LiveCommit::Live);
|
||||
// The Trigger amp is live now that the fade pair folded into the AHD — the one behavioural
|
||||
// consequence of that consolidation.
|
||||
CHECK(liveCommitFor(LiveDragKind::kDeckKnob, static_cast<int>(DeckParam::kTrigAttack)));
|
||||
CHECK(liveCommitFor(LiveDragKind::kDeckKnob, static_cast<int>(DeckParam::kTrigDecayCurve)));
|
||||
CHECK(!liveCommitFor(LiveDragKind::kDeckKnob, static_cast<int>(DeckParam::kTrigLength)));
|
||||
CHECK(!liveCommitFor(LiveDragKind::kDeckKnob, static_cast<int>(DeckParam::kMasterGain)));
|
||||
CHECK(!liveCommitFor(LiveDragKind::kDeckKnob, static_cast<int>(DeckParam::kAmpEnvSelect)));
|
||||
CHECK(knob(DeckParam::kTrigAttack) == LiveCommit::Live);
|
||||
CHECK(knob(DeckParam::kTrigDecayCurve) == LiveCommit::Live);
|
||||
// Rate keeps its own tier through the drag site: it must not arrive as Live (which would let
|
||||
// it move a sounding note) nor as Reload (which would re-decode the WAV under a swept knob).
|
||||
CHECK(knob(DeckParam::kRate) == LiveCommit::NoteOnLatched);
|
||||
CHECK(knob(DeckParam::kTrigLength) == LiveCommit::Reload);
|
||||
CHECK(knob(DeckParam::kMasterGain) == LiveCommit::Reload);
|
||||
CHECK(knob(DeckParam::kAmpEnvSelect) == LiveCommit::Reload);
|
||||
// The shell's processor-side sentinels (preview velocity is -2) and any out-of-range id
|
||||
// are not parameter-set controls, so they must never reach the enum.
|
||||
CHECK(!liveCommitFor(LiveDragKind::kDeckKnob, -2));
|
||||
CHECK(!liveCommitFor(LiveDragKind::kDeckKnob, -1));
|
||||
CHECK(!liveCommitFor(LiveDragKind::kDeckKnob, static_cast<int>(DeckParam::kCount)));
|
||||
CHECK(liveCommitFor(LiveDragKind::kDeckKnob, -2) == LiveCommit::Reload);
|
||||
CHECK(liveCommitFor(LiveDragKind::kDeckKnob, -1) == LiveCommit::Reload);
|
||||
CHECK(knob(DeckParam::kCount) == LiveCommit::Reload);
|
||||
// Every stage value an envelope node can reach is live, in either mode shape.
|
||||
CHECK(liveCommitFor(LiveDragKind::kEnvNode, -1));
|
||||
CHECK(liveCommitFor(LiveDragKind::kEnvNode, -1) == LiveCommit::Live);
|
||||
// Every other drag (markers, scrollbar, curve nodes) commits through a reload.
|
||||
CHECK(!liveCommitFor(LiveDragKind::kOther, static_cast<int>(DeckParam::kFilterCutoff)));
|
||||
CHECK(liveCommitFor(LiveDragKind::kOther, static_cast<int>(DeckParam::kFilterCutoff)) ==
|
||||
LiveCommit::Reload);
|
||||
}
|
||||
|
||||
// --- The overlay selection state machine ---------------------------------------
|
||||
@@ -582,9 +600,9 @@ static void testDeckKnobIsInertExactlyWithItsGroupsEnableToggle() {
|
||||
// that scale either shape stay live. (Which segment knobs, per envelope, is pinned in
|
||||
// spline_egs_tests alongside the rest of the spline rules.)
|
||||
static void testAModeToggleIsNeitherLiveNorAnOverlayRadio() {
|
||||
CHECK(!isLiveDeckParam(DeckParam::kAmpEnvMode));
|
||||
CHECK(!isLiveDeckParam(DeckParam::kPitchEnvMode));
|
||||
CHECK(!isLiveDeckParam(DeckParam::kFilterEnvMode));
|
||||
CHECK(deckParamCommit(DeckParam::kAmpEnvMode) == LiveCommit::Reload);
|
||||
CHECK(deckParamCommit(DeckParam::kPitchEnvMode) == LiveCommit::Reload);
|
||||
CHECK(deckParamCommit(DeckParam::kFilterEnvMode) == LiveCommit::Reload);
|
||||
CHECK(overlayEnvForModeToggle(radio(DeckParam::kAmpEnvMode)) == OverlayEnv::kAmp);
|
||||
CHECK(overlayEnvForModeToggle(radio(DeckParam::kPitchEnvMode)) == OverlayEnv::kPitch);
|
||||
CHECK(overlayEnvForModeToggle(radio(DeckParam::kFilterEnvMode)) == OverlayEnv::kFilter);
|
||||
@@ -644,6 +662,31 @@ static void testNoFaceLeavesSlackWhereItsDroppedControlsWere() {
|
||||
// The "residue lands in symmetric end margins" rule is knob_deck's own (layoutGroup), pinned
|
||||
// once by its synthetic residue>=2 fixture in test_knob_deck.cpp rather than restated here.
|
||||
|
||||
// PITCH/RATE carries three cells and measures exactly 192 — the KNOB row (3 x kDeckCellW plus
|
||||
// padding) is what it measures from, and the caption row must stay under that. The ceiling is
|
||||
// asserted by construction rather than as a comment: at a caption reserve of 80 the group is
|
||||
// still 192, and at 81 it is not, which is the whole content of "hard ceiling 80". Widening the
|
||||
// group is not the remedy if the caption text ever outgrows it — narrowing the mode toggle is.
|
||||
static void testThePitchRateGroupIsKnobRowDrivenAtExactlyOneNinetyTwo() {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||
const DeckGroupDesc* pitch = nullptr;
|
||||
for (const DeckGroupDesc& d : g) if (d.id == kGroupPitch) pitch = &d;
|
||||
CHECK(pitch != nullptr);
|
||||
if (!pitch) return;
|
||||
CHECK(pitch->cellIds.size() == 3);
|
||||
CHECK(pitch->cellIds[0] == static_cast<int>(DeckParam::kKeyTrack));
|
||||
CHECK(pitch->cellIds[1] == static_cast<int>(DeckParam::kRate));
|
||||
CHECK(pitch->cellIds[2] == static_cast<int>(DeckParam::kPitch));
|
||||
CHECK(deckGroupWidth(*pitch) == 192);
|
||||
CHECK(3 * kDeckCellW + 2 * kDeckGroupPadX == 192); // the knob row IS the measurement
|
||||
|
||||
DeckGroupDesc probe = *pitch;
|
||||
probe.captionWidth = 80;
|
||||
CHECK(deckGroupWidth(probe) == 192); // at the ceiling the caption row still fits under it
|
||||
probe.captionWidth = 81;
|
||||
CHECK(deckGroupWidth(probe) > 192); // one past it, the caption row takes over
|
||||
}
|
||||
|
||||
// Gate is the common face and its group widths are what the width budget is spent against:
|
||||
// pin them at the floor so a later edit anywhere in the deck cannot move one silently.
|
||||
// (Measured from the shipped descriptors, not copied out of a failing run.) The WRAP row a
|
||||
@@ -652,7 +695,7 @@ static void testNoFaceLeavesSlackWhereItsDroppedControlsWere() {
|
||||
static void testGateModeGroupWidthsAreUnchanged() {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
|
||||
const struct { int id; int width; } want[] = {
|
||||
{kGroupPitch, 150}, {kGroupPitchEnv, 252}, {kGroupFilter, 524},
|
||||
{kGroupPitch, 192}, {kGroupPitchEnv, 252}, {kGroupFilter, 524},
|
||||
{kGroupFilterEnv, 312}, {kGroupAmpEnv, 312}, {kGroupVelocity, 192},
|
||||
{kGroupVoice, 164}, {kGroupMaster, 72},
|
||||
};
|
||||
@@ -736,7 +779,7 @@ int main() {
|
||||
testDeckKnobIsInertExactlyWithItsGroupsEnableToggle();
|
||||
testAModeToggleIsNeitherLiveNorAnOverlayRadio();
|
||||
testTheModeTogglesCostNoGroupWidth();
|
||||
testEveryDeckControlIsClassifiedLiveOrReloading();
|
||||
testEveryDeckControlIsClassifiedIntoOneOfTheThreeCommitTiers();
|
||||
testOnlyALiveControlsDragTakesTheLiveTier();
|
||||
testDeckReadsPitchThenFilterThenAmpLeftToRight();
|
||||
testVelocityGroupOwnsTheThreeCurvesExclusively();
|
||||
@@ -750,6 +793,7 @@ int main() {
|
||||
testWrappedDeckHeightAtTheEditorFloorWidth();
|
||||
testDeckFitsInsideTheEnforcedMinimumWindow();
|
||||
testNoFaceLeavesSlackWhereItsDroppedControlsWere();
|
||||
testThePitchRateGroupIsKnobRowDrivenAtExactlyOneNinetyTwo();
|
||||
testGateModeGroupWidthsAreUnchanged();
|
||||
testGateSplineGateRoundTripsToTheSameLayout();
|
||||
testTheEditorFloorIsDerivedFromTheDeckWidthBudget();
|
||||
|
||||
Reference in New Issue
Block a user