instrument: latch the note done at the read-head run-off, fit the migrated fades, and lift the curve dial and overlay selection into pure modules

This commit is contained in:
2026-07-31 09:17:04 -04:00
parent 13e8c5c4d9
commit d60ab1524a
18 changed files with 575 additions and 129 deletions
+62 -2
View File
@@ -3,8 +3,9 @@
// 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 floor width and its fit
// inside the floor window, the hit-test reaching the new filter controls, the bipolar knob
// law's inverse pair, and the commit-tier routing — which controls are live, and which drags
// take the live tier.
// law's inverse pair, the commit-tier routing — which controls are live, and which drags take
// the live tier — and the overlay-selection state machine (exclusivity, the none resting state,
// and which selections are inert).
#include "../src/core/instrument/ui/deck_groups.h"
#include "../src/core/instrument/ui/sample_bands.h"
@@ -341,7 +342,66 @@ static void testOnlyALiveControlsDragTakesTheLiveTier() {
CHECK(!liveCommitFor(LiveDragKind::kOther, static_cast<int>(DeckParam::kFilterCutoff)));
}
// --- The overlay selection state machine ---------------------------------------
static int radio(DeckParam p) { return static_cast<int>(p); }
// EXCLUSIVITY: picking another deck's radio switches to it outright — two envelopes can never
// be overlay-active at once, whatever the previous selection was.
static void testOverlaySelectionIsExclusiveAcrossTheThreeEnvelopeDecks() {
const OverlayEnv states[] = {OverlayEnv::kNone, OverlayEnv::kAmp, OverlayEnv::kPitch,
OverlayEnv::kFilter};
for (OverlayEnv from : states) {
if (from != OverlayEnv::kAmp) {
CHECK(nextOverlaySelection(from, radio(DeckParam::kAmpEnvSelect)) == OverlayEnv::kAmp);
}
if (from != OverlayEnv::kPitch) {
CHECK(nextOverlaySelection(from, radio(DeckParam::kPitchEnvSelect)) ==
OverlayEnv::kPitch);
}
if (from != OverlayEnv::kFilter) {
CHECK(nextOverlaySelection(from, radio(DeckParam::kFilterEnvSelect)) ==
OverlayEnv::kFilter);
}
}
}
// kNone is a RESTING STATE the user can get back to: clicking the active radio clears it.
static void testClickingTheActiveOverlayRadioClearsToNone() {
CHECK(nextOverlaySelection(OverlayEnv::kAmp, radio(DeckParam::kAmpEnvSelect)) ==
OverlayEnv::kNone);
CHECK(nextOverlaySelection(OverlayEnv::kPitch, radio(DeckParam::kPitchEnvSelect)) ==
OverlayEnv::kNone);
CHECK(nextOverlaySelection(OverlayEnv::kFilter, radio(DeckParam::kFilterEnvSelect)) ==
OverlayEnv::kNone);
}
// A control that is not one of the three radios selects nothing and clears nothing.
static void testANonRadioIdLeavesTheOverlaySelectionAlone() {
CHECK(overlayEnvForRadio(radio(DeckParam::kFilterCutoff)) == OverlayEnv::kNone);
CHECK(overlayEnvForRadio(-1) == OverlayEnv::kNone);
CHECK(nextOverlaySelection(OverlayEnv::kFilter, radio(DeckParam::kFilterCutoff)) ==
OverlayEnv::kFilter);
CHECK(nextOverlaySelection(OverlayEnv::kAmp, 9999) == OverlayEnv::kAmp);
}
// An overlay whose deck group is switched OFF is inert, matching the drawn-but-dead knobs on
// the same params: a node drag must not reach a value the knob refuses.
static void testOverlayIsInertExactlyWhenItsGroupToggleIsOff() {
CHECK(overlayEnvInert(OverlayEnv::kPitch, /*pitchEnv=*/false, /*filter=*/true));
CHECK(!overlayEnvInert(OverlayEnv::kPitch, true, true));
CHECK(overlayEnvInert(OverlayEnv::kFilter, true, /*filter=*/false));
CHECK(!overlayEnvInert(OverlayEnv::kFilter, true, true));
// Amp has no enable toggle, so it is never inert; kNone draws nothing to grab.
CHECK(!overlayEnvInert(OverlayEnv::kAmp, false, false));
CHECK(!overlayEnvInert(OverlayEnv::kNone, false, false));
}
int main() {
testOverlaySelectionIsExclusiveAcrossTheThreeEnvelopeDecks();
testClickingTheActiveOverlayRadioClearsToNone();
testANonRadioIdLeavesTheOverlaySelectionAlone();
testOverlayIsInertExactlyWhenItsGroupToggleIsOff();
testEveryDeckControlIsClassifiedLiveOrReloading();
testOnlyALiveControlsDragTakesTheLiveTier();
testDeckReadsPitchThenFilterThenAmpLeftToRight();