diff --git a/CMakeLists.txt b/CMakeLists.txt index c95899660ef..367e28beaea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -414,12 +414,14 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/controllers/dlgprefcontrollerdlg.ui src/controllers/dlgprefcontrollers.cpp src/controllers/dlgprefcontrollersdlg.ui - src/controllers/engine/controllerengine.cpp - src/controllers/engine/controllerenginejsproxy.cpp - src/controllers/engine/colormapper.cpp - src/controllers/engine/colormapperjsproxy.cpp - src/controllers/engine/scriptconnection.cpp - src/controllers/engine/scriptconnectionjsproxy.cpp + src/controllers/scripting/controllerscriptenginebase.cpp + src/controllers/scripting/controllerscriptmoduleengine.cpp + src/controllers/scripting/colormapper.cpp + src/controllers/scripting/colormapperjsproxy.cpp + src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp + src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp + src/controllers/scripting/legacy/scriptconnection.cpp + src/controllers/scripting/legacy/scriptconnectionjsproxy.cpp src/controllers/keyboard/keyboardeventfilter.cpp src/controllers/learningutils.cpp src/controllers/midi/midicontroller.cpp @@ -1388,7 +1390,7 @@ add_executable(mixxx-test src/test/compatibility_test.cpp src/test/configobject_test.cpp src/test/controller_preset_validation_test.cpp - src/test/controllerengine_test.cpp + src/test/controllerscriptenginelegacy_test.cpp src/test/controlobjecttest.cpp src/test/coverartcache_test.cpp src/test/coverartutils_test.cpp diff --git a/src/control/controlobjectscript.cpp b/src/control/controlobjectscript.cpp index db11155d580..3c6a6495732 100644 --- a/src/control/controlobjectscript.cpp +++ b/src/control/controlobjectscript.cpp @@ -2,10 +2,11 @@ #include +#include "controllers/controllerdebug.h" #include "moc_controlobjectscript.cpp" ControlObjectScript::ControlObjectScript(const ConfigKey& key, QObject* pParent) - : ControlProxy(key, pParent, ControlFlag::NoAssertIfMissing) { + : ControlProxy(key, pParent, ControllerDebug::controlFlags()) { } bool ControlObjectScript::addScriptConnection(const ScriptConnection& conn) { diff --git a/src/control/controlobjectscript.h b/src/control/controlobjectscript.h index 90dce136552..219a4b0a991 100644 --- a/src/control/controlobjectscript.h +++ b/src/control/controlobjectscript.h @@ -3,8 +3,7 @@ #include #include "control/controlproxy.h" -#include "controllers/controllerdebug.h" -#include "controllers/engine/scriptconnection.h" +#include "controllers/scripting/legacy/scriptconnection.h" // this is used for communicate with controller scripts class ControlObjectScript : public ControlProxy { diff --git a/src/controllers/bulk/bulkcontroller.cpp b/src/controllers/bulk/bulkcontroller.cpp index 5cb65ee93de..7b62d843d9e 100644 --- a/src/controllers/bulk/bulkcontroller.cpp +++ b/src/controllers/bulk/bulkcontroller.cpp @@ -47,8 +47,8 @@ void BulkReader::run() { if (result >= 0) { Trace process("BulkReader process packet"); //qDebug() << "Read" << result << "bytes, pointer:" << data; - QByteArray outData((char*)data, transferred); - emit incomingData(outData, mixxx::Time::elapsed()); + QByteArray byteArray(reinterpret_cast(data), transferred); + emit incomingData(byteArray, mixxx::Time::elapsed()); } } qDebug() << "Stopped Reader"; diff --git a/src/controllers/controller.cpp b/src/controllers/controller.cpp index 0f6a25defc4..6753260c95a 100644 --- a/src/controllers/controller.cpp +++ b/src/controllers/controller.cpp @@ -9,7 +9,7 @@ #include "util/screensaver.h" Controller::Controller() - : m_pEngine(nullptr), + : m_pScriptEngineLegacy(nullptr), m_bIsOutputDevice(false), m_bIsInputDevice(false), m_bIsOpen(false), @@ -29,31 +29,30 @@ ControllerJSProxy* Controller::jsProxy() { void Controller::startEngine() { controllerDebug(" Starting engine"); - if (m_pEngine != nullptr) { + if (m_pScriptEngineLegacy) { qWarning() << "Controller: Engine already exists! Restarting:"; stopEngine(); } - m_pEngine = new ControllerEngine(this); + m_pScriptEngineLegacy = new ControllerScriptEngineLegacy(this); } void Controller::stopEngine() { controllerDebug(" Shutting down engine"); - if (m_pEngine == nullptr) { + if (!m_pScriptEngineLegacy) { qWarning() << "Controller::stopEngine(): No engine exists!"; return; } - m_pEngine->gracefulShutdown(); - delete m_pEngine; - m_pEngine = nullptr; + delete m_pScriptEngineLegacy; + m_pScriptEngineLegacy = nullptr; } -bool Controller::applyPreset(bool initializeScripts) { +bool Controller::applyPreset() { qDebug() << "Applying controller preset..."; const ControllerPreset* pPreset = preset(); // Load the script code into the engine - if (m_pEngine == nullptr) { + if (!m_pScriptEngineLegacy) { qWarning() << "Controller::applyPreset(): No engine exists!"; return false; } @@ -64,19 +63,8 @@ bool Controller::applyPreset(bool initializeScripts) { return true; } - bool success = m_pEngine->loadScriptFiles(scriptFiles); - if (success && initializeScripts) { - m_pEngine->initializeScripts(scriptFiles); - } - - // QFileInfo does not have a isValid/isEmpty/isNull method to check if it - // actually contains a reference, so we check if the filePath is empty as a - // workaround. - // See https://stackoverflow.com/a/45652741/1455128 for details. - if (initializeScripts && !pPreset->moduleFileInfo().filePath().isEmpty()) { - m_pEngine->loadModule(pPreset->moduleFileInfo()); - } - return success; + m_pScriptEngineLegacy->setScriptFiles(scriptFiles); + return m_pScriptEngineLegacy->initialize(); } void Controller::startLearning() { @@ -113,7 +101,7 @@ void Controller::triggerActivity() } } void Controller::receive(const QByteArray& data, mixxx::Duration timestamp) { - if (m_pEngine == nullptr) { + if (!m_pScriptEngineLegacy) { //qWarning() << "Controller::receive called with no active engine!"; // Don't complain, since this will always show after closing a device as // queued signals flush out @@ -122,7 +110,7 @@ void Controller::receive(const QByteArray& data, mixxx::Duration timestamp) { triggerActivity(); int length = data.size(); - if (ControllerDebug::enabled()) { + if (ControllerDebug::isEnabled()) { // Formatted packet display QString message = QString("%1: t:%2, %3 bytes:\n") .arg(m_sDeviceName, @@ -145,14 +133,5 @@ void Controller::receive(const QByteArray& data, mixxx::Duration timestamp) { controllerDebug(message); } - foreach (QString function, m_pEngine->getScriptFunctionPrefixes()) { - if (function == "") { - continue; - } - function.append(".incomingData"); - QJSValue incomingDataFunction = m_pEngine->wrapFunctionCode(function, 2); - m_pEngine->executeFunction(incomingDataFunction, data); - } - - m_pEngine->handleInput(data, timestamp); + m_pScriptEngineLegacy->handleIncomingData(data); } diff --git a/src/controllers/controller.h b/src/controllers/controller.h index 67df67fad6a..095e49fb0c9 100644 --- a/src/controllers/controller.h +++ b/src/controllers/controller.h @@ -1,19 +1,18 @@ #pragma once #include +#include #include "controllers/controllerpreset.h" #include "controllers/controllerpresetfilehandler.h" #include "controllers/controllerpresetinfo.h" #include "controllers/controllerpresetvisitor.h" #include "controllers/controllervisitor.h" -#include "controllers/engine/controllerengine.h" +#include "controllers/scripting/legacy/controllerscriptenginelegacy.h" #include "util/duration.h" class ControllerJSProxy; -/// Base class representing a physical (or software) controller. -/// /// This is a base class representing a physical (or software) controller. It /// must be inherited by a class that implements it on some API. Note that the /// subclass' destructor should call close() at a minimum. @@ -85,13 +84,7 @@ class Controller : public QObject, ConstControllerPresetVisitor { // this if they have an alternate way of handling such data.) virtual void receive(const QByteArray& data, mixxx::Duration timestamp); - /// Apply the preset to the controller. - /// Initializes both controller engine and static output mappings. - /// - /// @param initializeScripts Can be set to false to skip script - /// initialization for unit tests. - /// @return Returns whether it was successful. - virtual bool applyPreset(bool initializeScripts = true); + virtual bool applyPreset(); // Puts the controller in and out of learning mode. void startLearning(); @@ -108,17 +101,17 @@ class Controller : public QObject, ConstControllerPresetVisitor { // To be called in sub-class' open() functions after opening the device but // before starting any input polling/processing. - void startEngine(); + virtual void startEngine(); // To be called in sub-class' close() functions after stopping any input // polling/processing but before closing the device. - void stopEngine(); + virtual void stopEngine(); // To be called when receiving events void triggerActivity(); - inline ControllerEngine* getEngine() const { - return m_pEngine; + inline ControllerScriptEngineLegacy* getScriptEngine() const { + return m_pScriptEngineLegacy; } inline void setDeviceName(const QString& deviceName) { m_sDeviceName = deviceName; @@ -155,7 +148,7 @@ class Controller : public QObject, ConstControllerPresetVisitor { // Returns a pointer to the currently loaded controller preset. For internal // use only. virtual ControllerPreset* preset() = 0; - ControllerEngine* m_pEngine; + ControllerScriptEngineLegacy* m_pScriptEngineLegacy; // Verbose and unique device name suitable for display. QString m_sDeviceName; diff --git a/src/controllers/controllerdebug.cpp b/src/controllers/controllerdebug.cpp index ac1c3f39ef4..ce71d4381cf 100644 --- a/src/controllers/controllerdebug.cpp +++ b/src/controllers/controllerdebug.cpp @@ -5,8 +5,9 @@ //static bool ControllerDebug::s_enabled = false; +bool ControllerDebug::s_testing = false; //static -bool ControllerDebug::enabled() { +bool ControllerDebug::isEnabled() { return s_enabled || CmdlineArgs::Instance().getMidiDebug(); } diff --git a/src/controllers/controllerdebug.h b/src/controllers/controllerdebug.h index d99eaf146dc..16fe5918c76 100644 --- a/src/controllers/controllerdebug.h +++ b/src/controllers/controllerdebug.h @@ -2,6 +2,8 @@ #include +#include "control/control.h" + // Specifies whether or not we should dump incoming data to the console at // runtime. This is useful for end-user debugging and script-writing. class ControllerDebug { @@ -11,22 +13,36 @@ class ControllerDebug { static constexpr const char kLogMessagePrefix[] = "CDBG"; static constexpr int kLogMessagePrefixLength = sizeof(kLogMessagePrefix) - 1; - static bool enabled(); + static bool isEnabled(); /// Override the command-line argument (for testing) - static void enable() { - s_enabled = true; + static void setEnabled(bool enabled) { + s_enabled = enabled; } - /// Override the command-line argument (for testing) - static void disable() { - s_enabled = false; + static void setTesting(bool isTesting) { + s_testing = isTesting; + } + + /// Return the appropriate flag for ControlProxies in mappings. + /// + /// Normally, accessing an invalid control from a mapping should *not* + /// throw a debug assertion because controller mappings are considered + /// user data. If we're testing or controller debugging is enabled, we *do* + /// want assertions to prevent overlooking bugs in controller mappings. + static ControlFlags controlFlags() { + if (s_enabled || s_testing) { + return ControlFlag::None; + } + + return ControlFlag::AllowMissingOrInvalid; } private: ControllerDebug() = delete; static bool s_enabled; + static bool s_testing; }; // Usage: controllerDebug("hello" << "world"); @@ -39,7 +55,7 @@ class ControllerDebug { // output for mixxx.log with .noquote(), because in qt5 QDebug() is quoted by default. #define controllerDebug(stream) \ { \ - if (ControllerDebug::enabled()) { \ + if (ControllerDebug::isEnabled()) { \ QDebug(QtDebugMsg).noquote() << ControllerDebug::kLogMessagePrefix << stream; \ } \ } diff --git a/src/controllers/controllerpreset.h b/src/controllers/controllerpreset.h index 2eaa98b4f9a..931e655752a 100644 --- a/src/controllers/controllerpreset.h +++ b/src/controllers/controllerpreset.h @@ -55,14 +55,6 @@ class ControllerPreset { return m_scripts; } - void setModuleFileInfo(QFileInfo fileInfo) { - m_moduleFileInfo = std::move(fileInfo); - } - - QFileInfo moduleFileInfo() const { - return m_moduleFileInfo; - } - inline void setDirty(bool bDirty) { m_bDirty = bDirty; } @@ -203,7 +195,6 @@ class ControllerPreset { QString m_mixxxVersion; QList m_scripts; - QFileInfo m_moduleFileInfo; }; typedef QSharedPointer ControllerPresetPointer; diff --git a/src/controllers/controllerpresetfilehandler.cpp b/src/controllers/controllerpresetfilehandler.cpp index 7c351527054..55151d18582 100644 --- a/src/controllers/controllerpresetfilehandler.cpp +++ b/src/controllers/controllerpresetfilehandler.cpp @@ -142,20 +142,6 @@ void ControllerPresetFileHandler::addScriptFilesToPreset( preset->addScriptFile(filename, functionPrefix, file); scriptFile = scriptFile.nextSiblingElement("file"); } - - QString moduleFileName = controller.firstChildElement("module").text(); - - if (moduleFileName.isEmpty()) { - return; - } - - QFileInfo moduleFileInfo(preset->dirPath().absoluteFilePath(moduleFileName)); - if (!moduleFileInfo.isFile()) { - qWarning() << "Controller Module is not a file:" << moduleFileInfo.absoluteFilePath(); - return; - } - - preset->setModuleFileInfo(moduleFileInfo); } bool ControllerPresetFileHandler::writeDocument( diff --git a/src/controllers/engine/controllerengine.cpp b/src/controllers/engine/controllerengine.cpp deleted file mode 100644 index a802f1d31fa..00000000000 --- a/src/controllers/engine/controllerengine.cpp +++ /dev/null @@ -1,1377 +0,0 @@ -#include "controllers/engine/controllerengine.h" - -#include "control/controlobject.h" -#include "control/controlobjectscript.h" -#include "controllers/controller.h" -#include "controllers/controllerdebug.h" -#include "controllers/engine/colormapperjsproxy.h" -#include "controllers/engine/controllerenginejsproxy.h" -#include "controllers/engine/scriptconnectionjsproxy.h" -#include "errordialoghandler.h" -#include "mixer/playermanager.h" -#include "moc_controllerengine.cpp" -// to tell the msvs compiler about `isnan` -#include "util/math.h" -#include "util/time.h" - -namespace { -constexpr int kDecks = 16; - -// Use 1ms for the Alpha-Beta dt. We're assuming the OS actually gives us a 1ms -// timer. -constexpr int kScratchTimerMs = 1; -constexpr double kAlphaBetaDt = kScratchTimerMs / 1000.0; - -inline ControlFlags onlyAssertOnControllerDebug() { - if (ControllerDebug::enabled()) { - return ControlFlag::None; - } - - return ControlFlag::AllowMissingOrInvalid; -} -} // namespace - -ControllerEngine::ControllerEngine(Controller* controller) - : m_bDisplayingExceptionDialog(false), - m_pScriptEngine(nullptr), - m_pController(controller), - m_bTesting(false) { - // Handle error dialog buttons - qRegisterMetaType("QMessageBox::StandardButton"); - - // Pre-allocate arrays for average number of virtual decks - m_intervalAccumulator.resize(kDecks); - m_lastMovement.resize(kDecks); - m_dx.resize(kDecks); - m_rampTo.resize(kDecks); - m_ramp.resize(kDecks); - m_scratchFilters.resize(kDecks); - m_rampFactor.resize(kDecks); - m_brakeActive.resize(kDecks); - m_softStartActive.resize(kDecks); - // Initialize arrays used for testing and pointers - for (int i = 0; i < kDecks; ++i) { - m_dx[i] = 0.0; - m_scratchFilters[i] = new AlphaBetaFilter(); - m_ramp[i] = false; - } - - initializeScriptEngine(); -} - -ControllerEngine::~ControllerEngine() { - // Clean up - for (int i = 0; i < kDecks; ++i) { - delete m_scratchFilters[i]; - m_scratchFilters[i] = nullptr; - } - - uninitializeScriptEngine(); -} - -bool ControllerEngine::callFunctionOnObjects( - const QList& scriptFunctionPrefixes, - const QString& function, - const QJSValueList& args, - bool bFatalError) { - VERIFY_OR_DEBUG_ASSERT(m_pScriptEngine) { - return false; - } - - const QJSValue global = m_pScriptEngine->globalObject(); - - bool success = true; - for (const QString& prefixName : scriptFunctionPrefixes) { - QJSValue prefix = global.property(prefixName); - if (!prefix.isObject()) { - qWarning() << "ControllerEngine: No" << prefixName << "object in script"; - continue; - } - - QJSValue init = prefix.property(function); - if (!init.isCallable()) { - qWarning() << "ControllerEngine:" << prefixName << "has no" << function << " method"; - continue; - } - controllerDebug("ControllerEngine: Executing" << prefixName << "." << function); - QJSValue result = init.callWithInstance(prefix, args); - if (result.isError()) { - showScriptExceptionDialog(result, bFatalError); - success = false; - } - } - return success; -} - -QJSValue ControllerEngine::byteArrayToScriptValue(const QByteArray& byteArray) { - // The QJSEngine converts the QByteArray to an ArrayBuffer object. - QJSValue arrayBuffer = m_pScriptEngine->toScriptValue(byteArray); - // Convert the ArrayBuffer to a Uint8 typed array so scripts can access its bytes - // with the [] operator. - QJSValue result = m_byteArrayToScriptValueJSFunction.call( - QJSValueList{arrayBuffer}); - if (result.isError()) { - showScriptExceptionDialog(result); - } - return result; -} - -QJSValue ControllerEngine::wrapFunctionCode(const QString& codeSnippet, - int numberOfArgs) { - // This function is called from outside the controller engine, so we can't - // use VERIFY_OR_DEBUG_ASSERT here - if (m_pScriptEngine == nullptr) { - return QJSValue(); - } - - QJSValue wrappedFunction; - - auto i = m_scriptWrappedFunctionCache.constFind(codeSnippet); - if (i != m_scriptWrappedFunctionCache.constEnd()) { - wrappedFunction = i.value(); - } else { - QStringList wrapperArgList; - for (int i = 1; i <= numberOfArgs; i++) { - wrapperArgList << QString("arg%1").arg(i); - } - QString wrapperArgs = wrapperArgList.join(","); - QString wrappedCode = QStringLiteral("(function (") + wrapperArgs + - QStringLiteral(") { (") + codeSnippet + QStringLiteral(")(") + - wrapperArgs + QStringLiteral("); })"); - - wrappedFunction = evaluateCodeString(wrappedCode); - if (wrappedFunction.isError()) { - showScriptExceptionDialog(wrappedFunction); - } - m_scriptWrappedFunctionCache[codeSnippet] = wrappedFunction; - } - return wrappedFunction; -} - -void ControllerEngine::gracefulShutdown() { - if (m_pScriptEngine == nullptr) { - return; - } - - qDebug() << "ControllerEngine shutting down..."; - - // Stop all timers - stopAllTimers(); - - qDebug() << "Invoking shutdown() hook in scripts"; - callFunctionOnObjects(m_scriptFunctionPrefixes, "shutdown"); - - if (m_shutdownFunction.isCallable()) { - executeFunction(m_shutdownFunction, QJSValueList{}); - } - - // Prevents leaving decks in an unstable state - // if the controller is shut down while scratching - QHashIterator i(m_scratchTimers); - while (i.hasNext()) { - i.next(); - qDebug() << "Aborting scratching on deck" << i.value(); - // Clear scratch2_enable. PlayerManager::groupForDeck is 0-indexed. - QString group = PlayerManager::groupForDeck(i.value() - 1); - ControlObjectScript* pScratch2Enable = - getControlObjectScript(group, "scratch2_enable"); - if (pScratch2Enable != nullptr) { - pScratch2Enable->set(0); - } - } - - qDebug() << "Clearing function wrapper cache"; - m_scriptWrappedFunctionCache.clear(); - - // Free all the ControlObjectScripts - { - auto it = m_controlCache.begin(); - while (it != m_controlCache.end()) { - qDebug() - << "Deleting ControlObjectScript" - << it.key().group - << it.key().item; - delete it.value(); - // Advance iterator - it = m_controlCache.erase(it); - } - } -} - -void ControllerEngine::initializeScriptEngine() { - VERIFY_OR_DEBUG_ASSERT(!m_pScriptEngine) { - return; - } - - // Create the Script Engine - m_pScriptEngine = new QJSEngine(this); - - m_pScriptEngine->installExtensions(QJSEngine::ConsoleExtension); - - // Make this ControllerEngine instance available to scripts as 'engine'. - QJSValue engineGlobalObject = m_pScriptEngine->globalObject(); - ControllerEngineJSProxy* proxy = new ControllerEngineJSProxy(this); - engineGlobalObject.setProperty("engine", m_pScriptEngine->newQObject(proxy)); - - QJSValue mapper = m_pScriptEngine->newQMetaObject(&ColorMapperJSProxy::staticMetaObject); - engineGlobalObject.setProperty("ColorMapper", mapper); - - if (m_pController) { - qDebug() << "Controller in script engine is:" << m_pController->getName(); - - ControllerJSProxy* controllerProxy = m_pController->jsProxy(); - - // Make the Controller instance available to scripts - engineGlobalObject.setProperty("controller", m_pScriptEngine->newQObject(controllerProxy)); - - // ...under the legacy name as well - engineGlobalObject.setProperty("midi", m_pScriptEngine->newQObject(controllerProxy)); - } - - m_byteArrayToScriptValueJSFunction = evaluateCodeString("(function(arg1) { return new Uint8Array(arg1) })"); -} - -void ControllerEngine::uninitializeScriptEngine() { - // Delete the script engine, first clearing the pointer so that - // other threads will not get the dead pointer after we delete it. - if (m_pScriptEngine != nullptr) { - QJSEngine* engine = m_pScriptEngine; - m_pScriptEngine = nullptr; - engine->deleteLater(); - } -} - -void ControllerEngine::loadModule(const QFileInfo& moduleFileInfo) { - // QFileInfo does not have a isValid/isEmpty/isNull method to check if it - // actually contains a reference, so we check if the filePath is empty as a - // workaround. - // See https://stackoverflow.com/a/45652741/1455128 for details. - VERIFY_OR_DEBUG_ASSERT(!moduleFileInfo.filePath().isEmpty()) { - return; - } - - VERIFY_OR_DEBUG_ASSERT(moduleFileInfo.isFile()) { - return; - } -#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) - m_moduleFileInfo = moduleFileInfo; - - QJSValue mod = m_pScriptEngine->importModule(moduleFileInfo.absoluteFilePath()); - if (mod.isError()) { - showScriptExceptionDialog(mod); - return; - } - - connect(&m_scriptWatcher, - &QFileSystemWatcher::fileChanged, - this, - &ControllerEngine::scriptHasChanged); - m_scriptWatcher.addPath(moduleFileInfo.absoluteFilePath()); - - QJSValue initFunction = mod.property("init"); - executeFunction(initFunction, QJSValueList{}); - - QJSValue handleInputFunction = mod.property("handleInput"); - if (handleInputFunction.isCallable()) { - m_handleInputFunction = handleInputFunction; - } else { - scriptErrorDialog( - "Controller JavaScript module exports no handleInput function.", - QStringLiteral("handleInput"), - true); - } - - QJSValue shutdownFunction = mod.property("shutdown"); - if (shutdownFunction.isCallable()) { - m_shutdownFunction = shutdownFunction; - } else { - qDebug() << "Module exports no shutdown function."; - } -#else - Q_UNUSED(moduleFileInfo); -#endif -} - -void ControllerEngine::handleInput(const QByteArray& data, mixxx::Duration timestamp) { - if (m_handleInputFunction.isCallable()) { - QJSValueList args; - args << byteArrayToScriptValue(data); - args << timestamp.toDoubleMillis(); - executeFunction(m_handleInputFunction, args); - } -} - -bool ControllerEngine::loadScriptFiles(const QList& scripts) { - bool scriptsEvaluatedCorrectly = true; - for (const auto& script : scripts) { - if (!evaluateScriptFile(script.file)) { - scriptsEvaluatedCorrectly = false; - } - } - - m_lastScriptFiles = scripts; - - connect(&m_scriptWatcher, - &QFileSystemWatcher::fileChanged, - this, - &ControllerEngine::scriptHasChanged); - - if (!scriptsEvaluatedCorrectly) { - gracefulShutdown(); - uninitializeScriptEngine(); - } - - return scriptsEvaluatedCorrectly; -} - -void ControllerEngine::scriptHasChanged(const QString& scriptFilename) { - Q_UNUSED(scriptFilename); - disconnect(&m_scriptWatcher, &QFileSystemWatcher::fileChanged, this, &ControllerEngine::scriptHasChanged); - reloadScripts(); -} - -void ControllerEngine::reloadScripts() { - qDebug() << "ControllerEngine: Reloading Scripts"; - ControllerPresetPointer pPreset = m_pController->getPreset(); - - gracefulShutdown(); - uninitializeScriptEngine(); - - initializeScriptEngine(); - if (!loadScriptFiles(m_lastScriptFiles)) { - return; - } - - qDebug() << "Re-initializing scripts"; - initializeScripts(m_lastScriptFiles); - - // QFileInfo does not have a isValid/isEmpty/isNull method to check if it - // actually contains a reference, so we check if the filePath is empty as a - // workaround. - // See https://stackoverflow.com/a/45652741/1455128 for details. - if (!m_moduleFileInfo.filePath().isEmpty()) { - loadModule(m_moduleFileInfo); - } -} - -void ControllerEngine::initializeScripts(const QList& scripts) { - m_scriptFunctionPrefixes.clear(); - for (const ControllerPreset::ScriptFileInfo& script : scripts) { - // Skip empty prefixes. - if (!script.functionPrefix.isEmpty()) { - m_scriptFunctionPrefixes.append(script.functionPrefix); - } - } - - QJSValueList args; - args << QJSValue(m_pController->getName()); - args << QJSValue(ControllerDebug::enabled()); - - // Call the init method for all the prefixes. - bool success = callFunctionOnObjects(m_scriptFunctionPrefixes, "init", args, true); - - // We failed to initialize the controller scripts, shutdown the script - // engine to avoid error popups on every button press or slider move - if (!success) { - gracefulShutdown(); - uninitializeScriptEngine(); - } -} - -bool ControllerEngine::executeFunction(QJSValue functionObject, const QJSValueList& args) { - // This function is called from outside the controller engine, so we can't - // use VERIFY_OR_DEBUG_ASSERT here - if (!m_pScriptEngine) { - return false; - } - - if (functionObject.isError()) { - qDebug() << "ControllerEngine::executeFunction:" - << functionObject.toString(); - return false; - } - - // If it's not a function, we're done. - if (!functionObject.isCallable()) { - qDebug() << "ControllerEngine::executeFunction:" - << functionObject.toVariant() - << "Not a function"; - return false; - } - - // If it does happen to be a function, call it. - QJSValue returnValue = functionObject.call(args); - if (returnValue.isError()) { - showScriptExceptionDialog(returnValue); - return false; - } - return true; -} - -bool ControllerEngine::executeFunction(const QJSValue& functionObject, const QByteArray& data) { - // This function is called from outside the controller engine, so we can't - // use VERIFY_OR_DEBUG_ASSERT here - if (!m_pScriptEngine) { - return false; - } - QJSValueList args; - args << byteArrayToScriptValue(data); - args << QJSValue(data.size()); - return executeFunction(functionObject, args); -} - -QJSValue ControllerEngine::evaluateCodeString(const QString& program, const QString& fileName, int lineNumber) { - VERIFY_OR_DEBUG_ASSERT(m_pScriptEngine) { - return QJSValue::UndefinedValue; - } - return m_pScriptEngine->evaluate(program, fileName, lineNumber); -} - -void ControllerEngine::throwJSError(const QString& message) { -#if QT_VERSION < QT_VERSION_CHECK(5, 12, 0) - QString errorText = tr("Uncaught exception: %1").arg(message); - qWarning() << "ControllerEngine:" << errorText; - if (!m_bDisplayingExceptionDialog) { - scriptErrorDialog(errorText, errorText); - } -#else - m_pScriptEngine->throwError(message); -#endif -} - -void ControllerEngine::showScriptExceptionDialog( - const QJSValue& evaluationResult, bool bFatalError) { - VERIFY_OR_DEBUG_ASSERT(evaluationResult.isError()) { - return; - } - - QString errorMessage = evaluationResult.toString(); - QString line = evaluationResult.property("lineNumber").toString(); - QString backtrace = evaluationResult.property("stack").toString(); - QString filename = evaluationResult.property("fileName").toString(); - - QString errorText; - if (filename.isEmpty()) { - errorText = QString("Uncaught exception at line %1 in passed code.").arg(line); - } else { - errorText = QString("Uncaught exception at line %1 in file %2.").arg(line, filename); - } - - errorText += QStringLiteral("\n\nException:\n ") + errorMessage; - - // Do not include backtrace in dialog key because it might contain midi - // slider values that will differ most of the time. This would break - // the "Ignore" feature of the error dialog. - QString key = errorText; - qWarning() << "ControllerEngine:" << errorText; - - // Add backtrace to the error details - errorText += QStringLiteral("\n\nBacktrace:\n") + backtrace; - - if (!m_bDisplayingExceptionDialog) { - scriptErrorDialog(errorText, key, bFatalError); - } -} - -void ControllerEngine::scriptErrorDialog( - const QString& detailedError, const QString& key, bool bFatalError) { - if (m_bTesting) { - return; - } - - ErrorDialogProperties* props = - ErrorDialogHandler::instance()->newDialogProperties(); - - QString additionalErrorText; - if (bFatalError) { - additionalErrorText = - tr("The functionality provided by this controller mapping will " - "be disabled until the issue has been resolved."); - } else { - additionalErrorText = - tr("You can ignore this error for this session but " - "you may experience erratic behavior.") + - QString("
") + - tr("Try to recover by resetting your controller."); - } - - props->setType(DLG_WARNING); - props->setTitle(tr("Controller Preset Error")); - props->setText(QString(tr("The preset for your controller \"%1\" is not " - "working properly.")) - .arg(m_pController->getName())); - props->setInfoText(QStringLiteral("") + - tr("The script code needs to be fixed.") + QStringLiteral("

") + - additionalErrorText + QStringLiteral("

")); - - // Add "Details" text and set monospace font since they may contain - // backtraces and code. - props->setDetails(detailedError, true); - - // To prevent multiple windows for the same error - props->setKey(key); - - // Allow user to suppress further notifications about this particular error - if (!bFatalError) { - props->addButton(QMessageBox::Ignore); - props->addButton(QMessageBox::Retry); - props->setDefaultButton(QMessageBox::Ignore); - props->setEscapeButton(QMessageBox::Ignore); - } else { - props->addButton(QMessageBox::Close); - props->setDefaultButton(QMessageBox::Close); - props->setEscapeButton(QMessageBox::Close); - } - props->setModal(false); - - if (ErrorDialogHandler::instance()->requestErrorDialog(props)) { - m_bDisplayingExceptionDialog = true; - // Enable custom handling of the dialog buttons - connect(ErrorDialogHandler::instance(), - &ErrorDialogHandler::stdButtonClicked, - this, - &ControllerEngine::errorDialogButton); - } -} - -void ControllerEngine::errorDialogButton( - const QString& key, QMessageBox::StandardButton clickedButton) { - Q_UNUSED(key); - - m_bDisplayingExceptionDialog = false; - // Something was clicked, so disable this signal now - disconnect(ErrorDialogHandler::instance(), - &ErrorDialogHandler::stdButtonClicked, - this, - &ControllerEngine::errorDialogButton); - - if (clickedButton == QMessageBox::Retry) { - reloadScripts(); - } -} - -ControlObjectScript* ControllerEngine::getControlObjectScript(const QString& group, const QString& name) { - ConfigKey key = ConfigKey(group, name); - - if (!key.isValid()) { - qWarning() << "ControllerEngine: Requested control with invalid key" << key; - // Throw a debug assertion if controllerDebug is enabled - DEBUG_ASSERT(!ControllerDebug::enabled()); - return nullptr; - } - - ControlObjectScript* coScript = m_controlCache.value(key, nullptr); - if (coScript == nullptr) { - // create COT - coScript = new ControlObjectScript(key, this); - if (coScript->valid()) { - m_controlCache.insert(key, coScript); - } else { - delete coScript; - coScript = nullptr; - } - } - return coScript; -} - -double ControllerEngine::getValue(const QString& group, const QString& name) { - ControlObjectScript* coScript = getControlObjectScript(group, name); - if (coScript == nullptr) { - qWarning() << "ControllerEngine: Unknown control" << group << name << ", returning 0.0"; - return 0.0; - } - return coScript->get(); -} - -void ControllerEngine::setValue(const QString& group, const QString& name, double newValue) { - if (isnan(newValue)) { - qWarning() << "ControllerEngine: script setting [" << group << "," << name - << "] to NotANumber, ignoring."; - return; - } - - ControlObjectScript* coScript = getControlObjectScript(group, name); - - if (coScript) { - ControlObject* pControl = ControlObject::getControl( - coScript->getKey(), onlyAssertOnControllerDebug()); - if (pControl && !m_st.ignore(pControl, coScript->getParameterForValue(newValue))) { - coScript->slotSet(newValue); - } - } -} - -double ControllerEngine::getParameter(const QString& group, const QString& name) { - ControlObjectScript* coScript = getControlObjectScript(group, name); - if (coScript == nullptr) { - qWarning() << "ControllerEngine: Unknown control" << group << name << ", returning 0.0"; - return 0.0; - } - return coScript->getParameter(); -} - -void ControllerEngine::setParameter( - const QString& group, const QString& name, double newParameter) { - if (isnan(newParameter)) { - qWarning() << "ControllerEngine: script setting [" << group << "," << name - << "] to NotANumber, ignoring."; - return; - } - - ControlObjectScript* coScript = getControlObjectScript(group, name); - - if (coScript) { - ControlObject* pControl = ControlObject::getControl( - coScript->getKey(), onlyAssertOnControllerDebug()); - if (pControl && !m_st.ignore(pControl, newParameter)) { - coScript->setParameter(newParameter); - } - } -} - -double ControllerEngine::getParameterForValue( - const QString& group, const QString& name, double value) { - if (isnan(value)) { - qWarning() << "ControllerEngine: script setting [" << group << "," << name - << "] to NotANumber, ignoring."; - return 0.0; - } - - ControlObjectScript* coScript = getControlObjectScript(group, name); - - if (coScript == nullptr) { - qWarning() << "ControllerEngine: Unknown control" << group << name << ", returning 0.0"; - return 0.0; - } - - return coScript->getParameterForValue(value); -} - -void ControllerEngine::reset(const QString& group, const QString& name) { - ControlObjectScript* coScript = getControlObjectScript(group, name); - if (coScript != nullptr) { - coScript->reset(); - } -} - -double ControllerEngine::getDefaultValue(const QString& group, const QString& name) { - ControlObjectScript* coScript = getControlObjectScript(group, name); - - if (coScript == nullptr) { - qWarning() << "ControllerEngine: Unknown control" << group << name << ", returning 0.0"; - return 0.0; - } - - return coScript->getDefault(); -} - -double ControllerEngine::getDefaultParameter(const QString& group, const QString& name) { - ControlObjectScript* coScript = getControlObjectScript(group, name); - - if (coScript == nullptr) { - qWarning() << "ControllerEngine: Unknown control" << group << name << ", returning 0.0"; - return 0.0; - } - - return coScript->getParameterForValue(coScript->getDefault()); -} - -void ControllerEngine::log(const QString& message) { - controllerDebug(message); -} - -QJSValue ControllerEngine::makeConnection( - const QString& group, const QString& name, const QJSValue& callback) { - VERIFY_OR_DEBUG_ASSERT(m_pScriptEngine != nullptr) { - return QJSValue(); - } - - ControlObjectScript* coScript = getControlObjectScript(group, name); - if (coScript == nullptr) { - // The test setups do not run all of Mixxx, so ControlObjects not - // existing during tests is okay. - if (!m_bTesting) { - throwJSError("ControllerEngine: script tried to connect to ControlObject (" + - group + ", " + name + - ") which is non-existent."); - } - return QJSValue(); - } - - if (!callback.isCallable()) { - throwJSError("Tried to connect (" + group + ", " + name + ")" + " to an invalid callback. Make sure that your code contains no syntax errors."); - return QJSValue(); - } - - ScriptConnection connection; - connection.key = ConfigKey(group, name); - connection.controllerEngine = this; - connection.callback = callback; - connection.id = QUuid::createUuid(); - - if (coScript->addScriptConnection(connection)) { - return m_pScriptEngine->newQObject(new ScriptConnectionJSProxy(connection)); - } - - return QJSValue(); -} - -bool ControllerEngine::removeScriptConnection(const ScriptConnection& connection) { - ControlObjectScript* coScript = getControlObjectScript(connection.key.group, - connection.key.item); - - if (m_pScriptEngine == nullptr || coScript == nullptr) { - return false; - } - - return coScript->removeScriptConnection(connection); -} - -void ControllerEngine::triggerScriptConnection(const ScriptConnection& connection) { - VERIFY_OR_DEBUG_ASSERT(m_pScriptEngine) { - return; - } - - ControlObjectScript* coScript = getControlObjectScript(connection.key.group, - connection.key.item); - if (coScript == nullptr) { - return; - } - - connection.executeCallback(coScript->get()); -} - -// This function is a legacy version of makeConnection with several alternate -// ways of invoking it. The callback function can be passed either as a string of -// JavaScript code that evaluates to a function or an actual JavaScript function. -// If "true" is passed as a 4th parameter, all connections to the ControlObject -// are removed. If a ScriptConnectionInvokableWrapper is passed instead of a callback, -// it is disconnected. -// WARNING: These behaviors are quirky and confusing, so if you change this function, -// be sure to run the ControllerEngineTest suite to make sure you do not break old scripts. -QJSValue ControllerEngine::connectControl(const QString& group, - const QString& name, - const QJSValue& passedCallback, - bool disconnect) { - // The passedCallback may or may not actually be a function, so when - // the actual callback function is found, store it in this variable. - QJSValue actualCallbackFunction; - - if (passedCallback.isCallable()) { - if (!disconnect) { - // skip all the checks below and just make the connection - return makeConnection(group, name, passedCallback); - } - actualCallbackFunction = passedCallback; - } - - ControlObjectScript* coScript = getControlObjectScript(group, name); - // This check is redundant with makeConnection, but the - // ControlObjectScript is also needed here to check for duplicate connections. - if (coScript == nullptr) { - // The test setups do not run all of Mixxx, so ControlObjects not - // existing during tests is okay. - if (!m_bTesting) { - if (disconnect) { - throwJSError("ControllerEngine: script tried to disconnect from ControlObject (" + - group + ", " + name + ") which is non-existent."); - } else { - throwJSError("ControllerEngine: script tried to connect to ControlObject (" + - group + ", " + name + ") which is non-existent."); - } - } - // This is inconsistent with other failures, which return false. - // QJSValue() with no arguments is undefined in JavaScript. - return QJSValue(); - } - - if (passedCallback.isString()) { - // This check is redundant with makeConnection, but it must be done here - // before evaluating the code string. - VERIFY_OR_DEBUG_ASSERT(m_pScriptEngine != nullptr) { - return QJSValue(false); - } - - actualCallbackFunction = evaluateCodeString(passedCallback.toString()); - - if (!actualCallbackFunction.isCallable()) { - QString sErrorMessage("Invalid connection callback provided to engine.connectControl."); - if (actualCallbackFunction.isError()) { - sErrorMessage.append("\n" + actualCallbackFunction.toString()); - } - throwJSError(sErrorMessage); - return QJSValue(false); - } - - if (coScript->countConnections() > 0 && !disconnect) { - // This is inconsistent with the behavior when passing the callback as - // a function, but keep the old behavior to make sure old scripts do - // not break. - ScriptConnection connection = coScript->firstConnection(); - - qWarning() << "Tried to make duplicate connection between (" + - group + ", " + name + ") and " + passedCallback.toString() + - " but this is not allowed when passing a callback as a string. " + - "If you actually want to create duplicate connections, " + - "use engine.makeConnection. Returning reference to connection " + - connection.id.toString(); - - return m_pScriptEngine->newQObject(new ScriptConnectionJSProxy(connection)); - } - } else if (passedCallback.isQObject()) { - // Assume a ScriptConnection and assume that the script author - // wants to disconnect it, regardless of the disconnect parameter - // and regardless of whether it is connected to the same ControlObject - // specified by the first two parameters to this function. - QObject* qobject = passedCallback.toQObject(); - const QMetaObject* qmeta = qobject->metaObject(); - - qWarning() << "QObject passed to engine.connectControl. Assuming it is" - << "a connection object to disconnect and returning false."; - if (!strcmp(qmeta->className(), - "ScriptConnectionJSProxy")) { - ScriptConnectionJSProxy* proxy = - (ScriptConnectionJSProxy*)qobject; - proxy->disconnect(); - } - return QJSValue(false); - } - - // Support removing connections by passing "true" as the last parameter - // to this function, regardless of whether the callback is provided - // as a function or a string. - if (disconnect) { - // There is no way to determine which - // ScriptConnection to disconnect unless the script calls - // ScriptConnectionInvokableWrapper::disconnect(), so - // disconnect all ScriptConnections connected to the - // callback function, even though there may be multiple connections. - coScript->disconnectAllConnectionsToFunction(actualCallbackFunction); - return QJSValue(true); - } - - // If execution gets this far without returning, make - // a new connection to actualCallbackFunction. - return makeConnection(group, name, actualCallbackFunction); -} - -void ControllerEngine::trigger(const QString& group, const QString& name) { - ControlObjectScript* coScript = getControlObjectScript(group, name); - if (coScript != nullptr) { - coScript->emitValueChanged(); - } -} - -bool ControllerEngine::evaluateScriptFile(const QFileInfo& scriptFile) { - VERIFY_OR_DEBUG_ASSERT(m_pScriptEngine) { - return false; - } - - if (!scriptFile.exists()) { - qWarning() << "ControllerEngine: File does not exist:" << scriptFile.absoluteFilePath(); - return false; - } - m_scriptWatcher.addPath(scriptFile.absoluteFilePath()); - - qDebug() << "ControllerEngine: Loading" << scriptFile.absoluteFilePath(); - - // Read in the script file - QString filename = scriptFile.absoluteFilePath(); - QFile input(filename); - if (!input.open(QIODevice::ReadOnly)) { - qWarning() << QString("ControllerEngine: Problem opening the script file: %1, error # %2, %3") - .arg(filename, QString::number(input.error()), input.errorString()); - // Set up error dialog - ErrorDialogProperties* props = ErrorDialogHandler::instance()->newDialogProperties(); - props->setType(DLG_WARNING); - props->setTitle(tr("Controller Mapping File Problem")); - props->setText(tr("The mapping for controller \"%1\" cannot be opened.") - .arg(m_pController->getName())); - props->setInfoText( - tr("The functionality provided by this controller mapping will " - "be disabled until the issue has been resolved.")); - - // We usually don't translate the details field, but the cause of - // this problem lies in the user's system (e.g. a permission - // issue). Translating this will help users to fix the issue even - // when they don't speak english. - props->setDetails(tr("File:") + QStringLiteral(" ") + filename + - QStringLiteral("\n") + tr("Error:") + QStringLiteral(" ") + - input.errorString()); - - // Ask above layer to display the dialog & handle user response - ErrorDialogHandler::instance()->requestErrorDialog(props); - return false; - } - - QString scriptCode = ""; - scriptCode.append(input.readAll()); - scriptCode.append('\n'); - input.close(); - - // Evaluate the code - QJSValue scriptFunction = evaluateCodeString(scriptCode, filename); - if (scriptFunction.isError()) { - showScriptExceptionDialog(scriptFunction, true); - return false; - } - - return true; -} - -int ControllerEngine::beginTimer(int intervalMillis, QJSValue timerCallback, bool oneShot) { - if (timerCallback.isString()) { - timerCallback = evaluateCodeString(timerCallback.toString()); - } else if (!timerCallback.isCallable()) { - QString sErrorMessage( - "Invalid timer callback provided to engine.beginTimer. Valid callbacks are strings and functions. " - "Make sure that your code contains no syntax errors."); - if (timerCallback.isError()) { - sErrorMessage.append("\n" + timerCallback.toString()); - } - throwJSError(sErrorMessage); - return 0; - } - - if (intervalMillis < 20) { - qWarning() << "Timer request for" << intervalMillis - << "ms is too short. Setting to the minimum of 20ms."; - intervalMillis = 20; - } - - // This makes use of every QObject's internal timer mechanism. Nice, clean, - // and simple. See http://doc.trolltech.com/4.6/qobject.html#startTimer for - // details - int timerId = startTimer(intervalMillis); - TimerInfo info; - info.callback = timerCallback; - info.oneShot = oneShot; - m_timers[timerId] = info; - if (timerId == 0) { - qWarning() << "Script timer could not be created"; - } else if (oneShot) { - controllerDebug("Starting one-shot timer:" << timerId); - } else { - controllerDebug("Starting timer:" << timerId); - } - return timerId; -} - -void ControllerEngine::stopTimer(int timerId) { - if (!m_timers.contains(timerId)) { - qWarning() << "Killing timer" << timerId << ": That timer does not exist!"; - return; - } - controllerDebug("Killing timer:" << timerId); - killTimer(timerId); - m_timers.remove(timerId); -} - -void ControllerEngine::stopAllTimers() { - QMutableHashIterator i(m_timers); - while (i.hasNext()) { - i.next(); - stopTimer(i.key()); - } -} - -void ControllerEngine::timerEvent(QTimerEvent* event) { - int timerId = event->timerId(); - - // See if this is a scratching timer - if (m_scratchTimers.contains(timerId)) { - scratchProcess(timerId); - return; - } - - auto it = m_timers.constFind(timerId); - if (it == m_timers.constEnd()) { - qWarning() << "Timer" << timerId << "fired but there's no function mapped to it!"; - return; - } - - // NOTE(rryan): Do not assign by reference -- make a copy. I have no idea - // why but this causes segfaults in ~QScriptValue while scratching if we - // don't copy here -- even though internalExecute passes the QScriptValues - // by value. *boggle* - const TimerInfo timerTarget = it.value(); - if (timerTarget.oneShot) { - stopTimer(timerId); - } - - executeFunction(timerTarget.callback, QJSValueList()); -} - -void ControllerEngine::softTakeover(const QString& group, const QString& name, bool set) { - ConfigKey key = ConfigKey(group, name); - ControlObject* pControl = ControlObject::getControl(key, onlyAssertOnControllerDebug()); - if (!pControl) { - qWarning() << "Failed to" << (set ? "enable" : "disable") - << "softTakeover for invalid control" << key; - return; - } - if (set) { - m_st.enable(pControl); - } else { - m_st.disable(pControl); - } -} - -void ControllerEngine::softTakeoverIgnoreNextValue(const QString& group, const QString& name) { - ConfigKey key = ConfigKey(group, name); - ControlObject* pControl = ControlObject::getControl(key, onlyAssertOnControllerDebug()); - if (!pControl) { - qWarning() << "Failed to call softTakeoverIgnoreNextValue for invalid control" << key; - return; - } - - m_st.ignoreNext(pControl); -} - -double ControllerEngine::getDeckRate(const QString& group) { - double rate = 0.0; - ControlObjectScript* pRateRatio = getControlObjectScript(group, "rate_ratio"); - if (pRateRatio != nullptr) { - rate = pRateRatio->get(); - } - - // See if we're in reverse play - ControlObjectScript* pReverse = getControlObjectScript(group, "reverse"); - if (pReverse != nullptr && pReverse->get() == 1) { - rate = -rate; - } - return rate; -} - -bool ControllerEngine::isDeckPlaying(const QString& group) { - ControlObjectScript* pPlay = getControlObjectScript(group, "play"); - - if (pPlay == nullptr) { - QString error = QString("Could not getControlObjectScript()"); - scriptErrorDialog(error, error); - return false; - } - - return pPlay->get() > 0.0; -} - -void ControllerEngine::scratchEnable( - int deck, - int intervalsPerRev, - double rpm, - double alpha, - double beta, - bool ramp) { - // If we're already scratching this deck, override that with this request - if (m_dx[deck] != 0) { - //qDebug() << "Already scratching deck" << deck << ". Overriding."; - int timerId = m_scratchTimers.key(deck); - killTimer(timerId); - m_scratchTimers.remove(timerId); - } - - // Controller resolution in intervals per second at normal speed. - // (rev/min * ints/rev * mins/sec) - double intervalsPerSecond = (rpm * intervalsPerRev) / 60.0; - - if (intervalsPerSecond == 0.0) { - qWarning() << "Invalid rpm or intervalsPerRev supplied to scratchEnable. Ignoring request."; - return; - } - - m_dx[deck] = 1.0 / intervalsPerSecond; - m_intervalAccumulator[deck] = 0.0; - m_ramp[deck] = false; - m_rampFactor[deck] = 0.001; - m_brakeActive[deck] = false; - - // PlayerManager::groupForDeck is 0-indexed. - QString group = PlayerManager::groupForDeck(deck - 1); - - // Ramp velocity, default to stopped. - double initVelocity = 0.0; - - ControlObjectScript* pScratch2Enable = - getControlObjectScript(group, "scratch2_enable"); - - // If ramping is desired, figure out the deck's current speed - if (ramp) { - // See if the deck is already being scratched - if (pScratch2Enable != nullptr && pScratch2Enable->get() == 1) { - // If so, set the filter's initial velocity to the scratch speed - ControlObjectScript* pScratch2 = - getControlObjectScript(group, "scratch2"); - if (pScratch2 != nullptr) { - initVelocity = pScratch2->get(); - } - } else if (isDeckPlaying(group)) { - // If the deck is playing, set the filter's initial velocity to the - // playback speed - initVelocity = getDeckRate(group); - } - } - - // Initialize scratch filter - if (alpha != 0 && beta != 0) { - m_scratchFilters[deck]->init(kAlphaBetaDt, initVelocity, alpha, beta); - } else { - // Use filter's defaults if not specified - m_scratchFilters[deck]->init(kAlphaBetaDt, initVelocity); - } - - // 1ms is shortest possible, OS dependent - int timerId = startTimer(kScratchTimerMs); - - // Associate this virtual deck with this timer for later processing - m_scratchTimers[timerId] = deck; - - // Set scratch2_enable - if (pScratch2Enable != nullptr) { - pScratch2Enable->slotSet(1); - } -} - -void ControllerEngine::scratchTick(int deck, int interval) { - m_lastMovement[deck] = mixxx::Time::elapsed(); - m_intervalAccumulator[deck] += interval; -} - -void ControllerEngine::scratchProcess(int timerId) { - int deck = m_scratchTimers[timerId]; - // PlayerManager::groupForDeck is 0-indexed. - QString group = PlayerManager::groupForDeck(deck - 1); - AlphaBetaFilter* filter = m_scratchFilters[deck]; - if (!filter) { - qWarning() << "Scratch filter pointer is null on deck" << deck; - return; - } - - const double oldRate = filter->predictedVelocity(); - - // Give the filter a data point: - - // If we're ramping to end scratching and the wheel hasn't been turned very - // recently (spinback after lift-off,) feed fixed data - if (m_ramp[deck] && !m_softStartActive[deck] && - ((mixxx::Time::elapsed() - m_lastMovement[deck]) >= mixxx::Duration::fromMillis(1))) { - filter->observation(m_rampTo[deck] * m_rampFactor[deck]); - // Once this code path is run, latch so it always runs until reset - //m_lastMovement[deck] += mixxx::Duration::fromSeconds(1); - } else if (m_softStartActive[deck]) { - // pretend we have moved by (desired rate*default distance) - filter->observation(m_rampTo[deck] * kAlphaBetaDt); - } else { - // This will (and should) be 0 if no net ticks have been accumulated - // (i.e. the wheel is stopped) - filter->observation(m_dx[deck] * m_intervalAccumulator[deck]); - } - - const double newRate = filter->predictedVelocity(); - - // Actually do the scratching - ControlObjectScript* pScratch2 = getControlObjectScript(group, "scratch2"); - if (pScratch2 == nullptr) { - return; // abort and maybe it'll work on the next pass - } - pScratch2->set(newRate); - - // Reset accumulator - m_intervalAccumulator[deck] = 0; - - // End scratching if we're ramping and the current rate is really close to the rampTo value - if ((m_ramp[deck] && fabs(m_rampTo[deck] - newRate) <= 0.00001) || - // or if we brake or softStart and have crossed over the desired value, - ((m_brakeActive[deck] || m_softStartActive[deck]) && ((oldRate > m_rampTo[deck] && newRate < m_rampTo[deck]) || (oldRate < m_rampTo[deck] && newRate > m_rampTo[deck]))) || - // or if the deck was stopped manually during brake or softStart - ((m_brakeActive[deck] || m_softStartActive[deck]) && (!isDeckPlaying(group)))) { - // Not ramping no mo' - m_ramp[deck] = false; - - if (m_brakeActive[deck]) { - // If in brake mode, set scratch2 rate to 0 and turn off the play button. - pScratch2->slotSet(0.0); - ControlObjectScript* pPlay = getControlObjectScript(group, "play"); - if (pPlay != nullptr) { - pPlay->slotSet(0.0); - } - } - - // Clear scratch2_enable to end scratching. - ControlObjectScript* pScratch2Enable = - getControlObjectScript(group, "scratch2_enable"); - if (pScratch2Enable == nullptr) { - return; // abort and maybe it'll work on the next pass - } - pScratch2Enable->slotSet(0); - - // Remove timer - killTimer(timerId); - m_scratchTimers.remove(timerId); - - m_dx[deck] = 0.0; - m_brakeActive[deck] = false; - m_softStartActive[deck] = false; - } -} - -void ControllerEngine::scratchDisable(int deck, bool ramp) { - // PlayerManager::groupForDeck is 0-indexed. - QString group = PlayerManager::groupForDeck(deck - 1); - - m_rampTo[deck] = 0.0; - - // If no ramping is desired, disable scratching immediately - if (!ramp) { - // Clear scratch2_enable - ControlObjectScript* pScratch2Enable = getControlObjectScript(group, "scratch2_enable"); - if (pScratch2Enable != nullptr) { - pScratch2Enable->slotSet(0); - } - // Can't return here because we need scratchProcess to stop the timer. - // So it's still actually ramping, we just won't hear or see it. - } else if (isDeckPlaying(group)) { - // If so, set the target velocity to the playback speed - m_rampTo[deck] = getDeckRate(group); - } - - m_lastMovement[deck] = mixxx::Time::elapsed(); - m_ramp[deck] = true; // Activate the ramping in scratchProcess() -} - -bool ControllerEngine::isScratching(int deck) { - // PlayerManager::groupForDeck is 0-indexed. - QString group = PlayerManager::groupForDeck(deck - 1); - return getValue(group, "scratch2_enable") > 0; -} - -void ControllerEngine::spinback(int deck, bool activate, double factor, double rate) { - // defaults for args set in header file - brake(deck, activate, factor, rate); -} - -void ControllerEngine::brake(int deck, bool activate, double factor, double rate) { - // PlayerManager::groupForDeck is 0-indexed. - QString group = PlayerManager::groupForDeck(deck - 1); - - // kill timer when both enabling or disabling - int timerId = m_scratchTimers.key(deck); - killTimer(timerId); - m_scratchTimers.remove(timerId); - - // enable/disable scratch2 mode - ControlObjectScript* pScratch2Enable = getControlObjectScript(group, "scratch2_enable"); - if (pScratch2Enable != nullptr) { - pScratch2Enable->slotSet(activate ? 1 : 0); - } - - // used in scratchProcess for the different timer behavior we need - m_brakeActive[deck] = activate; - double initRate = rate; - - if (activate) { - // store the new values for this spinback/brake effect - if (initRate == 1.0) { // then rate is really 1.0 or was set to default - // in /res/common-controller-scripts.js so check for real value, - // taking pitch into account - initRate = getDeckRate(group); - } - // stop ramping at a rate which doesn't produce any audible output anymore - m_rampTo[deck] = 0.01; - // if we are currently softStart()ing, stop it - if (m_softStartActive[deck]) { - m_softStartActive[deck] = false; - AlphaBetaFilter* filter = m_scratchFilters[deck]; - if (filter != nullptr) { - initRate = filter->predictedVelocity(); - } - } - - // setup timer and set scratch2 - timerId = startTimer(kScratchTimerMs); - m_scratchTimers[timerId] = deck; - - ControlObjectScript* pScratch2 = getControlObjectScript(group, "scratch2"); - if (pScratch2 != nullptr) { - pScratch2->slotSet(initRate); - } - - // setup the filter with default alpha and beta*factor - double alphaBrake = 1.0 / 512; - // avoid decimals for fine adjusting - if (factor > 1) { - factor = ((factor - 1) / 10) + 1; - } - double betaBrake = ((1.0 / 512) / 1024) * factor; // default*factor - AlphaBetaFilter* filter = m_scratchFilters[deck]; - if (filter != nullptr) { - filter->init(kAlphaBetaDt, initRate, alphaBrake, betaBrake); - } - - // activate the ramping in scratchProcess() - m_ramp[deck] = true; - } -} - -void ControllerEngine::softStart(int deck, bool activate, double factor) { - // PlayerManager::groupForDeck is 0-indexed. - QString group = PlayerManager::groupForDeck(deck - 1); - - // kill timer when both enabling or disabling - int timerId = m_scratchTimers.key(deck); - killTimer(timerId); - m_scratchTimers.remove(timerId); - - // enable/disable scratch2 mode - ControlObjectScript* pScratch2Enable = getControlObjectScript(group, "scratch2_enable"); - if (pScratch2Enable != nullptr) { - pScratch2Enable->slotSet(activate ? 1 : 0); - } - - // used in scratchProcess for the different timer behavior we need - m_softStartActive[deck] = activate; - double initRate = 0.0; - - if (activate) { - // acquire deck rate - m_rampTo[deck] = getDeckRate(group); - - // if brake()ing, get current rate from filter - if (m_brakeActive[deck]) { - m_brakeActive[deck] = false; - - AlphaBetaFilter* filter = m_scratchFilters[deck]; - if (filter != nullptr) { - initRate = filter->predictedVelocity(); - } - } - - // setup timer, start playing and set scratch2 - timerId = startTimer(kScratchTimerMs); - m_scratchTimers[timerId] = deck; - - ControlObjectScript* pPlay = getControlObjectScript(group, "play"); - if (pPlay != nullptr) { - pPlay->slotSet(1.0); - } - - ControlObjectScript* pScratch2 = getControlObjectScript(group, "scratch2"); - if (pScratch2 != nullptr) { - pScratch2->slotSet(initRate); - } - - // setup the filter like in brake(), with default alpha and beta*factor - double alphaSoft = 1.0 / 512; - // avoid decimals for fine adjusting - if (factor > 1) { - factor = ((factor - 1) / 10) + 1; - } - double betaSoft = ((1.0 / 512) / 1024) * factor; // default: (1.0/512)/1024 - AlphaBetaFilter* filter = m_scratchFilters[deck]; - if (filter != nullptr) { // kAlphaBetaDt = 1/1000 seconds - filter->init(kAlphaBetaDt, initRate, alphaSoft, betaSoft); - } - - // activate the ramping in scratchProcess() - m_ramp[deck] = true; - } -} diff --git a/src/controllers/engine/controllerengine.h b/src/controllers/engine/controllerengine.h deleted file mode 100644 index 17a8b66e034..00000000000 --- a/src/controllers/engine/controllerengine.h +++ /dev/null @@ -1,184 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "controllers/controllerpreset.h" -#include "controllers/softtakeover.h" -#include "preferences/usersettings.h" -#include "util/alphabetafilter.h" -#include "util/duration.h" - -class Controller; -class ControlObjectScript; -class ControllerEngine; -class ControllerEngineJSProxy; -class EvaluationException; -class ScriptConnection; - -class ControllerEngine : public QObject { - Q_OBJECT - public: - ControllerEngine(Controller* controller); - virtual ~ControllerEngine(); - - void handleInput(const QByteArray& data, mixxx::Duration timestamp); - - bool executeFunction(QJSValue functionObject, const QJSValueList& arguments); - bool executeFunction(const QJSValue& functionObject, const QByteArray& data); - - /// Wrap a string of JS code in an anonymous function. This allows any JS - /// string that evaluates to a function to be used in MIDI mapping XML files - /// and ensures the function is executed with the correct 'this' object. - QJSValue wrapFunctionCode(const QString& codeSnippet, int numberOfArgs); - - /// Look up registered script function prefixes - const QList& getScriptFunctionPrefixes() { - return m_scriptFunctionPrefixes; - }; - - /// Shows a UI dialog notifying of a script evaluation error. - /// Precondition: QJSValue.isError() == true - void showScriptExceptionDialog(const QJSValue& evaluationResult, bool bFatal = false); - - bool removeScriptConnection(const ScriptConnection& conn); - /// Execute a ScriptConnection's JS callback - void triggerScriptConnection(const ScriptConnection& conn); - - inline void setTesting(bool testing) { - m_bTesting = testing; - }; - - protected: - double getValue(const QString& group, const QString& name); - void setValue(const QString& group, const QString& name, double newValue); - double getParameter(const QString& group, const QString& name); - void setParameter(const QString& group, const QString& name, double newValue); - double getParameterForValue(const QString& group, const QString& name, double value); - void reset(const QString& group, const QString& name); - double getDefaultValue(const QString& group, const QString& name); - double getDefaultParameter(const QString& group, const QString& name); - /// Connect a ControlObject's valueChanged() signal to a script callback function - /// Returns to the script a ScriptConnectionJSProxy - QJSValue makeConnection(const QString& group, const QString& name, const QJSValue& callback); - /// DEPRECATED: Use makeConnection instead. - QJSValue connectControl(const QString& group, - const QString& name, - const QJSValue& passedCallback, - bool disconnect = false); - /// Execute callbacks for all ScriptConnections connected to a ControlObject - /// DEPRECATED: Use ScriptConnectionJSProxy::trigger instead. - void trigger(const QString& group, const QString& name); - void log(const QString& message); - /// Returns a timer ID to the script - int beginTimer(int intervalMillis, QJSValue scriptCode, bool oneShot = false); - void stopTimer(int timerId); - - /// [En/dis]able soft-takeover status for a particular ControlObject - void softTakeover(const QString& group, const QString& name, bool set); - /// Ignores the next value for the given ControlObject. This should be called - /// before or after an absolute physical control (slider or knob with hard limits) - /// is changed to operate on a different ControlObject, allowing it to sync up to the - /// soft-takeover state without an abrupt jump. - void softTakeoverIgnoreNextValue(const QString& group, const QString& name); - - void scratchEnable( - int deck, - int intervalsPerRev, - double rpm, - double alpha, - double beta, - bool ramp = true); - /// Accumulates ticks of the controller wheel - void scratchTick(int deck, int interval); - void scratchDisable(int deck, bool ramp = true); - bool isScratching(int deck); - void brake(int deck, bool activate, double factor = 1.0, double rate = 1.0); - void spinback(int deck, bool activate, double factor = 1.8, double rate = -10.0); - void softStart(int deck, bool activate, double factor = 1.0); - - /// Handler for timers that scripts set. - virtual void timerEvent(QTimerEvent* event); - - public slots: - void loadModule(const QFileInfo& moduleFileInfo); - bool loadScriptFiles(const QList& scripts); - void initializeScripts(const QList& scripts); - void gracefulShutdown(); - void scriptHasChanged(const QString&); - - private slots: - void errorDialogButton(const QString& key, QMessageBox::StandardButton button); - - private: - bool evaluateScriptFile(const QFileInfo& scriptFile); - void initializeScriptEngine(); - void uninitializeScriptEngine(); - void reloadScripts(); - - void scriptErrorDialog(const QString& detailedError, const QString& key, bool bFatal = false); - void generateScriptFunctions(const QString& code); - /// Stops and removes all timers (for shutdown). - void stopAllTimers(); - - bool callFunctionOnObjects( - const QList&, - const QString&, - const QJSValueList& args = QJSValueList(), - bool bFatalError = false); - /// Convert a byteArray to a JS typed array over an ArrayBuffer - QJSValue byteArrayToScriptValue(const QByteArray& byteArray); - QJSValue evaluateCodeString(const QString& program, const QString& fileName = QString(), int lineNumber = 1); - - void throwJSError(const QString& message); - - bool m_bDisplayingExceptionDialog; - QJSEngine* m_pScriptEngine; - - ControlObjectScript* getControlObjectScript(const QString& group, const QString& name); - - // Scratching functions & variables - - /// Applies the accumulated movement to the track speed - void scratchProcess(int timerId); - - bool isDeckPlaying(const QString& group); - double getDeckRate(const QString& group); - - Controller* m_pController; - QJSValue m_handleInputFunction; - QJSValue m_shutdownFunction; - QList m_scriptFunctionPrefixes; - QHash m_controlCache; - struct TimerInfo { - QJSValue callback; - bool oneShot; - }; - QHash m_timers; - SoftTakeoverCtrl m_st; - // 256 (default) available virtual decks is enough I would think. - // If more are needed at run-time, these will move to the heap automatically - QVarLengthArray m_intervalAccumulator; - QVarLengthArray m_lastMovement; - QVarLengthArray m_dx, m_rampTo, m_rampFactor; - QVarLengthArray m_ramp, m_brakeActive, m_softStartActive; - QVarLengthArray m_scratchFilters; - QHash m_scratchTimers; - QHash m_scriptWrappedFunctionCache; - QJSValue m_byteArrayToScriptValueJSFunction; - // Filesystem watcher for script auto-reload - QFileSystemWatcher m_scriptWatcher; - QFileInfo m_moduleFileInfo; - QList m_lastScriptFiles; - - bool m_bTesting; - - friend class ScriptConnection; - friend class ControllerEngineJSProxy; - friend class ColorJSProxy; - friend class ColorMapperJSProxy; - friend class ControllerEngineTest; -}; diff --git a/src/controllers/engine/controllerenginejsproxy.cpp b/src/controllers/engine/controllerenginejsproxy.cpp deleted file mode 100644 index 47bad8c9ca2..00000000000 --- a/src/controllers/engine/controllerenginejsproxy.cpp +++ /dev/null @@ -1,153 +0,0 @@ -#include "controllerenginejsproxy.h" - -#include "controllers/engine/controllerengine.h" -#include "moc_controllerenginejsproxy.cpp" - -ControllerEngineJSProxy::ControllerEngineJSProxy(ControllerEngine* m_pEngine) - : m_pEngine(m_pEngine) { -} - -ControllerEngineJSProxy::~ControllerEngineJSProxy() { -} - -double ControllerEngineJSProxy::getValue( - const QString& group, - const QString& name) { - return m_pEngine->getValue(group, name); -} - -void ControllerEngineJSProxy::setValue( - const QString& group, - const QString& name, - double newValue) { - m_pEngine->setValue(group, name, newValue); -} - -double ControllerEngineJSProxy::getParameter( - const QString& group, - const QString& name) { - return m_pEngine->getParameter(group, name); -} - -void ControllerEngineJSProxy::setParameter( - const QString& group, - const QString& name, - double newValue) { - m_pEngine->setParameter(group, name, newValue); -} - -double ControllerEngineJSProxy::getParameterForValue( - const QString& group, - const QString& name, - double value) { - return m_pEngine->getParameterForValue(group, name, value); -} - -void ControllerEngineJSProxy::reset( - const QString& group, - const QString& name) { - m_pEngine->reset(group, name); -} - -double ControllerEngineJSProxy::getDefaultValue( - const QString& group, - const QString& name) { - return m_pEngine->getDefaultValue(group, name); -} - -double ControllerEngineJSProxy::getDefaultParameter( - const QString& group, - const QString& name) { - return m_pEngine->getDefaultParameter(group, name); -} - -QJSValue ControllerEngineJSProxy::makeConnection( - const QString& group, - const QString& name, - const QJSValue& callback) { - return m_pEngine->makeConnection(group, name, callback); -} - -QJSValue ControllerEngineJSProxy::connectControl( - const QString& group, - const QString& name, - const QJSValue& passedCallback, - bool disconnect) { - return m_pEngine->connectControl(group, name, passedCallback, disconnect); -} - -void ControllerEngineJSProxy::trigger( - const QString& group, - const QString& name) { - m_pEngine->trigger(group, name); -} - -void ControllerEngineJSProxy::log(const QString& message) { - m_pEngine->log(message); -} - -int ControllerEngineJSProxy::beginTimer( - int interval, - const QJSValue& scriptCode, - bool oneShot) { - return m_pEngine->beginTimer(interval, scriptCode, oneShot); -} - -void ControllerEngineJSProxy::stopTimer(int timerId) { - m_pEngine->stopTimer(timerId); -} - -void ControllerEngineJSProxy::scratchEnable( - int deck, - int intervalsPerRev, - double rpm, - double alpha, - double beta, - bool ramp) { - m_pEngine->scratchEnable(deck, intervalsPerRev, rpm, alpha, beta, ramp); -} - -void ControllerEngineJSProxy::scratchTick(int deck, int interval) { - m_pEngine->scratchTick(deck, interval); -} - -void ControllerEngineJSProxy::scratchDisable(int deck, bool ramp) { - m_pEngine->scratchDisable(deck, ramp); -} - -bool ControllerEngineJSProxy::isScratching(int deck) { - return m_pEngine->isScratching(deck); -} - -void ControllerEngineJSProxy::softTakeover( - const QString& group, - const QString& name, - bool set) { - m_pEngine->softTakeover(group, name, set); -} - -void ControllerEngineJSProxy::softTakeoverIgnoreNextValue( - const QString& group, - const QString& name) { - m_pEngine->softTakeoverIgnoreNextValue(group, name); -} - -void ControllerEngineJSProxy::brake( - int deck, - bool activate, - double factor, - double rate) { - m_pEngine->brake(deck, activate, factor, rate); -} - -void ControllerEngineJSProxy::spinback( - int deck, - bool activate, - double factor, - double rate) { - m_pEngine->spinback(deck, activate, factor, rate); -} - -void ControllerEngineJSProxy::softStart(int deck, bool activate, double factor) { - m_pEngine->softStart(deck, activate, factor); -} diff --git a/src/controllers/engine/controllerenginejsproxy.h b/src/controllers/engine/controllerenginejsproxy.h deleted file mode 100644 index 6db1d10d5ec..00000000000 --- a/src/controllers/engine/controllerenginejsproxy.h +++ /dev/null @@ -1,60 +0,0 @@ -#ifndef CONTROLLERENGINEJSPROXY_H -#define CONTROLLERENGINEJSPROXY_H - -#include -#include - -class ControllerEngine; - -// An object of this class gets exposed to the JS engine, so the methods of this class -// constitute the api that is provided to scripts under "engine" object. -// -// The implementation simply forwards its method calls to the ControllerEngine. -// We cannot expose ControllerEngine directly to the JS engine because the JS engine would take -// ownership of ControllerEngine. This is problematic when we reload a script file, because we -// destroy the existing JS engine to create a new one. Then, since the JS engine owns ControllerEngine -// it will try to delete it. See this Qt bug: https://bugreports.qt.io/browse/QTBUG-41171 -class ControllerEngineJSProxy : public QObject { - Q_OBJECT - public: - ControllerEngineJSProxy(ControllerEngine* m_pEngine); - - virtual ~ControllerEngineJSProxy(); - - Q_INVOKABLE double getValue(const QString& group, const QString& name); - Q_INVOKABLE void setValue(const QString& group, const QString& name, double newValue); - Q_INVOKABLE double getParameter(const QString& group, const QString& name); - Q_INVOKABLE void setParameter(const QString& group, const QString& name, double newValue); - Q_INVOKABLE double getParameterForValue( - const QString& group, const QString& name, double value); - Q_INVOKABLE void reset(const QString& group, const QString& name); - Q_INVOKABLE double getDefaultValue(const QString& group, const QString& name); - Q_INVOKABLE double getDefaultParameter(const QString& group, const QString& name); - Q_INVOKABLE QJSValue makeConnection(const QString& group, - const QString& name, - const QJSValue& callback); - // DEPRECATED: Use makeConnection instead. - Q_INVOKABLE QJSValue connectControl(const QString& group, - const QString& name, - const QJSValue& passedCallback, - bool disconnect = false); - // Called indirectly by the objects returned by connectControl - Q_INVOKABLE void trigger(const QString& group, const QString& name); - Q_INVOKABLE void log(const QString& message); - Q_INVOKABLE int beginTimer(int interval, const QJSValue& scriptCode, bool oneShot = false); - Q_INVOKABLE void stopTimer(int timerId); - Q_INVOKABLE void scratchEnable(int deck, int intervalsPerRev, double rpm, double alpha, double beta, bool ramp = true); - Q_INVOKABLE void scratchTick(int deck, int interval); - Q_INVOKABLE void scratchDisable(int deck, bool ramp = true); - Q_INVOKABLE bool isScratching(int deck); - Q_INVOKABLE void softTakeover(const QString& group, const QString& name, bool set); - Q_INVOKABLE void softTakeoverIgnoreNextValue(const QString& group, const QString& name); - Q_INVOKABLE void brake(int deck, bool activate, double factor = 1.0, double rate = 1.0); - Q_INVOKABLE void spinback(int deck, bool activate, double factor = 1.8, double rate = -10.0); - Q_INVOKABLE void softStart(int deck, bool activate, double factor = 1.0); - - private: - ControllerEngine* m_pEngine; -}; - -#endif // CONTROLLERENGINEJSPROXY_H diff --git a/src/controllers/engine/scriptconnectionjsproxy.cpp b/src/controllers/engine/scriptconnectionjsproxy.cpp deleted file mode 100644 index 7195e77ea94..00000000000 --- a/src/controllers/engine/scriptconnectionjsproxy.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "controllers/engine/scriptconnectionjsproxy.h" - -#include "controllers/engine/controllerengine.h" -#include "moc_scriptconnectionjsproxy.cpp" - -bool ScriptConnectionJSProxy::disconnect() { - // if the removeScriptConnection succeeded, the connection has been successfully disconnected - bool success = m_scriptConnection.controllerEngine->removeScriptConnection(m_scriptConnection); - m_isConnected = !success; - return success; -} - -void ScriptConnectionJSProxy::trigger() { - m_scriptConnection.controllerEngine->triggerScriptConnection(m_scriptConnection); -} diff --git a/src/controllers/hid/hidcontroller.cpp b/src/controllers/hid/hidcontroller.cpp index fbddbab6580..db784d61542 100644 --- a/src/controllers/hid/hidcontroller.cpp +++ b/src/controllers/hid/hidcontroller.cpp @@ -204,7 +204,7 @@ void HidController::sendBytesReport(QByteArray data, unsigned int reportID) { int result = hid_write(m_pHidDevice, (unsigned char*)data.constData(), data.size()); if (result == -1) { - if (ControllerDebug::enabled()) { + if (ControllerDebug::isEnabled()) { qWarning() << "Unable to send data to" << getName() << "serial #" << m_deviceInfo.serialNumber() << ":" << mixxx::convertWCStringToQString( diff --git a/src/controllers/midi/hss1394controller.cpp b/src/controllers/midi/hss1394controller.cpp index d35554bd29b..d73e2062124 100644 --- a/src/controllers/midi/hss1394controller.cpp +++ b/src/controllers/midi/hss1394controller.cpp @@ -35,7 +35,7 @@ void DeviceChannelListener::Process(const hss1394::uint8 *pBuffer, hss1394::uint if (i + 2 < uBufferSize) { note = pBuffer[i+1]; velocity = pBuffer[i+2]; - emit incomingData(status, note, velocity, timestamp); + emit receivedShortMessage(status, note, velocity, timestamp); } else { qWarning() << "Buffer underflow in DeviceChannelListener::Process()"; } @@ -43,8 +43,8 @@ void DeviceChannelListener::Process(const hss1394::uint8 *pBuffer, hss1394::uint break; default: // Handle platter messages and any others that are not 3 bytes - QByteArray outArray((char*)pBuffer,uBufferSize); - emit incomingData(outArray, timestamp); + QByteArray byteArray(reinterpret_cast(pBuffer), uBufferSize); + emit receivedSysex(byteArray, timestamp); i = uBufferSize; break; } @@ -106,19 +106,13 @@ int Hss1394Controller::open() { m_pChannelListener = new DeviceChannelListener(this, getName()); connect(m_pChannelListener, - QOverload::of(&DeviceChannelListener::incomingData), + &DeviceChannelListener::receivedShortMessage, this, - QOverload::of(&Hss1394Controller::receive)); + &Hss1394Controller::receivedShortMessage); connect(m_pChannelListener, - QOverload::of(&DeviceChannelListener::incomingData), + &DeviceChannelListener::receivedSysex, this, - QOverload::of(&Hss1394Controller::receive)); + &Hss1394Controller::receive); if (!m_pChannel->InstallChannelListener(m_pChannelListener)) { qDebug() << "HSS1394 channel listener could not be installed for device" << getName(); @@ -154,19 +148,13 @@ int Hss1394Controller::close() { } disconnect(m_pChannelListener, - QOverload::of(&DeviceChannelListener::incomingData), + &DeviceChannelListener::receivedShortMessage, this, - QOverload::of(&Hss1394Controller::receive)); + &Hss1394Controller::receivedShortMessage); disconnect(m_pChannelListener, - QOverload::of(&DeviceChannelListener::incomingData), + &DeviceChannelListener::receivedSysex, this, - QOverload::of(&Hss1394Controller::receive)); + &Hss1394Controller::receive); stopEngine(); MidiController::close(); diff --git a/src/controllers/midi/hss1394controller.h b/src/controllers/midi/hss1394controller.h index 622316eadfc..1f1cbf57e55 100644 --- a/src/controllers/midi/hss1394controller.h +++ b/src/controllers/midi/hss1394controller.h @@ -24,9 +24,11 @@ class DeviceChannelListener : public QObject, public hss1394::ChannelListener { void Disconnected(); void Reconnected(); signals: - void incomingData(unsigned char status, unsigned char control, unsigned char value, - mixxx::Duration timestamp); - void incomingData(const QByteArray& data, mixxx::Duration timestamp); + void receivedShortMessage(unsigned char status, + unsigned char control, + unsigned char value, + mixxx::Duration timestamp); + void receivedSysex(const QByteArray& data, mixxx::Duration timestamp); private: QString m_sName; diff --git a/src/controllers/midi/midicontroller.cpp b/src/controllers/midi/midicontroller.cpp index d484574f392..22c64458fce 100644 --- a/src/controllers/midi/midicontroller.cpp +++ b/src/controllers/midi/midicontroller.cpp @@ -51,9 +51,9 @@ bool MidiController::matchPreset(const PresetInfo& preset) { return false; } -bool MidiController::applyPreset(bool initializeScripts) { +bool MidiController::applyPreset() { // Handles the engine - bool result = Controller::applyPreset(initializeScripts); + bool result = Controller::applyPreset(); // Only execute this code if this is an output device if (isOutputDevice()) { @@ -109,7 +109,7 @@ void MidiController::createOutputHandlers() { qWarning() << errorLog; int deckNum = 0; - if (ControllerDebug::enabled()) { + if (ControllerDebug::isEnabled()) { failures.append(errorLog); } else if (PlayerManager::isDeckGroup(group, &deckNum)) { int numDecks = PlayerManager::numDecks(); @@ -195,20 +195,10 @@ void MidiController::commitTemporaryInputMappings() { m_temporaryInputMappings.clear(); } -void MidiController::receive(unsigned char status, unsigned char control, - unsigned char value, mixxx::Duration timestamp) { - QByteArray byteArray; - byteArray.append(status); - byteArray.append(control); - byteArray.append(value); - - ControllerEngine* pEngine = getEngine(); - // pEngine is nullptr in tests. - if (pEngine) { - pEngine->handleInput(byteArray, timestamp); - } - // legacy stuff below - +void MidiController::receivedShortMessage(unsigned char status, + unsigned char control, + unsigned char value, + mixxx::Duration timestamp) { // The rest of this function is for legacy mappings unsigned char channel = MidiUtils::channelFromStatus(status); unsigned char opCode = MidiUtils::opCodeFromStatus(status); @@ -256,7 +246,7 @@ void MidiController::processInputMapping(const MidiInputMapping& mapping, unsigned char opCode = MidiUtils::opCodeFromStatus(status); if (mapping.options.script) { - ControllerEngine* pEngine = getEngine(); + ControllerScriptEngineLegacy* pEngine = getScriptEngine(); if (pEngine == nullptr) { return; } @@ -481,8 +471,6 @@ double MidiController::computeValue( void MidiController::receive(const QByteArray& data, mixxx::Duration timestamp) { controllerDebug(MidiUtils::formatSysexMessage(getName(), data, timestamp)); - getEngine()->handleInput(data, timestamp); - // legacy stuff below MidiKey mappingKey(data.at(0), 0xFF); @@ -513,15 +501,11 @@ void MidiController::processInputMapping(const MidiInputMapping& mapping, mixxx::Duration timestamp) { // Custom script handler if (mapping.options.script) { - ControllerEngine* pEngine = getEngine(); + ControllerScriptEngineLegacy* pEngine = getScriptEngine(); if (pEngine == nullptr) { return; } - QJSValue function = pEngine->wrapFunctionCode(mapping.control.item, 2); - if (!pEngine->executeFunction(function, data)) { - qDebug() << "MidiController: Invalid script function" - << mapping.control.item; - } + pEngine->handleIncomingData(data); return; } qWarning() << "MidiController: No script function specified for" diff --git a/src/controllers/midi/midicontroller.h b/src/controllers/midi/midicontroller.h index f3e5a1f971a..aa46335c2a3 100644 --- a/src/controllers/midi/midicontroller.h +++ b/src/controllers/midi/midicontroller.h @@ -63,7 +63,8 @@ class MidiController : public Controller { } protected slots: - virtual void receive(unsigned char status, + virtual void receivedShortMessage( + unsigned char status, unsigned char control, unsigned char value, mixxx::Duration timestamp); @@ -72,27 +73,23 @@ class MidiController : public Controller { int close() override; private slots: - /// Apply the preset to the controller. - /// Initializes both controller engine and static output mappings. - /// - /// @param initializeScripts Can be set to false to skip script - /// initialization for unit tests. - /// @return Returns whether it was successful. - bool applyPreset(bool initializeScripts = false) override; + bool applyPreset() override; void learnTemporaryInputMappings(const MidiInputMappings& mappings); void clearTemporaryInputMappings(); void commitTemporaryInputMappings(); private: - void processInputMapping(const MidiInputMapping& mapping, - unsigned char status, - unsigned char control, - unsigned char value, - mixxx::Duration timestamp); - void processInputMapping(const MidiInputMapping& mapping, - const QByteArray& data, - mixxx::Duration timestamp); + void processInputMapping( + const MidiInputMapping& mapping, + unsigned char status, + unsigned char control, + unsigned char value, + mixxx::Duration timestamp); + void processInputMapping( + const MidiInputMapping& mapping, + const QByteArray& data, + mixxx::Duration timestamp); double computeValue(MidiOptions options, double _prevmidivalue, double _newmidivalue); void createOutputHandlers(); diff --git a/src/controllers/midi/portmidicontroller.cpp b/src/controllers/midi/portmidicontroller.cpp index 30a44cd0fda..d7eea579aa0 100644 --- a/src/controllers/midi/portmidicontroller.cpp +++ b/src/controllers/midi/portmidicontroller.cpp @@ -143,7 +143,7 @@ bool PortMidiController::poll() { if ((status & 0xF8) == 0xF8) { // Handle real-time MIDI messages at any time - receive(status, 0, 0, timestamp); + receivedShortMessage(status, 0, 0, timestamp); continue; } @@ -157,7 +157,7 @@ bool PortMidiController::poll() { //unsigned char channel = status & 0x0F; unsigned char note = Pm_MessageData1(m_midiBuffer[i].message); unsigned char velocity = Pm_MessageData2(m_midiBuffer[i].message); - receive(status, note, velocity, timestamp); + receivedShortMessage(status, note, velocity, timestamp); } } diff --git a/src/controllers/engine/colormapper.cpp b/src/controllers/scripting/colormapper.cpp similarity index 98% rename from src/controllers/engine/colormapper.cpp rename to src/controllers/scripting/colormapper.cpp index 5e6514ae411..2600b94dcf1 100644 --- a/src/controllers/engine/colormapper.cpp +++ b/src/controllers/scripting/colormapper.cpp @@ -1,4 +1,4 @@ -#include "controllers/engine/colormapper.h" +#include "controllers/scripting/colormapper.h" #include #include diff --git a/src/controllers/engine/colormapper.h b/src/controllers/scripting/colormapper.h similarity index 100% rename from src/controllers/engine/colormapper.h rename to src/controllers/scripting/colormapper.h diff --git a/src/controllers/engine/colormapperjsproxy.cpp b/src/controllers/scripting/colormapperjsproxy.cpp similarity index 97% rename from src/controllers/engine/colormapperjsproxy.cpp rename to src/controllers/scripting/colormapperjsproxy.cpp index 940f6ad4b4c..582bb2aa372 100644 --- a/src/controllers/engine/colormapperjsproxy.cpp +++ b/src/controllers/scripting/colormapperjsproxy.cpp @@ -1,4 +1,4 @@ -#include "controllers/engine/colormapperjsproxy.h" +#include "controllers/scripting/colormapperjsproxy.h" #include "moc_colormapperjsproxy.cpp" diff --git a/src/controllers/engine/colormapperjsproxy.h b/src/controllers/scripting/colormapperjsproxy.h similarity index 96% rename from src/controllers/engine/colormapperjsproxy.h rename to src/controllers/scripting/colormapperjsproxy.h index 13bf3b2f5dd..8b6cd79c423 100644 --- a/src/controllers/engine/colormapperjsproxy.h +++ b/src/controllers/scripting/colormapperjsproxy.h @@ -2,7 +2,7 @@ #include #include -#include "controllers/engine/colormapper.h" +#include "controllers/scripting/colormapper.h" /// ColorMapperJSProxy is a wrapper class that exposes ColorMapper via the /// QJSEngine and makes it possible to create and use ColorMapper object from diff --git a/src/controllers/scripting/controllerscriptenginebase.cpp b/src/controllers/scripting/controllerscriptenginebase.cpp new file mode 100644 index 00000000000..7c8a7c407d6 --- /dev/null +++ b/src/controllers/scripting/controllerscriptenginebase.cpp @@ -0,0 +1,218 @@ +#include "controllers/scripting/controllerscriptenginebase.h" + +#include "control/controlobject.h" +#include "controllers/controller.h" +#include "controllers/controllerdebug.h" +#include "controllers/scripting/colormapperjsproxy.h" +#include "errordialoghandler.h" +#include "mixer/playermanager.h" + +ControllerScriptEngineBase::ControllerScriptEngineBase(Controller* controller) + : m_bDisplayingExceptionDialog(false), + m_pJSEngine(nullptr), + m_pController(controller), + m_bTesting(false) { + // Handle error dialog buttons + qRegisterMetaType("QMessageBox::StandardButton"); +} + +bool ControllerScriptEngineBase::initialize() { + VERIFY_OR_DEBUG_ASSERT(!m_pJSEngine) { + return false; + } + + // Create the Script Engine + m_pJSEngine = std::make_shared(this); + + QJSValue engineGlobalObject = m_pJSEngine->globalObject(); + + QJSValue mapper = m_pJSEngine->newQMetaObject( + &ColorMapperJSProxy::staticMetaObject); + engineGlobalObject.setProperty("ColorMapper", mapper); + + if (m_pController) { + qDebug() << "Controller in script engine is:" + << m_pController->getName(); + + ControllerJSProxy* controllerProxy = m_pController->jsProxy(); + + // Make the Controller instance available to scripts + engineGlobalObject.setProperty( + "controller", m_pJSEngine->newQObject(controllerProxy)); + + // ...under the legacy name as well + engineGlobalObject.setProperty( + "midi", m_pJSEngine->newQObject(controllerProxy)); + } + + return true; +} + +void ControllerScriptEngineBase::shutdown() { + DEBUG_ASSERT(m_pJSEngine.use_count() == 1); + m_pJSEngine.reset(); +} + +void ControllerScriptEngineBase::reload() { + shutdown(); + initialize(); +} + +bool ControllerScriptEngineBase::executeFunction( + QJSValue functionObject, const QJSValueList& args) { + // This function is called from outside the controller engine, so we can't + // use VERIFY_OR_DEBUG_ASSERT here + if (!m_pJSEngine) { + return false; + } + + if (functionObject.isError()) { + qDebug() << "ControllerScriptHandlerBase::executeFunction:" + << functionObject.toString(); + return false; + } + + // If it's not a function, we're done. + if (!functionObject.isCallable()) { + qDebug() << "ControllerScriptHandlerBase::executeFunction:" + << functionObject.toVariant() << "Not a function"; + return false; + } + + // If it does happen to be a function, call it. + QJSValue returnValue = functionObject.call(args); + if (returnValue.isError()) { + showScriptExceptionDialog(returnValue); + return false; + } + return true; +} + +void ControllerScriptEngineBase::showScriptExceptionDialog( + const QJSValue& evaluationResult, bool bFatalError) { + VERIFY_OR_DEBUG_ASSERT(evaluationResult.isError()) { + return; + } + + QString errorMessage = evaluationResult.toString(); + QString line = evaluationResult.property("lineNumber").toString(); + QString backtrace = evaluationResult.property("stack").toString(); + QString filename = evaluationResult.property("fileName").toString(); + + QString errorText; + if (filename.isEmpty()) { + errorText = QString("Uncaught exception at line %1 in passed code.") + .arg(line); + } else { + errorText = QString("Uncaught exception at line %1 in file %2.") + .arg(line, filename); + } + + errorText += QStringLiteral("\n\nException:\n ") + errorMessage; + + // Do not include backtrace in dialog key because it might contain midi + // slider values that will differ most of the time. This would break + // the "Ignore" feature of the error dialog. + QString key = errorText; + qWarning() << "ControllerScriptHandlerBase:" << errorText; + + // Add backtrace to the error details + errorText += QStringLiteral("\n\nBacktrace:\n") + backtrace; + + if (!m_bDisplayingExceptionDialog) { + scriptErrorDialog(errorText, key, bFatalError); + } +} + +void ControllerScriptEngineBase::scriptErrorDialog( + const QString& detailedError, const QString& key, bool bFatalError) { + if (m_bTesting) { + return; + } + + ErrorDialogProperties* props = + ErrorDialogHandler::instance()->newDialogProperties(); + + QString additionalErrorText; + if (bFatalError) { + additionalErrorText = + tr("The functionality provided by this controller mapping will " + "be disabled until the issue has been resolved."); + } else { + // This happens when an exception is throws in an input handler (e. g. + // when pressing a button on the midi controller). In case you ignore + // the issue, the button might not work if there's a bug in the + // mapping, but the other buttons probably will. + additionalErrorText = + tr("You can ignore this error for this session but " + "you may experience erratic behavior.") + + QString("
") + + tr("Try to recover by resetting your controller."); + } + + props->setType(DLG_WARNING); + props->setTitle(tr("Controller Preset Error")); + props->setText(tr("The preset for your controller \"%1\" is not working properly.") + .arg(m_pController->getName())); + props->setInfoText(QStringLiteral("") + + tr("The script code needs to be fixed.") + QStringLiteral("

") + + additionalErrorText + QStringLiteral("

")); + + // Add "Details" text and set monospace font since they may contain + // backtraces and code. + props->setDetails(detailedError, true); + + // To prevent multiple windows for the same error + props->setKey(key); + + if (bFatalError) { + props->addButton(QMessageBox::Close); + props->setDefaultButton(QMessageBox::Close); + props->setEscapeButton(QMessageBox::Close); + } else { + // Allow user to suppress further notifications about this particular + // error + props->addButton(QMessageBox::Ignore); + props->addButton(QMessageBox::Retry); + props->setDefaultButton(QMessageBox::Ignore); + props->setEscapeButton(QMessageBox::Ignore); + } + props->setModal(false); + + if (ErrorDialogHandler::instance()->requestErrorDialog(props)) { + m_bDisplayingExceptionDialog = true; + // Enable custom handling of the dialog buttons + connect(ErrorDialogHandler::instance(), + &ErrorDialogHandler::stdButtonClicked, + this, + &ControllerScriptEngineBase::errorDialogButton); + } +} + +void ControllerScriptEngineBase::errorDialogButton( + const QString& key, QMessageBox::StandardButton clickedButton) { + Q_UNUSED(key); + + m_bDisplayingExceptionDialog = false; + // Something was clicked, so disable this signal now + disconnect(ErrorDialogHandler::instance(), + &ErrorDialogHandler::stdButtonClicked, + this, + &ControllerScriptEngineBase::errorDialogButton); + + if (clickedButton == QMessageBox::Retry) { + reload(); + } +} + +void ControllerScriptEngineBase::throwJSError(const QString& message) { +#if QT_VERSION < QT_VERSION_CHECK(5, 12, 0) + QString errorText = tr("Uncaught exception: %1").arg(message); + qWarning() << "ControllerEngine:" << errorText; + if (!m_bDisplayingExceptionDialog) { + scriptErrorDialog(errorText, errorText); + } +#else + m_pJSEngine->throwError(message); +#endif +} diff --git a/src/controllers/scripting/controllerscriptenginebase.h b/src/controllers/scripting/controllerscriptenginebase.h new file mode 100644 index 00000000000..8760673ce93 --- /dev/null +++ b/src/controllers/scripting/controllerscriptenginebase.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "controllers/controllerpreset.h" +#include "util/duration.h" + +class Controller; +class EvaluationException; + +/// ControllerScriptEngineBase manages the JavaScript engine for controller scripts. +/// ControllerScriptModuleEngine implements the current system using JS modules. +/// ControllerScriptEngineLegacy implements the legacy hybrid JS/XML system. +class ControllerScriptEngineBase : public QObject { + Q_OBJECT + public: + explicit ControllerScriptEngineBase(Controller* controller); + virtual ~ControllerScriptEngineBase() override = default; + + virtual bool initialize(); + + bool executeFunction(QJSValue functionObject, const QJSValueList& arguments); + + /// Shows a UI dialog notifying of a script evaluation error. + /// Precondition: QJSValue.isError() == true + void showScriptExceptionDialog(const QJSValue& evaluationResult, bool bFatal = false); + void throwJSError(const QString& message); + + inline void setTesting(bool testing) { + m_bTesting = testing; + }; + + bool isTesting() const { + return m_bTesting; + } + + protected: + virtual void shutdown(); + + void scriptErrorDialog(const QString& detailedError, const QString& key, bool bFatal = false); + + bool m_bDisplayingExceptionDialog; + std::shared_ptr m_pJSEngine; + + Controller* m_pController; + + bool m_bTesting; + + protected slots: + void reload(); + + private slots: + void errorDialogButton(const QString& key, QMessageBox::StandardButton button); + + friend class ColorMapperJSProxy; +}; diff --git a/src/controllers/scripting/controllerscriptmoduleengine.cpp b/src/controllers/scripting/controllerscriptmoduleengine.cpp new file mode 100644 index 00000000000..540c19c980e --- /dev/null +++ b/src/controllers/scripting/controllerscriptmoduleengine.cpp @@ -0,0 +1,52 @@ +#include "controllers/scripting/controllerscriptmoduleengine.h" + +ControllerScriptModuleEngine::ControllerScriptModuleEngine(Controller* controller) + : ControllerScriptEngineBase(controller) { + connect(&m_fileWatcher, + &QFileSystemWatcher::fileChanged, + this, + &ControllerScriptModuleEngine::reload); +} + +ControllerScriptModuleEngine::~ControllerScriptModuleEngine() { + shutdown(); +} + +bool ControllerScriptModuleEngine::initialize() { + ControllerScriptEngineBase::initialize(); +#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) + m_pJSEngine->installExtensions(QJSEngine::ConsoleExtension); + // TODO: Add new ControlObject JS API to scripting environment. + + QJSValue mod = + m_pJSEngine->importModule(m_moduleFileInfo.absoluteFilePath()); + if (mod.isError()) { + showScriptExceptionDialog(mod); + shutdown(); + return false; + } + + if (!m_fileWatcher.addPath(m_moduleFileInfo.absoluteFilePath())) { + qWarning() << "Failed to watch script file" << m_moduleFileInfo.absoluteFilePath(); + } + + QJSValue initFunction = mod.property("init"); + if (!executeFunction(initFunction, QJSValueList{})) { + shutdown(); + return false; + } + + QJSValue shutdownFunction = mod.property("shutdown"); + if (shutdownFunction.isCallable()) { + m_shutdownFunction = shutdownFunction; + } else { + qDebug() << "Module exports no shutdown function."; + } +#endif + return true; +} + +void ControllerScriptModuleEngine::shutdown() { + executeFunction(m_shutdownFunction, QJSValueList()); + ControllerScriptEngineBase::shutdown(); +} diff --git a/src/controllers/scripting/controllerscriptmoduleengine.h b/src/controllers/scripting/controllerscriptmoduleengine.h new file mode 100644 index 00000000000..76b14ee774c --- /dev/null +++ b/src/controllers/scripting/controllerscriptmoduleengine.h @@ -0,0 +1,25 @@ +#pragma once + +#include "controllers/scripting/controllerscriptenginebase.h" + +/// ControllerScriptModuleEngine loads and executes script module files for controller mappings. +class ControllerScriptModuleEngine : public ControllerScriptEngineBase { + Q_OBJECT + public: + explicit ControllerScriptModuleEngine(Controller* controller); + ~ControllerScriptModuleEngine() override; + + bool initialize() override; + + void setModuleFileInfo(const QFileInfo& moduleFileInfo) { + m_moduleFileInfo = moduleFileInfo; + } + + private: + void shutdown() override; + + QJSValue m_shutdownFunction; + + QFileInfo m_moduleFileInfo; + QFileSystemWatcher m_fileWatcher; +}; diff --git a/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp new file mode 100644 index 00000000000..cdac16e0ce9 --- /dev/null +++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp @@ -0,0 +1,254 @@ +#include "controllers/scripting/legacy/controllerscriptenginelegacy.h" + +#include "control/controlobject.h" +#include "controllers/controller.h" +#include "controllers/controllerdebug.h" +#include "controllers/scripting/colormapperjsproxy.h" +#include "controllers/scripting/legacy/controllerscriptinterfacelegacy.h" +#include "errordialoghandler.h" +#include "mixer/playermanager.h" + +ControllerScriptEngineLegacy::ControllerScriptEngineLegacy(Controller* controller) + : ControllerScriptEngineBase(controller) { + connect(&m_fileWatcher, + &QFileSystemWatcher::fileChanged, + this, + &ControllerScriptEngineLegacy::reload); +} + +ControllerScriptEngineLegacy::~ControllerScriptEngineLegacy() { + shutdown(); +} + +bool ControllerScriptEngineLegacy::callFunctionOnObjects( + const QList& scriptFunctionPrefixes, + const QString& function, + const QJSValueList& args, + bool bFatalError) { + VERIFY_OR_DEBUG_ASSERT(m_pJSEngine) { + return false; + } + + const QJSValue global = m_pJSEngine->globalObject(); + + bool success = true; + for (const QString& prefixName : scriptFunctionPrefixes) { + QJSValue prefix = global.property(prefixName); + if (!prefix.isObject()) { + qWarning() << "No" << prefixName << "object in script"; + continue; + } + + QJSValue init = prefix.property(function); + if (!init.isCallable()) { + qWarning() << prefixName << "has no" + << function << " method"; + continue; + } + controllerDebug("Executing" + << prefixName << "." << function); + QJSValue result = init.callWithInstance(prefix, args); + if (result.isError()) { + showScriptExceptionDialog(result, bFatalError); + success = false; + } + } + return success; +} + +QJSValue ControllerScriptEngineLegacy::wrapFunctionCode( + const QString& codeSnippet, int numberOfArgs) { + // This function is called from outside the controller engine, so we can't + // use VERIFY_OR_DEBUG_ASSERT here + if (!m_pJSEngine) { + return QJSValue(); + } + + QJSValue wrappedFunction; + + const auto it = m_scriptWrappedFunctionCache.constFind(codeSnippet); + if (it != m_scriptWrappedFunctionCache.constEnd()) { + wrappedFunction = it.value(); + } else { + QStringList wrapperArgList; + wrapperArgList.reserve(numberOfArgs); + for (int i = 1; i <= numberOfArgs; i++) { + wrapperArgList << QString("arg%1").arg(i); + } + QString wrapperArgs = wrapperArgList.join(","); + QString wrappedCode = QStringLiteral("(function (") + wrapperArgs + + QStringLiteral(") { (") + codeSnippet + QStringLiteral(")(") + + wrapperArgs + QStringLiteral("); })"); + + wrappedFunction = m_pJSEngine->evaluate(wrappedCode); + if (wrappedFunction.isError()) { + showScriptExceptionDialog(wrappedFunction); + } + m_scriptWrappedFunctionCache[codeSnippet] = wrappedFunction; + } + return wrappedFunction; +} + +bool ControllerScriptEngineLegacy::initialize() { + if (!ControllerScriptEngineBase::initialize()) { + return false; + } + + // Binary data is passed from the Controller as a QByteArray, which + // QJSEngine::toScriptValue converts to an ArrayBuffer in JavaScript. + // ArrayBuffer cannot be accessed with the [] operator in JS; it needs + // to be converted to a typed array (Uint8Array in this case) first. + // This function generates a wrapper function from a JS callback to do + // that conversion automatically. + m_makeArrayBufferWrapperFunction = m_pJSEngine->evaluate(QStringLiteral( + // arg2 is the timestamp for ControllerScriptModuleEngine. + // In ControllerScriptEngineLegacy it is the length of the array. + "(function(callback) {" + " return function(arrayBuffer, arg2) {" + " callback(new Uint8Array(arrayBuffer), arg2);" + " };" + "})")); + + // Make this ControllerScriptHandler instance available to scripts as 'engine'. + QJSValue engineGlobalObject = m_pJSEngine->globalObject(); + ControllerScriptInterfaceLegacy* legacyScriptInterface = + new ControllerScriptInterfaceLegacy(this); + engineGlobalObject.setProperty( + "engine", m_pJSEngine->newQObject(legacyScriptInterface)); + + for (const ControllerPreset::ScriptFileInfo& script : std::as_const(m_scriptFiles)) { + if (!evaluateScriptFile(script.file)) { + shutdown(); + return false; + } + if (!script.functionPrefix.isEmpty()) { + m_scriptFunctionPrefixes.append(script.functionPrefix); + } + } + + // For testing, do not actually initialize the scripts, just check for + // syntax errors above. + if (m_bTesting) { + return true; + } + + for (QString functionName : std::as_const(m_scriptFunctionPrefixes)) { + if (functionName.isEmpty()) { + continue; + } + functionName.append(QStringLiteral(".incomingData")); + m_incomingDataFunctions.append( + wrapArrayBufferCallback( + wrapFunctionCode(functionName, 2))); + } + + QJSValueList args; + if (m_pController) { + args << QJSValue(m_pController->getName()); + } else { // m_pController is nullptr in tests. + args << QJSValue(); + } + args << QJSValue(ControllerDebug::isEnabled()); + if (!callFunctionOnObjects(m_scriptFunctionPrefixes, "init", args, true)) { + shutdown(); + return false; + } + + return true; +} + +void ControllerScriptEngineLegacy::shutdown() { + callFunctionOnObjects(m_scriptFunctionPrefixes, "shutdown"); + m_scriptWrappedFunctionCache.clear(); + m_incomingDataFunctions.clear(); + m_scriptFunctionPrefixes.clear(); + ControllerScriptEngineBase::shutdown(); +} + +bool ControllerScriptEngineLegacy::handleIncomingData(const QByteArray& data) { + // This function is called from outside the controller engine, so we can't + // use VERIFY_OR_DEBUG_ASSERT here + if (!m_pJSEngine) { + return false; + } + + QJSValueList args; + args << m_pJSEngine->toScriptValue(data); + args << QJSValue(data.size()); + + for (const QJSValue& function : std::as_const(m_incomingDataFunctions)) { + ControllerScriptEngineBase::executeFunction(function, args); + } + + return true; +} + +bool ControllerScriptEngineLegacy::evaluateScriptFile(const QFileInfo& scriptFile) { + VERIFY_OR_DEBUG_ASSERT(m_pJSEngine) { + return false; + } + + if (!scriptFile.exists()) { + qWarning() << "File does not exist:" + << scriptFile.absoluteFilePath(); + return false; + } + + // If the script is invalid, it should be watched so the user can fix it + // without having to restart Mixxx. So, add it to the watcher before + // evaluating it. + if (!m_fileWatcher.addPath(scriptFile.absoluteFilePath())) { + qWarning() << "Failed to watch script file" << scriptFile.absoluteFilePath(); + }; + + qDebug() << "Loading" + << scriptFile.absoluteFilePath(); + + // Read in the script file + QString filename = scriptFile.absoluteFilePath(); + QFile input(filename); + if (!input.open(QIODevice::ReadOnly)) { + qWarning() << QString( + "Problem opening the script file: %1, " + "error # %2, %3") + .arg(filename, + QString::number(input.error()), + input.errorString()); + // Set up error dialog + ErrorDialogProperties* props = ErrorDialogHandler::instance()->newDialogProperties(); + props->setType(DLG_WARNING); + props->setTitle(tr("Controller Mapping File Problem")); + props->setText(tr("The mapping for controller \"%1\" cannot be opened.") + .arg(m_pController->getName())); + props->setInfoText( + tr("The functionality provided by this controller mapping will " + "be disabled until the issue has been resolved.")); + + // We usually don't translate the details field, but the cause of + // this problem lies in the user's system (e.g. a permission + // issue). Translating this will help users to fix the issue even + // when they don't speak english. + props->setDetails(tr("File:") + QStringLiteral(" ") + filename + + QStringLiteral("\n") + tr("Error:") + QStringLiteral(" ") + + input.errorString()); + + // Ask above layer to display the dialog & handle user response + ErrorDialogHandler::instance()->requestErrorDialog(props); + return false; + } + + QString scriptCode = QString(input.readAll()) + QStringLiteral("\n"); + input.close(); + + QJSValue scriptFunction = m_pJSEngine->evaluate(scriptCode, filename); + if (scriptFunction.isError()) { + showScriptExceptionDialog(scriptFunction, true); + return false; + } + + return true; +} + +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 new file mode 100644 index 00000000000..b488e3e547b --- /dev/null +++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include + +#include "controllers/controllerpreset.h" +#include "controllers/scripting/controllerscriptenginebase.h" + +/// ControllerScriptEngineLegacy loads and executes controller scripts for the legacy +/// JS/XML hybrid controller mapping system. +class ControllerScriptEngineLegacy : public ControllerScriptEngineBase { + Q_OBJECT + public: + explicit ControllerScriptEngineLegacy(Controller* controller); + ~ControllerScriptEngineLegacy() override; + + bool initialize() override; + + bool handleIncomingData(const QByteArray& data); + + /// Wrap a string of JS code in an anonymous function. This allows any JS + /// string that evaluates to a function to be used in MIDI mapping XML files + /// and ensures the function is executed with the correct 'this' object. + QJSValue wrapFunctionCode(const QString& codeSnippet, int numberOfArgs); + + public slots: + void setScriptFiles(const QList& scripts) { + m_fileWatcher.removePaths(m_fileWatcher.files()); + m_scriptFiles = scripts; + } + + private: + bool evaluateScriptFile(const QFileInfo& scriptFile); + void shutdown() override; + + QJSValue wrapArrayBufferCallback(const QJSValue& callback); + bool callFunctionOnObjects(const QList& scriptFunctionPrefixes, + const QString&, + const QJSValueList& args = QJSValueList(), + bool bFatalError = false); + + QJSValue m_makeArrayBufferWrapperFunction; + QList m_scriptFunctionPrefixes; + QList m_incomingDataFunctions; + QHash m_scriptWrappedFunctionCache; + QList m_scriptFiles; + + QFileSystemWatcher m_fileWatcher; + + // There is lots of tight coupling between ControllerScriptEngineLegacy + // and ControllerScriptInterface. This is probably not worth improving in legacy code. + friend class ControllerScriptInterfaceLegacy; + std::shared_ptr jsEngine() const { + return m_pJSEngine; + } + + friend class ControllerScriptEngineLegacyTest; +}; diff --git a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp new file mode 100644 index 00000000000..8215b7d2804 --- /dev/null +++ b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp @@ -0,0 +1,876 @@ +#include "controllerscriptinterfacelegacy.h" + +#include "control/controlobject.h" +#include "control/controlobjectscript.h" +#include "controllers/controllerdebug.h" +#include "controllers/scripting/legacy/controllerscriptenginelegacy.h" +#include "controllers/scripting/legacy/scriptconnectionjsproxy.h" +#include "mixer/playermanager.h" +#include "util/math.h" +#include "util/time.h" + +namespace { +const int kDecks = 16; + +// Use 1ms for the Alpha-Beta dt. We're assuming the OS actually gives us a 1ms +// timer. +const int kScratchTimerMs = 1; +const double kAlphaBetaDt = kScratchTimerMs / 1000.0; +} // anonymous namespace + +ControllerScriptInterfaceLegacy::ControllerScriptInterfaceLegacy( + ControllerScriptEngineLegacy* m_pEngine) + : m_pScriptEngineLegacy(m_pEngine) { + // Pre-allocate arrays for average number of virtual decks + m_intervalAccumulator.resize(kDecks); + m_lastMovement.resize(kDecks); + m_dx.resize(kDecks); + m_rampTo.resize(kDecks); + m_ramp.resize(kDecks); + m_scratchFilters.resize(kDecks); + m_rampFactor.resize(kDecks); + m_brakeActive.resize(kDecks); + m_softStartActive.resize(kDecks); + // Initialize arrays used for testing and pointers + for (int i = 0; i < kDecks; ++i) { + m_dx[i] = 0.0; + m_scratchFilters[i] = new AlphaBetaFilter(); + m_ramp[i] = false; + } +} + +ControllerScriptInterfaceLegacy::~ControllerScriptInterfaceLegacy() { + // Stop all timers + QMutableHashIterator i(m_timers); + while (i.hasNext()) { + i.next(); + stopTimer(i.key()); + } + + // Prevents leaving decks in an unstable state + // if the controller is shut down while scratching + QHashIterator it(m_scratchTimers); + while (it.hasNext()) { + it.next(); + qDebug() << "Aborting scratching on deck" << it.value(); + // Clear scratch2_enable. PlayerManager::groupForDeck is 0-indexed. + QString group = PlayerManager::groupForDeck(it.value() - 1); + ControlObjectScript* pScratch2Enable = + getControlObjectScript(group, "scratch2_enable"); + if (pScratch2Enable != nullptr) { + pScratch2Enable->set(0); + } + } + + for (int i = 0; i < kDecks; ++i) { + delete m_scratchFilters[i]; + m_scratchFilters[i] = nullptr; + } + + // Free all the ControlObjectScripts + { + auto it = m_controlCache.begin(); + while (it != m_controlCache.end()) { + qDebug() + << "Deleting ControlObjectScript" + << it.key().group + << it.key().item; + delete it.value(); + // Advance iterator + it = m_controlCache.erase(it); + } + } +} + +ControlObjectScript* ControllerScriptInterfaceLegacy::getControlObjectScript( + const QString& group, const QString& name) { + ConfigKey key = ConfigKey(group, name); + ControlObjectScript* coScript = m_controlCache.value(key, nullptr); + if (coScript == nullptr) { + // create COT + coScript = new ControlObjectScript(key, this); + if (coScript->valid()) { + m_controlCache.insert(key, coScript); + } else { + delete coScript; + coScript = nullptr; + } + } + return coScript; +} + +double ControllerScriptInterfaceLegacy::getValue(const QString& group, const QString& name) { + ControlObjectScript* coScript = getControlObjectScript(group, name); + if (coScript == nullptr) { + qWarning() << "Unknown control" << group << name + << ", returning 0.0"; + return 0.0; + } + return coScript->get(); +} + +void ControllerScriptInterfaceLegacy::setValue( + const QString& group, const QString& name, double newValue) { + if (isnan(newValue)) { + qWarning() << "script setting [" << group << "," + << name << "] to NotANumber, ignoring."; + return; + } + + ControlObjectScript* coScript = getControlObjectScript(group, name); + + if (coScript != nullptr) { + ControlObject* pControl = ControlObject::getControl( + coScript->getKey(), ControllerDebug::controlFlags()); + if (pControl && + !m_st.ignore( + pControl, coScript->getParameterForValue(newValue))) { + coScript->slotSet(newValue); + } + } +} + +double ControllerScriptInterfaceLegacy::getParameter(const QString& group, const QString& name) { + ControlObjectScript* coScript = getControlObjectScript(group, name); + if (coScript == nullptr) { + qWarning() << "Unknown control" << group << name + << ", returning 0.0"; + return 0.0; + } + return coScript->getParameter(); +} + +void ControllerScriptInterfaceLegacy::setParameter( + const QString& group, const QString& name, double newParameter) { + if (isnan(newParameter)) { + qWarning() << "script setting [" << group << "," + << name << "] to NotANumber, ignoring."; + return; + } + + ControlObjectScript* coScript = getControlObjectScript(group, name); + + if (coScript != nullptr) { + ControlObject* pControl = ControlObject::getControl( + coScript->getKey(), ControllerDebug::controlFlags()); + if (pControl && !m_st.ignore(pControl, newParameter)) { + coScript->setParameter(newParameter); + } + } +} + +double ControllerScriptInterfaceLegacy::getParameterForValue( + const QString& group, const QString& name, double value) { + if (isnan(value)) { + qWarning() << "script setting [" << group << "," + << name << "] to NotANumber, ignoring."; + return 0.0; + } + + ControlObjectScript* coScript = getControlObjectScript(group, name); + + if (coScript == nullptr) { + qWarning() << "Unknown control" << group << name + << ", returning 0.0"; + return 0.0; + } + + return coScript->getParameterForValue(value); +} + +void ControllerScriptInterfaceLegacy::reset(const QString& group, const QString& name) { + ControlObjectScript* coScript = getControlObjectScript(group, name); + if (coScript != nullptr) { + coScript->reset(); + } +} + +double ControllerScriptInterfaceLegacy::getDefaultValue(const QString& group, const QString& name) { + ControlObjectScript* coScript = getControlObjectScript(group, name); + + if (coScript == nullptr) { + qWarning() << "Unknown control" << group << name + << ", returning 0.0"; + return 0.0; + } + + return coScript->getDefault(); +} + +double ControllerScriptInterfaceLegacy::getDefaultParameter( + const QString& group, const QString& name) { + ControlObjectScript* coScript = getControlObjectScript(group, name); + + if (coScript == nullptr) { + qWarning() << "Unknown control" << group << name + << ", returning 0.0"; + return 0.0; + } + + return coScript->getParameterForValue(coScript->getDefault()); +} + +QJSValue ControllerScriptInterfaceLegacy::makeConnection( + const QString& group, const QString& name, const QJSValue& callback) { + auto pJsEngine = m_pScriptEngineLegacy->jsEngine(); + VERIFY_OR_DEBUG_ASSERT(pJsEngine) { + return QJSValue(); + } + + ControlObjectScript* coScript = getControlObjectScript(group, name); + if (coScript == nullptr) { + // The test setups do not run all of Mixxx, so ControlObjects not + // existing during tests is okay. + if (!m_pScriptEngineLegacy->isTesting()) { + m_pScriptEngineLegacy->throwJSError( + "script tried to connect to " + "ControlObject (" + + group + ", " + name + ") which is non-existent."); + } + return QJSValue(); + } + + if (!callback.isCallable()) { + m_pScriptEngineLegacy->throwJSError("Tried to connect (" + group + ", " + name + + ")" + + " to an invalid callback. Make sure that your code contains no " + "syntax errors."); + return QJSValue(); + } + + ScriptConnection connection; + connection.key = ConfigKey(group, name); + connection.engineJSProxy = this; + connection.controllerEngine = m_pScriptEngineLegacy; + connection.callback = callback; + connection.id = QUuid::createUuid(); + + if (coScript->addScriptConnection(connection)) { + return pJsEngine->newQObject( + new ScriptConnectionJSProxy(connection)); + } + + return QJSValue(); +} + +bool ControllerScriptInterfaceLegacy::removeScriptConnection( + const ScriptConnection& connection) { + ControlObjectScript* coScript = + getControlObjectScript(connection.key.group, connection.key.item); + + if (m_pScriptEngineLegacy->jsEngine() == nullptr || coScript == nullptr) { + return false; + } + + return coScript->removeScriptConnection(connection); +} + +void ControllerScriptInterfaceLegacy::triggerScriptConnection( + const ScriptConnection& connection) { + VERIFY_OR_DEBUG_ASSERT(m_pScriptEngineLegacy->jsEngine()) { + return; + } + + ControlObjectScript* coScript = + getControlObjectScript(connection.key.group, connection.key.item); + if (coScript == nullptr) { + return; + } + + connection.executeCallback(coScript->get()); +} + +// This function is a legacy version of makeConnection with several alternate +// ways of invoking it. The callback function can be passed either as a string of +// JavaScript code that evaluates to a function or an actual JavaScript function. +// If "true" is passed as a 4th parameter, all connections to the ControlObject +// are removed. If a ScriptConnectionInvokableWrapper is passed instead of a callback, +// it is disconnected. +// WARNING: These behaviors are quirky and confusing, so if you change this function, +// be sure to run the ControllerScriptInterfaceTest suite to make sure you do not break old scripts. +QJSValue ControllerScriptInterfaceLegacy::connectControl(const QString& group, + const QString& name, + const QJSValue& passedCallback, + bool disconnect) { + // The passedCallback may or may not actually be a function, so when + // the actual callback function is found, store it in this variable. + QJSValue actualCallbackFunction; + + if (passedCallback.isCallable()) { + if (!disconnect) { + // skip all the checks below and just make the connection + return makeConnection(group, name, passedCallback); + } + actualCallbackFunction = passedCallback; + } + + auto pJsEngine = m_pScriptEngineLegacy->jsEngine(); + + ControlObjectScript* coScript = getControlObjectScript(group, name); + // This check is redundant with makeConnection, but the + // ControlObjectScript is also needed here to check for duplicate connections. + if (coScript == nullptr) { + // The test setups do not run all of Mixxx, so ControlObjects not + // existing during tests is okay. + if (!m_pScriptEngineLegacy->isTesting()) { + if (disconnect) { + m_pScriptEngineLegacy->throwJSError( + "script tried to disconnect from " + "ControlObject (" + + group + ", " + name + ") which is non-existent."); + } else { + m_pScriptEngineLegacy->throwJSError( + "script tried to connect to " + "ControlObject (" + + group + ", " + name + ") which is non-existent."); + } + } + // This is inconsistent with other failures, which return false. + // QJSValue() with no arguments is undefined in JavaScript. + return QJSValue(); + } + + if (passedCallback.isString()) { + // This check is redundant with makeConnection, but it must be done here + // before evaluating the code string. + VERIFY_OR_DEBUG_ASSERT(pJsEngine != nullptr) { + return QJSValue(false); + } + + actualCallbackFunction = + pJsEngine->evaluate(passedCallback.toString()); + + if (!actualCallbackFunction.isCallable()) { + QString sErrorMessage( + "Invalid connection callback provided to " + "engine.connectControl."); + if (actualCallbackFunction.isError()) { + sErrorMessage.append("\n" + actualCallbackFunction.toString()); + } + m_pScriptEngineLegacy->throwJSError(sErrorMessage); + return QJSValue(false); + } + + if (coScript->countConnections() > 0 && !disconnect) { + // This is inconsistent with the behavior when passing the callback as + // a function, but keep the old behavior to make sure old scripts do + // not break. + ScriptConnection connection = coScript->firstConnection(); + + qWarning() << "Tried to make duplicate connection between (" + + group + ", " + name + ") and " + + passedCallback.toString() + + " but this is not allowed when passing a callback " + "as a string. " + + "If you actually want to create duplicate " + "connections, " + + "use engine.makeConnection. Returning reference to " + "connection " + + connection.id.toString(); + + return pJsEngine->newQObject( + new ScriptConnectionJSProxy(connection)); + } + } else if (passedCallback.isQObject()) { + // Assume a ScriptConnection and assume that the script author + // wants to disconnect it, regardless of the disconnect parameter + // and regardless of whether it is connected to the same ControlObject + // specified by the first two parameters to this function. + QObject* qobject = passedCallback.toQObject(); + const QMetaObject* qmeta = qobject->metaObject(); + + qWarning() << "QObject passed to engine.connectControl. Assuming it is" + << "a connection object to disconnect and returning false."; + if (!strcmp(qmeta->className(), "ScriptConnectionJSProxy")) { + ScriptConnectionJSProxy* proxy = (ScriptConnectionJSProxy*)qobject; + proxy->disconnect(); + } + return QJSValue(false); + } + + // Support removing connections by passing "true" as the last parameter + // to this function, regardless of whether the callback is provided + // as a function or a string. + if (disconnect) { + // There is no way to determine which + // ScriptConnection to disconnect unless the script calls + // ScriptConnectionInvokableWrapper::disconnect(), so + // disconnect all ScriptConnections connected to the + // callback function, even though there may be multiple connections. + coScript->disconnectAllConnectionsToFunction(actualCallbackFunction); + return QJSValue(true); + } + + // If execution gets this far without returning, make + // a new connection to actualCallbackFunction. + return makeConnection(group, name, actualCallbackFunction); +} + +void ControllerScriptInterfaceLegacy::trigger(const QString& group, const QString& name) { + ControlObjectScript* coScript = getControlObjectScript(group, name); + if (coScript != nullptr) { + coScript->emitValueChanged(); + } +} + +void ControllerScriptInterfaceLegacy::log(const QString& message) { + controllerDebug(message); +} +int ControllerScriptInterfaceLegacy::beginTimer( + int intervalMillis, QJSValue timerCallback, bool oneShot) { + if (timerCallback.isString()) { + timerCallback = m_pScriptEngineLegacy->jsEngine()->evaluate(timerCallback.toString()); + } else if (!timerCallback.isCallable()) { + QString sErrorMessage( + "Invalid timer callback provided to engine.beginTimer. Valid " + "callbacks are strings and functions. " + "Make sure that your code contains no syntax errors."); + if (timerCallback.isError()) { + sErrorMessage.append("\n" + timerCallback.toString()); + } + m_pScriptEngineLegacy->throwJSError(sErrorMessage); + return 0; + } + + if (intervalMillis < 20) { + qWarning() << "Timer request for" << intervalMillis + << "ms is too short. Setting to the minimum of 20ms."; + intervalMillis = 20; + } + + // This makes use of every QObject's internal timer mechanism. Nice, clean, + // and simple. See http://doc.trolltech.com/4.6/qobject.html#startTimer for + // details + int timerId = startTimer(intervalMillis); + TimerInfo info; + info.callback = timerCallback; + info.oneShot = oneShot; + m_timers[timerId] = info; + if (timerId == 0) { + qWarning() << "Script timer could not be created"; + } else if (oneShot) { + controllerDebug("Starting one-shot timer:" << timerId); + } else { + controllerDebug("Starting timer:" << timerId); + } + return timerId; +} + +void ControllerScriptInterfaceLegacy::stopTimer(int timerId) { + if (!m_timers.contains(timerId)) { + qWarning() << "Killing timer" << timerId + << ": That timer does not exist!"; + return; + } + controllerDebug("Killing timer:" << timerId); + killTimer(timerId); + m_timers.remove(timerId); +} + +void ControllerScriptInterfaceLegacy::timerEvent(QTimerEvent* event) { + int timerId = event->timerId(); + + // See if this is a scratching timer + if (m_scratchTimers.contains(timerId)) { + scratchProcess(timerId); + return; + } + + auto it = m_timers.constFind(timerId); + if (it == m_timers.constEnd()) { + qWarning() << "Timer" << timerId + << "fired but there's no function mapped to it!"; + return; + } + + // NOTE(rryan): Do not assign by reference -- make a copy. I have no idea + // why but this causes segfaults in ~QScriptValue while scratching if we + // don't copy here -- even though internalExecute passes the QScriptValues + // by value. *boggle* + const TimerInfo timerTarget = it.value(); + if (timerTarget.oneShot) { + stopTimer(timerId); + } + + m_pScriptEngineLegacy->executeFunction(timerTarget.callback, QJSValueList()); +} + +void ControllerScriptInterfaceLegacy::softTakeover( + const QString& group, const QString& name, bool set) { + ControlObject* pControl = ControlObject::getControl( + ConfigKey(group, name), ControllerDebug::controlFlags()); + if (!pControl) { + return; + } + if (set) { + m_st.enable(pControl); + } else { + m_st.disable(pControl); + } +} + +void ControllerScriptInterfaceLegacy::softTakeoverIgnoreNextValue( + const QString& group, const QString& name) { + ControlObject* pControl = ControlObject::getControl( + ConfigKey(group, name), ControllerDebug::controlFlags()); + if (!pControl) { + return; + } + + m_st.ignoreNext(pControl); +} + +double ControllerScriptInterfaceLegacy::getDeckRate(const QString& group) { + double rate = 0.0; + ControlObjectScript* pRateRatio = + getControlObjectScript(group, "rate_ratio"); + if (pRateRatio != nullptr) { + rate = pRateRatio->get(); + } + + // See if we're in reverse play + ControlObjectScript* pReverse = getControlObjectScript(group, "reverse"); + if (pReverse != nullptr && pReverse->get() == 1) { + rate = -rate; + } + return rate; +} + +bool ControllerScriptInterfaceLegacy::isDeckPlaying(const QString& group) { + ControlObjectScript* pPlay = getControlObjectScript(group, "play"); + + if (pPlay == nullptr) { + QString error = QString("Could not getControlObjectScript()"); + m_pScriptEngineLegacy->scriptErrorDialog(error, error); + return false; + } + + return pPlay->get() > 0.0; +} + +void ControllerScriptInterfaceLegacy::scratchEnable(int deck, + int intervalsPerRev, + double rpm, + double alpha, + double beta, + bool ramp) { + // If we're already scratching this deck, override that with this request + if (static_cast(m_dx[deck])) { + //qDebug() << "Already scratching deck" << deck << ". Overriding."; + int timerId = m_scratchTimers.key(deck); + killTimer(timerId); + m_scratchTimers.remove(timerId); + } + + // Controller resolution in intervals per second at normal speed. + // (rev/min * ints/rev * mins/sec) + double intervalsPerSecond = (rpm * intervalsPerRev) / 60.0; + + if (intervalsPerSecond == 0.0) { + qWarning() << "Invalid rpm or intervalsPerRev supplied to " + "scratchEnable. Ignoring request."; + return; + } + + m_dx[deck] = 1.0 / intervalsPerSecond; + m_intervalAccumulator[deck] = 0.0; + m_ramp[deck] = false; + m_rampFactor[deck] = 0.001; + m_brakeActive[deck] = false; + + // PlayerManager::groupForDeck is 0-indexed. + QString group = PlayerManager::groupForDeck(deck - 1); + + // Ramp velocity, default to stopped. + double initVelocity = 0.0; + + ControlObjectScript* pScratch2Enable = + getControlObjectScript(group, "scratch2_enable"); + + // If ramping is desired, figure out the deck's current speed + if (ramp) { + // See if the deck is already being scratched + if (pScratch2Enable != nullptr && pScratch2Enable->get() == 1) { + // If so, set the filter's initial velocity to the scratch speed + ControlObjectScript* pScratch2 = + getControlObjectScript(group, "scratch2"); + if (pScratch2 != nullptr) { + initVelocity = pScratch2->get(); + } + } else if (isDeckPlaying(group)) { + // If the deck is playing, set the filter's initial velocity to the + // playback speed + initVelocity = getDeckRate(group); + } + } + + // Initialize scratch filter + if (static_cast(alpha) && static_cast(beta)) { + m_scratchFilters[deck]->init(kAlphaBetaDt, initVelocity, alpha, beta); + } else { + // Use filter's defaults if not specified + m_scratchFilters[deck]->init(kAlphaBetaDt, initVelocity); + } + + // 1ms is shortest possible, OS dependent + int timerId = startTimer(kScratchTimerMs); + + // Associate this virtual deck with this timer for later processing + m_scratchTimers[timerId] = deck; + + // Set scratch2_enable + if (pScratch2Enable != nullptr) { + pScratch2Enable->slotSet(1); + } +} + +void ControllerScriptInterfaceLegacy::scratchTick(int deck, int interval) { + m_lastMovement[deck] = mixxx::Time::elapsed(); + m_intervalAccumulator[deck] += interval; +} + +void ControllerScriptInterfaceLegacy::scratchProcess(int timerId) { + int deck = m_scratchTimers[timerId]; + // PlayerManager::groupForDeck is 0-indexed. + QString group = PlayerManager::groupForDeck(deck - 1); + AlphaBetaFilter* filter = m_scratchFilters[deck]; + if (!filter) { + qWarning() << "Scratch filter pointer is null on deck" << deck; + return; + } + + const double oldRate = filter->predictedVelocity(); + + // Give the filter a data point: + + // If we're ramping to end scratching and the wheel hasn't been turned very + // recently (spinback after lift-off,) feed fixed data + if (m_ramp[deck] && !m_softStartActive[deck] && + ((mixxx::Time::elapsed() - m_lastMovement[deck]) >= + mixxx::Duration::fromMillis(1))) { + filter->observation(m_rampTo[deck] * m_rampFactor[deck]); + // Once this code path is run, latch so it always runs until reset + //m_lastMovement[deck] += mixxx::Duration::fromSeconds(1); + } else if (m_softStartActive[deck]) { + // pretend we have moved by (desired rate*default distance) + filter->observation(m_rampTo[deck] * kAlphaBetaDt); + } else { + // This will (and should) be 0 if no net ticks have been accumulated + // (i.e. the wheel is stopped) + filter->observation(m_dx[deck] * m_intervalAccumulator[deck]); + } + + const double newRate = filter->predictedVelocity(); + + // Actually do the scratching + ControlObjectScript* pScratch2 = getControlObjectScript(group, "scratch2"); + if (pScratch2 == nullptr) { + return; // abort and maybe it'll work on the next pass + } + pScratch2->set(newRate); + + // Reset accumulator + m_intervalAccumulator[deck] = 0; + + // End scratching if we're ramping and the current rate is really close to the rampTo value + if ((m_ramp[deck] && fabs(m_rampTo[deck] - newRate) <= 0.00001) || + // or if we brake or softStart and have crossed over the desired value, + ((m_brakeActive[deck] || m_softStartActive[deck]) && + ((oldRate > m_rampTo[deck] && newRate < m_rampTo[deck]) || + (oldRate < m_rampTo[deck] && + newRate > m_rampTo[deck]))) || + // or if the deck was stopped manually during brake or softStart + ((m_brakeActive[deck] || m_softStartActive[deck]) && + (!isDeckPlaying(group)))) { + // Not ramping no mo' + m_ramp[deck] = false; + + if (m_brakeActive[deck]) { + // If in brake mode, set scratch2 rate to 0 and turn off the play button. + pScratch2->slotSet(0.0); + ControlObjectScript* pPlay = getControlObjectScript(group, "play"); + if (pPlay != nullptr) { + pPlay->slotSet(0.0); + } + } + + // Clear scratch2_enable to end scratching. + ControlObjectScript* pScratch2Enable = + getControlObjectScript(group, "scratch2_enable"); + if (pScratch2Enable == nullptr) { + return; // abort and maybe it'll work on the next pass + } + pScratch2Enable->slotSet(0); + + // Remove timer + killTimer(timerId); + m_scratchTimers.remove(timerId); + + m_dx[deck] = 0.0; + m_brakeActive[deck] = false; + m_softStartActive[deck] = false; + } +} + +void ControllerScriptInterfaceLegacy::scratchDisable(int deck, bool ramp) { + // PlayerManager::groupForDeck is 0-indexed. + QString group = PlayerManager::groupForDeck(deck - 1); + + m_rampTo[deck] = 0.0; + + // If no ramping is desired, disable scratching immediately + if (!ramp) { + // Clear scratch2_enable + ControlObjectScript* pScratch2Enable = getControlObjectScript(group, "scratch2_enable"); + if (pScratch2Enable != nullptr) { + pScratch2Enable->slotSet(0); + } + // Can't return here because we need scratchProcess to stop the timer. + // So it's still actually ramping, we just won't hear or see it. + } else if (isDeckPlaying(group)) { + // If so, set the target velocity to the playback speed + m_rampTo[deck] = getDeckRate(group); + } + + m_lastMovement[deck] = mixxx::Time::elapsed(); + m_ramp[deck] = true; // Activate the ramping in scratchProcess() +} + +bool ControllerScriptInterfaceLegacy::isScratching(int deck) { + // PlayerManager::groupForDeck is 0-indexed. + QString group = PlayerManager::groupForDeck(deck - 1); + return getValue(group, "scratch2_enable") > 0; +} + +void ControllerScriptInterfaceLegacy::spinback( + int deck, bool activate, double factor, double rate) { + // defaults for args set in header file + brake(deck, activate, factor, rate); +} + +void ControllerScriptInterfaceLegacy::brake(int deck, bool activate, double factor, double rate) { + // PlayerManager::groupForDeck is 0-indexed. + QString group = PlayerManager::groupForDeck(deck - 1); + + // kill timer when both enabling or disabling + int timerId = m_scratchTimers.key(deck); + killTimer(timerId); + m_scratchTimers.remove(timerId); + + // enable/disable scratch2 mode + ControlObjectScript* pScratch2Enable = getControlObjectScript(group, "scratch2_enable"); + if (pScratch2Enable != nullptr) { + pScratch2Enable->slotSet(activate ? 1 : 0); + } + + // used in scratchProcess for the different timer behavior we need + m_brakeActive[deck] = activate; + double initRate = rate; + + if (activate) { + // store the new values for this spinback/brake effect + if (initRate == 1.0) { // then rate is really 1.0 or was set to default + // in /res/common-controller-scripts.js so check for real value, + // taking pitch into account + initRate = getDeckRate(group); + } + // stop ramping at a rate which doesn't produce any audible output anymore + m_rampTo[deck] = 0.01; + // if we are currently softStart()ing, stop it + if (m_softStartActive[deck]) { + m_softStartActive[deck] = false; + AlphaBetaFilter* filter = m_scratchFilters[deck]; + if (filter != nullptr) { + initRate = filter->predictedVelocity(); + } + } + + // setup timer and set scratch2 + timerId = startTimer(kScratchTimerMs); + m_scratchTimers[timerId] = deck; + + ControlObjectScript* pScratch2 = getControlObjectScript(group, "scratch2"); + if (pScratch2 != nullptr) { + pScratch2->slotSet(initRate); + } + + // setup the filter with default alpha and beta*factor + double alphaBrake = 1.0 / 512; + // avoid decimals for fine adjusting + if (factor > 1) { + factor = ((factor - 1) / 10) + 1; + } + double betaBrake = ((1.0 / 512) / 1024) * factor; // default*factor + AlphaBetaFilter* filter = m_scratchFilters[deck]; + if (filter != nullptr) { + filter->init(kAlphaBetaDt, initRate, alphaBrake, betaBrake); + } + + // activate the ramping in scratchProcess() + m_ramp[deck] = true; + } +} + +void ControllerScriptInterfaceLegacy::softStart(int deck, bool activate, double factor) { + // PlayerManager::groupForDeck is 0-indexed. + QString group = PlayerManager::groupForDeck(deck - 1); + + // kill timer when both enabling or disabling + int timerId = m_scratchTimers.key(deck); + killTimer(timerId); + m_scratchTimers.remove(timerId); + + // enable/disable scratch2 mode + ControlObjectScript* pScratch2Enable = getControlObjectScript(group, "scratch2_enable"); + if (pScratch2Enable != nullptr) { + pScratch2Enable->slotSet(activate ? 1 : 0); + } + + // used in scratchProcess for the different timer behavior we need + m_softStartActive[deck] = activate; + double initRate = 0.0; + + if (activate) { + // acquire deck rate + m_rampTo[deck] = getDeckRate(group); + + // if brake()ing, get current rate from filter + if (m_brakeActive[deck]) { + m_brakeActive[deck] = false; + + AlphaBetaFilter* filter = m_scratchFilters[deck]; + if (filter != nullptr) { + initRate = filter->predictedVelocity(); + } + } + + // setup timer, start playing and set scratch2 + timerId = startTimer(kScratchTimerMs); + m_scratchTimers[timerId] = deck; + + ControlObjectScript* pPlay = getControlObjectScript(group, "play"); + if (pPlay != nullptr) { + pPlay->slotSet(1.0); + } + + ControlObjectScript* pScratch2 = getControlObjectScript(group, "scratch2"); + if (pScratch2 != nullptr) { + pScratch2->slotSet(initRate); + } + + // setup the filter like in brake(), with default alpha and beta*factor + double alphaSoft = 1.0 / 512; + // avoid decimals for fine adjusting + if (factor > 1) { + factor = ((factor - 1) / 10) + 1; + } + double betaSoft = ((1.0 / 512) / 1024) * factor; // default: (1.0/512)/1024 + AlphaBetaFilter* filter = m_scratchFilters[deck]; + if (filter != nullptr) { // kAlphaBetaDt = 1/1000 seconds + filter->init(kAlphaBetaDt, initRate, alphaSoft, betaSoft); + } + + // activate the ramping in scratchProcess() + m_ramp[deck] = true; + } +} diff --git a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h new file mode 100644 index 00000000000..11087394eb5 --- /dev/null +++ b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h @@ -0,0 +1,90 @@ +#pragma once + +#include +#include + +#include "controllers/softtakeover.h" +#include "util/alphabetafilter.h" + +class ControllerScriptEngineLegacy; +class ControlObjectScript; +class ScriptConnection; +class ConfigKey; + +/// ControllerScriptInterfaceLegacy is the legacy API for controller scripts to interact +/// with Mixxx. It is inserted into the JS environment as the "engine" object. +class ControllerScriptInterfaceLegacy : public QObject { + Q_OBJECT + public: + ControllerScriptInterfaceLegacy(ControllerScriptEngineLegacy* m_pEngine); + + virtual ~ControllerScriptInterfaceLegacy(); + + Q_INVOKABLE double getValue(const QString& group, const QString& name); + Q_INVOKABLE void setValue(const QString& group, const QString& name, double newValue); + Q_INVOKABLE double getParameter(const QString& group, const QString& name); + Q_INVOKABLE void setParameter(const QString& group, const QString& name, double newValue); + Q_INVOKABLE double getParameterForValue( + const QString& group, const QString& name, double value); + Q_INVOKABLE void reset(const QString& group, const QString& name); + Q_INVOKABLE double getDefaultValue(const QString& group, const QString& name); + Q_INVOKABLE double getDefaultParameter(const QString& group, const QString& name); + Q_INVOKABLE QJSValue makeConnection( + const QString& group, const QString& name, const QJSValue& callback); + // DEPRECATED: Use makeConnection instead. + Q_INVOKABLE QJSValue connectControl(const QString& group, + const QString& name, + const QJSValue& passedCallback, + bool disconnect = false); + // Called indirectly by the objects returned by connectControl + Q_INVOKABLE void trigger(const QString& group, const QString& name); + Q_INVOKABLE void log(const QString& message); + Q_INVOKABLE int beginTimer(int interval, QJSValue scriptCode, bool oneShot = false); + Q_INVOKABLE void stopTimer(int timerId); + Q_INVOKABLE void scratchEnable(int deck, + int intervalsPerRev, + double rpm, + double alpha, + double beta, + bool ramp = true); + Q_INVOKABLE void scratchTick(int deck, int interval); + Q_INVOKABLE void scratchDisable(int deck, bool ramp = true); + Q_INVOKABLE bool isScratching(int deck); + Q_INVOKABLE void softTakeover(const QString& group, const QString& name, bool set); + Q_INVOKABLE void softTakeoverIgnoreNextValue(const QString& group, const QString& name); + Q_INVOKABLE void brake(int deck, bool activate, double factor = 1.0, double rate = 1.0); + Q_INVOKABLE void spinback(int deck, bool activate, double factor = 1.8, double rate = -10.0); + Q_INVOKABLE void softStart(int deck, bool activate, double factor = 1.0); + + bool removeScriptConnection(const ScriptConnection& conn); + /// Execute a ScriptConnection's JS callback + void triggerScriptConnection(const ScriptConnection& conn); + + /// Handler for timers that scripts set. + virtual void timerEvent(QTimerEvent* event); + + private: + QHash m_controlCache; + ControlObjectScript* getControlObjectScript(const QString& group, const QString& name); + + SoftTakeoverCtrl m_st; + + struct TimerInfo { + QJSValue callback; + bool oneShot; + }; + QHash m_timers; + + QVarLengthArray m_intervalAccumulator; + QVarLengthArray m_lastMovement; + QVarLengthArray m_dx, m_rampTo, m_rampFactor; + QVarLengthArray m_ramp, m_brakeActive, m_softStartActive; + QVarLengthArray m_scratchFilters; + QHash m_scratchTimers; + /// Applies the accumulated movement to the track speed + void scratchProcess(int timerId); + bool isDeckPlaying(const QString& group); + double getDeckRate(const QString& group); + + ControllerScriptEngineLegacy* m_pScriptEngineLegacy; +}; diff --git a/src/controllers/engine/scriptconnection.cpp b/src/controllers/scripting/legacy/scriptconnection.cpp similarity index 83% rename from src/controllers/engine/scriptconnection.cpp rename to src/controllers/scripting/legacy/scriptconnection.cpp index f059d127e9a..2f66346db3f 100644 --- a/src/controllers/engine/scriptconnection.cpp +++ b/src/controllers/scripting/legacy/scriptconnection.cpp @@ -1,6 +1,6 @@ -#include "controllers/engine/scriptconnection.h" +#include "controllers/scripting/legacy/scriptconnection.h" -#include "controllers/engine/controllerengine.h" +#include "controllers/scripting/legacy/controllerscriptenginelegacy.h" void ScriptConnection::executeCallback(double value) const { QJSValueList args; diff --git a/src/controllers/engine/scriptconnection.h b/src/controllers/scripting/legacy/scriptconnection.h similarity index 78% rename from src/controllers/engine/scriptconnection.h rename to src/controllers/scripting/legacy/scriptconnection.h index 153a4b83f86..f2e6558e64b 100644 --- a/src/controllers/engine/scriptconnection.h +++ b/src/controllers/scripting/legacy/scriptconnection.h @@ -5,7 +5,8 @@ #include "preferences/configobject.h" -class ControllerEngine; +class ControllerScriptEngineLegacy; +class ControllerScriptInterfaceLegacy; /// ScriptConnection is a connection between a ControlObject and a /// script callback function that gets executed when the value @@ -15,7 +16,8 @@ class ScriptConnection { ConfigKey key; QUuid id; QJSValue callback; - ControllerEngine* controllerEngine; + ControllerScriptInterfaceLegacy* engineJSProxy; + ControllerScriptEngineLegacy* controllerEngine; void executeCallback(double value) const; diff --git a/src/controllers/scripting/legacy/scriptconnectionjsproxy.cpp b/src/controllers/scripting/legacy/scriptconnectionjsproxy.cpp new file mode 100644 index 00000000000..7ba44b18c4d --- /dev/null +++ b/src/controllers/scripting/legacy/scriptconnectionjsproxy.cpp @@ -0,0 +1,14 @@ +#include "controllers/scripting/legacy/scriptconnectionjsproxy.h" + +#include "controllers/scripting/legacy/controllerscriptinterfacelegacy.h" + +bool ScriptConnectionJSProxy::disconnect() { + // if the removeScriptConnection succeeded, the connection has been successfully disconnected + bool success = m_scriptConnection.engineJSProxy->removeScriptConnection(m_scriptConnection); + m_isConnected = !success; + return success; +} + +void ScriptConnectionJSProxy::trigger() { + m_scriptConnection.engineJSProxy->triggerScriptConnection(m_scriptConnection); +} diff --git a/src/controllers/engine/scriptconnectionjsproxy.h b/src/controllers/scripting/legacy/scriptconnectionjsproxy.h similarity index 92% rename from src/controllers/engine/scriptconnectionjsproxy.h rename to src/controllers/scripting/legacy/scriptconnectionjsproxy.h index 344fb38c010..2b80b5e4c08 100644 --- a/src/controllers/engine/scriptconnectionjsproxy.h +++ b/src/controllers/scripting/legacy/scriptconnectionjsproxy.h @@ -2,7 +2,7 @@ #include -#include "controllers/engine/scriptconnection.h" +#include "controllers/scripting/legacy/scriptconnection.h" /// ScriptConnectionJSProxy provides scripts with an interface to ScriptConnection. class ScriptConnectionJSProxy : public QObject { diff --git a/src/test/colormapperjsproxy_test.cpp b/src/test/colormapperjsproxy_test.cpp index f6a9e29bf5f..753f6fe79c5 100644 --- a/src/test/colormapperjsproxy_test.cpp +++ b/src/test/colormapperjsproxy_test.cpp @@ -1,4 +1,4 @@ -#include "controllers/engine/colormapperjsproxy.h" +#include "controllers/scripting/colormapperjsproxy.h" #include diff --git a/src/test/controller_preset_validation_test.cpp b/src/test/controller_preset_validation_test.cpp index f941c08ed20..bf458dcb0f7 100644 --- a/src/test/controller_preset_validation_test.cpp +++ b/src/test/controller_preset_validation_test.cpp @@ -30,7 +30,7 @@ FakeController::FakeController() : m_bMidiPreset(false), m_bHidPreset(false) { startEngine(); - getEngine()->setTesting(true); + getScriptEngine()->setTesting(true); } FakeController::~FakeController() { @@ -82,8 +82,7 @@ bool ControllerPresetValidationTest::testLoadPreset(const PresetInfo& preset) { FakeController controller; controller.setDeviceName("Test Controller"); controller.setPreset(*pPreset); - // Do not initialize the scripts. - bool result = controller.applyPreset(false); + bool result = controller.applyPreset(); controller.stopEngine(); return result; } diff --git a/src/test/controllerengine_test.cpp b/src/test/controllerscriptenginelegacy_test.cpp similarity index 89% rename from src/test/controllerengine_test.cpp rename to src/test/controllerscriptenginelegacy_test.cpp index fc29ef15d9b..ff6b97ed7d4 100644 --- a/src/test/controllerengine_test.cpp +++ b/src/test/controllerscriptenginelegacy_test.cpp @@ -1,4 +1,4 @@ -#include "controllers/engine/controllerengine.h" +#include "controllers/scripting/legacy/controllerscriptenginelegacy.h" #include #include @@ -17,7 +17,7 @@ typedef std::unique_ptr ScopedTemporaryFile; -class ControllerEngineTest : public MixxxTest { +class ControllerScriptEngineLegacyTest : public MixxxTest { protected: static ScopedTemporaryFile makeTemporaryFile(const QString& contents) { QByteArray contentsBa = contents.toLocal8Bit(); @@ -32,12 +32,12 @@ class ControllerEngineTest : public MixxxTest { mixxx::Time::setTestMode(true); mixxx::Time::setTestElapsedTime(mixxx::Duration::fromMillis(10)); QThread::currentThread()->setObjectName("Main"); - cEngine = new ControllerEngine(nullptr); - ControllerDebug::enable(); + cEngine = new ControllerScriptEngineLegacy(nullptr); + cEngine->initialize(); + ControllerDebug::setTesting(true); } void TearDown() override { - cEngine->gracefulShutdown(); delete cEngine; mixxx::Time::setTestMode(false); } @@ -47,11 +47,11 @@ class ControllerEngineTest : public MixxxTest { } QJSValue evaluate(const QString& code) { - return cEngine->evaluateCodeString(code); + return cEngine->jsEngine()->evaluate(code); } bool evaluateAndAssert(const QString& code) { - return !cEngine->evaluateCodeString(code).isError(); + return !evaluate(code).isError(); } void processEvents() { @@ -65,50 +65,62 @@ class ControllerEngineTest : public MixxxTest { application()->processEvents(); } - ControllerEngine* cEngine; + ControllerScriptEngineLegacy* cEngine; }; -TEST_F(ControllerEngineTest, commonScriptHasNoErrors) { +TEST_F(ControllerScriptEngineLegacyTest, commonScriptHasNoErrors) { QFileInfo commonScript("./res/controllers/common-controller-scripts.js"); EXPECT_TRUE(evaluateScriptFile(commonScript)); } -TEST_F(ControllerEngineTest, setValue) { +TEST_F(ControllerScriptEngineLegacyTest, setValue) { auto co = std::make_unique(ConfigKey("[Test]", "co")); EXPECT_TRUE(evaluateAndAssert("engine.setValue('[Test]', 'co', 1.0);")); EXPECT_DOUBLE_EQ(1.0, co->get()); } -TEST_F(ControllerEngineTest, getValue_InvalidKey) { - ControllerDebug::disable(); +TEST_F(ControllerScriptEngineLegacyTest, getValue_InvalidKey) { + ControllerDebug::setEnabled(false); + ControllerDebug::setTesting(false); EXPECT_TRUE(evaluateAndAssert("engine.getValue('', '');")); EXPECT_TRUE(evaluateAndAssert("engine.getValue('', 'invalid');")); EXPECT_TRUE(evaluateAndAssert("engine.getValue('[Invalid]', '');")); - ControllerDebug::enable(); + ControllerDebug::setTesting(true); + ControllerDebug::setEnabled(true); } -TEST_F(ControllerEngineTest, setValue_InvalidControl) { +TEST_F(ControllerScriptEngineLegacyTest, setValue_InvalidControl) { + ControllerDebug::setEnabled(false); + ControllerDebug::setTesting(false); EXPECT_TRUE(evaluateAndAssert("engine.setValue('[Nothing]', 'nothing', 1.0);")); + ControllerDebug::setTesting(true); + ControllerDebug::setEnabled(true); } -TEST_F(ControllerEngineTest, getValue_InvalidControl) { +TEST_F(ControllerScriptEngineLegacyTest, getValue_InvalidControl) { + ControllerDebug::setEnabled(false); + ControllerDebug::setTesting(false); EXPECT_TRUE(evaluateAndAssert("engine.getValue('[Nothing]', 'nothing');")); + ControllerDebug::setTesting(true); + ControllerDebug::setEnabled(true); } -TEST_F(ControllerEngineTest, setValue_IgnoresNaN) { +TEST_F(ControllerScriptEngineLegacyTest, setValue_IgnoresNaN) { auto co = std::make_unique(ConfigKey("[Test]", "co")); co->set(10.0); EXPECT_TRUE(evaluateAndAssert("engine.setValue('[Test]', 'co', NaN);")); EXPECT_DOUBLE_EQ(10.0, co->get()); } -TEST_F(ControllerEngineTest, getSetValue) { +TEST_F(ControllerScriptEngineLegacyTest, getSetValue) { auto co = std::make_unique(ConfigKey("[Test]", "co")); - EXPECT_TRUE(evaluateAndAssert("engine.setValue('[Test]', 'co', engine.getValue('[Test]', 'co') + 1);")); + EXPECT_TRUE( + evaluateAndAssert("engine.setValue('[Test]', 'co', " + "engine.getValue('[Test]', 'co') + 1);")); EXPECT_DOUBLE_EQ(1.0, co->get()); } -TEST_F(ControllerEngineTest, setParameter) { +TEST_F(ControllerScriptEngineLegacyTest, setParameter) { auto co = std::make_unique(ConfigKey("[Test]", "co"), -10.0, 10.0); @@ -120,7 +132,7 @@ TEST_F(ControllerEngineTest, setParameter) { EXPECT_DOUBLE_EQ(0.0, co->get()); } -TEST_F(ControllerEngineTest, setParameter_OutOfRange) { +TEST_F(ControllerScriptEngineLegacyTest, setParameter_OutOfRange) { auto co = std::make_unique(ConfigKey("[Test]", "co"), -10.0, 10.0); @@ -130,7 +142,7 @@ TEST_F(ControllerEngineTest, setParameter_OutOfRange) { EXPECT_DOUBLE_EQ(-10.0, co->get()); } -TEST_F(ControllerEngineTest, setParameter_NaN) { +TEST_F(ControllerScriptEngineLegacyTest, setParameter_NaN) { // Test that NaNs are ignored. auto co = std::make_unique(ConfigKey("[Test]", "co"), -10.0, @@ -139,7 +151,7 @@ TEST_F(ControllerEngineTest, setParameter_NaN) { EXPECT_DOUBLE_EQ(0.0, co->get()); } -TEST_F(ControllerEngineTest, getSetParameter) { +TEST_F(ControllerScriptEngineLegacyTest, getSetParameter) { auto co = std::make_unique(ConfigKey("[Test]", "co"), -10.0, 10.0); @@ -149,7 +161,7 @@ TEST_F(ControllerEngineTest, getSetParameter) { EXPECT_DOUBLE_EQ(2.0, co->get()); } -TEST_F(ControllerEngineTest, softTakeover_setValue) { +TEST_F(ControllerScriptEngineLegacyTest, softTakeover_setValue) { auto co = std::make_unique(ConfigKey("[Test]", "co"), -10.0, 10.0); @@ -181,7 +193,7 @@ TEST_F(ControllerEngineTest, softTakeover_setValue) { EXPECT_DOUBLE_EQ(0.0, co->get()); } -TEST_F(ControllerEngineTest, softTakeover_setParameter) { +TEST_F(ControllerScriptEngineLegacyTest, softTakeover_setParameter) { auto co = std::make_unique(ConfigKey("[Test]", "co"), -10.0, 10.0); @@ -213,7 +225,7 @@ TEST_F(ControllerEngineTest, softTakeover_setParameter) { EXPECT_DOUBLE_EQ(0.0, co->get()); } -TEST_F(ControllerEngineTest, softTakeover_ignoreNextValue) { +TEST_F(ControllerScriptEngineLegacyTest, softTakeover_ignoreNextValue) { auto co = std::make_unique(ConfigKey("[Test]", "co"), -10.0, 10.0); @@ -236,7 +248,7 @@ TEST_F(ControllerEngineTest, softTakeover_ignoreNextValue) { EXPECT_DOUBLE_EQ(0.0, co->get()); } -TEST_F(ControllerEngineTest, reset) { +TEST_F(ControllerScriptEngineLegacyTest, reset) { // Test that NaNs are ignored. auto co = std::make_unique(ConfigKey("[Test]", "co"), -10.0, @@ -246,11 +258,11 @@ TEST_F(ControllerEngineTest, reset) { EXPECT_DOUBLE_EQ(0.0, co->get()); } -TEST_F(ControllerEngineTest, log) { +TEST_F(ControllerScriptEngineLegacyTest, log) { EXPECT_TRUE(evaluateAndAssert("engine.log('Test that logging works.');")); } -TEST_F(ControllerEngineTest, trigger) { +TEST_F(ControllerScriptEngineLegacyTest, trigger) { auto co = std::make_unique(ConfigKey("[Test]", "co")); auto pass = std::make_unique(ConfigKey("[Test]", "passed")); @@ -271,7 +283,7 @@ TEST_F(ControllerEngineTest, trigger) { // depending on how it is invoked, so we need a lot of tests to make sure old scripts // do not break. -TEST_F(ControllerEngineTest, connectControl_ByString) { +TEST_F(ControllerScriptEngineLegacyTest, connectControl_ByString) { // Test that connecting and disconnecting by function name works. auto co = std::make_unique(ConfigKey("[Test]", "co")); auto pass = std::make_unique(ConfigKey("[Test]", "passed")); @@ -294,7 +306,7 @@ TEST_F(ControllerEngineTest, connectControl_ByString) { EXPECT_DOUBLE_EQ(1.0, pass->get()); } -TEST_F(ControllerEngineTest, connectControl_ByStringForbidDuplicateConnections) { +TEST_F(ControllerScriptEngineLegacyTest, connectControl_ByStringForbidDuplicateConnections) { // Test that connecting a control to a callback specified by a string // does not make duplicate connections. This behavior is inconsistent // with the behavior when specifying a callback as a function, but @@ -317,7 +329,7 @@ TEST_F(ControllerEngineTest, connectControl_ByStringForbidDuplicateConnections) EXPECT_DOUBLE_EQ(1.0, pass->get()); } -TEST_F(ControllerEngineTest, +TEST_F(ControllerScriptEngineLegacyTest, connectControl_ByStringRedundantConnectionObjectsAreNotIndependent) { // Test that multiple connections are not allowed when passing // the callback to engine.connectControl as a function name string. @@ -359,7 +371,7 @@ TEST_F(ControllerEngineTest, EXPECT_EQ(1.0, counter->get()); } -TEST_F(ControllerEngineTest, connectControl_ByFunction) { +TEST_F(ControllerScriptEngineLegacyTest, connectControl_ByFunction) { // Test that connecting and disconnecting with a function value works. auto co = std::make_unique(ConfigKey("[Test]", "co")); auto pass = std::make_unique(ConfigKey("[Test]", "passed")); @@ -377,7 +389,7 @@ TEST_F(ControllerEngineTest, connectControl_ByFunction) { EXPECT_DOUBLE_EQ(1.0, pass->get()); } -TEST_F(ControllerEngineTest, connectControl_ByFunctionAllowDuplicateConnections) { +TEST_F(ControllerScriptEngineLegacyTest, connectControl_ByFunctionAllowDuplicateConnections) { // Test that duplicate connections are allowed when passing callbacks as functions. auto co = std::make_unique(ConfigKey("[Test]", "co")); auto pass = std::make_unique(ConfigKey("[Test]", "passed")); @@ -398,7 +410,7 @@ TEST_F(ControllerEngineTest, connectControl_ByFunctionAllowDuplicateConnections) EXPECT_DOUBLE_EQ(2.0, pass->get()); } -TEST_F(ControllerEngineTest, connectControl_toDisconnectRemovesAllConnections) { +TEST_F(ControllerScriptEngineLegacyTest, connectControl_toDisconnectRemovesAllConnections) { // Test that every connection to a ControlObject is disconnected // by calling engine.connectControl(..., true). Individual connections // can only be disconnected by storing the connection object returned by @@ -425,7 +437,7 @@ TEST_F(ControllerEngineTest, connectControl_toDisconnectRemovesAllConnections) { EXPECT_DOUBLE_EQ(2.0, pass->get()); } -TEST_F(ControllerEngineTest, connectControl_ByLambda) { +TEST_F(ControllerScriptEngineLegacyTest, connectControl_ByLambda) { // Test that connecting with an anonymous function works. auto co = std::make_unique(ConfigKey("[Test]", "co")); auto pass = std::make_unique(ConfigKey("[Test]", "passed")); @@ -447,7 +459,7 @@ TEST_F(ControllerEngineTest, connectControl_ByLambda) { EXPECT_DOUBLE_EQ(1.0, pass->get()); } -TEST_F(ControllerEngineTest, connectionObject_Disconnect) { +TEST_F(ControllerScriptEngineLegacyTest, connectionObject_Disconnect) { // Test that disconnecting using the 'disconnect' method on the connection // object returned from connectControl works. auto co = std::make_unique(ConfigKey("[Test]", "co")); @@ -471,7 +483,7 @@ TEST_F(ControllerEngineTest, connectionObject_Disconnect) { EXPECT_DOUBLE_EQ(1.0, pass->get()); } -TEST_F(ControllerEngineTest, connectionObject_reflectDisconnect) { +TEST_F(ControllerScriptEngineLegacyTest, connectionObject_reflectDisconnect) { // Test that checks if disconnecting yields the appropriate feedback auto co = std::make_unique(ConfigKey("[Test]", "co")); auto pass = std::make_unique(ConfigKey("[Test]", "passed")); @@ -494,7 +506,7 @@ TEST_F(ControllerEngineTest, connectionObject_reflectDisconnect) { EXPECT_DOUBLE_EQ(4.0, pass->get()); } -TEST_F(ControllerEngineTest, connectionObject_DisconnectByPassingToConnectControl) { +TEST_F(ControllerScriptEngineLegacyTest, connectionObject_DisconnectByPassingToConnectControl) { // Test that passing a connection object back to engine.connectControl // removes the connection auto co = std::make_unique(ConfigKey("[Test]", "co")); @@ -535,7 +547,7 @@ TEST_F(ControllerEngineTest, connectionObject_DisconnectByPassingToConnectContro EXPECT_DOUBLE_EQ(1.0, pass->get()); } -TEST_F(ControllerEngineTest, connectionObject_MakesIndependentConnection) { +TEST_F(ControllerScriptEngineLegacyTest, connectionObject_MakesIndependentConnection) { // Test that multiple connections can be made to the same CO with // the same callback function and that calling their 'disconnect' method // only disconnects the callback for that object. @@ -574,7 +586,7 @@ TEST_F(ControllerEngineTest, connectionObject_MakesIndependentConnection) { EXPECT_EQ(3.0, counter->get()); } -TEST_F(ControllerEngineTest, connectionObject_trigger) { +TEST_F(ControllerScriptEngineLegacyTest, connectionObject_trigger) { // Test that triggering using the 'trigger' method on the connection // object returned from connectControl works. auto co = std::make_unique(ConfigKey("[Test]", "co")); @@ -595,7 +607,7 @@ TEST_F(ControllerEngineTest, connectionObject_trigger) { EXPECT_DOUBLE_EQ(1.0, counter->get()); } -TEST_F(ControllerEngineTest, connectionExecutesWithCorrectThisObject) { +TEST_F(ControllerScriptEngineLegacyTest, connectionExecutesWithCorrectThisObject) { // Test that callback functions are executed with JavaScript's // 'this' keyword referring to the object in which the connection // was created. diff --git a/src/test/midicontrollertest.cpp b/src/test/midicontrollertest.cpp index fa66aa5f680..e0b875fa620 100644 --- a/src/test/midicontrollertest.cpp +++ b/src/test/midicontrollertest.cpp @@ -38,10 +38,9 @@ class MidiControllerTest : public MixxxTest { m_pController->visit(&preset); } - void receive(unsigned char status, unsigned char control, - unsigned char value) { + void receivedShortMessage(unsigned char status, unsigned char control, unsigned char value) { // TODO(rryan): This test doesn't care about timestamps. - m_pController->receive(status, control, value, mixxx::Time::elapsed()); + m_pController->receivedShortMessage(status, control, value, mixxx::Time::elapsed()); } MidiControllerPreset m_preset; @@ -64,15 +63,15 @@ TEST_F(MidiControllerTest, ReceiveMessage_PushButtonCO_PushOnOff) { loadPreset(m_preset); // Receive an on/off, sets the control on/off with each press. - receive(MIDI_NOTE_ON | channel, control, 0x7F); + receivedShortMessage(MIDI_NOTE_ON | channel, control, 0x7F); EXPECT_LT(0.0, cpb.get()); - receive(MIDI_NOTE_OFF | channel, control, 0x00); + receivedShortMessage(MIDI_NOTE_OFF | channel, control, 0x00); EXPECT_DOUBLE_EQ(0.0, cpb.get()); // Receive an on/off, sets the control on/off with each press. - receive(MIDI_NOTE_ON | channel, control, 0x7F); + receivedShortMessage(MIDI_NOTE_ON | channel, control, 0x7F); EXPECT_LT(0.0, cpb.get()); - receive(MIDI_NOTE_OFF | channel, control, 0x00); + receivedShortMessage(MIDI_NOTE_OFF | channel, control, 0x00); EXPECT_DOUBLE_EQ(0.0, cpb.get()); } @@ -90,15 +89,15 @@ TEST_F(MidiControllerTest, ReceiveMessage_PushButtonCO_PushOnOn) { loadPreset(m_preset); // Receive an on/off, sets the control on/off with each press. - receive(MIDI_NOTE_ON | channel, control, 0x7F); + receivedShortMessage(MIDI_NOTE_ON | channel, control, 0x7F); EXPECT_LT(0.0, cpb.get()); - receive(MIDI_NOTE_ON | channel, control, 0x00); + receivedShortMessage(MIDI_NOTE_ON | channel, control, 0x00); EXPECT_DOUBLE_EQ(0.0, cpb.get()); // Receive an on/off, sets the control on/off with each press. - receive(MIDI_NOTE_ON | channel, control, 0x7F); + receivedShortMessage(MIDI_NOTE_ON | channel, control, 0x7F); EXPECT_LT(0.0, cpb.get()); - receive(MIDI_NOTE_ON | channel, control, 0x00); + receivedShortMessage(MIDI_NOTE_ON | channel, control, 0x00); EXPECT_DOUBLE_EQ(0.0, cpb.get()); } @@ -123,13 +122,13 @@ TEST_F(MidiControllerTest, ReceiveMessage_PushButtonCO_ToggleOnOff_ButtonMidiOpt // NOTE(rryan): This behavior is broken! // Toggle the switch on, sets the push button on. - receive(MIDI_NOTE_ON | channel, control, 0x7F); + receivedShortMessage(MIDI_NOTE_ON | channel, control, 0x7F); EXPECT_LT(0.0, cpb.get()); // The push button is stuck down here! // Toggle the switch off, sets the push button off. - receive(MIDI_NOTE_OFF | channel, control, 0x00); + receivedShortMessage(MIDI_NOTE_OFF | channel, control, 0x00); EXPECT_DOUBLE_EQ(0.0, cpb.get()); } @@ -154,13 +153,13 @@ TEST_F(MidiControllerTest, ReceiveMessage_PushButtonCO_ToggleOnOff_SwitchMidiOpt // NOTE(rryan): This behavior is broken! // Toggle the switch on, sets the push button on. - receive(MIDI_NOTE_ON | channel, control, 0x7F); + receivedShortMessage(MIDI_NOTE_ON | channel, control, 0x7F); EXPECT_LT(0.0, cpb.get()); // The push button is stuck down here! // Toggle the switch off, sets the push button on again. - receive(MIDI_NOTE_OFF | channel, control, 0x00); + receivedShortMessage(MIDI_NOTE_OFF | channel, control, 0x00); EXPECT_LT(0.0, cpb.get()); // NOTE(rryan): What is supposed to happen in this case? It's an open @@ -196,15 +195,15 @@ TEST_F(MidiControllerTest, ReceiveMessage_PushButtonCO_PushCC) { loadPreset(m_preset); // Receive an on/off, sets the control on/off with each press. - receive(MIDI_CC | channel, control, 0x7F); + receivedShortMessage(MIDI_CC | channel, control, 0x7F); EXPECT_LT(0.0, cpb.get()); - receive(MIDI_CC | channel, control, 0x00); + receivedShortMessage(MIDI_CC | channel, control, 0x00); EXPECT_DOUBLE_EQ(0.0, cpb.get()); // Receive an on/off, sets the control on/off with each press. - receive(MIDI_CC | channel, control, 0x7F); + receivedShortMessage(MIDI_CC | channel, control, 0x7F); EXPECT_LT(0.0, cpb.get()); - receive(MIDI_CC | channel, control, 0x00); + receivedShortMessage(MIDI_CC | channel, control, 0x00); EXPECT_DOUBLE_EQ(0.0, cpb.get()); } @@ -225,14 +224,14 @@ TEST_F(MidiControllerTest, ReceiveMessage_ToggleCO_PushOnOff) { loadPreset(m_preset); // Receive an on/off, toggles the control. - receive(MIDI_NOTE_ON | channel, control, 0x7F); - receive(MIDI_NOTE_OFF | channel, control, 0x00); + receivedShortMessage(MIDI_NOTE_ON | channel, control, 0x7F); + receivedShortMessage(MIDI_NOTE_OFF | channel, control, 0x00); EXPECT_LT(0.0, cpb.get()); // Receive an on/off, toggles the control. - receive(MIDI_NOTE_ON | channel, control, 0x7F); - receive(MIDI_NOTE_OFF | channel, control, 0x00); + receivedShortMessage(MIDI_NOTE_ON | channel, control, 0x7F); + receivedShortMessage(MIDI_NOTE_OFF | channel, control, 0x00); EXPECT_DOUBLE_EQ(0.0, cpb.get()); } @@ -252,14 +251,14 @@ TEST_F(MidiControllerTest, ReceiveMessage_ToggleCO_PushOnOn) { loadPreset(m_preset); // Receive an on/off, toggles the control. - receive(MIDI_NOTE_ON | channel, control, 0x7F); - receive(MIDI_NOTE_ON | channel, control, 0x00); + receivedShortMessage(MIDI_NOTE_ON | channel, control, 0x7F); + receivedShortMessage(MIDI_NOTE_ON | channel, control, 0x00); EXPECT_LT(0.0, cpb.get()); // Receive an on/off, toggles the control. - receive(MIDI_NOTE_ON | channel, control, 0x7F); - receive(MIDI_NOTE_ON | channel, control, 0x00); + receivedShortMessage(MIDI_NOTE_ON | channel, control, 0x7F); + receivedShortMessage(MIDI_NOTE_ON | channel, control, 0x00); EXPECT_DOUBLE_EQ(0.0, cpb.get()); } @@ -289,12 +288,12 @@ TEST_F(MidiControllerTest, ReceiveMessage_ToggleCO_ToggleOnOff_ButtonMidiOption) // Toggle the switch on, since it is interpreted as a button press it // toggles the button on. - receive(MIDI_NOTE_ON | channel, control, 0x7F); + receivedShortMessage(MIDI_NOTE_ON | channel, control, 0x7F); EXPECT_LT(0.0, cpb.get()); // Toggle the switch off, since it is interpreted as a button release it // does nothing to the toggle button. - receive(MIDI_NOTE_OFF | channel, control, 0x00); + receivedShortMessage(MIDI_NOTE_OFF | channel, control, 0x00); EXPECT_LT(0.0, cpb.get()); } @@ -324,12 +323,12 @@ TEST_F(MidiControllerTest, ReceiveMessage_ToggleCO_ToggleOnOff_SwitchMidiOption) // Toggle the switch on, since it is interpreted as a button press it // toggles the control on. - receive(MIDI_NOTE_ON | channel, control, 0x7F); + receivedShortMessage(MIDI_NOTE_ON | channel, control, 0x7F); EXPECT_LT(0.0, cpb.get()); // Toggle the switch off, since it is interpreted as a button press it // toggles the control off. - receive(MIDI_NOTE_OFF | channel, control, 0x00); + receivedShortMessage(MIDI_NOTE_OFF | channel, control, 0x00); EXPECT_DOUBLE_EQ(0.0, cpb.get()); // Meanwhile, the GUI toggles the control on again. @@ -339,12 +338,12 @@ TEST_F(MidiControllerTest, ReceiveMessage_ToggleCO_ToggleOnOff_SwitchMidiOption) // Toggle the switch on, since it is interpreted as a button press it // toggles the control off (since it was on). - receive(MIDI_NOTE_ON | channel, control, 0x7F); + receivedShortMessage(MIDI_NOTE_ON | channel, control, 0x7F); EXPECT_DOUBLE_EQ(0.0, cpb.get()); // Toggle the switch off, since it is interpreted as a button press it // toggles the control on (since it was off). - receive(MIDI_NOTE_OFF | channel, control, 0x00); + receivedShortMessage(MIDI_NOTE_OFF | channel, control, 0x00); EXPECT_LT(0.0, cpb.get()); } @@ -363,14 +362,14 @@ TEST_F(MidiControllerTest, ReceiveMessage_ToggleCO_PushCC) { loadPreset(m_preset); // Receive an on/off, toggles the control. - receive(MIDI_CC | channel, control, 0x7F); - receive(MIDI_CC | channel, control, 0x00); + receivedShortMessage(MIDI_CC | channel, control, 0x7F); + receivedShortMessage(MIDI_CC | channel, control, 0x00); EXPECT_LT(0.0, cpb.get()); // Receive an on/off, toggles the control. - receive(MIDI_CC | channel, control, 0x7F); - receive(MIDI_CC | channel, control, 0x00); + receivedShortMessage(MIDI_CC | channel, control, 0x7F); + receivedShortMessage(MIDI_CC | channel, control, 0x00); EXPECT_DOUBLE_EQ(0.0, cpb.get()); } @@ -391,15 +390,15 @@ TEST_F(MidiControllerTest, ReceiveMessage_PotMeterCO_7BitCC) { loadPreset(m_preset); // Receive a 0, MIDI parameter should map to the min value. - receive(MIDI_CC | channel, control, 0x00); + receivedShortMessage(MIDI_CC | channel, control, 0x00); EXPECT_DOUBLE_EQ(kMinValue, potmeter.get()); // Receive a 0x7F, MIDI parameter should map to the potmeter max value. - receive(MIDI_CC | channel, control, 0x7F); + receivedShortMessage(MIDI_CC | channel, control, 0x7F); EXPECT_DOUBLE_EQ(kMaxValue, potmeter.get()); // Receive a 0x40, MIDI parameter should map to the potmeter middle value. - receive(MIDI_CC | channel, control, 0x40); + receivedShortMessage(MIDI_CC | channel, control, 0x40); EXPECT_DOUBLE_EQ(kMiddleValue, potmeter.get()); } @@ -434,40 +433,40 @@ TEST_F(MidiControllerTest, ReceiveMessage_PotMeterCO_14BitCC) { // Receive a 0x0000 (lsb-first), MIDI parameter should map to the min value. potmeter.set(0); - receive(MIDI_CC | channel, lsb_control, 0x00); - receive(MIDI_CC | channel, msb_control, 0x00); + receivedShortMessage(MIDI_CC | channel, lsb_control, 0x00); + receivedShortMessage(MIDI_CC | channel, msb_control, 0x00); EXPECT_DOUBLE_EQ(kMinValue, potmeter.get()); // Receive a 0x0000 (msb-first), MIDI parameter should map to the min value. potmeter.set(0); - receive(MIDI_CC | channel, msb_control, 0x00); - receive(MIDI_CC | channel, lsb_control, 0x00); + receivedShortMessage(MIDI_CC | channel, msb_control, 0x00); + receivedShortMessage(MIDI_CC | channel, lsb_control, 0x00); EXPECT_DOUBLE_EQ(kMinValue, potmeter.get()); // Receive a 0x3FFF (lsb-first), MIDI parameter should map to the max value. potmeter.set(0); - receive(MIDI_CC | channel, lsb_control, 0x7F); - receive(MIDI_CC | channel, msb_control, 0x7F); + receivedShortMessage(MIDI_CC | channel, lsb_control, 0x7F); + receivedShortMessage(MIDI_CC | channel, msb_control, 0x7F); EXPECT_DOUBLE_EQ(kMaxValue, potmeter.get()); // Receive a 0x3FFF (msb-first), MIDI parameter should map to the max value. potmeter.set(0); - receive(MIDI_CC | channel, msb_control, 0x7F); - receive(MIDI_CC | channel, lsb_control, 0x7F); + receivedShortMessage(MIDI_CC | channel, msb_control, 0x7F); + receivedShortMessage(MIDI_CC | channel, lsb_control, 0x7F); EXPECT_DOUBLE_EQ(kMaxValue, potmeter.get()); // Receive a 0x2000 (lsb-first), MIDI parameter should map to the middle // value. potmeter.set(0); - receive(MIDI_CC | channel, lsb_control, 0x00); - receive(MIDI_CC | channel, msb_control, 0x40); + receivedShortMessage(MIDI_CC | channel, lsb_control, 0x00); + receivedShortMessage(MIDI_CC | channel, msb_control, 0x40); EXPECT_DOUBLE_EQ(kMiddleValue, potmeter.get()); // Receive a 0x2000 (msb-first), MIDI parameter should map to the middle // value. potmeter.set(0); - receive(MIDI_CC | channel, msb_control, 0x40); - receive(MIDI_CC | channel, lsb_control, 0x00); + receivedShortMessage(MIDI_CC | channel, msb_control, 0x40); + receivedShortMessage(MIDI_CC | channel, lsb_control, 0x00); EXPECT_DOUBLE_EQ(kMiddleValue, potmeter.get()); // Check the 14-bit resolution is actually present. Receive a 0x2001 @@ -475,8 +474,8 @@ TEST_F(MidiControllerTest, ReceiveMessage_PotMeterCO_14BitCC) { // amount. Scaling is not quite linear for MIDI parameters so just check // that incrementing the LSB by 1 is greater than the middle value. potmeter.set(0); - receive(MIDI_CC | channel, msb_control, 0x40); - receive(MIDI_CC | channel, lsb_control, 0x01); + receivedShortMessage(MIDI_CC | channel, msb_control, 0x40); + receivedShortMessage(MIDI_CC | channel, lsb_control, 0x01); EXPECT_LT(kMiddleValue, potmeter.get()); // Check the 14-bit resolution is actually present. Receive a 0x2001 @@ -484,8 +483,8 @@ TEST_F(MidiControllerTest, ReceiveMessage_PotMeterCO_14BitCC) { // amount. Scaling is not quite linear for MIDI parameters so just check // that incrementing the LSB by 1 is greater than the middle value. potmeter.set(0); - receive(MIDI_CC | channel, lsb_control, 0x01); - receive(MIDI_CC | channel, msb_control, 0x40); + receivedShortMessage(MIDI_CC | channel, lsb_control, 0x01); + receivedShortMessage(MIDI_CC | channel, msb_control, 0x40); EXPECT_LT(kMiddleValue, potmeter.get()); } @@ -505,21 +504,21 @@ TEST_F(MidiControllerTest, ReceiveMessage_PotMeterCO_14BitPitchBend) { loadPreset(m_preset); // Receive a 0x0000, MIDI parameter should map to the min value. - receive(MIDI_PITCH_BEND | channel, 0x00, 0x00); + receivedShortMessage(MIDI_PITCH_BEND | channel, 0x00, 0x00); EXPECT_DOUBLE_EQ(kMinValue, potmeter.get()); // Receive a 0x3FFF, MIDI parameter should map to the potmeter max value. - receive(MIDI_PITCH_BEND | channel, 0x7F, 0x7F); + receivedShortMessage(MIDI_PITCH_BEND | channel, 0x7F, 0x7F); EXPECT_DOUBLE_EQ(kMaxValue, potmeter.get()); // Receive a 0x2000, MIDI parameter should map to the potmeter middle value. - receive(MIDI_PITCH_BEND | channel, 0x00, 0x40); + receivedShortMessage(MIDI_PITCH_BEND | channel, 0x00, 0x40); EXPECT_DOUBLE_EQ(kMiddleValue, potmeter.get()); // Check the 14-bit resolution is actually present. Receive a 0x2001, MIDI // parameter should map to the middle value plus a tiny amount. Scaling is // not quite linear for MIDI parameters so just check that incrementing the // LSB by 1 is greater than the middle value. - receive(MIDI_PITCH_BEND | channel, 0x01, 0x40); + receivedShortMessage(MIDI_PITCH_BEND | channel, 0x01, 0x40); EXPECT_LT(kMiddleValue, potmeter.get()); } diff --git a/src/test/portmidicontroller_test.cpp b/src/test/portmidicontroller_test.cpp index 3e53a3c3e34..b443d52fac2 100644 --- a/src/test/portmidicontroller_test.cpp +++ b/src/test/portmidicontroller_test.cpp @@ -35,9 +35,13 @@ class MockPortMidiController : public PortMidiController { PortMidiController::sendSysexMsg(data, length); } - MOCK_METHOD4(receive, void(unsigned char, unsigned char, unsigned char, - mixxx::Duration)); + MOCK_METHOD4(receivedShortMessage, + void(unsigned char, unsigned char, unsigned char, mixxx::Duration)); MOCK_METHOD2(receive, void(const QByteArray&, mixxx::Duration)); + + // These tests are unrelated to scripting. + MOCK_METHOD0(startEngine, void()); + MOCK_METHOD0(stopEngine, void()); }; class MockPortMidiDevice : public PortMidiDevice { @@ -228,9 +232,9 @@ TEST_F(PortMidiControllerTest, Poll_Read_Basic) { .WillOnce(DoAll(SetArrayArgument<0>(messages.begin(), messages.end()), Return(messages.size()))); - EXPECT_CALL(*m_pController, receive(0x90, 0x3C, 0x40, _)) + EXPECT_CALL(*m_pController, receivedShortMessage(0x90, 0x3C, 0x40, _)) .InSequence(read); - EXPECT_CALL(*m_pController, receive(0x80, 0x3C, 0x40, _)) + EXPECT_CALL(*m_pController, receivedShortMessage(0x80, 0x3C, 0x40, _)) .InSequence(read); pollDevice(); @@ -265,9 +269,9 @@ TEST_F(PortMidiControllerTest, Poll_Read_SysExWithRealtime) { .InSequence(read) .WillOnce(DoAll(SetArrayArgument<0>(messages.begin(), messages.end()), Return(messages.size()))); - EXPECT_CALL(*m_pController, receive(0xF8, 0x00, 0x00, _)) + EXPECT_CALL(*m_pController, receivedShortMessage(0xF8, 0x00, 0x00, _)) .InSequence(read); - EXPECT_CALL(*m_pController, receive(0xFA, 0x00, 0x00, _)) + EXPECT_CALL(*m_pController, receivedShortMessage(0xFA, 0x00, 0x00, _)) .InSequence(read); EXPECT_CALL(*m_pController, receive(sysex, _)) .InSequence(read); @@ -363,7 +367,7 @@ TEST_F(PortMidiControllerTest, Poll_Read_SysExInterrupted_FollowedByNormalMessag .InSequence(read) .WillOnce(DoAll(SetArrayArgument<0>(messages.begin(), messages.end()), Return(messages.size()))); - EXPECT_CALL(*m_pController, receive(0x90, 0x3C, 0x40, _)) + EXPECT_CALL(*m_pController, receivedShortMessage(0x90, 0x3C, 0x40, _)) .InSequence(read); pollDevice();