Q-W2v: split VST god-modules — editor 8 face-axis TUs (+pure layout hoist), processor 3 TUs, component_state_io codec split (extension drops the voice engine), zone_params.h, core/wire putLE; formats frozen, 61/61 green
This commit is contained in:
@@ -160,6 +160,26 @@ static void testFilterIndices() {
|
||||
CHECK(filterNameIndices(names, "zzz").empty());
|
||||
}
|
||||
|
||||
// --- The Browse-modal regions (Q-W2v hoist, T2-06) ---------------------------
|
||||
|
||||
// Title / search / content / footer partition the window top-to-bottom without gaps;
|
||||
// Back right-anchors in the title band; Cancel/Load flank the footer.
|
||||
static void testBrowseModalPartition() {
|
||||
const BrowseModal m = computeBrowseModal(840, 620);
|
||||
CHECK(m.title.y == 0 && m.title.width == 840 && m.title.height == kTitleHeight);
|
||||
CHECK(m.back.right() == 840 - kPad && m.back.bottom() <= m.title.bottom());
|
||||
CHECK(m.search.y == m.title.bottom());
|
||||
CHECK(m.search.height == searchBoxRect(840).height);
|
||||
CHECK(m.content.y == m.search.bottom());
|
||||
CHECK(m.content.bottom() == 620 - 30); // the footer band (kBrowseFooterH)
|
||||
CHECK(m.cancel.x == kPad);
|
||||
CHECK(m.confirm.right() == 840 - kPad);
|
||||
CHECK(m.cancel.y == m.content.bottom() + 3 && m.cancel.y == m.confirm.y);
|
||||
// Degenerate short window: the footer clamps below the search box (no inversion).
|
||||
const BrowseModal s = computeBrowseModal(840, 40);
|
||||
CHECK(s.content.bottom() >= s.content.y);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testContentHeight();
|
||||
testMaxOffsetFitsAndOverflows();
|
||||
@@ -174,6 +194,7 @@ int main() {
|
||||
testSearchBoxRect();
|
||||
testNameMatch();
|
||||
testFilterIndices();
|
||||
testBrowseModalPartition();
|
||||
|
||||
if (g_fail == 0) std::printf("browser_scroll: all tests passed\n");
|
||||
return g_fail != 0;
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -311,6 +311,78 @@ static void testZoneHitTestMisses() {
|
||||
CHECK(zoneHitTest(L, 3, L.sampleList.x + 2, midY).zoneIndex == -1);
|
||||
}
|
||||
|
||||
// --- r11 Sample / Zone face layout (Q-W2v hoist, T2-06) ----------------------
|
||||
|
||||
// The band stack at the default 840x620 with a 120px deck: title / hero / cluster /
|
||||
// deck in order, hero elastic (absorbs the slack), deck bottom-anchored at kPad.
|
||||
static void testSampleBandsStackAndElasticHero() {
|
||||
const SampleBands b = computeSampleBands(840, 620, 120);
|
||||
CHECK(b.title.y == 0 && b.title.height == kTitleHeight && b.title.width == 840);
|
||||
CHECK(b.hero.y == b.title.bottom());
|
||||
CHECK(b.hero.height >= 150); // above the hero floor
|
||||
CHECK(b.cluster.y > b.hero.bottom()); // cluster below the hero (+gap)
|
||||
CHECK(b.deck.bottom() == 620 - kPad); // deck bottom-anchored
|
||||
CHECK(b.deck.height == 120);
|
||||
// Nav buttons right-anchored inside the title band, Browse left of Zone.
|
||||
CHECK(b.navZone.right() == 840 - kPad);
|
||||
CHECK(b.navBrowse.right() < b.navZone.x);
|
||||
CHECK(b.navZone.bottom() <= b.title.bottom());
|
||||
// A too-short window: the hero keeps its floor; the lower bands clip below.
|
||||
const SampleBands s = computeSampleBands(840, 200, 120);
|
||||
CHECK(s.hero.height == 150);
|
||||
CHECK(s.deck.bottom() > 200); // clips past the window bottom (defensive case)
|
||||
}
|
||||
|
||||
// The cluster's right-anchored run tiles left of the channel toggle without overlap:
|
||||
// rootStrip | preview | velCell(velKnob+velLabel) | curveBtn | (toggle).
|
||||
static void testClusterRectsRunAndKnobCentering() {
|
||||
const Rect cluster = Rect::ltrb(0, 500, 840, 552);
|
||||
const ChannelToggleRects chan = channelToggleRects(cluster);
|
||||
CHECK(chan.stereo.right() == 840 - kPad);
|
||||
CHECK(chan.mono.right() == chan.stereo.x);
|
||||
const ClusterRects cr = clusterRects(cluster, chan.mono, 28);
|
||||
CHECK(cr.curveBtn.right() == chan.mono.x - kPad);
|
||||
CHECK(cr.velCell.right() == cr.curveBtn.x - kPad);
|
||||
CHECK(cr.preview.right() == cr.velCell.x - kPad);
|
||||
CHECK(cr.rootStrip.x == cluster.x + kPad);
|
||||
CHECK(cr.rootStrip.right() == cr.preview.x - kPad);
|
||||
// The knob square centers in the cell and the label band sits beneath it.
|
||||
CHECK(cr.velKnob.width == 28);
|
||||
CHECK(cr.velKnob.x - cr.velCell.x == cr.velCell.right() - cr.velKnob.right());
|
||||
CHECK(cr.velLabel.y == cr.velKnob.bottom());
|
||||
CHECK(cr.velLabel.bottom() == cr.velCell.bottom());
|
||||
}
|
||||
|
||||
// The Zone surface: content below the title; strip below the add/delete row; the note
|
||||
// entry fields tile in three ordered segments; deck + curve button split the panel.
|
||||
static void testZoneSurfaceLayoutAnchors() {
|
||||
const Rect content = zoneContentArea(840, 620);
|
||||
CHECK(content.y == kTitleHeight && content.bottom() == 620);
|
||||
const Rect back = zoneBackRect(840, 620);
|
||||
CHECK(back.right() == 840 - kPad && back.bottom() <= kTitleHeight);
|
||||
const Rect addR = zoneAddRect(content);
|
||||
const Rect delR = zoneDeleteRect(addR);
|
||||
CHECK(addR.y == content.y + 4);
|
||||
CHECK(delR.x == addR.right() + 8 && delR.y == addR.y);
|
||||
const Rect strip = zonesStripArea(content);
|
||||
CHECK(strip.y == addR.bottom() + 12);
|
||||
CHECK(strip.x == content.x + kPad && strip.right() == content.right() - kPad);
|
||||
const Rect fields = noteEntryFieldsArea(content);
|
||||
CHECK(fields.y == strip.bottom() + 8);
|
||||
const Rect f0 = noteEntryFieldRect(fields, 0);
|
||||
const Rect f1 = noteEntryFieldRect(fields, 1);
|
||||
const Rect f2 = noteEntryFieldRect(fields, 2);
|
||||
CHECK(f0.x < f1.x && f1.x < f2.x);
|
||||
CHECK(f2.right() == fields.right());
|
||||
CHECK(noteEntryFieldRect(fields, 3).width == 0); // out-of-range -> empty
|
||||
const Rect panel = zonesControlPanel(content);
|
||||
const Rect deck = zonesDeckArea(content);
|
||||
const Rect curve = zonesCurveButton(content);
|
||||
CHECK(panel.y == strip.bottom() + 8 + 18 + 8);
|
||||
CHECK(deck.y == panel.y && deck.right() < curve.x); // curve column reserved
|
||||
CHECK(curve.right() == panel.right() && curve.y == panel.y);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testContainsHalfOpen();
|
||||
testContainsDegenerate();
|
||||
@@ -332,6 +404,9 @@ int main() {
|
||||
testZoneRowStacksAndSelects();
|
||||
testZoneRowControlsMapToFields();
|
||||
testZoneHitTestMisses();
|
||||
testSampleBandsStackAndElasticHero();
|
||||
testClusterRectsRunAndKnobCentering();
|
||||
testZoneSurfaceLayoutAnchors();
|
||||
|
||||
if (g_fail == 0) std::printf("editor_geometry: all tests passed\n");
|
||||
return g_fail != 0;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// cross-artifact contract guard — the same pattern assignment_request_tests uses.
|
||||
|
||||
#include "../src/core/wire/instrument_drop.h"
|
||||
#include "../src/core/instrument/map/sample_map.h" // deserializeComponentState — the instrument's OWN reader
|
||||
#include "../src/core/instrument/map/component_state_io.h" // deserializeComponentState — the instrument's OWN reader
|
||||
|
||||
#include "version_generated.h" // REASAMPLER_CHANNEL_IS_BETA — pins the per-channel class ID
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
using namespace reasampler;
|
||||
using namespace reasampler::wire;
|
||||
using namespace reasampler::instrument::map; // ComponentState + the codec (Q-W2v re-namespace)
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
// channel-count stride downmixToMono divides by).
|
||||
|
||||
#include "../src/core/instrument/map/sample_map.h"
|
||||
#include "../src/core/instrument/map/component_state_io.h" // the Q-W2v codec split (formats FROZEN; suite unchanged)
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
@@ -36,7 +37,8 @@
|
||||
|
||||
using namespace reasampler;
|
||||
using namespace reasampler::instrument::engine;
|
||||
using namespace reasampler::capture; // wav_trim (WavLayout) — sample_map re-exports live in reasampler until Q-W2v
|
||||
using namespace reasampler::instrument::map; // sample_map + component_state_io (Q-W2v re-namespace)
|
||||
using namespace reasampler::capture; // wav_trim (WavLayout)
|
||||
using namespace reasampler::model;
|
||||
|
||||
static int g_fail = 0;
|
||||
|
||||
@@ -9,9 +9,12 @@
|
||||
// reference-bound std::string — the Cursor never outlives its buffer.
|
||||
|
||||
#include "../src/core/wire/wire.h"
|
||||
#include "../src/core/wire/bytes.h" // putLE / ByteReader — the ONE LE byte codec (Q-W2v, T4-20)
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace reasampler;
|
||||
using namespace reasampler::wire;
|
||||
@@ -173,7 +176,56 @@ static void testParseUnsignedDecimal() {
|
||||
CHECK(!wire::parseUnsignedDecimal(std::string(40, '9'), v)); // long run cannot wrap
|
||||
}
|
||||
|
||||
// --- core/wire/bytes.h — the LE byte codec (Q-W2v, T4-20) --------------------
|
||||
|
||||
// putLE emits exactly the bytes the retired hand-rolled putU32le/putU64le emitted
|
||||
// (LSB first, fixed width) — the FROZEN wire byte order.
|
||||
static void testPutLEExactBytes() {
|
||||
std::vector<std::uint8_t> out;
|
||||
wire::putLE(out, static_cast<std::uint32_t>(0x0403'0201u));
|
||||
CHECK(out.size() == 4);
|
||||
CHECK(out[0] == 0x01 && out[1] == 0x02 && out[2] == 0x03 && out[3] == 0x04);
|
||||
out.clear();
|
||||
wire::putLE(out, static_cast<std::uint64_t>(0x0807'0605'0403'0201ull));
|
||||
CHECK(out.size() == 8);
|
||||
CHECK(out[0] == 0x01 && out[7] == 0x08);
|
||||
}
|
||||
|
||||
// ByteReader round-trips putLE output, and the double bit-cast is lossless.
|
||||
static void testByteReaderRoundTrip() {
|
||||
std::vector<std::uint8_t> out;
|
||||
wire::putLE(out, static_cast<std::uint32_t>(7));
|
||||
wire::putLE(out, static_cast<std::uint64_t>(wire::doubleToBits(-2.5)));
|
||||
wire::putLE(out, static_cast<std::uint8_t>(0xAB));
|
||||
wire::putLE(out,
|
||||
static_cast<std::uint64_t>(static_cast<std::int64_t>(-42))); // i64 image
|
||||
wire::ByteReader r(out);
|
||||
CHECK(r.peekU32() == 7);
|
||||
CHECK(r.u32() == 7);
|
||||
CHECK(wire::bitsToDouble(r.u64()) == -2.5);
|
||||
CHECK(r.u8() == 0xAB);
|
||||
CHECK(r.i64() == -42);
|
||||
CHECK(r.ok);
|
||||
}
|
||||
|
||||
// Truncation latches ok=false and every subsequent read yields zero/empty —
|
||||
// the partial-parse contract every codec consumer leans on.
|
||||
static void testByteReaderLatchesOnTruncation() {
|
||||
std::vector<std::uint8_t> out;
|
||||
wire::putLE(out, static_cast<std::uint32_t>(9));
|
||||
out.pop_back(); // truncate mid-u32
|
||||
wire::ByteReader r(out);
|
||||
CHECK(r.u32() == 0);
|
||||
CHECK(!r.ok);
|
||||
CHECK(r.u8() == 0); // latched: even an in-bounds width now fails
|
||||
CHECK(r.str(1).empty());
|
||||
CHECK(r.peekU32() == 0);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testPutLEExactBytes();
|
||||
testByteReaderRoundTrip();
|
||||
testByteReaderLatchesOnTruncation();
|
||||
testPutFieldExactBytes();
|
||||
testFieldRoundTripIncludingSeparators();
|
||||
testLiteralMismatchFails();
|
||||
|
||||
Reference in New Issue
Block a user