Files
reasampler/tests/test_component_state_io.cpp
T

322 lines
14 KiB
C++

// component_state_io unit tests (Q-W2v). The HISTORICAL codec suite — the full
// envelope/payload version ladder, every legacy lift, the golden byte fixtures —
// lives in test_sample_map.cpp and runs unmodified against the split module; this
// target exists as the module's OWN executable (house rule: every pure module has
// one) and as the STRUCTURAL PROOF the codec links WITHOUT the voice engine
// (T2-07): it links component_state_io + velocity_curve + master_gain only — a
// sampler_core/pitch_shift symbol reaching this link is a regression.
#include "../src/core/instrument/map/component_state_io.h"
#include <cstdio>
#include <string>
#include <vector>
using namespace reasampler;
using namespace reasampler::instrument::map;
static int failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
++failures; \
} \
} while (0)
// A full round-trip through the CURRENT envelope (v11): every field survives.
static void testComponentStateRoundTrip() {
ComponentState in;
in.selectionId = "smp-1";
in.channelMode = ChannelMode::Stereo;
in.channelModeExplicit = true;
in.lastConsumedAssignGeneration = 42;
in.previewVelocity = 99;
in.voiceCount = 7;
in.voiceMode = VoiceMode::Mono;
in.monoTrigger = MonoTrigger::Legato;
in.masterGainLinear = 0.5;
in.instanceGuid = "0123456789abcdef0123456789abcdef";
SampleRefEntry e;
e.sampleId = "smp-1";
e.ref.relativePath = "bank/smp-1.wav";
e.ref.rootNote = 64;
e.ref.loop.hasLoop = true;
e.ref.loop.start = 100;
e.ref.loop.end = 2000;
e.ref.channelCount = 2;
e.displayName = "My Capture";
in.sampleRefs.push_back(e);
PerformanceZone z;
z.sampleId = "smp-1";
z.lowNote = 30;
z.highNote = 90;
z.rootOverride = 61;
z.startPoint = 5;
z.keyTrack = 1.5;
z.play.playMode = PlayMode::Trigger;
z.play.trigger.lengthFraction = 0.75;
z.play.trigger.fadeInFrames = 441;
z.play.trigger.fadeOutFrames = 882;
in.map.zones.push_back(z);
const std::vector<std::uint8_t> bytes = serializeComponentState(in);
const ComponentState out = deserializeComponentState(bytes, 48000.0);
CHECK(out.selectionId == "smp-1");
CHECK(out.channelMode == ChannelMode::Stereo);
CHECK(out.channelModeExplicit);
CHECK(out.lastConsumedAssignGeneration == 42);
CHECK(out.previewVelocity == 99);
CHECK(out.voiceCount == 7);
CHECK(out.voiceMode == VoiceMode::Mono);
CHECK(out.monoTrigger == MonoTrigger::Legato);
CHECK(out.masterGainLinear == 0.5);
CHECK(out.instanceGuid == "0123456789abcdef0123456789abcdef");
CHECK(out.sampleRefs.size() == 1);
if (out.sampleRefs.size() == 1) {
CHECK(out.sampleRefs[0].sampleId == "smp-1");
CHECK(out.sampleRefs[0].ref.relativePath == "bank/smp-1.wav");
CHECK(out.sampleRefs[0].ref.rootNote == 64);
CHECK(out.sampleRefs[0].ref.loop.hasLoop);
CHECK(out.sampleRefs[0].ref.loop.start == 100);
CHECK(out.sampleRefs[0].ref.loop.end == 2000);
CHECK(out.sampleRefs[0].ref.channelCount == 2);
CHECK(out.sampleRefs[0].displayName == "My Capture");
}
CHECK(out.map.zones.size() == 1);
if (out.map.zones.size() == 1) {
const PerformanceZone& oz = out.map.zones[0];
CHECK(oz.sampleId == "smp-1");
CHECK(oz.lowNote == 30);
CHECK(oz.highNote == 90);
CHECK(oz.rootOverride && *oz.rootOverride == 61);
CHECK(oz.startPoint && *oz.startPoint == 5);
CHECK(oz.keyTrack == 1.5);
CHECK(oz.play.playMode == PlayMode::Trigger);
CHECK(oz.play.trigger.lengthFraction == 0.75);
CHECK(oz.play.trigger.fadeInFrames == 441);
CHECK(oz.play.trigger.fadeOutFrames == 882);
}
}
// GOLDEN FULL-BLOB FIXTURE (reviewer follow-up, Q-W2v). testEnvelopePrefixBytesFrozen below
// only pins the first 5 bytes of a near-EMPTY blob; it cannot catch a drift anywhere past the
// mode byte (a field re-ordered or dropped inside the voice/gain/refs/guid/zone tail would
// still pass it). This test builds a canonical v11 ComponentState that exercises EVERY field
// family at once (two zones — one Trigger with every optional override set, one Gate with all
// optionals absent — a two-entry sample-refs table, non-default voice/gain/channel-mode
// fields, and a non-flat velocity curve) and asserts the encoded bytes equal an EXACT expected
// vector. The vector below is the current writer's PROVABLY-CORRECT output (proven by the
// round-trip test above) captured as the golden — so the byte layout itself becomes
// un-driftable, not just its first 5 bytes.
static void testGoldenFullBlobFixture() {
ComponentState in;
in.selectionId = "kick";
in.channelMode = ChannelMode::Stereo;
in.channelModeExplicit = true;
in.lastConsumedAssignGeneration = 12345;
in.previewVelocity = 100;
in.voiceCount = 24;
in.voiceMode = VoiceMode::Mono;
in.monoTrigger = MonoTrigger::Legato;
in.masterGainLinear = 2.0;
in.instanceGuid = "guid-1234-5678-abcd";
SampleRefEntry kickRef;
kickRef.sampleId = "kick";
kickRef.ref.relativePath = "bank/kick.wav";
kickRef.ref.rootNote = 36;
kickRef.ref.loop.hasLoop = true;
kickRef.ref.loop.start = 1000;
kickRef.ref.loop.end = 5000;
kickRef.ref.channelCount = 2;
kickRef.displayName = "Kick Drum";
in.sampleRefs.push_back(kickRef);
SampleRefEntry snareRef;
snareRef.sampleId = "snare";
snareRef.ref.relativePath = "bank/snare.wav";
snareRef.ref.rootNote = 38;
snareRef.ref.loop.hasLoop = false;
snareRef.ref.loop.start = 0;
snareRef.ref.loop.end = 0;
snareRef.ref.channelCount = 1;
snareRef.displayName = "Snare";
in.sampleRefs.push_back(snareRef);
// Zone A: every optional field present, Trigger mode, non-flat velocity curve.
PerformanceZone zoneA;
zoneA.sampleId = "kick";
zoneA.lowNote = 24;
zoneA.highNote = 60;
zoneA.rootOverride = 36;
SampleLoop loopA;
loopA.hasLoop = true;
loopA.start = 1000;
loopA.end = 5000;
zoneA.loopOverride = loopA;
zoneA.startPoint = 250;
zoneA.keyTrack = 0.5;
zoneA.velocityCurve = reasampler::instrument::engine::VelocityCurve::fromPoints(
{VelocityPoint{0.0, 0.2}, VelocityPoint{64.0, 0.6}, VelocityPoint{127.0, 1.0}});
zoneA.play.playMode = PlayMode::Trigger;
zoneA.play.adsr.attackSeconds = 0.01;
zoneA.play.adsr.holdSeconds = 0.05;
zoneA.play.adsr.decaySeconds = 0.02;
zoneA.play.adsr.sustainLevel = 0.8;
zoneA.play.adsr.releaseSeconds = 0.15;
zoneA.play.trigger.lengthFraction = 0.75;
zoneA.play.trigger.fadeInFrames = 100;
zoneA.play.trigger.fadeOutFrames = 200;
zoneA.play.pitchEngine = PitchEngine::Preserve;
zoneA.play.pitchEnv.enabled = true;
zoneA.play.pitchEnv.attackSeconds = 0.02;
zoneA.play.pitchEnv.decaySeconds = 0.03;
zoneA.play.pitchEnv.peakSemitones = 5.0;
in.map.zones.push_back(zoneA);
// Zone B: every optional field absent, Gate mode, default flat velocity curve.
PerformanceZone zoneB;
zoneB.sampleId = "snare";
zoneB.lowNote = 61;
zoneB.highNote = 90;
zoneB.keyTrack = 2.0;
zoneB.play.playMode = PlayMode::Gate;
zoneB.play.adsr.attackSeconds = 0.005;
zoneB.play.adsr.holdSeconds = 0.0;
zoneB.play.adsr.decaySeconds = 0.1;
zoneB.play.adsr.sustainLevel = 0.5;
zoneB.play.adsr.releaseSeconds = 0.2;
zoneB.play.pitchEngine = PitchEngine::Varispeed;
in.map.zones.push_back(zoneB);
const std::vector<std::uint8_t> bytes = serializeComponentState(in);
// clang-format off
static const std::uint8_t kGolden[] = {
0x0b,0x00,0x00,0x00,0x01,0x39,0x30,0x00,0x00,0x00,0x00,0x00,0x00,0x64,0x18,0x01,
0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x01,0x02,0x00,0x00,0x00,0x04,0x00,
0x00,0x00,0x6b,0x69,0x63,0x6b,0x0d,0x00,0x00,0x00,0x62,0x61,0x6e,0x6b,0x2f,0x6b,
0x69,0x63,0x6b,0x2e,0x77,0x61,0x76,0x24,0x00,0x00,0x00,0x01,0xe8,0x03,0x00,0x00,
0x00,0x00,0x00,0x00,0x88,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,
0x09,0x00,0x00,0x00,0x4b,0x69,0x63,0x6b,0x20,0x44,0x72,0x75,0x6d,0x05,0x00,0x00,
0x00,0x73,0x6e,0x61,0x72,0x65,0x0e,0x00,0x00,0x00,0x62,0x61,0x6e,0x6b,0x2f,0x73,
0x6e,0x61,0x72,0x65,0x2e,0x77,0x61,0x76,0x26,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
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,0x07,0x00,0x00,
0x00,0x02,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x6b,0x69,0x63,0x6b,0x18,0x00,0x00,
0x00,0x3c,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,0xe8,0x3f,0x64,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xc8,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x7b,0x14,0xae,0x47,0xe1,0x7a,0x94,0x3f,
0xb8,0x1e,0x85,0xeb,0x51,0xb8,0x9e,0x3f,0x00,0x00,0x00,0x00,0x00,0x00,0x14,0x40,
0x7b,0x14,0xae,0x47,0xe1,0x7a,0x84,0x3f,0x7b,0x14,0xae,0x47,0xe1,0x7a,0x94,0x3f,
0x9a,0x99,0x99,0x99,0x99,0x99,0xe9,0x3f,0x33,0x33,0x33,0x33,0x33,0x33,0xc3,0x3f,
0x00,0x00,0x00,0x00,0x00,0x00,0xe0,0x3f,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x9a,0x99,0x99,0x99,0x99,0x99,0xc9,0x3f,0x00,0x00,0x00,0x00,
0x00,0x00,0x50,0x40,0x33,0x33,0x33,0x33,0x33,0x33,0xe3,0x3f,0x00,0x00,0x00,0x00,
0x00,0xc0,0x5f,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0xf0,0x3f,0x05,0x00,0x00,0x00,
0x73,0x6e,0x61,0x72,0x65,0x3d,0x00,0x00,0x00,0x5a,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xf0,
0x3f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x7b,0x14,0xae,0x47,0xe1,
0x7a,0x74,0x3f,0x9a,0x99,0x99,0x99,0x99,0x99,0xb9,0x3f,0x00,0x00,0x00,0x00,0x00,
0x00,0xe0,0x3f,0x9a,0x99,0x99,0x99,0x99,0x99,0xc9,0x3f,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x40,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0xf0,0x3f,0x00,0x00,0x00,0x00,0x00,0xc0,0x5f,0x40,0x00,
0x00,0x00,0x00,0x00,0x00,0xf0,0x3f,
};
// clang-format on
CHECK(bytes.size() == sizeof(kGolden));
if (bytes.size() == sizeof(kGolden)) {
bool same = true;
for (std::size_t i = 0; i < bytes.size(); ++i) {
if (bytes[i] != kGolden[i]) { same = false; break; }
}
CHECK(same);
}
}
// The FROZEN envelope prefix: version tag v11 LE, then the mode byte — a drift in
// either is a byte-format break the round-trip alone can't prove (both sides could
// drift together). Pins the writer's absolute bytes.
static void testEnvelopePrefixBytesFrozen() {
ComponentState in; // defaults: mono, implicit, no refs, no selection, no zones
const std::vector<std::uint8_t> bytes = serializeComponentState(in);
CHECK(bytes.size() > 5);
if (bytes.size() > 5) {
CHECK(bytes[0] == 11 && bytes[1] == 0 && bytes[2] == 0 && bytes[3] == 0);
CHECK(bytes[4] == 0); // ChannelMode::Mono
}
CHECK(kComponentStateVersion == 11);
CHECK(kZonesPayloadVersion == 7);
CHECK(kZonesFormatMarker == 0xFFFFFF00u);
}
// A v1 selection blob lifts to {id, one full-keyboard zone} — the oldest live lift.
static void testV1SelectionLift() {
const std::vector<std::uint8_t> v1 = serializeSelection("old-pick");
const ComponentState out = deserializeComponentState(v1, 48000.0);
CHECK(out.selectionId == "old-pick");
CHECK(out.map.zones.size() == 1);
if (out.map.zones.size() == 1) {
CHECK(out.map.zones[0].sampleId == "old-pick");
CHECK(out.map.zones[0].lowNote == 0);
CHECK(out.map.zones[0].highNote == 127);
}
}
// Truncation degrades to a partial/empty parse — never out-of-bounds, never throws.
static void testTruncationDegradesCleanly() {
ComponentState in;
in.selectionId = "smp-2";
PerformanceZone z;
z.sampleId = "smp-2";
in.map.zones.push_back(z);
const std::vector<std::uint8_t> bytes = serializeComponentState(in);
for (std::size_t cut = 0; cut < bytes.size(); ++cut) {
const std::vector<std::uint8_t> part(bytes.begin(),
bytes.begin() + static_cast<long>(cut));
const ComponentState out = deserializeComponentState(part, 48000.0);
(void)out; // reaching here without UB/throw is the contract under test
}
CHECK(true);
}
// serializePerformance/deserializePerformance round-trip through the v2 envelope.
static void testPerformanceRoundTrip() {
PerformanceMap in;
PerformanceZone z;
z.sampleId = "zone-a";
z.lowNote = 10;
z.highNote = 20;
in.zones.push_back(z);
const PerformanceMap out = deserializePerformance(serializePerformance(in), 48000.0);
CHECK(out.zones.size() == 1);
if (out.zones.size() == 1) {
CHECK(out.zones[0].sampleId == "zone-a");
CHECK(out.zones[0].lowNote == 10);
CHECK(out.zones[0].highNote == 20);
}
}
int main() {
testComponentStateRoundTrip();
testGoldenFullBlobFixture();
testEnvelopePrefixBytesFrozen();
testV1SelectionLift();
testTruncationDegradesCleanly();
testPerformanceRoundTrip();
if (failures == 0) {
std::printf("component_state_io_tests: all tests passed\n");
return 0;
}
std::printf("component_state_io_tests: %d FAILURE(S)\n", failures);
return 1;
}