Files
daniel de5654fb6f Service both VST3 parameter channels, and promote pitch key-track and Trigger length so all 44 ids issue
The SDK's own single-component sample drains inputParameterChanges in
process() and implements setParamNormalized; automation was reading the
GUI channel alone. The audio thread now patches a block it solely owns.
2026-08-02 16:22:17 -04:00

195 lines
8.3 KiB
C++

// 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 <cstdio>
#include <set>
#include <string>
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<ParamId> 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<int>(DeckParam::kCount); ++i) {
const auto deck = static_cast<DeckParam>(i);
if (deckParamCommit(deck) == LiveCommit::Reload) continue;
CHECK(paramIdFor(deck) != 0);
}
}
static void testEveryNumberedRowIsIssued() {
// Key-track (pitch) and Trigger length were reserved-but-unissued while they routed through
// the reload tier; both are note-on-latched now, so both are issued under the numbers that
// were held for them and no other id moved — which is what the block-and-step scheme bought.
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);
// The whole table is issued today — 44 of 44.
CHECK(exposedParams().size() == paramTable().size());
CHECK(exposedParams().size() == 44);
}
static void testEveryRowCarriesADistinctTitleAndShortTitle() {
std::set<std::string> titles;
std::set<std::string> 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();
testEveryNumberedRowIsIssued();
testEveryRowCarriesADistinctTitleAndShortTitle();
if (g_fail == 0) std::printf("param_id: all tests passed\n");
return g_fail == 0 ? 0 : 1;
}