Report the instrument's automatable parameters to the host under a frozen id table, in signal-flow order, with real units
42 of 44 ids issued: pitch key-track and Trigger length stay reserved pending a live path. Master gain reclassified Live — it never reloaded.
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
// 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 <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
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<double>::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<int>(UnitKind::Dimensionless); ++kind) {
|
||||
char tiny[4];
|
||||
std::memset(tiny, 'x', sizeof(tiny));
|
||||
formatPlain(static_cast<UnitKind>(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;
|
||||
}
|
||||
Reference in New Issue
Block a user