// 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, each over the plain value ITS OWN surface derives (the // editor's exponent read goes to the stored field, not through the knob law). 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/map/sample_map.h" #include "../src/core/instrument/param/param_id.h" #include "../src/core/instrument/param/param_units.h" #include "../src/core/instrument/ui/deck_values.h" #include #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"); } // The EDITOR's derivation, spelled the way ReaSamplerEditor::deckPlainValue spells it — a curve // exponent is read off its stored field, never round-tripped through the knob law. Driving the // sweep through this rather than through toPlain(deckParamNorm(...)) is what makes the exponent // half of it a real assertion instead of a round trip on both sides. static double editorPlainValue(DeckParam deck, PlaySeconds& play) { using reasampler::instrument::ui::UnitCategory; if (reasampler::instrument::ui::deckParamUnit(deck) == UnitCategory::Exponent) { const double* stored = reasampler::instrument::ui::deckDoubleField(deck, play); return stored ? *stored : 0.0; } return toPlain(deck, reasampler::instrument::ui::deckParamNorm(deck, play)); } static void testTheEditorAndTheHostPrintTheSameDigitsAtTheSameStoredValue() { // The host derives its plain value from the normalized one it holds; the editor derives its // from the STORED field. 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 the parameter set carries. for (const ParamRow& row : exposedParams()) { if (valueHomeFor(row.deck) == ValueHome::InstanceScalar) continue; // own tests below 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); const double storedNorm = reasampler::instrument::ui::deckParamNorm(row.deck, play); if (row.deck == DeckParam::kFilterMorph) { // MORPH ALONE: its toPlain is full-double (`clamp01(n) * 100`), so the float the // model stores and the double the host holds are genuinely different inputs, and // at a value landing on a display rounding boundary that is worth one integer // percent. Cutoff/Q/drive cast to float INSIDE toPlain, so they are bit-identical // either way and are held to the string below like everything else. const double hostPlain = toPlain(row.deck, norm); const double editorPlain = toPlain(row.deck, storedNorm); 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; } // A write the model CLAMPED (Trigger length's (0,1] floor) is not a settled state: // setParamNormalized caches what the model TOOK, so the host never holds the rejected // value. Only the four float-stored positions differ by a ulp rather than a clamp. if (!storesNormalized(row.deck) && storedNorm != norm) continue; char hostBuf[24]; char editorBuf[24]; formatPlainFor(row.deck, toPlain(row.deck, norm), hostBuf, sizeof(hostBuf)); formatPlainFor(row.deck, editorPlainValue(row.deck, play), editorBuf, sizeof(editorBuf)); 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; } } } } // A continuous range stays continuous at the host boundary (Daniel, 2026-08-02): the knob // detent is a drag affordance only, so an exponent inside it but not exactly neutral — reachable // via an overlay knot drag — must read back true to the host, the same digits the editor shows. static void testAnOffDetentExponentReadsTrueToBothTheHostAndTheEditor() { PlaySeconds play; // The detent is +/-0.01 in NORM, which is a ~+/-0.047 band in the exponent — so 1.04 is // inside it and still prints as a distinct number. play.adsr.attackCurve = 1.04; char editorBuf[24]; formatPlainFor(DeckParam::kAttackCurve, editorPlainValue(DeckParam::kAttackCurve, play), editorBuf, sizeof(editorBuf)); CHECK(std::string(editorBuf) == "1.04"); const double hostNorm = reasampler::instrument::ui::deckParamNorm(DeckParam::kAttackCurve, play); char hostBuf[24]; formatPlainFor(DeckParam::kAttackCurve, toPlain(DeckParam::kAttackCurve, hostNorm), hostBuf, sizeof(hostBuf)); CHECK(std::string(hostBuf) == "1.04"); } static void testKeyTrackPrintsTheSameDigitsFromEitherSurface() { using reasampler::instrument::map::InstrumentParams; for (int step = 0; step <= 40; ++step) { const double norm = step / 40.0; InstrumentParams params; params.keyTrack = reasampler::instrument::ui::keyTrackFromNorm(norm); const double editorNorm = reasampler::instrument::ui::keyTrackNormFrom(params.keyTrack); char hostBuf[24]; char editorBuf[24]; formatPlainFor(DeckParam::kKeyTrack, toPlain(DeckParam::kKeyTrack, norm), hostBuf, sizeof(hostBuf)); formatPlainFor(DeckParam::kKeyTrack, toPlain(DeckParam::kKeyTrack, editorNorm), editorBuf, sizeof(editorBuf)); if (std::strcmp(hostBuf, editorBuf) != 0) { std::printf("FAIL key-track at norm %.4f: host \"%s\" vs editor \"%s\"\n", 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(); testAnOffDetentExponentReadsTrueToBothTheHostAndTheEditor(); testMasterGainPrintsTheSameDigitsFromEitherSurface(); testKeyTrackPrintsTheSameDigitsFromEitherSurface(); testTypingBackADisplayedValueLandsOnIt(); testAShortBufferIsNeverOverrunAndAlwaysTerminates(); testEveryExposedParameterHasAFormatterThatWritesSomething(); if (g_fail == 0) std::printf("param_format: all tests passed\n"); return g_fail == 0 ? 0 : 1; }