// Standalone tests for the ONE formatter per unit category: the digit shapes each category // prints, and the property that makes the editor's knob label and the host's parameter string // identical — both call THIS function, over a plain value derived the way each surface derives // it. No VST3, no REAPER, no framework. #include "../src/core/instrument/param/param_format.h" #include "../src/core/instrument/engine/master_gain.h" #include "../src/core/instrument/param/param_id.h" #include "../src/core/instrument/ui/deck_values.h" #include #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) static std::string digits(UnitKind kind, double plain) { char buf[24]; formatPlain(kind, plain, buf, sizeof(buf)); return std::string(buf); } static void testEachCategoryPrintsItsSpecifiedShape() { // A time constant never switches to seconds, so the ceiling reads 10000 and not 10. CHECK(digits(UnitKind::Time, 0.5) == "0.5"); CHECK(digits(UnitKind::Time, 3.0) == "3.0"); CHECK(digits(UnitKind::Time, 10.0) == "10"); CHECK(digits(UnitKind::Time, 104.0) == "104"); CHECK(digits(UnitKind::Time, 10000.0) == "10000"); // Semitones are always signed, including at zero — an unsigned "0.0" beside a "+3.5" reads // as a different kind of quantity. CHECK(digits(UnitKind::Semitones, 3.5) == "+3.5"); CHECK(digits(UnitKind::Semitones, -12.0) == "-12.0"); CHECK(digits(UnitKind::Semitones, 0.0) == "+0.0"); CHECK(digits(UnitKind::PercentUnipolar, 100.0) == "100"); CHECK(digits(UnitKind::PercentKeyTrack, 200.0) == "200"); CHECK(digits(UnitKind::PercentBipolar, -40.0) == "-40"); CHECK(digits(UnitKind::PercentBipolar, 40.0) == "+40"); // Rate keeps a decimal: its snap grid is whole semitones, which do not land on integer // percent, so an integer display would print a snapped position the snap cannot produce. CHECK(digits(UnitKind::PercentRate, 105.946) == "105.9"); CHECK(digits(UnitKind::PercentRate, 200.0) == "200.0"); CHECK(digits(UnitKind::Decibels, 0.0) == "+0.0"); CHECK(digits(UnitKind::Decibels, -12.0) == "-12.0"); CHECK(digits(UnitKind::Decibels, -std::numeric_limits::infinity()) == "-inf"); // Cutoff's "k" abbreviation is retired — one static units string cannot switch with // magnitude, and keeping "12.8k" on one surface alone is the divergence this file forbids. CHECK(digits(UnitKind::Hertz, 240.0) == "240"); CHECK(digits(UnitKind::Hertz, 12800.0) == "12800"); // The caret on a curve dial is the editor's static cell chrome, never part of the value. CHECK(digits(UnitKind::Dimensionless, 1.0) == "1.00"); CHECK(digits(UnitKind::Dimensionless, 0.1) == "0.10"); } static void testTheEditorAndTheHostPrintTheSameDigitsAtTheSameStoredValue() { // The host derives its plain value from the normalized one it holds; the editor derives its // from the STORED field, through the deck's own read. If those two derivations disagreed at // any reachable value the two surfaces would print different numbers for one control — this // is that property, swept over the whole travel of every exposed control. for (const ParamRow& row : exposedParams()) { if (row.deck == DeckParam::kMasterGain) continue; // not stored in PlaySeconds for (int step = 0; step <= 40; ++step) { const double norm = step / 40.0; PlaySeconds play; reasampler::instrument::ui::setDeckParam(row.deck, play, norm, /*segment=*/0); char hostBuf[24]; formatPlainFor(row.deck, toPlain(row.deck, norm), hostBuf, sizeof(hostBuf)); const double editorNorm = reasampler::instrument::ui::deckParamNorm(row.deck, play); char editorBuf[24]; formatPlainFor(row.deck, toPlain(row.deck, editorNorm), editorBuf, sizeof(editorBuf)); if (storesNormalized(row.deck)) { // The filter's four store their position as a FLOAT, so a norm the host has sent // but we have not yet stored differs from the stored one by up to a float ulp. // At a value landing exactly on a display rounding boundary that is worth one // digit, so these four are held to the PLAIN value rather than to the string — // the derivation is still asserted to be one derivation. const double hostPlain = toPlain(row.deck, norm); const double editorPlain = toPlain(row.deck, editorNorm); const double tolerance = std::fabs(hostPlain) * 1e-6 + 1e-9; if (std::fabs(hostPlain - editorPlain) > tolerance) { std::printf("FAIL param %u at norm %.4f: host %.9g vs editor %.9g\n", row.id, norm, hostPlain, editorPlain); ++g_fail; } continue; } if (std::strcmp(hostBuf, editorBuf) != 0) { std::printf("FAIL param %u at norm %.4f: host \"%s\" vs editor \"%s\"\n", row.id, norm, hostBuf, editorBuf); ++g_fail; } } } } static void testMasterGainPrintsTheSameDigitsFromEitherSurface() { using reasampler::instrument::engine::masterGainLinearFromNorm; using reasampler::instrument::engine::masterGainNormFromLinear; for (int step = 0; step <= 40; ++step) { const double norm = step / 40.0; // The editor reads the processor's stored LINEAR gain back through the taper; the host // holds the normalized value directly. const double editorNorm = masterGainNormFromLinear(masterGainLinearFromNorm(norm)); char hostBuf[24]; char editorBuf[24]; formatPlainFor(DeckParam::kMasterGain, toPlain(DeckParam::kMasterGain, norm), hostBuf, sizeof(hostBuf)); formatPlainFor(DeckParam::kMasterGain, toPlain(DeckParam::kMasterGain, editorNorm), editorBuf, sizeof(editorBuf)); if (std::strcmp(hostBuf, editorBuf) != 0) { std::printf("FAIL master gain at norm %.4f: host \"%s\" vs editor \"%s\"\n", norm, hostBuf, editorBuf); ++g_fail; } } } static void testTypingBackADisplayedValueLandsOnIt() { // getParamValueByString's half: the digits the host just showed must parse to the same // plain value, with or without the unit a user may retype beside them. double plain = 0.0; CHECK(parsePlain(UnitKind::Time, "104", plain) && plain == 104.0); CHECK(parsePlain(UnitKind::Time, "104 ms", plain) && plain == 104.0); CHECK(parsePlain(UnitKind::Semitones, "+3.5", plain) && plain == 3.5); CHECK(parsePlain(UnitKind::Semitones, "-12.0st", plain) && plain == -12.0); CHECK(parsePlain(UnitKind::Hertz, "12800Hz", plain) && plain == 12800.0); CHECK(parsePlain(UnitKind::Decibels, "-inf", plain) && !std::isfinite(plain) && plain < 0.0); CHECK(!parsePlain(UnitKind::Time, "abc", plain)); CHECK(!parsePlain(UnitKind::Time, nullptr, plain)); } static void testAShortBufferIsNeverOverrunAndAlwaysTerminates() { for (int kind = 0; kind <= static_cast(UnitKind::Dimensionless); ++kind) { char tiny[4]; std::memset(tiny, 'x', sizeof(tiny)); formatPlain(static_cast(kind), 1500.0, tiny, sizeof(tiny)); CHECK(tiny[3] == '\0'); } } static void testEveryExposedParameterHasAFormatterThatWritesSomething() { for (const ParamRow& row : exposedParams()) { char buf[24]; formatPlainFor(row.deck, toPlain(row.deck, 0.5), buf, sizeof(buf)); if (buf[0] == '\0') { std::printf("FAIL param %u produced an empty string\n", row.id); ++g_fail; } } } int main() { testEachCategoryPrintsItsSpecifiedShape(); testTheEditorAndTheHostPrintTheSameDigitsAtTheSameStoredValue(); testMasterGainPrintsTheSameDigitsFromEitherSurface(); testTypingBackADisplayedValueLandsOnIt(); testAShortBufferIsNeverOverrunAndAlwaysTerminates(); testEveryExposedParameterHasAFormatterThatWritesSomething(); if (g_fail == 0) std::printf("param_format: all tests passed\n"); return g_fail == 0 ? 0 : 1; }