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