Γ-W1-T2: the master bus — a true-peak limiter whose ceiling is a theorem, the meter's published half, and the plugin's first PDC report

This commit is contained in:
2026-08-01 19:05:57 -04:00
parent 4fa021edae
commit 3baf4ee50b
17 changed files with 1187 additions and 65 deletions
+140 -32
View File
@@ -453,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,0x0e,0x00,0x00,
0x64,0x04,0x00,0x00,0x00,0x6b,0x69,0x63,0x6b,0x00,0xff,0xff,0xff,0x0f,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,
@@ -549,6 +549,8 @@ static void testGoldenFullBlobFixture() {
// --- payload v14 bake Hold, at its one-bar default ---
0x02,0x00,0x00,0x00, // quarterExponent 2 (== 1/1)
0x00, // Straight
// --- payload v15 limiter enable ---
0x00, // bypassed (the default)
};
// clang-format on
CHECK(bytes.size() == sizeof(kGolden));
@@ -596,19 +598,22 @@ static void testEnvelopePrefixBytesFrozen() {
CHECK(bytes[4] == 0); // ChannelMode::Mono
}
CHECK(kComponentStateVersion == 11);
CHECK(kParamsPayloadVersion == 14);
CHECK(kParamsPayloadVersion == 15);
CHECK(kParamsSingleRecordVersion == 8);
CHECK(kParamsFormatMarker == 0xFFFFFF00u);
// 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.
// The filter, staged-curve, loop, velocity, spline, bake-Hold and limiter 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. This pins the NUMBERS only; that
// each tail's bytes sit in the order its number implies is
// testAppendedTailsSitInVersionOrderOnTheWire's job.
CHECK(kParamsFilterVersion > kParamsSingleRecordVersion);
CHECK(kParamsCurveVersion > kParamsFilterVersion);
CHECK(kParamsLoopVersion > kParamsCurveVersion);
CHECK(kParamsVelocityVersion > kParamsLoopVersion);
CHECK(kParamsSplineVersion > kParamsVelocityVersion);
CHECK(kParamsBakeHoldVersion > kParamsSplineVersion);
CHECK(kParamsPayloadVersion == kParamsBakeHoldVersion);
CHECK(kParamsLimiterVersion > kParamsBakeHoldVersion);
CHECK(kParamsPayloadVersion == kParamsLimiterVersion);
}
// --- The filter tail (payload v9) --------------------------------------------
@@ -787,21 +792,28 @@ 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
// The three 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;
static constexpr std::size_t kLimiterTailBytes = 1;
// The v14 tail, re-appended after a splice so the record still ends where the reader expects.
// The v14/v15 tails, re-appended after a splice so the record still ends where the reader
// expects. They go back in wire order: Hold first, then the limiter byte.
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) {
static void putLimiterTail(std::vector<std::uint8_t>& out, bool enabled) {
legacy::u8v(out, enabled ? 1 : 0);
}
static void putDefaultTrailingTails(std::vector<std::uint8_t>& out) {
putBakeHoldTail(out, 2, note::DivisionModifier::Straight); // 1/1, the field's default
putLimiterTail(out, false); // bypassed, the field's default
}
// A hard-flag COUNT that disagrees with the curve fromPoints already built, but is still
@@ -836,8 +848,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() >= kHardFlagTailBytes + kBakeHoldTailBytes);
bytes.resize(bytes.size() - kHardFlagTailBytes - kBakeHoldTailBytes);
CHECK(bytes.size() >= kHardFlagTailBytes + kBakeHoldTailBytes + kLimiterTailBytes);
bytes.resize(bytes.size() - kHardFlagTailBytes - kBakeHoldTailBytes - kLimiterTailBytes);
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
@@ -846,7 +858,7 @@ static void testV13HardFlagInBoundsMismatchDropsFlagsOnly() {
legacy::u32v(bytes, 2); // pitch: correct count, unchanged
legacy::u8v(bytes, 0);
legacy::u8v(bytes, 0);
putDefaultBakeHoldTail(bytes);
putDefaultTrailingTails(bytes);
const ComponentState out = deserializeComponentState(bytes, 48000.0);
// Every param preceding AND following the corrupted amp tail survives untouched.
@@ -884,8 +896,8 @@ static void testV13HardFlagOutOfBoundsCountSurvivesWithoutWipingTheRecord() {
in.params.loopCrossfadeFrames = 321;
std::vector<std::uint8_t> bytes = serializeComponentState(in);
CHECK(bytes.size() >= kHardFlagTailBytes + kBakeHoldTailBytes);
bytes.resize(bytes.size() - kHardFlagTailBytes - kBakeHoldTailBytes);
CHECK(bytes.size() >= kHardFlagTailBytes + kBakeHoldTailBytes + kLimiterTailBytes);
bytes.resize(bytes.size() - kHardFlagTailBytes - kBakeHoldTailBytes - kLimiterTailBytes);
legacy::u32v(bytes, 1000); // amp: a count its own tail cannot possibly carry
// …and nothing at all after it, so the blob simply ends inside the v13 tail.
@@ -903,6 +915,7 @@ static void testV13HardFlagOutOfBoundsCountSurvivesWithoutWipingTheRecord() {
CHECK(out.params.velocityCurve.size() == 2); // unaffected: not misapplied, not discarded
CHECK(!out.params.velocityCurve.points()[0].hard);
CHECK(out.params.bakeHold == InstrumentParams{}.bakeHold);
CHECK(!out.params.limiterEnabled);
}
// The stranding case, and the reason a bogus count DRAINS rather than skipping in place: the
@@ -918,6 +931,9 @@ static void testV13HardFlagCountThatStrandsAlignmentLeavesTheHoldAbsentNotFabric
in.params.play.adsr.releaseSeconds = 0.44;
in.params.loopCrossfadeFrames = 321;
in.params.bakeHold = note::makeDivision(-2, note::DivisionModifier::Triplet);
// Enabled on the in-state so the drain has something to cost on the LAST tail too: a drain
// that stopped short of it would hand back the stored `true` off bytes it cannot trust.
in.params.limiterEnabled = true;
// 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
@@ -928,12 +944,13 @@ static void testV13HardFlagCountThatStrandsAlignmentLeavesTheHoldAbsentNotFabric
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.
// Everything after it — the amp flags, both well-formed neighbour blocks, the Hold and the
// limiter byte — is exactly what the serializer wrote, which is the whole hazard.
constexpr std::size_t kThreePointFlagTail = (4 + 3) + (4 + 2) + (4 + 2);
constexpr std::size_t kTrailingTails = kBakeHoldTailBytes + kLimiterTailBytes;
std::vector<std::uint8_t> bytes = serializeComponentState(in);
CHECK(bytes.size() >= kThreePointFlagTail + kBakeHoldTailBytes);
const std::size_t ampCountAt = bytes.size() - kThreePointFlagTail - kBakeHoldTailBytes;
CHECK(bytes.size() >= kThreePointFlagTail + kTrailingTails);
const std::size_t ampCountAt = bytes.size() - kThreePointFlagTail - kTrailingTails;
for (std::size_t i = 0; i < 4; ++i) bytes[ampCountAt + i] = i == 0 ? 0x00 : 0xFF;
const ComponentState out = deserializeComponentState(bytes, 48000.0);
@@ -950,6 +967,8 @@ static void testV13HardFlagCountThatStrandsAlignmentLeavesTheHoldAbsentNotFabric
// clamps to the top rung.
CHECK(out.params.bakeHold !=
note::makeDivision(note::kMaxQuarterExponent, note::DivisionModifier::Straight));
// The drain reaches the last tail as well: the stored `true` is past the damage too.
CHECK(!out.params.limiterEnabled);
}
// Numeric domains are established at the DOOR, not at each consumer. A NaN pitch depth reaches
@@ -1025,10 +1044,10 @@ static void testV13HardFlagTailTruncatedMidCountSurvivesWithoutWipingTheRecord()
in.params.loopCrossfadeFrames = 5;
std::vector<std::uint8_t> bytes = serializeComponentState(in);
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);
CHECK(bytes.size() >= kHardFlagTailBytes + kBakeHoldTailBytes + kLimiterTailBytes);
// Drops the bake-Hold and limiter tails with the flags: the truncation strands everything
// after it, which is the whole point — both lift to their defaults alongside the flags.
bytes.resize(bytes.size() - kHardFlagTailBytes - kBakeHoldTailBytes - kLimiterTailBytes);
legacy::u8v(bytes, 0x02); // half of the amp tail's 4-byte LE count, then nothing
legacy::u8v(bytes, 0x00);
@@ -1214,6 +1233,87 @@ static void testPriorPayloadVersionsLiftToAHardSeam() {
}
}
// The v15 rung. The limiter enable is a strict SUFFIX on v14, so a v14 blob is a valid prefix
// of it and lifts to BYPASSED — the migration bar for a project saved before the limiter
// existed: it reopens with the limiter off and therefore sounding identical.
//
// The v14 case is also the load-bearing ORDERING proof at the reader. A v14 blob is the
// current one with its last byte cut, so if the limiter byte were written AHEAD of the Hold
// the cut would take the Hold's modifier instead and the v14 read would resolve the Hold off
// the limiter byte — the stored division below would not survive. Transposing the two writes
// fails here, not merely in the byte fixture.
static void testLimiterEnableRoundTripsAndV14LiftsToBypassedWithItsHoldIntact() {
ComponentState in;
in.selectionId = "pad";
in.params.limiterEnabled = true;
in.params.keyTrack = 0.25; // a neighbour ahead of the new byte, so a misread shows up here too
// Ξ's v14 field, off its default, so the lift below can prove it came back untouched.
in.params.bakeHold = note::makeDivision(-1, note::DivisionModifier::Dotted);
const ComponentState out = deserializeComponentState(serializeComponentState(in), 48000.0);
CHECK(out.params.limiterEnabled);
CHECK(out.params.keyTrack == 0.25);
CHECK(out.params.bakeHold == note::makeDivision(-1, note::DivisionModifier::Dotted));
// The same state stamped v14, with exactly the one appended byte cut away: byte-for-byte
// what the Ξ binary wrote. Its Hold must survive in full.
const ComponentState v14 = deserializeComponentState(
payloadDowngradedTo(in, kParamsBakeHoldVersion, kLimiterTailBytes), 48000.0);
CHECK(!v14.params.limiterEnabled);
CHECK(v14.params.bakeHold == note::makeDivision(-1, note::DivisionModifier::Dotted));
CHECK(v14.params.keyTrack == 0.25);
// And a v13 blob, one rung further back, lifts to BOTH defaults.
const ComponentState v13 = deserializeComponentState(
payloadDowngradedTo(in, kParamsSplineVersion, kBakeHoldTailBytes + kLimiterTailBytes),
48000.0);
CHECK(!v13.params.limiterEnabled);
CHECK(v13.params.bakeHold == InstrumentParams{}.bakeHold);
CHECK(v13.params.keyTrack == 0.25);
// Bypassed is the default at the struct as well as on the wire.
CHECK(!InstrumentParams{}.limiterEnabled);
const ComponentState fresh =
deserializeComponentState(serializeComponentState(ComponentState{}), 48000.0);
CHECK(!fresh.params.limiterEnabled);
}
// The ORDERING proof at the WRITER, stated in bytes rather than in prose: the payload's whole
// discipline is that each version's fields are a strict suffix on the previous version's, so
// v14's Hold pair must be emitted BEFORE v15's limiter byte or every v14 blob already saved
// mis-parses. Asserted at absolute offsets from the end of the blob, with both fields off
// their defaults, so transposing the two writes fails on the values and not just the layout.
static void testAppendedTailsSitInVersionOrderOnTheWire() {
ComponentState in;
in.selectionId = "pad";
in.params.bakeHold = note::makeDivision(-2, note::DivisionModifier::Triplet);
in.params.limiterEnabled = true;
const std::vector<std::uint8_t> bytes = serializeComponentState(in);
CHECK(bytes.size() > kBakeHoldTailBytes + kLimiterTailBytes);
// The last six bytes are, in order: the v14 Hold's 4-byte LE exponent, its 1-byte
// modifier, then the v15 limiter byte.
const std::size_t holdAt = bytes.size() - kBakeHoldTailBytes - kLimiterTailBytes;
CHECK(bytes[holdAt + 0] == 0xfe); // -2 as int32 LE two's-complement
CHECK(bytes[holdAt + 1] == 0xff);
CHECK(bytes[holdAt + 2] == 0xff);
CHECK(bytes[holdAt + 3] == 0xff);
CHECK(bytes[holdAt + 4] == static_cast<std::uint8_t>(note::DivisionModifier::Triplet));
CHECK(bytes[bytes.size() - 1] == 0x01); // the limiter enable, last
// The same claim from the other side: flipping only the limiter changes only the LAST
// byte, so the byte the limiter owns cannot be one the Hold also writes.
ComponentState off = in;
off.params.limiterEnabled = false;
const std::vector<std::uint8_t> offBytes = serializeComponentState(off);
CHECK(offBytes.size() == bytes.size());
if (offBytes.size() == bytes.size()) {
for (std::size_t i = 0; i + 1 < bytes.size(); ++i) CHECK(offBytes[i] == bytes[i]);
CHECK(offBytes[bytes.size() - 1] == 0x00);
}
}
// The sharp edge of the bipolar change: v12 widened the filter curve's y domain, and the lift
// is a pure DOMAIN RE-TAG — no rescaling, no rounding. A pre-v12 curve's y values all lie in
// [0,1], which is inside [-1,+1], so every knot must come back bit-identical, the depth beside
@@ -1308,10 +1408,10 @@ static void testV13BlobLiftsToTheDefaultHold() {
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);
// Stamp the payload back to v13 and drop the v14 and v15 tails both: byte-for-byte what
// the v13 binary would have written.
const std::vector<std::uint8_t> v13 = payloadDowngradedTo(
in, kParamsSplineVersion, kBakeHoldTailBytes + kLimiterTailBytes);
const ComponentState out = deserializeComponentState(v13, 48000.0);
CHECK(out.params.bakeHold == InstrumentParams{}.bakeHold);
CHECK(out.selectionId == "pad");
@@ -1327,18 +1427,22 @@ 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);
CHECK(bytes.size() >= kBakeHoldTailBytes + kLimiterTailBytes);
bytes.resize(bytes.size() - kBakeHoldTailBytes - kLimiterTailBytes);
legacy::u32v(bytes, static_cast<std::uint32_t>(static_cast<std::int32_t>(9999)));
legacy::u8v(bytes, 200); // an unnamed modifier byte
putLimiterTail(bytes, true); // a well-formed byte after it, so the clamp is the only fault
const ComponentState out = deserializeComponentState(bytes, 48000.0);
CHECK(out.params.bakeHold ==
note::makeDivision(note::kMaxQuarterExponent, note::DivisionModifier::Straight));
// The tail behind the corrupt pair still lands on its own field: the clamp consumed exactly
// the five bytes it was owed, so the limiter byte was not read out of the Hold's modifier.
CHECK(out.params.limiterEnabled);
}
// 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.
// A blob truncated INSIDE the v14 tail costs the Hold alone — and, with the v15 byte stranded
// behind it, the limiter's revive is what stops that truncation resetting the record anyway.
static void testBakeHoldTruncatedTailSurvivesWithoutWipingTheRecord() {
ComponentState in;
in.selectionId = "pad";
@@ -1346,15 +1450,17 @@ static void testBakeHoldTruncatedTailSurvivesWithoutWipingTheRecord() {
in.params.play.adsr.attackSeconds = 0.017;
in.params.loopCrossfadeFrames = 96;
in.params.bakeHold = note::makeDivision(4, note::DivisionModifier::Triplet);
in.params.limiterEnabled = true;
std::vector<std::uint8_t> bytes = serializeComponentState(in);
CHECK(bytes.size() >= kBakeHoldTailBytes);
bytes.resize(bytes.size() - kBakeHoldTailBytes);
CHECK(bytes.size() >= kBakeHoldTailBytes + kLimiterTailBytes);
bytes.resize(bytes.size() - kBakeHoldTailBytes - kLimiterTailBytes);
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.params.limiterEnabled); // stranded behind the Hold, and revived not wiped
CHECK(out.selectionId == "pad");
CHECK(out.params.rootOverride && *out.params.rootOverride == 71);
CHECK(out.params.play.adsr.attackSeconds == 0.017);
@@ -1935,6 +2041,8 @@ int main() {
testLoopSpanAndCrossfadeRoundTrip();
testNegativeCrossfadeOnTheWireLiftsToZero();
testPriorPayloadVersionsLiftToAHardSeam();
testLimiterEnableRoundTripsAndV14LiftsToBypassedWithItsHoldIntact();
testAppendedTailsSitInVersionOrderOnTheWire();
testPreV12FilterVelocityLiftsAsAPureDomainReTag();
testWriterEmitsCurrentPayloadVersion();
testSingleZoneMigrationIsLossless();