// Standalone tests for the plain-value layer: unit strings, plain ranges, monotonicity, the // exact-preimage requirement at every default, and the read-side-only property of the filter's // four — no VST3, no REAPER, no framework. #include "../src/core/instrument/param/param_units.h" #include "../src/core/instrument/param/param_id.h" #include "../src/core/instrument/engine/filter/filter_params.h" #include "../src/core/instrument/engine/master_gain.h" #include "../src/core/instrument/map/play_seconds.h" #include "../src/core/instrument/map/sample_map.h" #include "../src/core/instrument/ui/deck_values.h" #include #include #include using namespace reasampler; using namespace reasampler::instrument::param; using reasampler::instrument::map::PlaySeconds; using reasampler::instrument::ui::DeckParam; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) #define CHECK_ID(cond, id) do { if(!(cond)) { \ std::printf("FAIL line %d (param %u): %s\n", __LINE__, (id), #cond); ++g_fail; } } while(0) static void testEveryUnitStringAndRangeMatchesTheSpecifiedTable() { struct Expect { ParamId id; const char* units; double min; double max; }; // docs/product/parameter-automation.md 6.7.1, per parameter rather than per category, so a // control silently reclassified into the wrong category fails here. const Expect kExpected[] = { {kParamKeyTrackPitch, "%", 0.0, 200.0}, {kParamRate, "%", 50.0, 200.0}, {kParamPitchOffset, "st", -24.0, 24.0}, {kParamPitchEnvAttack, "ms", 0.0, 10000.0}, {kParamPitchEnvAttackCurve, "", 0.1, 10.0}, {kParamPitchEnvHold, "%", 0.0, 100.0}, {kParamPitchEnvDecay, "ms", 0.0, 10000.0}, {kParamPitchEnvDecayCurve, "", 0.1, 10.0}, {kParamPitchEnvDepth, "st", -24.0, 24.0}, {kParamFilterMorph, "%", 0.0, 100.0}, {kParamFilterCutoff, "Hz", 20.0, 20000.0}, {kParamFilterQ, "", 0.1, 10.0}, {kParamFilterDrive, "", 0.0, 4.0}, {kParamFilterModAmount, "%", -100.0, 100.0}, {kParamFilterVelAmount, "%", -100.0, 100.0}, {kParamKeyTrackFilter, "%", 0.0, 200.0}, {kParamFilterEnvAttack, "ms", 0.0, 10000.0}, {kParamFilterEnvAttackCurve, "", 0.1, 10.0}, {kParamFilterEnvHold, "ms", 0.0, 10000.0}, {kParamFilterEnvDecay, "ms", 0.0, 10000.0}, {kParamFilterEnvDecayCurve, "", 0.1, 10.0}, {kParamFilterEnvSustain, "%", 0.0, 100.0}, {kParamFilterEnvRelease, "ms", 0.0, 10000.0}, {kParamFilterEnvReleaseCurve, "", 0.1, 10.0}, {kParamFilterTrigAttack, "ms", 0.0, 10000.0}, {kParamFilterTrigAttackCurve, "", 0.1, 10.0}, {kParamFilterTrigHold, "%", 0.0, 100.0}, {kParamFilterTrigDecay, "ms", 0.0, 10000.0}, {kParamFilterTrigDecayCurve, "", 0.1, 10.0}, {kParamAmpAttack, "ms", 0.0, 10000.0}, {kParamAmpAttackCurve, "", 0.1, 10.0}, {kParamAmpHold, "ms", 0.0, 10000.0}, {kParamAmpDecay, "ms", 0.0, 10000.0}, {kParamAmpDecayCurve, "", 0.1, 10.0}, {kParamAmpSustain, "%", 0.0, 100.0}, {kParamAmpRelease, "ms", 0.0, 10000.0}, {kParamAmpReleaseCurve, "", 0.1, 10.0}, {kParamTriggerLength, "%", 0.0, 100.0}, {kParamAmpTrigAttack, "ms", 0.0, 10000.0}, {kParamAmpTrigAttackCurve, "", 0.1, 10.0}, {kParamAmpTrigHold, "%", 0.0, 100.0}, {kParamAmpTrigDecay, "ms", 0.0, 10000.0}, {kParamAmpTrigDecayCurve, "", 0.1, 10.0}, {kParamMasterGain, "dB", -60.0, 24.0}, }; const std::size_t count = sizeof(kExpected) / sizeof(kExpected[0]); // Every exposed parameter is covered, and nothing else is listed. CHECK(count == exposedParams().size()); for (const Expect& e : kExpected) { const ParamRow* row = exposedRowFor(e.id); CHECK_ID(row != nullptr, e.id); if (!row) continue; CHECK_ID(std::strcmp(unitStringFor(row->deck), e.units) == 0, e.id); const PlainRange range = plainRangeFor(row->deck); // Relative rather than exact: the filter's three endpoints are float constants, so // 0.1f widened is not the double 0.1. A wrong RANGE — 0.2, or 20 — still fails. CHECK_ID(std::fabs(range.min - e.min) <= std::fabs(e.min) * 1e-6 + 1e-12, e.id); CHECK_ID(std::fabs(range.max - e.max) <= std::fabs(e.max) * 1e-6 + 1e-12, e.id); } } static void testEveryDefaultHasAnExactNormalizedPreimage() { // A host's reset-to-default arrives as toPlain(defaultNormalizedValue) and there is no // editor-side taper bypass available to it. Exactly equal, not near: a gain landing a hair // off unity is an audible error, and a stage time landing a hair off its default is a value // the user never dialled. for (const ParamRow& row : exposedParams()) { const double norm = defaultNormalized(row.deck); CHECK_ID(norm >= 0.0 && norm <= 1.0, row.id); CHECK_ID(toPlain(row.deck, norm) == defaultPlain(row.deck), row.id); } } static void testTheHostAndTheEditorAgreeOnEveryDefaultPosition() { // The criterion is that a host's reset-to-default and the editor's double-click land on the // SAME value — and those are different code paths: ParameterInfo::defaultNormalizedValue // comes from defaultNormalized (a per-CATEGORY switch), the editor's needle from // deckParamNorm (a per-ID one). Asserting the param module against itself would not see the // two disagree, and they carry three independently written full scales to disagree about. using reasampler::instrument::ui::deckParamNorm; const PlaySeconds defaults; for (const ParamRow& row : exposedParams()) { const double hostNorm = defaultNormalized(row.deck); // The four that STORE their normalized position take it verbatim on both surfaces, and // the two instance scalars are not in PlaySeconds at all — each read where it lives. double editorNorm = 0.0; switch (valueHomeFor(row.deck)) { case ValueHome::InstanceScalar: editorNorm = (row.deck == DeckParam::kMasterGain) ? reasampler::instrument::engine::masterGainNormFromLinear(1.0) // unity : reasampler::instrument::ui::keyTrackNormFrom( reasampler::instrument::map::InstrumentParams{}.keyTrack); break; case ValueHome::ParamSetNorm: case ValueHome::ParamSet: editorNorm = deckParamNorm(row.deck, defaults); break; case ValueHome::None: CHECK_ID(false, row.id); // an exposed control with no home reads nothing continue; } CHECK_ID(hostNorm == editorNorm, row.id); } } // The criterion names the editor's DOUBLE-CLICK, and that gesture is resetDeckParam, not // deckParamNorm over a default-constructed set. Asserted directly: reset a DIALLED set and its // stored field must read back at exactly the normalized value the host resets to. (The two // instance scalars have no resetDeckParam entry — the shell resets those from InstrumentParams, // which the test above covers at the same position.) static void testADoubleClickResetLandsOnTheHostsDefaultNormalized() { using reasampler::instrument::ui::deckParamNorm; using reasampler::instrument::ui::resetDeckParam; using reasampler::instrument::ui::setDeckParam; for (const ParamRow& row : exposedParams()) { if (valueHomeFor(row.deck) == ValueHome::InstanceScalar) continue; PlaySeconds dialled; // Away from the default first, so a reset that did nothing at all cannot pass. setDeckParam(row.deck, dialled, 0.37, /*segment=*/0); CHECK_ID(deckParamNorm(row.deck, dialled) != defaultNormalized(row.deck) || defaultNormalized(row.deck) == 0.37, row.id); resetDeckParam(row.deck, dialled); CHECK_ID(deckParamNorm(row.deck, dialled) == defaultNormalized(row.deck), row.id); } } static void testTheFiltersFourTakeTheirStoredNormVerbatim() { // Their stored value IS the normalized one, so no taper may participate in their default: // this fails the moment someone routes them through toNormalized(toPlain(x)). PlaySeconds defaults; const DeckParam kStoredNorm[] = {DeckParam::kFilterMorph, DeckParam::kFilterCutoff, DeckParam::kFilterQ, DeckParam::kFilterDrive}; for (DeckParam deck : kStoredNorm) { CHECK(storesNormalized(deck)); const float* stored = reasampler::instrument::ui::deckFloatField(deck, defaults); CHECK(stored != nullptr); if (stored) CHECK(defaultNormalized(deck) == static_cast(*stored)); } // And nothing else claims to store its norm — a control wrongly in that set would silently // skip the taper on the reset path. for (const ParamRow& row : exposedParams()) { const bool listed = row.deck == DeckParam::kFilterMorph || row.deck == DeckParam::kFilterCutoff || row.deck == DeckParam::kFilterQ || row.deck == DeckParam::kFilterDrive; CHECK_ID(storesNormalized(row.deck) == listed, row.id); } } static void testToPlainIsMonotoneAcrossTheWholeTravel() { // Monotonicity is required everywhere; round-trip exactness at an arbitrary norm is required // NOWHERE and is deliberately not asserted — no log map delivers it in double, and demanding // it would rule out the taper the range needs. for (const ParamRow& row : exposedParams()) { double previous = toPlain(row.deck, 0.0); for (int step = 1; step <= 200; ++step) { const double plain = toPlain(row.deck, step / 200.0); CHECK_ID(plain >= previous, row.id); previous = plain; } } } static void testTheEndpointsAreTheDeclaredPlainRange() { for (const ParamRow& row : exposedParams()) { const PlainRange range = plainRangeFor(row.deck); const double top = toPlain(row.deck, 1.0); CHECK_ID(std::fabs(top - range.max) <= std::fabs(range.max) * 1e-6 + 1e-9, row.id); if (row.deck == DeckParam::kMasterGain) { // Norm 0 is TRUE silence, not the -60 dB floor — the one plain value outside the // declared range, and the reason the dB formatter has an -inf case at all. CHECK(!std::isfinite(toPlain(row.deck, 0.0))); continue; } const double bottom = toPlain(row.deck, 0.0); CHECK_ID(std::fabs(bottom - range.min) <= std::fabs(range.min) * 1e-6 + 1e-9, row.id); } } static void testNoExposedControlIsDiscrete() { // This is what makes "stepCount = 0 on all of them" structural rather than lucky: stepCount // is only meaningful for a discrete control, and every discrete control is reload or rebuild // tier and therefore never reaches the list. UnitCategory::None is the deck's own name for // "no continuous unit" — toggles, radios, the curve-popup cells, the integer voice count. for (const ParamRow& row : exposedParams()) { CHECK_ID(reasampler::instrument::ui::deckParamUnit(row.deck) != reasampler::instrument::ui::UnitCategory::None, row.id); } } static void testTheAddedDriveInverseUndoesTheFrozenLaw() { using reasampler::instrument::engine::filter::filterDriveDepthFromNorm; using reasampler::instrument::engine::filter::filterNormFromDriveDepth; for (int step = 0; step <= 100; ++step) { const float norm = static_cast(step) / 100.0f; const float back = filterNormFromDriveDepth(filterDriveDepthFromNorm(norm)); CHECK(std::fabs(back - norm) < 1e-6f); } CHECK(filterNormFromDriveDepth(0.0f) == 0.0f); CHECK(filterNormFromDriveDepth(-1.0f) == 0.0f); CHECK(filterNormFromDriveDepth(1000.0f) == 1.0f); } int main() { testEveryUnitStringAndRangeMatchesTheSpecifiedTable(); testEveryDefaultHasAnExactNormalizedPreimage(); testTheHostAndTheEditorAgreeOnEveryDefaultPosition(); testADoubleClickResetLandsOnTheHostsDefaultNormalized(); testTheFiltersFourTakeTheirStoredNormVerbatim(); testToPlainIsMonotoneAcrossTheWholeTravel(); testTheEndpointsAreTheDeclaredPlainRange(); testNoExposedControlIsDiscrete(); testTheAddedDriveInverseUndoesTheFrozenLaw(); if (g_fail == 0) std::printf("param_units: all tests passed\n"); return g_fail == 0 ? 0 : 1; }