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:
+117
-25
@@ -78,6 +78,89 @@ static void testFilterGroupCarriesItsFiveToneControlsPlusModulation() {
|
||||
CHECK(fe.rowToggle.id == -1);
|
||||
}
|
||||
|
||||
// Exactly the three envelope decks carry an overlay-select radio, each its own, and no other
|
||||
// group has one — the exclusivity the shell enforces is only meaningful if the id space is.
|
||||
static void testOnlyTheThreeEnvelopeDecksCarryARadio() {
|
||||
for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) {
|
||||
const std::vector<DeckGroupDesc> g = sampleDeckGroups(mode);
|
||||
int radios = 0;
|
||||
for (const DeckGroupDesc& d : g) {
|
||||
if (d.captionRadio.id < 0) continue;
|
||||
++radios;
|
||||
const int want = d.id == kGroupAmpEnv ? cell(DeckParam::kAmpEnvSelect)
|
||||
: d.id == kGroupPitchEnv ? cell(DeckParam::kPitchEnvSelect)
|
||||
: d.id == kGroupFilterEnv ? cell(DeckParam::kFilterEnvSelect)
|
||||
: -1;
|
||||
CHECK(d.captionRadio.id == want);
|
||||
}
|
||||
CHECK(radios == 3);
|
||||
}
|
||||
}
|
||||
|
||||
// The mode-driven shape switch, on BOTH the amp and the filter envelope: Gate shows the
|
||||
// AHDSR's five stages, Trigger the AHD's three (behind the play span on the amp deck), and
|
||||
// neither mode leaks the other's controls onto the deck.
|
||||
static void testGateAndTriggerFacesCarryTheirOwnShapes() {
|
||||
const std::vector<DeckGroupDesc> gate = sampleDeckGroups(PlayMode::Gate);
|
||||
const std::vector<DeckGroupDesc> trig = sampleDeckGroups(PlayMode::Trigger);
|
||||
const DeckGroupDesc& gAmp = gate[static_cast<std::size_t>(indexOfGroup(gate, kGroupAmpEnv))];
|
||||
const DeckGroupDesc& tAmp = trig[static_cast<std::size_t>(indexOfGroup(trig, kGroupAmpEnv))];
|
||||
const std::vector<int> gateAmp = {cell(DeckParam::kAttack), cell(DeckParam::kHold),
|
||||
cell(DeckParam::kDecay), cell(DeckParam::kSustain),
|
||||
cell(DeckParam::kRelease)};
|
||||
const std::vector<int> trigAmp = {cell(DeckParam::kTrigLength), cell(DeckParam::kTrigAttack),
|
||||
cell(DeckParam::kTrigHold), cell(DeckParam::kTrigDecay),
|
||||
-1};
|
||||
CHECK(gAmp.cellIds == gateAmp);
|
||||
CHECK(tAmp.cellIds == trigAmp);
|
||||
|
||||
const DeckGroupDesc& gFe = gate[static_cast<std::size_t>(indexOfGroup(gate, kGroupFilterEnv))];
|
||||
const DeckGroupDesc& tFe = trig[static_cast<std::size_t>(indexOfGroup(trig, kGroupFilterEnv))];
|
||||
const std::vector<int> trigFe = {cell(DeckParam::kFilterTrigAttack),
|
||||
cell(DeckParam::kFilterTrigHold),
|
||||
cell(DeckParam::kFilterTrigDecay), -1, -1};
|
||||
CHECK(tFe.cellIds == trigFe);
|
||||
CHECK(gFe.cellIds != tFe.cellIds);
|
||||
// Same cell count either way, so the group's width — and its neighbours' placement —
|
||||
// survives a mode flip.
|
||||
CHECK(gFe.cellIds.size() == tFe.cellIds.size());
|
||||
CHECK(deckGroupWidth(gFe) == deckGroupWidth(tFe));
|
||||
}
|
||||
|
||||
// Every SLOPED stage knob carries an inner curve dial; Hold, Sustain, and everything that is
|
||||
// not a stage carries none. This is the "which segments are sloped" rule, asserted rather than
|
||||
// read.
|
||||
static void testOnlySlopedStageKnobsCarryAnInnerCurveDial() {
|
||||
const DeckParam sloped[] = {
|
||||
DeckParam::kAttack, DeckParam::kDecay, DeckParam::kRelease,
|
||||
DeckParam::kTrigAttack, DeckParam::kTrigDecay,
|
||||
DeckParam::kPitchEnvAttack, DeckParam::kPitchEnvDecay,
|
||||
DeckParam::kFilterEnvAttack, DeckParam::kFilterEnvDecay, DeckParam::kFilterEnvRelease,
|
||||
DeckParam::kFilterTrigAttack, DeckParam::kFilterTrigDecay,
|
||||
};
|
||||
for (DeckParam p : sloped) {
|
||||
const DeckParam c = curveParamFor(p);
|
||||
CHECK(c != DeckParam::kCount);
|
||||
// A curve control is itself flat — no inner dial on an inner dial.
|
||||
CHECK(curveParamFor(c) == DeckParam::kCount);
|
||||
}
|
||||
const DeckParam flat[] = {
|
||||
DeckParam::kHold, DeckParam::kSustain, DeckParam::kTrigHold,
|
||||
DeckParam::kPitchEnvHold, DeckParam::kFilterEnvHold, DeckParam::kFilterEnvSustain,
|
||||
DeckParam::kFilterTrigHold, DeckParam::kTrigLength, DeckParam::kPitchEnvDepth,
|
||||
DeckParam::kFilterCutoff, DeckParam::kMasterGain, DeckParam::kKeyTrack,
|
||||
};
|
||||
for (DeckParam p : flat) CHECK(curveParamFor(p) == DeckParam::kCount);
|
||||
|
||||
// Every sloped knob maps to a DISTINCT curve control — a copy-paste that pointed two
|
||||
// stages at one exponent would tie two dials together silently.
|
||||
for (std::size_t i = 0; i < sizeof(sloped) / sizeof(sloped[0]); ++i) {
|
||||
for (std::size_t j = i + 1; j < sizeof(sloped) / sizeof(sloped[0]); ++j) {
|
||||
CHECK(curveParamFor(sloped[i]) != curveParamFor(sloped[j]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void testAmpGroupWidthSurvivesAGateTriggerFlip() {
|
||||
// The reserved blanks are what stop a mode flip reflowing the groups beside AMP.
|
||||
const std::vector<DeckGroupDesc> gate = sampleDeckGroups(PlayMode::Gate);
|
||||
@@ -86,7 +169,7 @@ static void testAmpGroupWidthSurvivesAGateTriggerFlip() {
|
||||
const DeckGroupDesc& b = trig[static_cast<std::size_t>(indexOfGroup(trig, kGroupAmpEnv))];
|
||||
CHECK(deckGroupWidth(a) == deckGroupWidth(b));
|
||||
CHECK(a.cellIds.size() == b.cellIds.size());
|
||||
CHECK(b.cellIds[3] == -1 && b.cellIds[4] == -1);
|
||||
CHECK(b.cellIds[4] == -1); // the Trigger face's one reserved blank
|
||||
// Every other group is mode-independent, so the whole deck's height is too.
|
||||
CHECK(deckHeight(gate, kAvailAtMinWidth) == deckHeight(trig, kAvailAtMinWidth));
|
||||
}
|
||||
@@ -188,16 +271,25 @@ static void testBipolarKnobLawRoundTripsAndIsExactAtCentre() {
|
||||
}
|
||||
|
||||
static void testEveryDeckControlIsClassifiedLiveOrReloading() {
|
||||
// The live set: the six filter tone/modulation knobs, plus every stage time and stage
|
||||
// level on all three envelopes.
|
||||
// The live set: the six filter tone/modulation knobs, plus every stage time, stage level,
|
||||
// hold fraction and curve exponent on all three envelopes — in BOTH mode shapes.
|
||||
const DeckParam live[] = {
|
||||
DeckParam::kFilterMorph, DeckParam::kFilterCutoff, DeckParam::kFilterQ,
|
||||
DeckParam::kFilterDrive, DeckParam::kFilterModAmt, DeckParam::kFilterKeyTrack,
|
||||
DeckParam::kAttack, DeckParam::kHold, DeckParam::kDecay, DeckParam::kSustain,
|
||||
DeckParam::kRelease,
|
||||
DeckParam::kTrigAttack, DeckParam::kTrigHold, DeckParam::kTrigDecay,
|
||||
DeckParam::kFilterEnvAttack, DeckParam::kFilterEnvHold, DeckParam::kFilterEnvDecay,
|
||||
DeckParam::kFilterEnvSustain, DeckParam::kFilterEnvRelease,
|
||||
DeckParam::kPitchEnvAttack, DeckParam::kPitchEnvDecay, DeckParam::kPitchEnvDepth,
|
||||
DeckParam::kFilterTrigAttack, DeckParam::kFilterTrigHold, DeckParam::kFilterTrigDecay,
|
||||
DeckParam::kPitchEnvAttack, DeckParam::kPitchEnvHold, DeckParam::kPitchEnvDecay,
|
||||
DeckParam::kPitchEnvDepth,
|
||||
DeckParam::kAttackCurve, DeckParam::kDecayCurve, DeckParam::kReleaseCurve,
|
||||
DeckParam::kTrigAttackCurve, DeckParam::kTrigDecayCurve,
|
||||
DeckParam::kPitchEnvAttackCurve, DeckParam::kPitchEnvDecayCurve,
|
||||
DeckParam::kFilterEnvAttackCurve, DeckParam::kFilterEnvDecayCurve,
|
||||
DeckParam::kFilterEnvReleaseCurve,
|
||||
DeckParam::kFilterTrigAttackCurve, DeckParam::kFilterTrigDecayCurve,
|
||||
};
|
||||
for (DeckParam p : live) CHECK(isLiveDeckParam(p));
|
||||
|
||||
@@ -206,8 +298,9 @@ static void testEveryDeckControlIsClassifiedLiveOrReloading() {
|
||||
const DeckParam reloads[] = {
|
||||
DeckParam::kPlayMode, DeckParam::kPitchEngine, DeckParam::kPitchEnvEnable,
|
||||
DeckParam::kFilterEnable, DeckParam::kFilterLaw, DeckParam::kFilterVel,
|
||||
DeckParam::kKeyTrack, DeckParam::kTrigLength, DeckParam::kTrigFadeIn,
|
||||
DeckParam::kTrigFadeOut, DeckParam::kVoiceCount, DeckParam::kVoiceMode,
|
||||
DeckParam::kKeyTrack, DeckParam::kTrigLength,
|
||||
DeckParam::kAmpEnvSelect, DeckParam::kPitchEnvSelect, DeckParam::kFilterEnvSelect,
|
||||
DeckParam::kVoiceCount, DeckParam::kVoiceMode,
|
||||
DeckParam::kMonoTrigger, DeckParam::kMasterGain,
|
||||
};
|
||||
for (DeckParam p : reloads) CHECK(!isLiveDeckParam(p));
|
||||
@@ -228,28 +321,24 @@ static void testEveryDeckControlIsClassifiedLiveOrReloading() {
|
||||
static void testOnlyALiveControlsDragTakesTheLiveTier() {
|
||||
// isLiveDeckParam 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),
|
||||
PlayMode::Gate));
|
||||
// A knob's routing is the knob's, not the play mode's.
|
||||
CHECK(liveCommitFor(LiveDragKind::kDeckKnob, static_cast<int>(DeckParam::kAttack),
|
||||
PlayMode::Trigger));
|
||||
CHECK(!liveCommitFor(LiveDragKind::kDeckKnob, static_cast<int>(DeckParam::kTrigFadeIn),
|
||||
PlayMode::Trigger));
|
||||
CHECK(!liveCommitFor(LiveDragKind::kDeckKnob, static_cast<int>(DeckParam::kMasterGain),
|
||||
PlayMode::Gate));
|
||||
CHECK(liveCommitFor(LiveDragKind::kDeckKnob, static_cast<int>(DeckParam::kFilterCutoff)));
|
||||
CHECK(liveCommitFor(LiveDragKind::kDeckKnob, static_cast<int>(DeckParam::kAttack)));
|
||||
// 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)));
|
||||
// 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, PlayMode::Gate));
|
||||
CHECK(!liveCommitFor(LiveDragKind::kDeckKnob, -1, PlayMode::Gate));
|
||||
CHECK(!liveCommitFor(LiveDragKind::kDeckKnob, static_cast<int>(DeckParam::kCount),
|
||||
PlayMode::Gate));
|
||||
// An envelope-node drag edits the AHDSR in Gate; the same drag in Trigger rewrites the
|
||||
// play span, which is not a live control.
|
||||
CHECK(liveCommitFor(LiveDragKind::kEnvNode, -1, PlayMode::Gate));
|
||||
CHECK(!liveCommitFor(LiveDragKind::kEnvNode, -1, PlayMode::Trigger));
|
||||
CHECK(!liveCommitFor(LiveDragKind::kDeckKnob, -2));
|
||||
CHECK(!liveCommitFor(LiveDragKind::kDeckKnob, -1));
|
||||
CHECK(!liveCommitFor(LiveDragKind::kDeckKnob, static_cast<int>(DeckParam::kCount)));
|
||||
// Every stage value an envelope node can reach is live, in either mode shape.
|
||||
CHECK(liveCommitFor(LiveDragKind::kEnvNode, -1));
|
||||
// Every other drag (markers, scrollbar, curve nodes) commits through a reload.
|
||||
CHECK(!liveCommitFor(LiveDragKind::kOther, static_cast<int>(DeckParam::kFilterCutoff),
|
||||
PlayMode::Gate));
|
||||
CHECK(!liveCommitFor(LiveDragKind::kOther, static_cast<int>(DeckParam::kFilterCutoff)));
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -257,6 +346,9 @@ int main() {
|
||||
testOnlyALiveControlsDragTakesTheLiveTier();
|
||||
testDeckReadsPitchThenFilterThenAmpLeftToRight();
|
||||
testFilterGroupCarriesItsFiveToneControlsPlusModulation();
|
||||
testOnlyTheThreeEnvelopeDecksCarryARadio();
|
||||
testGateAndTriggerFacesCarryTheirOwnShapes();
|
||||
testOnlySlopedStageKnobsCarryAnInnerCurveDial();
|
||||
testAmpGroupWidthSurvivesAGateTriggerFlip();
|
||||
testWrappedDeckHeightAtTheEditorFloorWidth();
|
||||
testDeckFitsInsideTheEnforcedMinimumWindow();
|
||||
|
||||
Reference in New Issue
Block a user