// Standalone tests for reasampler::instrument::ui::deck_values — no VST3, no REAPER, no // framework. Covers the deck's parameter-set binding: the norm <-> stored-value round trip on a // representative control of each domain, the DOUBLE-CLICK RESET (each ring of a dual-ring knob // resetting only its own field), and the ms time-constant formatter across its whole range. #include "../src/core/instrument/ui/deck_values.h" #include "../src/core/instrument/engine/master_gain.h" #include "../src/core/instrument/ui/deck_groups.h" #include #include #include #include using namespace reasampler; using namespace reasampler::instrument::ui; namespace engine = reasampler::instrument::engine; // the stretcher's own rate bounds + clamp 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 stage-time ceiling has TWO names — the overlay's schematic domain and the knob's — and they // must be the same number or a maxed knob stops landing on the canvas edge. Asserted, not assumed. static void testTheTwoCeilingNamesAreOneNumber() { CHECK(kEnvTimeMaxSeconds == kGateStageMaxSeconds); CHECK(kEnvTimeMaxSeconds == kStageTimeMaxSeconds); CHECK(kEnvTimeMaxSeconds == 10.0); } // Every domain the binding maps: a stage time through the shared taper, a level, a fraction, // a normalized filter position, a bipolar depth, and a curve exponent over its log travel. static void testNormRoundTripsThroughEveryValueDomain() { PlaySeconds p; setDeckParam(DeckParam::kAttack, p, 0.25, 0); CHECK(p.adsr.attackSeconds == timeSecondsFromNorm(0.25)); // The VALUE round trip is what has to be exact (param_taper.h); the needle returning to the // very same norm double is explicitly NOT required of a log map. The residual is bounded by // the taper's output quantum read back through the map — under 1e-7 of the travel across the // whole domain, which is four orders below one drag pixel. CHECK(std::fabs(deckParamNorm(DeckParam::kAttack, p) - 0.25) < 1e-7); // The raised ceiling costs the low end nothing: a several-second stage is reachable by hand, // AND everything under 100 ms still gets more than 40 % of the knob's travel to itself. setDeckParam(DeckParam::kDecay, p, 0.95, 0); CHECK(p.adsr.decaySeconds > 5.0 && p.adsr.decaySeconds < kEnvTimeMaxSeconds); setDeckParam(DeckParam::kDecay, p, 0.42, 0); CHECK(p.adsr.decaySeconds < 0.100); setDeckParam(DeckParam::kSustain, p, 0.4, 0); CHECK(p.adsr.sustainLevel == 0.4); CHECK(deckParamNorm(DeckParam::kSustain, p) == 0.4); setDeckParam(DeckParam::kTrigHold, p, 0.75, 0); CHECK(p.trigAhd.holdFraction == 0.75); CHECK(deckParamNorm(DeckParam::kTrigHold, p) == 0.75); // Named field, not just a round trip: cutoff and morph are both normalized positions with // the same 1.0 default, so a getter+setter pair that swapped them would round-trip cleanly. setDeckParam(DeckParam::kFilterCutoff, p, 0.25, 0); CHECK(p.filter.settings.cutoffNorm == 0.25f); CHECK(p.filter.settings.morphNorm == 1.0f); CHECK(deckParamNorm(DeckParam::kFilterCutoff, p) == 0.25); // Bipolar: the centre detent is exact in BOTH directions, so a knob parked at centre // persists no depth at all. setDeckParam(DeckParam::kFilterModAmt, p, 0.5, 0); CHECK(p.filter.modAmount == 0.0); CHECK(deckParamNorm(DeckParam::kFilterModAmt, p) == 0.5); setDeckParam(DeckParam::kFilterModAmt, p, 1.0, 0); CHECK(p.filter.modAmount == 1.0); // A curve exponent off neutral survives the round trip; the centre snaps to exactly 1.0. setDeckParam(DeckParam::kAttackCurve, p, 1.0, 0); CHECK(p.adsr.attackCurve > 1.0); CHECK(deckParamNorm(DeckParam::kAttackCurve, p) == 1.0); setDeckParam(DeckParam::kAttackCurve, p, 0.5, 0); CHECK(p.adsr.attackCurve == 1.0); // Out-of-range norms clamp rather than writing an out-of-domain param. setDeckParam(DeckParam::kDecay, p, 2.0, 0); CHECK(p.adsr.decaySeconds == kEnvTimeMaxSeconds); setDeckParam(DeckParam::kDecay, p, -1.0, 0); CHECK(p.adsr.decaySeconds == 0.0); } // Rate's range is the STRETCHER's, aliased rather than restated, so the knob's two ends and the // engine's clamp cannot become two opinions. Asserted against the engine constants themselves. static void testRateKnobEndsAreTheStretchersOwnBounds() { CHECK(kRateMinRatio == engine::kStretchRateMin); CHECK(kRateMaxRatio == engine::kStretchRateMax); PlaySeconds p; setDeckParam(DeckParam::kRate, p, 0.0, 0); CHECK(p.playRate == engine::kStretchRateMin); CHECK(engine::clampStretchRate(p.playRate) == p.playRate); // the clamp has nothing to do setDeckParam(DeckParam::kRate, p, 1.0, 0); CHECK(p.playRate == engine::kStretchRateMax); CHECK(engine::clampStretchRate(p.playRate) == p.playRate); // And nowhere on the travel does the knob produce a rate the engine would move. for (int i = 0; i <= 1000; ++i) { setDeckParam(DeckParam::kRate, p, static_cast(i) / 1000.0, 0); CHECK(engine::clampStretchRate(p.playRate) == p.playRate); if (engine::clampStretchRate(p.playRate) != p.playRate) return; } } // The two new bindings write the two new fields and nothing else — both are doubles on // PlaySeconds with adjacent homes, so a getter/setter pair that crossed them would still // round-trip. The centre detent is exact on both, which is what lets an untouched knob persist // unity rate and zero transposition. static void testRateAndPitchBindTheirOwnFields() { PlaySeconds p; setDeckParam(DeckParam::kRate, p, 0.5, 0); CHECK(p.playRate == 1.0); CHECK(p.pitchOffsetSemitones == 0.0); CHECK(deckParamNorm(DeckParam::kRate, p) == 0.5); setDeckParam(DeckParam::kPitch, p, 0.5, 0); CHECK(p.pitchOffsetSemitones == 0.0); CHECK(p.playRate == 1.0); CHECK(deckParamNorm(DeckParam::kPitch, p) == 0.5); // Pitch rides the SAME centre-expanded depth taper as the pitch envelope's own depth, over // the SAME throw — a second constant here would be the defect the spec names. setDeckParam(DeckParam::kPitch, p, 1.0, 0); CHECK(p.pitchOffsetSemitones == kPitchDepthMaxSemis); CHECK(kPitchDepthMaxSemis == kVelocityPitchRangeSemitones); setDeckParam(DeckParam::kPitch, p, 0.0, 0); CHECK(p.pitchOffsetSemitones == -kPitchDepthMaxSemis); CHECK(p.playRate == 1.0); // untouched by every write above but its own // A move on Rate leaves the offset alone, in the other direction. setDeckParam(DeckParam::kPitch, p, 0.5, 0); setDeckParam(DeckParam::kRate, p, 0.0, 0); CHECK(p.pitchOffsetSemitones == 0.0); } // Shift's whole unit on BOTH new knobs is the semitone, not the percent their labels read in. // Asserted through the deck's own snap entry point (the shell calls nothing else), and in // semitones, which is the unit the rule is stated in. static void testShiftSnapsBothNewKnobsToWholeSemitones() { CHECK(deckParamUnit(DeckParam::kRate) == UnitCategory::Semitones); CHECK(deckParamUnit(DeckParam::kPitch) == UnitCategory::Semitones); PlaySeconds p; // Rate: a norm a third of the way up is 8 semitones below unity — snapping must land on a // whole one, and the knob must still be able to reach an octave and a fifth by hand. for (double norm : {0.13, 0.37, 0.5, 0.62, 0.88}) { setDeckParam(DeckParam::kRate, p, snapDeckParamNorm(DeckParam::kRate, norm), 0); const double semis = 12.0 * std::log2(p.playRate); CHECK(std::fabs(semis - std::round(semis)) < 1e-9); if (!(std::fabs(semis - std::round(semis)) < 1e-9)) return; } // The two landmarks by name: unity, and a fifth up. setDeckParam(DeckParam::kRate, p, snapDeckParamNorm(DeckParam::kRate, 0.5), 0); CHECK(p.playRate == 1.0); setDeckParam(DeckParam::kRate, p, snapDeckParamNorm(DeckParam::kRate, 0.5 + 7.0 / 24.0), 0); CHECK(std::fabs(12.0 * std::log2(p.playRate) - 7.0) < 1e-9); // Pitch: whole semitones on the centre-expanded taper, exactly (its taper resolves onto a // micro-semitone grid, so a whole semitone is ON that grid). for (double norm : {0.17, 0.33, 0.71, 0.94}) { setDeckParam(DeckParam::kPitch, p, snapDeckParamNorm(DeckParam::kPitch, norm), 0); CHECK(p.pitchOffsetSemitones == std::round(p.pitchOffsetSemitones)); if (p.pitchOffsetSemitones != std::round(p.pitchOffsetSemitones)) return; } } // The dual-ring reset contract: the outer ring resets the stage VALUE and the inner dial resets // the EXPONENT, each leaving the other exactly as it was. Both fields are asserted in both // directions — checking only the field that changed would pass even if the reset clobbered its // neighbour. static void testResetTouchesOnlyItsOwnRingOnADualRingKnob() { const PlaySeconds defaults; const struct { DeckParam knob; DeckParam curve; } pairs[] = { {DeckParam::kAttack, DeckParam::kAttackCurve}, {DeckParam::kDecay, DeckParam::kDecayCurve}, {DeckParam::kRelease, DeckParam::kReleaseCurve}, {DeckParam::kTrigAttack, DeckParam::kTrigAttackCurve}, {DeckParam::kPitchEnvDecay, DeckParam::kPitchEnvDecayCurve}, {DeckParam::kFilterEnvRelease, DeckParam::kFilterEnvReleaseCurve}, }; for (const auto& pr : pairs) { // Dial BOTH rings well away from their defaults. PlaySeconds p; setDeckParam(pr.knob, p, 0.6, 0); setDeckParam(pr.curve, p, 0.9, 0); const double dialledValue = deckParamNorm(pr.knob, p); const double dialledCurve = deckParamNorm(pr.curve, p); CHECK(dialledValue != deckParamNorm(pr.knob, defaults)); CHECK(dialledCurve != deckParamNorm(pr.curve, defaults)); // INNER: the exponent goes to exactly the linear neutral, the value does not move. PlaySeconds inner = p; resetDeckParam(pr.curve, inner); CHECK(deckParamNorm(pr.curve, inner) == deckParamNorm(pr.curve, defaults)); CHECK(deckParamNorm(pr.curve, inner) == 0.5); // the exponent itself is 1.0 CHECK(deckParamNorm(pr.knob, inner) == dialledValue); // OUTER: the value goes to its default, the exponent does not move. PlaySeconds outer = p; resetDeckParam(pr.knob, outer); CHECK(deckParamNorm(pr.knob, outer) == deckParamNorm(pr.knob, defaults)); CHECK(deckParamNorm(pr.curve, outer) == dialledCurve); } } // The exponent reset is specified as EXACTLY 1.0 — the identity curveMap short-circuits on // (curve_law.h), not merely something that rounds to it. static void testInnerResetLandsOnTheExactLinearNeutral() { PlaySeconds p; setDeckParam(DeckParam::kAttackCurve, p, 0.2, 0); CHECK(p.adsr.attackCurve < 1.0); resetDeckParam(DeckParam::kAttackCurve, p); CHECK(p.adsr.attackCurve == 1.0); setDeckParam(DeckParam::kFilterTrigDecayCurve, p, 0.95, 0); CHECK(p.filter.trigEnv.decayCurve > 1.0); resetDeckParam(DeckParam::kFilterTrigDecayCurve, p); CHECK(p.filter.trigEnv.decayCurve == 1.0); } // A reset lands on the field's own stored default, EXACTLY — the defaults are read off a fresh // PlaySeconds and COPIED rather than round-tripped, which is what makes the two stage times whose // defaults are neither 0 nor 1 land bit for bit at a non-power-of-two ceiling. static void testResetLandsOnTheStoredDefaultOfEachControl() { const PlaySeconds defaults; PlaySeconds p; setDeckParam(DeckParam::kSustain, p, 0.1, 0); setDeckParam(DeckParam::kTrigLength, p, 0.3, 0); setDeckParam(DeckParam::kFilterKeyTrack, p, 0.9, 0); setDeckParam(DeckParam::kPitchEnvDepth, p, 1.0, 0); setDeckParam(DeckParam::kAttack, p, 0.5, 0); setDeckParam(DeckParam::kRelease, p, 0.5, 0); CHECK(p.adsr.attackSeconds != defaults.adsr.attackSeconds); CHECK(p.adsr.releaseSeconds != defaults.adsr.releaseSeconds); resetDeckParam(DeckParam::kSustain, p); resetDeckParam(DeckParam::kTrigLength, p); resetDeckParam(DeckParam::kFilterKeyTrack, p); resetDeckParam(DeckParam::kPitchEnvDepth, p); resetDeckParam(DeckParam::kAttack, p); resetDeckParam(DeckParam::kRelease, p); CHECK(p.adsr.sustainLevel == defaults.adsr.sustainLevel); CHECK(p.trigger.lengthFraction == defaults.trigger.lengthFraction); CHECK(p.filter.keyTrack == defaults.filter.keyTrack); CHECK(p.pitchEnv.peakSemitones == defaults.pitchEnv.peakSemitones); CHECK(p.adsr.attackSeconds == defaults.adsr.attackSeconds); CHECK(p.adsr.releaseSeconds == defaults.adsr.releaseSeconds); } // EVERY knob resets to its own stored default, not just the six dual-ring pairs above. Swept // over the whole control-id space so a control added later cannot quietly miss the reset table: // perturb, reset, and require the control to read exactly what a fresh PlaySeconds reads. // Compared against the STORED FIELD directly (deckDoubleField/deckFloatField), not the // normalized read-back: deckParamNorm is not guaranteed injective, so a norm match is weaker // than the criterion — verification against a default-constructed PlaySeconds. static void testEveryKnobIdResetsToItsDefault() { PlaySeconds defaults; for (int i = 0; i < static_cast(DeckParam::kCount); ++i) { const DeckParam id = static_cast(i); if (deckParamUnit(id) == UnitCategory::None) continue; // no reset gesture if (id == DeckParam::kMasterGain || id == DeckParam::kKeyTrack) continue; // not in PlaySeconds PlaySeconds p; setDeckParam(id, p, 0.37, 0); setDeckParam(id, p, 0.83, 0); // two writes: one of the two is off every default if (double* pd = deckDoubleField(id, p)) { CHECK(*pd != *deckDoubleField(id, defaults)); resetDeckParam(id, p); CHECK(*pd == *deckDoubleField(id, defaults)); } else if (float* pf = deckFloatField(id, p)) { CHECK(*pf != *deckFloatField(id, defaults)); resetDeckParam(id, p); CHECK(*pf == *deckFloatField(id, defaults)); } else { CHECK(false); // every non-None, non-excluded id must own a reset field } } } // THE exact-preimage criterion, per unit category, against a default-constructed PlaySeconds and // against master gain's unity. A host's reset-to-default arrives as toPlain(defaultNorm) with no // bypass available, so this is the assertion the reset bypass CANNOT stand in for. static void testEveryDefaultHasAnExactNormalizedPreimage() { const PlaySeconds d; const struct { DeckParam id; double stored; } msKnobs[] = { {DeckParam::kAttack, d.adsr.attackSeconds}, {DeckParam::kHold, d.adsr.holdSeconds}, {DeckParam::kDecay, d.adsr.decaySeconds}, {DeckParam::kRelease, d.adsr.releaseSeconds}, {DeckParam::kTrigAttack, d.trigAhd.attackSeconds}, {DeckParam::kTrigDecay, d.trigAhd.decaySeconds}, {DeckParam::kPitchEnvAttack, d.pitchEnv.shape.attackSeconds}, {DeckParam::kPitchEnvDecay, d.pitchEnv.shape.decaySeconds}, {DeckParam::kFilterEnvAttack, d.filter.env.attackSeconds}, {DeckParam::kFilterEnvHold, d.filter.env.holdSeconds}, {DeckParam::kFilterEnvDecay, d.filter.env.decaySeconds}, {DeckParam::kFilterEnvRelease, d.filter.env.releaseSeconds}, {DeckParam::kFilterTrigAttack, d.filter.trigEnv.attackSeconds}, {DeckParam::kFilterTrigDecay, d.filter.trigEnv.decaySeconds}, }; for (const auto& k : msKnobs) { CHECK(timeSecondsFromNorm(deckParamNorm(k.id, d)) == k.stored); } // The two whose defaults are neither 0 nor the ceiling are the ones that can actually fail. CHECK(d.adsr.attackSeconds == 0.003 && d.adsr.releaseSeconds == 0.060); CHECK(depthSemitonesFromNorm(deckParamNorm(DeckParam::kPitchEnvDepth, d), kPitchDepthMaxSemis) == d.pitchEnv.peakSemitones); CHECK(deckParamNorm(DeckParam::kSustain, d) == d.adsr.sustainLevel); CHECK(deckParamNorm(DeckParam::kTrigLength, d) == d.trigger.lengthFraction); CHECK(deckParamNorm(DeckParam::kTrigHold, d) == d.trigAhd.holdFraction); CHECK(deckBipolarFromNorm(deckParamNorm(DeckParam::kFilterModAmt, d)) == d.filter.modAmount); // The PITCH/RATE pair. Rate's preimage is the taper's unity detent, which sits at true // centre only because these bounds are reciprocal; Pitch's is the depth taper's exact zero. CHECK(rateRatioFromNorm(deckParamNorm(DeckParam::kRate, d), kRateMinRatio, kRateMaxRatio) == d.playRate); CHECK(depthSemitonesFromNorm(deckParamNorm(DeckParam::kPitch, d), kPitchDepthMaxSemis) == d.pitchOffsetSemitones); CHECK(util::curveFromKnobNorm(deckParamNorm(DeckParam::kAttackCurve, d)) == d.adsr.attackCurve); // Master gain's unity: the case where a hair off is an audible gain error rather than a // cosmetic one. Its taper is engine/master_gain's — consumed here, not defined here. CHECK(instrument::engine::masterGainLinearFromNorm(instrument::engine::masterGainNormFromLinear(1.0)) == 1.0); } // Shift's snap unit is a property of the control's UNIT and lands on a whole unit of what the // control DISPLAYS — which is why three controls sharing the Percent category take three // different norm steps. static void testShiftSnapsToAWholeUnitOfTheDisplayedValue() { // Milliseconds: the snapped norm reads back as an exact whole millisecond. const double ms = timeSecondsFromNorm(snapDeckParamNorm(DeckParam::kAttack, timeNormFromSeconds(0.03472))); CHECK(ms == 0.035); // Semitones. CHECK(depthSemitonesFromNorm( snapDeckParamNorm(DeckParam::kPitchEnvDepth, depthNormFromSemitones(6.6, kPitchDepthMaxSemis)), kPitchDepthMaxSemis) == 7.0); // Percent, 0..100 %: the norm IS the fraction. CHECK(snapDeckParamNorm(DeckParam::kSustain, 0.4162) == 0.42); // Percent, 0..200 %: a whole DISPLAYED percent is half a norm percent. CHECK(snapDeckParamNorm(DeckParam::kFilterKeyTrack, 0.4162) == 0.4150); // Percent, +/-100 %: likewise, measured on the bipolar value. CHECK(snapDeckParamNorm(DeckParam::kFilterVel, deckNormFromBipolar(-0.4162)) == deckNormFromBipolar(-0.42)); // Exponent: whole numbers, which puts the linear neutral one snap from centre. Compared as // the norm the snap RETURNS — the exponent's own log travel is not an exact round trip. CHECK(snapDeckParamNorm(DeckParam::kAttackCurve, util::knobNormFromCurve(2.6)) == util::knobNormFromCurve(3.0)); CHECK(snapDeckParamNorm(DeckParam::kAttackCurve, util::knobNormFromCurve(1.4)) == util::knobNormFromCurve(util::kCurveNeutral)); // Decibels, likewise compared as the returned norm. CHECK(snapDeckParamNorm(DeckParam::kMasterGain, instrument::engine::masterGainNormFromDb(-6.4)) == instrument::engine::masterGainNormFromDb(-6.0)); // Already-integer and discrete controls are untouched. CHECK(snapDeckParamNorm(DeckParam::kVoiceCount, 0.4162) == 0.4162); CHECK(snapDeckParamNorm(DeckParam::kPlayMode, 0.4162) == 0.4162); CHECK(deckParamUnit(DeckParam::kVoiceCount) == UnitCategory::None); CHECK(deckParamUnit(DeckParam::kAmpVelCurve) == UnitCategory::None); } // The taper and the raised ceiling are persistence-neutral BY CONSTRUCTION: the binding only // READS the stored seconds, so a value dialled under the old 2 s ceiling reloads bit-identical // and simply sits somewhere else on the knob. Nothing on the load path rewrites it. static void testAValueStoredUnderTheOldCeilingIsReadNotRewritten() { PlaySeconds p; p.adsr.decaySeconds = 1.75; // reachable by hand at the retired 2 s ceiling p.adsr.releaseSeconds = 2.0; const double normDecay = deckParamNorm(DeckParam::kDecay, p); CHECK(p.adsr.decaySeconds == 1.75); // reading the norm mutated nothing CHECK(p.adsr.releaseSeconds == 2.0); CHECK(normDecay > 0.0 && normDecay < 1.0); // still on the knob, just at a new angle CHECK(deckParamNorm(DeckParam::kRelease, p) > normDecay); // And a no-op touch survives the norm the knob would hand back — for THIS value, which is // exactly on the taper's output quantum grid (1.75 s parses to a grid-aligned double). A // legacy value off the grid (e.g. 1.2345678912345) WOULD be re-quantized on first touch; // that is correct, intended behaviour, not a gap this test is claiming to cover. setDeckParam(DeckParam::kDecay, p, normDecay, 0); CHECK(p.adsr.decaySeconds == 1.75); } // The filter's four tone controls are wire-frozen in the payload: their stored value IS their // normalized position, and nothing in the taper pass may re-map it. Their snap is display-side // only, which is what this separates. static void testTheFilterFourKeepTheirIdentityTaper() { PlaySeconds p; const double positions[] = {0.0, 0.125, 0.5, 0.73, 1.0}; for (double n : positions) { setDeckParam(DeckParam::kFilterCutoff, p, n, 0); setDeckParam(DeckParam::kFilterQ, p, n, 0); setDeckParam(DeckParam::kFilterMorph, p, n, 0); setDeckParam(DeckParam::kFilterDrive, p, n, 0); CHECK(p.filter.settings.cutoffNorm == static_cast(n)); CHECK(p.filter.settings.resonanceNorm == static_cast(n)); CHECK(p.filter.settings.morphNorm == static_cast(n)); CHECK(p.filter.settings.driveNorm == static_cast(n)); CHECK(deckParamNorm(DeckParam::kFilterCutoff, p) == static_cast(static_cast(n))); } } // The single-button commit seam. A one-button toggle carries no segment, so the commit derives // the NEXT state from the parameter set and hands it to setDeckParam's unchanged segment // contract. Driven end-to-end — derive, apply, re-derive — because the property that matters is // that repeated clicks alternate the stored field rather than latching it. static void testASingleButtonsDerivedSegmentFlipsTheFieldItNames() { PlaySeconds p; // Enables: off by default, so the first derived segment must be ON. CHECK(!p.pitchEnv.enabled); CHECK(nextToggleSegment(DeckParam::kPitchEnvEnable, p) == 1); setDeckParam(DeckParam::kPitchEnvEnable, p, 0.0, nextToggleSegment(DeckParam::kPitchEnvEnable, p)); CHECK(p.pitchEnv.enabled); CHECK(nextToggleSegment(DeckParam::kPitchEnvEnable, p) == 0); setDeckParam(DeckParam::kPitchEnvEnable, p, 0.0, nextToggleSegment(DeckParam::kPitchEnvEnable, p)); CHECK(!p.pitchEnv.enabled); CHECK(!p.filter.enabled); CHECK(nextToggleSegment(DeckParam::kFilterEnable, p) == 1); setDeckParam(DeckParam::kFilterEnable, p, 0.0, nextToggleSegment(DeckParam::kFilterEnable, p)); CHECK(p.filter.enabled); // Mode selectors: Staged by default, so the first derived segment is Spline. Flipping the // amp to Spline also forces Trigger (the drawn-EG rule), which is setDeckParam's own job // and must survive the derived segment reaching it unchanged. CHECK(p.ampSpline.mode == EnvMode::Staged); CHECK(nextToggleSegment(DeckParam::kAmpEnvMode, p) == 1); setDeckParam(DeckParam::kAmpEnvMode, p, 0.0, nextToggleSegment(DeckParam::kAmpEnvMode, p)); CHECK(p.ampSpline.mode == EnvMode::Spline); CHECK(p.playMode == PlayMode::Trigger); CHECK(nextToggleSegment(DeckParam::kAmpEnvMode, p) == 0); setDeckParam(DeckParam::kAmpEnvMode, p, 0.0, nextToggleSegment(DeckParam::kAmpEnvMode, p)); CHECK(p.ampSpline.mode == EnvMode::Staged); for (DeckParam id : {DeckParam::kPitchEnvMode, DeckParam::kFilterEnvMode}) { setDeckParam(id, p, 0.0, nextToggleSegment(id, p)); } CHECK(p.pitchSpline.mode == EnvMode::Spline); CHECK(p.filterSpline.mode == EnvMode::Spline); // Every control that still carries its own segment answers "not mine", so the shell can // tell the two commit paths apart on the answer alone. for (DeckParam id : {DeckParam::kPlayMode, DeckParam::kPitchEngine, DeckParam::kFilterLaw, DeckParam::kVoiceMode, DeckParam::kMonoTrigger, DeckParam::kFilterCutoff, DeckParam::kCount}) { CHECK(nextToggleSegment(id, p) == -1); } } // The cross-check the hand-maintained list above cannot catch: a control RE-STYLED to a // single button (kEnable/kMode) with no nextToggleSegment entry silently commits segment -1, // which setDeckParam reads as "off" — a latch, not a toggle. Swept over every group // sampleDeckGroups actually ships, in both play modes, rather than a fixed id list, so a // future re-style is caught the moment it lands here with no entry above. kLimiterEnable is // the one shipped kEnable that is excluded: it lives on `InstrumentParams::limiterEnabled`, // outside `PlaySeconds`, and commits through its own handler (editor_input_deck.cpp) rather // than through nextToggleSegment/setDeckParam at all. static void testEveryShippedSingleButtonToggleHasADerivedSegment() { const PlaySeconds p; for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) { for (const DeckGroupDesc& g : sampleDeckGroups(mode)) { for (const DeckToggleDesc* t : {&g.captionToggle, &g.captionToggle2, &g.rowToggle}) { if (t->id < 0) continue; if (t->style != DeckToggleStyle::kEnable && t->style != DeckToggleStyle::kMode) continue; const DeckParam id = static_cast(t->id); if (id == DeckParam::kLimiterEnable) continue; CHECK(nextToggleSegment(id, p) != -1); } } } } int main() { testTheTwoCeilingNamesAreOneNumber(); testASingleButtonsDerivedSegmentFlipsTheFieldItNames(); testEveryShippedSingleButtonToggleHasADerivedSegment(); testNormRoundTripsThroughEveryValueDomain(); testRateKnobEndsAreTheStretchersOwnBounds(); testRateAndPitchBindTheirOwnFields(); testShiftSnapsBothNewKnobsToWholeSemitones(); testResetTouchesOnlyItsOwnRingOnADualRingKnob(); testInnerResetLandsOnTheExactLinearNeutral(); testResetLandsOnTheStoredDefaultOfEachControl(); testEveryKnobIdResetsToItsDefault(); testEveryDefaultHasAnExactNormalizedPreimage(); testShiftSnapsToAWholeUnitOfTheDisplayedValue(); testAValueStoredUnderTheOldCeilingIsReadNotRewritten(); testTheFilterFourKeepTheirIdentityTaper(); if (g_fail) { std::printf("%d FAILURE(S)\n", g_fail); return 1; } std::printf("deck_values tests passed\n"); return 0; }