// 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 #include 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; }