// Standalone tests for reasampler::instrument::bake::bake_reset — no VST3, no REAPER, no // framework. Same fast assert loop as the sibling pure tests. // // Covers the ratified reset scope PER PARAMETER, in both directions: every control whose // effect the render printed comes back at its default, and every mapping fact comes back // untouched. Asserted field by field rather than by struct equality on purpose — a // whole-struct compare would pass while silently resetting a survivor, or vice versa. The // sweep runs over TWO independently dialed fixtures that share only the survivors, which is // what makes it a property of the survivors alone rather than of one input. #include "../src/core/instrument/bake/bake_reset.h" // The loop's neutral is loop_marks' definition of it, not this test's reading of it: what a // reset loop LOOKS like on the band is the thing worth asserting, and only that module says. #include "../src/core/instrument/ui/loop_marks.h" #include #include using namespace reasampler; using namespace reasampler::instrument::bake; using reasampler::instrument::map::InstrumentParams; using reasampler::instrument::map::PlaySeconds; using reasampler::instrument::ui::LoopMarks; using reasampler::instrument::ui::StoredLoop; using reasampler::instrument::ui::resolveLoopMarks; namespace note = reasampler::instrument::note; namespace filter = reasampler::instrument::engine::filter; static int g_fail = 0; static const char* g_ctx = ""; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL [%s] line %d: %s\n", g_ctx, __LINE__, #cond); ++g_fail; } } while(0) namespace { // Every field moved off its default, so a reset that misses one is visible. InstrumentParams dialed() { InstrumentParams p; p.rootOverride = 43; p.loopOverride = SampleLoop{true, 111, 222}; p.startPoint = 4321; p.loopCrossfadeFrames = 512; p.keyTrack = 0.5; p.velocityCurve = VelocityCurve::linear(); p.play.playMode = PlayMode::Trigger; p.play.adsr.attackSeconds = 0.4; p.play.adsr.holdSeconds = 0.3; p.play.adsr.decaySeconds = 0.2; p.play.adsr.sustainLevel = 0.1; p.play.adsr.releaseSeconds = 0.9; p.play.adsr.attackCurve = 2.5; p.play.adsr.decayCurve = 0.4; p.play.adsr.releaseCurve = 3.0; p.play.trigger.lengthFraction = 0.25; p.play.trigAhd.attackSeconds = 0.11; p.play.trigAhd.decaySeconds = 0.22; p.play.trigAhd.holdFraction = 0.33; p.play.trigAhd.attackCurve = 1.7; p.play.trigAhd.decayCurve = 2.2; p.play.pitchEngine = PitchEngine::Varispeed; p.play.pitchEnv.enabled = true; p.play.pitchEnv.peakSemitones = -7.0; p.play.pitchEnv.shape.attackSeconds = 0.05; p.play.pitchEnv.shape.decaySeconds = 0.15; p.play.pitchVelocityCurve = VelocityCurve::linear(); p.play.playRate = 0.5; p.play.pitchOffsetSemitones = -7.5; p.play.filter.enabled = true; p.play.filter.settings = filter::FilterSettings{0.3f, 0.8f, 0.1f, 0.7f, filter::MorphLaw::HighNotchLow}; p.play.filter.modAmount = -0.8; p.play.filter.velAmount = 0.6; p.play.filter.keyTrack = 1.5; p.play.filter.env.attackSeconds = 0.7; p.play.filter.trigEnv.decaySeconds = 0.8; p.play.filter.velocityCurve = VelocityCurve::linear(); p.play.ampSpline.mode = EnvMode::Spline; p.play.ampSpline.contour = VelocityCurve::linear(); p.play.pitchSpline.mode = EnvMode::Spline; p.play.pitchSpline.contour = VelocityCurve::linear(); p.play.filterSpline.mode = EnvMode::Spline; p.play.filterSpline.contour = VelocityCurve::linear(); p.bakeHold = note::makeDivision(0, note::DivisionModifier::Dotted); p.limiterEnabled = true; return p; } // A SECOND dialed instrument, agreeing with the first on the SURVIVORS and disagreeing on // every other field — including play mode, which is a chosen neutral rather than a survivor. // Both run the same neutral sweep: if the reset ever inverted into "copy the dialed set, then // clear a blacklist", a non-survivor would come through and at most one fixture could still // land on the defaults. A parameter added later is covered here by moving it in BOTH // fixtures, which is the same work as adding it to the sweep. InstrumentParams dialedOther() { InstrumentParams p; p.rootOverride = 43; // survivor — same as dialed() p.keyTrack = 0.5; // survivor — same as dialed() p.loopOverride = SampleLoop{true, 7, 909}; p.startPoint = 12; p.loopCrossfadeFrames = 64; p.velocityCurve = VelocityCurve::rampDown(); p.play.playMode = PlayMode::Gate; p.play.adsr.attackSeconds = 1.4; p.play.adsr.holdSeconds = 1.3; p.play.adsr.decaySeconds = 1.2; p.play.adsr.sustainLevel = 0.7; p.play.adsr.releaseSeconds = 1.9; p.play.adsr.attackCurve = 0.3; p.play.adsr.decayCurve = 3.4; p.play.adsr.releaseCurve = 0.6; p.play.trigger.lengthFraction = 0.75; p.play.trigAhd.attackSeconds = 0.91; p.play.trigAhd.decaySeconds = 0.82; p.play.trigAhd.holdFraction = 0.13; p.play.trigAhd.attackCurve = 0.7; p.play.trigAhd.decayCurve = 0.9; p.play.pitchEngine = PitchEngine::Preserve; p.play.pitchEnv.enabled = true; p.play.pitchEnv.peakSemitones = 11.0; p.play.pitchEnv.shape.attackSeconds = 0.25; p.play.pitchEnv.shape.decaySeconds = 0.35; p.play.pitchVelocityCurve = VelocityCurve::rampDown(); p.play.playRate = 1.75; p.play.pitchOffsetSemitones = 3.25; p.play.filter.enabled = true; p.play.filter.settings = filter::FilterSettings{0.9f, 0.2f, 0.4f, 0.05f, filter::MorphLaw::HighNotchLow}; p.play.filter.modAmount = 0.45; p.play.filter.velAmount = -0.35; p.play.filter.keyTrack = -0.9; p.play.filter.env.attackSeconds = 1.7; p.play.filter.trigEnv.decaySeconds = 1.8; p.play.filter.velocityCurve = VelocityCurve::rampDown(); p.play.ampSpline.mode = EnvMode::Spline; p.play.ampSpline.contour = VelocityCurve::flat(); p.play.pitchSpline.mode = EnvMode::Spline; p.play.pitchSpline.contour = VelocityCurve::flat(); p.play.filterSpline.mode = EnvMode::Spline; p.play.filterSpline.contour = VelocityCurve::flat(); p.bakeHold = note::makeDivision(2, note::DivisionModifier::Triplet); p.limiterEnabled = true; return p; } bool sameCurve(const VelocityCurve& a, const VelocityCurve& b) { if (a.domain() != b.domain() || a.size() != b.size()) return false; for (std::size_t i = 0; i < a.size(); ++i) { if (a.points()[i].velocity != b.points()[i].velocity) return false; if (a.points()[i].value != b.points()[i].value) return false; } return true; } // THE reset neutral, field by field. Run against every fixture, so the two can never be // asserted against two different notions of neutral. void checkNeutral(const BakeReset& reset) { const InstrumentParams& after = reset.params; const InstrumentParams fresh; // the defaults every reset control must land on const PlaySeconds freshPlay; // --- RESET: loop points, start point, crossfade --------------------------------- CHECK(!after.loopOverride.has_value()); CHECK(!after.startPoint.has_value()); CHECK(after.loopCrossfadeFrames == 0); // --- RESET: the velocity transfer curves ---------------------------------------- CHECK(sameCurve(after.velocityCurve, VelocityCurve::flat())); CHECK(sameCurve(after.play.pitchVelocityCurve, VelocityCurve::zero())); CHECK(sameCurve(after.play.filter.velocityCurve, VelocityCurve::zero())); // --- RESET: play mode, to TRIGGER rather than to the struct's Gate default ------- // bake_reset.cpp carries the argument at the assignment. CHECK(after.play.playMode == PlayMode::Trigger); CHECK(freshPlay.playMode == PlayMode::Gate); // and that really is NOT the default // The Trigger face it lands on plays the whole file flat: full span, unity throughout. CHECK(after.play.trigger.lengthFraction == 1.0); CHECK(after.play.trigAhd.attackSeconds == 0.0); CHECK(after.play.trigAhd.decaySeconds == 0.0); // --- RESET: the amp envelope, staged, every stage and every curve exponent ------ CHECK(after.play.adsr.attackSeconds == freshPlay.adsr.attackSeconds); CHECK(after.play.adsr.holdSeconds == freshPlay.adsr.holdSeconds); CHECK(after.play.adsr.decaySeconds == freshPlay.adsr.decaySeconds); CHECK(after.play.adsr.sustainLevel == freshPlay.adsr.sustainLevel); CHECK(after.play.adsr.releaseSeconds == freshPlay.adsr.releaseSeconds); CHECK(after.play.adsr.attackCurve == freshPlay.adsr.attackCurve); CHECK(after.play.adsr.decayCurve == freshPlay.adsr.decayCurve); CHECK(after.play.adsr.releaseCurve == freshPlay.adsr.releaseCurve); CHECK(after.play.trigger.lengthFraction == freshPlay.trigger.lengthFraction); CHECK(after.play.trigAhd.attackSeconds == freshPlay.trigAhd.attackSeconds); CHECK(after.play.trigAhd.decaySeconds == freshPlay.trigAhd.decaySeconds); CHECK(after.play.trigAhd.holdFraction == freshPlay.trigAhd.holdFraction); CHECK(after.play.trigAhd.attackCurve == freshPlay.trigAhd.attackCurve); CHECK(after.play.trigAhd.decayCurve == freshPlay.trigAhd.decayCurve); // --- RESET: pitch engine + pitch envelope --------------------------------------- CHECK(after.play.pitchEngine == freshPlay.pitchEngine); CHECK(after.play.pitchEngine == PitchEngine::Preserve); // the product default CHECK(!after.play.pitchEnv.enabled); CHECK(after.play.pitchEnv.peakSemitones == 0.0); CHECK(after.play.pitchEnv.shape.attackSeconds == freshPlay.pitchEnv.shape.attackSeconds); CHECK(after.play.pitchEnv.shape.decaySeconds == freshPlay.pitchEnv.shape.decaySeconds); // --- RESET: Rate and the baseline Pitch offset ----------------------------------- // Both are processing the bake already printed, so the whitelist leaves them at their // defaults — the safe direction. A second bake of the result at a still-dialled rate would // otherwise re-stretch what the first one baked in. CHECK(after.play.playRate == 1.0); CHECK(after.play.pitchOffsetSemitones == 0.0); CHECK(after.play.playRate == freshPlay.playRate); CHECK(after.play.pitchOffsetSemitones == freshPlay.pitchOffsetSemitones); // --- RESET: the filter, its control positions, and its velocity/key-tracking mod -- CHECK(!after.play.filter.enabled); CHECK(after.play.filter.modAmount == 0.0); CHECK(after.play.filter.velAmount == 0.0); CHECK(after.play.filter.keyTrack == 0.0); CHECK(after.play.filter.settings.cutoffNorm == freshPlay.filter.settings.cutoffNorm); CHECK(after.play.filter.settings.resonanceNorm == freshPlay.filter.settings.resonanceNorm); CHECK(after.play.filter.settings.morphNorm == freshPlay.filter.settings.morphNorm); CHECK(after.play.filter.settings.driveNorm == freshPlay.filter.settings.driveNorm); CHECK(after.play.filter.settings.morphLaw == freshPlay.filter.settings.morphLaw); CHECK(after.play.filter.env.attackSeconds == freshPlay.filter.env.attackSeconds); CHECK(after.play.filter.trigEnv.decaySeconds == freshPlay.filter.trigEnv.decaySeconds); // --- RESET: the three spline contours AND their mode flags ---------------------- // The flag selects which shape ran, so the shape it selected is in the audio; with // both contours reset it also has nothing left to preserve. CHECK(after.play.ampSpline.mode == EnvMode::Staged); CHECK(after.play.pitchSpline.mode == EnvMode::Staged); CHECK(after.play.filterSpline.mode == EnvMode::Staged); CHECK(sameCurve(after.play.ampSpline.contour, VelocityCurve::rampDown())); CHECK(sameCurve(after.play.pitchSpline.contour, VelocityCurve::rampDown())); CHECK(sameCurve(after.play.filterSpline.contour, VelocityCurve::rampDown())); // --- RESET: the bake's own Hold division ----------------------------------------- // It sizes the render window for the one case that cannot derive one, so the length it // chose is in the printed file, and the next bake re-derives from that file. CHECK(after.bakeHold == fresh.bakeHold); // --- RESET: the master-bus limiter enable ---------------------------------------- // Read the asymmetry recorded at BakeReset before reasoning about this one: it is not // the master stage resetting as a unit. CHECK(!after.limiterEnabled); CHECK(!fresh.limiterEnabled); // --- RESET: master gain ---------------------------------------------------------- CHECK(reset.masterGainLinear == 1.0); } } // namespace int main() { const InstrumentParams before = dialed(); const BakeReset reset = resetAfterBake(before); const InstrumentParams& after = reset.params; const InstrumentParams fresh; // The fixtures must really move every field the sweep asserts, or the sweep is vacuous. CHECK(fresh.keyTrack != before.keyTrack); CHECK(before.limiterEnabled != fresh.limiterEnabled); CHECK(before.bakeHold != fresh.bakeHold); CHECK(dialedOther().bakeHold != fresh.bakeHold); CHECK(dialedOther().bakeHold != before.bakeHold); CHECK(!sameCurve(before.velocityCurve, VelocityCurve::flat())); CHECK(!sameCurve(before.play.ampSpline.contour, VelocityCurve::rampDown())); CHECK(!sameCurve(dialedOther().play.ampSpline.contour, VelocityCurve::rampDown())); // --- SURVIVE: mapping facts, absent from the printed audio ---------------------- CHECK(after.rootOverride.has_value()); CHECK(after.rootOverride == before.rootOverride); CHECK(after.keyTrack == before.keyTrack); CHECK(after.keyTrack == 0.5); // and it is the dialed value, not the default 1.0 // --- The neutral, over both fixtures -------------------------------------------- g_ctx = "dialed"; checkNeutral(reset); g_ctx = "dialedOther"; { const BakeReset other = resetAfterBake(dialedOther()); checkNeutral(other); // The survivors travel from the OTHER fixture too, so the sweep above is agreeing // with a reset that read its input rather than one that ignores it wholesale. CHECK(other.params.rootOverride == before.rootOverride); CHECK(other.params.keyTrack == before.keyTrack); } g_ctx = ""; // --- A GATE-dialed instrument lands on Trigger too ------------------------------- // This is a reset to a chosen neutral, not the dialed value surviving. { InstrumentParams gated = dialed(); gated.play.playMode = PlayMode::Gate; CHECK(resetAfterBake(gated).params.play.playMode == PlayMode::Trigger); } // --- The loop enable and its points come back together --------------------------- // What the band shows after a bake is the reset override read against the newly banked // entry, which carries NO loop intrinsic (the shell records that pairing where it builds // the entry) — so the enable has nothing left to fall back to. { constexpr std::int64_t kFrames = 1000; const LoopMarks dialedMarks = resolveLoopMarks(StoredLoop{before.loopOverride, std::nullopt, before.loopCrossfadeFrames, before.startPoint}, kFrames); CHECK(dialedMarks.hasLoop); // the fixture really did dial a loop on… CHECK(!dialedMarks.parked); // …at its own positions CHECK(dialedMarks.crossfade == 512); const LoopMarks neutral = resolveLoopMarks(StoredLoop{after.loopOverride, std::nullopt, after.loopCrossfadeFrames, after.startPoint}, kFrames); CHECK(!neutral.hasLoop); CHECK(neutral.parked); // re-offered on the defaults, not left coincident CHECK(neutral.loopStart < neutral.loopEnd); CHECK(neutral.crossfade == 0); CHECK(neutral.start == 0); } // --- An absent root override stays absent (nothing is invented) ------------------ { InstrumentParams noRoot = dialed(); noRoot.rootOverride.reset(); CHECK(!resetAfterBake(noRoot).params.rootOverride.has_value()); } if (g_fail == 0) std::printf("bake_reset: all tests passed\n"); return g_fail ? 1 : 0; }