instrument: one VELOCITY deck for all three velocity curves, bipolar and off by default for pitch and filter
Payload v12 appends the new velocity->pitch curve and folds the retired filter velAmount into its now-bipolar curve, so pre-v12 projects reopen sounding identical. Preview button takes a drawn play triangle.
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
#include "../src/core/instrument/engine/master_gain.h" // masterGainMaxLinear (the v8 wire cap)
|
||||
#include "../src/core/util/curve_law.h" // kCurveNeutral (the migration neutral)
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
@@ -134,7 +135,7 @@ static void putRecordBody(std::vector<std::uint8_t>& out, const Zone& z, std::ui
|
||||
const std::vector<VelocityPoint> pts =
|
||||
z.curve.empty() ? std::vector<VelocityPoint>{{0.0, 1.0}, {127.0, 1.0}} : z.curve;
|
||||
u32v(out, static_cast<std::uint32_t>(pts.size()));
|
||||
for (const VelocityPoint& p : pts) { f64v(out, p.velocity); f64v(out, p.amp); }
|
||||
for (const VelocityPoint& p : pts) { f64v(out, p.velocity); f64v(out, p.value); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,7 +282,8 @@ static void testComponentStateRoundTrip() {
|
||||
in.params.startPoint = 5;
|
||||
in.params.keyTrack = 1.5;
|
||||
in.params.velocityCurve = reasampler::instrument::engine::VelocityCurve::fromPoints(
|
||||
{VelocityPoint{0.0, 0.2}, VelocityPoint{64.0, 0.6}, VelocityPoint{127.0, 1.0}});
|
||||
{VelocityPoint{0.0, 0.2}, VelocityPoint{64.0, 0.6}, VelocityPoint{127.0, 1.0}},
|
||||
reasampler::instrument::engine::CurveDomain::Unipolar);
|
||||
in.params.play.playMode = PlayMode::Trigger;
|
||||
in.params.play.adsr.attackSeconds = 0.01;
|
||||
in.params.play.adsr.holdSeconds = 0.05;
|
||||
@@ -422,7 +424,8 @@ static void testGoldenFullBlobFixture() {
|
||||
in.params.startPoint = 250;
|
||||
in.params.keyTrack = 0.5;
|
||||
in.params.velocityCurve = reasampler::instrument::engine::VelocityCurve::fromPoints(
|
||||
{VelocityPoint{0.0, 0.2}, VelocityPoint{64.0, 0.6}, VelocityPoint{127.0, 1.0}});
|
||||
{VelocityPoint{0.0, 0.2}, VelocityPoint{64.0, 0.6}, VelocityPoint{127.0, 1.0}},
|
||||
reasampler::instrument::engine::CurveDomain::Unipolar);
|
||||
in.params.play.playMode = PlayMode::Trigger;
|
||||
in.params.play.adsr.attackSeconds = 0.01;
|
||||
in.params.play.adsr.holdSeconds = 0.05;
|
||||
@@ -450,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,0x0b,0x00,0x00,
|
||||
0x64,0x04,0x00,0x00,0x00,0x6b,0x69,0x63,0x6b,0x00,0xff,0xff,0xff,0x0c,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,
|
||||
@@ -472,18 +475,18 @@ static void testGoldenFullBlobFixture() {
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // driveNorm 0.0
|
||||
0x00, // morphLaw = HighBandLow
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // modAmount 0.0
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // velAmount 0.0
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0xf0,0x3f, // velAmount slot: frozen constant 1.0
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // keyTrack 0.0
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // env attack 0.0
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // env hold 0.0
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // env decay 0.0
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0xf0,0x3f, // env sustain 1.0
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // env release 0.0
|
||||
0x02,0x00,0x00,0x00, // filter curve: 2 points (linear)
|
||||
0x02,0x00,0x00,0x00, // filter curve: 2 points (flat at zero)
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // velocity 0.0
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // amp 0.0
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // value 0.0
|
||||
0x00,0x00,0x00,0x00,0x00,0xc0,0x5f,0x40, // velocity 127.0
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0xf0,0x3f, // amp 1.0
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // value 0.0
|
||||
// --- payload v10 staged-curve tail, at its NEUTRAL default (this fixture sets no
|
||||
// curve or AHD field), in the header's documented order ---
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0xf0,0x3f, // amp attack curve 1.0
|
||||
@@ -507,6 +510,12 @@ static void testGoldenFullBlobFixture() {
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0xf0,0x3f, // filt AHD dec curve 1.0
|
||||
// --- payload v11 loop-crossfade tail ---
|
||||
0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00, // loopCrossfadeFrames 256
|
||||
// --- payload v12 velocity->pitch curve, at its off default (flat at zero) ---
|
||||
0x02,0x00,0x00,0x00, // 2 points
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // velocity 0.0
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // value 0.0
|
||||
0x00,0x00,0x00,0x00,0x00,0xc0,0x5f,0x40, // velocity 127.0
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // value 0.0
|
||||
};
|
||||
// clang-format on
|
||||
CHECK(bytes.size() == sizeof(kGolden));
|
||||
@@ -554,16 +563,17 @@ static void testEnvelopePrefixBytesFrozen() {
|
||||
CHECK(bytes[4] == 0); // ChannelMode::Mono
|
||||
}
|
||||
CHECK(kComponentStateVersion == 11);
|
||||
CHECK(kParamsPayloadVersion == 11);
|
||||
CHECK(kParamsPayloadVersion == 12);
|
||||
CHECK(kParamsSingleRecordVersion == 8);
|
||||
CHECK(kParamsFormatMarker == 0xFFFFFF00u);
|
||||
// The filter, staged-curve and loop 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 and velocity 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(kParamsPayloadVersion == kParamsLoopVersion);
|
||||
CHECK(kParamsVelocityVersion > kParamsLoopVersion);
|
||||
CHECK(kParamsPayloadVersion == kParamsVelocityVersion);
|
||||
}
|
||||
|
||||
// --- The filter tail (payload v9) --------------------------------------------
|
||||
@@ -601,7 +611,7 @@ static void testV8RecordLiftsToTheOffNeutralFilter() {
|
||||
CHECK(f.settings.driveNorm == def.settings.driveNorm);
|
||||
CHECK(f.settings.morphLaw == reasampler::instrument::engine::filter::MorphLaw::HighBandLow);
|
||||
CHECK(f.modAmount == 0.0);
|
||||
CHECK(f.velAmount == 0.0);
|
||||
for (int v = 0; v <= 127; ++v) CHECK(f.velocityCurve.eval(v) == 0.0);
|
||||
CHECK(f.keyTrack == 0.0);
|
||||
CHECK(f.env.sustainLevel == 1.0);
|
||||
CHECK(f.env.attackSeconds == 0.0 && f.env.decaySeconds == 0.0 &&
|
||||
@@ -628,15 +638,17 @@ static void testFilterTailRoundTripsLosslessly() {
|
||||
f.settings.driveNorm = 0.5f;
|
||||
f.settings.morphLaw = reasampler::instrument::engine::filter::MorphLaw::HighNotchLow;
|
||||
f.modAmount = -0.625;
|
||||
f.velAmount = 0.5;
|
||||
f.keyTrack = 1.5;
|
||||
f.env.attackSeconds = 0.031;
|
||||
f.env.holdSeconds = 0.062;
|
||||
f.env.decaySeconds = 0.125;
|
||||
f.env.sustainLevel = 0.25;
|
||||
f.env.releaseSeconds = 0.5;
|
||||
// Bipolar, and reaching into the negative half the retired unipolar shape could not
|
||||
// express: a codec that read this back through the old domain would clamp it to 0.
|
||||
f.velocityCurve = reasampler::instrument::engine::VelocityCurve::fromPoints(
|
||||
{VelocityPoint{0.0, 0.1}, VelocityPoint{100.0, 0.4}, VelocityPoint{127.0, 0.9}});
|
||||
{VelocityPoint{0.0, -0.75}, VelocityPoint{100.0, 0.4}, VelocityPoint{127.0, 0.9}},
|
||||
reasampler::instrument::engine::CurveDomain::Bipolar);
|
||||
// The amp's own curve stays different, so a codec that read one into the other fails here.
|
||||
in.params.velocityCurve = reasampler::instrument::engine::VelocityCurve::flat();
|
||||
|
||||
@@ -650,7 +662,6 @@ static void testFilterTailRoundTripsLosslessly() {
|
||||
CHECK(g.settings.driveNorm == f.settings.driveNorm);
|
||||
CHECK(g.settings.morphLaw == reasampler::instrument::engine::filter::MorphLaw::HighNotchLow);
|
||||
CHECK(g.modAmount == f.modAmount);
|
||||
CHECK(g.velAmount == f.velAmount);
|
||||
CHECK(g.keyTrack == f.keyTrack);
|
||||
CHECK(g.env.attackSeconds == f.env.attackSeconds);
|
||||
CHECK(g.env.holdSeconds == f.env.holdSeconds);
|
||||
@@ -658,11 +669,41 @@ static void testFilterTailRoundTripsLosslessly() {
|
||||
CHECK(g.env.sustainLevel == f.env.sustainLevel);
|
||||
CHECK(g.env.releaseSeconds == f.env.releaseSeconds);
|
||||
CHECK(g.velocityCurve.size() == 3);
|
||||
CHECK(g.velocityCurve.equals(f.velocityCurve));
|
||||
CHECK(g.velocityCurve.domain() == reasampler::instrument::engine::CurveDomain::Bipolar);
|
||||
CHECK(g.velocityCurve.eval(0.0) == -0.75); // the negative half survives the round trip
|
||||
CHECK(g.velocityCurve.eval(100.0) == 0.4);
|
||||
CHECK(g.velocityCurve.eval(127.0) == 0.9);
|
||||
CHECK(out.params.velocityCurve.equals(
|
||||
reasampler::instrument::engine::VelocityCurve::flat()));
|
||||
}
|
||||
|
||||
// The velocity->PITCH curve (payload v12) is a third, independent slot: it round-trips whole,
|
||||
// and neither of the other two leaks into it.
|
||||
static void testPitchVelocityCurveRoundTripsIndependently() {
|
||||
ComponentState in;
|
||||
in.selectionId = "pad";
|
||||
in.params.play.pitchVelocityCurve = reasampler::instrument::engine::VelocityCurve::fromPoints(
|
||||
{VelocityPoint{0.0, -1.0}, VelocityPoint{64.0, 0.25}, VelocityPoint{127.0, 0.5}},
|
||||
reasampler::instrument::engine::CurveDomain::Bipolar);
|
||||
in.params.play.filter.velocityCurve = reasampler::instrument::engine::VelocityCurve::fromPoints(
|
||||
{VelocityPoint{0.0, 0.2}, VelocityPoint{127.0, -0.6}},
|
||||
reasampler::instrument::engine::CurveDomain::Bipolar);
|
||||
in.params.velocityCurve = reasampler::instrument::engine::VelocityCurve::linear();
|
||||
|
||||
const ComponentState out =
|
||||
deserializeComponentState(serializeComponentState(in), 48000.0);
|
||||
const reasampler::instrument::engine::VelocityCurve& p = out.params.play.pitchVelocityCurve;
|
||||
CHECK(p.size() == 3);
|
||||
CHECK(p.domain() == reasampler::instrument::engine::CurveDomain::Bipolar);
|
||||
CHECK(p.eval(0.0) == -1.0);
|
||||
CHECK(p.eval(64.0) == 0.25);
|
||||
CHECK(p.eval(127.0) == 0.5);
|
||||
// The other two slots kept their own values — no cross-talk between the three curves.
|
||||
CHECK(out.params.play.filter.velocityCurve.eval(127.0) == -0.6);
|
||||
CHECK(out.params.velocityCurve.eval(127.0) == 1.0);
|
||||
CHECK(out.params.velocityCurve.eval(0.0) == 0.0);
|
||||
}
|
||||
|
||||
// A non-finite modAmount/velAmount/keyTrack (a corrupt blob, or any writer that skipped the
|
||||
// same guard the v8 master gain already applies) must lift to the neutral default rather than
|
||||
// reach Voice::tickFilterCutoff, where both clamp compares are false against NaN and the
|
||||
@@ -673,7 +714,6 @@ static void testNonFiniteFilterFieldsLiftToTheNeutralDefault() {
|
||||
FilterSeconds& f = in.params.play.filter;
|
||||
f.enabled = true;
|
||||
f.modAmount = std::numeric_limits<double>::quiet_NaN();
|
||||
f.velAmount = std::numeric_limits<double>::infinity();
|
||||
f.keyTrack = -std::numeric_limits<double>::infinity();
|
||||
|
||||
const ComponentState out =
|
||||
@@ -681,7 +721,6 @@ static void testNonFiniteFilterFieldsLiftToTheNeutralDefault() {
|
||||
const FilterSeconds& g = out.params.play.filter;
|
||||
const FilterSeconds def;
|
||||
CHECK(g.modAmount == def.modAmount);
|
||||
CHECK(g.velAmount == def.velAmount);
|
||||
CHECK(g.keyTrack == def.keyTrack);
|
||||
// The fallback is per-field, not per-record: the untouched fields still round-trip.
|
||||
CHECK(g.enabled);
|
||||
@@ -747,6 +786,7 @@ static void testNegativeCrossfadeOnTheWireLiftsToZero() {
|
||||
// output with version N stamped in and the (N+1..current) tails cut. Building the older blobs
|
||||
// that way exercises the tolerant-reader path rather than assuming it: if a tail ever stopped
|
||||
// being a pure suffix, these would decode as garbage instead of as the documented lift.
|
||||
static const std::size_t kVelocityTailBytes = 4 + 2 * 2 * 8; // v12: the 2-pt pitch curve
|
||||
static const std::size_t kLoopTailBytes = 8; // v11: crossfade, one int64
|
||||
static const std::size_t kCurveTailBytes = 19 * 8; // v10: nineteen doubles
|
||||
static const std::size_t kFilterTailBytes =
|
||||
@@ -775,8 +815,30 @@ static std::vector<std::uint8_t> payloadDowngradedTo(const ComponentState& state
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// A project saved before this change reopens sounding identical: its loop span still applies
|
||||
// and its seam is still hard, at EVERY prior single-record version.
|
||||
// Overwrite the frozen filter velAmount slot: the writer emits a constant 1.0 there now, so a
|
||||
// pre-v12 fixture has to plant its own depth. Located by the DISTINCT modAmount immediately
|
||||
// preceding it rather than by a byte offset, so a tail growing ahead of it cannot rot this.
|
||||
static void plantPreV12FilterDepth(std::vector<std::uint8_t>& bytes, double modAmount,
|
||||
double velAmount) {
|
||||
std::vector<std::uint8_t> needle;
|
||||
legacy::f64v(needle, modAmount);
|
||||
std::size_t at = 0;
|
||||
int hits = 0;
|
||||
for (std::size_t i = 0; i + 2 * needle.size() <= bytes.size(); ++i) {
|
||||
if (std::equal(needle.begin(), needle.end(), bytes.begin() + static_cast<long>(i))) {
|
||||
at = i;
|
||||
++hits;
|
||||
}
|
||||
}
|
||||
CHECK(hits == 1); // an ambiguous anchor would plant the depth in the wrong slot
|
||||
std::vector<std::uint8_t> depth;
|
||||
legacy::f64v(depth, velAmount);
|
||||
for (std::size_t k = 0; k < depth.size(); ++k) bytes[at + needle.size() + k] = depth[k];
|
||||
}
|
||||
|
||||
// A project saved before this change reopens sounding identical: its loop span still applies,
|
||||
// its seam is still hard, and velocity still modulates pitch not at all, at EVERY prior
|
||||
// single-record version.
|
||||
static void testPriorPayloadVersionsLiftToAHardSeam() {
|
||||
ComponentState in;
|
||||
in.selectionId = "pad";
|
||||
@@ -792,6 +854,11 @@ static void testPriorPayloadVersionsLiftToAHardSeam() {
|
||||
// it — proving the cuts land where the ladder says they do.
|
||||
in.params.play.adsr.attackCurve = 4.0;
|
||||
in.params.loopCrossfadeFrames = 777; // present in the bytes only at v11
|
||||
// Present in the bytes only at v12: an off-default pitch curve, so a lift that leaked one
|
||||
// in from anywhere else fails rather than coincidentally matching the default.
|
||||
in.params.play.pitchVelocityCurve = reasampler::instrument::engine::VelocityCurve::fromPoints(
|
||||
{VelocityPoint{0.0, 0.5}, VelocityPoint{127.0, 1.0}},
|
||||
reasampler::instrument::engine::CurveDomain::Bipolar);
|
||||
|
||||
struct Case {
|
||||
std::uint32_t pv;
|
||||
@@ -799,9 +866,10 @@ static void testPriorPayloadVersionsLiftToAHardSeam() {
|
||||
bool keepsCurveTail;
|
||||
};
|
||||
const Case cases[] = {
|
||||
{10, kLoopTailBytes, true},
|
||||
{9, kLoopTailBytes + kCurveTailBytes, false},
|
||||
{8, kLoopTailBytes + kCurveTailBytes + kFilterTailBytes, false},
|
||||
{11, kVelocityTailBytes, true},
|
||||
{10, kVelocityTailBytes + kLoopTailBytes, true},
|
||||
{9, kVelocityTailBytes + kLoopTailBytes + kCurveTailBytes, false},
|
||||
{8, kVelocityTailBytes + kLoopTailBytes + kCurveTailBytes + kFilterTailBytes, false},
|
||||
};
|
||||
for (const Case& c : cases) {
|
||||
const ComponentState out =
|
||||
@@ -813,14 +881,57 @@ static void testPriorPayloadVersionsLiftToAHardSeam() {
|
||||
CHECK(out.params.startPoint && *out.params.startPoint == 128);
|
||||
CHECK(out.params.keyTrack == 0.5);
|
||||
CHECK(out.params.play.adsr.releaseSeconds == 0.25);
|
||||
// The documented pre-change behaviour: a hard seam.
|
||||
CHECK(out.params.loopCrossfadeFrames == 0);
|
||||
// The documented pre-change behaviour: a hard seam and no velocity->pitch at all.
|
||||
CHECK(out.params.loopCrossfadeFrames == (c.pv >= 11 ? 777 : 0));
|
||||
for (int v = 0; v <= 127; ++v) {
|
||||
CHECK(out.params.play.pitchVelocityCurve.eval(v) == 0.0);
|
||||
}
|
||||
// And the cut landed on the tail boundary the ladder claims, not somewhere inside it.
|
||||
CHECK(out.params.play.adsr.attackCurve ==
|
||||
(c.keepsCurveTail ? 4.0 : reasampler::util::kCurveNeutral));
|
||||
}
|
||||
}
|
||||
|
||||
// The sharp edge of the bipolar change: a pre-v12 blob stored the filter's velocity response
|
||||
// as a [0,1] SHAPE times a separate depth, and the lift folds that depth into the knots. The
|
||||
// lifted curve must evaluate to exactly the product the pre-change voice computed.
|
||||
static void testPreV12FilterVelocityDepthFoldsIntoTheCurve() {
|
||||
ComponentState in;
|
||||
in.selectionId = "pad";
|
||||
FilterSeconds& f = in.params.play.filter;
|
||||
f.enabled = true;
|
||||
f.modAmount = -0.6251953125; // distinct and exactly representable: the planting anchor
|
||||
// What an old blob's curve looked like: a shape confined to [0,1], with the sign and the
|
||||
// amount living in the depth beside it.
|
||||
const reasampler::instrument::engine::VelocityCurve shape =
|
||||
reasampler::instrument::engine::VelocityCurve::fromPoints(
|
||||
{VelocityPoint{0.0, 0.0}, VelocityPoint{64.0, 0.25}, VelocityPoint{127.0, 1.0}},
|
||||
reasampler::instrument::engine::CurveDomain::Bipolar);
|
||||
f.velocityCurve = shape;
|
||||
|
||||
for (const double depth : {-0.75, 0.5, 0.0}) {
|
||||
std::vector<std::uint8_t> bytes =
|
||||
payloadDowngradedTo(in, kParamsLoopVersion, kVelocityTailBytes);
|
||||
plantPreV12FilterDepth(bytes, f.modAmount, depth);
|
||||
const ComponentState out = deserializeComponentState(bytes, 48000.0);
|
||||
const reasampler::instrument::engine::VelocityCurve& lifted =
|
||||
out.params.play.filter.velocityCurve;
|
||||
CHECK(lifted.domain() == reasampler::instrument::engine::CurveDomain::Bipolar);
|
||||
for (int v = 0; v <= 127; ++v) {
|
||||
CHECK(std::fabs(lifted.eval(v) - depth * shape.eval(v)) < 1e-12);
|
||||
}
|
||||
CHECK(out.params.play.filter.modAmount == f.modAmount); // the anchor itself survives
|
||||
}
|
||||
|
||||
// At v12 the same slot is ignored: the curve is read verbatim, whatever sits in it.
|
||||
std::vector<std::uint8_t> current = serializeComponentState(in);
|
||||
plantPreV12FilterDepth(current, f.modAmount, 0.0);
|
||||
const ComponentState now = deserializeComponentState(current, 48000.0);
|
||||
for (int v = 0; v <= 127; ++v) {
|
||||
CHECK(std::fabs(now.params.play.filter.velocityCurve.eval(v) - shape.eval(v)) < 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -1306,11 +1417,12 @@ static void testSampleRefsTruncatedMidEntry() {
|
||||
// The tail after the refs table is instanceGuid(4, empty) + selectionId(4+4="kick") +
|
||||
// the current params payload for DEFAULT params (marker4+version4 + overrides3 + the
|
||||
// 91-byte play tail + keyTrack8 + curve(4+2*16, the flat 2-point default) + the 134-byte
|
||||
// v9 filter tail + the 160-byte v10 staged-curve tail) = 452 bytes; entry two is 47 bytes
|
||||
// (id 4+3, path 4+7, root4, loop 1+8+8, channels4, name 4+0). Cutting 472 keeps the first
|
||||
// 27 of entry two's 47 — mid loop.start (offset 23..31).
|
||||
CHECK(bytes.size() > 472);
|
||||
bytes.resize(bytes.size() - 472);
|
||||
// v9 filter tail + the 152-byte v10 staged-curve tail + the 8-byte v11 crossfade + the
|
||||
// 36-byte v12 pitch curve) = 488 bytes; entry two is 47 bytes (id 4+3, path 4+7, root4,
|
||||
// loop 1+8+8, channels4, name 4+0). Cutting 508 keeps the first 27 of entry two's 47 —
|
||||
// mid loop.start (offset 23..31).
|
||||
CHECK(bytes.size() > 508);
|
||||
bytes.resize(bytes.size() - 508);
|
||||
const ComponentState back = deserializeComponentState(bytes, 44100.0);
|
||||
CHECK(back.sampleRefs.size() == 1);
|
||||
CHECK(back.sampleRefs.size() == 1 && back.sampleRefs[0].sampleId == "kick");
|
||||
@@ -1393,6 +1505,7 @@ int main() {
|
||||
testLoopSpanAndCrossfadeRoundTrip();
|
||||
testNegativeCrossfadeOnTheWireLiftsToZero();
|
||||
testPriorPayloadVersionsLiftToAHardSeam();
|
||||
testPreV12FilterVelocityDepthFoldsIntoTheCurve();
|
||||
testWriterEmitsCurrentPayloadVersion();
|
||||
testSingleZoneMigrationIsLossless();
|
||||
testMigratedFadeContourTracksTheRetiredEqualPowerShape();
|
||||
@@ -1415,6 +1528,7 @@ int main() {
|
||||
testTruncationDegradesCleanly();
|
||||
testV8RecordLiftsToTheOffNeutralFilter();
|
||||
testFilterTailRoundTripsLosslessly();
|
||||
testPitchVelocityCurveRoundTripsIndependently();
|
||||
testNonFiniteFilterFieldsLiftToTheNeutralDefault();
|
||||
testNonFiniteAhdSecondsLiftToZero();
|
||||
if (failures == 0) {
|
||||
|
||||
Reference in New Issue
Block a user