Q-W1 pt2: core/shell/app relocation + sub-namespaces; one concrete ui::Rect (LTRB fork retired); slot_map split from bank_book; BankIndex→BankModel; 59/59 green

This commit is contained in:
2026-07-28 20:48:56 -04:00
parent 67a41728f3
commit 847936f813
222 changed files with 2247 additions and 2079 deletions
+2 -1
View File
@@ -15,13 +15,14 @@
// degenerate/too-narrow bar handled without crash or overlap.
// * Resize: no inventory item cut off or overlapping across a representative width range.
#include "../src/action_bar.h"
#include "../src/core/ui/action_bar.h"
#include <cstddef>
#include <cstdio>
#include <vector>
using namespace reasampler;
using namespace reasampler::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -5,12 +5,13 @@
// back from ext state (PreVersioning / Unknown / Stamped). The ext-state I/O (persist) and
// the show-version action (main) are DAW-verified shell.
#include "../src/app_version.h"
#include "../src/core/version/app_version.h"
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::version;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -25,12 +25,13 @@
// configure_file input in CMakeLists.txt. They are NOT the shipped version: never bump
// them on release — their padded shape is the entire point.
#include "../src/app_version.h"
#include "../src/core/version/app_version.h"
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::version;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -9,12 +9,13 @@
// trailing-garbage input -> nullopt (the reader's "no pending request" fallback hinges
// on it).
#include "../src/assignment_request.h"
#include "../src/core/wire/assignment_request.h"
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::wire;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+9 -8
View File
@@ -10,13 +10,14 @@
// pool, set named, invalid id); JSON round-trip lossless (full book); legacy
// bank_index → pool migration.
#include "../src/bank_book.h"
#include "../src/core/model/bank_book.h"
#include <cstdio>
#include <string>
#include <vector>
using namespace reasampler;
using namespace reasampler::model;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
@@ -339,9 +340,9 @@ static void testJsonEmptyBookRoundTrip() {
}
static void testLegacyMigration() {
// A bare legacy bank_index JSON (BankIndex::serialize output — has "samples", no
// A bare legacy bank_index JSON (BankModel::serialize output — has "samples", no
// "banks") must promote into the pool: a book of { pool } with zero named banks.
BankIndex legacy;
BankModel legacy;
CHECK(legacy.add(sampleWith("old1")) == AddResult::Added);
CHECK(legacy.add(sampleWith("old2")) == AddResult::Added);
std::string legacyJson = legacy.serialize();
@@ -361,7 +362,7 @@ static void testLegacyMigration() {
}
// An EMPTY legacy index ("{\"samples\":[]}" style via serialize) also migrates.
BankIndex emptyLegacy;
BankModel emptyLegacy;
auto back2 = BankBook::deserialize(emptyLegacy.serialize());
CHECK(back2.has_value());
CHECK(back2 && back2->size() == 1 && back2->pool().index.empty());
@@ -408,7 +409,7 @@ static void testLoadPrefersBanksBlob() {
CHECK(src.bank("drums")->index.add(sampleWith("new1")) == AddResult::Added);
const std::string banksJson = src.serialize();
BankIndex stale;
BankModel stale;
CHECK(stale.add(sampleWith("stale-old")) == AddResult::Added);
const std::string legacyJson = stale.serialize();
@@ -424,7 +425,7 @@ static void testLoadPrefersBanksBlob() {
static void testLoadMigratesLegacyWhenNoBanks() {
// No `banks` key, a legacy `bank_index` present: migrate into the pool, zero named.
BankIndex legacy;
BankModel legacy;
CHECK(legacy.add(sampleWith("l1")) == AddResult::Added);
CHECK(legacy.add(sampleWith("l2")) == AddResult::Added);
const std::string legacyJson = legacy.serialize();
@@ -449,7 +450,7 @@ static void testLoadEmptyWhenNeither() {
static void testLoadMalformedBanksDegradesWithoutLegacyFallback() {
// A present-but-malformed `banks` blob must degrade to an empty book and must NOT
// resurrect the stale legacy key (that would revive superseded single-bank state).
BankIndex stale;
BankModel stale;
CHECK(stale.add(sampleWith("stale")) == AddResult::Added);
const std::string legacyJson = stale.serialize();
@@ -936,7 +937,7 @@ static void testBankBookSlotsRoundTrip() {
static void testMigrationDefaultsToInsertionOrderDense() {
// A pre-L7 legacy bank_index blob carries no slot data. On load -> reconcileSlots
// seeds dense insertion order (no gaps), so it is visually identical.
BankIndex legacy;
BankModel legacy;
CHECK(legacy.add(sampleWith("o1")) == AddResult::Added);
CHECK(legacy.add(sampleWith("o2")) == AddResult::Added);
CHECK(legacy.add(sampleWith("o3")) == AddResult::Added);
+2 -1
View File
@@ -13,7 +13,7 @@
// keyboard nav (arrow clamp, row moves, shift-extend, partial-last-row clamp,
// fresh-panel focus).
#include "../src/bank_grid.h"
#include "../src/core/ui/bank_grid.h"
#include <cmath>
#include <cstdio>
@@ -21,6 +21,7 @@
#include <vector>
using namespace reasampler;
using namespace reasampler::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+31 -30
View File
@@ -6,12 +6,13 @@
// present AND absent), dedup-by-hash collapse, tier filter + tier move,
// relative-path invariant, empty-index round-trip, malformed/truncated JSON.
#include "../src/bank_model.h"
#include "../src/core/model/bank_model.h"
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::model;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
@@ -65,12 +66,12 @@ static Sample minimalSample(const std::string& seed) {
}
static void testFullFieldRoundTrip() {
BankIndex idx;
BankModel idx;
CHECK(idx.add(fullSample("a")) == AddResult::Added);
CHECK(idx.add(minimalSample("b")) == AddResult::Added);
std::string json = idx.serialize();
auto back = BankIndex::deserialize(json);
auto back = BankModel::deserialize(json);
CHECK(back.has_value());
CHECK(back && *back == idx);
@@ -104,7 +105,7 @@ static void testFullFieldRoundTrip() {
}
static void testDedupByHash() {
BankIndex idx;
BankModel idx;
Sample a = fullSample("x");
CHECK(idx.add(a) == AddResult::Added);
@@ -128,7 +129,7 @@ static void testDedupByHash() {
}
static void testTierFilterAndMove() {
BankIndex idx;
BankModel idx;
Sample scratch = minimalSample("s"); scratch.tier = Tier::Scratch;
Sample archive = fullSample("a"); archive.tier = Tier::Archive;
CHECK(idx.add(scratch) == AddResult::Added);
@@ -153,7 +154,7 @@ static void testTierFilterAndMove() {
}
static void testRelativePathInvariant() {
BankIndex idx;
BankModel idx;
// POSIX absolute, Windows drive, Windows backslash, UNC — all rejected.
const char* absolutes[] = {
@@ -184,7 +185,7 @@ static void testRelativePathInvariant() {
// bypasses dedup (an in-place refresh is not a new insert). The relative-paths-only
// invariant still guards the replacement.
static void testUpdateInPlace() {
BankIndex idx;
BankModel idx;
CHECK(idx.add(minimalSample("a")) == AddResult::Added); // id "min-a"
CHECK(idx.add(minimalSample("b")) == AddResult::Added); // id "min-b"
CHECK(idx.add(minimalSample("c")) == AddResult::Added); // id "min-c"
@@ -221,10 +222,10 @@ static void testUpdateInPlace() {
}
static void testEmptyIndexRoundTrip() {
BankIndex idx;
BankModel idx;
CHECK(idx.empty());
std::string json = idx.serialize();
auto back = BankIndex::deserialize(json);
auto back = BankModel::deserialize(json);
CHECK(back.has_value());
CHECK(back && back->empty());
CHECK(back && *back == idx);
@@ -244,17 +245,17 @@ static void testMalformedJson() {
"{\"samples\":[{\"createdTimestamp\":notanumber}]}",
};
for (const char* j : bad) {
auto r = BankIndex::deserialize(j);
auto r = BankModel::deserialize(j);
CHECK(!r.has_value()); // signaled as nullopt, no crash / UB
}
// A well-formed empty object deserializes to an empty index (lenient root).
auto ok = BankIndex::deserialize("{}");
auto ok = BankModel::deserialize("{}");
CHECK(ok.has_value() && ok->empty());
}
static void testRemoveAndQuery() {
BankIndex idx;
BankModel idx;
CHECK(idx.add(fullSample("1")) == AddResult::Added);
CHECK(idx.add(fullSample("2")) == AddResult::Added);
CHECK(idx.query("id-1") != nullptr);
@@ -267,7 +268,7 @@ static void testRemoveAndQuery() {
// Fix 1: drive-relative and bare-drive forms must be rejected by add().
static void testAbsolutePathDriveRelative() {
BankIndex idx;
BankModel idx;
// Drive-relative: resolves against the drive's CWD, not the project root.
Sample dr = minimalSample("dr");
@@ -295,7 +296,7 @@ static void testAbsolutePathDriveRelative() {
static void testUnicodeEscapeDecoding() {
// é = U+00E9 → 2-byte UTF-8: 0xC3 0xA9
// JSON: "é"
auto r1 = BankIndex::deserialize(
auto r1 = BankModel::deserialize(
"{\"samples\":[{\"id\":\"u1\",\"relativePath\":\"bank/u.wav\","
"\"displayName\":\"\\u00e9\","
"\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0,"
@@ -319,7 +320,7 @@ static void testUnicodeEscapeDecoding() {
// 中 = U+4E2D → 3-byte UTF-8: 0xE4 0xB8 0xAD
// JSON: "中"
auto r2 = BankIndex::deserialize(
auto r2 = BankModel::deserialize(
"{\"samples\":[{\"id\":\"u2\",\"relativePath\":\"bank/u.wav\","
"\"displayName\":\"\\u4e2d\","
"\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0,"
@@ -343,7 +344,7 @@ static void testUnicodeEscapeDecoding() {
}
// 😀 = U+1F600 → surrogate pair 😀 → 4-byte UTF-8: 0xF0 0x9F 0x98 0x80
auto r3 = BankIndex::deserialize(
auto r3 = BankModel::deserialize(
"{\"samples\":[{\"id\":\"u3\",\"relativePath\":\"bank/u.wav\","
"\"displayName\":\"\\uD83D\\uDE00\","
"\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0,"
@@ -368,7 +369,7 @@ static void testUnicodeEscapeDecoding() {
}
// Unpaired high surrogate (no following \uDCxx) → nullopt.
auto r4 = BankIndex::deserialize(
auto r4 = BankModel::deserialize(
"{\"samples\":[{\"id\":\"u4\",\"relativePath\":\"bank/u.wav\","
"\"displayName\":\"\\uD83D\","
"\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0,"
@@ -384,7 +385,7 @@ static void testUnicodeEscapeDecoding() {
// Fix 3: strtoll overflow must reject the value, not clamp it silently.
static void testIntegerOverflow() {
// A timestamp value that overflows int64_t (> 9223372036854775807).
auto r = BankIndex::deserialize(
auto r = BankModel::deserialize(
"{\"samples\":[{\"id\":\"ov1\",\"relativePath\":\"bank/ov.wav\","
"\"displayName\":\"\","
"\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0,"
@@ -400,7 +401,7 @@ static void testIntegerOverflow() {
// Fix 4: out-of-range enum values must reject the sample, not produce invalid enum.
static void testEnumRangeValidation() {
// tier: 99 is not a valid Tier enumerator.
auto r1 = BankIndex::deserialize(
auto r1 = BankModel::deserialize(
"{\"samples\":[{\"id\":\"en1\",\"relativePath\":\"bank/en.wav\","
"\"displayName\":\"\","
"\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0,"
@@ -413,7 +414,7 @@ static void testEnumRangeValidation() {
CHECK(!r1.has_value());
// sourceMode: 99 is not a valid SourceMode enumerator.
auto r2 = BankIndex::deserialize(
auto r2 = BankModel::deserialize(
"{\"samples\":[{\"id\":\"en2\",\"relativePath\":\"bank/en.wav\","
"\"displayName\":\"\","
"\"sourceMode\":99,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0,"
@@ -442,7 +443,7 @@ static void testLegacyJsonDefaults() {
"\"key\":null,\"levels\":{\"peakDb\":0.0,\"rmsDb\":0.0,\"lufs\":0.0},"
"\"clipped\":false,\"tier\":0,\"contentHash\":\"h-leg1\","
"\"provenance\":null,\"createdTimestamp\":0}]}";
auto r = BankIndex::deserialize(legacy);
auto r = BankModel::deserialize(legacy);
CHECK(r.has_value());
if (r) {
const Sample* s = r->query("leg1");
@@ -453,7 +454,7 @@ static void testLegacyJsonDefaults() {
// Re-serialize is lossless: parsing it again yields an equal index. This
// proves the absent fields did not silently gain values on the way out.
std::string out = r->serialize();
auto again = BankIndex::deserialize(out);
auto again = BankModel::deserialize(out);
CHECK(again.has_value());
CHECK(again && *again == *r);
if (again) {
@@ -471,7 +472,7 @@ static void testLegacyJsonDefaults() {
// storing a bogus value.
static void testSeamFieldBoundaries() {
// rootNote at both MIDI edges + equal-and-end-anchored loop points round-trip.
BankIndex idx;
BankModel idx;
Sample lo = minimalSample("lo"); lo.contentHash = "h-lo";
lo.rootNote = 0;
lo.loop = LoopPoints{0, 0}; // zero-length marker at frame 0
@@ -484,7 +485,7 @@ static void testSeamFieldBoundaries() {
CHECK(idx.add(hi) == AddResult::Added);
CHECK(idx.add(end) == AddResult::Added);
auto back = BankIndex::deserialize(idx.serialize());
auto back = BankModel::deserialize(idx.serialize());
CHECK(back.has_value());
CHECK(back && *back == idx);
if (back) {
@@ -514,21 +515,21 @@ static void testSeamFieldBoundaries() {
"\"clipped\":false,\"tier\":0,\"contentHash\":\"h-bad\","
"\"provenance\":null,\"createdTimestamp\":0}]}";
CHECK(!BankIndex::deserialize(std::string(head) + "\"rootNote\":128," + tail).has_value());
CHECK(!BankIndex::deserialize(std::string(head) + "\"rootNote\":-1," + tail).has_value());
CHECK(!BankIndex::deserialize(
CHECK(!BankModel::deserialize(std::string(head) + "\"rootNote\":128," + tail).has_value());
CHECK(!BankModel::deserialize(std::string(head) + "\"rootNote\":-1," + tail).has_value());
CHECK(!BankModel::deserialize(
std::string(head) + "\"loop\":{\"start\":10,\"end\":5}," + tail).has_value()); // start > end
CHECK(!BankIndex::deserialize(
CHECK(!BankModel::deserialize(
std::string(head) + "\"loop\":{\"start\":-1,\"end\":5}," + tail).has_value()); // negative start
}
// S2 test case 3: the seam-field addition is purely additive — dedup-by-hash, tier
// moves/filtering, and BankIndex ordering are byte-for-byte unchanged by the
// moves/filtering, and BankModel ordering are byte-for-byte unchanged by the
// presence (or absence) of rootNote/loop. Two samples differing ONLY in seam fields
// but sharing a content hash still collapse; a seam-populated sample tiers exactly
// like any other.
static void testSeamFieldsAdditiveInvariant() {
BankIndex idx;
BankModel idx;
Sample a = fullSample("z"); // has rootNote + loop populated
CHECK(idx.add(a) == AddResult::Added);
+3 -3
View File
@@ -1,4 +1,4 @@
// Standalone tests for reasampler::vst::bank_sync — no REAPER, no VST3, no framework.
// Standalone tests for reasampler::instrument::map::bank_sync — no REAPER, no VST3, no framework.
// The S9 bank-generation change-detection + the S8 assignment-request consume DECISION
// (the yes/no maths the instrument's off-audio-thread poll runs). The shell owns the
// cadence + side effects; this proves the decision rules without a host.
@@ -8,7 +8,7 @@
// (no request / not-newer / non-target / unresolvable-drop / apply), asserting both the
// apply flag AND the advanced-marker value so a stale request is never re-evaluated.
#include "../src/vst/bank_sync.h"
#include "../src/core/instrument/map/bank_sync.h"
#include <cstdint>
#include <cstdio>
@@ -16,7 +16,7 @@
#include <string>
using namespace reasampler;
using namespace reasampler::vst;
using namespace reasampler::instrument::map;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -9,13 +9,14 @@
// * batch result aggregation: all-success; partial-failure ORDERING (failed
// ordinals reported in unit order); all-failed; single-unit noun singular.
#include "../src/batch_capture.h"
#include "../src/core/capture/batch_capture.h"
#include <cstdio>
#include <string>
#include <vector>
using namespace reasampler;
using namespace reasampler::capture;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+4 -3
View File
@@ -1,4 +1,4 @@
// Standalone tests for reasampler::vst::bridge_marshal — no VST3, no REAPER, no test
// Standalone tests for reasampler::instrument::map::bridge_marshal — no VST3, no REAPER, no test
// framework. Same fast assert loop as the sibling pure tests: assert the REAPER
// bridge-read marshalling (GetProjExtState result decode) directly, so the DAW-facing
// shell only has to invoke the API.
@@ -8,11 +8,12 @@
// (the instrument now parses the bank through the shared bank_book JSON path), so its
// cases are gone with it.
#include "../src/vst/bridge_marshal.h"
#include "../src/core/instrument/map/bridge_marshal.h"
#include <cstdio>
using namespace reasampler::vst;
using namespace reasampler;
using namespace reasampler::instrument::map;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+18 -17
View File
@@ -1,4 +1,4 @@
// Standalone tests for reasampler::vst::browser_scroll — no VST3, no REAPER, no framework.
// Standalone tests for reasampler::instrument::ui::browser_scroll — no VST3, no REAPER, no framework.
// Same fast assert loop as the sibling pure editor tests: assert the S12 scroll-window +
// scrollbar-thumb + type-to-filter-search geometry LAYERED over the S10 capture_browser.
//
@@ -11,13 +11,14 @@
// (case-insensitive substring, empty-query identity, no-match); filterNameIndices preserving order
// and returning every index for an empty query.
#include "../src/vst/browser_scroll.h"
#include "../src/core/instrument/ui/browser_scroll.h"
#include <cstdio>
#include <string>
#include <vector>
using namespace reasampler::vst;
using namespace reasampler;
using namespace reasampler::instrument::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
@@ -45,7 +46,7 @@ static void testMaxOffsetFitsAndOverflows() {
CHECK(scrollMaxOffset(L, L.columns) == 0);
// Many rows overflow -> max = content - gridHeight.
const int many = L.columns * 20;
const int expect = scrollContentHeight(L, many) - L.grid.height();
const int expect = scrollContentHeight(L, many) - L.grid.height;
CHECK(scrollMaxOffset(L, many) == expect);
CHECK(expect > 0);
}
@@ -67,7 +68,7 @@ static void testVisibleRangeTop() {
const VisibleRange vr = visibleCardRange(L, many, 0);
CHECK(vr.first == 0);
// At offset 0, the last visible row is the one containing (gridH-1).
const int expectedLastRow = (L.grid.height() - 1) / kBrowserCardHeight + 1;
const int expectedLastRow = (L.grid.height - 1) / kBrowserCardHeight + 1;
CHECK(vr.last == expectedLastRow * L.columns);
}
@@ -89,30 +90,30 @@ static void testScrolledCellShiftsUp() {
const BrowserLayout L = wideLayout();
const Rect base = cardCellRect(L, 3);
const Rect shifted = scrolledCardCellRect(L, 3, 40);
CHECK(shifted.top == base.top - 40);
CHECK(shifted.bottom == base.bottom - 40);
CHECK(shifted.left == base.left);
CHECK(shifted.y == base.y - 40);
CHECK(shifted.bottom() == base.bottom() - 40);
CHECK(shifted.x == base.x);
}
// --- scrollbar thumb ----------------------------------------------------------
static void testThumbEmptyWhenFits() {
const BrowserLayout L = wideLayout();
CHECK(scrollThumbRect(L, L.columns, 0).height() == 0); // one row fits -> no thumb
CHECK(scrollThumbRect(L, L.columns, 0).height == 0); // one row fits -> no thumb
}
static void testThumbProportionalAndClamped() {
const BrowserLayout L = wideLayout();
const int many = L.columns * 20;
const Rect atTop = scrollThumbRect(L, many, 0);
CHECK(atTop.height() > 0);
CHECK(atTop.top == L.grid.top); // at offset 0 the thumb starts at the track top
CHECK(atTop.width() == kScrollbarWidth);
CHECK(atTop.right == L.grid.right);
CHECK(atTop.height > 0);
CHECK(atTop.y == L.grid.y); // at offset 0 the thumb starts at the track top
CHECK(atTop.width == kScrollbarWidth);
CHECK(atTop.right() == L.grid.right());
// At max offset, the thumb bottom reaches the grid bottom (pinned to the end).
const int maxOff = scrollMaxOffset(L, many);
const Rect atMax = scrollThumbRect(L, many, maxOff);
CHECK(atMax.bottom == L.grid.top + L.grid.height());
CHECK(atMax.bottom() == L.grid.y + L.grid.height);
}
static void testThumbDragIsInverse() {
@@ -126,7 +127,7 @@ static void testThumbDragIsInverse() {
CHECK(thumbDragToOffset(L, many, maxOff, -100000) == 0);
// Dragging the thumb by the whole track span from top reaches (near) max.
const Rect thumb = scrollThumbRect(L, many, 0);
const int trackSpan = L.grid.height() - thumb.height();
const int trackSpan = L.grid.height - thumb.height;
const int off = thumbDragToOffset(L, many, 0, trackSpan);
CHECK(off >= maxOff - 2 && off <= maxOff);
}
@@ -135,8 +136,8 @@ static void testThumbDragIsInverse() {
static void testSearchBoxRect() {
const Rect r = searchBoxRect(200);
CHECK(r.left == 0 && r.top == 0 && r.right == 200 && r.height() == kSearchBoxHeight);
CHECK(searchBoxRect(0).width() == 0);
CHECK(r.x == 0 && r.y == 0 && r.right() == 200 && r.height == kSearchBoxHeight);
CHECK(searchBoxRect(0).width == 0);
}
static void testNameMatch() {
+49 -48
View File
@@ -1,4 +1,4 @@
// Standalone tests for reasampler::vst::capture_browser — no VST3, no REAPER, no framework.
// Standalone tests for reasampler::instrument::ui::capture_browser — no VST3, no REAPER, no framework.
// Same fast assert loop as the sibling pure tests (embed_strip / editor_geometry): assert
// the capture-first browser's card-grid + bank-filter-tab layout and hit-testing directly.
//
@@ -10,11 +10,12 @@
// the strip into equal segments with the last tab absorbing the remainder; filterTabHitTest
// hitting each tab and missing off-strip.
#include "../src/vst/capture_browser.h"
#include "../src/core/instrument/ui/capture_browser.h"
#include <cstdio>
using namespace reasampler::vst;
using namespace reasampler;
using namespace reasampler::instrument::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
@@ -25,12 +26,12 @@ static int g_fail = 0;
static void testLayoutNormalArea() {
// Wide enough for several columns of the fixed-width card.
const BrowserLayout L = layoutBrowser(560, 300);
CHECK(L.tabStrip.left == 0 && L.tabStrip.top == 0 && L.tabStrip.right == 560);
CHECK(L.tabStrip.height() == kBrowserTabHeight);
CHECK(L.tabStrip.x == 0 && L.tabStrip.y == 0 && L.tabStrip.right() == 560);
CHECK(L.tabStrip.height == kBrowserTabHeight);
// The grid starts right below the tab strip and fills the rest, contiguous.
CHECK(L.grid.top == L.tabStrip.bottom);
CHECK(L.grid.bottom == 300 && L.grid.right == 560);
// columns = grid.width() / cardWidth (>= 1).
CHECK(L.grid.y == L.tabStrip.bottom());
CHECK(L.grid.bottom() == 300 && L.grid.right() == 560);
// columns = grid.width / cardWidth (>= 1).
CHECK(L.columns == 560 / kBrowserCardWidth);
CHECK(L.columns >= 1);
}
@@ -39,22 +40,22 @@ static void testLayoutNarrowAreaSingleColumn() {
// Narrower than one card: still a single column, no inversion.
const BrowserLayout L = layoutBrowser(kBrowserCardWidth - 10, 200);
CHECK(L.columns == 1);
CHECK(L.grid.width() >= 0);
CHECK(L.tabStrip.height() == kBrowserTabHeight);
CHECK(L.grid.width >= 0);
CHECK(L.tabStrip.height == kBrowserTabHeight);
}
static void testLayoutZeroArea() {
const BrowserLayout L = layoutBrowser(0, 0);
CHECK(L.tabStrip.width() == 0 && L.tabStrip.height() == 0);
CHECK(L.grid.width() == 0);
CHECK(L.tabStrip.width == 0 && L.tabStrip.height == 0);
CHECK(L.grid.width == 0);
CHECK(L.columns == 1); // never zero (avoids a divide-by-zero in card layout)
}
static void testLayoutTinyHeightClampsTabStrip() {
// A height below the tab band: the tab strip clamps to the area, the grid is empty.
const BrowserLayout L = layoutBrowser(560, kBrowserTabHeight - 6);
CHECK(L.tabStrip.height() == kBrowserTabHeight - 6);
CHECK(L.grid.height() <= 0); // no room left for cards
CHECK(L.tabStrip.height == kBrowserTabHeight - 6);
CHECK(L.grid.height <= 0); // no room left for cards
}
// --- card rects ---------------------------------------------------------------
@@ -64,32 +65,32 @@ static void testCardCellsTileRowMajor() {
const int cols = L.columns;
// Card 0 is top-left of the grid.
const Rect c0 = cardCellRect(L, 0);
CHECK(c0.left == L.grid.left && c0.top == L.grid.top);
CHECK(c0.width() == kBrowserCardWidth && c0.height() == kBrowserCardHeight);
CHECK(c0.x == L.grid.x && c0.y == L.grid.y);
CHECK(c0.width == kBrowserCardWidth && c0.height == kBrowserCardHeight);
// Card 1 is one card-width to the right, same row.
const Rect c1 = cardCellRect(L, 1);
CHECK(c1.left == L.grid.left + kBrowserCardWidth);
CHECK(c1.top == c0.top);
CHECK(c1.x == L.grid.x + kBrowserCardWidth);
CHECK(c1.y == c0.y);
// The first card of the SECOND row wraps back to the left, one card-height down.
const Rect wrap = cardCellRect(L, cols);
CHECK(wrap.left == L.grid.left);
CHECK(wrap.top == L.grid.top + kBrowserCardHeight);
CHECK(wrap.x == L.grid.x);
CHECK(wrap.y == L.grid.y + kBrowserCardHeight);
}
static void testCardCellNegativeIndex() {
const BrowserLayout L = layoutBrowser(560, 300);
const Rect r = cardCellRect(L, -1);
CHECK(r.left == 0 && r.top == 0 && r.right == 0 && r.bottom == 0);
CHECK(r.x == 0 && r.y == 0 && r.right() == 0 && r.bottom() == 0);
}
static void testCardContentInsetByGutter() {
const BrowserLayout L = layoutBrowser(560, 300);
const Rect cell = cardCellRect(L, 0);
const Rect content = cardContentRect(L, 0);
CHECK(content.left == cell.left + kBrowserCardGutter);
CHECK(content.top == cell.top + kBrowserCardGutter);
CHECK(content.right == cell.right - kBrowserCardGutter);
CHECK(content.bottom == cell.bottom - kBrowserCardGutter);
CHECK(content.x == cell.x + kBrowserCardGutter);
CHECK(content.y == cell.y + kBrowserCardGutter);
CHECK(content.right() == cell.right() - kBrowserCardGutter);
CHECK(content.bottom() == cell.bottom() - kBrowserCardGutter);
}
static void testThumbnailAboveLabel() {
@@ -98,12 +99,12 @@ static void testThumbnailAboveLabel() {
const Rect thumb = cardThumbnailRect(L, 0);
const Rect label = cardLabelRect(L, 0);
// Thumbnail is the top band of the content; the label is the remainder below it, contiguous.
CHECK(thumb.left == content.left && thumb.right == content.right);
CHECK(thumb.top == content.top);
CHECK(thumb.height() == kBrowserThumbHeight);
CHECK(label.top == thumb.bottom);
CHECK(label.bottom == content.bottom);
CHECK(label.left == content.left && label.right == content.right);
CHECK(thumb.x == content.x && thumb.right() == content.right());
CHECK(thumb.y == content.y);
CHECK(thumb.height == kBrowserThumbHeight);
CHECK(label.y == thumb.bottom());
CHECK(label.bottom() == content.bottom());
CHECK(label.x == content.x && label.right() == content.right());
}
// --- cardHitTest --------------------------------------------------------------
@@ -111,8 +112,8 @@ static void testThumbnailAboveLabel() {
static void testCardHitCenterOfCard() {
const BrowserLayout L = layoutBrowser(560, 300);
const Rect content = cardContentRect(L, 3);
const int cx = content.left + content.width() / 2;
const int cy = content.top + content.height() / 2;
const int cx = content.x + content.width / 2;
const int cy = content.y + content.height / 2;
CHECK(cardHitTest(L, 12, cx, cy) == 3);
}
@@ -121,21 +122,21 @@ static void testCardHitMissesGutter() {
// A point in the gutter between the content and the cell edge (top-left corner of cell 0)
// is a miss — only the card CONTENT counts.
const Rect cell = cardCellRect(L, 0);
CHECK(cardHitTest(L, 12, cell.left, cell.top) == -1);
CHECK(cardHitTest(L, 12, cell.x, cell.y) == -1);
}
static void testCardHitMissesPastLastCard() {
const BrowserLayout L = layoutBrowser(560, 300);
// Only 2 cards exist; a point on where card 5 WOULD be is a miss.
const Rect content = cardContentRect(L, 5);
const int cx = content.left + content.width() / 2;
const int cy = content.top + content.height() / 2;
const int cx = content.x + content.width / 2;
const int cy = content.y + content.height / 2;
CHECK(cardHitTest(L, 2, cx, cy) == -1);
}
static void testCardHitMissesTabStrip() {
const BrowserLayout L = layoutBrowser(560, 300);
CHECK(cardHitTest(L, 12, 10, L.tabStrip.top + 2) == -1);
CHECK(cardHitTest(L, 12, 10, L.tabStrip.y + 2) == -1);
}
static void testCardHitZeroCards() {
@@ -150,21 +151,21 @@ static void testFilterTabsTileStrip() {
const int n = 4; // "All" + 3 banks
const Rect t0 = filterTabRect(L, n, 0);
const Rect tLast = filterTabRect(L, n, n - 1);
CHECK(t0.left == L.tabStrip.left);
CHECK(t0.x == L.tabStrip.x);
// Adjacent tabs share an exact edge (no gap).
CHECK(filterTabRect(L, n, 0).right == filterTabRect(L, n, 1).left);
CHECK(filterTabRect(L, n, 1).right == filterTabRect(L, n, 2).left);
CHECK(filterTabRect(L, n, 0).right() == filterTabRect(L, n, 1).x);
CHECK(filterTabRect(L, n, 1).right() == filterTabRect(L, n, 2).x);
// The last tab reaches the strip's right edge exactly (absorbs the remainder).
CHECK(tLast.right == L.tabStrip.right);
CHECK(tLast.right() == L.tabStrip.right());
// All tabs share the strip's height.
CHECK(t0.top == L.tabStrip.top && t0.bottom == L.tabStrip.bottom);
CHECK(t0.y == L.tabStrip.y && t0.bottom() == L.tabStrip.bottom());
}
static void testFilterTabOutOfRange() {
const BrowserLayout L = layoutBrowser(560, 300);
CHECK(filterTabRect(L, 3, -1).width() == 0);
CHECK(filterTabRect(L, 3, 3).width() == 0);
CHECK(filterTabRect(L, 0, 0).width() == 0);
CHECK(filterTabRect(L, 3, -1).width == 0);
CHECK(filterTabRect(L, 3, 3).width == 0);
CHECK(filterTabRect(L, 0, 0).width == 0);
}
static void testFilterTabHit() {
@@ -172,12 +173,12 @@ static void testFilterTabHit() {
const int n = 3;
for (int i = 0; i < n; ++i) {
const Rect t = filterTabRect(L, n, i);
const int cx = t.left + t.width() / 2;
const int cy = t.top + t.height() / 2;
const int cx = t.x + t.width / 2;
const int cy = t.y + t.height / 2;
CHECK(filterTabHitTest(L, n, cx, cy) == i);
}
// Below the strip (in the grid) -> no tab.
CHECK(filterTabHitTest(L, n, 20, L.grid.top + 4) == -1);
CHECK(filterTabHitTest(L, n, 20, L.grid.y + 4) == -1);
}
int main() {
+4 -3
View File
@@ -1,9 +1,9 @@
// Standalone tests for reasampler::capture_paths — no REAPER, no framework.
// The capture shell is DAW-bound and only verifiable in REAPER; this covers the
// one genuinely pure piece: the bank-folder / unique-name / project-relative
// path arithmetic that feeds BankIndex::add's relative-only invariant.
// path arithmetic that feeds BankModel::add's relative-only invariant.
#include "../src/capture_paths.h"
#include "../src/core/capture/capture_paths.h"
#include <cstdint>
#include <cstdio>
@@ -12,6 +12,7 @@
#include <vector>
using namespace reasampler;
using namespace reasampler::capture;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
@@ -74,7 +75,7 @@ static void testDeriveRelativePathIsProjectRelative() {
BankPaths p = deriveBankPaths("C:\\Users\\d\\proj", "master mix", "1753080000");
// Relative path is under the fixed bank subfolder, forward-slashed, .wav.
CHECK(p.relativePath == "reasampler_bank/master_mix_1753080000.wav");
// It must NOT be absolute by any of BankIndex::add's rejection rules:
// It must NOT be absolute by any of BankModel::add's rejection rules:
// no leading '/', no drive letter, no backslash, no UNC prefix.
CHECK(p.relativePath.find(':') == std::string::npos);
CHECK(p.relativePath.find('\\') == std::string::npos);
+2 -1
View File
@@ -7,11 +7,12 @@
// -> None); cursor-cue mapping (incl. Replace only for Replace); slot rects include empties
// (gap layout), dense layout matches a plain grid, slot hit-test returns slot index + miss.
#include "../src/card_drag.h"
#include "../src/core/ui/card_drag.h"
#include <cstdio>
using namespace reasampler;
using namespace reasampler::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -6,12 +6,13 @@
// long capture, non-4/4 meters (3/4 and 6/8), unstamped meter -> blank, unknown tempo ->
// blank (s.ms still derivable); s.ms zero / sub-second / multi-second / ms carry / negative.
#include "../src/card_meta.h"
#include "../src/core/ui/card_meta.h"
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -7,11 +7,12 @@
// row, hover hit-test returns the right row and "no hit" outside/past the last row; and the
// shared half-open box hit-test agrees with layout (no double-claimed pixel).
#include "../src/component_geometry.h"
#include "../src/core/ui/component_geometry.h"
#include <cstdio>
using namespace reasampler;
using namespace reasampler::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+33 -32
View File
@@ -1,14 +1,15 @@
// Standalone tests for reasampler::vst::curve_popup — no VST3, no REAPER, no framework. Same
// Standalone tests for reasampler::instrument::ui::curve_popup — no VST3, no REAPER, no framework. Same
// fast assert loop as the sibling pure tests. Assert the r11 popup-sheet geometry at the size
// clamps (the spec's width clamp(60%, 360..520) / height clamp(55%, 260..380)), the centering,
// the title-row/close-button placement, the curve-box remainder, and the outside-sheet
// dismissal test.
#include "../src/vst/curve_popup.h"
#include "../src/core/instrument/ui/curve_popup.h"
#include <cstdio>
using namespace reasampler::vst;
using namespace reasampler;
using namespace reasampler::instrument::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
@@ -17,66 +18,66 @@ static int g_fail = 0;
static void testDefaultWindowMidClamp() {
// 840x620: 60% = 504 (inside 360..520), 55% = 341 (inside 260..380).
const CurvePopupLayout pl = computeCurvePopup(840, 620);
CHECK(pl.sheet.width() == 504);
CHECK(pl.sheet.height() == 341);
CHECK(pl.sheet.width == 504);
CHECK(pl.sheet.height == 341);
// Centered (within the integer-division pixel).
CHECK(pl.sheet.left == (840 - 504) / 2);
CHECK(pl.sheet.top == (620 - 341) / 2);
CHECK(pl.sheet.x == (840 - 504) / 2);
CHECK(pl.sheet.y == (620 - 341) / 2);
}
static void testMinClamp() {
// The 560x460 constraint floor: 60% = 336 -> clamps UP to 360; 55% = 253 -> up to 260.
const CurvePopupLayout pl = computeCurvePopup(560, 460);
CHECK(pl.sheet.width() == kCurvePopupMinW);
CHECK(pl.sheet.height() == kCurvePopupMinH);
CHECK(pl.sheet.left >= 0 && pl.sheet.right <= 560);
CHECK(pl.sheet.top >= 0 && pl.sheet.bottom <= 460);
CHECK(pl.sheet.width == kCurvePopupMinW);
CHECK(pl.sheet.height == kCurvePopupMinH);
CHECK(pl.sheet.x >= 0 && pl.sheet.right() <= 560);
CHECK(pl.sheet.y >= 0 && pl.sheet.bottom() <= 460);
}
static void testMaxClamp() {
// A large window: 60% of 1600 = 960 -> clamps DOWN to 520; 55% of 900 = 495 -> down to 380.
const CurvePopupLayout pl = computeCurvePopup(1600, 900);
CHECK(pl.sheet.width() == kCurvePopupMaxW);
CHECK(pl.sheet.height() == kCurvePopupMaxH);
CHECK(pl.sheet.width == kCurvePopupMaxW);
CHECK(pl.sheet.height == kCurvePopupMaxH);
}
static void testDegenerateWindowNeverOverhangs() {
// A window smaller than the min clamp: the sheet caps at the window dimension (defensive —
// below checkSizeConstraint, but geometry must stay sane).
const CurvePopupLayout pl = computeCurvePopup(300, 200);
CHECK(pl.sheet.width() == 300);
CHECK(pl.sheet.height() == 200);
CHECK(pl.sheet.left == 0 && pl.sheet.top == 0);
CHECK(pl.sheet.width == 300);
CHECK(pl.sheet.height == 200);
CHECK(pl.sheet.x == 0 && pl.sheet.y == 0);
}
static void testTitleRowAndCurveBox() {
const CurvePopupLayout pl = computeCurvePopup(840, 620);
// Close: 18x18, right-anchored inside the title row.
CHECK(pl.close.width() == kCurvePopupCloseSize && pl.close.height() == kCurvePopupCloseSize);
CHECK(pl.close.right == pl.sheet.right - kCurvePopupPad);
CHECK(pl.close.top >= pl.sheet.top);
CHECK(pl.close.bottom <= pl.sheet.top + kCurvePopupTitleH);
CHECK(pl.close.width == kCurvePopupCloseSize && pl.close.height == kCurvePopupCloseSize);
CHECK(pl.close.right() == pl.sheet.right() - kCurvePopupPad);
CHECK(pl.close.y >= pl.sheet.y);
CHECK(pl.close.bottom() <= pl.sheet.y + kCurvePopupTitleH);
// Title text: left of the close button, in the title row.
CHECK(pl.title.left == pl.sheet.left + kCurvePopupPad);
CHECK(pl.title.right <= pl.close.left);
CHECK(pl.title.x == pl.sheet.x + kCurvePopupPad);
CHECK(pl.title.right() <= pl.close.x);
// Curve box: fills the remainder below the title row, inside the sheet margins.
CHECK(pl.curveBox.top >= pl.sheet.top + kCurvePopupTitleH);
CHECK(pl.curveBox.left == pl.sheet.left + kCurvePopupPad);
CHECK(pl.curveBox.right == pl.sheet.right - kCurvePopupPad);
CHECK(pl.curveBox.bottom == pl.sheet.bottom - kCurvePopupPad);
CHECK(pl.curveBox.width() > 0 && pl.curveBox.height() > 0);
CHECK(pl.curveBox.y >= pl.sheet.y + kCurvePopupTitleH);
CHECK(pl.curveBox.x == pl.sheet.x + kCurvePopupPad);
CHECK(pl.curveBox.right() == pl.sheet.right() - kCurvePopupPad);
CHECK(pl.curveBox.bottom() == pl.sheet.bottom() - kCurvePopupPad);
CHECK(pl.curveBox.width > 0 && pl.curveBox.height > 0);
}
static void testOutsideSheetDismissTest() {
const CurvePopupLayout pl = computeCurvePopup(840, 620);
// On the wash: outside.
CHECK(popupOutsideSheet(pl, 0, 0));
CHECK(popupOutsideSheet(pl, pl.sheet.left - 1, pl.sheet.top + 10));
CHECK(popupOutsideSheet(pl, pl.sheet.right, pl.sheet.top + 10)); // half-open right edge
CHECK(popupOutsideSheet(pl, pl.sheet.x - 1, pl.sheet.y + 10));
CHECK(popupOutsideSheet(pl, pl.sheet.right(), pl.sheet.y + 10)); // half-open right edge
// On the sheet (title row, curve box, padding): inside.
CHECK(!popupOutsideSheet(pl, pl.sheet.left, pl.sheet.top));
CHECK(!popupOutsideSheet(pl, pl.curveBox.left + 5, pl.curveBox.top + 5));
CHECK(!popupOutsideSheet(pl, pl.sheet.right - 1, pl.sheet.bottom - 1));
CHECK(!popupOutsideSheet(pl, pl.sheet.x, pl.sheet.y));
CHECK(!popupOutsideSheet(pl, pl.curveBox.x + 5, pl.curveBox.y + 5));
CHECK(!popupOutsideSheet(pl, pl.sheet.right() - 1, pl.sheet.bottom() - 1));
}
int main() {
+2 -1
View File
@@ -9,13 +9,14 @@
// * Path-list assembly: single, multi, dedupe (cross-bank copy case), skip-missing,
// skip-unresolved, empty selection, order preservation, mixed tallies.
#include "../src/drag_out.h"
#include "../src/core/ui/drag_out.h"
#include <cstdio>
#include <string>
#include <vector>
using namespace reasampler;
using namespace reasampler::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+99 -98
View File
@@ -1,4 +1,4 @@
// Standalone tests for reasampler::vst::editor_geometry — no VST3, no REAPER, no test
// Standalone tests for reasampler::instrument::ui::editor_geometry — no VST3, no REAPER, no test
// framework. Same fast assert loop as the sibling pure tests (mode_switch et al.):
// assert the IPlugView LICE editor's layout math + hit-testing directly.
//
@@ -8,11 +8,12 @@
// the button, missing on the title/canvas, missing outside the surface, and boundary
// pixels; layout<->hit-test agreement (a click on the drawn button rect hits it).
#include "../src/vst/editor_geometry.h"
#include "../src/core/instrument/ui/editor_geometry.h"
#include <cstdio>
using namespace reasampler::vst;
using namespace reasampler;
using namespace reasampler::instrument::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
@@ -21,7 +22,7 @@ static int g_fail = 0;
// --- contains() ---------------------------------------------------------------
static void testContainsHalfOpen() {
Rect r{10, 20, 50, 40}; // [10,50) x [20,40)
Rect r = Rect::ltrb(10, 20, 50, 40); // [10,50) x [20,40)
CHECK(contains(r, 10, 20)); // top-left inclusive
CHECK(contains(r, 49, 39)); // bottom-right exclusive edge, inside
CHECK(!contains(r, 50, 30)); // right edge excluded
@@ -31,9 +32,9 @@ static void testContainsHalfOpen() {
}
static void testContainsDegenerate() {
CHECK(!contains(Rect{10, 10, 10, 20}, 10, 15)); // zero width
CHECK(!contains(Rect{10, 10, 20, 10}, 15, 10)); // zero height
CHECK(!contains(Rect{20, 10, 10, 20}, 15, 15)); // inverted (right < left)
CHECK(!contains(Rect::ltrb(10, 10, 10, 20), 10, 15)); // zero width
CHECK(!contains(Rect::ltrb(10, 10, 20, 10), 15, 10)); // zero height
CHECK(!contains(Rect::ltrb(20, 10, 10, 20), 15, 15)); // inverted (right < left)
}
// --- layoutEditor: normal view ------------------------------------------------
@@ -43,20 +44,20 @@ static void testLayoutNormalView() {
// rest; button sits inside the canvas, inset by the margin.
const EditorLayout L = layoutEditor(400, 260);
CHECK(L.titleBar.left == 0 && L.titleBar.top == 0);
CHECK(L.titleBar.right == 400);
CHECK(L.titleBar.height() > 0 && L.titleBar.height() <= 260);
CHECK(L.titleBar.x == 0 && L.titleBar.y == 0);
CHECK(L.titleBar.right() == 400);
CHECK(L.titleBar.height > 0 && L.titleBar.height <= 260);
// Canvas begins right below the title bar and reaches the bottom-right.
CHECK(L.canvas.top == L.titleBar.bottom);
CHECK(L.canvas.right == 400 && L.canvas.bottom == 260);
CHECK(L.canvas.y == L.titleBar.bottom());
CHECK(L.canvas.right() == 400 && L.canvas.bottom() == 260);
// Button is inside the canvas (does not overhang any edge).
CHECK(L.button.left >= L.canvas.left);
CHECK(L.button.top >= L.canvas.top);
CHECK(L.button.right <= L.canvas.right);
CHECK(L.button.bottom <= L.canvas.bottom);
CHECK(L.button.width() > 0 && L.button.height() > 0);
CHECK(L.button.x >= L.canvas.x);
CHECK(L.button.y >= L.canvas.y);
CHECK(L.button.right() <= L.canvas.right());
CHECK(L.button.bottom() <= L.canvas.bottom());
CHECK(L.button.width > 0 && L.button.height > 0);
}
// --- layoutEditor: tiny view (clamping) ---------------------------------------
@@ -65,25 +66,25 @@ static void testLayoutTinyViewClampsButton() {
// A view narrower/shorter than the button's natural size: the button must clamp to
// the canvas and never produce an inverted or overhanging rect.
const EditorLayout L = layoutEditor(40, 40);
CHECK(L.button.right <= L.canvas.right);
CHECK(L.button.bottom <= L.canvas.bottom);
CHECK(L.button.right >= L.button.left); // never inverted
CHECK(L.button.bottom >= L.button.top);
CHECK(L.button.right() <= L.canvas.right());
CHECK(L.button.bottom() <= L.canvas.bottom());
CHECK(L.button.right() >= L.button.x); // never inverted
CHECK(L.button.bottom() >= L.button.y);
// Title bar clamps to the client height when the view is shorter than its height.
CHECK(L.titleBar.bottom <= 40);
CHECK(L.titleBar.bottom() <= 40);
}
// --- layoutEditor: zero view (all empty, no inversion) ------------------------
static void testLayoutZeroView() {
const EditorLayout L = layoutEditor(0, 0);
CHECK(L.titleBar.width() <= 0 || L.titleBar.height() <= 0);
CHECK(L.canvas.width() <= 0 || L.canvas.height() <= 0);
CHECK(L.titleBar.width <= 0 || L.titleBar.height <= 0);
CHECK(L.canvas.width <= 0 || L.canvas.height <= 0);
// No rect is inverted.
CHECK(L.button.right >= L.button.left);
CHECK(L.button.bottom >= L.button.top);
CHECK(L.canvas.right >= L.canvas.left);
CHECK(L.canvas.bottom >= L.canvas.top);
CHECK(L.button.right() >= L.button.x);
CHECK(L.button.bottom() >= L.button.y);
CHECK(L.canvas.right() >= L.canvas.x);
CHECK(L.canvas.bottom() >= L.canvas.y);
// A click anywhere on an empty layout hits nothing.
CHECK(hitTest(L, 0, 0) == HitTarget::kNone);
CHECK(hitTest(L, 5, 5) == HitTarget::kNone);
@@ -94,15 +95,15 @@ static void testLayoutZeroView() {
static void testHitTestButton() {
const EditorLayout L = layoutEditor(400, 260);
// Center of the button hits it.
const int cx = (L.button.left + L.button.right) / 2;
const int cy = (L.button.top + L.button.bottom) / 2;
const int cx = (L.button.x + L.button.right()) / 2;
const int cy = (L.button.y + L.button.bottom()) / 2;
CHECK(hitTest(L, cx, cy) == HitTarget::kButton);
}
static void testHitTestMissesNonButton() {
const EditorLayout L = layoutEditor(400, 260);
// Title bar is inert in the spike.
CHECK(hitTest(L, 200, L.titleBar.top + 1) == HitTarget::kNone);
CHECK(hitTest(L, 200, L.titleBar.y + 1) == HitTarget::kNone);
// Empty canvas away from the button.
CHECK(hitTest(L, 380, 240) == HitTarget::kNone);
// Outside the surface entirely.
@@ -113,9 +114,9 @@ static void testHitTestMissesNonButton() {
static void testHitTestButtonBoundary() {
const EditorLayout L = layoutEditor(400, 260);
// Top-left corner of the button is inclusive; the right/bottom edges are excluded.
CHECK(hitTest(L, L.button.left, L.button.top) == HitTarget::kButton);
CHECK(hitTest(L, L.button.right, L.button.top) == HitTarget::kNone);
CHECK(hitTest(L, L.button.left, L.button.bottom) == HitTarget::kNone);
CHECK(hitTest(L, L.button.x, L.button.y) == HitTarget::kButton);
CHECK(hitTest(L, L.button.right(), L.button.y) == HitTarget::kNone);
CHECK(hitTest(L, L.button.x, L.button.bottom()) == HitTarget::kNone);
}
// --- layout<->hit-test agreement ----------------------------------------------
@@ -124,8 +125,8 @@ static void testHitTestButtonBoundary() {
// load-bearing consistency invariant between what the shell draws and what it routes.
static void testHitTestMatchesDrawnButton() {
const EditorLayout L = layoutEditor(320, 200);
for (int y = L.button.top; y < L.button.bottom; ++y) {
for (int x = L.button.left; x < L.button.right; ++x) {
for (int y = L.button.y; y < L.button.bottom(); ++y) {
for (int x = L.button.x; x < L.button.right(); ++x) {
CHECK(hitTest(L, x, y) == HitTarget::kButton);
}
}
@@ -138,14 +139,14 @@ static void testSampleRowRectStacks() {
const Rect r0 = sampleRowRect(L, 0);
const Rect r1 = sampleRowRect(L, 1);
// Row 0 starts at the canvas top and spans its full width.
CHECK(r0.top == L.canvas.top);
CHECK(r0.left == L.canvas.left && r0.right == L.canvas.right);
CHECK(r0.height() == kSampleRowHeight);
CHECK(r0.y == L.canvas.y);
CHECK(r0.x == L.canvas.x && r0.right() == L.canvas.right());
CHECK(r0.height == kSampleRowHeight);
// Row 1 sits directly below row 0 (no gap, no overlap).
CHECK(r1.top == r0.bottom);
CHECK(r1.height() == kSampleRowHeight);
CHECK(r1.y == r0.bottom());
CHECK(r1.height == kSampleRowHeight);
// A negative index is an empty rect.
CHECK(sampleRowRect(L, -1).width() == 0 && sampleRowRect(L, -1).height() == 0);
CHECK(sampleRowRect(L, -1).width == 0 && sampleRowRect(L, -1).height == 0);
}
static void testSampleRowHitTestMapsClickToRow() {
@@ -153,35 +154,35 @@ static void testSampleRowHitTestMapsClickToRow() {
const int rows = 5;
// A click in the vertical middle of row 2 resolves to index 2.
const Rect r2 = sampleRowRect(L, 2);
const int midY = (r2.top + r2.bottom) / 2;
const int midY = (r2.y + r2.bottom()) / 2;
CHECK(sampleRowHitTest(L, rows, 200, midY) == 2);
// Row 0's top-left corner hits row 0.
const Rect r0 = sampleRowRect(L, 0);
CHECK(sampleRowHitTest(L, rows, r0.left, r0.top) == 0);
CHECK(sampleRowHitTest(L, rows, r0.x, r0.y) == 0);
}
static void testSampleRowHitTestMisses() {
const EditorLayout L = layoutEditor(400, 260);
const int rows = 3;
// Above the first row (in the title bar) -> no row.
CHECK(sampleRowHitTest(L, rows, 200, L.titleBar.top) == -1);
CHECK(sampleRowHitTest(L, rows, 200, L.titleBar.y) == -1);
// Below the last row -> no row.
const Rect last = sampleRowRect(L, rows - 1);
CHECK(sampleRowHitTest(L, rows, 200, last.bottom + 1) == -1);
CHECK(sampleRowHitTest(L, rows, 200, last.bottom() + 1) == -1);
// Left of the canvas -> no row.
CHECK(sampleRowHitTest(L, rows, L.canvas.left - 1, last.top) == -1);
CHECK(sampleRowHitTest(L, rows, L.canvas.x - 1, last.y) == -1);
// Zero rows -> always -1.
CHECK(sampleRowHitTest(L, 0, 200, L.canvas.top + 1) == -1);
// At or below canvas.bottom -> always -1, even if rowCount would cover that y.
CHECK(sampleRowHitTest(L, 0, 200, L.canvas.y + 1) == -1);
// At or below canvas.bottom() -> always -1, even if rowCount would cover that y.
// This guards paint<->hit-test agreement: sampleRowRect does not clamp to canvas,
// so without this clip a row that extends past canvas.bottom would hit-test but
// so without this clip a row that extends past canvas.bottom() would hit-test but
// never be drawn (or vice versa).
CHECK(sampleRowHitTest(L, rows, 200, L.canvas.bottom) == -1);
CHECK(sampleRowHitTest(L, rows, 200, L.canvas.bottom()) == -1);
// Use a large rowCount so index arithmetic would return a valid row without the
// canvas.bottom guard — proving the guard fires independently of rowCount.
// canvas.bottom() guard — proving the guard fires independently of rowCount.
const int bigRows = 1000;
CHECK(sampleRowHitTest(L, bigRows, 200, L.canvas.bottom) == -1);
CHECK(sampleRowHitTest(L, bigRows, 200, L.canvas.bottom + 5) == -1);
CHECK(sampleRowHitTest(L, bigRows, 200, L.canvas.bottom()) == -1);
CHECK(sampleRowHitTest(L, bigRows, 200, L.canvas.bottom() + 5) == -1);
}
// The drawn-row <-> hit-test agreement: every pixel inside a row rect must resolve to
@@ -191,10 +192,10 @@ static void testSampleRowHitTestMatchesDrawnRows() {
const int rows = 4;
for (int i = 0; i < rows; ++i) {
const Rect r = sampleRowRect(L, i);
if (r.top >= L.canvas.bottom) break; // clipped rows aren't clickable targets
const int y = (r.top + r.bottom) / 2;
if (y >= L.canvas.bottom) continue;
CHECK(sampleRowHitTest(L, rows, r.left + 1, y) == i);
if (r.y >= L.canvas.bottom()) break; // clipped rows aren't clickable targets
const int y = (r.y + r.bottom()) / 2;
if (y >= L.canvas.bottom()) continue;
CHECK(sampleRowHitTest(L, rows, r.x + 1, y) == i);
}
}
@@ -204,30 +205,30 @@ static void testKeymapLayoutSplitsCanvas() {
const KeymapEditorLayout L = layoutKeymapEditor(600, 300);
// The left sample list and right zone panel partition the canvas with no overlap and
// no gap: the list's right edge is the panel's left edge.
CHECK(L.sampleList.left == L.base.canvas.left);
CHECK(L.sampleList.right == L.zonePanel.left);
CHECK(L.zonePanel.right == L.base.canvas.right);
CHECK(L.sampleList.top == L.base.canvas.top);
CHECK(L.zonePanel.top == L.base.canvas.top);
CHECK(L.sampleList.bottom == L.base.canvas.bottom);
CHECK(L.zonePanel.bottom == L.base.canvas.bottom);
CHECK(L.sampleList.width() > 0 && L.zonePanel.width() > 0);
CHECK(L.sampleList.x == L.base.canvas.x);
CHECK(L.sampleList.right() == L.zonePanel.x);
CHECK(L.zonePanel.right() == L.base.canvas.right());
CHECK(L.sampleList.y == L.base.canvas.y);
CHECK(L.zonePanel.y == L.base.canvas.y);
CHECK(L.sampleList.bottom() == L.base.canvas.bottom());
CHECK(L.zonePanel.bottom() == L.base.canvas.bottom());
CHECK(L.sampleList.width > 0 && L.zonePanel.width > 0);
// Add-Zone button caps the panel; zone rows stack below it.
CHECK(L.addZoneButton.top == L.zonePanel.top);
CHECK(L.addZoneButton.left == L.zonePanel.left && L.addZoneButton.right == L.zonePanel.right);
CHECK(L.zoneRowArea.top == L.addZoneButton.bottom);
CHECK(L.zoneRowArea.bottom == L.zonePanel.bottom);
CHECK(L.addZoneButton.y == L.zonePanel.y);
CHECK(L.addZoneButton.x == L.zonePanel.x && L.addZoneButton.right() == L.zonePanel.right());
CHECK(L.zoneRowArea.y == L.addZoneButton.bottom());
CHECK(L.zoneRowArea.bottom() == L.zonePanel.bottom());
}
static void checkNoInversion(const KeymapEditorLayout& L) {
CHECK(L.sampleList.right >= L.sampleList.left);
CHECK(L.zonePanel.right >= L.zonePanel.left);
CHECK(L.addZoneButton.right >= L.addZoneButton.left);
CHECK(L.addZoneButton.bottom >= L.addZoneButton.top);
CHECK(L.zoneRowArea.right >= L.zoneRowArea.left);
CHECK(L.zoneRowArea.bottom >= L.zoneRowArea.top);
CHECK(L.sampleList.right() >= L.sampleList.x);
CHECK(L.zonePanel.right() >= L.zonePanel.x);
CHECK(L.addZoneButton.right() >= L.addZoneButton.x);
CHECK(L.addZoneButton.bottom() >= L.addZoneButton.y);
CHECK(L.zoneRowArea.right() >= L.zoneRowArea.x);
CHECK(L.zoneRowArea.bottom() >= L.zoneRowArea.y);
// Regions stay within the client area.
CHECK(L.zonePanel.right <= L.base.canvas.right);
CHECK(L.zonePanel.right() <= L.base.canvas.right());
}
static void testKeymapLayoutTinyAndZeroNoInversion() {
@@ -243,36 +244,36 @@ static void testKeymapSampleRowInLeftColumn() {
const KeymapEditorLayout L = layoutKeymapEditor(600, 300);
const Rect r0 = keymapSampleRowRect(L, 0);
// Rows live in the LEFT column (not the full canvas width).
CHECK(r0.left == L.sampleList.left && r0.right == L.sampleList.right);
CHECK(r0.right < L.base.canvas.right); // strictly left of the zone panel
CHECK(r0.top == L.sampleList.top && r0.height() == kSampleRowHeight);
CHECK(r0.x == L.sampleList.x && r0.right() == L.sampleList.right());
CHECK(r0.right() < L.base.canvas.right()); // strictly left of the zone panel
CHECK(r0.y == L.sampleList.y && r0.height == kSampleRowHeight);
// Hit-test maps a left-column click to the row and rejects a click in the zone panel.
const int midY = (r0.top + r0.bottom) / 2;
CHECK(keymapSampleRowHitTest(L, 3, r0.left + 2, midY) == 0);
CHECK(keymapSampleRowHitTest(L, 3, L.zonePanel.left + 2, midY) == -1);
const int midY = (r0.y + r0.bottom()) / 2;
CHECK(keymapSampleRowHitTest(L, 3, r0.x + 2, midY) == 0);
CHECK(keymapSampleRowHitTest(L, 3, L.zonePanel.x + 2, midY) == -1);
}
static void testAddZoneHitTest() {
const KeymapEditorLayout L = layoutKeymapEditor(600, 300);
const int cx = (L.addZoneButton.left + L.addZoneButton.right) / 2;
const int cy = (L.addZoneButton.top + L.addZoneButton.bottom) / 2;
const int cx = (L.addZoneButton.x + L.addZoneButton.right()) / 2;
const int cy = (L.addZoneButton.y + L.addZoneButton.bottom()) / 2;
CHECK(addZoneHitTest(L, cx, cy));
// A click in the zone-row area below the button is NOT the Add button.
CHECK(!addZoneHitTest(L, cx, L.zoneRowArea.top + 2));
CHECK(!addZoneHitTest(L, cx, L.zoneRowArea.y + 2));
// A click in the left list is NOT the Add button.
CHECK(!addZoneHitTest(L, L.sampleList.left + 2, L.sampleList.top + 2));
CHECK(!addZoneHitTest(L, L.sampleList.x + 2, L.sampleList.y + 2));
}
static void testZoneRowStacksAndSelects() {
const KeymapEditorLayout L = layoutKeymapEditor(600, 300);
const Rect z0 = zoneRowRect(L, 0);
const Rect z1 = zoneRowRect(L, 1);
CHECK(z0.top == L.zoneRowArea.top && z0.height() == kZoneRowHeight);
CHECK(z1.top == z0.bottom); // stacked, no gap
CHECK(z0.left == L.zoneRowArea.left && z0.right == L.zoneRowArea.right);
CHECK(z0.y == L.zoneRowArea.y && z0.height == kZoneRowHeight);
CHECK(z1.y == z0.bottom()); // stacked, no gap
CHECK(z0.x == L.zoneRowArea.x && z0.right() == L.zoneRowArea.right());
// A click on the LABEL area (left part of a zone row) selects the zone with no field.
const int labelX = z0.left + 2; // far left = label, not a control
const int midY = (z0.top + z0.bottom) / 2;
const int labelX = z0.x + 2; // far left = label, not a control
const int midY = (z0.y + z0.bottom()) / 2;
const ZoneHit h = zoneHitTest(L, 2, labelX, midY);
CHECK(h.zoneIndex == 0 && h.field == ZoneField::kZoneNone);
}
@@ -280,10 +281,10 @@ static void testZoneRowStacksAndSelects() {
static void testZoneRowControlsMapToFields() {
const KeymapEditorLayout L = layoutKeymapEditor(600, 300);
const Rect row = zoneRowRect(L, 0);
const int midY = (row.top + row.bottom) / 2;
const int midY = (row.y + row.bottom()) / 2;
// The seven controls occupy the rightmost 7*kZoneCtrlWidth px, left-to-right:
// low-, low+, high-, high+, root-, root+, delete.
const int block = row.right - 7 * kZoneCtrlWidth;
const int block = row.right() - 7 * kZoneCtrlWidth;
const ZoneField expected[7] = {
ZoneField::kLowDown, ZoneField::kLowUp, ZoneField::kHighDown,
ZoneField::kHighUp, ZoneField::kRootDown, ZoneField::kRootUp,
@@ -300,14 +301,14 @@ static void testZoneRowControlsMapToFields() {
static void testZoneHitTestMisses() {
const KeymapEditorLayout L = layoutKeymapEditor(600, 300);
const Rect row = zoneRowRect(L, 0);
const int midY = (row.top + row.bottom) / 2;
const int midY = (row.y + row.bottom()) / 2;
// Zero zones -> always miss.
CHECK(zoneHitTest(L, 0, row.left + 2, midY).zoneIndex == -1);
CHECK(zoneHitTest(L, 0, row.x + 2, midY).zoneIndex == -1);
// Below the last zone row -> miss.
const Rect last = zoneRowRect(L, 2);
CHECK(zoneHitTest(L, 3, row.left + 2, last.bottom + 1).zoneIndex == -1);
CHECK(zoneHitTest(L, 3, row.x + 2, last.bottom() + 1).zoneIndex == -1);
// Left of the zone panel (in the sample list) -> miss.
CHECK(zoneHitTest(L, 3, L.sampleList.left + 2, midY).zoneIndex == -1);
CHECK(zoneHitTest(L, 3, L.sampleList.x + 2, midY).zoneIndex == -1);
}
int main() {
+44 -43
View File
@@ -1,4 +1,4 @@
// Standalone tests for reasampler::vst::embed_strip — no VST3, no REAPER, no framework.
// Standalone tests for reasampler::instrument::ui::embed_strip — no VST3, no REAPER, no framework.
// Same fast assert loop as the sibling pure tests (editor_geometry et al.): assert the
// embedded TCP/MCP strip's layout math + zone hit-testing + level fill directly.
//
@@ -9,11 +9,12 @@
// on overlap, missing on uncovered keys and off-band, and rejecting a null/empty list;
// levelFillRect clamping 0..1 and its endpoints.
#include "../src/vst/embed_strip.h"
#include "../src/core/instrument/ui/embed_strip.h"
#include <cstdio>
using namespace reasampler::vst;
using namespace reasampler;
using namespace reasampler::instrument::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
@@ -24,34 +25,34 @@ static int g_fail = 0;
static void testLayoutNormalArea() {
// A comfortable inline strip: keymap band on top, thin level band pinned to the bottom.
const EmbedLayout L = layoutEmbed(300, 40);
CHECK(L.keymap.left == 0 && L.keymap.top == 0 && L.keymap.right == 300);
CHECK(L.levelBand.left == 0 && L.levelBand.right == 300);
CHECK(L.keymap.x == 0 && L.keymap.y == 0 && L.keymap.right() == 300);
CHECK(L.levelBand.x == 0 && L.levelBand.right() == 300);
// Level band is the fixed height at the very bottom; keymap fills the rest, contiguous.
CHECK(L.levelBand.height() == kEmbedLevelBandHeight);
CHECK(L.levelBand.bottom == 40);
CHECK(L.keymap.bottom == L.levelBand.top);
CHECK(L.keymap.height() == 40 - kEmbedLevelBandHeight);
CHECK(L.levelBand.height == kEmbedLevelBandHeight);
CHECK(L.levelBand.bottom() == 40);
CHECK(L.keymap.bottom() == L.levelBand.y);
CHECK(L.keymap.height == 40 - kEmbedLevelBandHeight);
}
static void testLayoutTinyAreaKeepsKeymap() {
// A very short area: the level band must yield so the keymap keeps its minimum, and no
// rect inverts.
const EmbedLayout L = layoutEmbed(300, 8);
CHECK(L.keymap.height() >= 0);
CHECK(L.levelBand.height() >= 0);
CHECK(L.keymap.bottom == L.levelBand.top);
CHECK(L.levelBand.bottom == 8);
CHECK(L.keymap.height >= 0);
CHECK(L.levelBand.height >= 0);
CHECK(L.keymap.bottom() == L.levelBand.y);
CHECK(L.levelBand.bottom() == 8);
// The keymap is not starved below its floor when the area allows it.
CHECK(L.keymap.height() >= kEmbedKeymapMinHeight || 8 < kEmbedKeymapMinHeight);
CHECK(L.keymap.height >= kEmbedKeymapMinHeight || 8 < kEmbedKeymapMinHeight);
}
static void testLayoutZeroArea() {
const EmbedLayout L = layoutEmbed(0, 0);
CHECK(L.keymap.width() <= 0 && L.keymap.height() <= 0);
CHECK(L.levelBand.width() <= 0 && L.levelBand.height() <= 0);
CHECK(L.keymap.width <= 0 && L.keymap.height <= 0);
CHECK(L.levelBand.width <= 0 && L.levelBand.height <= 0);
// Negative dimensions clamp to a zero-area, non-inverted rect.
const EmbedLayout N = layoutEmbed(-50, -50);
CHECK(N.keymap.right >= N.keymap.left && N.keymap.bottom >= N.keymap.top);
CHECK(N.keymap.right() >= N.keymap.x && N.keymap.bottom() >= N.keymap.y);
}
// --- zoneSegmentRect ----------------------------------------------------------
@@ -60,9 +61,9 @@ static void testZoneSegmentFullSpan() {
// A zone covering the whole keyboard spans the entire keymap band width.
const EmbedLayout L = layoutEmbed(256, 40);
const Rect r = zoneSegmentRect(L, 0, 127);
CHECK(r.left == L.keymap.left);
CHECK(r.right == L.keymap.right);
CHECK(r.top == L.keymap.top && r.bottom == L.keymap.bottom);
CHECK(r.x == L.keymap.x);
CHECK(r.right() == L.keymap.right());
CHECK(r.y == L.keymap.y && r.bottom() == L.keymap.bottom());
}
static void testAdjacentZonesTileSeamlessly() {
@@ -71,10 +72,10 @@ static void testAdjacentZonesTileSeamlessly() {
const EmbedLayout L = layoutEmbed(256, 40);
const Rect lo = zoneSegmentRect(L, 0, 59);
const Rect hi = zoneSegmentRect(L, 60, 127);
CHECK(lo.left == L.keymap.left);
CHECK(hi.right == L.keymap.right);
CHECK(lo.right == hi.left); // seamless tile — the load-bearing assertion
CHECK(lo.right == L.keymap.left + 60 * 2); // 60 keys * 2px
CHECK(lo.x == L.keymap.x);
CHECK(hi.right() == L.keymap.right());
CHECK(lo.right() == hi.x); // seamless tile — the load-bearing assertion
CHECK(lo.right() == L.keymap.x + 60 * 2); // 60 keys * 2px
}
static void testZoneSegmentClampsBadNotes() {
@@ -82,9 +83,9 @@ static void testZoneSegmentClampsBadNotes() {
// Out-of-range notes clamp into the band; an inverted zone (low > high) collapses to a
// zero-or-positive-width rect, never inverts.
const Rect over = zoneSegmentRect(L, -10, 200);
CHECK(over.left == L.keymap.left && over.right == L.keymap.right);
CHECK(over.x == L.keymap.x && over.right() == L.keymap.right());
const Rect inv = zoneSegmentRect(L, 100, 20);
CHECK(inv.right >= inv.left);
CHECK(inv.right() >= inv.x);
}
// --- zoneAtPoint --------------------------------------------------------------
@@ -95,31 +96,31 @@ static void testZoneAtPointHits() {
// A point inside the low zone's segment resolves to zone 0; inside the high zone, 1.
const Rect lo = zoneSegmentRect(L, 0, 59);
const Rect hi = zoneSegmentRect(L, 60, 127);
const int yMid = (L.keymap.top + L.keymap.bottom) / 2;
CHECK(zoneAtPoint(L, zones, 2, lo.left + 1, yMid) == 0);
CHECK(zoneAtPoint(L, zones, 2, hi.right - 1, yMid) == 1);
const int yMid = (L.keymap.y + L.keymap.bottom()) / 2;
CHECK(zoneAtPoint(L, zones, 2, lo.x + 1, yMid) == 0);
CHECK(zoneAtPoint(L, zones, 2, hi.right() - 1, yMid) == 1);
}
static void testZoneAtPointFirstMatchOnOverlap() {
const EmbedLayout L = layoutEmbed(256, 40);
// Two overlapping zones; the FIRST in order must win the contested keys.
const EmbedZone zones[2] = {{0, 127}, {40, 80}};
const int yMid = (L.keymap.top + L.keymap.bottom) / 2;
const int yMid = (L.keymap.y + L.keymap.bottom()) / 2;
const Rect contested = zoneSegmentRect(L, 40, 80);
CHECK(zoneAtPoint(L, zones, 2, contested.left + 1, yMid) == 0); // zone 0 wins
CHECK(zoneAtPoint(L, zones, 2, contested.x + 1, yMid) == 0); // zone 0 wins
}
static void testZoneAtPointMisses() {
const EmbedLayout L = layoutEmbed(256, 40);
const EmbedZone zones[1] = {{60, 72}}; // a narrow zone; most keys uncovered
const int yMid = (L.keymap.top + L.keymap.bottom) / 2;
const int yMid = (L.keymap.y + L.keymap.bottom()) / 2;
// A key left of the zone is uncovered -> -1.
CHECK(zoneAtPoint(L, zones, 1, L.keymap.left + 1, yMid) == -1);
CHECK(zoneAtPoint(L, zones, 1, L.keymap.x + 1, yMid) == -1);
// A point in the level band (below the keymap) is off the keymap -> -1.
CHECK(zoneAtPoint(L, zones, 1, L.levelBand.left + 4, L.levelBand.top) == -1);
CHECK(zoneAtPoint(L, zones, 1, L.levelBand.x + 4, L.levelBand.y) == -1);
// Empty / null list -> -1.
CHECK(zoneAtPoint(L, zones, 0, L.keymap.left + 1, yMid) == -1);
CHECK(zoneAtPoint(L, nullptr, 3, L.keymap.left + 1, yMid) == -1);
CHECK(zoneAtPoint(L, zones, 0, L.keymap.x + 1, yMid) == -1);
CHECK(zoneAtPoint(L, nullptr, 3, L.keymap.x + 1, yMid) == -1);
}
// --- levelFillRect ------------------------------------------------------------
@@ -127,16 +128,16 @@ static void testZoneAtPointMisses() {
static void testLevelFillClamps() {
const EmbedLayout L = layoutEmbed(200, 40);
// Zero / negative -> empty.
CHECK(levelFillRect(L, 0.0).width() <= 0);
CHECK(levelFillRect(L, -1.0).width() <= 0);
CHECK(levelFillRect(L, 0.0).width <= 0);
CHECK(levelFillRect(L, -1.0).width <= 0);
// Full / over-full -> the whole band width.
CHECK(levelFillRect(L, 1.0).width() == L.levelBand.width());
CHECK(levelFillRect(L, 5.0).width() == L.levelBand.width());
CHECK(levelFillRect(L, 1.0).width == L.levelBand.width);
CHECK(levelFillRect(L, 5.0).width == L.levelBand.width);
// Half -> ~half the band, pinned to the band's left and vertical extent.
const Rect half = levelFillRect(L, 0.5);
CHECK(half.left == L.levelBand.left);
CHECK(half.top == L.levelBand.top && half.bottom == L.levelBand.bottom);
CHECK(half.width() == L.levelBand.width() / 2);
CHECK(half.x == L.levelBand.x);
CHECK(half.y == L.levelBand.y && half.bottom() == L.levelBand.bottom());
CHECK(half.width == L.levelBand.width / 2);
}
int main() {
+17 -16
View File
@@ -1,4 +1,4 @@
// Standalone tests for reasampler::vst::envelope_edit — no VST3, no REAPER, no framework.
// Standalone tests for reasampler::instrument::ui::envelope_edit — no VST3, no REAPER, no framework.
// Same fast assert loop as the sibling pure tests. Assert the S-VIEW-3 draggable-node INVERSE
// map: node hit-test + pixel-delta -> clamped/monotonic param set, HARD at the clamp + monotonic
// boundaries (the load-bearing "a drag can never produce a param a slider couldn't" invariant).
@@ -14,14 +14,15 @@
// pixel delta; zero-fade-out node grabbable at the right edge and draggable inward — FA2);
// degenerate area/duration + non-draggable node + cross-mode node -> no motion.
#include "../src/vst/envelope_edit.h"
#include "../src/core/instrument/ui/envelope_edit.h"
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <vector>
using namespace reasampler::vst;
using namespace reasampler;
using namespace reasampler::instrument::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
@@ -41,7 +42,7 @@ static bool findNode(const std::vector<EnvVertex>& poly, EnvNode node, EnvVertex
// scale (FA2 param-domain schematic — sample-length-free): (850-1-32)px over the 8.0s schematic
// domain => 102.125 px/s, each segment prefixed by the 8px separation base; the gateEnv() nodes
// draw at A x@28, H x@47, D x@85, RS x@235, RE x@284.
static Rect wideArea() { return Rect{20, 10, 1020, 110}; }
static Rect wideArea() { return Rect::ltrb(20, 10, 1020, 110); }
static constexpr double kTotal = 2.0;
static const double kGateSecPerPx = 1.0 / gatePxPerSecond(wideArea());
@@ -71,10 +72,10 @@ static void testHitGrabsDrawnHandle() {
const AmpEnvelope e = gateEnv();
const Rect a = wideArea();
// AttackEnd draws at x = left+28 (8px base + 0.2s * 102.125 px/s), y = top (level 1).
NodeHit h = nodeAtPoint(e, a, kTotal, a.left + 28, a.top);
NodeHit h = nodeAtPoint(e, a, kTotal, a.x + 28, a.y);
CHECK(h.hit && h.node == EnvNode::AttackEnd);
// The sustain node (DecayEnd) at left+85, level 0.5 -> ~top+50.
NodeHit s = nodeAtPoint(e, a, kTotal, a.left + 85, a.top + 50);
NodeHit s = nodeAtPoint(e, a, kTotal, a.x + 85, a.y + 50);
CHECK(s.hit && s.node == EnvNode::DecayEnd);
}
@@ -82,7 +83,7 @@ static void testHitMissesOffEveryNode() {
const AmpEnvelope e = gateEnv();
const Rect a = wideArea();
// A point far from any drawn handle (right of the release ramp, well away from a node).
NodeHit h = nodeAtPoint(e, a, kTotal, a.left + 700, a.top + 5);
NodeHit h = nodeAtPoint(e, a, kTotal, a.x + 700, a.y + 5);
CHECK(!h.hit);
}
@@ -90,11 +91,11 @@ static void testHitSkipsNonDraggableAnchors() {
const AmpEnvelope e = gateEnv();
const Rect a = wideArea();
// Origin draws at (left, bottom-1). Even a pixel-perfect grab there is NOT a draggable node.
NodeHit o = nodeAtPoint(e, a, kTotal, a.left, a.bottom - 1);
NodeHit o = nodeAtPoint(e, a, kTotal, a.x, a.bottom() - 1);
CHECK(!o.hit);
// ReleaseStart draws at (left+235, sustain level ~top+50) — the fixed plateau end. It is
// drawing-only -> not grabbable; no other node is within the radius, so this grab misses.
NodeHit rs = nodeAtPoint(e, a, kTotal, a.left + 235, a.top + 50);
NodeHit rs = nodeAtPoint(e, a, kTotal, a.x + 235, a.y + 50);
CHECK(!rs.hit);
}
@@ -106,7 +107,7 @@ static void testHitNearestNodeWinsOverDrawOrder() {
AmpEnvelope e = gateEnv();
e.holdSeconds = 0.01;
const Rect a = wideArea();
NodeHit h = nodeAtPoint(e, a, kTotal, a.left + 33, a.top);
NodeHit h = nodeAtPoint(e, a, kTotal, a.x + 33, a.y);
CHECK(h.hit && h.node == EnvNode::HoldEnd);
}
@@ -199,11 +200,11 @@ static void testGateTimeOnlyNodeIgnoresY() {
static void testGateReleaseEndGrabAndDrag() {
// The FA2 fix: ReleaseEnd is a drawn, IN-BOUNDS, grabbable handle (pre-FA2 it mapped past
// area.right and could never be grabbed). gateEnv() draws it at x@284, level 0 (bottom row).
// area.right() and could never be grabbed). gateEnv() draws it at x@284, level 0 (bottom row).
const AmpEnvelope e = gateEnv();
const Rect a = wideArea();
EnvClampBounds b;
NodeHit h = nodeAtPoint(e, a, kTotal, a.left + 284, a.bottom - 1);
NodeHit h = nodeAtPoint(e, a, kTotal, a.x + 284, a.bottom() - 1);
CHECK(h.hit && h.node == EnvNode::ReleaseEnd);
// Dragging it RIGHT lengthens the release at the gate timed scale; only release changes.
AmpEnvelope out = resolveNodeDrag(e, EnvNode::ReleaseEnd, a, kTotal, b, 85, 0);
@@ -289,14 +290,14 @@ static void testTriggerZeroFadeOutGrabbableAtRightEdge() {
e.fadeOutFraction = 0.0;
const Rect a = wideArea();
EnvClampBounds b;
NodeHit h = nodeAtPoint(e, a, kTotal, a.right - 1, a.top);
NodeHit h = nodeAtPoint(e, a, kTotal, a.right() - 1, a.y);
CHECK(h.hit && h.node == EnvNode::FadeOutStart);
// -100px = -0.2s on the 2.0s played span, applied OPPOSITE -> fadeOut 0.0 -> 0.1.
AmpEnvelope out = resolveNodeDrag(e, EnvNode::FadeOutStart, a, kTotal, b, -100, 0);
CHECK(near(out.fadeOutFraction, 0.1));
CHECK(near(out.lengthFraction, e.lengthFraction)); // length untouched
// LengthEnd sits at the same x but level 0 (bottom row) — grabbable at ITS drawn point.
NodeHit le = nodeAtPoint(e, a, kTotal, a.right - 1, a.bottom - 1);
NodeHit le = nodeAtPoint(e, a, kTotal, a.right() - 1, a.bottom() - 1);
CHECK(le.hit && le.node == EnvNode::LengthEnd);
}
@@ -327,7 +328,7 @@ static void testNonDraggableNodeNoMotion() {
static void testDegenerateAreaNoMotion() {
const AmpEnvelope e = gateEnv();
EnvClampBounds b;
const Rect zeroW = Rect{0, 0, 0, 100};
const Rect zeroW = Rect::ltrb(0, 0, 0, 100);
AmpEnvelope o1 = resolveNodeDrag(e, EnvNode::AttackEnd, zeroW, kTotal, b, 500, 0);
CHECK(near(o1.attackSeconds, e.attackSeconds));
AmpEnvelope o2 = resolveNodeDrag(e, EnvNode::AttackEnd, wideArea(), 0.0, b, 500, 0); // no time
@@ -347,7 +348,7 @@ static void testCrossModeNodeNoMotion() {
out = resolveNodeDrag(g, EnvNode::FadeInEnd, wideArea(), kTotal, b, 50, 0);
CHECK(near(out.fadeInFraction, g.fadeInFraction));
// And the zero-height baseline's ReleaseEnd is not even reported grabbable in Trigger mode.
const Rect flat = Rect{0, 0, 100, 0};
const Rect flat = Rect::ltrb(0, 0, 100, 0);
const NodeHit h = nodeAtPoint(t, flat, kTotal, 99, 0);
CHECK(!h.hit);
}
+47 -46
View File
@@ -1,4 +1,4 @@
// Standalone tests for reasampler::vst::envelope_overlay — no VST3, no REAPER, no framework.
// Standalone tests for reasampler::instrument::ui::envelope_overlay — no VST3, no REAPER, no framework.
// Same fast assert loop as the sibling pure tests. Assert the S-VIEW-3/FA2 amp-envelope ->
// polyline FORWARD map: the Gate BOUNDED-SCHEMATIC AHDSR shape (attack ramp / hold plateau /
// decay-to-sustain / fixed-width sustain plateau / in-bounds release) and the Trigger
@@ -14,12 +14,13 @@
// the played span, overlap clamp, full-length/zero-fade-out nodes in-bounds at right-1);
// degenerate flat baseline.
#include "../src/vst/envelope_overlay.h"
#include "../src/core/instrument/ui/envelope_overlay.h"
#include <cstdio>
#include <vector>
using namespace reasampler::vst;
using namespace reasampler;
using namespace reasampler::instrument::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
@@ -27,7 +28,7 @@ static int g_fail = 0;
// A comfortable overlay area: 1000px wide, 100px tall, offset so left/top != 0 (catches origin
// bugs). Under levelToY the level span is height-1 = 99 rows.
static Rect wideArea() { return Rect{20, 10, 1020, 110}; } // width 1000, height 100
static Rect wideArea() { return Rect::ltrb(20, 10, 1020, 110); } // width 1000, height 100
// Find the first vertex with a given node in a polyline; asserts presence via the returned bool.
static bool findNode(const std::vector<EnvVertex>& poly, EnvNode node, EnvVertex& out) {
@@ -41,32 +42,32 @@ static bool findNode(const std::vector<EnvVertex>& poly, EnvNode node, EnvVertex
static void testTimeToXEndpoints() {
const Rect a = wideArea();
CHECK(timeToX(a, 2.0, 0.0) == a.left); // t=0 -> left
CHECK(timeToX(a, 2.0, 2.0) == a.right - 1); // t=total -> last in-bounds column
CHECK(timeToX(a, 2.0, 1.0) == a.left + 500); // midpoint
CHECK(timeToX(a, 2.0, 0.0) == a.x); // t=0 -> left
CHECK(timeToX(a, 2.0, 2.0) == a.right() - 1); // t=total -> last in-bounds column
CHECK(timeToX(a, 2.0, 1.0) == a.x + 500); // midpoint
}
static void testTimeToXNegativePinsLeft() {
const Rect a = wideArea();
CHECK(timeToX(a, 2.0, -0.5) == a.left); // t<0 pins left
CHECK(timeToX(a, 2.0, -0.5) == a.x); // t<0 pins left
}
static void testTimeToXPastEndClamps() {
// FA2 bounds invariant: t past total pins to the last in-bounds column, never past right.
const Rect a = wideArea();
CHECK(timeToX(a, 2.0, 3.0) == a.right - 1);
CHECK(timeToX(a, 2.0, 1000.0) == a.right - 1);
CHECK(timeToX(a, 2.0, 3.0) == a.right() - 1);
CHECK(timeToX(a, 2.0, 1000.0) == a.right() - 1);
// A HUGE t must clamp in double space, not overflow the integer cast (32-bit long on
// Windows would wrap to LONG_MIN and pin to the WRONG edge).
CHECK(timeToX(a, 2.0, 1e15) == a.right - 1);
CHECK(timeToX(a, 2.0, 1e15) == a.right() - 1);
}
static void testGateTimedWidth() {
// 15% of the 1000px canvas is reserved for the sustain plateau -> 850px timed region.
CHECK(gateTimedWidth(wideArea()) == 850);
// Zero-width area -> 0; a tiny area still yields >= 1 so the px<->s scale never degenerates.
CHECK(gateTimedWidth(Rect{5, 5, 5, 45}) == 0);
CHECK(gateTimedWidth(Rect{0, 0, 1, 10}) == 1);
CHECK(gateTimedWidth(Rect::ltrb(5, 5, 5, 45)) == 0);
CHECK(gateTimedWidth(Rect::ltrb(0, 0, 1, 10)) == 1);
}
static void testGatePxPerSecond() {
@@ -74,30 +75,30 @@ static void testGatePxPerSecond() {
// 1000px canvas: (850 - 1 - 32) / 8.0s = 817/8 px/s. Independent of any sample duration.
const double expected = 817.0 / (4.0 * kGateStageMaxSeconds);
CHECK(gatePxPerSecond(wideArea()) == expected);
CHECK(gatePxPerSecond(Rect{5, 5, 5, 45}) == 0.0); // zero-width area -> 0
CHECK(gatePxPerSecond(Rect{0, 0, 10, 10}) > 0.0); // tiny area: usable floors at 1px, > 0
CHECK(gatePxPerSecond(Rect::ltrb(5, 5, 5, 45)) == 0.0); // zero-width area -> 0
CHECK(gatePxPerSecond(Rect::ltrb(0, 0, 10, 10)) > 0.0); // tiny area: usable floors at 1px, > 0
}
static void testTimeToXDegenerate() {
const Rect a = wideArea();
CHECK(timeToX(a, 0.0, 1.0) == a.left); // no duration -> left
const Rect z = Rect{5, 5, 5, 45}; // zero width
CHECK(timeToX(z, 2.0, 1.0) == z.left);
CHECK(timeToX(a, 0.0, 1.0) == a.x); // no duration -> left
const Rect z = Rect::ltrb(5, 5, 5, 45); // zero width
CHECK(timeToX(z, 2.0, 1.0) == z.x);
}
static void testLevelToYEndpoints() {
const Rect a = wideArea();
CHECK(levelToY(a, 1.0) == a.top); // level 1 -> top row
CHECK(levelToY(a, 0.0) == a.bottom - 1); // level 0 -> bottom row
CHECK(levelToY(a, 0.5) == a.top + 50); // mid: round((1-0.5)*99)=round(49.5)=50
CHECK(levelToY(a, 1.0) == a.y); // level 1 -> top row
CHECK(levelToY(a, 0.0) == a.bottom() - 1); // level 0 -> bottom row
CHECK(levelToY(a, 0.5) == a.y + 50); // mid: round((1-0.5)*99)=round(49.5)=50
}
static void testLevelToYClamps() {
const Rect a = wideArea();
CHECK(levelToY(a, 2.0) == a.top); // >1 clamps to top
CHECK(levelToY(a, -1.0) == a.bottom - 1); // <0 clamps to bottom
const Rect z = Rect{5, 5, 45, 5}; // zero height
CHECK(levelToY(z, 0.5) == z.top);
CHECK(levelToY(a, 2.0) == a.y); // >1 clamps to top
CHECK(levelToY(a, -1.0) == a.bottom() - 1); // <0 clamps to bottom
const Rect z = Rect::ltrb(5, 5, 45, 5); // zero height
CHECK(levelToY(z, 0.5) == z.y);
}
// --- Gate polyline ------------------------------------------------------------
@@ -149,11 +150,11 @@ static void testGateSchematicPlacement() {
const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, a, 2.0);
EnvVertex v;
CHECK(findNode(poly, EnvNode::AttackEnd, v) && v.x == a.left + 28);
CHECK(findNode(poly, EnvNode::HoldEnd, v) && v.x == a.left + 47);
CHECK(findNode(poly, EnvNode::DecayEnd, v) && v.x == a.left + 85);
CHECK(findNode(poly, EnvNode::ReleaseStart, v) && v.x == a.left + 235);
CHECK(findNode(poly, EnvNode::ReleaseEnd, v) && v.x == a.left + 284);
CHECK(findNode(poly, EnvNode::AttackEnd, v) && v.x == a.x + 28);
CHECK(findNode(poly, EnvNode::HoldEnd, v) && v.x == a.x + 47);
CHECK(findNode(poly, EnvNode::DecayEnd, v) && v.x == a.x + 85);
CHECK(findNode(poly, EnvNode::ReleaseStart, v) && v.x == a.x + 235);
CHECK(findNode(poly, EnvNode::ReleaseEnd, v) && v.x == a.x + 284);
}
static void testGateLayoutIndependentOfSampleDuration() {
@@ -195,7 +196,7 @@ static void testGateSustainPlateauFixedWidth() {
env.sustainLevel = 0.6;
env.releaseSeconds = 0.3;
const Rect a = wideArea();
const int plateauPx = a.width() - gateTimedWidth(a); // 150
const int plateauPx = a.width - gateTimedWidth(a); // 150
const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, a, 2.0);
EnvVertex decay, plateauEnd;
@@ -207,7 +208,7 @@ static void testGateSustainPlateauFixedWidth() {
static void testGateReleaseVisibleInBounds() {
// The FA2 fix: Release is a VISIBLE, in-bounds segment — ReleaseEnd sits strictly right of
// the plateau end and strictly inside the canvas (pre-FA2 it mapped past area.right and the
// the plateau end and strictly inside the canvas (pre-FA2 it mapped past area.right() and the
// shell clipped its handle away).
AmpEnvelope env;
env.mode = EnvMode::Gate;
@@ -223,7 +224,7 @@ static void testGateReleaseVisibleInBounds() {
CHECK(findNode(poly, EnvNode::ReleaseStart, plateauEnd));
CHECK(findNode(poly, EnvNode::ReleaseEnd, rel));
CHECK(rel.x > plateauEnd.x); // a visible ramp, not a collapsed point
CHECK(rel.x < a.right); // strictly in-bounds
CHECK(rel.x < a.right()); // strictly in-bounds
CHECK(rel.level == 0.0);
}
@@ -231,7 +232,7 @@ static void testGateOverrunCompressesFromRight() {
// Stages BEYOND the schematic domain (4.0s each > kGateStageMaxSeconds): the layout
// compresses from the right preserving the minimum gaps — ReleaseEnd pins to the last
// in-bounds column, but the trailing nodes stay strictly increasing and individually
// separated (>= kGateNodeSepPx), NOT piled on one pixel. NOTHING maps past area.right.
// separated (>= kGateNodeSepPx), NOT piled on one pixel. NOTHING maps past area.right().
AmpEnvelope env;
env.mode = EnvMode::Gate;
env.attackSeconds = 4.0;
@@ -246,12 +247,12 @@ static void testGateOverrunCompressesFromRight() {
EnvVertex plateauEnd, rel;
CHECK(findNode(poly, EnvNode::ReleaseStart, plateauEnd));
CHECK(findNode(poly, EnvNode::ReleaseEnd, rel));
CHECK(rel.x == a.right - 1); // pinned to the last in-bounds column
CHECK(rel.x == a.right() - 1); // pinned to the last in-bounds column
CHECK(plateauEnd.level == 0.7); // still at sustain
for (size_t i = 1; i < poly.size(); ++i) {
CHECK(poly[i].x > poly[i - 1].x); // strictly monotonic
CHECK(poly[i].x - poly[i - 1].x >= kGateNodeSepPx - 1); // min gaps survive compression
CHECK(poly[i].x >= a.left && poly[i].x < a.right); // in-bounds
CHECK(poly[i].x >= a.x && poly[i].x < a.right()); // in-bounds
}
}
@@ -279,8 +280,8 @@ static void testGateAllVerticesInBounds() {
for (const AmpEnvelope& env : {base, big, zero, trig, huge}) {
for (const EnvVertex& v : buildEnvelopePolyline(env, a, 2.0)) {
CHECK(v.x >= a.left && v.x < a.right);
CHECK(v.y >= a.top && v.y < a.bottom);
CHECK(v.x >= a.x && v.x < a.right());
CHECK(v.y >= a.y && v.y < a.bottom());
}
}
}
@@ -305,9 +306,9 @@ static void testTriggerShape() {
CHECK(poly[3].node == EnvNode::LengthEnd);
EnvVertex v;
CHECK(findNode(poly, EnvNode::FadeInEnd, v) && v.x == a.left + 100 && v.level == 1.0);
CHECK(findNode(poly, EnvNode::FadeOutStart, v) && v.x == a.left + 350 && v.level == 1.0);
CHECK(findNode(poly, EnvNode::LengthEnd, v) && v.x == a.left + 500 && v.level == 0.0);
CHECK(findNode(poly, EnvNode::FadeInEnd, v) && v.x == a.x + 100 && v.level == 1.0);
CHECK(findNode(poly, EnvNode::FadeOutStart, v) && v.x == a.x + 350 && v.level == 1.0);
CHECK(findNode(poly, EnvNode::LengthEnd, v) && v.x == a.x + 500 && v.level == 0.0);
}
static void testTriggerFadeOverlapClamp() {
@@ -324,7 +325,7 @@ static void testTriggerFadeOverlapClamp() {
CHECK(findNode(poly, EnvNode::FadeInEnd, fin));
CHECK(findNode(poly, EnvNode::FadeOutStart, fout));
CHECK(fin.x == fout.x); // fades meet exactly, never cross
CHECK(fin.x == a.left + 800);
CHECK(fin.x == a.x + 800);
}
static void testTriggerFullLengthZeroFadeOutInBounds() {
@@ -342,8 +343,8 @@ static void testTriggerFullLengthZeroFadeOutInBounds() {
EnvVertex fout, lend;
CHECK(findNode(poly, EnvNode::FadeOutStart, fout));
CHECK(findNode(poly, EnvNode::LengthEnd, lend));
CHECK(fout.x == a.right - 1); // present + in-bounds at zero fade-out
CHECK(lend.x == a.right - 1);
CHECK(fout.x == a.right() - 1); // present + in-bounds at zero fade-out
CHECK(lend.x == a.right() - 1);
CHECK(fout.level == 1.0 && lend.level == 0.0);
}
@@ -351,7 +352,7 @@ static void testTriggerFullLengthZeroFadeOutInBounds() {
static void testDegenerateFlatBaseline() {
AmpEnvelope env; // any params
const Rect zeroW = Rect{0, 0, 0, 100};
const Rect zeroW = Rect::ltrb(0, 0, 0, 100);
const std::vector<EnvVertex> p1 = buildEnvelopePolyline(env, zeroW, 2.0);
CHECK(p1.size() == 2); // always a drawable line
CHECK(p1.front().level == 0.0 && p1.back().level == 0.0);
@@ -360,7 +361,7 @@ static void testDegenerateFlatBaseline() {
const std::vector<EnvVertex> p2 = buildEnvelopePolyline(env, ok, 0.0); // no duration
CHECK(p2.size() == 2);
CHECK(p2.front().level == 0.0 && p2.back().level == 0.0);
CHECK(p2.front().x == ok.left && p2.back().x == ok.right - 1); // spans the area, in-bounds
CHECK(p2.front().x == ok.x && p2.back().x == ok.right() - 1); // spans the area, in-bounds
}
int main() {
+1
View File
@@ -11,6 +11,7 @@
#include <vector>
using namespace reasampler;
using namespace reasampler::util;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -13,11 +13,12 @@
// * Hit-test: Toggle / Tail returned for in-bounds points, None outside AND on the count
// label (a passive readout, never a control); half-open bounds; suppressed box claims none.
#include "../src/footer_bar.h"
#include "../src/core/ui/footer_bar.h"
#include <cstdio>
using namespace reasampler;
using namespace reasampler::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -10,7 +10,7 @@
// 5. reset() (project switch) re-arms the first-poll guard: the next observe()
// re-baselines and reports nothing new — never diffs across projects.
#include "../src/guid_diff.h"
#include "../src/core/view/guid_diff.h"
#include <cstdio>
#include <set>
@@ -18,6 +18,7 @@
#include <vector>
using namespace reasampler;
using namespace reasampler::view;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -5,12 +5,13 @@
// proof that the forbidden stretch bit is never set and that native-length insert
// carries no tempo bits.
#include "../src/insert_plan.h"
#include "../src/core/capture/insert_plan.h"
#include <cstdio>
#include <initializer_list>
using namespace reasampler;
using namespace reasampler::capture;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+3 -2
View File
@@ -6,8 +6,8 @@
// container -> the instrument's OWN reader -> assert the capture selected) IS the
// cross-artifact contract guard — the same pattern assignment_request_tests uses.
#include "../src/instrument_drop.h"
#include "../src/vst/sample_map.h" // deserializeComponentState — the instrument's OWN reader
#include "../src/core/wire/instrument_drop.h"
#include "../src/core/instrument/map/sample_map.h" // deserializeComponentState — the instrument's OWN reader
#include "version_generated.h" // REASAMPLER_CHANNEL_IS_BETA — pins the per-channel class ID
@@ -17,6 +17,7 @@
#include <vector>
using namespace reasampler;
using namespace reasampler::wire;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+1
View File
@@ -16,6 +16,7 @@
#include <vector>
using namespace reasampler;
using namespace reasampler::json;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+37 -36
View File
@@ -1,4 +1,4 @@
// Standalone tests for reasampler::vst::keyboard_strip — no VST3, no REAPER, no framework.
// Standalone tests for reasampler::instrument::ui::keyboard_strip — no VST3, no REAPER, no framework.
// Same fast assert loop as the sibling pure tests. Assert the capture-first editor's
// keyboard-strip layout, root marker, key mapping, zone-bar hit regions, and the drag-delta
// note resolver directly — the geometry that backs the single-capture root-set and the opt-in
@@ -14,11 +14,12 @@
// no-ops; isNaturalKey across a full octave (C4..B4), at boundary notes 0 and 127, and with
// out-of-range inputs that clamp to [0,127].
#include "../src/vst/keyboard_strip.h"
#include "../src/core/instrument/ui/keyboard_strip.h"
#include <cstdio>
using namespace reasampler::vst;
using namespace reasampler;
using namespace reasampler::instrument::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
@@ -32,13 +33,13 @@ static StripLayout wideStrip() { return layoutStrip(1280, 40); }
static void testLayoutNormalArea() {
const StripLayout L = layoutStrip(640, 40);
CHECK(L.keys.left == 0 && L.keys.top == 0);
CHECK(L.keys.right == 640 && L.keys.bottom == 40);
CHECK(L.keys.x == 0 && L.keys.y == 0);
CHECK(L.keys.right() == 640 && L.keys.bottom() == 40);
}
static void testLayoutZeroArea() {
const StripLayout L = layoutStrip(0, 0);
CHECK(L.keys.width() == 0 && L.keys.height() == 0);
CHECK(L.keys.width == 0 && L.keys.height == 0);
}
// --- keyLeftX / keyRect / rootMarkerRect --------------------------------------
@@ -46,8 +47,8 @@ static void testLayoutZeroArea() {
static void testKeyLeftMonotonicAndBounds() {
const StripLayout L = wideStrip();
// Key 0's left edge is the band left; the 128 boundary is the band right.
CHECK(keyLeftX(L, 0) == L.keys.left);
CHECK(keyLeftX(L, 128) == L.keys.right);
CHECK(keyLeftX(L, 0) == L.keys.x);
CHECK(keyLeftX(L, 128) == L.keys.right());
// Strictly non-decreasing across the span.
int prev = keyLeftX(L, 0);
for (int n = 1; n <= 128; ++n) {
@@ -62,17 +63,17 @@ static void testKeyLeftMonotonicAndBounds() {
static void testKeyRectHalfOpen() {
const StripLayout L = wideStrip();
const Rect k = keyRect(L, 60);
CHECK(k.left == keyLeftX(L, 60));
CHECK(k.right == keyLeftX(L, 61));
CHECK(k.top == L.keys.top && k.bottom == L.keys.bottom);
CHECK(k.width() == 10); // 10px/key
CHECK(k.x == keyLeftX(L, 60));
CHECK(k.right() == keyLeftX(L, 61));
CHECK(k.y == L.keys.y && k.bottom() == L.keys.bottom());
CHECK(k.width == 10); // 10px/key
}
static void testRootMarkerEqualsKeyRect() {
const StripLayout L = wideStrip();
const Rect m = rootMarkerRect(L, 64);
const Rect k = keyRect(L, 64);
CHECK(m.left == k.left && m.right == k.right && m.top == k.top && m.bottom == k.bottom);
CHECK(m.x == k.x && m.right() == k.right() && m.y == k.y && m.bottom() == k.bottom());
}
// --- keyAtPoint ---------------------------------------------------------------
@@ -81,17 +82,17 @@ static void testKeyAtPointInverts() {
const StripLayout L = wideStrip();
// A point in the middle of key 60's cell resolves to 60.
const Rect k = keyRect(L, 60);
CHECK(keyAtPoint(L, k.left + 5, k.top + 2) == 60);
CHECK(keyAtPoint(L, k.x + 5, k.y + 2) == 60);
// The very left of the band is key 0; just inside the right edge is key 127.
CHECK(keyAtPoint(L, L.keys.left, 2) == 0);
CHECK(keyAtPoint(L, L.keys.right - 1, 2) == 127);
CHECK(keyAtPoint(L, L.keys.x, 2) == 0);
CHECK(keyAtPoint(L, L.keys.right() - 1, 2) == 127);
}
static void testKeyAtPointOffBand() {
const StripLayout L = wideStrip();
CHECK(keyAtPoint(L, -5, 2) == -1); // left of band
CHECK(keyAtPoint(L, L.keys.right + 5, 2) == -1); // right of band
CHECK(keyAtPoint(L, 100, L.keys.bottom + 5) == -1); // below band
CHECK(keyAtPoint(L, L.keys.right() + 5, 2) == -1); // right of band
CHECK(keyAtPoint(L, 100, L.keys.bottom() + 5) == -1); // below band
}
// --- zoneBarRect --------------------------------------------------------------
@@ -99,17 +100,17 @@ static void testKeyAtPointOffBand() {
static void testZoneBarSpansInclusive() {
const StripLayout L = wideStrip();
const Rect bar = zoneBarRect(L, 12, 23); // C1..B1 inclusive
CHECK(bar.left == keyLeftX(L, 12));
CHECK(bar.right == keyLeftX(L, 24)); // high+1 -> the bar covers key 23 fully
CHECK(bar.width() == 120); // 12 keys * 10px
CHECK(bar.x == keyLeftX(L, 12));
CHECK(bar.right() == keyLeftX(L, 24)); // high+1 -> the bar covers key 23 fully
CHECK(bar.width == 120); // 12 keys * 10px
}
static void testZoneBarMalformedCollapses() {
const StripLayout L = wideStrip();
// low > high must collapse, never invert.
const Rect bar = zoneBarRect(L, 80, 40);
CHECK(bar.width() >= 0);
CHECK(bar.right >= bar.left);
CHECK(bar.width >= 0);
CHECK(bar.right() >= bar.x);
}
// --- zoneGrabAt ---------------------------------------------------------------
@@ -117,23 +118,23 @@ static void testZoneBarMalformedCollapses() {
static void testZoneGrabEdgesAndBody() {
const StripLayout L = wideStrip();
const Rect bar = zoneBarRect(L, 20, 60); // wide bar with a clear body
const int y = L.keys.top + 2;
const int y = L.keys.y + 2;
// Near the left edge -> low; near the right edge -> high; the middle -> body.
CHECK(zoneGrabAt(L, 20, 60, bar.left + 1, y) == ZoneGrab::kLowEdge);
CHECK(zoneGrabAt(L, 20, 60, bar.right - 1, y) == ZoneGrab::kHighEdge);
CHECK(zoneGrabAt(L, 20, 60, bar.left + bar.width() / 2, y) == ZoneGrab::kBody);
CHECK(zoneGrabAt(L, 20, 60, bar.x + 1, y) == ZoneGrab::kLowEdge);
CHECK(zoneGrabAt(L, 20, 60, bar.right() - 1, y) == ZoneGrab::kHighEdge);
CHECK(zoneGrabAt(L, 20, 60, bar.x + bar.width / 2, y) == ZoneGrab::kBody);
// Off the bar entirely -> none.
CHECK(zoneGrabAt(L, 20, 60, bar.right + 20, y) == ZoneGrab::kNone);
CHECK(zoneGrabAt(L, 20, 60, bar.right() + 20, y) == ZoneGrab::kNone);
}
static void testZoneGrabNarrowBarSplitsAtMidpointLowWins() {
const StripLayout L = wideStrip();
// A 1-key bar is narrower than 2*edge: no body; the low edge wins the exact midpoint.
const Rect bar = zoneBarRect(L, 50, 50);
const int y = L.keys.top + 2;
const int mid = bar.left + bar.width() / 2;
const int y = L.keys.y + 2;
const int mid = bar.x + bar.width / 2;
CHECK(zoneGrabAt(L, 50, 50, mid, y) == ZoneGrab::kLowEdge); // tie -> low
CHECK(zoneGrabAt(L, 50, 50, bar.right - 1, y) == ZoneGrab::kHighEdge);
CHECK(zoneGrabAt(L, 50, 50, bar.right() - 1, y) == ZoneGrab::kHighEdge);
}
// --- zoneBarAtPoint -----------------------------------------------------------
@@ -143,8 +144,8 @@ static void testZoneBarAtPointFirstMatch() {
const int lows[2] = {20, 30}; // zone 0 and zone 1 overlap on [30,50]
const int highs[2] = {50, 70};
const Rect overlap = zoneBarRect(L, 30, 50);
const int y = L.keys.top + 2;
const int cx = overlap.left + overlap.width() / 2;
const int y = L.keys.y + 2;
const int cx = overlap.x + overlap.width / 2;
// A point in the overlap resolves to the FIRST covering zone (draw order).
const ZoneBarHit hit = zoneBarAtPoint(L, lows, highs, 2, cx, y);
CHECK(hit.zoneIndex == 0);
@@ -219,7 +220,7 @@ static void testResolveDragProportionalNonDivisibleWidth() {
// a drag from note 0 by (width-1) pixels must land at keyAtPoint(width-1), which is 127.
const int width = 544;
const StripLayout L = layoutStrip(width, 40);
CHECK(keyAtPoint(L, width - 1, L.keys.top + 1) == 127);
CHECK(keyAtPoint(L, width - 1, L.keys.y + 1) == 127);
CHECK(resolveDragNote(L, 0, width - 1) == 127);
// Also verify mid-strip coherence: for each key N, a drag from 0 by N's left-edge
@@ -227,12 +228,12 @@ static void testResolveDragProportionalNonDivisibleWidth() {
// rounding may round down). The critical direction is that it must NOT over-shoot by
// more than 0 (it must reach at least the right key).
for (int n = 1; n < kStripKeyCount; ++n) {
const int leftPx = keyRect(L, n).left;
const int leftPx = keyRect(L, n).x;
const int resolved = resolveDragNote(L, 0, leftPx);
// The left edge of key N is the first pixel "in" that key, so we expect resolved == N.
// Allow resolved == N-1 only when the pixel is at the exact boundary (keyEdgeToX may
// produce the same x for adjacent keys when keys share a pixel). Disallow over-shoot.
const int expected = keyAtPoint(L, leftPx, L.keys.top + 1);
const int expected = keyAtPoint(L, leftPx, L.keys.y + 1);
CHECK(resolved >= expected - 1 && resolved <= expected + 1);
}
}
+33 -32
View File
@@ -1,4 +1,4 @@
// Standalone tests for reasampler::vst::knob_deck — no VST3, no REAPER, no framework. Same fast
// Standalone tests for reasampler::instrument::ui::knob_deck — no VST3, no REAPER, no framework. Same fast
// assert loop as the sibling pure tests. Assert the r11 deck layout HARD:
//
// * group width — caption row vs knob row max + padding; row-toggle and caption-toggle widths.
@@ -10,12 +10,13 @@
// * hit-test — knob cell hit (whole cell), toggle segment 0/1 boundaries, blank (-1) cells
// and fence padding miss, outside-deck miss.
#include "../src/vst/knob_deck.h"
#include "../src/core/instrument/ui/knob_deck.h"
#include <cstdio>
#include <vector>
using namespace reasampler::vst;
using namespace reasampler;
using namespace reasampler::instrument::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
@@ -75,20 +76,20 @@ static void testWrapAtNarrowWidthIsDeterministic() {
CHECK(dl.groups.size() == 5);
// Row membership: groups on row 1 share the first top; the wrapped groups sit one row
// pitch lower and restart at the left margin.
const int row0Top = dl.groups[0].box.top;
const int row0Top = dl.groups[0].box.y;
const int row1Top = row0Top + kDeckGroupH + kDeckRowGap;
CHECK(dl.groups[0].box.top == row0Top);
CHECK(dl.groups[1].box.top == row0Top);
CHECK(dl.groups[0].box.y == row0Top);
CHECK(dl.groups[1].box.y == row0Top);
bool sawWrap = false;
for (std::size_t i = 1; i < dl.groups.size(); ++i) {
if (dl.groups[i].box.top == row1Top && dl.groups[i - 1].box.top == row0Top) {
CHECK(dl.groups[i].box.left == 8); // wrapped row restarts at the left edge
if (dl.groups[i].box.y == row1Top && dl.groups[i - 1].box.y == row0Top) {
CHECK(dl.groups[i].box.x == 8); // wrapped row restarts at the left edge
sawWrap = true;
}
}
CHECK(sawWrap);
// Every box stays within the available width (no group straddles the right edge).
for (const auto& g : dl.groups) CHECK(g.box.right <= 8 + 544);
for (const auto& g : dl.groups) CHECK(g.box.right() <= 8 + 544);
}
static void testFirstGroupAlwaysPlaces() {
@@ -103,33 +104,33 @@ static void testGroupInnerGeometry() {
const DeckLayout dl = layoutDeck(deck, 8, 50, 824);
const DeckGroupLayout& amp = dl.groups[0];
// Caption row at the top padding; caption toggle right-anchored inside the box.
CHECK(amp.caption.top == amp.box.top + kDeckGroupPadY);
CHECK(amp.caption.y == amp.box.y + kDeckGroupPadY);
CHECK(amp.captionToggle.id == 100);
CHECK(amp.captionToggle.seg1.right == amp.box.right - kDeckGroupPadX);
CHECK(amp.captionToggle.seg0.right == amp.captionToggle.seg1.left);
CHECK(amp.captionToggle.seg0.width() == 44 && amp.captionToggle.seg1.width() == 44);
CHECK(amp.captionToggle.seg0.height() == kDeckToggleH);
CHECK(amp.captionToggle.seg1.right() == amp.box.right() - kDeckGroupPadX);
CHECK(amp.captionToggle.seg0.right() == amp.captionToggle.seg1.x);
CHECK(amp.captionToggle.seg0.width == 44 && amp.captionToggle.seg1.width == 44);
CHECK(amp.captionToggle.seg0.height == kDeckToggleH);
// The caption text rect stops before the toggle.
CHECK(amp.caption.right <= amp.captionToggle.seg0.left);
CHECK(amp.caption.right() <= amp.captionToggle.seg0.x);
// Cells: five, fixed size, abutting, inside the box, below the caption row.
CHECK(static_cast<int>(amp.cells.size()) == 5);
for (std::size_t i = 0; i < amp.cells.size(); ++i) {
const DeckCellLayout& c = amp.cells[i];
CHECK(c.cell.width() == kDeckCellW && c.cell.height() == kDeckCellH);
CHECK(c.cell.top == amp.box.top + kDeckGroupPadY + kDeckCaptionH + kDeckCaptionGap);
if (i > 0) CHECK(c.cell.left == amp.cells[i - 1].cell.right);
CHECK(c.cell.width == kDeckCellW && c.cell.height == kDeckCellH);
CHECK(c.cell.y == amp.box.y + kDeckGroupPadY + kDeckCaptionH + kDeckCaptionGap);
if (i > 0) CHECK(c.cell.x == amp.cells[i - 1].cell.right());
// Knob square centered horizontally, label band beneath it, both inside the cell.
CHECK(c.knob.width() == kDeckKnobSize && c.knob.height() == kDeckKnobSize);
CHECK(c.knob.left - c.cell.left == c.cell.right - c.knob.right);
CHECK(c.label.top >= c.knob.bottom);
CHECK(c.label.bottom <= c.cell.bottom);
CHECK(c.knob.width == kDeckKnobSize && c.knob.height == kDeckKnobSize);
CHECK(c.knob.x - c.cell.x == c.cell.right() - c.knob.right());
CHECK(c.label.y >= c.knob.bottom());
CHECK(c.label.bottom() <= c.cell.bottom());
}
// VOICE group's row toggle sits after its cell, vertically centered in the cell row.
const DeckGroupLayout& voice = dl.groups[3];
CHECK(voice.rowToggle.id == 104);
CHECK(voice.rowToggle.seg0.left == voice.cells[0].cell.right + kDeckToggleGap);
CHECK(voice.rowToggle.seg0.height() == kDeckToggleH);
CHECK(voice.rowToggle.seg0.top > voice.cells[0].cell.top);
CHECK(voice.rowToggle.seg0.x == voice.cells[0].cell.right() + kDeckToggleGap);
CHECK(voice.rowToggle.seg0.height == kDeckToggleH);
CHECK(voice.rowToggle.seg0.y > voice.cells[0].cell.y);
// MASTER has no toggles.
CHECK(dl.groups[4].captionToggle.id == -1);
CHECK(dl.groups[4].rowToggle.id == -1);
@@ -142,20 +143,20 @@ static void testHitTest() {
// Knob hit: anywhere in the cell (including the label band) resolves to the cell id.
const DeckCellLayout& c0 = amp.cells[0];
DeckHit h = hitTestDeck(dl, c0.cell.left + 1, c0.cell.top + 1);
DeckHit h = hitTestDeck(dl, c0.cell.x + 1, c0.cell.y + 1);
CHECK(h.kind == DeckHitKind::Knob && h.id == 1 && h.segment == -1);
h = hitTestDeck(dl, c0.label.left + 2, c0.label.top + 2);
h = hitTestDeck(dl, c0.label.x + 2, c0.label.y + 2);
CHECK(h.kind == DeckHitKind::Knob && h.id == 1);
// Caption toggle segments 0/1 at their boundary: last px of seg0, first px of seg1.
h = hitTestDeck(dl, amp.captionToggle.seg0.right - 1, amp.captionToggle.seg0.top + 1);
h = hitTestDeck(dl, amp.captionToggle.seg0.right() - 1, amp.captionToggle.seg0.y + 1);
CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 100 && h.segment == 0);
h = hitTestDeck(dl, amp.captionToggle.seg1.left, amp.captionToggle.seg1.top + 1);
h = hitTestDeck(dl, amp.captionToggle.seg1.x, amp.captionToggle.seg1.y + 1);
CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 100 && h.segment == 1);
// Row toggle.
const DeckGroupLayout& voice = dl.groups[3];
h = hitTestDeck(dl, voice.rowToggle.seg1.left + 1, voice.rowToggle.seg1.top + 1);
h = hitTestDeck(dl, voice.rowToggle.seg1.x + 1, voice.rowToggle.seg1.y + 1);
CHECK(h.kind == DeckHitKind::RowToggle && h.id == 104 && h.segment == 1);
// A blank cell (id -1) misses even though its rect exists.
@@ -164,11 +165,11 @@ static void testHitTest() {
const DeckLayout tl = layoutDeck(trig, 0, 0, 824);
const DeckCellLayout& blank = tl.groups[0].cells[4];
CHECK(blank.id == -1);
h = hitTestDeck(tl, blank.cell.left + 5, blank.cell.top + 5);
h = hitTestDeck(tl, blank.cell.x + 5, blank.cell.y + 5);
CHECK(h.kind == DeckHitKind::None);
// The fence padding inside the box misses; outside the deck misses.
h = hitTestDeck(dl, amp.box.left + 1, amp.box.bottom - 1);
h = hitTestDeck(dl, amp.box.x + 1, amp.box.bottom() - 1);
CHECK(h.kind == DeckHitKind::None);
h = hitTestDeck(dl, -50, -50);
CHECK(h.kind == DeckHitKind::None);
+2 -1
View File
@@ -9,12 +9,13 @@
// 3. Round-trip: managedLaneKey(laneNameForMode(m)) == "reasampler:" + m, so the
// Wave-3 minting path and the read path cannot drift.
#include "../src/lane_keys.h"
#include "../src/core/view/lane_keys.h"
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::view;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+4 -3
View File
@@ -1,4 +1,4 @@
// Standalone tests for reasampler::vst::master_gain — no VST3, no REAPER, no framework. Same
// Standalone tests for reasampler::instrument::engine::master_gain — no VST3, no REAPER, no framework. Same
// fast assert loop as the sibling pure tests. Assert the FB1 post-mixer gain taper HARD:
//
// * -inf bottom — norm 0 maps to -infinity dB and TRUE ZERO linear (silence, not an epsilon);
@@ -9,13 +9,14 @@
// * monotonicity — more norm never means less gain.
// * label — "-inf" at the bottom, signed one-decimal dB elsewhere.
#include "../src/vst/master_gain.h"
#include "../src/core/instrument/engine/master_gain.h"
#include <cmath>
#include <cstdio>
#include <cstring>
using namespace reasampler::vst;
using namespace reasampler;
using namespace reasampler::instrument::engine;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+3 -2
View File
@@ -7,13 +7,14 @@
// buttons are live and "…: Design" buttons are dead; when Arrange is active, the reverse. An
// unrecognized active id fails OPEN (every button live) so a future mode never dead-locks the bar.
#include "../src/mode_enable.h"
#include "../src/view_mode_model.h" // kArrangeModeId / kDesignModeId — the ids the rule keys off
#include "../src/core/ui/mode_enable.h"
#include "../src/core/view/view_mode_model.h" // kArrangeModeId / kDesignModeId — the ids the rule keys off
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -8,13 +8,14 @@
// evenly); hit-test hits per segment and misses (outside the band above/below/
// left/right, boundary pixels); degenerate widths and counts.
#include "../src/mode_switch.h"
#include "../src/core/view/mode_switch.h"
#include <cstddef>
#include <cstdio>
#include <vector>
using namespace reasampler;
using namespace reasampler::view;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+4 -3
View File
@@ -1,4 +1,4 @@
// Standalone tests for reasampler::vst::note_entry — no VST3, no REAPER, no framework.
// Standalone tests for reasampler::instrument::map::note_entry — no VST3, no REAPER, no framework.
// Assert the S12 direct-numeric-entry parse for a zone's low/high/root MIDI note.
//
// Covers: plain decimal integers (with +/- sign + surrounding whitespace); note names under the
@@ -6,11 +6,12 @@
// [0,127] rather than rejecting; empty / whitespace-only / unparseable input returning nullopt;
// the integer path taking precedence over the note-name path for a leading digit.
#include "../src/vst/note_entry.h"
#include "../src/core/instrument/map/note_entry.h"
#include <cstdio>
using namespace reasampler::vst;
using namespace reasampler;
using namespace reasampler::instrument::map;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -8,11 +8,12 @@
// a degenerate band reserves nothing; hit-test in/out/edge (half-open bounds); an empty button
// claims no point; draw and hit-test agree over the whole rect.
#include "../src/overflow_menu.h"
#include "../src/core/ui/overflow_menu.h"
#include <cstdio>
using namespace reasampler;
using namespace reasampler::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -7,12 +7,13 @@
// semantics, insertion-order preservation, malformed-parse -> nullopt (the persist
// shell's warn+fallback hinges on it), and round-trip of paths with JSON metacharacters.
#include "../src/owned_manifest.h"
#include "../src/core/model/owned_manifest.h"
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::model;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+64 -63
View File
@@ -1,4 +1,4 @@
// Standalone tests for reasampler::vst::param_slider — no VST3, no REAPER, no framework.
// Standalone tests for reasampler::instrument::ui::param_slider — no VST3, no REAPER, no framework.
// Same fast assert loop as the sibling pure editor tests (capture_browser / keyboard_strip):
// assert the S12/S15/S16 control-surface layout, toggle-segment split + hit-test, slider
// value<->pixel mapping (round-trip + clamping + endpoints), and point->control routing.
@@ -16,13 +16,14 @@
// o'clock), wrap-boundary + un-normalized arc inputs, the needle endpoint on the circle, and
// the vertical-drag delta->value map (up = increase) with clamping at 0/1.
#include "../src/vst/param_slider.h"
#include "../src/core/instrument/ui/param_slider.h"
#include <cmath>
#include <cstdio>
#include <vector>
using namespace reasampler::vst;
using namespace reasampler;
using namespace reasampler::instrument::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
@@ -33,7 +34,7 @@ static bool approx(double a, double b) { return (a - b) < 1e-9 && (b - a) < 1e-9
// --- layoutControls -----------------------------------------------------------
static void testLayoutStacksRows() {
const Rect panel{0, 100, 300, 400};
const Rect panel = Rect::ltrb(0, 100, 300, 400);
std::vector<ControlDesc> ctl{
{1, ControlKind::Toggle},
{2, ControlKind::Slider},
@@ -42,58 +43,58 @@ static void testLayoutStacksRows() {
const std::vector<ControlRow> rows = layoutControls(panel, ctl);
CHECK(rows.size() == 3);
// Row 0 sits at the panel top; each subsequent row is one row-height + gap below.
CHECK(rows[0].row.top == 100);
CHECK(rows[0].row.bottom == 100 + kControlRowHeight);
CHECK(rows[1].row.top == rows[0].row.bottom + kControlRowGap);
CHECK(rows[2].row.top == rows[1].row.bottom + kControlRowGap);
CHECK(rows[0].row.y == 100);
CHECK(rows[0].row.bottom() == 100 + kControlRowHeight);
CHECK(rows[1].row.y == rows[0].row.bottom() + kControlRowGap);
CHECK(rows[2].row.y == rows[1].row.bottom() + kControlRowGap);
// Ids + kinds carried through in order.
CHECK(rows[0].id == 1 && rows[0].kind == ControlKind::Toggle);
CHECK(rows[1].id == 2 && rows[1].kind == ControlKind::Slider);
// Label column then control column, contiguous, spanning the panel width.
CHECK(rows[0].label.left == panel.left);
CHECK(rows[0].control.left == rows[0].label.right);
CHECK(rows[0].control.right == panel.right);
CHECK(rows[0].label.width() == kControlLabelWidth);
CHECK(rows[0].label.x == panel.x);
CHECK(rows[0].control.x == rows[0].label.right());
CHECK(rows[0].control.right() == panel.right());
CHECK(rows[0].label.width == kControlLabelWidth);
}
static void testLayoutEmptyAndDegenerate() {
CHECK(layoutControls(Rect{0, 0, 300, 300}, {}).empty());
CHECK(layoutControls(Rect::ltrb(0, 0, 300, 300), {}).empty());
std::vector<ControlDesc> ctl{{1, ControlKind::Slider}};
CHECK(layoutControls(Rect{0, 0, 0, 0}, ctl).empty());
CHECK(layoutControls(Rect{0, 0, 300, 0}, ctl).empty());
CHECK(layoutControls(Rect::ltrb(0, 0, 0, 0), ctl).empty());
CHECK(layoutControls(Rect::ltrb(0, 0, 300, 0), ctl).empty());
}
static void testLayoutNarrowPanelClampsLabel() {
// A panel narrower than 2*labelWidth clamps the label column to half so a control column
// survives.
const Rect panel{0, 0, 100, 200};
const Rect panel = Rect::ltrb(0, 0, 100, 200);
const std::vector<ControlRow> rows = layoutControls(panel, {{1, ControlKind::Slider}});
CHECK(rows.size() == 1);
CHECK(rows[0].label.width() <= panel.width() / 2 + 1);
CHECK(rows[0].control.width() > 0);
CHECK(rows[0].label.width <= panel.width / 2 + 1);
CHECK(rows[0].control.width > 0);
}
// --- toggle -------------------------------------------------------------------
static void testToggleSegmentsTile() {
const Rect control{100, 0, 300, 22}; // width 200
const Rect control = Rect::ltrb(100, 0, 300, 22); // width 200
const Rect s0 = toggleSegmentRect(control, 0);
const Rect s1 = toggleSegmentRect(control, 1);
CHECK(s0.left == 100 && s0.right == 200);
CHECK(s1.left == 200 && s1.right == 300); // last absorbs remainder -> reaches control.right
CHECK(s0.x == 100 && s0.right() == 200);
CHECK(s1.x == 200 && s1.right() == 300); // last absorbs remainder -> reaches control.right()
// Out of range.
CHECK(toggleSegmentRect(control, 2).width() == 0);
CHECK(toggleSegmentRect(control, -1).width() == 0);
CHECK(toggleSegmentRect(control, 2).width == 0);
CHECK(toggleSegmentRect(control, -1).width == 0);
}
static void testToggleSegmentRemainderInLast() {
const Rect control{0, 0, 201, 22}; // odd width -> seg0 = 100, seg1 = 101 (absorbs remainder)
CHECK(toggleSegmentRect(control, 0).width() == 100);
CHECK(toggleSegmentRect(control, 1).right == 201);
const Rect control = Rect::ltrb(0, 0, 201, 22); // odd width -> seg0 = 100, seg1 = 101 (absorbs remainder)
CHECK(toggleSegmentRect(control, 0).width == 100);
CHECK(toggleSegmentRect(control, 1).right() == 201);
}
static void testToggleHitTest() {
const Rect control{100, 0, 300, 22};
const Rect control = Rect::ltrb(100, 0, 300, 22);
CHECK(toggleSegmentHitTest(control, 150, 10) == 0);
CHECK(toggleSegmentHitTest(control, 250, 10) == 1);
CHECK(toggleSegmentHitTest(control, 50, 10) == -1); // left of control
@@ -103,59 +104,59 @@ static void testToggleHitTest() {
// --- slider -------------------------------------------------------------------
static void testSliderTrackInsetsHalfHandle() {
const Rect control{100, 0, 300, 22};
const Rect control = Rect::ltrb(100, 0, 300, 22);
const Rect track = sliderTrackRect(control);
CHECK(track.left == control.left + kSliderHandleWidth / 2);
CHECK(track.right == control.right - kSliderHandleWidth / 2);
CHECK(track.x == control.x + kSliderHandleWidth / 2);
CHECK(track.right() == control.right() - kSliderHandleWidth / 2);
// A control too narrow for a handle yields an empty track.
CHECK(sliderTrackRect(Rect{0, 0, kSliderHandleWidth - 1, 22}).width() == 0);
CHECK(sliderTrackRect(Rect::ltrb(0, 0, kSliderHandleWidth - 1, 22)).width == 0);
}
static void testSliderHandleAtEndpointsAndMid() {
const Rect control{100, 0, 300, 22};
const Rect control = Rect::ltrb(100, 0, 300, 22);
const Rect track = sliderTrackRect(control);
const int half = kSliderHandleWidth / 2;
// Value 0 -> handle centered at track.left.
// Value 0 -> handle centered at track.x.
const Rect h0 = sliderHandleRect(control, 0.0);
CHECK(h0.left + half == track.left);
// Value 1 -> handle centered at track.right.
CHECK(h0.x + half == track.x);
// Value 1 -> handle centered at track.right().
const Rect h1 = sliderHandleRect(control, 1.0);
CHECK(h1.left + half == track.right);
CHECK(h1.x + half == track.right());
// Value 0.5 -> centered at the track middle.
const Rect hm = sliderHandleRect(control, 0.5);
CHECK(hm.left + half == track.left + track.width() / 2);
CHECK(hm.x + half == track.x + track.width / 2);
}
static void testSliderHandleClampsOutOfRange() {
const Rect control{0, 0, 200, 22};
CHECK(sliderHandleRect(control, -0.5).left == sliderHandleRect(control, 0.0).left);
CHECK(sliderHandleRect(control, 5.0).left == sliderHandleRect(control, 1.0).left);
const Rect control = Rect::ltrb(0, 0, 200, 22);
CHECK(sliderHandleRect(control, -0.5).x == sliderHandleRect(control, 0.0).x);
CHECK(sliderHandleRect(control, 5.0).x == sliderHandleRect(control, 1.0).x);
}
static void testValueAtPointEndpointsSaturate() {
const Rect control{100, 0, 300, 22};
const Rect control = Rect::ltrb(100, 0, 300, 22);
const Rect track = sliderTrackRect(control);
CHECK(approx(valueAtPoint(control, track.left - 20), 0.0));
CHECK(approx(valueAtPoint(control, track.left), 0.0));
CHECK(approx(valueAtPoint(control, track.right + 20), 1.0));
CHECK(approx(valueAtPoint(control, track.right), 1.0));
CHECK(approx(valueAtPoint(control, track.x - 20), 0.0));
CHECK(approx(valueAtPoint(control, track.x), 0.0));
CHECK(approx(valueAtPoint(control, track.right() + 20), 1.0));
CHECK(approx(valueAtPoint(control, track.right()), 1.0));
}
static void testValueAtPointIsHandleInverse() {
// Round-trip: a value -> handle center -> valueAtPoint recovers (within one pixel quantum).
const Rect control{50, 0, 450, 22}; // wide track for pixel resolution
const Rect control = Rect::ltrb(50, 0, 450, 22); // wide track for pixel resolution
const Rect track = sliderTrackRect(control);
for (double v : {0.1, 0.25, 0.5, 0.75, 0.9}) {
const Rect h = sliderHandleRect(control, v);
const int centerX = h.left + kSliderHandleWidth / 2;
const int centerX = h.x + kSliderHandleWidth / 2;
const double back = valueAtPoint(control, centerX);
CHECK(back >= v - 0.01 && back <= v + 0.01);
CHECK(centerX >= track.left && centerX <= track.right);
CHECK(centerX >= track.x && centerX <= track.right());
}
}
static void testValueAtPointDegenerateTrack() {
CHECK(approx(valueAtPoint(Rect{0, 0, kSliderHandleWidth - 1, 22}, 5), 0.0));
CHECK(approx(valueAtPoint(Rect::ltrb(0, 0, kSliderHandleWidth - 1, 22), 5), 0.0));
}
// --- knob (FA4) -----------------------------------------------------------------
@@ -164,21 +165,21 @@ static bool nearWithin(double a, double b, double tol) { return (a - b) < tol &&
static void testKnobGeometryInscribesCell() {
// A 44x44 cell at (100,0): center (122,22), radius 22.
const KnobGeometry g = computeKnob(Rect{100, 0, 144, 44});
const KnobGeometry g = computeKnob(Rect::ltrb(100, 0, 144, 44));
CHECK(approx(g.centerX, 122.0));
CHECK(approx(g.centerY, 22.0));
CHECK(approx(g.radius, 22.0));
// A wide cell inscribes on the smaller (vertical) dimension.
const KnobGeometry w = computeKnob(Rect{0, 0, 200, 22});
const KnobGeometry w = computeKnob(Rect::ltrb(0, 0, 200, 22));
CHECK(approx(w.radius, 11.0));
CHECK(approx(w.centerX, 100.0));
// Degenerate cells yield radius 0.
CHECK(computeKnob(Rect{0, 0, 0, 22}).radius == 0.0);
CHECK(computeKnob(Rect{0, 0, 22, 0}).radius == 0.0);
CHECK(computeKnob(Rect::ltrb(0, 0, 0, 22)).radius == 0.0);
CHECK(computeKnob(Rect::ltrb(0, 0, 22, 0)).radius == 0.0);
}
static void testKnobHitTestCircle() {
const KnobGeometry g = computeKnob(Rect{100, 0, 144, 44}); // center (122,22), r 22
const KnobGeometry g = computeKnob(Rect::ltrb(100, 0, 144, 44)); // center (122,22), r 22
CHECK(knobHitTest(g, 122, 22)); // center — always hits
CHECK(!knobHitTest(g, 122 + 22, 22)); // exactly on the boundary — boundary exclusive
CHECK(!knobHitTest(g, 122 + 22, 44)); // cell corner: inside the rect, outside the circle
@@ -223,7 +224,7 @@ static void testKnobArcWrapBoundary() {
}
static void testKnobNeedlePointOnCircle() {
const KnobGeometry g = computeKnob(Rect{100, 0, 144, 44}); // center (122,22), r 22
const KnobGeometry g = computeKnob(Rect::ltrb(100, 0, 144, 44)); // center (122,22), r 22
// Default arc, value 0 -> 7 o'clock -> needle points down-left from center.
const KnobPoint p7 = knobNeedlePoint(g, KnobArc{}, 0.0);
// 210° clockwise from 12: sin(210°)=-0.5, cos(210°)=-√3/2 -> x = cx - r/2, y = cy + r*√3/2
@@ -265,7 +266,7 @@ static void testKnobDragClamps() {
// --- controlAtPoint routing ---------------------------------------------------
static void testControlAtPointRoutes() {
const Rect panel{0, 0, 300, 400};
const Rect panel = Rect::ltrb(0, 0, 300, 400);
std::vector<ControlDesc> ctl{
{10, ControlKind::Toggle},
{20, ControlKind::Slider},
@@ -274,26 +275,26 @@ static void testControlAtPointRoutes() {
const std::vector<ControlRow> rows = layoutControls(panel, ctl);
// A point in the toggle's control area routes to the toggle id.
const Rect tctl = rows[0].control;
CHECK(controlAtPoint(rows, (tctl.left + tctl.right) / 2, (tctl.top + tctl.bottom) / 2) == 10);
CHECK(controlAtPoint(rows, (tctl.x + tctl.right()) / 2, (tctl.y + tctl.bottom()) / 2) == 10);
// A point on the slider's track routes to the slider id.
const Rect strack = sliderTrackRect(rows[1].control);
CHECK(controlAtPoint(rows, (strack.left + strack.right) / 2,
(strack.top + strack.bottom) / 2) == 20);
CHECK(controlAtPoint(rows, (strack.x + strack.right()) / 2,
(strack.y + strack.bottom()) / 2) == 20);
// A point at the knob's center routes to the knob id; the control-rect corner (outside
// the circle) is a miss.
const KnobGeometry kg = computeKnob(rows[2].control);
CHECK(controlAtPoint(rows, static_cast<int>(kg.centerX), static_cast<int>(kg.centerY)) == 30);
CHECK(controlAtPoint(rows, rows[2].control.left + 1, rows[2].control.top + 1) == -1);
CHECK(controlAtPoint(rows, rows[2].control.x + 1, rows[2].control.y + 1) == -1);
}
static void testControlAtPointMisses() {
const Rect panel{0, 0, 300, 400};
const Rect panel = Rect::ltrb(0, 0, 300, 400);
const std::vector<ControlRow> rows =
layoutControls(panel, {{10, ControlKind::Toggle}, {20, ControlKind::Slider}});
// The label column is not interactive.
CHECK(controlAtPoint(rows, rows[0].label.left + 2, rows[0].label.top + 4) == -1);
CHECK(controlAtPoint(rows, rows[0].label.x + 2, rows[0].label.y + 4) == -1);
// The gap between rows is a miss.
const int gapY = rows[0].row.bottom + kControlRowGap / 2;
const int gapY = rows[0].row.bottom() + kControlRowGap / 2;
CHECK(controlAtPoint(rows, 200, gapY) == -1);
// Off-panel below.
CHECK(controlAtPoint(rows, 200, 5000) == -1);
+2 -1
View File
@@ -11,13 +11,14 @@
// merge, steep disjoint-span merge, no-bin-dropped spike sweep, no-column-empty
// coverage, col clamp, degenerate inputs.
#include "../src/peaks.h"
#include "../src/core/audio/peaks.h"
#include <cmath>
#include <cstdio>
#include <vector>
using namespace reasampler;
using namespace reasampler::audio;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -27,13 +27,14 @@
// the master's splice decision (jump/lag/frac/fadeLen AND firing frame) exactly, on
// decorrelated stereo content where an independent per-channel search provably diverges.
#include "../src/vst/pitch_shift.h"
#include "../src/core/instrument/engine/pitch_shift.h"
#include <cmath>
#include <cstdio>
#include <vector>
using namespace reasampler;
using namespace reasampler::instrument::engine;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+6 -5
View File
@@ -15,15 +15,16 @@
// The Sample-JSON round-trip of the fingerprint (leveraging M1's existing provenance
// round-trip) is exercised in test_bank_model.cpp — see the fingerprint case there.
#include "../src/provenance.h"
#include "../src/core/model/provenance.h"
#include "../src/bank_model.h" // recipe-through-Sample-JSON round-trip (M1 seam)
#include "../src/core/model/bank_model.h" // recipe-through-Sample-JSON round-trip (M1 seam)
#include <cstdio>
#include <string>
#include <vector>
using namespace reasampler;
using namespace reasampler::model;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
@@ -246,7 +247,7 @@ static void testHugeGuidCountRejectedBeforeReserve() {
// --- recorded-recipe model round-trips through the Sample JSON ----------------
// The fingerprint rides in Provenance.fxChainSnapshot (one string), which M1's
// BankIndex JSON already round-trips. Prove a real recipe survives that path intact.
// BankModel JSON already round-trips. Prove a real recipe survives that path intact.
static void testRecipeThroughSampleJson() {
const CaptureRecipe r = baseRecipe();
@@ -260,10 +261,10 @@ static void testRecipeThroughSampleJson() {
prov.fxChainSnapshot = buildFingerprint(r);
s.provenance = prov;
BankIndex idx;
BankModel idx;
CHECK(idx.add(s) == AddResult::Added);
auto back = BankIndex::deserialize(idx.serialize());
auto back = BankModel::deserialize(idx.serialize());
CHECK(back.has_value());
const Sample* child = back ? back->query("child-1") : nullptr;
CHECK(child != nullptr);
+2 -1
View File
@@ -7,11 +7,12 @@
// tail-label inset or is degenerate; hit-test in/out/edge (half-open bounds); a
// suppressed/empty button claims no point; draw and hit-test agree over the whole rect.
#include "../src/prune_button.h"
#include "../src/core/ui/prune_button.h"
#include <cstdio>
using namespace reasampler;
using namespace reasampler::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+4 -2
View File
@@ -15,8 +15,8 @@
// Plus: determinism (present-order output), duplicate-`present` de-dup, exact-string
// (non-normalizing) match, and the referencedPaths() union query directly.
#include "../src/bank_book.h"
#include "../src/prune_reconcile.h"
#include "../src/core/model/bank_book.h"
#include "../src/core/reclaim/prune_reconcile.h"
#include <algorithm>
#include <cstdint>
@@ -26,6 +26,8 @@
#include <vector>
using namespace reasampler;
using namespace reasampler::model;
using namespace reasampler::reclaim;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -3,12 +3,13 @@
// record-mode/recipe bookkeeping (channel count + tap -> I_RECMODE / I_RECMODE_FLAGS)
// and the wet/dry -> tap decision, plus the recorded-file -> Sample mapping.
#include "../src/realtime_record.h"
#include "../src/core/capture/realtime_record.h"
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::capture;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -6,7 +6,7 @@
// FX-bypass plan (corrects the "items captured through parent FX" defect), and the
// capture-action taxonomy table (stable ids, scope x tail-variant matrix).
#include "../src/render_settings.h"
#include "../src/core/capture/render_settings.h"
#include <cmath>
#include <cstdio>
@@ -14,6 +14,7 @@
#include <string>
using namespace reasampler;
using namespace reasampler::capture;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+18 -15
View File
@@ -22,7 +22,7 @@
// stride contract across the seam (that the byte stride wav_trim reports matches the
// channel-count stride downmixToMono divides by).
#include "../src/vst/sample_map.h"
#include "../src/core/instrument/map/sample_map.h"
#include <cmath>
#include <cstdio>
@@ -30,18 +30,21 @@
#include <string>
#include <vector>
#include "../src/bank_book.h"
#include "../src/bank_model.h"
#include "../src/vst/master_gain.h" // masterGainMaxLinear (the v8 master-gain wire cap)
#include "../src/core/model/bank_book.h"
#include "../src/core/model/bank_model.h"
#include "../src/core/instrument/engine/master_gain.h" // masterGainMaxLinear (the v8 master-gain wire cap)
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::model;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
// Build a Sample with the fields sample_map reads. Relative path is required by
// BankIndex::add (relative-only invariant); a content hash is set so dedup does not
// BankModel::add (relative-only invariant); a content hash is set so dedup does not
// collapse distinct entries.
static Sample makeSample(const std::string& id, const std::string& name,
const std::string& rel, std::optional<int> root) {
@@ -62,7 +65,7 @@ static std::string bookJson(const std::vector<Sample>& poolSamples,
for (const Sample& s : poolSamples) book.pool().index.add(s);
if (!drumSamples.empty()) {
book.createBank("drums-id", "Drums");
BankIndex* di = book.index("drums-id");
BankModel* di = book.index("drums-id");
for (const Sample& s : drumSamples) di->add(s);
}
return book.serialize();
@@ -1305,7 +1308,7 @@ static void testComponentStateMasterGainWriterClamps() {
hi.masterGainLinear = 1000.0;
CHECK(std::fabs(deserializeComponentState(serializeComponentState(hi), 44100.0)
.masterGainLinear -
vst::masterGainMaxLinear()) < 1e-9);
instrument::engine::masterGainMaxLinear()) < 1e-9);
ComponentState lo;
lo.masterGainLinear = -5.0;
CHECK(deserializeComponentState(serializeComponentState(lo), 44100.0).masterGainLinear ==
@@ -1686,7 +1689,7 @@ static void testVelocityCurveRoundTrip() {
// A second zone left at the flat default proves the field is per-record and defaults to flat y=1.
PerformanceMap m;
PerformanceZone z = zone("lead", 20, 100);
z.velocityCurve = vst::VelocityCurve::linear();
z.velocityCurve = VelocityCurve::linear();
z.velocityCurve.addPoint(60.0, 0.3); // an interior knot to exercise multi-point round-trip
m.zones.push_back(z);
m.zones.push_back(zone("pad", 0, 19)); // default flat curve
@@ -1694,7 +1697,7 @@ static void testVelocityCurveRoundTrip() {
CHECK(back.zones.size() == 2);
if (back.zones.size() != 2) return;
CHECK(back.zones[0].velocityCurve.equals(z.velocityCurve)); // exact point round-trip
CHECK(back.zones[1].velocityCurve.equals(vst::VelocityCurve::flat())); // default preserved
CHECK(back.zones[1].velocityCurve.equals(VelocityCurve::flat())); // default preserved
// And the flat default really is unity everywhere (R10-F1 Option A), not the old linear ramp.
CHECK(back.zones[1].velocityCurve.eval(1.0) == 1.0);
CHECK(back.zones[1].velocityCurve.eval(64.0) == 1.0);
@@ -1706,12 +1709,12 @@ static void testVelocityCurveThroughComponentEnvelope() {
ComponentState s;
s.selectionId = "pick";
PerformanceZone z = zone("pick", 0, 127);
z.velocityCurve = vst::VelocityCurve::linear();
z.velocityCurve = VelocityCurve::linear();
s.map.zones.push_back(z);
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
CHECK(back.map.zones.size() == 1);
if (back.map.zones.size() != 1) return;
CHECK(back.map.zones[0].velocityCurve.equals(vst::VelocityCurve::linear()));
CHECK(back.map.zones[0].velocityCurve.equals(VelocityCurve::linear()));
}
static void testVelocityCurveResolvesToZone() {
@@ -1720,12 +1723,12 @@ static void testVelocityCurveResolvesToZone() {
const std::string json = bookJson({makeSample("a", "Kick", "b/a.wav", 36)}, {});
PerformanceMap m;
PerformanceZone z = zone("a", 0, 127);
z.velocityCurve = vst::VelocityCurve::linear();
z.velocityCurve = VelocityCurve::linear();
m.zones.push_back(z);
const ResolvedPerformance r = resolvePerformance(json, m);
CHECK(r.zones.size() == 1);
if (r.zones.size() != 1) return;
CHECK(r.zones[0].velocityCurve.equals(vst::VelocityCurve::linear()));
CHECK(r.zones[0].velocityCurve.equals(VelocityCurve::linear()));
}
// FA1 bug 3a — the COMPOSED end-to-end regression, mirroring the processor's reload composition
@@ -1740,7 +1743,7 @@ static void testVelocityCurveEndToEndThroughReloadComposition() {
ComponentState s;
s.selectionId = "a";
PerformanceZone z = zone("a", 0, 127);
z.velocityCurve = vst::VelocityCurve::linear();
z.velocityCurve = VelocityCurve::linear();
s.map.zones.push_back(z);
const ComponentState back = deserializeComponentState(serializeComponentState(s), 48000.0);
CHECK(back.map.zones.size() == 1);
@@ -1817,7 +1820,7 @@ static void testVelocityCurveV6BackCompatLiftsToFlat() {
CHECK(back.zones[0].sampleId == "v6saved");
CHECK(back.zones[0].keyTrack == 0.5); // the v6 field still read correctly
// No curve tail -> flat y=1 default (the deliberate behavior change).
CHECK(back.zones[0].velocityCurve.equals(vst::VelocityCurve::flat()));
CHECK(back.zones[0].velocityCurve.equals(VelocityCurve::flat()));
CHECK(back.zones[0].velocityCurve.eval(20.0) == 1.0); // a soft hit now plays at unity
}
+4 -2
View File
@@ -22,7 +22,7 @@
// the pure identity matcher (UID hex / module filename base / display name, beta
// over-protect), and the composed pruneOrphans exclusion proof.
#include "../src/sample_usage.h"
#include "../src/core/wire/sample_usage.h"
#include <cstdio>
#include <optional>
@@ -30,9 +30,11 @@
#include <unordered_set>
#include <vector>
#include "../src/prune_reconcile.h" // mergeReferenced + pruneOrphans (composed proof)
#include "../src/core/reclaim/prune_reconcile.h" // mergeReferenced + pruneOrphans (composed proof)
using namespace reasampler;
using namespace reasampler::reclaim;
using namespace reasampler::wire;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+8 -7
View File
@@ -17,7 +17,7 @@
// by the CMake target linking neither SDK — this file includes only sampler_core.h +
// the standard library, which is itself the compile-time proof.
#include "../src/vst/sampler_core.h"
#include "../src/core/instrument/engine/sampler_core.h"
#include <algorithm>
#include <cmath>
@@ -25,6 +25,7 @@
#include <vector>
using namespace reasampler;
using namespace reasampler::instrument::engine;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
@@ -426,7 +427,7 @@ static void testNoteOffReleasesNewestSameNote() {
// A LINEAR velocity curve keeps the two velocities distinguishable (velocity/127). The default
// flat y=1 curve (S-VIEW-9 R10-F1) would render both at unity, collapsing the distinction this
// note-off-selection test relies on — so we opt this zone back to the linear response.
km.zones[0].velocityCurve = vst::VelocityCurve::linear();
km.zones[0].velocityCurve = VelocityCurve::linear();
VoiceEngine eng(8, km);
std::size_t first = eng.noteOn(60, velOld); // older voice, lower gain
@@ -751,7 +752,7 @@ static void testVelocityDefaultCurveIsFlatUnity() {
// (not a hardcoded map) drives the gain, and that eval is applied at note-on.
static void testVelocityLinearCurveReproducesRamp() {
Keymap km = Keymap::singleSampleChromatic(dcSample(100, 60)); // DC 1.0
km.zones[0].velocityCurve = vst::VelocityCurve::linear();
km.zones[0].velocityCurve = VelocityCurve::linear();
{
VoiceEngine eng(1, km);
eng.noteOn(60, 127);
@@ -776,7 +777,7 @@ static void testVelocityLinearCurveReproducesRamp() {
// curve's shaped value, not the linear one. Proves the whole curve, not just the endpoints, applies.
static void testVelocityShapedCurveDrivesGain() {
Keymap km = Keymap::singleSampleChromatic(dcSample(100, 60)); // DC 1.0
vst::VelocityCurve curve = vst::VelocityCurve::linear();
VelocityCurve curve = VelocityCurve::linear();
curve.addPoint(64.0, 0.9); // pull the mid-velocity response UP to 0.9
km.zones[0].velocityCurve = curve;
VoiceEngine eng(1, km);
@@ -1429,7 +1430,7 @@ static void testVelocityCurveAppliesUnderPreserve() {
SampleData s = dcSample(4000, 60);
s.play.pitchEngine = PitchEngine::Preserve;
Keymap km = Keymap::singleSampleChromatic(std::move(s));
km.zones[0].velocityCurve = vst::VelocityCurve::linear();
km.zones[0].velocityCurve = VelocityCurve::linear();
VoiceEngine eng(1, km, /*preserveCap=*/0, /*window=*/256);
eng.noteOn(62, vel); // transposed: the genuine shifter path (not the unity demotion)
std::vector<AudioSample> out;
@@ -1578,7 +1579,7 @@ static void testMonoRepressHeldNoteMovesToTop() {
static void testMonoRetriggerFallbackUsesOriginalVelocity() {
SampleData s = dcLevelSample(200000, 1.0f, 60);
Keymap km = Keymap::singleSampleChromatic(std::move(s));
km.zones[0].velocityCurve = vst::VelocityCurve::linear(); // gain = velocity/127
km.zones[0].velocityCurve = VelocityCurve::linear(); // gain = velocity/127
VoiceEngine eng(4, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger);
eng.noteOn(60, 32); // soft first note
CHECK(approx(probeFrame(eng), 32.0 / 127.0, 1e-4));
@@ -1645,7 +1646,7 @@ static void testMonoLegatoRetunesWithoutReadRestart() {
s.rootNote = 60;
s.play.adsr = flatAdsr();
Keymap km = Keymap::singleSampleChromatic(std::move(s));
km.zones[0].velocityCurve = vst::VelocityCurve::linear();
km.zones[0].velocityCurve = VelocityCurve::linear();
VoiceEngine eng(2, km, 0, 0, VoiceMode::Mono, MonoTrigger::Legato);
eng.noteOn(60, 127); // unity: read advances 1/frame, full gain
std::vector<AudioSample> out;
+2 -1
View File
@@ -10,13 +10,14 @@
// left/right chevron precedence at the ends, dead space between visible tabs,
// outside the band above/below/left/right, half-open boundary pixels).
#include "../src/tab_strip.h"
#include "../src/core/ui/tab_strip.h"
#include <cstddef>
#include <cstdio>
#include <vector>
using namespace reasampler;
using namespace reasampler::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -3,12 +3,13 @@
// (None -> Auto -> Manual -> None), the manual-length clamp to the 8 s cap, and the
// toggle label text. The drawing / click hit-testing is DAW-verified in bank_panel.
#include "../src/tail_control.h"
#include "../src/core/capture/tail_control.h"
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::capture;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -8,7 +8,7 @@
// interaction-state transform behaves, the WCAG math is correct against known anchors, and
// the Direction C spectral ramp interpolates its endpoints.
#include "../src/theme.h"
#include "../src/core/ui/theme.h"
#include <cmath>
#include <cstdio>
@@ -16,6 +16,7 @@
#include <initializer_list>
using namespace reasampler;
using namespace reasampler::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -5,12 +5,13 @@
// match); placement BELOW the anchor centred; horizontal clamp at both client edges; the
// bottom-edge flip to ABOVE; the both-clip clamp for a tall tooltip; degenerate inputs -> empty.
#include "../src/tooltip.h"
#include "../src/core/ui/tooltip.h"
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::ui;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+4 -3
View File
@@ -1,4 +1,4 @@
// Standalone tests for reasampler::vst::trigger_seam — no VST3, no REAPER, no framework.
// Standalone tests for reasampler::instrument::map::trigger_seam — no VST3, no REAPER, no framework.
// Same fast assert loop as the sibling pure tests.
//
// Covers: triggerPlayLength (zero play length, startFrame set, startFrame past frameCount,
@@ -6,11 +6,12 @@
// (zero play length, rounding); round-trip fidelity; the Finding 1 regression (start-point
// set — the case that was broken before this module existed).
#include "../src/vst/trigger_seam.h"
#include "../src/core/instrument/map/trigger_seam.h"
#include <cstdio>
using namespace reasampler::vst;
using namespace reasampler;
using namespace reasampler::instrument::map;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+4 -3
View File
@@ -1,4 +1,4 @@
// Standalone tests for reasampler::vst::velocity_curve — no VST3, no REAPER, no framework. Same fast
// Standalone tests for reasampler::instrument::engine::velocity_curve — no VST3, no REAPER, no framework. Same fast
// assert loop as the sibling pure tests. Assert the S-VIEW-9 velocity->amp transfer curve HARD:
//
// * eval — flat y=1 default (R10-F1 Option A: EVERY velocity -> 1.0), linear ramp, curved shape
@@ -11,12 +11,13 @@
// * fromPoints — the deserialization repair: sorts by X, box-clamps, forces endpoints, and falls
// back to flat() for a sub-2-point list.
#include "../src/vst/velocity_curve.h"
#include "../src/core/instrument/engine/velocity_curve.h"
#include <cmath>
#include <cstdio>
using namespace reasampler::vst;
using namespace reasampler;
using namespace reasampler::instrument::engine;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+3 -2
View File
@@ -15,14 +15,15 @@
// (park-while-parked) so untagged leaves return to visible after toggling back;
// guards the in-DAW "all leaves hidden after toggling twice" regression.
#include "../src/view_mode_model.h"
#include "../src/lane_keys.h" // laneNameForMode — assert the minting plan's durable keys
#include "../src/core/view/view_mode_model.h"
#include "../src/core/view/lane_keys.h" // laneNameForMode — assert the minting plan's durable keys
#include <algorithm>
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::view;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -3,12 +3,13 @@
// one genuinely pure piece: the I_FOLDERDEPTH walk that turns REAPER's linear
// track stream into the parent<->child FolderTree the pure model consumes.
#include "../src/view_tree.h"
#include "../src/core/view/view_tree.h"
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::view;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+2 -1
View File
@@ -7,7 +7,7 @@
// extraction (whole / tail window / clamp / out-of-range); truncate plan (kept<all,
// no-op keep-all, kept==0, grow rejected) with exact size-field values.
#include "../src/wav_trim.h"
#include "../src/core/capture/wav_trim.h"
#include <cstdint>
#include <cstdio>
@@ -15,6 +15,7 @@
#include <vector>
using namespace reasampler;
using namespace reasampler::capture;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
+37 -36
View File
@@ -1,4 +1,4 @@
// Standalone tests for reasampler::vst::waveform_view — no VST3, no REAPER, no framework.
// Standalone tests for reasampler::instrument::ui::waveform_view — no VST3, no REAPER, no framework.
// Same fast assert loop as the sibling pure tests. Assert the S11 waveform surface's
// frame<->pixel mapping, marker grab regions, drag-delta frame resolver (with clamps), and
// the zero-crossing snap — the geometry + snap that back the draggable start/loop markers.
@@ -9,61 +9,62 @@
// no-ops); nearestZeroCrossing (nearest sign-change, sample-on-zero, equidistant-tie-to-lower,
// no-crossing keeps target, target clamp, degenerate buffers).
#include "../src/vst/waveform_view.h"
#include "../src/core/instrument/ui/waveform_view.h"
#include <cstdio>
#include <vector>
using namespace reasampler::vst;
using reasampler::AudioSample;
using namespace reasampler;
using namespace reasampler::instrument::ui;
using reasampler::audio::AudioSample;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
// A comfortable waveform area: 1000px wide, offset so left != 0 (catches origin bugs).
static Rect wideArea() { return Rect{20, 10, 1020, 90}; } // width 1000
static Rect wideArea() { return Rect::ltrb(20, 10, 1020, 90); } // width 1000
// --- frameToX / xToFrame ------------------------------------------------------
static void testFrameToXEndpoints() {
const Rect a = wideArea();
CHECK(frameToX(a, 1000, 0) == a.left); // frame 0 -> left edge
CHECK(frameToX(a, 1000, 1000) == a.right); // frameCount -> right edge
CHECK(frameToX(a, 1000, 500) == a.left + 500); // midpoint (1:1 here)
CHECK(frameToX(a, 1000, 0) == a.x); // frame 0 -> left edge
CHECK(frameToX(a, 1000, 1000) == a.right()); // frameCount -> right edge
CHECK(frameToX(a, 1000, 500) == a.x + 500); // midpoint (1:1 here)
}
static void testFrameToXClampsOutOfRange() {
const Rect a = wideArea();
CHECK(frameToX(a, 1000, -50) == a.left); // below 0 pins left
CHECK(frameToX(a, 1000, 5000) == a.right); // above count pins right
CHECK(frameToX(a, 1000, -50) == a.x); // below 0 pins left
CHECK(frameToX(a, 1000, 5000) == a.right()); // above count pins right
}
static void testFrameToXDegenerate() {
const Rect a = wideArea();
CHECK(frameToX(a, 0, 100) == a.left); // no frames -> left
const Rect z = Rect{5, 5, 5, 45}; // zero width
CHECK(frameToX(z, 1000, 500) == z.left);
CHECK(frameToX(a, 0, 100) == a.x); // no frames -> left
const Rect z = Rect::ltrb(5, 5, 5, 45); // zero width
CHECK(frameToX(z, 1000, 500) == z.x);
}
static void testXToFrameInverse() {
const Rect a = wideArea();
CHECK(xToFrame(a, 1000, a.left) == 0);
CHECK(xToFrame(a, 1000, a.right) == 1000);
CHECK(xToFrame(a, 1000, a.left + 250) == 250); // 1:1 map here
CHECK(xToFrame(a, 1000, a.x) == 0);
CHECK(xToFrame(a, 1000, a.right()) == 1000);
CHECK(xToFrame(a, 1000, a.x + 250) == 250); // 1:1 map here
}
static void testXToFrameClampsOutside() {
const Rect a = wideArea();
CHECK(xToFrame(a, 1000, a.left - 100) == 0); // left of area -> 0
CHECK(xToFrame(a, 1000, a.right + 100) == 1000); // right of area -> frameCount
CHECK(xToFrame(a, 0, a.left + 10) == 0); // no frames -> 0
CHECK(xToFrame(a, 1000, a.x - 100) == 0); // left of area -> 0
CHECK(xToFrame(a, 1000, a.right() + 100) == 1000); // right of area -> frameCount
CHECK(xToFrame(a, 0, a.x + 10) == 0); // no frames -> 0
}
static void testFrameToXRoundTrip() {
// Round-trip at a non-1:1 scale: 800px area over 2000 frames (2.5 frames/px). frameToX then
// xToFrame should land within a couple frames (rounding both directions).
const Rect a = Rect{0, 0, 800, 60};
const Rect a = Rect::ltrb(0, 0, 800, 60);
for (std::int64_t f = 0; f <= 2000; f += 137) {
const int x = frameToX(a, 2000, f);
const std::int64_t back = xToFrame(a, 2000, x);
@@ -77,39 +78,39 @@ static void testMarkerAtPointGrabsWithinBand() {
const Rect a = wideArea();
// Markers at frames 100, 500, 900 -> x = left+100, left+500, left+900.
const std::int64_t frames[3] = {100, 500, 900};
const int midY = a.top + a.height() / 2;
CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 100, midY) == 0);
CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 500, midY) == 1);
CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 900, midY) == 2);
const int midY = a.y + a.height / 2;
CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 100, midY) == 0);
CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 500, midY) == 1);
CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 900, midY) == 2);
// Within the grab band on either side of the line.
CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 500 + kMarkerGrabWidth, midY) == 1);
CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 500 - kMarkerGrabWidth, midY) == 1);
CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 500 + kMarkerGrabWidth, midY) == 1);
CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 500 - kMarkerGrabWidth, midY) == 1);
}
static void testMarkerAtPointMissesBetween() {
const Rect a = wideArea();
const std::int64_t frames[3] = {100, 500, 900};
const int midY = a.top + a.height() / 2;
const int midY = a.y + a.height / 2;
// Well away from any marker line.
CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 300, midY) == -1);
CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 300, midY) == -1);
// Off the area vertically.
CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 500, a.top - 5) == -1);
CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 500, a.y - 5) == -1);
}
static void testMarkerAtPointFirstMatchOnOverlap() {
const Rect a = wideArea();
// Two markers at the same frame -> first in order wins.
const std::int64_t frames[2] = {400, 400};
const int midY = a.top + a.height() / 2;
CHECK(markerAtPoint(a, 1000, frames, 2, a.left + 400, midY) == 0);
const int midY = a.y + a.height / 2;
CHECK(markerAtPoint(a, 1000, frames, 2, a.x + 400, midY) == 0);
}
static void testMarkerAtPointRejectsNullEmpty() {
const Rect a = wideArea();
const int midY = a.top + a.height() / 2;
CHECK(markerAtPoint(a, 1000, nullptr, 3, a.left + 100, midY) == -1);
const int midY = a.y + a.height / 2;
CHECK(markerAtPoint(a, 1000, nullptr, 3, a.x + 100, midY) == -1);
const std::int64_t frames[1] = {100};
CHECK(markerAtPoint(a, 1000, frames, 0, a.left + 100, midY) == -1);
CHECK(markerAtPoint(a, 1000, frames, 0, a.x + 100, midY) == -1);
}
// --- resolveDragFrame ---------------------------------------------------------
@@ -130,7 +131,7 @@ static void testResolveDragFrameClamps() {
static void testResolveDragFrameRounds() {
// 500px area over 1000 frames -> 2 frames/px. A +3px drag -> round(6.0)=6; the rounding is
// at the frame centre. Use a scale where a fractional result appears.
const Rect a = Rect{0, 0, 300, 60}; // 1000 frames / 300px = 3.33 frames/px
const Rect a = Rect::ltrb(0, 0, 300, 60); // 1000 frames / 300px = 3.33 frames/px
// +3px -> 3*1000/300 = 10.0 -> 10 frames.
CHECK(resolveDragFrame(a, 1000, 100, 3) == 110);
// +1px -> 1000/300 = 3.33 -> rounds to 3.
@@ -138,7 +139,7 @@ static void testResolveDragFrameRounds() {
}
static void testResolveDragFrameDegenerate() {
const Rect z = Rect{0, 0, 0, 60}; // zero width
const Rect z = Rect::ltrb(0, 0, 0, 60); // zero width
CHECK(resolveDragFrame(z, 1000, 300, 100) == 300); // pinned to start
const Rect a = wideArea();
CHECK(resolveDragFrame(a, 0, 300, 100) == 0); // no frames -> clamp(start)=0
+1
View File
@@ -14,6 +14,7 @@
#include <string>
using namespace reasampler;
using namespace reasampler::wire;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \