diff --git a/src/core/instrument/map/component_state_io.cpp b/src/core/instrument/map/component_state_io.cpp index 2307c5f..bcec2b6 100644 --- a/src/core/instrument/map/component_state_io.cpp +++ b/src/core/instrument/map/component_state_io.cpp @@ -191,7 +191,7 @@ void readZonesPayload(ByteReader& r, PerformanceMap& map, double projectRate) { const double amp = bitsToDouble(r.u64()); pts.push_back(VelocityPoint{vel, amp}); } - if (r.ok) z.velocityCurve = reasampler::VelocityCurve::fromPoints(std::move(pts)); + if (r.ok) z.velocityCurve = reasampler::instrument::engine::VelocityCurve::fromPoints(std::move(pts)); } // Payload versions 4 (branch-only frames tail, never shipped) and any unknown pv leave the // seconds product defaults on z.play — a v4 blob cannot exist outside this branch. diff --git a/src/core/wire/bytes.h b/src/core/wire/bytes.h index db4cff8..8b6c5fa 100644 --- a/src/core/wire/bytes.h +++ b/src/core/wire/bytes.h @@ -31,7 +31,7 @@ namespace reasampler::wire { // (cast at the call site, the established idiom: u32 for int, u64 for int64). template inline void putLE(std::vector& out, T v) { - static_assert(std::is_unsigned_v, "putLE takes the unsigned wire image"); + static_assert(std::is_unsigned_v && !std::is_same_v, "putLE takes the unsigned wire image"); for (std::size_t b = 0; b < sizeof(T); ++b) { out.push_back(static_cast((v >> (b * 8)) & 0xFF)); } diff --git a/src/shell/instrument/editor_input_browse_zone.cpp b/src/shell/instrument/editor_input_browse_zone.cpp index 063b592..1587c23 100644 --- a/src/shell/instrument/editor_input_browse_zone.cpp +++ b/src/shell/instrument/editor_input_browse_zone.cpp @@ -205,7 +205,6 @@ void ReaSamplerEditor::mouseDownZone(int w, int h, int x, int y) { const Rect back = zoneBackRect(w, h); if (contains(back, x, y)) { view_ = View::kSample; invalidate(); return; } const Rect content = zoneContentArea(w, h); - const int pad = 8; Rect addR = zoneAddRect(content); if (contains(addR, x, y)) { // Add a narrow default zone for the picked capture (or the first visible sample as a diff --git a/src/shell/instrument/editor_paint_browse_zone.cpp b/src/shell/instrument/editor_paint_browse_zone.cpp index 2d99f7e..60c5d82 100644 --- a/src/shell/instrument/editor_paint_browse_zone.cpp +++ b/src/shell/instrument/editor_paint_browse_zone.cpp @@ -171,7 +171,6 @@ void ReaSamplerEditor::paintZone(LICE_IBitmap* bmp, int w, int h) { } const Rect content = zoneContentArea(w, h); - const int pad = 8; // A single "+ Add Zone" affordance at the top of the content, then the keyboard strip // with one bar per zone. Delete is a small × on the selected zone (keystroke also). diff --git a/tests/test_component_state_io.cpp b/tests/test_component_state_io.cpp index d84ad8c..0b4a618 100644 --- a/tests/test_component_state_io.cpp +++ b/tests/test_component_state_io.cpp @@ -101,6 +101,147 @@ static void testComponentStateRoundTrip() { } } +// 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 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. @@ -166,6 +307,7 @@ static void testPerformanceRoundTrip() { int main() { testComponentStateRoundTrip(); + testGoldenFullBlobFixture(); testEnvelopePrefixBytesFrozen(); testV1SelectionLift(); testTruncationDegradesCleanly();