180 lines
7.0 KiB
C++
180 lines
7.0 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);
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
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;
|
|
}
|