Merge Γ-W3-T2: the bake's reset list verified complete by construction, and the limiter's exclusion from the printed master stage recorded

This commit is contained in:
2026-08-02 07:52:03 -04:00
5 changed files with 277 additions and 32 deletions
+6 -2
View File
@@ -21,8 +21,9 @@ decision about what the render made obsolete.
loop runs to `BakePlan::renderFrames()` and stops. That is why a Gate bake with a sustain loop runs to `BakePlan::renderFrames()` and stops. That is why a Gate bake with a sustain
loop active terminates: the gate is released at `noteOffFrame` so the tail is real, but loop active terminates: the gate is released at `noteOffFrame` so the tail is real, but
even a pathological envelope cannot run past the window. even a pathological envelope cannot run past the window.
- **The whole signal chain is printed, master gain included** — the gain multiply in - **The voice chain and master gain are printed; the limiter is not.** The gain multiply in
`bake_render.cpp` carries the argument for why. `bake_render.cpp` carries the argument for the gain, and `bake_reset.h` records where the
printed master stage stops.
- **A degenerate or unholdable window is refused, not rendered.** `planBake` refuses a - **A degenerate or unholdable window is refused, not rendered.** `planBake` refuses a
collapsed window, a non-positive rate, a window that rounds to no frames, and one past collapsed window, a non-positive rate, a window that rounds to no frames, and one past
`kMaxBakeFrames` — an unbounded window is a `bad_alloc` inside a UI tick, and the `kMaxBakeFrames` — an unbounded window is a `bad_alloc` inside a UI tick, and the
@@ -50,6 +51,9 @@ decision about what the render made obsolete.
- **Play mode resets to TRIGGER, not to the value struct's Gate default** — the one - **Play mode resets to TRIGGER, not to the value struct's Gate default** — the one
classification this track made against the ratified rule rather than reading off it. classification this track made against the ratified rule rather than reading off it.
`bake_reset.cpp` carries the argument at the assignment. `bake_reset.cpp` carries the argument at the assignment.
- **`kStageTimeMaxSeconds` (the stage-time ceiling `param_taper` owns) is not a reset-list
candidate at all** — it bounds a knob's taper, is never itself a dialed value, and so has
no disposition to classify against the ratified reset rule.
## Modules ## Modules
+3 -1
View File
@@ -17,4 +17,6 @@ reasampler_test(bake_window LINK bake_plan bake_render)
# sample_map carries InstrumentParams, which is the whole of what a reset rewrites. # sample_map carries InstrumentParams, which is the whole of what a reset rewrites.
reasampler_pure_library(bake_reset SOURCES bake_reset.cpp LINK PUBLIC sample_map) reasampler_pure_library(bake_reset SOURCES bake_reset.cpp LINK PUBLIC sample_map)
reasampler_test(bake_reset LINK bake_reset) # loop_marks is a TEST-only edge: it defines what a neutral loop looks like on the band, so
# the reset's loop assertions read it rather than restating it.
reasampler_test(bake_reset LINK bake_reset loop_marks)
+4
View File
@@ -13,6 +13,10 @@ namespace reasampler::instrument::bake {
// The two surfaces a bake resets. Master gain lives on the processor rather than in the // The two surfaces a bake resets. Master gain lives on the processor rather than in the
// parameter set; it is answered here because renderBake prints it into the file (see // parameter set; it is answered here because renderBake prints it into the file (see
// bake_render.cpp's gain multiply) rather than left to the shell. // bake_render.cpp's gain multiply) rather than left to the shell.
//
// Gain is the ONLY master-stage control the render prints — the limiter runs in the
// processor's block, off the bake path — so "the bake prints the gain" does not generalize
// to the master stage as a whole, and cannot be used to classify anything else on it.
struct BakeReset { struct BakeReset {
map::InstrumentParams params; map::InstrumentParams params;
double masterGainLinear = 1.0; // unity — renderBake printed the dialed gain double masterGainLinear = 1.0; // unity — renderBake printed the dialed gain
+39
View File
@@ -231,6 +231,45 @@ int main() {
CHECK(identical); CHECK(identical);
} }
// --- Regression baseline: the neutral render is the source, sample for sample --------
// A Trigger voice at its own root under Varispeed reads at ratio exactly 1 and hits no
// filter, so every printed frame equals its source frame PROVIDED the amp curve's gain at
// the plan's velocity is exactly 1.0 too (asserted below rather than assumed) — that exact
// value is a property of flat()'s two endpoints cancelling at velocity 100, not a
// guarantee of eval() at an arbitrary velocity. An added stage, a moved default, or a lost
// early-out anywhere in the chain moves a sample here.
{
SampleData s;
s.frames.resize(4000);
// A ramp, not DC: an off-by-one read or a reversed span is invisible in a constant.
for (std::size_t i = 0; i < s.frames.size(); ++i)
s.frames[i] = static_cast<float>(i) / 4000.f - 0.5f;
s.sampleRate = kRate;
s.rootNote = 60;
s.play.playMode = PlayMode::Trigger;
s.play.pitchEngine = PitchEngine::Varispeed;
// Shorter than the play span, so the window closes before any note-end shaping.
const BakePlan plan = planOf(/*total=*/1000, /*noteOn=*/0, /*noteOff=*/1000);
CHECK(s.velocityCurve.eval(100.0) == 1.0); // names the real cause if this ever fails
const BakeAudio audio = renderBake(s, plan, kUnity);
CHECK(audio.channelCount == 1);
CHECK(audio.frameCount() == 1000);
bool identity = audio.frameCount() == 1000;
for (std::size_t f = 0; identity && f < 1000; ++f)
identity = (audio.interleaved[f] == s.frames[f]);
CHECK(identity);
// …and the gain rides that as an exact scalar, which is the only other thing the
// render is permitted to do to the signal.
const BakeAudio halved = renderBake(s, plan, 0.5);
bool scaled = halved.frameCount() == 1000;
for (std::size_t f = 0; scaled && f < 1000; ++f)
scaled = (halved.interleaved[f] == s.frames[f] * 0.5f);
CHECK(scaled);
}
// --- Refusals ----------------------------------------------------------------------- // --- Refusals -----------------------------------------------------------------------
{ {
SampleData empty; // nothing decoded SampleData empty; // nothing decoded
+225 -29
View File
@@ -1,23 +1,35 @@
// Standalone tests for reasampler::instrument::bake::bake_reset — no VST3, no REAPER, no // Standalone tests for reasampler::instrument::bake::bake_reset — no VST3, no REAPER, no
// framework. Same fast assert loop as the sibling pure tests. // framework. Same fast assert loop as the sibling pure tests.
// //
// Covers the ratified reset scope PER PARAMETER, in both directions: every control whose // The ratified reset scope itself is bake/CLAUDE.md's; this sweep checks it field by field
// effect the render printed comes back at its default, and every mapping fact comes back // (never struct equality, which would pass while silently resetting a survivor) over TWO
// untouched. Asserted field by field rather than by struct equality on purpose — a // independently dialed fixtures, so a pass is a property of the survivors, not of one input.
// whole-struct compare would pass while silently resetting a survivor, or vice versa.
#include "../src/core/instrument/bake/bake_reset.h" #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 <cstdint>
#include <cstdio> #include <cstdio>
#include <optional>
using namespace reasampler; using namespace reasampler;
using namespace reasampler::instrument::bake; using namespace reasampler::instrument::bake;
using reasampler::instrument::map::InstrumentParams; using reasampler::instrument::map::InstrumentParams;
using reasampler::instrument::map::PlaySeconds; 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 int g_fail = 0;
static const char* g_ctx = "";
#define CHECK(cond) do { if(!(cond)) { \ #define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) std::printf("FAIL [%s] line %d: %s\n", g_ctx, __LINE__, #cond); ++g_fail; } } while(0)
namespace { namespace {
@@ -45,24 +57,118 @@ InstrumentParams dialed() {
p.play.trigAhd.decaySeconds = 0.22; p.play.trigAhd.decaySeconds = 0.22;
p.play.trigAhd.holdFraction = 0.33; p.play.trigAhd.holdFraction = 0.33;
p.play.trigAhd.attackCurve = 1.7; p.play.trigAhd.attackCurve = 1.7;
p.play.trigAhd.decayCurve = 2.2;
p.play.pitchEngine = PitchEngine::Varispeed; p.play.pitchEngine = PitchEngine::Varispeed;
p.play.pitchEnv.enabled = true; p.play.pitchEnv.enabled = true;
p.play.pitchEnv.peakSemitones = -7.0; p.play.pitchEnv.peakSemitones = -7.0;
p.play.pitchEnv.shape.attackSeconds = 0.05; p.play.pitchEnv.shape.attackSeconds = 0.05;
p.play.pitchEnv.shape.decaySeconds = 0.15;
p.play.pitchEnv.shape.holdFraction = 0.6;
p.play.pitchEnv.shape.attackCurve = 2.1;
p.play.pitchEnv.shape.decayCurve = 0.4;
p.play.pitchVelocityCurve = VelocityCurve::linear(); p.play.pitchVelocityCurve = VelocityCurve::linear();
p.play.playRate = 0.5; p.play.playRate = 0.5;
p.play.pitchOffsetSemitones = -7.5; p.play.pitchOffsetSemitones = -7.5;
p.play.filter.enabled = true; 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.modAmount = -0.8;
p.play.filter.velAmount = 0.6; p.play.filter.velAmount = 0.6;
p.play.filter.keyTrack = 1.5; p.play.filter.keyTrack = 1.5;
p.play.filter.env.attackSeconds = 0.7; p.play.filter.env.attackSeconds = 0.7;
p.play.filter.env.holdSeconds = 0.15;
p.play.filter.env.decaySeconds = 0.35;
p.play.filter.env.sustainLevel = 0.25;
p.play.filter.env.releaseSeconds = 0.55;
p.play.filter.env.attackCurve = 2.3;
p.play.filter.env.decayCurve = 0.5;
p.play.filter.env.releaseCurve = 2.8;
p.play.filter.trigEnv.attackSeconds = 0.12;
p.play.filter.trigEnv.decaySeconds = 0.8; p.play.filter.trigEnv.decaySeconds = 0.8;
p.play.filter.trigEnv.holdFraction = 0.44;
p.play.filter.trigEnv.attackCurve = 1.6;
p.play.filter.trigEnv.decayCurve = 0.6;
p.play.filter.velocityCurve = VelocityCurve::linear(); p.play.filter.velocityCurve = VelocityCurve::linear();
p.play.ampSpline.mode = EnvMode::Spline; p.play.ampSpline.mode = EnvMode::Spline;
p.play.ampSpline.contour = VelocityCurve::linear(); p.play.ampSpline.contour = VelocityCurve::linear();
p.play.pitchSpline.mode = EnvMode::Spline; p.play.pitchSpline.mode = EnvMode::Spline;
p.play.pitchSpline.contour = VelocityCurve::linear();
p.play.filterSpline.mode = EnvMode::Spline; 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. The inherent limit this does NOT cover: nothing forces a newly added
// `InstrumentParams` field to be dialled in either fixture at all, let alone asserted.
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.pitchEnv.shape.holdFraction = 0.2;
p.play.pitchEnv.shape.attackCurve = 0.5;
p.play.pitchEnv.shape.decayCurve = 2.4;
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.env.holdSeconds = 0.95;
p.play.filter.env.decaySeconds = 0.75;
p.play.filter.env.sustainLevel = 0.85;
p.play.filter.env.releaseSeconds = 0.15;
p.play.filter.env.attackCurve = 0.4;
p.play.filter.env.decayCurve = 2.9;
p.play.filter.env.releaseCurve = 0.35;
p.play.filter.trigEnv.attackSeconds = 0.62;
p.play.filter.trigEnv.decaySeconds = 1.8;
p.play.filter.trigEnv.holdFraction = 0.77;
p.play.filter.trigEnv.attackCurve = 0.3;
p.play.filter.trigEnv.decayCurve = 2.5;
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; return p;
} }
@@ -75,22 +181,13 @@ bool sameCurve(const VelocityCurve& a, const VelocityCurve& b) {
return true; return true;
} }
} // namespace // THE reset neutral, field by field. Run against every fixture, so the two can never be
// asserted against two different notions of neutral.
int main() { void checkNeutral(const BakeReset& reset) {
const InstrumentParams before = dialed();
const BakeReset reset = resetAfterBake(before);
const InstrumentParams& after = reset.params; const InstrumentParams& after = reset.params;
const InstrumentParams fresh; // the defaults every reset control must land on const InstrumentParams fresh; // the defaults every reset control must land on
const PlaySeconds freshPlay; const PlaySeconds freshPlay;
// --- 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
CHECK(fresh.keyTrack != before.keyTrack); // the fixture really did move it
// --- RESET: loop points, start point, crossfade --------------------------------- // --- RESET: loop points, start point, crossfade ---------------------------------
CHECK(!after.loopOverride.has_value()); CHECK(!after.loopOverride.has_value());
CHECK(!after.startPoint.has_value()); CHECK(!after.startPoint.has_value());
@@ -98,26 +195,17 @@ int main() {
// --- RESET: the velocity transfer curves ---------------------------------------- // --- RESET: the velocity transfer curves ----------------------------------------
CHECK(sameCurve(after.velocityCurve, VelocityCurve::flat())); CHECK(sameCurve(after.velocityCurve, VelocityCurve::flat()));
CHECK(!sameCurve(after.velocityCurve, before.velocityCurve));
CHECK(sameCurve(after.play.pitchVelocityCurve, VelocityCurve::zero())); CHECK(sameCurve(after.play.pitchVelocityCurve, VelocityCurve::zero()));
CHECK(sameCurve(after.play.filter.velocityCurve, VelocityCurve::zero())); CHECK(sameCurve(after.play.filter.velocityCurve, VelocityCurve::zero()));
// --- RESET: play mode, to TRIGGER rather than to the struct's Gate default ------- // --- RESET: play mode, to TRIGGER rather than to the struct's Gate default -------
// The bake's product is a finished one-shot; Trigger plays it back verbatim, Gate would // bake_reset.cpp carries the argument at the assignment.
// re-gate its printed release tail and each iteration would truncate the last one's.
CHECK(after.play.playMode == PlayMode::Trigger); CHECK(after.play.playMode == PlayMode::Trigger);
CHECK(freshPlay.playMode == PlayMode::Gate); // and that really is NOT the default 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. // 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.trigger.lengthFraction == 1.0);
CHECK(after.play.trigAhd.attackSeconds == 0.0); CHECK(after.play.trigAhd.attackSeconds == 0.0);
CHECK(after.play.trigAhd.decaySeconds == 0.0); CHECK(after.play.trigAhd.decaySeconds == 0.0);
{
// …and a GATE-dialed instrument lands there 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);
}
// --- RESET: the amp envelope, staged, every stage and every curve exponent ------ // --- RESET: the amp envelope, staged, every stage and every curve exponent ------
CHECK(after.play.adsr.attackSeconds == freshPlay.adsr.attackSeconds); CHECK(after.play.adsr.attackSeconds == freshPlay.adsr.attackSeconds);
@@ -133,6 +221,7 @@ int main() {
CHECK(after.play.trigAhd.decaySeconds == freshPlay.trigAhd.decaySeconds); CHECK(after.play.trigAhd.decaySeconds == freshPlay.trigAhd.decaySeconds);
CHECK(after.play.trigAhd.holdFraction == freshPlay.trigAhd.holdFraction); CHECK(after.play.trigAhd.holdFraction == freshPlay.trigAhd.holdFraction);
CHECK(after.play.trigAhd.attackCurve == freshPlay.trigAhd.attackCurve); CHECK(after.play.trigAhd.attackCurve == freshPlay.trigAhd.attackCurve);
CHECK(after.play.trigAhd.decayCurve == freshPlay.trigAhd.decayCurve);
// --- RESET: pitch engine + pitch envelope --------------------------------------- // --- RESET: pitch engine + pitch envelope ---------------------------------------
CHECK(after.play.pitchEngine == freshPlay.pitchEngine); CHECK(after.play.pitchEngine == freshPlay.pitchEngine);
@@ -140,6 +229,10 @@ int main() {
CHECK(!after.play.pitchEnv.enabled); CHECK(!after.play.pitchEnv.enabled);
CHECK(after.play.pitchEnv.peakSemitones == 0.0); CHECK(after.play.pitchEnv.peakSemitones == 0.0);
CHECK(after.play.pitchEnv.shape.attackSeconds == freshPlay.pitchEnv.shape.attackSeconds); CHECK(after.play.pitchEnv.shape.attackSeconds == freshPlay.pitchEnv.shape.attackSeconds);
CHECK(after.play.pitchEnv.shape.decaySeconds == freshPlay.pitchEnv.shape.decaySeconds);
CHECK(after.play.pitchEnv.shape.holdFraction == freshPlay.pitchEnv.shape.holdFraction);
CHECK(after.play.pitchEnv.shape.attackCurve == freshPlay.pitchEnv.shape.attackCurve);
CHECK(after.play.pitchEnv.shape.decayCurve == freshPlay.pitchEnv.shape.decayCurve);
// --- RESET: Rate and the baseline Pitch offset ----------------------------------- // --- RESET: Rate and the baseline Pitch offset -----------------------------------
// Both are processing the bake already printed, so the whitelist leaves them at their // Both are processing the bake already printed, so the whitelist leaves them at their
@@ -150,13 +243,29 @@ int main() {
CHECK(after.play.playRate == freshPlay.playRate); CHECK(after.play.playRate == freshPlay.playRate);
CHECK(after.play.pitchOffsetSemitones == freshPlay.pitchOffsetSemitones); CHECK(after.play.pitchOffsetSemitones == freshPlay.pitchOffsetSemitones);
// --- RESET: the filter, including its velocity/key-tracking mod ----------------- // --- RESET: the filter, its control positions, and its velocity/key-tracking mod --
CHECK(!after.play.filter.enabled); CHECK(!after.play.filter.enabled);
CHECK(after.play.filter.modAmount == 0.0); CHECK(after.play.filter.modAmount == 0.0);
CHECK(after.play.filter.velAmount == 0.0); CHECK(after.play.filter.velAmount == 0.0);
CHECK(after.play.filter.keyTrack == 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.env.attackSeconds == freshPlay.filter.env.attackSeconds);
CHECK(after.play.filter.env.holdSeconds == freshPlay.filter.env.holdSeconds);
CHECK(after.play.filter.env.decaySeconds == freshPlay.filter.env.decaySeconds);
CHECK(after.play.filter.env.sustainLevel == freshPlay.filter.env.sustainLevel);
CHECK(after.play.filter.env.releaseSeconds == freshPlay.filter.env.releaseSeconds);
CHECK(after.play.filter.env.attackCurve == freshPlay.filter.env.attackCurve);
CHECK(after.play.filter.env.decayCurve == freshPlay.filter.env.decayCurve);
CHECK(after.play.filter.env.releaseCurve == freshPlay.filter.env.releaseCurve);
CHECK(after.play.filter.trigEnv.attackSeconds == freshPlay.filter.trigEnv.attackSeconds);
CHECK(after.play.filter.trigEnv.decaySeconds == freshPlay.filter.trigEnv.decaySeconds); CHECK(after.play.filter.trigEnv.decaySeconds == freshPlay.filter.trigEnv.decaySeconds);
CHECK(after.play.filter.trigEnv.holdFraction == freshPlay.filter.trigEnv.holdFraction);
CHECK(after.play.filter.trigEnv.attackCurve == freshPlay.filter.trigEnv.attackCurve);
CHECK(after.play.filter.trigEnv.decayCurve == freshPlay.filter.trigEnv.decayCurve);
// --- RESET: the three spline contours AND their mode flags ---------------------- // --- 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 // The flag selects which shape ran, so the shape it selected is in the audio; with
@@ -165,10 +274,97 @@ int main() {
CHECK(after.play.pitchSpline.mode == EnvMode::Staged); CHECK(after.play.pitchSpline.mode == EnvMode::Staged);
CHECK(after.play.filterSpline.mode == EnvMode::Staged); CHECK(after.play.filterSpline.mode == EnvMode::Staged);
CHECK(sameCurve(after.play.ampSpline.contour, VelocityCurve::rampDown())); CHECK(sameCurve(after.play.ampSpline.contour, VelocityCurve::rampDown()));
CHECK(!sameCurve(after.play.ampSpline.contour, before.play.ampSpline.contour)); 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 ---------------------------------------------------------- // --- RESET: master gain ----------------------------------------------------------
CHECK(reset.masterGainLinear == 1.0); 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;
// A sample of the fields checkNeutral asserts, confirming the fixtures actually moved them
// off default — not the whole sweep, but enough spot checks that a fixture regressing to
// the defaults (making the sweep vacuous) would show here first.
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's loop intrinsic — assumed std::nullopt below, which is what the shell lands
// today; if it ever populated one, the enable would have something to fall back to and
// this assumption, not just this test, would need revisiting.
{
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) ------------------ // --- An absent root override stays absent (nothing is invented) ------------------
{ {