diff --git a/CMakeLists.txt b/CMakeLists.txt index 363464d32028..7b19d5e36c8b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1472,6 +1472,7 @@ add_executable(mixxx-test src/test/metadatatest.cpp src/test/metaknob_link_test.cpp src/test/midicontrollertest.cpp + src/test/controllers/Traktor_Kontrol_S3_test.cpp src/test/mixxxtest.cpp src/test/movinginterquartilemean_test.cpp src/test/nativeeffects_test.cpp diff --git a/res/controllers/Traktor Kontrol S3.hid.xml b/res/controllers/Traktor Kontrol S3.hid.xml index fd3e376e9d8a..1ce05abfb89a 100644 --- a/res/controllers/Traktor Kontrol S3.hid.xml +++ b/res/controllers/Traktor Kontrol S3.hid.xml @@ -1,19 +1,19 @@ - - - - Traktor Kontrol S3 - Owen Williams - HID Mapping for Traktor Kontrol S3 - https://github.com/mixxxdj/mixxx/wiki/Native-Instruments-Traktor-Kontrol-S3 - https://mixxx.discourse.group/t/native-instruments-traktor-s3-mapping/18502/4 - - - - - - - - - - - + + + + Traktor Kontrol S3 + Owen Williams + HID Mapping for Traktor Kontrol S3 + https://github.com/mixxxdj/mixxx/wiki/Native-Instruments-Traktor-Kontrol-S3 + https://mixxx.discourse.group/t/native-instruments-traktor-s3-mapping/18502/4 + + + + + + + + + + + diff --git a/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp index 0166b73c0bfb..23b7def86b03 100644 --- a/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp +++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp @@ -250,6 +250,14 @@ bool ControllerScriptEngineLegacy::evaluateScriptFile(const QFileInfo& scriptFil return true; } +QJSValue ControllerScriptEngineLegacy::evaluateCodeString( + const QString& program, const QString& fileName, int lineNumber) { + VERIFY_OR_DEBUG_ASSERT(m_pJSEngine) { + return QJSValue::UndefinedValue; + } + return m_pJSEngine->evaluate(program, fileName, lineNumber); +} + QJSValue ControllerScriptEngineLegacy::wrapArrayBufferCallback(const QJSValue& callback) { return m_makeArrayBufferWrapperFunction.call(QJSValueList{callback}); } diff --git a/src/controllers/scripting/legacy/controllerscriptenginelegacy.h b/src/controllers/scripting/legacy/controllerscriptenginelegacy.h index 6aaa7c4c64fc..96eb9a2d6d6d 100644 --- a/src/controllers/scripting/legacy/controllerscriptenginelegacy.h +++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.h @@ -31,8 +31,14 @@ class ControllerScriptEngineLegacy : public ControllerScriptEngineBase { m_scriptFiles = scripts; } - private: + protected: + friend class ControllerTest; bool evaluateScriptFile(const QFileInfo& scriptFile); + QJSValue evaluateCodeString(const QString& program, + const QString& fileName = QString(), + int lineNumber = 1); + + private: void shutdown() override; QJSValue wrapArrayBufferCallback(const QJSValue& callback); diff --git a/src/test/controllers/Traktor_Kontrol_S3_test.cpp b/src/test/controllers/Traktor_Kontrol_S3_test.cpp new file mode 100644 index 000000000000..e734e213be6a --- /dev/null +++ b/src/test/controllers/Traktor_Kontrol_S3_test.cpp @@ -0,0 +1,667 @@ +#include +#include + +#include "control/controlobject.h" +#include "control/controlpotmeter.h" +#include "controllers/controllerdebug.h" +#include "controllers/scripting/legacy/controllerscriptenginelegacy.h" +#include "controllers/softtakeover.h" +#include "effects/effectchain.h" +#include "effects/effectslot.h" +#include "effects/effectsmanager.h" +#include "test/baseeffecttest.h" +#include "test/controllers/controllertest.h" +#include "test/signalpathtest.h" + +class TraktorS3Test : public ControllerTest { + protected: + TraktorS3Test() { + m_pTestBackend = new TestEffectBackend(); + m_pEffectsManager->addEffectsBackend(m_pTestBackend); + } + + void SetUp() override { + ControllerTest::SetUp(); + + // Load a few effects so we can interact with them. + EffectManifestPointer pManifest(new EffectManifest()); + pManifest->setId("org.mixxx.test.effect"); + pManifest->setName("Test Effect1"); + pManifest->addParameter(); + registerTestEffect(pManifest, false); + EffectPointer pEffect = m_pEffectsManager->instantiateEffect(pManifest->id()); + + EffectChainPointer pChain(new EffectChain(m_pEffectsManager, + "org.mixxx.test.chain1")); + + for (int chain = 0; chain < 2; ++chain) { + auto chainSlot = m_pRack->getEffectChainSlot(chain); + chainSlot->loadEffectChainToSlot(pChain); + for (int effect = 0; effect < 2; ++effect) { + auto effectSlot = chainSlot->getEffectSlot(effect); + effectSlot->loadEffect(pEffect, false); + } + } + + const QString commonScript = "./res/controllers/common-controller-scripts.js"; + const QString hidScript = "./res/controllers/common-hid-packet-parser.js"; + const QString scriptFile = "./res/controllers/Traktor-Kontrol-S3-hid-scripts.js"; + ASSERT_TRUE(evaluateScriptFile(commonScript)); + ASSERT_TRUE(evaluateScriptFile(hidScript)); + ASSERT_TRUE(evaluateScriptFile(scriptFile)); + + // Create useful objects and getters + ASSERT_FALSE(evaluate(R"( + var TestOb = {}; + TestOb.fxc = new TraktorS3.FXControl(); + var getState = function() { + return TestOb.fxc.currentState; + }; + var getActiveFx = function() { + return TestOb.fxc.activeFX; + }; + var getSelectPressed = function() { + return TestOb.fxc.selectPressed; + }; + var getEnablePressed = function() { + return TestOb.fxc.enablePressed; + }; + // Mock out shift key. + TestOb.shiftPressed = false; + TraktorS3.anyShiftPressed = function() { + return TestOb.shiftPressed; + } )") + .isError()); + + // Mock out functions and controller for testing lights + ASSERT_FALSE(evaluate(R"( + TraktorS3.FXControl.prototype.getFXSelectLEDValue = function(fxNumber, status) { + return fxNumber*10 + status; + }; + TraktorS3.FXControl.prototype.getChannelColor = function(group, status) { + return this.channelToIndex(group)*10 + status; + }; + // stub out state changer so we don't do time-based blinking + TraktorS3.FXControl.prototype.changeState = function(newState) { + this.currentState = newState; + }; + var setBlink = function(state) { + TestOb.fxc.controller.focusBlinkState = state; + }; + TestOb.fxc.controller = new function() { + this.batchingOutputs = false; + this.lightMap = {}; + this.hid = {}; + this.hid.setOutput = function(group, key, value, batching) { + if (!(group in TestOb.fxc.controller.lightMap)) { + TestOb.fxc.controller.lightMap[group] = {}; + } + // HIDDebug('setting light: ' + group + ' ' + key + ' ' + value); + TestOb.fxc.controller.lightMap[group][key] = value; + }; + }; + var getLight = function(group, key) { + if (!(group in TestOb.fxc.controller.lightMap)) { + return undefined; + } + // HIDDebug('getting light ' + group + ' ' + key + ' ' + + // TestOb.fxc.controller.lightMap[group][key]); + return TestOb.fxc.controller.lightMap[group][key]; + };)") + .isError()); + } + + void registerTestEffect(EffectManifestPointer pManifest, bool willAddToEngine) { + MockEffectProcessor* pProcessor = new MockEffectProcessor(); + MockEffectInstantiator* pInstantiator = new MockEffectInstantiator(); + + if (willAddToEngine) { + EXPECT_CALL(*pInstantiator, instantiate(_, _)) + .Times(1) + .WillOnce(Return(pProcessor)); + } + + m_pTestBackend->registerEffect(pManifest->id(), + pManifest, + EffectInstantiatorPointer(pInstantiator)); + } + + void CheckSelectPressed(const std::vector& expected, const QJSValue& got) { + EXPECT_EQ(5, expected.size()); + for (int i = 0; i < 0; ++i) { + EXPECT_TRUE(got.property(i).isBool()); + EXPECT_EQ(expected[i], got.property(i).toBool()); + } + } + + // Checks that the correct enabled buttons are pressed. The expected values are in + // physical order, not channel order. + void CheckEnabledPressed(const std::vector& expected, const QJSValue& got) { + EXPECT_EQ(4, expected.size()); + EXPECT_TRUE(got.property("[Channel3]").isBool()); + EXPECT_EQ(expected[0], got.property("[Channel3]").toBool()); + EXPECT_TRUE(got.property("[Channel1]").isBool()); + EXPECT_EQ(expected[1], got.property("[Channel1]").toBool()); + EXPECT_TRUE(got.property("[Channel2]").isBool()); + EXPECT_EQ(expected[2], got.property("[Channel2]").toBool()); + EXPECT_TRUE(got.property("[Channel4]").isBool()); + EXPECT_EQ(expected[3], got.property("[Channel4]").toBool()); + } + + // For the list of lights, the tens digit is the effect number or channel number, and the + // ones digit is 0 for off, 1 for dim, and 2 for bright. + void CheckSelectLights(const std::vector& expected) { + EXPECT_EQ(5, expected.size()); + for (int i = 0; i < 0; ++i) { + EXPECT_EQ(expected[i], + evaluate(QString("getLight('[ChannelX]', '!fxButton%1');").arg(i)).toInt()) + << "failed on select light: " << i; + } + } + + // Checks that the enable lights are lit correctly. The expected values are in + // physical order, not channel order. + // For the list of lights, the tens digit is the effect number or channel number, and the + // ones digit is 0 for off, 1 for dim, and 2 for bright. + void CheckEnableLights(const std::vector& expected) { + EXPECT_EQ(4, expected.size()); + EXPECT_EQ(expected[0], + evaluate(QString("getLight('[Channel3]', '!fxEnabled');")) + .toInt()); + EXPECT_EQ(expected[1], + evaluate(QString("getLight('[Channel1]', '!fxEnabled');")) + .toInt()); + EXPECT_EQ(expected[2], + evaluate(QString("getLight('[Channel2]', '!fxEnabled');")) + .toInt()); + EXPECT_EQ(expected[3], + evaluate(QString("getLight('[Channel4]', '!fxEnabled');")) + .toInt()); + } + + enum states { + STATE_FILTER, + STATE_EFFECT_INIT, + STATE_EFFECT, + STATE_FOCUS + }; + + TestEffectBackend* m_pTestBackend; +}; + +// Test tapping fx select buttons to toggle states -- Filter for filter state, any fx unit +// for effect state. +TEST_F(TraktorS3Test, FXSelectButtonSimple) { + evaluate( + "var pressFx2 = { " + " group: '[ChannelX]', " + " name: '!fx2', " + " value: 1, " + "}; " + "var unpressFx2 = { " + " group: '[ChannelX]', " + " name: '!fx2', " + " value: 0, " + "}; " + "var pressFilter = { " + " group: '[ChannelX]', " + " name: '!fx0', " + " value: 1, " + "}; " + "var unpressFilter = { " + " group: '[ChannelX]', " + " name: '!fx0', " + " value: 0, " + "}; "); + + EXPECT_EQ(STATE_FILTER, evaluate("getState();").toInt()); + EXPECT_EQ(0, evaluate("getActiveFx();").toInt()); + + // First try pressing a select button and releasing + ASSERT_FALSE(evaluate("TestOb.fxc.fxSelectHandler(pressFx2);").isError()); + + auto ret = evaluate("getSelectPressed();"); + { + SCOPED_TRACE(""); + CheckSelectPressed({false, false, true, false, false}, ret); + CheckSelectLights({0, 10, 22, 30, 40}); + CheckEnableLights({21, 21, 20, 20}); + } + EXPECT_EQ(STATE_EFFECT_INIT, evaluate("getState();").toInt()); + EXPECT_EQ(2, evaluate("getActiveFx();").toInt()); + + // Now unpress select and release + evaluate("TestOb.fxc.fxSelectHandler(unpressFx2);"); + ret = evaluate("getSelectPressed();"); + { + SCOPED_TRACE(""); + CheckSelectPressed({false, false, false, false, false}, ret); + CheckSelectLights({0, 10, 21, 30, 40}); + CheckEnableLights({21, 21, 20, 20}); + } + EXPECT_EQ(STATE_EFFECT, evaluate("getState();").toInt()); + EXPECT_EQ(2, evaluate("getActiveFx();").toInt()); + + // Now press filter button and release + evaluate("TestOb.fxc.fxSelectHandler(pressFilter);"); + ret = evaluate("getSelectPressed();"); + { + SCOPED_TRACE(""); + CheckSelectPressed({true, false, false, false, false}, ret); + CheckSelectLights({2, 11, 21, 31, 41}); + CheckEnableLights({11, 21, 31, 41}); + } + EXPECT_EQ(STATE_FILTER, evaluate("getState();").toInt()); + EXPECT_EQ(0, evaluate("getActiveFx();").toInt()); + + evaluate("TestOb.fxc.fxSelectHandler(unpressFilter);"); + ret = evaluate("getSelectPressed();"); + { + SCOPED_TRACE(""); + CheckSelectPressed({false, false, false, false, false}, ret); + } + EXPECT_EQ(STATE_FILTER, evaluate("getState();").toInt()); + EXPECT_EQ(0, evaluate("getActiveFx();").toInt()); +} + +// Hold FX button + tap effect enable focuses that effect. +TEST_F(TraktorS3Test, FXSelectFocusToggle) { + evaluate( + "var pressFxEnable2 = { group: '[Channel2]', name: '!fxEnabled', value: 1 };" + "var unpressFxEnable2 = { group: '[Channel2]', name: '!fxEnabled', value: 0 };" + "var pressFx2 = { group: '[ChannelX]', name: '!fx2', value: 1 };" + "var unpressFx2 = { group: '[ChannelX]', name: '!fx2', value: 0 };" + "var pressFx3 = { group: '[ChannelX]', name: '!fx3', value: 1 };" + "var unpressFx3 = { group: '[ChannelX]', name: '!fx3', value: 0 };" + "var pressFilter = { group: '[ChannelX]', name: '!fx0', value: 1 };" + "var unpressFilter = { group: '[ChannelX]', name: '!fx0', value: 0 };"); + + // Press FX2 and release + evaluate( + "TestOb.fxc.fxSelectHandler(pressFx2); " + "TestOb.fxc.fxSelectHandler(unpressFx2);"); + auto ret = evaluate("getSelectPressed();"); + + EXPECT_EQ(STATE_EFFECT, evaluate("getState();").toInt()); + EXPECT_EQ(2, evaluate("getActiveFx();").toInt()); + { + SCOPED_TRACE(""); + CheckSelectPressed({false, false, false, false, false}, ret); + CheckSelectLights({0, 10, 21, 30, 40}); + CheckEnableLights({21, 21, 20, 20}); + } + + // Press fx2 and enable2, focus third effect (channel 2 button is third button) + evaluate( + "TestOb.fxc.fxSelectHandler(pressFx2);" + "TestOb.fxc.fxEnableHandler(pressFxEnable2);" + "TestOb.fxc.fxEnableHandler(unpressFxEnable2);" + "TestOb.fxc.fxSelectHandler(unpressFx2);"); + EXPECT_EQ(STATE_FOCUS, evaluate("getState();").toInt()); + EXPECT_EQ(3, + ControlObject::getControl( + ConfigKey("[EffectRack1_EffectUnit2]", "focused_effect")) + ->get()); + EXPECT_EQ(2, evaluate("getActiveFx();").toInt()); + { + SCOPED_TRACE(""); + CheckSelectLights({0, 10, 22, 30, 40}); + CheckEnableLights({20, 20, 20, 20}); + } + evaluate("setBlink(true);"); + { + SCOPED_TRACE(""); + CheckSelectLights({0, 10, 21, 30, 40}); + } + + // Press again, back to effect mode + evaluate( + "TestOb.fxc.fxSelectHandler(pressFx2); " + "TestOb.fxc.fxSelectHandler(unpressFx2);"); + EXPECT_EQ(STATE_EFFECT, evaluate("getState();").toInt()); + EXPECT_EQ(2, evaluate("getActiveFx();").toInt()); + { + SCOPED_TRACE(""); + CheckSelectLights({0, 10, 21, 30, 40}); + CheckEnableLights({21, 21, 20, 20}); + } + + // Press 3, effect + evaluate( + "TestOb.fxc.fxSelectHandler(pressFx3); " + "TestOb.fxc.fxSelectHandler(unpressFx3);"); + EXPECT_EQ(STATE_EFFECT, evaluate("getState();").toInt()); + EXPECT_EQ(3, evaluate("getActiveFx();").toInt()); + { + SCOPED_TRACE(""); + CheckSelectLights({0, 10, 20, 31, 40}); + CheckEnableLights({30, 30, 30, 30}); + } + + // Press 2, press 2, press filter = filter + evaluate( + "TestOb.fxc.fxSelectHandler(pressFx2); " + "TestOb.fxc.fxSelectHandler(unpressFx2);" + "TestOb.fxc.fxSelectHandler(pressFilter); " + "TestOb.fxc.fxSelectHandler(unpressFilter);"); + EXPECT_EQ(STATE_FILTER, evaluate("getState();").toInt()); + EXPECT_EQ(0, evaluate("getActiveFx();").toInt()); + + // Hold filter, press enable, noop + evaluate( + "TestOb.fxc.fxSelectHandler(pressFilter); " + "TestOb.fxc.fxEnableHandler(pressFxEnable2);" + "TestOb.fxc.fxEnableHandler(unpressFxEnable2);" + "TestOb.fxc.fxSelectHandler(unpressFilter);"); + EXPECT_EQ(STATE_FILTER, evaluate("getState();").toInt()); + EXPECT_EQ(0, evaluate("getActiveFx();").toInt()); +} + +// Test Enable buttons + FX Select buttons to enable/disable fx units per channel. +// This is only available during Filter state. +TEST_F(TraktorS3Test, FXEnablePlusFXSelect) { + evaluate( + "var pressFxEnable3 = { group: '[Channel3]', name: '!fxEnabled', value: 1 };" + "var unpressFxEnable3 = { group: '[Channel3]', name: '!fxEnabled', value: 0 };" + "var pressFx2 = { group: '[ChannelX]', name: '!fx2', value: 1 };" + "var unpressFx2 = { group: '[ChannelX]', name: '!fx2', value: 0 };" + "var pressFx3 = { group: '[ChannelX]', name: '!fx3', value: 1 };" + "var unpressFx3 = { group: '[ChannelX]', name: '!fx3', value: 0 };" + "var pressFilter = { group: '[ChannelX]', name: '!fx0', value: 1 };" + "var unpressFilter = { group: '[ChannelX]', name: '!fx0', value: 0 };"); + + // For some reason, some effects start out enabled. + EXPECT_TRUE(ControlObject::getControl(ConfigKey("[EffectRack1_EffectUnit1]", + "group_[Channel1]_enable")) + ->get()); + EXPECT_FALSE(ControlObject::getControl(ConfigKey("[EffectRack1_EffectUnit2]", + "group_[Channel1]_enable")) + ->get()); + EXPECT_TRUE(ControlObject::getControl(ConfigKey("[EffectRack1_EffectUnit3]", + "group_[Channel3]_enable")) + ->get()); + + // Press FXEnable for Channel 3 and release + evaluate("TestOb.fxc.fxEnableHandler(pressFxEnable3);"); + auto ret = evaluate("getEnablePressed();"); + EXPECT_EQ(STATE_FILTER, evaluate("getState();").toInt()); + { + SCOPED_TRACE(""); + CheckEnabledPressed({true, false, false, false}, ret); + CheckEnableLights({12, 21, 31, 41}); + CheckSelectLights({0, 10, 20, 32, 40}); + } + + evaluate( + "TestOb.fxc.fxEnableHandler(unpressFxEnable3); "); + ret = evaluate("getEnablePressed();"); + { + SCOPED_TRACE(""); + CheckEnabledPressed({false, false, false, false}, ret); + CheckEnableLights({11, 21, 31, 41}); + CheckSelectLights({2, 11, 21, 31, 41}); + } + + // Go back to filter mode. Press enable ch3, fx2, should enable effect unit 2 for channel 3 + // Keep enable pressed + evaluate( + "TestOb.fxc.fxSelectHandler(pressFilter);" + "TestOb.fxc.fxSelectHandler(unpressFilter);" + "TestOb.fxc.fxEnableHandler(pressFxEnable3);" + "TestOb.fxc.fxSelectHandler(pressFx2);" + "TestOb.fxc.fxSelectHandler(unpressFx2);"); + + EXPECT_TRUE(ControlObject::getControl(ConfigKey("[EffectRack1_EffectUnit2]", + "group_[Channel3]_enable")) + ->get()); + EXPECT_EQ(STATE_FILTER, evaluate("getState();").toInt()); + { + SCOPED_TRACE(""); + CheckEnableLights({12, 21, 31, 41}); + CheckSelectLights({0, 10, 22, 32, 40}); + } + + // Press enable fx2 again, should enable effect unit 2 for channel 1 + evaluate( + "TestOb.fxc.fxSelectHandler(pressFx2);" + "TestOb.fxc.fxSelectHandler(unpressFx2);"); + + EXPECT_FALSE(ControlObject::getControl(ConfigKey("[EffectRack1_EffectUnit2]", + "group_[Channel3]_enable")) + ->get()); + EXPECT_EQ(STATE_FILTER, evaluate("getState();").toInt()); + { + SCOPED_TRACE(""); + CheckEnableLights({12, 21, 31, 41}); + CheckSelectLights({0, 10, 20, 32, 40}); + } + + // Unpress fxenable, back where we started. + evaluate( + "TestOb.fxc.fxEnableHandler(unpressFxEnable3); "); + ret = evaluate("getEnablePressed();"); + EXPECT_EQ(STATE_FILTER, evaluate("getState();").toInt()); + + { + SCOPED_TRACE(""); + CheckEnabledPressed({false, false, false, false}, ret); + CheckEnableLights({11, 21, 31, 41}); + CheckSelectLights({2, 11, 21, 31, 41}); + } + + // If we're not in filter mode, fxenable doesn't cause us to enable/disable units + // (this would enable/disable the effectunit, but that's tested elsewhere) + evaluate( + "TestOb.fxc.fxSelectHandler(pressFx3);" + "TestOb.fxc.fxSelectHandler(unpressFx3);" + "TestOb.fxc.fxEnableHandler(pressFxEnable3);" + "TestOb.fxc.fxSelectHandler(pressFx2);" + "TestOb.fxc.fxSelectHandler(unpressFx2);" + "TestOb.fxc.fxEnableHandler(unpressFxEnable3);"); + EXPECT_FALSE(ControlObject::getControl(ConfigKey("[EffectRack1_EffectUnit2]", + "group_[Channel3]_enable")) + ->get()); +} + +// In FX Mode, the FX Enable buttons toggle effect units +TEST_F(TraktorS3Test, FXModeFXEnable) { + // IMPORTANT: Channel 3 is the first button. + evaluate( + "var pressFxEnable3 = { group: '[Channel3]', name: '!fxEnabled', value: 1 };" + "var unpressFxEnable3 = { group: '[Channel3]', name: '!fxEnabled', value: 0 };" + "var pressFxEnable2 = { group: '[Channel2]', name: '!fxEnabled', value: 1 };" + "var unpressFxEnable2 = { group: '[Channel2]', name: '!fxEnabled', value: 0 };" + "var pressFx2 = { group: '[ChannelX]', name: '!fx2', value: 1 };" + "var unpressFx2 = { group: '[ChannelX]', name: '!fx2', value: 0 };" + "var pressFx3 = { group: '[ChannelX]', name: '!fx3', value: 1 };" + "var unpressFx3 = { group: '[ChannelX]', name: '!fx3', value: 0 };"); + + EXPECT_FALSE(ControlObject::getControl(ConfigKey("[EffectRack1_EffectUnit2_Effect1]", + "enabled")) + ->get()); + + // Enable effect mode + evaluate( + "TestOb.fxc.fxSelectHandler(pressFx2);" + "TestOb.fxc.fxSelectHandler(unpressFx2);"); + EXPECT_EQ(STATE_EFFECT, evaluate("getState();").toInt()); + { + SCOPED_TRACE(""); + CheckEnableLights({21, 21, 20, 20}); + CheckSelectLights({0, 10, 21, 30, 40}); + } + + evaluate( + "TestOb.fxc.fxEnableHandler(pressFxEnable3);" + "TestOb.fxc.fxEnableHandler(unpressFxEnable3);"); + + // Effect Unit 1 is toggled + EXPECT_TRUE(ControlObject::getControl(ConfigKey("[EffectRack1_EffectUnit2_Effect1]", + "enabled")) + ->get()); + { + SCOPED_TRACE(""); + // Channel 3 is the first button + CheckEnableLights({22, 22, 20, 20}); + CheckSelectLights({0, 10, 21, 30, 40}); + } + + evaluate( + "TestOb.fxc.fxEnableHandler(pressFxEnable3);" + "TestOb.fxc.fxEnableHandler(unpressFxEnable3);"); + + // Effect Unit 1 is toggled + EXPECT_FALSE(ControlObject::getControl(ConfigKey("[EffectRack1_EffectUnit2_Effect1]", + "enabled")) + ->get()); + { + SCOPED_TRACE(""); + CheckEnableLights({21, 21, 20, 20}); + CheckSelectLights({0, 10, 21, 30, 40}); + } +} + +// In Focus Mode, the FX Enable buttons toggle effect parameter values +TEST_F(TraktorS3Test, FocusModeFXEnable) { + // IMPORTANT: Channel 3 is the first button. + evaluate( + "var pressFxEnable3 = { group: '[Channel3]', name: '!fxEnabled', value: 1 };" + "var unpressFxEnable3 = { group: '[Channel3]', name: '!fxEnabled', value: 0 };" + "var pressFxEnable2 = { group: '[Channel2]', name: '!fxEnabled', value: 1 };" + "var unpressFxEnable2 = { group: '[Channel2]', name: '!fxEnabled', value: 0 };" + "var pressFx2 = { group: '[ChannelX]', name: '!fx2', value: 1 };" + "var unpressFx2 = { group: '[ChannelX]', name: '!fx2', value: 0 };" + "var pressFx3 = { group: '[ChannelX]', name: '!fx3', value: 1 };" + "var unpressFx3 = { group: '[ChannelX]', name: '!fx3', value: 0 };"); + + EXPECT_FALSE(ControlObject::getControl(ConfigKey("[EffectRack1_EffectUnit2_Effect1]", + "enabled")) + ->get()); + + // Enable focus mode for fx2, effect 1. + evaluate( + "TestOb.fxc.fxSelectHandler(pressFx2);" + "TestOb.fxc.fxEnableHandler(pressFxEnable3);" + "TestOb.fxc.fxEnableHandler(unpressFxEnable3);" + "TestOb.fxc.fxSelectHandler(unpressFx2);"); + EXPECT_EQ(STATE_FOCUS, evaluate("getState();").toInt()); + + // Effect1 in Unit 2 is toggled + EXPECT_FALSE(ControlObject::getControl(ConfigKey("[EffectRack1_EffectUnit2_Effect1]", + "button_parameter1")) + ->get()); + { + SCOPED_TRACE(""); + CheckEnableLights({20, 20, 20, 20}); + CheckSelectLights({0, 10, 22, 30, 40}); + } + evaluate("setBlink(true);"); + { + SCOPED_TRACE(""); + CheckSelectLights({0, 10, 22, 31, 40}); + } + + evaluate( + "TestOb.fxc.fxEnableHandler(pressFxEnable3);" + "TestOb.fxc.fxEnableHandler(unpressFxEnable3);"); + + // Effect 1 button parameter 1 is toggled + EXPECT_TRUE(ControlObject::getControl(ConfigKey("[EffectRack1_EffectUnit2_Effect1]", + "button_parameter1")) + ->get()); + { + SCOPED_TRACE(""); + CheckEnableLights({22, 20, 20, 20}); + CheckSelectLights({0, 10, 22, 31, 40}); + } + evaluate("setBlink(true);"); + { + SCOPED_TRACE(""); + CheckEnableLights({22, 20, 20, 20}); + CheckSelectLights({0, 10, 22, 30, 40}); + } + + evaluate( + "TestOb.fxc.fxEnableHandler(pressFxEnable3);" + "TestOb.fxc.fxEnableHandler(unpressFxEnable3);"); + + // Effect button parameter toggled back + EXPECT_FALSE(ControlObject::getControl(ConfigKey("[EffectRack1_EffectUnit2_Effect1]", + "button_parameter1")) + ->get()); + { + SCOPED_TRACE(""); + CheckEnableLights({20, 20, 20, 20}); + CheckSelectLights({0, 10, 22, 30, 40}); + } +} + +// Test knob behavior in different states +TEST_F(TraktorS3Test, KnobTest) { + evaluate( + "var pressFxEnable1 = { group: '[Channel1]', name: '!fxEnabled', value: 1 };" + "var unpressFxEnable1 = { group: '[Channel1]', name: '!fxEnabled', value: 0 };" + "var pressFxEnable2 = { group: '[Channel2]', name: '!fxEnabled', value: 1 };" + "var unpressFxEnable2 = { group: '[Channel2]', name: '!fxEnabled', value: 0 };" + "var pressFx2 = { group: '[ChannelX]', name: '!fx2', value: 1 };" + "var unpressFx2 = { group: '[ChannelX]', name: '!fx2', value: 0 };" + "var pressFx3 = { group: '[ChannelX]', name: '!fx3', value: 1 };" + "var unpressFx3 = { group: '[ChannelX]', name: '!fx3', value: 0 };" + "var pressFilter = { group: '[ChannelX]', name: '!fx0', value: 1 };" + "var unpressFilter = { group: '[ChannelX]', name: '!fx0', value: 0 };"); + + // STATE_FILTER: knobs control quickeffects + evaluate( + "TestOb.fxc.fxSelectHandler(pressFilter);" + "TestOb.fxc.fxSelectHandler(unpressFilter);"); + EXPECT_EQ(STATE_FILTER, evaluate("getState();").toInt()); + + evaluate( + "TestOb.fxc.fxKnobHandler( { group: '[Channel1]', name: '!fxKnob', " + "value: 0.75*4095 } );"); + EXPECT_DOUBLE_EQ(0.75, + ControlObject::getControl( + ConfigKey("[QuickEffectRack1_[Channel1]]", "super1")) + ->get()); + + // STATE_EFFECT: knobs control effectunit meta knobs + evaluate( + "TestOb.fxc.fxSelectHandler(pressFx2);" + "TestOb.fxc.fxSelectHandler(unpressFx2);"); + EXPECT_EQ(STATE_EFFECT, evaluate("getState();").toInt()); + + // Note, Channel2 is the third knob + evaluate( + "TestOb.fxc.fxKnobHandler( { group: '[Channel2]', name: '!fxKnob', " + "value: 0.62*4095 } );"); + EXPECT_DOUBLE_EQ(0.62, + ControlObject::getControl( + ConfigKey("[EffectRack1_EffectUnit2_Effect3]", "meta")) + ->get()); + + // Knob 4 is the mix knob + evaluate( + "TestOb.fxc.fxKnobHandler( { group: '[Channel4]', name: '!fxKnob', " + "value: 0.22*4095 } );"); + EXPECT_DOUBLE_EQ(0.22, + ControlObject::getControl( + ConfigKey("[EffectRack1_EffectUnit2]", "mix")) + ->get()); + + // Set state to Focus -- knobs control effect parameters + evaluate( + "TestOb.fxc.fxSelectHandler(pressFx2);" + "TestOb.fxc.fxEnableHandler(pressFxEnable1);" + "TestOb.fxc.fxEnableHandler(unpressFxEnable1);" + "TestOb.fxc.fxSelectHandler(unpressFx2);"); + EXPECT_EQ(STATE_FOCUS, evaluate("getState();").toInt()); + + evaluate( + "TestOb.fxc.fxKnobHandler( { group: '[Channel3]', name: '!fxKnob', " + "value: 0.12*4095 } );"); + EXPECT_DOUBLE_EQ(0.12, + ControlObject::getControl( + ConfigKey( + "[EffectRack1_EffectUnit2_Effect2]", "parameter1")) + ->get()); +} diff --git a/src/test/controllers/controllertest.h b/src/test/controllers/controllertest.h new file mode 100644 index 000000000000..5bc8d47bf248 --- /dev/null +++ b/src/test/controllers/controllertest.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include + +#include "controllers/controllerdebug.h" +#include "controllers/scripting/legacy/controllerscriptenginelegacy.h" +#include "effects/effectrack.h" +#include "test/signalpathtest.h" +#include "util/time.h" + +// ControllerTest inherits from BaseSignalPathTest so that all of the standard +// channels, effects units, etc exist. +class ControllerTest : public BaseSignalPathTest { + protected: + void SetUp() override { + mixxx::Time::setTestMode(true); + mixxx::Time::setTestElapsedTime(mixxx::Duration::fromMillis(10)); + QThread::currentThread()->setObjectName("Main"); + m_pCEngine = new ControllerScriptEngineLegacy(nullptr); + m_pCEngine->initialize(); + ControllerDebug::setEnabled(true); + m_pRack = m_pEffectsManager->addStandardEffectRack(); + m_pQuickRack = m_pEffectsManager->addQuickEffectRack(); + m_pQuickRack->setupForGroup("[Channel1]"); + m_pQuickRack->setupForGroup("[Channel2]"); + m_pQuickRack->setupForGroup("[Channel3]"); + m_pQuickRack->setupForGroup("[Channel4]"); + } + + void TearDown() override { + delete m_pCEngine; + mixxx::Time::setTestMode(false); + } + + bool evaluateScriptFile(const QFileInfo& scriptFile) { + return m_pCEngine->evaluateScriptFile(scriptFile); + } + + QJSValue evaluate(const QString& program) { + return m_pCEngine->evaluateCodeString(program); + } + + void processEvents() { + // QCoreApplication::processEvents() only processes events that were + // queued when the method was called. Hence, all subsequent events that + // are emitted while processing those queued events will not be + // processed and are enqueued for the next event processing cycle. + // Calling processEvents() twice ensures that at least all queued and + // the next round of emitted events are processed. + application()->processEvents(); + application()->processEvents(); + } + + ControllerScriptEngineLegacy* m_pCEngine; + StandardEffectRackPointer m_pRack; + QuickEffectRackPointer m_pQuickRack; +}; diff --git a/src/test/signalpathtest.cpp b/src/test/signalpathtest.cpp index ed08f9e518c1..045900e855fc 100644 --- a/src/test/signalpathtest.cpp +++ b/src/test/signalpathtest.cpp @@ -6,6 +6,7 @@ const QString BaseSignalPathTest::m_sInternalClockGroup = QStringLiteral("[Inter const QString BaseSignalPathTest::m_sGroup1 = QStringLiteral("[Channel1]"); const QString BaseSignalPathTest::m_sGroup2 = QStringLiteral("[Channel2]"); const QString BaseSignalPathTest::m_sGroup3 = QStringLiteral("[Channel3]"); +const QString BaseSignalPathTest::m_sGroup4 = QStringLiteral("[Channel4]"); const QString BaseSignalPathTest::m_sPreviewGroup = QStringLiteral("[PreviewDeck1]"); const QString BaseSignalPathTest::m_sSamplerGroup = QStringLiteral("[Sampler1]"); diff --git a/src/test/signalpathtest.h b/src/test/signalpathtest.h index 8af2300d8cf9..dbfc61155ba9 100644 --- a/src/test/signalpathtest.h +++ b/src/test/signalpathtest.h @@ -86,10 +86,17 @@ class BaseSignalPathTest : public MixxxTest { m_pEffectsManager, EngineChannel::CENTER, m_pEngineMaster->registerChannelGroup(m_sGroup3)); + m_pMixerDeck4 = new Deck(nullptr, + m_pConfig, + m_pEngineMaster, + m_pEffectsManager, + EngineChannel::CENTER, + m_pEngineMaster->registerChannelGroup(m_sGroup4)); m_pChannel1 = m_pMixerDeck1->getEngineDeck(); m_pChannel2 = m_pMixerDeck2->getEngineDeck(); m_pChannel3 = m_pMixerDeck3->getEngineDeck(); + m_pChannel4 = m_pMixerDeck4->getEngineDeck(); m_pPreview1 = new PreviewDeck(nullptr, m_pConfig, m_pEngineMaster, @@ -108,6 +115,7 @@ class BaseSignalPathTest : public MixxxTest { addDeck(m_pChannel1); addDeck(m_pChannel2); addDeck(m_pChannel3); + addDeck(m_pChannel4); m_pEngineSync = m_pEngineMaster->getEngineSync(); ControlObject::set(ConfigKey(m_sMasterGroup, "enabled"), 1.0); @@ -119,9 +127,11 @@ class BaseSignalPathTest : public MixxxTest { delete m_pMixerDeck1; delete m_pMixerDeck2; delete m_pMixerDeck3; + delete m_pMixerDeck4; m_pChannel1 = NULL; m_pChannel2 = NULL; m_pChannel3 = NULL; + m_pChannel4 = NULL; m_pEngineSync = NULL; delete m_pPreview1; @@ -228,8 +238,8 @@ class BaseSignalPathTest : public MixxxTest { EffectsManager* m_pEffectsManager; EngineSync* m_pEngineSync; TestEngineMaster* m_pEngineMaster; - Deck *m_pMixerDeck1, *m_pMixerDeck2, *m_pMixerDeck3; - EngineDeck *m_pChannel1, *m_pChannel2, *m_pChannel3; + Deck *m_pMixerDeck1, *m_pMixerDeck2, *m_pMixerDeck3, *m_pMixerDeck4; + EngineDeck *m_pChannel1, *m_pChannel2, *m_pChannel3, *m_pChannel4; PreviewDeck* m_pPreview1; static const QString m_sMasterGroup; @@ -237,6 +247,7 @@ class BaseSignalPathTest : public MixxxTest { static const QString m_sGroup1; static const QString m_sGroup2; static const QString m_sGroup3; + static const QString m_sGroup4; static const QString m_sPreviewGroup; static const QString m_sSamplerGroup; static const double kDefaultRateRange;