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.
This commit is contained in:
2026-08-02 16:22:17 -04:00
parent bfaa0f2614
commit de5654fb6f
44 changed files with 1205 additions and 302 deletions
+85 -21
View File
@@ -1,17 +1,21 @@
// 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.
// 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 <cmath>
#include <cstdio>
#include <cstring>
#include <limits>
#include <string>
using namespace reasampler;
@@ -61,34 +65,40 @@ static void testEachCategoryPrintsItsSpecifiedShape() {
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, 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.
// 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 (row.deck == DeckParam::kMasterGain) continue; // not stored in PlaySeconds
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);
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.
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, editorNorm);
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",
@@ -97,6 +107,16 @@ static void testTheEditorAndTheHostPrintTheSameDigitsAtTheSameStoredValue() {
}
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);
@@ -106,6 +126,48 @@ static void testTheEditorAndTheHostPrintTheSameDigitsAtTheSameStoredValue() {
}
}
// The one place the two surfaces GENUINELY diverge, asserted so it stays a known property rather
// than a surprise: an exponent inside curve_law's centre detent but not exactly neutral is
// reachable only through an overlay knot drag, and the host — which holds the norm and nothing
// else — reads it back as the neutral the knob law snaps to.
static void testAnOffDetentExponentReadsNeutralToTheHostAndTrueToTheEditor() {
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.00");
}
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;
@@ -165,7 +227,9 @@ static void testEveryExposedParameterHasAFormatterThatWritesSomething() {
int main() {
testEachCategoryPrintsItsSpecifiedShape();
testTheEditorAndTheHostPrintTheSameDigitsAtTheSameStoredValue();
testAnOffDetentExponentReadsNeutralToTheHostAndTrueToTheEditor();
testMasterGainPrintsTheSameDigitsFromEitherSurface();
testKeyTrackPrintsTheSameDigitsFromEitherSurface();
testTypingBackADisplayedValueLandsOnIt();
testAShortBufferIsNeverOverrunAndAlwaysTerminates();
testEveryExposedParameterHasAFormatterThatWritesSomething();