Bake window: derived note lengths carry exact durations, not ladder rungs — a long take is no longer cut at 384 beats
Hold keeps its picker. Also: one home for the %-fold, duration-ordered Hold travel, and a corrupt tail degrades to absent rather than fabricating one.
This commit is contained in:
@@ -793,11 +793,15 @@ static void testNonFiniteAhdSecondsLiftToZero() {
|
||||
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.
|
||||
// The v14 tail, re-appended after a splice so the record still ends where the reader expects.
|
||||
static void putBakeHoldTail(std::vector<std::uint8_t>& out, int quarterExponent,
|
||||
note::DivisionModifier modifier) {
|
||||
legacy::u32v(out, static_cast<std::uint32_t>(static_cast<std::int32_t>(quarterExponent)));
|
||||
legacy::u8v(out, static_cast<std::uint8_t>(modifier));
|
||||
}
|
||||
|
||||
static void putDefaultBakeHoldTail(std::vector<std::uint8_t>& out) {
|
||||
legacy::u32v(out, 2); // quarterExponent 2 == 1/1
|
||||
legacy::u8v(out, 0); // Straight
|
||||
putBakeHoldTail(out, 2, note::DivisionModifier::Straight); // 1/1, the field's default
|
||||
}
|
||||
|
||||
// A hard-flag COUNT that disagrees with the curve fromPoints already built, but is still
|
||||
@@ -866,11 +870,10 @@ static void testV13HardFlagInBoundsMismatchDropsFlagsOnly() {
|
||||
CHECK(out.params.velocityCurve.equals(reasampler::instrument::engine::VelocityCurve::flat()));
|
||||
}
|
||||
|
||||
// A hard-flag COUNT that exceeds what its OWN tail carries — a genuinely corrupt/out-of-bounds
|
||||
// count — must be BOUND-AND-SKIPPED without consuming any of the following bytes, so the
|
||||
// FILTER/PITCH tails immediately after the AMP block still parse at their correct offset. The
|
||||
// old behavior (r.ok = false) reset the ENTIRE params record to defaults on this path, which is
|
||||
// strictly worse than the documented "drops only the hard points" promise.
|
||||
// A hard-flag COUNT that exceeds what its own tail carries is a genuinely corrupt count: the
|
||||
// record parsed AHEAD of it survives (never the old "reset everything to defaults"), and the
|
||||
// stream is drained rather than guessed at — see the stranding test below for why guessing is
|
||||
// worse. Here nothing follows that the drain can cost, so the two behaviours coincide.
|
||||
static void testV13HardFlagOutOfBoundsCountSurvivesWithoutWipingTheRecord() {
|
||||
ComponentState in;
|
||||
in.selectionId = "pad";
|
||||
@@ -884,20 +887,12 @@ static void testV13HardFlagOutOfBoundsCountSurvivesWithoutWipingTheRecord() {
|
||||
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.
|
||||
legacy::u32v(bytes, 2); // filter: correct count, unchanged
|
||||
legacy::u8v(bytes, 0);
|
||||
legacy::u8v(bytes, 0);
|
||||
legacy::u32v(bytes, 2); // pitch: correct count, unchanged
|
||||
legacy::u8v(bytes, 0);
|
||||
legacy::u8v(bytes, 0);
|
||||
putDefaultBakeHoldTail(bytes);
|
||||
// …and nothing at all after it, so the blob simply ends inside the v13 tail.
|
||||
|
||||
const ComponentState out = deserializeComponentState(bytes, 48000.0);
|
||||
// The whole record survives — including everything the v13 section itself carries ahead of
|
||||
// the hard-flag tail (the three spline EGs) and the two well-formed tails after the
|
||||
// corrupted one — only the AMP curve's hard-flag application is lost.
|
||||
// the hard-flag tail (the three spline EGs). Only the hard-flag applications and the tail
|
||||
// that never arrived are lost.
|
||||
CHECK(out.selectionId == "pad");
|
||||
CHECK(out.params.rootOverride && *out.params.rootOverride == 44);
|
||||
CHECK(out.params.play.adsr.releaseSeconds == 0.44);
|
||||
@@ -907,6 +902,82 @@ static void testV13HardFlagOutOfBoundsCountSurvivesWithoutWipingTheRecord() {
|
||||
CHECK(out.params.play.ampSpline.mode == EnvMode::Staged);
|
||||
CHECK(out.params.velocityCurve.size() == 2); // unaffected: not misapplied, not discarded
|
||||
CHECK(!out.params.velocityCurve.points()[0].hard);
|
||||
CHECK(out.params.bakeHold == InstrumentParams{}.bakeHold);
|
||||
}
|
||||
|
||||
// The stranding case, and the reason a bogus count DRAINS rather than skipping in place: the
|
||||
// blob keeps going after the corrupt block, so "skip nothing and read on" starts every later
|
||||
// tail mid-block. The bake Hold is the tail that makes it visible — it CLAMPS whatever it
|
||||
// reads, so a misaligned read installs a legal-looking division rather than failing loudly.
|
||||
// The bar is that it degrades to ABSENT (the field's own default), never to a fabricated value
|
||||
// — and in particular never to the top rung the misread count used to clamp to.
|
||||
static void testV13HardFlagCountThatStrandsAlignmentLeavesTheHoldAbsentNotFabricated() {
|
||||
ComponentState in;
|
||||
in.selectionId = "pad";
|
||||
in.params.rootOverride = 44;
|
||||
in.params.play.adsr.releaseSeconds = 0.44;
|
||||
in.params.loopCrossfadeFrames = 321;
|
||||
in.params.bakeHold = note::makeDivision(-2, note::DivisionModifier::Triplet);
|
||||
|
||||
// A THREE-point amp curve, so its flag block is three bytes rather than two: the
|
||||
// misaligned reads below then land on bytes that decode to something other than the
|
||||
// default, which is what makes "absent" and "fabricated" distinguishable at all.
|
||||
in.params.velocityCurve = reasampler::instrument::engine::VelocityCurve::fromPoints(
|
||||
{VelocityPoint{0.0, 0.0}, VelocityPoint{64.0, 0.5, /*hard=*/true},
|
||||
VelocityPoint{127.0, 1.0}},
|
||||
reasampler::instrument::engine::CurveDomain::Unipolar);
|
||||
|
||||
// A REAL blob with exactly ONE corrupt field: the amp hard-flag count, patched in place.
|
||||
// Everything after it — the amp flags, both well-formed neighbour blocks, and the Hold —
|
||||
// is exactly what the serializer wrote, which is the whole hazard.
|
||||
constexpr std::size_t kThreePointFlagTail = (4 + 3) + (4 + 2) + (4 + 2);
|
||||
std::vector<std::uint8_t> bytes = serializeComponentState(in);
|
||||
CHECK(bytes.size() >= kThreePointFlagTail + kBakeHoldTailBytes);
|
||||
const std::size_t ampCountAt = bytes.size() - kThreePointFlagTail - kBakeHoldTailBytes;
|
||||
for (std::size_t i = 0; i < 4; ++i) bytes[ampCountAt + i] = i == 0 ? 0x00 : 0xFF;
|
||||
|
||||
const ComponentState out = deserializeComponentState(bytes, 48000.0);
|
||||
CHECK(out.selectionId == "pad");
|
||||
CHECK(out.params.rootOverride && *out.params.rootOverride == 44);
|
||||
CHECK(out.params.play.adsr.releaseSeconds == 0.44);
|
||||
CHECK(out.params.loopCrossfadeFrames == 321);
|
||||
CHECK(out.params.play.ampSpline.mode == EnvMode::Staged);
|
||||
// Absent, not the stored value (its tail is past the damage and cannot be trusted)…
|
||||
CHECK(out.params.bakeHold == InstrumentParams{}.bakeHold);
|
||||
CHECK(out.params.bakeHold != note::makeDivision(-2, note::DivisionModifier::Triplet));
|
||||
// …and above all not the division a misaligned read manufactures: the flag bytes read as
|
||||
// the next count, and the next-but-one block's bytes read as the Hold, whose exponent
|
||||
// clamps to the top rung.
|
||||
CHECK(out.params.bakeHold !=
|
||||
note::makeDivision(note::kMaxQuarterExponent, note::DivisionModifier::Straight));
|
||||
}
|
||||
|
||||
// Numeric domains are established at the DOOR, not at each consumer. A NaN pitch depth reaches
|
||||
// the bake's pow() and the voice's ratio multiply; a NaN %-length and a NaN stage time reach
|
||||
// narrowing casts that are undefined on one; and a root override outside MIDI range makes the
|
||||
// bake's render note and the sample's own root disagree, which is a read rate other than 1 and
|
||||
// therefore a window sized in the truncating direction.
|
||||
static void testOutOfDomainWireValuesAreBoundedAtTheCodec() {
|
||||
const PlaySeconds defaults;
|
||||
ComponentState in;
|
||||
in.selectionId = "pad";
|
||||
in.params.keyTrack = 0.5; // a neighbouring field, to show the guards are per-field
|
||||
in.params.rootOverride = 9999;
|
||||
in.params.play.pitchEnv.peakSemitones = std::numeric_limits<double>::quiet_NaN();
|
||||
in.params.play.trigger.lengthFraction = std::numeric_limits<double>::quiet_NaN();
|
||||
in.params.play.adsr.releaseSeconds = std::numeric_limits<double>::infinity();
|
||||
|
||||
const ComponentState out = deserializeComponentState(serializeComponentState(in), 48000.0);
|
||||
CHECK(out.params.rootOverride && *out.params.rootOverride == 127);
|
||||
CHECK(out.params.play.pitchEnv.peakSemitones == defaults.pitchEnv.peakSemitones);
|
||||
CHECK(out.params.play.trigger.lengthFraction == defaults.trigger.lengthFraction);
|
||||
CHECK(out.params.play.adsr.releaseSeconds == defaults.adsr.releaseSeconds);
|
||||
CHECK(out.params.keyTrack == 0.5);
|
||||
|
||||
ComponentState low = in;
|
||||
low.params.rootOverride = -5;
|
||||
const ComponentState lowOut = deserializeComponentState(serializeComponentState(low), 48000.0);
|
||||
CHECK(lowOut.params.rootOverride && *lowOut.params.rootOverride == 0);
|
||||
}
|
||||
|
||||
// A hard-flag tail truncated mid-COUNT-FIELD (only 2 of its 4 length bytes present, and
|
||||
@@ -1862,6 +1933,8 @@ int main() {
|
||||
testNonFiniteAhdSecondsLiftToZero();
|
||||
testV13HardFlagInBoundsMismatchDropsFlagsOnly();
|
||||
testV13HardFlagOutOfBoundsCountSurvivesWithoutWipingTheRecord();
|
||||
testV13HardFlagCountThatStrandsAlignmentLeavesTheHoldAbsentNotFabricated();
|
||||
testOutOfDomainWireValuesAreBoundedAtTheCodec();
|
||||
testV13HardFlagTailTruncatedMidCountSurvivesWithoutWipingTheRecord();
|
||||
testBakeHoldRoundTripsAndDisturbsNothingElse();
|
||||
testV13BlobLiftsToTheDefaultHold();
|
||||
|
||||
Reference in New Issue
Block a user