filter: sweep the corner continuously; raise the editor floor to 840x620

Cutoff-only re-solve (15.5 vs 56.9 ns/frame) makes the unquantized sweep
affordable, replacing the 2048-step mod quantizer. Live-compute parameters
remain blocked on a shell-architecture ruling.
This commit is contained in:
2026-07-30 18:12:10 -04:00
parent 39389c1183
commit 0cfd9b6236
14 changed files with 237 additions and 88 deletions
+36 -20
View File
@@ -1,10 +1,12 @@
// Standalone tests for reasampler::instrument::ui::deck_groups — no VST3, no REAPER, no
// framework. knob_deck's own tests pin how a descriptor list LAYS OUT; these pin WHICH
// descriptors the Sample face carries: the signal-flow group order (pitch -> filter -> amp),
// the Filter group's contents, the wrapped deck height at the editor's two pinned widths, the
// hit-test reaching the new filter controls, and the bipolar knob law's inverse pair.
// the Filter group's contents, the wrapped deck height at the editor's floor width and its fit
// inside the floor window, the hit-test reaching the new filter controls, and the bipolar knob
// law's inverse pair.
#include "../src/core/instrument/ui/deck_groups.h"
#include "../src/core/instrument/ui/sample_bands.h"
#include <cmath>
#include <cstdio>
@@ -17,10 +19,9 @@ static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
// The editor's two pinned client widths (checkSizeConstraint's 560 floor, the 840 default),
// less the band allocator's kPad inset on each side.
static constexpr int kAvailAtMinWidth = 560 - 16;
static constexpr int kAvailAtDefaultWidth = 840 - 16;
// The editor's floor width, which is also its default (checkSizeConstraint clamps to it), less
// the band allocator's kPad inset on each side.
static constexpr int kAvailAtMinWidth = kEditorMinWidth - 2 * kPad;
static int indexOfGroup(const std::vector<DeckGroupDesc>& g, int id) {
for (std::size_t i = 0; i < g.size(); ++i) {
@@ -86,34 +87,48 @@ static void testAmpGroupWidthSurvivesAGateTriggerFlip() {
CHECK(a.cellIds.size() == b.cellIds.size());
CHECK(b.cellIds[3] == -1 && b.cellIds[4] == -1);
// Every other group is mode-independent, so the whole deck's height is too.
CHECK(deckHeight(gate, kAvailAtDefaultWidth) == deckHeight(trig, kAvailAtDefaultWidth));
CHECK(deckHeight(gate, kAvailAtMinWidth) == deckHeight(trig, kAvailAtMinWidth));
}
static void testWrappedDeckHeightAtThePinnedEditorWidths() {
static void testWrappedDeckHeightAtTheEditorFloorWidth() {
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
// At the default 840 the deck takes two rows: PITCH + PITCH ENV + FILTER fill the first,
// the remaining four fit the second.
CHECK(deckRowCount(g, kAvailAtDefaultWidth) == 2);
CHECK(deckHeight(g, kAvailAtDefaultWidth) == 2 * kDeckGroupH + kDeckRowGap);
// At the 560 floor it takes four: FILTER (440px) fits the row alone, but not alongside
// PITCH + PITCH ENV (318 + 12 + 440 = 770 > 544), so it wraps to its own row.
CHECK(deckRowCount(g, kAvailAtMinWidth) == 4);
CHECK(deckHeight(g, kAvailAtMinWidth) == 4 * kDeckGroupH + 3 * kDeckRowGap);
// At the floor (== default) 840 the deck takes two rows: PITCH + PITCH ENV + FILTER fill
// the first, the remaining four fit the second.
CHECK(deckRowCount(g, kAvailAtMinWidth) == 2);
CHECK(deckHeight(g, kAvailAtMinWidth) == 2 * kDeckGroupH + kDeckRowGap);
// Whole groups only, never split: every group's box lies inside the available width or is
// the first of its row.
const DeckLayout dl = layoutDeck(g, 8, 0, kAvailAtDefaultWidth);
const DeckLayout dl = layoutDeck(g, kPad, 0, kAvailAtMinWidth);
CHECK(dl.groups.size() == g.size());
for (const DeckGroupLayout& gl : dl.groups) {
CHECK(gl.box.x >= 8);
CHECK(gl.box.x >= kPad);
CHECK(gl.box.height == kDeckGroupH);
}
}
// The guard the raised floor exists to provide: at the smallest window the host can produce,
// the deck band still lands inside the client area AND the waveform still gets its two-lane
// floor. Growing the deck past what 620 px can hold fails HERE instead of silently pushing
// FILTER ENV / AMP / VOICE / MASTER off-screen, where there is no scroll to reach them.
static void testDeckFitsInsideTheEnforcedMinimumWindow() {
for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) {
const std::vector<DeckGroupDesc> g = sampleDeckGroups(mode);
const int h = deckHeight(g, kAvailAtMinWidth);
const SampleBands b = computeSampleBands(kEditorMinWidth, kEditorMinHeight, h);
CHECK(b.decks.height == h);
// Bottom-anchored INSIDE the pad is the whole assertion: the degrade path pushes the
// deck down until the waveform hits its floor, so any deck too tall to fit stops
// landing on this exact line. A `<= kEditorMinHeight` bound would not catch it — the
// degrade can still leave the deck ending at the window edge.
CHECK(b.decks.bottom() == kEditorMinHeight - kPad);
CHECK(b.waveform.height >= kWaveformMinHeight);
}
}
static void testHitTestResolvesTheNewFilterControls() {
const std::vector<DeckGroupDesc> g = sampleDeckGroups(PlayMode::Gate);
const DeckLayout dl = layoutDeck(g, 8, 40, kAvailAtDefaultWidth);
const DeckLayout dl = layoutDeck(g, kPad, 40, kAvailAtMinWidth);
const DeckGroupLayout& f =
dl.groups[static_cast<std::size_t>(indexOfGroup(g, kGroupFilter))];
@@ -175,7 +190,8 @@ int main() {
testDeckReadsPitchThenFilterThenAmpLeftToRight();
testFilterGroupCarriesItsFiveToneControlsPlusModulation();
testAmpGroupWidthSurvivesAGateTriggerFlip();
testWrappedDeckHeightAtThePinnedEditorWidths();
testWrappedDeckHeightAtTheEditorFloorWidth();
testDeckFitsInsideTheEnforcedMinimumWindow();
testHitTestResolvesTheNewFilterControls();
testBipolarKnobLawRoundTripsAndIsExactAtCentre();
if (g_fail == 0) std::printf("deck_groups: all tests passed\n");