Bake window derives itself: %-knob fold, declick pad, Gate held to exhaustion, preview velocity; Hold is the one knob a loop needs
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
|
||||
using namespace reasampler;
|
||||
using namespace reasampler::instrument::map;
|
||||
namespace note = reasampler::instrument::note; // the bake Hold's ladder
|
||||
|
||||
static int failures = 0;
|
||||
|
||||
@@ -452,7 +453,7 @@ static void testGoldenFullBlobFixture() {
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,
|
||||
0x00,0x05,0x00,0x00,0x00,0x53,0x6e,0x61,0x72,0x65,0x13,0x00,0x00,0x00,0x67,0x75,
|
||||
0x69,0x64,0x2d,0x31,0x32,0x33,0x34,0x2d,0x35,0x36,0x37,0x38,0x2d,0x61,0x62,0x63,
|
||||
0x64,0x04,0x00,0x00,0x00,0x6b,0x69,0x63,0x6b,0x00,0xff,0xff,0xff,0x0d,0x00,0x00,
|
||||
0x64,0x04,0x00,0x00,0x00,0x6b,0x69,0x63,0x6b,0x00,0xff,0xff,0xff,0x0e,0x00,0x00,
|
||||
0x00,0x01,0x24,0x00,0x00,0x00,0x01,0x01,0xe8,0x03,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x88,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0xfa,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x01,0x9a,0x99,0x99,0x99,0x99,0x99,0xa9,0x3f,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
@@ -545,6 +546,9 @@ static void testGoldenFullBlobFixture() {
|
||||
0x03,0x00,0x00,0x00, 0x00,0x00,0x00, // amp curve hard flags (3 points)
|
||||
0x02,0x00,0x00,0x00, 0x00,0x00, // filter curve hard flags
|
||||
0x02,0x00,0x00,0x00, 0x00,0x00, // pitch curve hard flags
|
||||
// --- payload v14 bake Hold, at its one-bar default ---
|
||||
0x02,0x00,0x00,0x00, // quarterExponent 2 (== 1/1)
|
||||
0x00, // Straight
|
||||
};
|
||||
// clang-format on
|
||||
CHECK(bytes.size() == sizeof(kGolden));
|
||||
@@ -592,18 +596,19 @@ static void testEnvelopePrefixBytesFrozen() {
|
||||
CHECK(bytes[4] == 0); // ChannelMode::Mono
|
||||
}
|
||||
CHECK(kComponentStateVersion == 11);
|
||||
CHECK(kParamsPayloadVersion == 13);
|
||||
CHECK(kParamsPayloadVersion == 14);
|
||||
CHECK(kParamsSingleRecordVersion == 8);
|
||||
CHECK(kParamsFormatMarker == 0xFFFFFF00u);
|
||||
// The filter, staged-curve, loop, velocity and spline tails rode PAYLOAD bumps, not
|
||||
// envelope ones — the two axes stay independent, so a future envelope field cannot collide
|
||||
// with any of them on one number.
|
||||
// The filter, staged-curve, loop, velocity, spline and bake-Hold tails rode PAYLOAD bumps,
|
||||
// not envelope ones — the two axes stay independent, so a future envelope field cannot
|
||||
// collide with any of them on one number.
|
||||
CHECK(kParamsFilterVersion > kParamsSingleRecordVersion);
|
||||
CHECK(kParamsCurveVersion > kParamsFilterVersion);
|
||||
CHECK(kParamsLoopVersion > kParamsCurveVersion);
|
||||
CHECK(kParamsVelocityVersion > kParamsLoopVersion);
|
||||
CHECK(kParamsSplineVersion > kParamsVelocityVersion);
|
||||
CHECK(kParamsPayloadVersion == kParamsSplineVersion);
|
||||
CHECK(kParamsBakeHoldVersion > kParamsSplineVersion);
|
||||
CHECK(kParamsPayloadVersion == kParamsBakeHoldVersion);
|
||||
}
|
||||
|
||||
// --- The filter tail (payload v9) --------------------------------------------
|
||||
@@ -782,6 +787,19 @@ static void testNonFiniteAhdSecondsLiftToZero() {
|
||||
|
||||
// --- The v13 hard-flag tail: corruption must never widen past its own three curves -----------
|
||||
|
||||
// The two trailing blocks of a CURRENT blob, so the splice tests below can cut back to the
|
||||
// hard flags and rewrite them without hand-counting the payload twice. Every velocity curve
|
||||
// in those fixtures is at its default 2-point shape, which is what pins the flag block sizes.
|
||||
static constexpr std::size_t kHardFlagTailBytes = 4 + 2 + 4 + 2 + 4 + 2;
|
||||
static constexpr std::size_t kBakeHoldTailBytes = 4 + 1;
|
||||
|
||||
// The v14 tail at its one-bar default, re-appended after a splice so the record still ends
|
||||
// where the reader expects it to.
|
||||
static void putDefaultBakeHoldTail(std::vector<std::uint8_t>& out) {
|
||||
legacy::u32v(out, 2); // quarterExponent 2 == 1/1
|
||||
legacy::u8v(out, 0); // Straight
|
||||
}
|
||||
|
||||
// A hard-flag COUNT that disagrees with the curve fromPoints already built, but is still
|
||||
// IN-BOUNDS (the blob really does carry that many bytes) — the documented promise
|
||||
// (component_state_io.h) is that the tail is dropped, never misapplied, and nothing else in
|
||||
@@ -814,8 +832,8 @@ static void testV13HardFlagInBoundsMismatchDropsFlagsOnly() {
|
||||
// order in params_payload.cpp) is deterministic and this test can splice it exactly.
|
||||
|
||||
std::vector<std::uint8_t> bytes = serializeComponentState(in);
|
||||
CHECK(bytes.size() >= 18);
|
||||
bytes.resize(bytes.size() - 18); // drop the three well-formed 4+2-byte blocks
|
||||
CHECK(bytes.size() >= kHardFlagTailBytes + kBakeHoldTailBytes);
|
||||
bytes.resize(bytes.size() - kHardFlagTailBytes - kBakeHoldTailBytes);
|
||||
legacy::u32v(bytes, 5); // amp: bogus count...
|
||||
for (int i = 0; i < 5; ++i) legacy::u8v(bytes, 0); // ...with 5 REAL bytes, so nothing shifts
|
||||
legacy::u32v(bytes, 2); // filter: correct count, unchanged
|
||||
@@ -824,6 +842,7 @@ static void testV13HardFlagInBoundsMismatchDropsFlagsOnly() {
|
||||
legacy::u32v(bytes, 2); // pitch: correct count, unchanged
|
||||
legacy::u8v(bytes, 0);
|
||||
legacy::u8v(bytes, 0);
|
||||
putDefaultBakeHoldTail(bytes);
|
||||
|
||||
const ComponentState out = deserializeComponentState(bytes, 48000.0);
|
||||
// Every param preceding AND following the corrupted amp tail survives untouched.
|
||||
@@ -862,8 +881,8 @@ static void testV13HardFlagOutOfBoundsCountSurvivesWithoutWipingTheRecord() {
|
||||
in.params.loopCrossfadeFrames = 321;
|
||||
|
||||
std::vector<std::uint8_t> bytes = serializeComponentState(in);
|
||||
CHECK(bytes.size() >= 18);
|
||||
bytes.resize(bytes.size() - 18); // drop the three well-formed hard-flag blocks
|
||||
CHECK(bytes.size() >= kHardFlagTailBytes + kBakeHoldTailBytes);
|
||||
bytes.resize(bytes.size() - kHardFlagTailBytes - kBakeHoldTailBytes);
|
||||
legacy::u32v(bytes, 1000); // amp: a count its own tail cannot possibly carry
|
||||
// No amp flag bytes follow — bound-and-skip must consume none, so the well-formed
|
||||
// filter/pitch blocks right after it land exactly where they belong.
|
||||
@@ -873,6 +892,7 @@ static void testV13HardFlagOutOfBoundsCountSurvivesWithoutWipingTheRecord() {
|
||||
legacy::u32v(bytes, 2); // pitch: correct count, unchanged
|
||||
legacy::u8v(bytes, 0);
|
||||
legacy::u8v(bytes, 0);
|
||||
putDefaultBakeHoldTail(bytes);
|
||||
|
||||
const ComponentState out = deserializeComponentState(bytes, 48000.0);
|
||||
// The whole record survives — including everything the v13 section itself carries ahead of
|
||||
@@ -904,8 +924,10 @@ static void testV13HardFlagTailTruncatedMidCountSurvivesWithoutWipingTheRecord()
|
||||
in.params.loopCrossfadeFrames = 5;
|
||||
|
||||
std::vector<std::uint8_t> bytes = serializeComponentState(in);
|
||||
CHECK(bytes.size() >= 18);
|
||||
bytes.resize(bytes.size() - 18); // drop the three well-formed hard-flag blocks
|
||||
CHECK(bytes.size() >= kHardFlagTailBytes + kBakeHoldTailBytes);
|
||||
// Drops the bake-Hold tail with the flags: the truncation strands everything after it,
|
||||
// which is the whole point — Hold lifts to its default alongside the flags.
|
||||
bytes.resize(bytes.size() - kHardFlagTailBytes - kBakeHoldTailBytes);
|
||||
legacy::u8v(bytes, 0x02); // half of the amp tail's 4-byte LE count, then nothing
|
||||
legacy::u8v(bytes, 0x00);
|
||||
|
||||
@@ -918,6 +940,7 @@ static void testV13HardFlagTailTruncatedMidCountSurvivesWithoutWipingTheRecord()
|
||||
CHECK(out.params.loopCrossfadeFrames == 5);
|
||||
CHECK(out.params.velocityCurve.size() == 2);
|
||||
CHECK(!out.params.velocityCurve.points()[0].hard);
|
||||
CHECK(out.params.bakeHold == InstrumentParams{}.bakeHold);
|
||||
}
|
||||
|
||||
// --- The loop tail (payload v11) ---------------------------------------------
|
||||
@@ -1145,6 +1168,98 @@ static void testPreV12FilterVelocityLiftsAsAPureDomainReTag() {
|
||||
CHECK(back.params.play.filter.velAmount == -0.75);
|
||||
}
|
||||
|
||||
// --- The bake Hold tail (payload v14) -----------------------------------------
|
||||
|
||||
// Hold survives a save/reload as its {rung, modifier} pair, and it is the ONLY field the v14
|
||||
// bump touches — everything either side of it in the record comes back untouched.
|
||||
static void testBakeHoldRoundTripsAndDisturbsNothingElse() {
|
||||
ComponentState in;
|
||||
in.selectionId = "pad";
|
||||
in.params.rootOverride = 33;
|
||||
in.params.keyTrack = 0.25;
|
||||
in.params.loopCrossfadeFrames = 64;
|
||||
in.params.play.adsr.releaseSeconds = 0.31;
|
||||
in.params.play.ampSpline.mode = EnvMode::Spline;
|
||||
// A triplet on a rung well away from the default, so neither field can be read off the
|
||||
// other's default and still pass.
|
||||
in.params.bakeHold = note::makeDivision(-2, note::DivisionModifier::Triplet);
|
||||
|
||||
const ComponentState out =
|
||||
deserializeComponentState(serializeComponentState(in), 48000.0);
|
||||
CHECK(out.params.bakeHold == note::makeDivision(-2, note::DivisionModifier::Triplet));
|
||||
CHECK(out.params.bakeHold != InstrumentParams{}.bakeHold);
|
||||
CHECK(out.selectionId == "pad");
|
||||
CHECK(out.params.rootOverride && *out.params.rootOverride == 33);
|
||||
CHECK(out.params.keyTrack == 0.25);
|
||||
CHECK(out.params.loopCrossfadeFrames == 64);
|
||||
CHECK(out.params.play.adsr.releaseSeconds == 0.31);
|
||||
CHECK(out.params.play.ampSpline.mode == EnvMode::Spline);
|
||||
}
|
||||
|
||||
// A v13 blob is a strict PREFIX of v14, so it must lift to the one-bar Hold default with
|
||||
// every other field intact — the reason a project saved before Hold existed reopens the same.
|
||||
static void testV13BlobLiftsToTheDefaultHold() {
|
||||
ComponentState in;
|
||||
in.selectionId = "pad";
|
||||
in.params.rootOverride = 55;
|
||||
in.params.play.adsr.decaySeconds = 0.09;
|
||||
in.params.play.filter.enabled = true;
|
||||
in.params.loopCrossfadeFrames = 128;
|
||||
in.params.bakeHold = note::makeDivision(5, note::DivisionModifier::Dotted);
|
||||
|
||||
// Stamp the payload back to v13 and drop exactly the v14 tail: byte-for-byte what the
|
||||
// previous binary would have written.
|
||||
const std::vector<std::uint8_t> v13 =
|
||||
payloadDowngradedTo(in, kParamsSplineVersion, kBakeHoldTailBytes);
|
||||
const ComponentState out = deserializeComponentState(v13, 48000.0);
|
||||
CHECK(out.params.bakeHold == InstrumentParams{}.bakeHold);
|
||||
CHECK(out.selectionId == "pad");
|
||||
CHECK(out.params.rootOverride && *out.params.rootOverride == 55);
|
||||
CHECK(out.params.play.adsr.decaySeconds == 0.09);
|
||||
CHECK(out.params.play.filter.enabled);
|
||||
CHECK(out.params.loopCrossfadeFrames == 128);
|
||||
}
|
||||
|
||||
// A corrupt rung/modifier pair clamps to the nearest legal division rather than being held as
|
||||
// an unrepresentable one — makeDivision is the only door, and the codec goes through it.
|
||||
static void testBakeHoldCorruptPairClampsToTheLadder() {
|
||||
ComponentState in;
|
||||
in.selectionId = "pad";
|
||||
std::vector<std::uint8_t> bytes = serializeComponentState(in);
|
||||
CHECK(bytes.size() >= kBakeHoldTailBytes);
|
||||
bytes.resize(bytes.size() - kBakeHoldTailBytes);
|
||||
legacy::u32v(bytes, static_cast<std::uint32_t>(static_cast<std::int32_t>(9999)));
|
||||
legacy::u8v(bytes, 200); // an unnamed modifier byte
|
||||
|
||||
const ComponentState out = deserializeComponentState(bytes, 48000.0);
|
||||
CHECK(out.params.bakeHold ==
|
||||
note::makeDivision(note::kMaxQuarterExponent, note::DivisionModifier::Straight));
|
||||
}
|
||||
|
||||
// A blob truncated INSIDE the v14 tail costs the Hold alone. It sits last, so without the
|
||||
// revive a stray missing byte would reset every parameter ahead of it to defaults.
|
||||
static void testBakeHoldTruncatedTailSurvivesWithoutWipingTheRecord() {
|
||||
ComponentState in;
|
||||
in.selectionId = "pad";
|
||||
in.params.rootOverride = 71;
|
||||
in.params.play.adsr.attackSeconds = 0.017;
|
||||
in.params.loopCrossfadeFrames = 96;
|
||||
in.params.bakeHold = note::makeDivision(4, note::DivisionModifier::Triplet);
|
||||
|
||||
std::vector<std::uint8_t> bytes = serializeComponentState(in);
|
||||
CHECK(bytes.size() >= kBakeHoldTailBytes);
|
||||
bytes.resize(bytes.size() - kBakeHoldTailBytes);
|
||||
legacy::u8v(bytes, 0x02); // two of the exponent's four bytes, then nothing
|
||||
legacy::u8v(bytes, 0x00);
|
||||
|
||||
const ComponentState out = deserializeComponentState(bytes, 48000.0);
|
||||
CHECK(out.params.bakeHold == InstrumentParams{}.bakeHold);
|
||||
CHECK(out.selectionId == "pad");
|
||||
CHECK(out.params.rootOverride && *out.params.rootOverride == 71);
|
||||
CHECK(out.params.play.adsr.attackSeconds == 0.017);
|
||||
CHECK(out.params.loopCrossfadeFrames == 96);
|
||||
}
|
||||
|
||||
// The WRITER emits the CURRENT payload version, and the marker + version sit at the head of
|
||||
// the payload — the self-describing property every legacy branch depends on. Asserted
|
||||
// against the semantic constants, not literals.
|
||||
@@ -1632,11 +1747,11 @@ static void testSampleRefsTruncatedMidEntry() {
|
||||
// 91-byte play tail + keyTrack8 + curve(4+2*16, the flat 2-point default) + the 134-byte
|
||||
// v9 filter tail + the 152-byte v10 staged-curve tail + the 8-byte v11 crossfade + the
|
||||
// 36-byte v12 pitch curve + the 135-byte v13 dual-state tail, three 39-byte spline EGs and
|
||||
// three 6-byte hard-flag tails) = 623 bytes; entry two is 47 bytes (id 4+3, path 4+7,
|
||||
// root4, loop 1+8+8, channels4, name 4+0). Cutting 643 keeps the first 27 of entry two's
|
||||
// 47 — mid loop.start (offset 23..31).
|
||||
CHECK(bytes.size() > 643);
|
||||
bytes.resize(bytes.size() - 643);
|
||||
// three 6-byte hard-flag tails + the 5-byte v14 bake-Hold tail) = 628 bytes; entry two is
|
||||
// 47 bytes (id 4+3, path 4+7, root4, loop 1+8+8, channels4, name 4+0). Cutting 648 keeps
|
||||
// the first 27 of entry two's 47 — mid loop.start (offset 23..31).
|
||||
CHECK(bytes.size() > 648);
|
||||
bytes.resize(bytes.size() - 648);
|
||||
const ComponentState back = deserializeComponentState(bytes, 44100.0);
|
||||
CHECK(back.sampleRefs.size() == 1);
|
||||
CHECK(back.sampleRefs.size() == 1 && back.sampleRefs[0].sampleId == "kick");
|
||||
@@ -1748,6 +1863,10 @@ int main() {
|
||||
testV13HardFlagInBoundsMismatchDropsFlagsOnly();
|
||||
testV13HardFlagOutOfBoundsCountSurvivesWithoutWipingTheRecord();
|
||||
testV13HardFlagTailTruncatedMidCountSurvivesWithoutWipingTheRecord();
|
||||
testBakeHoldRoundTripsAndDisturbsNothingElse();
|
||||
testV13BlobLiftsToTheDefaultHold();
|
||||
testBakeHoldCorruptPairClampsToTheLadder();
|
||||
testBakeHoldTruncatedTailSurvivesWithoutWipingTheRecord();
|
||||
if (failures == 0) {
|
||||
std::printf("component_state_io_tests: all tests passed\n");
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user