1 /***************************************************************************
2                           controllerengine.cpp  -  description
3                           -------------------
4     begin                : Sat Apr 30 2011
5     copyright            : (C) 2011 by Sean M. Pappalardo
6     email                : spappalardo@mixxx.org
7  ***************************************************************************/
8 
9 #include "controllers/controllerengine.h"
10 
11 #include "control/controlobject.h"
12 #include "control/controlobjectscript.h"
13 #include "controllers/colormapperjsproxy.h"
14 #include "controllers/controller.h"
15 #include "controllers/controllerdebug.h"
16 #include "errordialoghandler.h"
17 #include "mixer/playermanager.h"
18 #include "moc_controllerengine.cpp"
19 #include "util/fpclassify.h"
20 #include "util/time.h"
21 
22 // Used for id's inside controlConnection objects
23 // (closure compatible version of connectControl)
24 #include <QUuid>
25 
26 namespace {
27 constexpr int kDecks = 16;
28 
29 // Use 1ms for the Alpha-Beta dt. We're assuming the OS actually gives us a 1ms
30 // timer.
31 constexpr int kScratchTimerMs = 1;
32 constexpr double kAlphaBetaDt = kScratchTimerMs / 1000.0;
33 } // namespace
34 
ControllerEngine(Controller * controller,UserSettingsPointer pConfig)35 ControllerEngine::ControllerEngine(
36         Controller* controller, UserSettingsPointer pConfig)
37         : m_pEngine(nullptr),
38           m_pController(controller),
39           m_pConfig(pConfig),
40           m_bPopups(true),
41           m_pBaClass(nullptr) {
42     // Handle error dialog buttons
43     qRegisterMetaType<QMessageBox::StandardButton>("QMessageBox::StandardButton");
44 
45     // Pre-allocate arrays for average number of virtual decks
46     m_intervalAccumulator.resize(kDecks);
47     m_lastMovement.resize(kDecks);
48     m_dx.resize(kDecks);
49     m_rampTo.resize(kDecks);
50     m_ramp.resize(kDecks);
51     m_scratchFilters.resize(kDecks);
52     m_rampFactor.resize(kDecks);
53     m_brakeActive.resize(kDecks);
54     m_softStartActive.resize(kDecks);
55     // Initialize arrays used for testing and pointers
56     for (int i = 0; i < kDecks; ++i) {
57         m_dx[i] = 0.0;
58         m_scratchFilters[i] = new AlphaBetaFilter();
59         m_ramp[i] = false;
60     }
61 
62     initializeScriptEngine();
63 }
64 
~ControllerEngine()65 ControllerEngine::~ControllerEngine() {
66     // Clean up
67     for (int i = 0; i < kDecks; ++i) {
68         delete m_scratchFilters[i];
69         m_scratchFilters[i] = nullptr;
70     }
71 
72     uninitializeScriptEngine();
73 }
74 
75 /* -------- ------------------------------------------------------
76 Purpose: Calls the same method on a list of JS Objects
77 Input:   -
78 Output:  -
79 -------- ------------------------------------------------------ */
callFunctionOnObjects(const QList<QString> & scriptFunctionPrefixes,const QString & function,const QScriptValueList & args)80 void ControllerEngine::callFunctionOnObjects(const QList<QString>& scriptFunctionPrefixes,
81         const QString& function,
82         const QScriptValueList& args) {
83     VERIFY_OR_DEBUG_ASSERT(m_pEngine) {
84         return;
85     }
86 
87     const QScriptValue global = m_pEngine->globalObject();
88 
89     for (const QString& prefixName : scriptFunctionPrefixes) {
90         QScriptValue prefix = global.property(prefixName);
91         if (!prefix.isValid() || !prefix.isObject()) {
92             qWarning() << "ControllerEngine: No" << prefixName << "object in script";
93             continue;
94         }
95 
96         QScriptValue init = prefix.property(function);
97         if (!init.isValid() || !init.isFunction()) {
98             qWarning() << "ControllerEngine:" << prefixName << "has no" << function << " method";
99             continue;
100         }
101         controllerDebug("ControllerEngine: Executing" << prefixName << "." << function);
102         init.call(prefix, args);
103     }
104 }
105 
106 /* ------------------------------------------------------------------
107 Purpose: Turn a snippet of JS into a QScriptValue function.
108          Wrapping it in an anonymous function allows any JS that
109          evaluates to a function to be used in MIDI mapping XML files
110          and ensures the function is executed with the correct
111          'this' object.
112 Input:   QString snippet of JS that evaluates to a function,
113          int number of arguments that the function takes
114 Output:  QScriptValue of JS snippet wrapped in an anonymous function
115 ------------------------------------------------------------------- */
wrapFunctionCode(const QString & codeSnippet,int numberOfArgs)116 QScriptValue ControllerEngine::wrapFunctionCode(const QString& codeSnippet,
117                                                 int numberOfArgs) {
118     // This function is called from outside the controller engine, so we can't
119     // use VERIFY_OR_DEBUG_ASSERT here
120     if (m_pEngine == nullptr) {
121         return QScriptValue();
122     }
123 
124     QScriptValue wrappedFunction;
125 
126     auto i = m_scriptWrappedFunctionCache.constFind(codeSnippet);
127     if (i != m_scriptWrappedFunctionCache.constEnd()) {
128         wrappedFunction = i.value();
129     } else {
130         QStringList wrapperArgList;
131         for (int i = 1; i <= numberOfArgs; i++) {
132             wrapperArgList << QString("arg%1").arg(i);
133         }
134         QString wrapperArgs = wrapperArgList.join(",");
135         QString wrappedCode = "(function (" + wrapperArgs + ") { (" +
136                                 codeSnippet + ")(" + wrapperArgs + "); })";
137         wrappedFunction = m_pEngine->evaluate(wrappedCode);
138         checkException();
139         m_scriptWrappedFunctionCache[codeSnippet] = wrappedFunction;
140     }
141     return wrappedFunction;
142 }
143 
getThisObjectInFunctionCall()144 QScriptValue ControllerEngine::getThisObjectInFunctionCall() {
145     VERIFY_OR_DEBUG_ASSERT(m_pEngine != nullptr) {
146         return QScriptValue();
147     }
148 
149     QScriptContext *ctxt = m_pEngine->currentContext();
150     // Our current context is a function call. We want to grab the 'this'
151     // from the caller's context, so we walk up the stack.
152     if (ctxt) {
153         ctxt = ctxt->parentContext();
154     }
155     return ctxt ? ctxt->thisObject() : QScriptValue();
156 }
157 
158 /* -------- ------------------------------------------------------
159 Purpose: Shuts down scripts in an orderly fashion
160             (stops timers then executes shutdown functions)
161 Input:   -
162 Output:  -
163 -------- ------------------------------------------------------ */
gracefulShutdown()164 void ControllerEngine::gracefulShutdown() {
165     if (m_pEngine == nullptr) {
166         return;
167     }
168 
169     qDebug() << "ControllerEngine shutting down...";
170 
171     // Stop all timers
172     stopAllTimers();
173 
174     qDebug() << "Invoking shutdown() hook in scripts";
175     callFunctionOnObjects(m_scriptFunctionPrefixes, "shutdown");
176 
177     // Prevents leaving decks in an unstable state
178     //  if the controller is shut down while scratching
179     QHashIterator<int, int> i(m_scratchTimers);
180     while (i.hasNext()) {
181         i.next();
182         qDebug() << "Aborting scratching on deck" << i.value();
183         // Clear scratch2_enable. PlayerManager::groupForDeck is 0-indexed.
184         QString group = PlayerManager::groupForDeck(i.value() - 1);
185         ControlObjectScript* pScratch2Enable =
186                 getControlObjectScript(group, "scratch2_enable");
187         if (pScratch2Enable != nullptr) {
188             pScratch2Enable->set(0);
189         }
190     }
191 
192     qDebug() << "Clearing function wrapper cache";
193     m_scriptWrappedFunctionCache.clear();
194 
195     // Free all the ControlObjectScripts
196     {
197         auto it = m_controlCache.begin();
198         while (it != m_controlCache.end()) {
199             qDebug()
200                     << "Deleting ControlObjectScript"
201                     << it.key().group
202                     << it.key().item;
203             delete it.value();
204             // Advance iterator
205             it = m_controlCache.erase(it);
206         }
207     }
208 
209     delete m_pBaClass;
210     m_pBaClass = nullptr;
211 }
212 
isReady()213 bool ControllerEngine::isReady() {
214     bool ret = m_pEngine != nullptr;
215     return ret;
216 }
217 
initializeScriptEngine()218 void ControllerEngine::initializeScriptEngine() {
219     // Clear any errors from previous script engine usages
220     m_scriptErrors.clear();
221 
222     // Create the Script Engine
223     m_pEngine = new QScriptEngine(this);
224 
225     // Make this ControllerEngine instance available to scripts as 'engine'.
226     QScriptValue engineGlobalObject = m_pEngine->globalObject();
227     engineGlobalObject.setProperty("engine", m_pEngine->newQObject(this));
228 
229     if (m_pController) {
230         qDebug() << "Controller in script engine is:" << m_pController->getName();
231 
232         // Make the Controller instance available to scripts
233         engineGlobalObject.setProperty("controller", m_pEngine->newQObject(m_pController));
234 
235         // ...under the legacy name as well
236         engineGlobalObject.setProperty("midi", m_pEngine->newQObject(m_pController));
237     }
238 
239     QScriptValue constructor = m_pEngine->newFunction(ColorMapperJSProxyConstructor);
240     QScriptValue metaObject = m_pEngine->newQMetaObject(&ColorMapperJSProxy::staticMetaObject, constructor);
241     engineGlobalObject.setProperty("ColorMapper", metaObject);
242 
243     m_pBaClass = new ByteArrayClass(m_pEngine);
244     engineGlobalObject.setProperty("ByteArray", m_pBaClass->constructor());
245 }
246 
uninitializeScriptEngine()247 void ControllerEngine::uninitializeScriptEngine() {
248     // Delete the script engine, first clearing the pointer so that
249     // other threads will not get the dead pointer after we delete it.
250     if (m_pEngine != nullptr) {
251         QScriptEngine* engine = m_pEngine;
252         m_pEngine = nullptr;
253         engine->deleteLater();
254     }
255 }
256 
257 /* -------- ------------------------------------------------------
258    Purpose: Load all script files given in the supplied list
259    Input:   List of script paths and file names to load
260    Output:  Returns true if no errors occurred.
261    -------- ------------------------------------------------------ */
loadScriptFiles(const QList<ControllerPreset::ScriptFileInfo> & scripts)262 bool ControllerEngine::loadScriptFiles(const QList<ControllerPreset::ScriptFileInfo>& scripts) {
263     bool result = true;
264     for (const auto& script : scripts) {
265         if (!evaluate(script.file)) {
266             result = false;
267         }
268 
269         if (m_scriptErrors.contains(script.name)) {
270             qWarning() << "Errors occurred while loading" << script.name;
271         }
272     }
273 
274     m_lastScriptFiles = scripts;
275 
276     connect(&m_scriptWatcher,
277             &QFileSystemWatcher::fileChanged,
278             this,
279             &ControllerEngine::scriptHasChanged);
280 
281     bool success = result && m_scriptErrors.isEmpty();
282     if (!success) {
283         gracefulShutdown();
284         uninitializeScriptEngine();
285     }
286 
287     return success;
288 }
289 
290 // Slot to run when a script file has changed
scriptHasChanged(const QString & scriptFilename)291 void ControllerEngine::scriptHasChanged(const QString& scriptFilename) {
292     Q_UNUSED(scriptFilename);
293     qDebug() << "ControllerEngine: Reloading Scripts";
294     ControllerPresetPointer pPreset = m_pController->getPreset();
295 
296     disconnect(&m_scriptWatcher,
297             &QFileSystemWatcher::fileChanged,
298             this,
299             &ControllerEngine::scriptHasChanged);
300 
301     gracefulShutdown();
302     uninitializeScriptEngine();
303 
304     initializeScriptEngine();
305     if (!loadScriptFiles(m_lastScriptFiles)) {
306         return;
307     }
308 
309     qDebug() << "Re-initializing scripts";
310     initializeScripts(m_lastScriptFiles);
311 }
312 
313 /* -------- ------------------------------------------------------
314    Purpose: Run the initialization function for each loaded script
315                 if it exists
316    Input:   -
317    Output:  -
318    -------- ------------------------------------------------------ */
initializeScripts(const QList<ControllerPreset::ScriptFileInfo> & scripts)319 void ControllerEngine::initializeScripts(const QList<ControllerPreset::ScriptFileInfo>& scripts) {
320 
321     m_scriptFunctionPrefixes.clear();
322     for (const ControllerPreset::ScriptFileInfo& script : scripts) {
323         // Skip empty prefixes.
324         if (!script.functionPrefix.isEmpty()) {
325             m_scriptFunctionPrefixes.append(script.functionPrefix);
326         }
327     }
328 
329     QScriptValueList args;
330     args << QScriptValue(m_pController->getName());
331     args << QScriptValue(ControllerDebug::enabled());
332 
333     // Call the init method for all the prefixes.
334     callFunctionOnObjects(m_scriptFunctionPrefixes, "init", args);
335 
336     // We failed to initialize the controller scripts, shutdown the script
337     // engine to avoid error popups on every button press or slider move
338     if (checkException(true)) {
339         gracefulShutdown();
340         uninitializeScriptEngine();
341     }
342 }
343 
344 /* -------- ------------------------------------------------------
345    Purpose: Validate script syntax, then evaluate() it so the
346             functions are registered & available for use.
347    Input:   -
348    Output:  -
349    -------- ------------------------------------------------------ */
evaluate(const QString & filepath)350 bool ControllerEngine::evaluate(const QString& filepath) {
351     return evaluate(QFileInfo(filepath));
352 }
353 
syntaxIsValid(const QString & scriptCode,const QString & filename)354 bool ControllerEngine::syntaxIsValid(const QString& scriptCode, const QString& filename) {
355     if (m_pEngine == nullptr) {
356         return false;
357     }
358 
359     QScriptSyntaxCheckResult result = m_pEngine->checkSyntax(scriptCode);
360 
361     // Note: Do not translate the error messages that go into the "details"
362     // part of the error dialog. These serve as starting point for mapping
363     // developers and might not always be fluent in the language of mapping
364     // user.
365     QString error;
366     switch (result.state()) {
367         case (QScriptSyntaxCheckResult::Valid): break;
368         case (QScriptSyntaxCheckResult::Intermediate):
369             error = QStringLiteral("Incomplete code");
370             break;
371         case (QScriptSyntaxCheckResult::Error):
372             error = QStringLiteral("Syntax error");
373             break;
374     }
375 
376     // If we didn't encounter an error, exit early
377     if (error.isEmpty()) {
378         return true;
379     }
380 
381     if (filename.isEmpty()) {
382         error = QString("%1 at line %2, column %3")
383                         .arg(error,
384                                 QString::number(result.errorLineNumber()),
385                                 QString::number(result.errorColumnNumber()));
386     } else {
387         error = QString("%1 at line %2, column %3 in file %4")
388                         .arg(error,
389                                 QString::number(result.errorLineNumber()),
390                                 QString::number(result.errorColumnNumber()),
391                                 filename);
392     }
393 
394     QString errorMessage = result.errorMessage();
395     if (!errorMessage.isEmpty()) {
396         error += QStringLiteral("\n\nError:  \n") + errorMessage;
397     }
398 
399     if (filename.isEmpty()) {
400         error += QStringLiteral("\n\nCode:\n") + scriptCode;
401     }
402 
403     qWarning() << "ControllerEngine:" << error;
404     scriptErrorDialog(error, error, true);
405     return false;
406 }
407 
408 /* -------- ------------------------------------------------------
409 Purpose: Evaluate & run script code
410 Input:   'this' object if applicable, Code string
411 Output:  false if an exception
412 -------- ------------------------------------------------------ */
internalExecute(const QScriptValue & thisObject,const QString & scriptCode)413 bool ControllerEngine::internalExecute(
414         const QScriptValue& thisObject, const QString& scriptCode) {
415     // A special version of safeExecute since we're evaluating strings, not actual functions
416     //  (execute() would print an error that it's not a function every time a timer fires.)
417     if (m_pEngine == nullptr) {
418         return false;
419     }
420 
421     if (!syntaxIsValid(scriptCode)) {
422         return false;
423     }
424 
425     QScriptValue scriptFunction = m_pEngine->evaluate(scriptCode);
426 
427     if (checkException()) {
428         qDebug() << "Exception evaluating:" << scriptCode;
429         return false;
430     }
431 
432     if (!scriptFunction.isFunction()) {
433         // scriptCode was plain code called in evaluate above
434         return false;
435     }
436 
437     return internalExecute(thisObject, scriptFunction, QScriptValueList());
438 }
439 
440 /* -------- ------------------------------------------------------
441 Purpose: Evaluate & run script code
442 Input:   'this' object if applicable, Code string
443 Output:  false if an exception
444 -------- ------------------------------------------------------ */
internalExecute(const QScriptValue & thisObject,QScriptValue functionObject,const QScriptValueList & args)445 bool ControllerEngine::internalExecute(const QScriptValue& thisObject,
446         QScriptValue functionObject,
447         const QScriptValueList& args) {
448     if (m_pEngine == nullptr) {
449         qDebug() << "ControllerEngine::execute: No script engine exists!";
450         return false;
451     }
452 
453     if (functionObject.isError()) {
454         qWarning() << "ControllerEngine::internalExecute:"
455                    << functionObject.toString();
456         return false;
457     }
458 
459     // If it's not a function, we're done.
460     if (!functionObject.isFunction()) {
461         qWarning() << "ControllerEngine::internalExecute:"
462                    << functionObject.toVariant() << "Not a function";
463         return false;
464     }
465 
466     // If it does happen to be a function, call it.
467     QScriptValue rc = functionObject.call(thisObject, args);
468     if (!rc.isValid()) {
469         qWarning() << "QScriptValue is not a function or ...";
470         return false;
471     }
472 
473     return !checkException();
474 }
475 
execute(const QScriptValue & functionObject,unsigned char channel,unsigned char control,unsigned char value,unsigned char status,const QString & group,mixxx::Duration timestamp)476 bool ControllerEngine::execute(const QScriptValue& functionObject,
477         unsigned char channel,
478         unsigned char control,
479         unsigned char value,
480         unsigned char status,
481         const QString& group,
482         mixxx::Duration timestamp) {
483     Q_UNUSED(timestamp);
484     if (m_pEngine == nullptr) {
485         return false;
486     }
487     QScriptValueList args;
488     args << QScriptValue(channel);
489     args << QScriptValue(control);
490     args << QScriptValue(value);
491     args << QScriptValue(status);
492     args << QScriptValue(group);
493     return internalExecute(m_pEngine->globalObject(), functionObject, args);
494 }
495 
execute(const QScriptValue & function,const QByteArray & data,mixxx::Duration timestamp)496 bool ControllerEngine::execute(const QScriptValue& function,
497         const QByteArray& data,
498         mixxx::Duration timestamp) {
499     Q_UNUSED(timestamp);
500     if (m_pEngine == nullptr) {
501         return false;
502     }
503     QScriptValueList args;
504     args << m_pBaClass->newInstance(data);
505     args << QScriptValue(data.size());
506     return internalExecute(m_pEngine->globalObject(), function, args);
507 }
508 
509 /* -------- ------------------------------------------------------
510    Purpose: Check to see if a script threw an exception
511    Input:   QScriptValue returned from call(scriptFunctionName)
512    Output:  true if there was an exception
513    -------- ------------------------------------------------------ */
checkException(bool bFatal)514 bool ControllerEngine::checkException(bool bFatal) {
515     if (m_pEngine == nullptr) {
516         return false;
517     }
518 
519     if (m_pEngine->hasUncaughtException()) {
520         QScriptValue exception = m_pEngine->uncaughtException();
521         QString errorMessage = exception.toString();
522         QString line =
523                 QString::number(m_pEngine->uncaughtExceptionLineNumber());
524         QString filename = exception.property("fileName").toString();
525 
526         // Note: Do not translate the error messages that go into the "details"
527         // part of the error dialog. These serve as starting point for mapping
528         // developers and might not always be fluent in the language of mapping
529         // user.
530         QStringList error;
531         error << (filename.isEmpty() ? "" : filename) << errorMessage << line;
532         m_scriptErrors.insert(
533                 (filename.isEmpty() ? "passed code" : filename), error);
534 
535         QString errorText;
536         if (filename.isEmpty()) {
537             errorText = QString("Uncaught exception at line %1 in passed code.").arg(line);
538         } else {
539             errorText = QString("Uncaught exception at line %1 in file %2.").arg(line, filename);
540         }
541 
542         errorText += QStringLiteral("\n\nException:\n  ") + errorMessage;
543 
544         // Do not include backtrace in dialog key because it might contain midi
545         // slider values that will differ most of the time. This would break
546         // the "Ignore" feature of the error dialog.
547         QString key = errorText;
548 
549         // Add backtrace to the error details
550         errorText += QStringLiteral("\n\nBacktrace:\n  ") +
551                 m_pEngine->uncaughtExceptionBacktrace().join("\n  ");
552 
553         scriptErrorDialog(errorText, key, bFatal);
554         m_pEngine->clearExceptions();
555         return true;
556     }
557     return false;
558 }
559 
560 /*  -------- ------------------------------------------------------
561     Purpose: Common error dialog creation code for run-time exceptions
562                 Allows users to ignore the error or reload the mappings
563     Input:   Detailed error string
564     Output:  -
565     -------- ------------------------------------------------------ */
scriptErrorDialog(const QString & detailedError,const QString & key,bool bFatalError)566 void ControllerEngine::scriptErrorDialog(
567         const QString& detailedError, const QString& key, bool bFatalError) {
568     qWarning() << "ControllerEngine:" << detailedError;
569 
570     if (!m_bPopups) {
571         return;
572     }
573 
574     ErrorDialogProperties* props =
575             ErrorDialogHandler::instance()->newDialogProperties();
576 
577     QString additionalErrorText;
578     if (bFatalError) {
579         additionalErrorText =
580                 tr("The functionality provided by this controller mapping will "
581                    "be disabled until the issue has been resolved.");
582     } else {
583         additionalErrorText =
584                 tr("You can ignore this error for this session but "
585                    "you may experience erratic behavior.") +
586                 QString("<br>") +
587                 tr("Try to recover by resetting your controller.");
588     }
589 
590     props->setType(DLG_WARNING);
591     props->setTitle(tr("Controller Mapping Error"));
592     props->setText(QString(tr("The mapping for your controller \"%1\" is not "
593                               "working properly."))
594                            .arg(m_pController->getName()));
595     props->setInfoText(QStringLiteral("<html>") +
596             tr("The script code needs to be fixed.") + QStringLiteral("<p>") +
597             additionalErrorText + QStringLiteral("</p></html>"));
598 
599     // Add "Details" text and set monospace font since they may contain
600     // backtraces and code.
601     props->setDetails(detailedError, true);
602 
603     // To prevent multiple windows for the same error
604     props->setKey(key);
605 
606     // Allow user to suppress further notifications about this particular error
607     if (!bFatalError) {
608         props->addButton(QMessageBox::Ignore);
609         props->addButton(QMessageBox::Retry);
610     }
611     props->addButton(QMessageBox::Close);
612     props->setDefaultButton(QMessageBox::Close);
613     props->setEscapeButton(QMessageBox::Close);
614     props->setModal(false);
615 
616     if (ErrorDialogHandler::instance()->requestErrorDialog(props)) {
617         // Enable custom handling of the dialog buttons
618         connect(ErrorDialogHandler::instance(),
619                 &ErrorDialogHandler::stdButtonClicked,
620                 this,
621                 &ControllerEngine::errorDialogButton);
622     }
623 }
624 
625 /* -------- ------------------------------------------------------
626     Purpose: Slot to handle custom button clicks in error dialogs
627     Input:   Key of dialog, StandardButton that was clicked
628     Output:  -
629     -------- ------------------------------------------------------ */
errorDialogButton(const QString & key,QMessageBox::StandardButton button)630 void ControllerEngine::errorDialogButton(const QString& key, QMessageBox::StandardButton button) {
631     Q_UNUSED(key);
632 
633     // Something was clicked, so disable this signal now
634     disconnect(ErrorDialogHandler::instance(),
635             &ErrorDialogHandler::stdButtonClicked,
636             this,
637             &ControllerEngine::errorDialogButton);
638 
639     if (button == QMessageBox::Retry) {
640         emit resetController();
641     }
642 }
643 
getControlObjectScript(const QString & group,const QString & name)644 ControlObjectScript* ControllerEngine::getControlObjectScript(const QString& group, const QString& name) {
645     ConfigKey key = ConfigKey(group, name);
646 
647     if (!key.isValid()) {
648         qWarning() << "ControllerEngine: Requested control with invalid key" << key;
649         return nullptr;
650     }
651 
652     ControlObjectScript* coScript = m_controlCache.value(key, nullptr);
653     if (coScript == nullptr) {
654         // create COT
655         coScript = new ControlObjectScript(key, this);
656         if (coScript->valid()) {
657             m_controlCache.insert(key, coScript);
658         } else {
659             delete coScript;
660             coScript = nullptr;
661         }
662     }
663     return coScript;
664 }
665 
666 /* -------- ------------------------------------------------------
667    Purpose: Returns the current value of a Mixxx control (for scripts)
668    Input:   Control group (e.g. [Channel1]), Key name (e.g. [filterHigh])
669    Output:  The value
670    -------- ------------------------------------------------------ */
getValue(const QString & group,const QString & name)671 double ControllerEngine::getValue(const QString& group, const QString& name) {
672     ControlObjectScript* coScript = getControlObjectScript(group, name);
673     if (coScript == nullptr) {
674         qWarning() << "ControllerEngine: Unknown control" << group << name << ", returning 0.0";
675         return 0.0;
676     }
677     return coScript->get();
678 }
679 
680 /* -------- ------------------------------------------------------
681    Purpose: Sets new value of a Mixxx control (for scripts)
682    Input:   Control group, Key name, new value
683    Output:  -
684    -------- ------------------------------------------------------ */
setValue(const QString & group,const QString & name,double newValue)685 void ControllerEngine::setValue(const QString& group, const QString& name, double newValue) {
686     if (util_isnan(newValue)) {
687         qWarning() << "ControllerEngine: script setting [" << group << "," << name
688                  << "] to NotANumber, ignoring.";
689         return;
690     }
691 
692     ControlObjectScript* coScript = getControlObjectScript(group, name);
693 
694     if (coScript) {
695         ControlObject* pControl = ControlObject::getControl(
696                 coScript->getKey(), ControlFlag::AllowMissingOrInvalid);
697         if (pControl && !m_st.ignore(pControl, coScript->getParameterForValue(newValue))) {
698             coScript->slotSet(newValue);
699         }
700     }
701 }
702 
703 /* -------- ------------------------------------------------------
704    Purpose: Returns the normalized value of a Mixxx control (for scripts)
705    Input:   Control group (e.g. [Channel1]), Key name (e.g. [filterHigh])
706    Output:  The value
707    -------- ------------------------------------------------------ */
getParameter(const QString & group,const QString & name)708 double ControllerEngine::getParameter(const QString& group, const QString& name) {
709     ControlObjectScript* coScript = getControlObjectScript(group, name);
710     if (coScript == nullptr) {
711         qWarning() << "ControllerEngine: Unknown control" << group << name << ", returning 0.0";
712         return 0.0;
713     }
714     return coScript->getParameter();
715 }
716 
717 /* -------- ------------------------------------------------------
718    Purpose: Sets new normalized parameter of a Mixxx control (for scripts)
719    Input:   Control group, Key name, new value
720    Output:  -
721    -------- ------------------------------------------------------ */
setParameter(const QString & group,const QString & name,double newParameter)722 void ControllerEngine::setParameter(
723         const QString& group, const QString& name, double newParameter) {
724     if (util_isnan(newParameter)) {
725         qWarning() << "ControllerEngine: script setting [" << group << "," << name
726                  << "] to NotANumber, ignoring.";
727         return;
728     }
729 
730     ControlObjectScript* coScript = getControlObjectScript(group, name);
731 
732     if (coScript) {
733         ControlObject* pControl = ControlObject::getControl(
734                 coScript->getKey(), ControlFlag::AllowMissingOrInvalid);
735         if (pControl && !m_st.ignore(pControl, newParameter)) {
736           coScript->setParameter(newParameter);
737         }
738     }
739 }
740 
741 /* -------- ------------------------------------------------------
742    Purpose: normalize a value of a Mixxx control (for scripts)
743    Input:   Control group, Key name, new value
744    Output:  -
745    -------- ------------------------------------------------------ */
getParameterForValue(const QString & group,const QString & name,double value)746 double ControllerEngine::getParameterForValue(
747         const QString& group, const QString& name, double value) {
748     if (util_isnan(value)) {
749         qWarning() << "ControllerEngine: script setting [" << group << "," << name
750                  << "] to NotANumber, ignoring.";
751         return 0.0;
752     }
753 
754     ControlObjectScript* coScript = getControlObjectScript(group, name);
755 
756     if (coScript == nullptr) {
757         qWarning() << "ControllerEngine: Unknown control" << group << name << ", returning 0.0";
758         return 0.0;
759     }
760 
761     return coScript->getParameterForValue(value);
762 }
763 
764 /* -------- ------------------------------------------------------
765    Purpose: Resets the value of a Mixxx control (for scripts)
766    Input:   Control group, Key name, new value
767    Output:  -
768    -------- ------------------------------------------------------ */
reset(const QString & group,const QString & name)769 void ControllerEngine::reset(const QString& group, const QString& name) {
770     ControlObjectScript* coScript = getControlObjectScript(group, name);
771     if (coScript != nullptr) {
772         coScript->reset();
773     }
774 }
775 
776 /* -------- ------------------------------------------------------
777    Purpose: default value of a Mixxx control (for scripts)
778    Input:   Control group, Key name, new value
779    Output:  -
780    -------- ------------------------------------------------------ */
getDefaultValue(const QString & group,const QString & name)781 double ControllerEngine::getDefaultValue(const QString& group, const QString& name) {
782     ControlObjectScript* coScript = getControlObjectScript(group, name);
783 
784     if (coScript == nullptr) {
785         qWarning() << "ControllerEngine: Unknown control" << group << name << ", returning 0.0";
786         return 0.0;
787     }
788 
789     return coScript->getDefault();
790 }
791 
792 /* -------- ------------------------------------------------------
793    Purpose: default parameter of a Mixxx control (for scripts)
794    Input:   Control group, Key name, new value
795    Output:  -
796    -------- ------------------------------------------------------ */
getDefaultParameter(const QString & group,const QString & name)797 double ControllerEngine::getDefaultParameter(const QString& group, const QString& name) {
798     ControlObjectScript* coScript = getControlObjectScript(group, name);
799 
800     if (coScript == nullptr) {
801         qWarning() << "ControllerEngine: Unknown control" << group << name << ", returning 0.0";
802         return 0.0;
803     }
804 
805     return coScript->getParameterForValue(coScript->getDefault());
806 }
807 
808 /* -------- ------------------------------------------------------
809    Purpose: qDebugs script output so it ends up in mixxx.log
810    Input:   String to log
811    Output:  -
812    -------- ------------------------------------------------------ */
log(const QString & message)813 void ControllerEngine::log(const QString& message) {
814     controllerDebug(message);
815 }
816 
817 // Purpose: Connect a ControlObject's valueChanged() signal to a script callback function
818 // Input:   Control group (e.g. '[Channel1]'), Key name (e.g. 'pfl'), script callback
819 // Output:  a ScriptConnectionInvokableWrapper turned into a QtScriptValue.
820 //          The script should store this object to call its
821 //          'disconnect' and 'trigger' methods as needed.
822 //          If unsuccessful, returns undefined.
makeConnection(const QString & group,const QString & name,const QScriptValue & callback)823 QScriptValue ControllerEngine::makeConnection(const QString& group,
824         const QString& name,
825         const QScriptValue& callback) {
826     VERIFY_OR_DEBUG_ASSERT(m_pEngine != nullptr) {
827         qWarning() << "Tried to connect script callback, but there is no script engine!";
828         return QScriptValue();
829     }
830 
831     ControlObjectScript* coScript = getControlObjectScript(group, name);
832     if (coScript == nullptr) {
833         qWarning() << "ControllerEngine: script tried to connect to ControlObject (" +
834                       group + ", " + name +
835                       ") which is non-existent, ignoring.";
836         return QScriptValue();
837     }
838 
839     if (!callback.isFunction()) {
840         qWarning() << "Tried to connect (" + group + ", " + name + ")"
841                    << "to an invalid callback, ignoring.";
842         return QScriptValue();
843     }
844 
845     ScriptConnection connection;
846     connection.key = ConfigKey(group, name);
847     connection.controllerEngine = this;
848     connection.callback = callback;
849     connection.context = getThisObjectInFunctionCall();
850     connection.id = QUuid::createUuid();
851 
852     if (coScript->addScriptConnection(connection)) {
853         return m_pEngine->newQObject(
854             new ScriptConnectionInvokableWrapper(connection),
855             QScriptEngine::ScriptOwnership);
856     }
857 
858     return QScriptValue();
859 }
860 
861 /* -------- ------------------------------------------------------
862    Purpose: Execute a ScriptConnection's callback
863    Input:   the value of the connected ControlObject to pass to the callback
864    -------- ------------------------------------------------------ */
executeCallback(double value) const865 void ScriptConnection::executeCallback(double value) const {
866     QScriptValueList args;
867     args << QScriptValue(value);
868     args << QScriptValue(key.group);
869     args << QScriptValue(key.item);
870     QScriptValue func = callback; // copy function because QScriptValue::call is not const
871     QScriptValue result = func.call(context, args);
872     if (result.isError()) {
873         qWarning() << "ControllerEngine: Invocation of connection " << id.toString()
874                    << "connected to (" + key.group + ", " + key.item + ") failed:"
875                    << result.toString();
876     }
877 }
878 
879 /* -------- ------------------------------------------------------
880    Purpose: (Dis)connects a ScriptConnection
881    Input:   the ScriptConnection to disconnect
882    -------- ------------------------------------------------------ */
removeScriptConnection(const ScriptConnection & connection)883 bool ControllerEngine::removeScriptConnection(const ScriptConnection& connection) {
884     ControlObjectScript* coScript = getControlObjectScript(connection.key.group,
885                                                            connection.key.item);
886 
887     if (m_pEngine == nullptr || coScript == nullptr) {
888         return false;
889     }
890 
891     return coScript->removeScriptConnection(connection);
892 }
893 
disconnect()894 bool ScriptConnectionInvokableWrapper::disconnect() {
895     // if the removeScriptConnection succeeded, the connection has been successfully disconnected
896     bool success = m_scriptConnection.controllerEngine->removeScriptConnection(m_scriptConnection);
897     m_isConnected = !success;
898     return success;
899 }
900 
901 /* -------- ------------------------------------------------------
902    Purpose: Triggers the callback function of a ScriptConnection
903    Input:   the ScriptConnection to trigger
904    -------- ------------------------------------------------------ */
triggerScriptConnection(const ScriptConnection & connection)905 void ControllerEngine::triggerScriptConnection(const ScriptConnection& connection) {
906     if (m_pEngine == nullptr) {
907         return;
908     }
909 
910     ControlObjectScript* coScript = getControlObjectScript(connection.key.group,
911                                                            connection.key.item);
912     if (coScript == nullptr) {
913         return;
914     }
915 
916     connection.executeCallback(coScript->get());
917 }
918 
trigger()919 void ScriptConnectionInvokableWrapper::trigger() {
920     m_scriptConnection.controllerEngine->triggerScriptConnection(m_scriptConnection);
921 }
922 
923 // This function is a legacy version of makeConnection with several alternate
924 // ways of invoking it. The callback function can be passed either as a string of
925 // JavaScript code that evaluates to a function or an actual JavaScript function.
926 // If "true" is passed as a 4th parameter, all connections to the ControlObject
927 // are removed. If a ScriptConnectionInvokableWrapper is passed instead of a callback,
928 // it is disconnected.
929 // WARNING: These behaviors are quirky and confusing, so if you change this function,
930 // be sure to run the ControllerEngineTest suite to make sure you do not break old scripts.
connectControl(const QString & group,const QString & name,const QScriptValue & passedCallback,bool disconnect)931 QScriptValue ControllerEngine::connectControl(const QString& group,
932         const QString& name,
933         const QScriptValue& passedCallback,
934         bool disconnect) {
935     // The passedCallback may or may not actually be a function, so when
936     // the actual callback function is found, store it in this variable.
937     QScriptValue actualCallbackFunction;
938 
939     if (passedCallback.isFunction()) {
940         if (!disconnect) {
941             // skip all the checks below and just make the connection
942             return makeConnection(group, name, passedCallback);
943         }
944         actualCallbackFunction = passedCallback;
945     }
946 
947     ControlObjectScript* coScript = getControlObjectScript(group, name);
948     // This check is redundant with makeConnection, but the
949     // ControlObjectScript is also needed here to check for duplicate connections.
950     if (coScript == nullptr) {
951         if (disconnect) {
952             qWarning() << "ControllerEngine: script tried to disconnect from ControlObject (" +
953                           group + ", " + name + ") which is non-existent, ignoring.";
954         } else {
955             qWarning() << "ControllerEngine: script tried to connect to ControlObject (" +
956                            group + ", " + name + ") which is non-existent, ignoring.";
957         }
958         // This is inconsistent with other failures, which return false.
959         // QScriptValue() with no arguments is undefined in JavaScript.
960         return QScriptValue();
961     }
962 
963     if (passedCallback.isString()) {
964         // This check is redundant with makeConnection, but it must be done here
965         // before evaluating the code string.
966         VERIFY_OR_DEBUG_ASSERT(m_pEngine != nullptr) {
967             qWarning() << "Tried to connect script callback, but there is no script engine!";
968             return QScriptValue(false);
969         }
970 
971         actualCallbackFunction = m_pEngine->evaluate(passedCallback.toString());
972 
973         if (checkException() || !actualCallbackFunction.isFunction()) {
974             qWarning() << "Could not evaluate callback function:"
975                         << passedCallback.toString();
976             return QScriptValue(false);
977         }
978 
979         if (coScript->countConnections() > 0 && !disconnect) {
980             // This is inconsistent with the behavior when passing the callback as
981             // a function, but keep the old behavior to make sure old scripts do
982             // not break.
983             ScriptConnection connection = coScript->firstConnection();
984 
985             qWarning() << "Tried to make duplicate connection between (" +
986                           group + ", " + name + ") and " + passedCallback.toString() +
987                           " but this is not allowed when passing a callback as a string. " +
988                           "If you actually want to create duplicate connections, " +
989                           "use engine.makeConnection. Returning reference to connection " +
990                           connection.id.toString();
991 
992             return m_pEngine->newQObject(
993                 new ScriptConnectionInvokableWrapper(connection),
994                 QScriptEngine::ScriptOwnership);
995         }
996     } else if (passedCallback.isQObject()) {
997         // Assume a ScriptConnection and assume that the script author
998         // wants to disconnect it, regardless of the disconnect parameter
999         // and regardless of whether it is connected to the same ControlObject
1000         // specified by the first two parameters to this function.
1001         QObject *qobject = passedCallback.toQObject();
1002         const QMetaObject *qmeta = qobject->metaObject();
1003 
1004         qWarning() << "QObject passed to engine.connectControl. Assuming it is"
1005                   << "a connection object to disconnect and returning false.";
1006         if (!strcmp(qmeta->className(),
1007                 "ScriptConnectionInvokableWrapper")) {
1008             ScriptConnectionInvokableWrapper* proxy =
1009                     (ScriptConnectionInvokableWrapper*)qobject;
1010             proxy->disconnect();
1011         }
1012         return QScriptValue(false);
1013     }
1014 
1015     // Support removing connections by passing "true" as the last parameter
1016     // to this function, regardless of whether the callback is provided
1017     // as a function or a string.
1018     if (disconnect) {
1019         // There is no way to determine which
1020         // ScriptConnection to disconnect unless the script calls
1021         // ScriptConnectionInvokableWrapper::disconnect(), so
1022         // disconnect all ScriptConnections connected to the
1023         // callback function, even though there may be multiple connections.
1024         coScript->disconnectAllConnectionsToFunction(actualCallbackFunction);
1025         return QScriptValue(true);
1026     }
1027 
1028     // If execution gets this far without returning, make
1029     // a new connection to actualCallbackFunction.
1030     return makeConnection(group, name, actualCallbackFunction);
1031 }
1032 
1033 /* -------- ------------------------------------------------------
1034    DEPRECATED: Use ScriptConnectionInvokableWrapper::trigger instead
1035    Purpose: Emits valueChanged() so all ScriptConnections held by a
1036             ControlObjectScript have their callback executed
1037    Input:   -
1038    Output:  -
1039    -------- ------------------------------------------------------ */
trigger(const QString & group,const QString & name)1040 void ControllerEngine::trigger(const QString& group, const QString& name) {
1041     ControlObjectScript* coScript = getControlObjectScript(group, name);
1042     if (coScript != nullptr) {
1043         coScript->emitValueChanged();
1044     }
1045 }
1046 
1047 /* -------- ------------------------------------------------------
1048    Purpose: Evaluate a script file
1049    Input:   Script filename
1050    Output:  false if the script file has errors or doesn't exist
1051    -------- ------------------------------------------------------ */
evaluate(const QFileInfo & scriptFile)1052 bool ControllerEngine::evaluate(const QFileInfo& scriptFile) {
1053     if (m_pEngine == nullptr) {
1054         return false;
1055     }
1056 
1057     if (!scriptFile.exists()) {
1058         qWarning() << "ControllerEngine: File does not exist:" << scriptFile.absoluteFilePath();
1059         return false;
1060     }
1061     m_scriptWatcher.addPath(scriptFile.absoluteFilePath());
1062 
1063     qDebug() << "ControllerEngine: Loading" << scriptFile.absoluteFilePath();
1064 
1065     // Read in the script file
1066     QString filename = scriptFile.absoluteFilePath();
1067     QFile input(filename);
1068     if (!input.open(QIODevice::ReadOnly)) {
1069         qWarning() << QString("ControllerEngine: Problem opening the script file: %1, error # %2, %3")
1070                 .arg(filename, QString::number(input.error()), input.errorString());
1071         if (m_bPopups) {
1072             // Set up error dialog
1073             ErrorDialogProperties* props = ErrorDialogHandler::instance()->newDialogProperties();
1074             props->setType(DLG_WARNING);
1075             props->setTitle(tr("Controller Mapping File Problem"));
1076             props->setText(tr("The mapping for controller \"%1\" cannot be opened.").arg(m_pController->getName()));
1077             props->setInfoText(tr("The functionality provided by this controller mapping will be disabled until the issue has been resolved."));
1078 
1079             // We usually don't translate the details field, but the cause of
1080             // this problem lies in the user's system (e.g. a permission
1081             // issue). Translating this will help users to fix the issue even
1082             // when they don't speak english.
1083             props->setDetails(tr("File:") + QStringLiteral(" ") + filename +
1084                     QStringLiteral("\n") + tr("Error:") + QStringLiteral(" ") +
1085                     input.errorString());
1086 
1087             // Ask above layer to display the dialog & handle user response
1088             ErrorDialogHandler::instance()->requestErrorDialog(props);
1089         }
1090         return false;
1091     }
1092 
1093     QString scriptCode = "";
1094     scriptCode.append(input.readAll());
1095     scriptCode.append('\n');
1096     input.close();
1097 
1098     // Check syntax
1099     if (!syntaxIsValid(scriptCode, filename)) {
1100         return false;
1101     }
1102 
1103     // Evaluate the code
1104     QScriptValue scriptFunction = m_pEngine->evaluate(scriptCode, filename);
1105 
1106     // Record errors
1107     if (checkException(true)) {
1108         return false;
1109     }
1110 
1111     return true;
1112 }
1113 
hasErrors(const QString & filename)1114 bool ControllerEngine::hasErrors(const QString& filename) {
1115     bool ret = m_scriptErrors.contains(filename);
1116     return ret;
1117 }
1118 
1119 /* -------- ------------------------------------------------------
1120    Purpose: Creates & starts a timer that runs some script code
1121                 on timeout
1122    Input:   Number of milliseconds, script function to call,
1123                 whether it should fire just once
1124    Output:  The timer's ID, 0 if starting it failed
1125    -------- ------------------------------------------------------ */
beginTimer(int interval,const QScriptValue & timerCallback,bool oneShot)1126 int ControllerEngine::beginTimer(int interval, const QScriptValue& timerCallback, bool oneShot) {
1127     if (!timerCallback.isFunction() && !timerCallback.isString()) {
1128         qWarning() << "Invalid timer callback provided to beginTimer."
1129                    << "Valid callbacks are strings and functions.";
1130         return 0;
1131     }
1132 
1133     if (interval < 20) {
1134         qWarning() << "Timer request for" << interval
1135                    << "ms is too short. Setting to the minimum of 20ms.";
1136         interval = 20;
1137     }
1138 
1139     // This makes use of every QObject's internal timer mechanism. Nice, clean,
1140     // and simple. See http://doc.trolltech.com/4.6/qobject.html#startTimer for
1141     // details
1142     int timerId = startTimer(interval);
1143     TimerInfo info;
1144     info.callback = timerCallback;
1145     info.context = getThisObjectInFunctionCall();
1146     info.oneShot = oneShot;
1147     m_timers[timerId] = info;
1148     if (timerId == 0) {
1149         qWarning() << "Script timer could not be created";
1150     } else if (oneShot) {
1151         controllerDebug("Starting one-shot timer:" << timerId);
1152     } else {
1153         controllerDebug("Starting timer:" << timerId);
1154     }
1155     return timerId;
1156 }
1157 
1158 /* -------- ------------------------------------------------------
1159    Purpose: Stops & removes a timer
1160    Input:   ID of timer to stop
1161    Output:  -
1162    -------- ------------------------------------------------------ */
stopTimer(int timerId)1163 void ControllerEngine::stopTimer(int timerId) {
1164     if (!m_timers.contains(timerId)) {
1165         qWarning() << "Killing timer" << timerId << ": That timer does not exist!";
1166         return;
1167     }
1168     controllerDebug("Killing timer:" << timerId);
1169     killTimer(timerId);
1170     m_timers.remove(timerId);
1171 }
1172 
stopAllTimers()1173 void ControllerEngine::stopAllTimers() {
1174     QMutableHashIterator<int, TimerInfo> i(m_timers);
1175     while (i.hasNext()) {
1176         i.next();
1177         stopTimer(i.key());
1178     }
1179 }
1180 
timerEvent(QTimerEvent * event)1181 void ControllerEngine::timerEvent(QTimerEvent *event) {
1182     int timerId = event->timerId();
1183 
1184     // See if this is a scratching timer
1185     if (m_scratchTimers.contains(timerId)) {
1186         scratchProcess(timerId);
1187         return;
1188     }
1189 
1190     auto it = m_timers.constFind(timerId);
1191     if (it == m_timers.constEnd()) {
1192         qWarning() << "Timer" << timerId << "fired but there's no function mapped to it!";
1193         return;
1194     }
1195 
1196     // NOTE(rryan): Do not assign by reference -- make a copy. I have no idea
1197     // why but this causes segfaults in ~QScriptValue while scratching if we
1198     // don't copy here -- even though internalExecute passes the QScriptValues
1199     // by value. *boggle*
1200     const TimerInfo timerTarget = it.value();
1201     if (timerTarget.oneShot) {
1202         stopTimer(timerId);
1203     }
1204 
1205     if (timerTarget.callback.isString()) {
1206         internalExecute(timerTarget.context, timerTarget.callback.toString());
1207     } else if (timerTarget.callback.isFunction()) {
1208         internalExecute(timerTarget.context, timerTarget.callback,
1209                         QScriptValueList());
1210     }
1211 }
1212 
getDeckRate(const QString & group)1213 double ControllerEngine::getDeckRate(const QString& group) {
1214     double rate = 0.0;
1215     ControlObjectScript* pRateRatio = getControlObjectScript(group, "rate_ratio");
1216     if (pRateRatio != nullptr) {
1217         rate = pRateRatio->get();
1218     }
1219 
1220     // See if we're in reverse play
1221     ControlObjectScript* pReverse = getControlObjectScript(group, "reverse");
1222     if (pReverse != nullptr && pReverse->get() == 1) {
1223         rate = -rate;
1224     }
1225     return rate;
1226 }
1227 
isDeckPlaying(const QString & group)1228 bool ControllerEngine::isDeckPlaying(const QString& group) {
1229     ControlObjectScript* pPlay = getControlObjectScript(group, "play");
1230 
1231     if (pPlay == nullptr) {
1232       QString error = QString("Could not getControlObjectScript()");
1233       scriptErrorDialog(error, error);
1234       return false;
1235     }
1236 
1237     return pPlay->get() > 0.0;
1238 }
1239 
1240 /* -------- ------------------------------------------------------
1241     Purpose: Enables scratching for relative controls
1242     Input:   Virtual deck to scratch,
1243              Number of intervals per revolution of the controller wheel,
1244              RPM for the track at normal speed (usually 33+1/3),
1245              (optional) alpha value for the filter,
1246              (optional) beta value for the filter
1247     Output:  -
1248     -------- ------------------------------------------------------ */
scratchEnable(int deck,int intervalsPerRev,double rpm,double alpha,double beta,bool ramp)1249 void ControllerEngine::scratchEnable(int deck, int intervalsPerRev, double rpm,
1250                                      double alpha, double beta, bool ramp) {
1251 
1252     // If we're already scratching this deck, override that with this request
1253     if (m_dx[deck] != 0) {
1254         //qDebug() << "Already scratching deck" << deck << ". Overriding.";
1255         int timerId = m_scratchTimers.key(deck);
1256         killTimer(timerId);
1257         m_scratchTimers.remove(timerId);
1258     }
1259 
1260     // Controller resolution in intervals per second at normal speed.
1261     // (rev/min * ints/rev * mins/sec)
1262     double intervalsPerSecond = (rpm * intervalsPerRev) / 60.0;
1263 
1264     if (intervalsPerSecond == 0.0) {
1265         qWarning() << "Invalid rpm or intervalsPerRev supplied to scratchEnable. Ignoring request.";
1266         return;
1267     }
1268 
1269     m_dx[deck] = 1.0 / intervalsPerSecond;
1270     m_intervalAccumulator[deck] = 0.0;
1271     m_ramp[deck] = false;
1272     m_rampFactor[deck] = 0.001;
1273     m_brakeActive[deck] = false;
1274 
1275     // PlayerManager::groupForDeck is 0-indexed.
1276     QString group = PlayerManager::groupForDeck(deck - 1);
1277 
1278     // Ramp velocity, default to stopped.
1279     double initVelocity = 0.0;
1280 
1281     ControlObjectScript* pScratch2Enable =
1282             getControlObjectScript(group, "scratch2_enable");
1283 
1284     // If ramping is desired, figure out the deck's current speed
1285     if (ramp) {
1286         // See if the deck is already being scratched
1287         if (pScratch2Enable != nullptr && pScratch2Enable->get() == 1) {
1288             // If so, set the filter's initial velocity to the scratch speed
1289             ControlObjectScript* pScratch2 =
1290                     getControlObjectScript(group, "scratch2");
1291             if (pScratch2 != nullptr) {
1292                 initVelocity = pScratch2->get();
1293             }
1294         } else if (isDeckPlaying(group)) {
1295             // If the deck is playing, set the filter's initial velocity to the
1296             // playback speed
1297             initVelocity = getDeckRate(group);
1298         }
1299     }
1300 
1301     // Initialize scratch filter
1302     if (alpha != 0 && beta != 0) {
1303         m_scratchFilters[deck]->init(kAlphaBetaDt, initVelocity, alpha, beta);
1304     } else {
1305         // Use filter's defaults if not specified
1306         m_scratchFilters[deck]->init(kAlphaBetaDt, initVelocity);
1307     }
1308 
1309     // 1ms is shortest possible, OS dependent
1310     int timerId = startTimer(kScratchTimerMs);
1311 
1312     // Associate this virtual deck with this timer for later processing
1313     m_scratchTimers[timerId] = deck;
1314 
1315     // Set scratch2_enable
1316     if (pScratch2Enable != nullptr) {
1317         pScratch2Enable->slotSet(1);
1318     }
1319 }
1320 
1321 /* -------- ------------------------------------------------------
1322     Purpose: Accumulates "ticks" of the controller wheel
1323     Input:   Virtual deck to scratch, interval value (usually +1 or -1)
1324     Output:  -
1325     -------- ------------------------------------------------------ */
scratchTick(int deck,int interval)1326 void ControllerEngine::scratchTick(int deck, int interval) {
1327     m_lastMovement[deck] = mixxx::Time::elapsed();
1328     m_intervalAccumulator[deck] += interval;
1329 }
1330 
1331 /* -------- ------------------------------------------------------
1332     Purpose: Applies the accumulated movement to the track speed
1333     Input:   ID of timer for this deck
1334     Output:  -
1335     -------- ------------------------------------------------------ */
scratchProcess(int timerId)1336 void ControllerEngine::scratchProcess(int timerId) {
1337     int deck = m_scratchTimers[timerId];
1338     // PlayerManager::groupForDeck is 0-indexed.
1339     QString group = PlayerManager::groupForDeck(deck - 1);
1340     AlphaBetaFilter* filter = m_scratchFilters[deck];
1341     if (!filter) {
1342         qWarning() << "Scratch filter pointer is null on deck" << deck;
1343         return;
1344     }
1345 
1346     const double oldRate = filter->predictedVelocity();
1347 
1348     // Give the filter a data point:
1349 
1350     // If we're ramping to end scratching and the wheel hasn't been turned very
1351     // recently (spinback after lift-off,) feed fixed data
1352     if (m_ramp[deck] && !m_softStartActive[deck] &&
1353         ((mixxx::Time::elapsed() - m_lastMovement[deck]) >= mixxx::Duration::fromMillis(1))) {
1354         filter->observation(m_rampTo[deck] * m_rampFactor[deck]);
1355         // Once this code path is run, latch so it always runs until reset
1356         //m_lastMovement[deck] += mixxx::Duration::fromSeconds(1);
1357     } else if (m_softStartActive[deck]) {
1358         // pretend we have moved by (desired rate*default distance)
1359         filter->observation(m_rampTo[deck]*kAlphaBetaDt);
1360     } else {
1361         // This will (and should) be 0 if no net ticks have been accumulated
1362         // (i.e. the wheel is stopped)
1363         filter->observation(m_dx[deck] * m_intervalAccumulator[deck]);
1364     }
1365 
1366     const double newRate = filter->predictedVelocity();
1367 
1368     // Actually do the scratching
1369     ControlObjectScript* pScratch2 = getControlObjectScript(group, "scratch2");
1370     if (pScratch2 == nullptr) {
1371         return; // abort and maybe it'll work on the next pass
1372     }
1373     pScratch2->set(newRate);
1374 
1375     // Reset accumulator
1376     m_intervalAccumulator[deck] = 0;
1377 
1378     // End scratching if we're ramping and the current rate is really close to the rampTo value
1379     if ((m_ramp[deck] && fabs(m_rampTo[deck] - newRate) <= 0.00001) ||
1380         // or if we brake or softStart and have crossed over the desired value,
1381         ((m_brakeActive[deck] || m_softStartActive[deck]) && (
1382             (oldRate > m_rampTo[deck] && newRate < m_rampTo[deck]) ||
1383             (oldRate < m_rampTo[deck] && newRate > m_rampTo[deck]))) ||
1384         // or if the deck was stopped manually during brake or softStart
1385         ((m_brakeActive[deck] || m_softStartActive[deck]) && (!isDeckPlaying(group)))) {
1386         // Not ramping no mo'
1387         m_ramp[deck] = false;
1388 
1389         if (m_brakeActive[deck]) {
1390             // If in brake mode, set scratch2 rate to 0 and turn off the play button.
1391             pScratch2->slotSet(0.0);
1392             ControlObjectScript* pPlay = getControlObjectScript(group, "play");
1393             if (pPlay != nullptr) {
1394                 pPlay->slotSet(0.0);
1395             }
1396         }
1397 
1398         // Clear scratch2_enable to end scratching.
1399         ControlObjectScript* pScratch2Enable =
1400                 getControlObjectScript(group, "scratch2_enable");
1401         if (pScratch2Enable == nullptr) {
1402             return; // abort and maybe it'll work on the next pass
1403         }
1404         pScratch2Enable->slotSet(0);
1405 
1406         // Remove timer
1407         killTimer(timerId);
1408         m_scratchTimers.remove(timerId);
1409 
1410         m_dx[deck] = 0.0;
1411         m_brakeActive[deck] = false;
1412         m_softStartActive[deck] = false;
1413     }
1414 }
1415 
1416 /* -------- ------------------------------------------------------
1417     Purpose: Stops scratching the specified virtual deck
1418     Input:   Virtual deck to stop scratching
1419     Output:  -
1420     -------- ------------------------------------------------------ */
scratchDisable(int deck,bool ramp)1421 void ControllerEngine::scratchDisable(int deck, bool ramp) {
1422     // PlayerManager::groupForDeck is 0-indexed.
1423     QString group = PlayerManager::groupForDeck(deck - 1);
1424 
1425     m_rampTo[deck] = 0.0;
1426 
1427     // If no ramping is desired, disable scratching immediately
1428     if (!ramp) {
1429         // Clear scratch2_enable
1430         ControlObjectScript* pScratch2Enable = getControlObjectScript(group, "scratch2_enable");
1431         if (pScratch2Enable != nullptr) {
1432             pScratch2Enable->slotSet(0);
1433         }
1434         // Can't return here because we need scratchProcess to stop the timer.
1435         // So it's still actually ramping, we just won't hear or see it.
1436     } else if (isDeckPlaying(group)) {
1437         // If so, set the target velocity to the playback speed
1438         m_rampTo[deck] = getDeckRate(group);
1439     }
1440 
1441     m_lastMovement[deck] = mixxx::Time::elapsed();
1442     m_ramp[deck] = true;    // Activate the ramping in scratchProcess()
1443 }
1444 
1445 /* -------- ------------------------------------------------------
1446     Purpose: Tells if the specified deck is currently scratching
1447              (Scripts need this to implement spinback-after-lift-off)
1448     Input:   Virtual deck to inquire about
1449     Output:  True if so
1450     -------- ------------------------------------------------------ */
isScratching(int deck)1451 bool ControllerEngine::isScratching(int deck) {
1452     // PlayerManager::groupForDeck is 0-indexed.
1453     QString group = PlayerManager::groupForDeck(deck - 1);
1454     return getValue(group, "scratch2_enable") > 0;
1455 }
1456 
1457 /*  -------- ------------------------------------------------------
1458     Purpose: [En/dis]ables soft-takeover status for a particular ControlObject
1459     Input:   ControlObject group and key values,
1460                 whether to set the soft-takeover status or not
1461     Output:  -
1462     -------- ------------------------------------------------------ */
softTakeover(const QString & group,const QString & name,bool set)1463 void ControllerEngine::softTakeover(const QString& group, const QString& name, bool set) {
1464     ConfigKey key = ConfigKey(group, name);
1465     ControlObject* pControl = ControlObject::getControl(key, ControlFlag::AllowMissingOrInvalid);
1466     if (!pControl) {
1467         qWarning() << "Failed to" << (set ? "enable" : "disable")
1468                    << "softTakeover for invalid control" << key;
1469         return;
1470     }
1471     if (set) {
1472         m_st.enable(pControl);
1473     } else {
1474         m_st.disable(pControl);
1475     }
1476 }
1477 
1478 /*  -------- ------------------------------------------------------
1479      Purpose: Ignores the next value for the given ControlObject
1480                 This should be called before or after an absolute physical
1481                 control (slider or knob with hard limits) is changed to operate
1482                 on a different ControlObject, allowing it to sync up to the
1483                 soft-takeover state without an abrupt jump.
1484      Input:   ControlObject group and key values
1485      Output:  -
1486      -------- ------------------------------------------------------ */
softTakeoverIgnoreNextValue(const QString & group,const QString & name)1487 void ControllerEngine::softTakeoverIgnoreNextValue(
1488         const QString& group, const QString& name) {
1489     ConfigKey key = ConfigKey(group, name);
1490     ControlObject* pControl = ControlObject::getControl(key, ControlFlag::AllowMissingOrInvalid);
1491     if (!pControl) {
1492         qWarning() << "Failed to call softTakeoverIgnoreNextValue for invalid control" << key;
1493         return;
1494     }
1495 
1496     m_st.ignoreNext(pControl);
1497 }
1498 
1499 /*  -------- ------------------------------------------------------
1500     Purpose: [En/dis]ables spinback effect for the channel
1501     Input:   deck, activate/deactivate, factor (optional),
1502              rate (optional)
1503     Output:  -
1504     -------- ------------------------------------------------------ */
spinback(int deck,bool activate,double factor,double rate)1505 void ControllerEngine::spinback(int deck, bool activate, double factor, double rate) {
1506     // defaults for args set in header file
1507     brake(deck, activate, factor, rate);
1508 }
1509 
1510 /*  -------- ------------------------------------------------------
1511     Purpose: [En/dis]ables brake/spinback effect for the channel
1512     Input:   deck, activate/deactivate, factor (optional),
1513              rate (optional, necessary for spinback)
1514     Output:  -
1515     -------- ------------------------------------------------------ */
brake(int deck,bool activate,double factor,double rate)1516 void ControllerEngine::brake(int deck, bool activate, double factor, double rate) {
1517     // PlayerManager::groupForDeck is 0-indexed.
1518     QString group = PlayerManager::groupForDeck(deck - 1);
1519 
1520     // kill timer when both enabling or disabling
1521     int timerId = m_scratchTimers.key(deck);
1522     killTimer(timerId);
1523     m_scratchTimers.remove(timerId);
1524 
1525     // enable/disable scratch2 mode
1526     ControlObjectScript* pScratch2Enable = getControlObjectScript(group, "scratch2_enable");
1527     if (pScratch2Enable != nullptr) {
1528         pScratch2Enable->slotSet(activate ? 1 : 0);
1529     }
1530 
1531     // used in scratchProcess for the different timer behavior we need
1532     m_brakeActive[deck] = activate;
1533     double initRate = rate;
1534 
1535     if (activate) {
1536         // store the new values for this spinback/brake effect
1537         if (initRate == 1.0) {// then rate is really 1.0 or was set to default
1538             // in /res/common-controller-scripts.js so check for real value,
1539             // taking pitch into account
1540             initRate = getDeckRate(group);
1541         }
1542         // stop ramping at a rate which doesn't produce any audible output anymore
1543         m_rampTo[deck] = 0.01;
1544         // if we are currently softStart()ing, stop it
1545         if (m_softStartActive[deck]) {
1546             m_softStartActive[deck] = false;
1547             AlphaBetaFilter* filter = m_scratchFilters[deck];
1548             if (filter != nullptr) {
1549                 initRate = filter->predictedVelocity();
1550             }
1551         }
1552 
1553         // setup timer and set scratch2
1554         timerId = startTimer(kScratchTimerMs);
1555         m_scratchTimers[timerId] = deck;
1556 
1557         ControlObjectScript* pScratch2 = getControlObjectScript(group, "scratch2");
1558         if (pScratch2 != nullptr) {
1559             pScratch2->slotSet(initRate);
1560         }
1561 
1562         // setup the filter with default alpha and beta*factor
1563         double alphaBrake = 1.0/512;
1564         // avoid decimals for fine adjusting
1565         if (factor>1) {
1566             factor = ((factor-1)/10)+1;
1567         }
1568         double betaBrake = ((1.0/512)/1024)*factor; // default*factor
1569         AlphaBetaFilter* filter = m_scratchFilters[deck];
1570         if (filter != nullptr) {
1571             filter->init(kAlphaBetaDt, initRate, alphaBrake, betaBrake);
1572         }
1573 
1574         // activate the ramping in scratchProcess()
1575         m_ramp[deck] = true;
1576     }
1577 }
1578 
1579 /*  -------- ------------------------------------------------------
1580     Purpose: [En/dis]ables softStart effect for the channel
1581     Input:   deck, activate/deactivate, factor (optional)
1582     Output:  -
1583     -------- ------------------------------------------------------ */
softStart(int deck,bool activate,double factor)1584 void ControllerEngine::softStart(int deck, bool activate, double factor) {
1585     // PlayerManager::groupForDeck is 0-indexed.
1586     QString group = PlayerManager::groupForDeck(deck - 1);
1587 
1588     // kill timer when both enabling or disabling
1589     int timerId = m_scratchTimers.key(deck);
1590     killTimer(timerId);
1591     m_scratchTimers.remove(timerId);
1592 
1593     // enable/disable scratch2 mode
1594     ControlObjectScript* pScratch2Enable = getControlObjectScript(group, "scratch2_enable");
1595     if (pScratch2Enable != nullptr) {
1596         pScratch2Enable->slotSet(activate ? 1 : 0);
1597     }
1598 
1599     // used in scratchProcess for the different timer behavior we need
1600     m_softStartActive[deck] = activate;
1601     double initRate = 0.0;
1602 
1603     if (activate) {
1604         // acquire deck rate
1605         m_rampTo[deck] = getDeckRate(group);
1606 
1607         // if brake()ing, get current rate from filter
1608         if (m_brakeActive[deck]) {
1609             m_brakeActive[deck] = false;
1610 
1611             AlphaBetaFilter* filter = m_scratchFilters[deck];
1612             if (filter != nullptr) {
1613                 initRate = filter->predictedVelocity();
1614             }
1615         }
1616 
1617         // setup timer, start playing and set scratch2
1618         timerId = startTimer(kScratchTimerMs);
1619         m_scratchTimers[timerId] = deck;
1620 
1621         ControlObjectScript* pPlay = getControlObjectScript(group, "play");
1622         if (pPlay != nullptr) {
1623             pPlay->slotSet(1.0);
1624         }
1625 
1626         ControlObjectScript* pScratch2 = getControlObjectScript(group, "scratch2");
1627         if (pScratch2 != nullptr) {
1628             pScratch2->slotSet(initRate);
1629         }
1630 
1631         // setup the filter like in brake(), with default alpha and beta*factor
1632         double alphaSoft = 1.0/512;
1633         // avoid decimals for fine adjusting
1634         if (factor>1) {
1635             factor = ((factor-1)/10)+1;
1636         }
1637         double betaSoft = ((1.0/512)/1024)*factor; // default: (1.0/512)/1024
1638         AlphaBetaFilter* filter = m_scratchFilters[deck];
1639         if (filter != nullptr) { // kAlphaBetaDt = 1/1000 seconds
1640             filter->init(kAlphaBetaDt, initRate, alphaSoft, betaSoft);
1641         }
1642 
1643         // activate the ramping in scratchProcess()
1644         m_ramp[deck] = true;
1645     }
1646 }
1647