Files
reasampler/tests/test_param_live.cpp
T
daniel de5654fb6f 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.
2026-08-02 16:22:17 -04:00

153 lines
6.6 KiB
C++

// Standalone tests for the audio thread's parameter patch. The load-bearing one is the
// EQUIVALENCE assertion: patching a control into the live block must produce, bit for bit, the
// block the model path would have folded — which is what makes a second routing table safe.
#include "../src/core/instrument/param/param_live.h"
#include "../src/core/instrument/param/param_id.h"
#include "../src/core/instrument/param/param_units.h"
#include "../src/core/instrument/map/sample_map.h"
#include "../src/core/instrument/ui/deck_values.h"
#include <cstdio>
#include <cstring>
using namespace reasampler;
using namespace reasampler::instrument::param;
using reasampler::instrument::engine::LiveValues;
using reasampler::instrument::engine::foldLive;
using reasampler::instrument::map::InstrumentParams;
using reasampler::instrument::map::PlaySeconds;
using reasampler::instrument::map::resolvePlay;
using reasampler::instrument::ui::DeckParam;
static int g_fail = 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)
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
namespace {
constexpr int kRate = 48000;
// The MODEL path, verbatim: the write the editor makes, resolved and folded the way every
// publisher folds it. This is the reference the patch is measured against.
LiveValues modelBlock(const InstrumentParams& params) {
return foldLive(resolvePlay(params.play, kRate), params.keyTrack);
}
// A parameter set deliberately away from its defaults, so an equivalence that only holds at the
// default cannot pass. Not every field — just enough that a mis-routed patch lands on a value
// that differs from the one it should have written.
InstrumentParams dialledParams() {
InstrumentParams p;
p.keyTrack = 1.5;
p.play.adsr.attackSeconds = 0.31;
p.play.adsr.holdSeconds = 0.07;
p.play.adsr.decaySeconds = 0.44;
p.play.adsr.sustainLevel = 0.62;
p.play.adsr.releaseSeconds = 0.9;
p.play.adsr.attackCurve = 2.5;
p.play.trigger.lengthFraction = 0.4;
p.play.trigAhd.attackSeconds = 0.12;
p.play.trigAhd.holdFraction = 0.3;
p.play.playRate = 1.2;
p.play.pitchOffsetSemitones = -5.0;
p.play.pitchEnv.enabled = true;
p.play.pitchEnv.peakSemitones = 7.0;
p.play.pitchEnv.shape.attackSeconds = 0.02;
p.play.filter.enabled = true;
p.play.filter.settings.cutoffNorm = 0.42f;
p.play.filter.settings.resonanceNorm = 0.66f;
p.play.filter.modAmount = -0.4;
p.play.filter.velAmount = 0.25;
p.play.filter.keyTrack = 0.75;
p.play.filter.env.attackSeconds = 0.05;
p.play.filter.trigEnv.decaySeconds = 0.6;
return p;
}
} // namespace
// THE assertion this module exists for. For every exposed control and several normalized
// positions: writing it through the model and folding must equal patching it into the folded
// block. Bytes, not fields — a member the patch forgot to route is caught as surely as one it
// routed to the wrong place.
static void testPatchingAControlEqualsFoldingTheModelAfterTheSameWrite() {
const double kPositions[] = {0.0, 0.137, 0.5, 0.813, 1.0};
for (const ParamRow& row : exposedParams()) {
// Master gain is not carried by the block at all — the processor's own atomic is its
// route to the audio, and the patch reports that by refusing it.
if (row.deck == DeckParam::kMasterGain) {
LiveValues block = modelBlock(dialledParams());
const LiveValues before = block;
CHECK_ID(!applyLiveParam(block, row.deck, 0.25, kRate), row.id);
CHECK_ID(std::memcmp(&before, &block, sizeof(LiveValues)) == 0, row.id);
continue;
}
for (double norm : kPositions) {
InstrumentParams written = dialledParams();
if (row.deck == DeckParam::kKeyTrack) {
written.keyTrack = reasampler::instrument::ui::keyTrackFromNorm(norm);
} else {
reasampler::instrument::ui::setDeckParam(row.deck, written.play, norm,
/*segment=*/0);
}
const LiveValues expected = modelBlock(written);
LiveValues patched = modelBlock(dialledParams());
CHECK_ID(applyLiveParam(patched, row.deck, norm, kRate), row.id);
CHECK_ID(std::memcmp(&expected, &patched, sizeof(LiveValues)) == 0, row.id);
}
}
}
// A drawn contour makes Trigger length inert — the editor's knob goes dead and the fold pins the
// fraction at 1.0. A host lane pointed at it must be equally inert, or automation would re-open a
// control the model says is closed.
static void testTriggerLengthIsInertUnderADrawnEnvelope() {
InstrumentParams p = dialledParams();
p.play.ampSpline.mode = reasampler::EnvMode::Spline;
LiveValues block = modelBlock(p);
CHECK(block.splineActive);
CHECK(block.lengthFraction == 1.0);
CHECK(applyLiveParam(block, DeckParam::kTrigLength, 0.2, kRate));
CHECK(block.lengthFraction == 1.0);
}
// A control with no parameter row is refused rather than silently landing somewhere.
static void testAnUnexposedControlIsRefused() {
LiveValues block = modelBlock(dialledParams());
const LiveValues before = block;
CHECK(!applyLiveParam(block, DeckParam::kPlayMode, 1.0, kRate));
CHECK(!applyLiveParam(block, DeckParam::kVoiceCount, 1.0, kRate));
CHECK(std::memcmp(&before, &block, sizeof(LiveValues)) == 0);
}
// Every exposed control resolves to a home the host's read and write paths actually reach. The
// guard the promotion of pitch key-track needed: id 1000 was issued against a control whose value
// is not in PlaySeconds, and nothing failed to compile.
static void testEveryExposedControlHasAValueHome() {
for (const ParamRow& row : exposedParams()) {
CHECK_ID(valueHomeFor(row.deck) != ValueHome::None, row.id);
}
// And the instance-scalar set is exactly the two the shell branches on by name.
CHECK(valueHomeFor(DeckParam::kMasterGain) == ValueHome::InstanceScalar);
CHECK(valueHomeFor(DeckParam::kKeyTrack) == ValueHome::InstanceScalar);
int instanceScalars = 0;
for (const ParamRow& row : paramTable()) {
if (valueHomeFor(row.deck) == ValueHome::InstanceScalar) ++instanceScalars;
}
CHECK(instanceScalars == 2);
}
int main() {
testPatchingAControlEqualsFoldingTheModelAfterTheSameWrite();
testTriggerLengthIsInertUnderADrawnEnvelope();
testAnUnexposedControlIsRefused();
testEveryExposedControlHasAValueHome();
if (g_fail == 0) std::printf("param_live: all tests passed\n");
return g_fail == 0 ? 0 : 1;
}