// Standalone tests for the FOREVER-FROZEN VST3 parameter id table and the exposed set derived // from the commit predicate — no VST3, no REAPER, no framework. // // The asserted numbers are LITERALS on purpose. A test that recomputed them from cellIds, from // the enum's position, or from the table itself would defeat the freeze it exists to hold: the // point is that changing any id has to break this file. #include "../src/core/instrument/param/param_id.h" #include #include #include using namespace reasampler; using namespace reasampler::instrument::param; using reasampler::instrument::ui::DeckParam; using reasampler::instrument::ui::LiveCommit; using reasampler::instrument::ui::deckParamCommit; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) static void testEveryIdHoldsTheNumberItShippedWith() { // docs/product/parameter-automation.md 6.2, transcribed. If this list and the table // disagree, the table moved and every automation lane recorded against it now means // something else. struct Expect { ParamId id; DeckParam deck; }; const Expect kExpected[] = { {1000, DeckParam::kKeyTrack}, {1010, DeckParam::kRate}, {1020, DeckParam::kPitch}, {1100, DeckParam::kPitchEnvAttack}, {1101, DeckParam::kPitchEnvAttackCurve}, {1110, DeckParam::kPitchEnvHold}, {1120, DeckParam::kPitchEnvDecay}, {1121, DeckParam::kPitchEnvDecayCurve}, {1130, DeckParam::kPitchEnvDepth}, {1200, DeckParam::kFilterMorph}, {1210, DeckParam::kFilterCutoff}, {1220, DeckParam::kFilterQ}, {1230, DeckParam::kFilterDrive}, {1240, DeckParam::kFilterModAmt}, {1250, DeckParam::kFilterVel}, {1260, DeckParam::kFilterKeyTrack}, {1300, DeckParam::kFilterEnvAttack}, {1301, DeckParam::kFilterEnvAttackCurve}, {1310, DeckParam::kFilterEnvHold}, {1320, DeckParam::kFilterEnvDecay}, {1321, DeckParam::kFilterEnvDecayCurve}, {1330, DeckParam::kFilterEnvSustain}, {1340, DeckParam::kFilterEnvRelease}, {1341, DeckParam::kFilterEnvReleaseCurve}, {1350, DeckParam::kFilterTrigAttack}, {1351, DeckParam::kFilterTrigAttackCurve}, {1360, DeckParam::kFilterTrigHold}, {1370, DeckParam::kFilterTrigDecay}, {1371, DeckParam::kFilterTrigDecayCurve}, {1400, DeckParam::kAttack}, {1401, DeckParam::kAttackCurve}, {1410, DeckParam::kHold}, {1420, DeckParam::kDecay}, {1421, DeckParam::kDecayCurve}, {1430, DeckParam::kSustain}, {1440, DeckParam::kRelease}, {1441, DeckParam::kReleaseCurve}, {1450, DeckParam::kTrigLength}, {1460, DeckParam::kTrigAttack}, {1461, DeckParam::kTrigAttackCurve}, {1470, DeckParam::kTrigHold}, {1480, DeckParam::kTrigDecay}, {1481, DeckParam::kTrigDecayCurve}, {1700, DeckParam::kMasterGain}, }; const std::size_t expectedCount = sizeof(kExpected) / sizeof(kExpected[0]); CHECK(expectedCount == 44); CHECK(paramTable().size() == expectedCount); if (paramTable().size() != expectedCount) return; for (std::size_t i = 0; i < expectedCount; ++i) { CHECK(paramTable()[i].id == kExpected[i].id); CHECK(paramTable()[i].deck == kExpected[i].deck); CHECK(paramIdFor(kExpected[i].deck) == kExpected[i].id); } } static void testIdsAreUniqueAscendingInBlockAndOnStep() { std::set seen; ParamId previous = 0; for (const ParamRow& row : paramTable()) { CHECK(seen.insert(row.id).second); // unique CHECK(row.id > previous); // ascending == presentation order previous = row.id; CHECK(row.id >= 1000); // 0 is a plausible accident, never an id const ParamId withinBlock = row.id % 100; // On the step, or one past it — a curve dial takes its outer knob's id + 1, and only a // curve dial may. const bool onStep = withinBlock % 10 == 0; const bool innerDial = withinBlock % 10 == 1; CHECK(onStep || innerDial); } } static void testAnInnerDialSitsBesideTheKnobItShapes() { for (const ParamRow& row : paramTable()) { if (row.id % 10 != 1) continue; // Its outer knob is the row numbered one below it, and curveParamFor must agree that the // dial belongs to that knob. const ParamRow* outer = nullptr; for (const ParamRow& candidate : paramTable()) { if (candidate.id == row.id - 1) outer = &candidate; } CHECK(outer != nullptr); if (outer) CHECK(reasampler::instrument::ui::curveParamFor(outer->deck) == row.deck); } } static void testABlockCarriesOnlyItsOwnGroup() { for (const ParamRow& row : paramTable()) { const ParamId block = row.id / 100 * 100; switch (block) { case 1000: CHECK(row.unit == kUnitPitch); break; case 1100: CHECK(row.unit == kUnitPitchEnv); break; case 1200: CHECK(row.unit == kUnitFilter); break; case 1300: CHECK(row.unit == kUnitFilterEnv); break; case 1400: CHECK(row.unit == kUnitAmp); break; case 1700: CHECK(row.unit == kUnitMaster); break; default: CHECK(false); break; // 1500/1600 are reserved and must stay empty } } } static void testTheExposedSetIsExactlyThePredicateAnswer() { // Asserted against deckParamCommit, never against a literal count: the list follows the // predicate, and the predicate is never bent to fill the list. for (const ParamRow& row : paramTable()) { const bool live = deckParamCommit(row.deck) != LiveCommit::Reload; CHECK(isExposed(row.deck) == live); const bool listed = exposedRowFor(row.id) != nullptr; CHECK(listed == live); } for (const ParamRow& row : exposedParams()) { CHECK(deckParamCommit(row.deck) != LiveCommit::Reload); } // Every control the predicate calls live must HAVE a row — a live control with no number is // a parameter the host can never be told about. for (int i = 0; i < static_cast(DeckParam::kCount); ++i) { const auto deck = static_cast(i); if (deckParamCommit(deck) == LiveCommit::Reload) continue; CHECK(paramIdFor(deck) != 0); } } static void testAReservedRowIsNumberedButNotIssued() { // Key-track (pitch) and Trigger length are note-on-latch candidates that still route through // the reload tier, so they are NOT issued to the host today. Their numbers stay reserved // rather than retired: nothing shipped under them, so a later promotion issues the same id // and no other id moves. CHECK(paramIdFor(DeckParam::kKeyTrack) == 1000); CHECK(paramIdFor(DeckParam::kTrigLength) == 1450); CHECK(!isExposed(DeckParam::kKeyTrack)); CHECK(!isExposed(DeckParam::kTrigLength)); CHECK(exposedRowFor(1000) == nullptr); CHECK(exposedRowFor(1450) == nullptr); // Master gain IS issued: one atomic store the audio thread picks up next block is the live // tier by that tier's own definition. CHECK(isExposed(DeckParam::kMasterGain)); CHECK(exposedRowFor(1700) != nullptr); CHECK(exposedParams().size() == paramTable().size() - 2); } static void testEveryRowCarriesADistinctTitleAndShortTitle() { std::set titles; std::set shortTitles; for (const ParamRow& row : paramTable()) { CHECK(row.title && row.title[0] != '\0'); CHECK(row.shortTitle && row.shortTitle[0] != '\0'); CHECK(std::string(row.title) != std::string(row.shortTitle)); CHECK(titles.insert(row.title).second); CHECK(shortTitles.insert(row.shortTitle).second); } } int main() { testEveryIdHoldsTheNumberItShippedWith(); testIdsAreUniqueAscendingInBlockAndOnStep(); testAnInnerDialSitsBesideTheKnobItShapes(); testABlockCarriesOnlyItsOwnGroup(); testTheExposedSetIsExactlyThePredicateAnswer(); testAReservedRowIsNumberedButNotIssued(); testEveryRowCarriesADistinctTitleAndShortTitle(); if (g_fail == 0) std::printf("param_id: all tests passed\n"); return g_fail == 0 ? 0 : 1; }