1
/***************************************************************************
2
controllerengine.cpp - description
4
begin : Sat Apr 30 2011
5
copyright : (C) 2011 by Sean M. Pappalardo
6
email : spappalardo@mixxx.org
7
***************************************************************************/
9
#include "controllers/controllerengine.h"
11
#include "controllers/controller.h"
12
#include "controllers/defs_controllers.h"
13
#include "controlobject.h"
14
#include "controlobjectthread.h"
15
#include "errordialoghandler.h"
17
// #include <QScriptSyntaxCheckResult>
20
#include <float.h> // for _isnan() on VC++
21
#define isnan(x) _isnan(x) // VC++ uses _isnan() instead of isnan()
23
#include <math.h> // for isnan() everywhere else
26
const int kDecks = 16;
28
ControllerEngine::ControllerEngine(Controller* controller)
30
m_pController(controller),
35
// Handle error dialog buttons
36
qRegisterMetaType<QMessageBox::StandardButton>("QMessageBox::StandardButton");
38
// Pre-allocate arrays for average number of virtual decks
39
m_intervalAccumulator.resize(kDecks);
41
m_rampTo.resize(kDecks);
42
m_ramp.resize(kDecks);
43
m_pitchFilter.resize(kDecks);
45
// Initialize arrays used for testing and pointers
46
for (int i=0; i < kDecks; i++) {
48
m_pitchFilter[i] = new PitchFilter();
52
initializeScriptEngine();
55
ControllerEngine::~ControllerEngine() {
57
for (int i=0; i < kDecks; i++) {
58
delete m_pitchFilter[i];
59
m_pitchFilter[i] = NULL;
62
// Delete the script engine, first clearing the pointer so that
63
// other threads will not get the dead pointer after we delete it.
64
if (m_pEngine != NULL) {
65
QScriptEngine *engine = m_pEngine;
67
engine->deleteLater();
71
/* -------- ------------------------------------------------------
72
Purpose: Shuts down scripts in an orderly fashion
73
(stops timers then executes shutdown functions)
76
-------- ------------------------------------------------------ */
77
void ControllerEngine::gracefulShutdown() {
78
qDebug() << "ControllerEngine shutting down...";
80
// Clear the m_connectedControls hash so we stop responding
82
m_connectedControls.clear();
87
// Call each script's shutdown function if it exists
88
foreach (QString prefix, m_scriptFunctionPrefixes) {
92
QString shutdownName = QString("%1.shutdown").arg(prefix);
94
qDebug() << " Executing" << shutdownName;;
96
if (!internalExecute(shutdownName)) {
97
qWarning() << "ControllerEngine: No" << shutdownName << "function in script";
101
// Prevents leaving decks in an unstable state if the controller is shut
102
// down while scratching
103
QHashIterator<int, int> i(m_scratchTimers);
104
while (i.hasNext()) {
106
qDebug() << " Aborting scratching on deck" << i.value();
107
// Clear scratch2_enable
108
QString group = QString("[Channel%1]").arg(i.value());
109
ControlObjectThread *cot = getControlObjectThread(group, "scratch2_enable");
115
// Free all the control object threads
116
QList<ConfigKey> keys = m_controlCache.keys();
117
QList<ConfigKey>::iterator it = keys.begin();
118
QList<ConfigKey>::iterator end = keys.end();
121
ControlObjectThread *cot = m_controlCache.take(key);
130
bool ControllerEngine::isReady() {
131
bool ret = m_pEngine != NULL;
135
void ControllerEngine::initializeScriptEngine() {
136
// Create the script engine
137
m_pEngine = new QScriptEngine(this);
139
// Make this ControllerEngine instance available to scripts as 'engine'.
140
QScriptValue engineGlobalObject = m_pEngine->globalObject();
141
engineGlobalObject.setProperty("engine", m_pEngine->newQObject(this));
144
qDebug() << "Controller in script engine is:" << m_pController->getName();
146
// Make the Controller instance available to scripts
147
engineGlobalObject.setProperty("controller", m_pEngine->newQObject(m_pController));
148
// ...under the legacy name as well
149
engineGlobalObject.setProperty("midi", m_pEngine->newQObject(m_pController));
152
m_pBaClass = new ByteArrayClass(m_pEngine);
153
engineGlobalObject.setProperty("ByteArray", m_pBaClass->constructor());
156
/* -------- ------------------------------------------------------
157
Purpose: Load all script files given in the supplied list
158
Input: Global ConfigObject, QString list of file names to load
160
-------- ------------------------------------------------------ */
161
void ControllerEngine::loadScriptFiles(QString configPath,
162
QList<QString> scriptFileNames) {
163
// Set the Debug flag
165
m_bDebug = m_pController->debugging();
167
qDebug() << "ControllerEngine: Loading & evaluating all script code";
169
// scriptPaths holds the paths to search in when we're looking for scripts
170
QList<QString> scriptPaths;
171
scriptPaths.append(USER_PRESETS_PATH);
172
scriptPaths.append(LOCAL_PRESETS_PATH);
173
scriptPaths.append(configPath.append("controllers/"));
175
foreach (QString curScriptFileName, scriptFileNames) {
176
evaluate(curScriptFileName, scriptPaths);
178
if (m_scriptErrors.contains(curScriptFileName)) {
179
qDebug() << "Errors occured while loading " << curScriptFileName;
186
/* -------- ------------------------------------------------------
187
Purpose: Run the initialization function for each loaded script
191
-------- ------------------------------------------------------ */
192
void ControllerEngine::initializeScripts(QList<QString> scriptFunctionPrefixes) {
193
m_scriptFunctionPrefixes = scriptFunctionPrefixes;
195
foreach (QString prefix, m_scriptFunctionPrefixes) {
199
QString initMethod = QString("%1.init").arg(prefix);
201
qDebug() << "ControllerEngine: Executing" << initMethod;
204
QScriptValueList args;
205
args << QScriptValue(m_pController->getName());
206
args << QScriptValue(m_bDebug);
207
if (!execute(initMethod, args)) {
208
qWarning() << "ControllerEngine: No" << initMethod << "function in script";
215
/* -------- ------------------------------------------------------
216
Purpose: Validate script syntax, then evaluate() it so the
217
functions are registered & available for use.
220
-------- ------------------------------------------------------ */
221
bool ControllerEngine::evaluate(QString filepath) {
222
QList<QString> dummy;
223
bool ret = evaluate(filepath, dummy);
228
/* -------- ------------------------------------------------------
229
Purpose: Evaluate & call a script function
231
Output: false if an invalid function or an exception
232
-------- ------------------------------------------------------ */
233
bool ControllerEngine::execute(QString function) {
234
if (m_pEngine == NULL)
237
QScriptValue scriptFunction = m_pEngine->evaluate(function);
239
if (checkException())
242
if (!scriptFunction.isFunction())
245
scriptFunction.call(QScriptValue());
246
if (checkException())
253
/* -------- ------------------------------------------------------
254
Purpose: Evaluate & run script code
256
Output: false if an exception
257
-------- ------------------------------------------------------ */
258
bool ControllerEngine::internalExecute(QString scriptCode) {
259
// A special version of execute since we're evaluating strings, not actual functions
260
// (execute() would print an error that it's not a function every time a timer fires.)
261
if (m_pEngine == NULL)
265
QScriptSyntaxCheckResult result = m_pEngine->checkSyntax(scriptCode);
267
switch (result.state()) {
268
case (QScriptSyntaxCheckResult::Valid): break;
269
case (QScriptSyntaxCheckResult::Intermediate):
270
error = "Incomplete code";
272
case (QScriptSyntaxCheckResult::Error):
273
error = "Syntax error";
277
error = QString("%1: %2 at line %3, column %4 of script code:\n%5\n")
279
result.errorMessage(),
280
QString::number(result.errorLineNumber()),
281
QString::number(result.errorColumnNumber()),
285
qCritical() << "ControllerEngine:" << error;
287
scriptErrorDialog(error);
292
QScriptValue scriptFunction = m_pEngine->evaluate(scriptCode);
294
if (checkException()) {
295
qDebug() << "Exception";
299
// If it's not a function, we're done.
300
if (!scriptFunction.isFunction()) {
304
// If it does happen to be a function, call it.
305
scriptFunction.call(QScriptValue());
306
if (checkException()) {
307
qDebug() << "Exception";
314
/**-------- ------------------------------------------------------
315
Purpose: Evaluate & call a script function with argument list
316
Input: Function name, argument list
317
Output: false if an invalid function or an exception
318
-------- ------------------------------------------------------ */
319
bool ControllerEngine::execute(QString function, QScriptValueList args) {
320
if(m_pEngine == NULL) {
321
qDebug() << "ControllerEngine::execute: No script engine exists!";
325
QScriptValue scriptFunction = m_pEngine->evaluate(function);
327
if (checkException())
329
if (!scriptFunction.isFunction())
332
scriptFunction.call(QScriptValue(), args);
334
if (checkException())
339
/**-------- ------------------------------------------------------
340
Purpose: Evaluate & call a script function
341
Input: Function name, data string (e.g. device ID)
342
Output: false if an invalid function or an exception
343
-------- ------------------------------------------------------ */
344
bool ControllerEngine::execute(QString function, QString data) {
345
if (m_pEngine == NULL) {
346
qDebug() << "ControllerEngine::execute: No script engine exists!";
350
QScriptValue scriptFunction = m_pEngine->evaluate(function);
352
if (checkException()) {
353
qDebug() << "ControllerEngine::execute: Exception";
357
if (!scriptFunction.isFunction()) {
358
qDebug() << "ControllerEngine::execute: Not a function";
362
QScriptValueList args;
363
args << QScriptValue(data);
365
scriptFunction.call(QScriptValue(), args);
366
if (checkException()) {
367
qDebug() << "ControllerEngine::execute: Exception";
373
/**-------- ------------------------------------------------------
374
Purpose: Evaluate & call a script function
375
Input: Function name, ponter to data buffer, length of buffer
376
Output: false if an invalid function or an exception
377
-------- ------------------------------------------------------ */
378
bool ControllerEngine::execute(QString function, const QByteArray data) {
379
if (m_pEngine == NULL) {
383
if (!m_pEngine->canEvaluate(function)) {
384
qCritical() << "ControllerEngine: ?Syntax error in function" << function;
388
QScriptValue scriptFunction = m_pEngine->evaluate(function);
390
if (checkException())
392
if (!scriptFunction.isFunction())
395
// const char* buffer=reinterpret_cast<const char*>(data);
397
QScriptValueList args;
398
// args << QScriptValue(data);
399
args << QScriptValue(m_pBaClass->newInstance(data));
400
args << QScriptValue(data.size());
401
// args << QScriptValue(m_pBaClass->newInstance(QByteArray::fromRawData(buffer,length)));
402
// args << QScriptValue(length);
404
scriptFunction.call(QScriptValue(), args);
405
if (checkException())
410
/* -------- ------------------------------------------------------
411
Purpose: Check to see if a script threw an exception
412
Input: QScriptValue returned from call(scriptFunctionName)
413
Output: true if there was an exception
414
-------- ------------------------------------------------------ */
415
bool ControllerEngine::checkException() {
416
if(m_pEngine == NULL) {
420
if (m_pEngine->hasUncaughtException()) {
421
QScriptValue exception = m_pEngine->uncaughtException();
422
QString errorMessage = exception.toString();
423
int line = m_pEngine->uncaughtExceptionLineNumber();
424
QStringList backtrace = m_pEngine->uncaughtExceptionBacktrace();
425
QString filename = exception.property("fileName").toString();
428
error << (filename.isEmpty() ? "" : filename) << errorMessage << QString(line);
429
m_scriptErrors.insert((filename.isEmpty() ? "passed code" : filename), error);
431
QString errorText = QString(tr("Uncaught exception at line %1 in file %2: %3"))
432
.arg(QString::number(line),
433
(filename.isEmpty() ? "" : filename),
436
if (filename.isEmpty())
437
errorText = QString(tr("Uncaught exception at line %1 in passed code: %2"))
438
.arg(QString::number(line), errorMessage);
441
qCritical() << "ControllerEngine:" << errorText
444
else scriptErrorDialog(errorText);
450
/* -------- ------------------------------------------------------
451
Purpose: Common error dialog creation code for run-time exceptions
452
Allows users to ignore the error or reload the mappings
453
Input: Detailed error string
455
-------- ------------------------------------------------------ */
456
void ControllerEngine::scriptErrorDialog(QString detailedError) {
457
qWarning() << "ControllerEngine:" << detailedError;
458
ErrorDialogProperties* props = ErrorDialogHandler::instance()->newDialogProperties();
459
props->setType(DLG_WARNING);
460
props->setTitle(tr("Controller script error"));
461
props->setText(tr("A control you just used is not working properly."));
462
props->setInfoText(tr("<html>(The script code needs to be fixed.)"
463
"<br>For now, you can:<ul><li>Ignore this error for this session but you may experience erratic behavior</li>"
464
"<li>Try to recover by resetting your controller</li></ul></html>"));
465
props->setDetails(detailedError);
466
props->setKey(detailedError); // To prevent multiple windows for the same error
468
// Allow user to suppress further notifications about this particular error
469
props->addButton(QMessageBox::Ignore);
471
props->addButton(QMessageBox::Retry);
472
props->addButton(QMessageBox::Close);
473
props->setDefaultButton(QMessageBox::Close);
474
props->setEscapeButton(QMessageBox::Close);
475
props->setModal(false);
477
if (ErrorDialogHandler::instance()->requestErrorDialog(props)) {
478
// Enable custom handling of the dialog buttons
479
connect(ErrorDialogHandler::instance(), SIGNAL(stdButtonClicked(QString, QMessageBox::StandardButton)),
480
this, SLOT(errorDialogButton(QString, QMessageBox::StandardButton)));
484
/* -------- ------------------------------------------------------
485
Purpose: Slot to handle custom button clicks in error dialogs
486
Input: Key of dialog, StandardButton that was clicked
488
-------- ------------------------------------------------------ */
489
void ControllerEngine::errorDialogButton(QString key, QMessageBox::StandardButton button) {
492
// Something was clicked, so disable this signal now
493
disconnect(ErrorDialogHandler::instance(),
494
SIGNAL(stdButtonClicked(QString, QMessageBox::StandardButton)),
496
SLOT(errorDialogButton(QString, QMessageBox::StandardButton)));
498
if (button == QMessageBox::Retry) {
499
emit(resetController());
503
/* -------- ------------------------------------------------------
504
Purpose: Returns a list of functions available in the QtScript
507
Output: functionList QStringList
508
-------- ------------------------------------------------------ */
509
QStringList ControllerEngine::getScriptFunctions() {
510
QStringList ret = m_scriptFunctions;
514
void ControllerEngine::generateScriptFunctions(QString scriptCode) {
515
// QStringList functionList;
516
QStringList codeLines = scriptCode.split("\n");
518
// qDebug() << "ControllerEngine: m_scriptCode=" << m_scriptCode;
521
qDebug() << "ControllerEngine:" << codeLines.count() << "lines of code being searched for functions";
523
// grep 'function' midi/midi-mappings-scripts.js|grep -i '(msg)'|sed -e 's/function \(.*\)(msg).*/\1/i' -e 's/[= ]//g'
524
QRegExp rx("*.*function*(*)*"); // Find all lines with function names in them
525
rx.setPatternSyntax(QRegExp::Wildcard);
527
int position = codeLines.indexOf(rx);
529
while (position != -1) { // While there are more matches
531
QString line = codeLines.takeAt(position); // Pull & remove the current match from the list.
533
if (line.indexOf('#') != 0 && line.indexOf("//") != 0) { // ignore commented out lines
534
QStringList field = line.split(" ");
535
if (m_bDebug) qDebug() << "ControllerEngine: Found function:" << field[0]
536
<< "at line" << position;
537
m_scriptFunctions.append(field[0]);
539
position = codeLines.indexOf(rx);
543
ControlObjectThread* ControllerEngine::getControlObjectThread(QString group, QString name) {
544
ConfigKey key = ConfigKey(group, name);
546
ControlObjectThread *cot = NULL;
547
if(!m_controlCache.contains(key)) {
548
ControlObject *co = ControlObject::getControl(key);
550
cot = new ControlObjectThread(co);
551
m_controlCache.insert(key, cot);
554
cot = m_controlCache.value(key);
560
/* -------- ------------------------------------------------------
561
Purpose: Returns the current value of a Mixxx control (for scripts)
562
Input: Control group (e.g. [Channel1]), Key name (e.g. [filterHigh])
564
-------- ------------------------------------------------------ */
565
double ControllerEngine::getValue(QString group, QString name) {
566
ControlObjectThread *cot = getControlObjectThread(group, name);
568
qWarning() << "ControllerEngine: Unknown control" << group << name << ", returning 0.0";
574
/* -------- ------------------------------------------------------
575
Purpose: Sets new value of a Mixxx control (for scripts)
576
Input: Control group, Key name, new value
578
-------- ------------------------------------------------------ */
579
void ControllerEngine::setValue(QString group, QString name, double newValue) {
580
if (isnan(newValue)) {
581
qWarning() << "ControllerEngine: script setting [" << group << "," << name
582
<< "] to NotANumber, ignoring.";
586
ControlObjectThread *cot = getControlObjectThread(group, name);
588
if (cot != NULL && !m_st.ignore(cot->getControlObject(), newValue)) {
589
cot->slotSet(newValue);
590
// We call emitValueChanged so that script functions connected to this
591
// control will get updates.
592
cot->emitValueChanged();
596
/* -------- ------------------------------------------------------
597
Purpose: qDebugs script output so it ends up in mixxx.log
600
-------- ------------------------------------------------------ */
601
void ControllerEngine::log(QString message) {
605
/* -------- ------------------------------------------------------
606
Purpose: Emits valueChanged() so device outputs update
609
-------- ------------------------------------------------------ */
610
void ControllerEngine::trigger(QString group, QString name) {
611
ControlObjectThread *cot = getControlObjectThread(group, name);
613
cot->emitValueChanged();
617
/**-------- ------------------------------------------------------
618
Purpose: (Dis)connects a ControlObject valueChanged() signal to/from a script function
619
Input: Control group (e.g. [Channel1]), Key name (e.g. [filterHigh]),
620
script function name, true if you want to disconnect
621
Output: true if successful
622
-------- ------------------------------------------------------ */
623
bool ControllerEngine::connectControl(QString group, QString name, QString function, bool disconnect) {
624
ControlObjectThread* cobj = getControlObjectThread(group, name);
625
ConfigKey key(group, name);
628
qWarning() << "ControllerEngine: script connecting [" << group << "," << name
629
<< "], which is non-existent. ignoring.";
633
// Don't add duplicates
634
if (!disconnect && m_connectedControls.contains(key, function)) {
638
if (m_pEngine == NULL) {
642
QScriptValue slot = m_pEngine->evaluate(function);
644
if (!checkException() && slot.isFunction()) {
646
//qDebug() << "ControllerEngine::connectControl disconnected " << group << name << " from " << function;
647
m_connectedControls.remove(key, function);
648
// Only disconnect the signal if there are no other instances of this control using it
649
if (!m_connectedControls.contains(key)) {
650
this->disconnect(cobj, SIGNAL(valueChanged(double)),
651
this, SLOT(slotValueChanged(double)));
654
//qDebug() << "ControllerEngine::connectControl connected " << group << name << " to " << function;
655
connect(cobj, SIGNAL(valueChanged(double)),
656
this, SLOT(slotValueChanged(double)));
657
m_connectedControls.insert(key, function);
664
/**-------- ------------------------------------------------------
665
Purpose: Receives valueChanged() slots from ControlObjects, and
666
fires off the appropriate script function.
667
-------- ------------------------------------------------------ */
668
void ControllerEngine::slotValueChanged(double value) {
669
ControlObjectThread* sender = dynamic_cast<ControlObjectThread*>(this->sender());
670
if (sender == NULL) {
671
qWarning() << "ControllerEngine::slotValueChanged() Shouldn't happen -- sender == NULL";
675
ControlObject* pSenderCO = sender->getControlObject();
676
if (pSenderCO == NULL) {
677
qWarning() << "ControllerEngine::slotValueChanged() The sender's CO is NULL.";
680
ConfigKey key = pSenderCO->getKey();
682
if (m_connectedControls.contains(key)) {
683
QMultiHash<ConfigKey, QString>::iterator i = m_connectedControls.find(key);
684
while (i != m_connectedControls.end() && i.key() == key) {
685
QString function = i.value();
687
//qDebug() << "ControllerEngine::slotValueChanged() received signal from " << key.group << key.item << " ... firing : " << function;
689
// Could branch to execute from here, but for now do it this way.
690
QScriptValue function_value = m_pEngine->evaluate(function);
691
QScriptValueList args;
692
args << QScriptValue(value);
693
args << QScriptValue(key.group); // Added by Math`
694
args << QScriptValue(key.item); // Added by Math`
695
QScriptValue result = function_value.call(QScriptValue(), args);
696
if (result.isError()) {
697
qWarning()<< "ControllerEngine: Call to " << function << " resulted in an error: " << result.toString();
702
qWarning() << "ControllerEngine::slotValueChanged() Received signal from ControlObject that is not connected to a script function.";
706
/* -------- ------------------------------------------------------
707
Purpose: Evaluate a script file
708
Input: Script filename
709
Output: false if the script file has errors or doesn't exist
710
-------- ------------------------------------------------------ */
711
bool ControllerEngine::evaluate(QString scriptName, QList<QString> scriptPaths) {
712
if (m_pEngine == NULL) {
716
QString filename = "";
719
if (scriptPaths.length() == 0) {
720
// If we aren't given any paths to search, assume that scriptName
721
// contains the full file name
722
filename = scriptName;
723
input.setFileName(filename);
725
foreach (QString scriptPath, scriptPaths) {
726
QDir scriptPathDir(scriptPath);
727
filename = scriptPathDir.absoluteFilePath(scriptName);
728
input.setFileName(filename);
729
if (input.exists()) {
735
qDebug() << "ControllerEngine: Loading" << filename;
737
// Read in the script file
738
if (!input.open(QIODevice::ReadOnly)) {
740
QString("ControllerEngine: Problem opening the script file: %1, error # %2, %3")
741
.arg(filename, QString("%1").arg(input.error()), input.errorString());
744
qCritical() << errorLog;
746
qWarning() << errorLog;
748
// Set up error dialog
749
ErrorDialogProperties* props = ErrorDialogHandler::instance()->newDialogProperties();
750
props->setType(DLG_WARNING);
751
props->setTitle("Controller script file problem");
752
props->setText(QString("There was a problem opening the controller script file %1.").arg(filename));
753
props->setInfoText(input.errorString());
755
// Ask above layer to display the dialog & handle user response
756
ErrorDialogHandler::instance()->requestErrorDialog(props);
762
QString scriptCode = "";
763
scriptCode.append(input.readAll());
764
scriptCode.append('\n');
768
QScriptSyntaxCheckResult result = m_pEngine->checkSyntax(scriptCode);
770
switch (result.state()) {
771
case (QScriptSyntaxCheckResult::Valid): break;
772
case (QScriptSyntaxCheckResult::Intermediate):
773
error = "Incomplete code";
775
case (QScriptSyntaxCheckResult::Error):
776
error = "Syntax error";
780
error = QString("%1 at line %2, column %3 in file %4: %5")
782
QString::number(result.errorLineNumber()),
783
QString::number(result.errorColumnNumber()),
784
filename, result.errorMessage());
787
qCritical() << "ControllerEngine:" << error;
789
qWarning() << "ControllerEngine:" << error;
791
ErrorDialogProperties* props = ErrorDialogHandler::instance()->newDialogProperties();
792
props->setType(DLG_WARNING);
793
props->setTitle("Controller script file error");
794
props->setText(QString("There was an error in the controller script file %1.").arg(filename));
795
props->setInfoText("The functionality provided by this script file will be disabled.");
796
props->setDetails(error);
798
ErrorDialogHandler::instance()->requestErrorDialog(props);
805
QScriptValue scriptFunction = m_pEngine->evaluate(scriptCode, filename);
808
if (checkException()) {
812
// Add the code we evaluated to our index
813
generateScriptFunctions(scriptCode);
819
* Check whether a source file that was evaluated()'d has errors.
821
bool ControllerEngine::hasErrors(QString filename) {
822
bool ret = m_scriptErrors.contains(filename);
827
* Get the errors for a source file that was evaluated()'d
829
const QStringList ControllerEngine::getErrors(QString filename) {
830
QStringList ret = m_scriptErrors.value(filename, QStringList());
835
/* -------- ------------------------------------------------------
836
Purpose: Creates & starts a timer that runs some script code
838
Input: Number of milliseconds, script function to call,
839
whether it should fire just once
840
Output: The timer's ID, 0 if starting it failed
841
-------- ------------------------------------------------------ */
842
int ControllerEngine::beginTimer(int interval, QString scriptCode, bool oneShot) {
844
qWarning() << "Timer request for" << interval << "ms is too short. Setting to the minimum of 20ms.";
847
// This makes use of every QObject's internal timer mechanism. Nice, clean, and simple.
848
// See http://doc.trolltech.com/4.6/qobject.html#startTimer for details
849
int timerId = startTimer(interval);
850
QPair<QString, bool> timerTarget;
851
timerTarget.first = scriptCode;
852
// timerTarget.second = oneShot;
853
m_timers[timerId]=timerTarget;
856
qWarning() << "Controller script timer could not be created";
857
} else if (m_bDebug) {
858
qDebug() << "Starting" << (oneShot ? "one-shot timer:" : "timer:")
864
/* -------- ------------------------------------------------------
865
Purpose: Stops & removes a timer
866
Input: ID of timer to stop
868
-------- ------------------------------------------------------ */
869
void ControllerEngine::stopTimer(int timerId) {
870
if (!m_timers.contains(timerId)) {
871
qWarning() << "Killing timer" << timerId << ": That timer does not exist!";
875
qDebug() << "Killing timer:" << timerId;
879
m_timers.remove(timerId);
882
/* -------- ------------------------------------------------------
883
Purpose: Stops & removes all timers (for shutdown)
886
-------- ------------------------------------------------------ */
887
void ControllerEngine::stopAllTimers() {
888
QMutableHashIterator<int, QPair<QString, bool> > i(m_timers);
889
while (i.hasNext()) {
895
/* -------- ------------------------------------------------------
896
Purpose: Runs the appropriate script code on timer events
899
-------- ------------------------------------------------------ */
900
void ControllerEngine::timerEvent(QTimerEvent *event) {
901
int timerId = event->timerId();
903
// See if this is a scratching timer
904
if (m_scratchTimers.contains(timerId)) {
905
scratchProcess(timerId);
909
if (!m_timers.contains(timerId)) {
910
qWarning() << "Timer" << timerId << "fired but there's no function mapped to it!";
914
QPair<QString, bool> timerTarget = m_timers[timerId];
915
if (timerTarget.second) {
919
internalExecute(timerTarget.first);
922
/* -------- ------------------------------------------------------
923
Purpose: Enables scratching for relative controls
924
Input: Virtual deck to scratch,
925
Number of intervals per revolution of the controller wheel,
926
RPM for the track at normal speed (usually 33+1/3),
927
(optional) alpha value for the filter,
928
(optional) beta value for the filter
930
-------- ------------------------------------------------------ */
931
void ControllerEngine::scratchEnable(int deck, int intervalsPerRev, float rpm,
932
float alpha, float beta, bool ramp) {
934
// If we're already scratching this deck, override that with this request
936
//qDebug() << "Already scratching deck" << deck << ". Overriding.";
937
int timerId = m_scratchTimers.key(deck);
939
m_scratchTimers.remove(timerId);
942
// Controller resolution in intervals per second at normal speed (rev/min * ints/rev * mins/sec)
943
float intervalsPerSecond = (rpm * intervalsPerRev)/60;
945
m_dx[deck] = 1/intervalsPerSecond;
946
m_intervalAccumulator[deck] = 0;
947
m_ramp[deck] = false;
949
QString group = QString("[Channel%1]").arg(deck);
952
float initVelocity = 0.0; // Default to stopped
953
ControlObjectThread *cot = getControlObjectThread(group, "scratch2_enable");
955
// If ramping is desired, figure out the deck's current speed
957
// See if the deck is already being scratched
958
if (cot != NULL && cot->get() == 1) {
959
// If so, set the filter's initial velocity to the scratch speed
960
cot = getControlObjectThread(group, "scratch2");
962
initVelocity=cot->get();
965
// See if deck is playing
966
cot = getControlObjectThread(group, "play");
967
if (cot != NULL && cot->get() == 1) {
968
// If so, set the filter's initial velocity to the playback speed
971
cot = getControlObjectThread(group, "rate");
976
cot = getControlObjectThread(group, "rateRange");
978
rate = rate * cot->get();
981
// Add 1 since the deck is playing
984
// See if we're in reverse play
985
cot = getControlObjectThread(group, "reverse");
987
if (cot != NULL && cot->get() == 1) {
995
// Initialize pitch filter (0.001s = 1ms) (We're assuming the OS actually
996
// gives us a 1ms timer below)
998
m_pitchFilter[deck]->init(0.001, initVelocity, alpha, beta);
1000
// Use filter's defaults if not specified
1001
m_pitchFilter[deck]->init(0.001, initVelocity);
1004
// 1ms is shortest possible, OS dependent
1005
int timerId = startTimer(1);
1007
// Associate this virtual deck with this timer for later processing
1008
m_scratchTimers[timerId] = deck;
1010
// Set scratch2_enable
1011
cot = getControlObjectThread(group, "scratch2_enable");
1017
/* -------- ------------------------------------------------------
1018
Purpose: Accumulates "ticks" of the controller wheel
1019
Input: Virtual deck to scratch, interval value (usually +1 or -1)
1021
-------- ------------------------------------------------------ */
1022
void ControllerEngine::scratchTick(int deck, int interval) {
1023
m_intervalAccumulator[deck] += interval;
1026
/* -------- ------------------------------------------------------
1027
Purpose: Applies the accumulated movement to the track speed
1028
Input: ID of timer for this deck
1030
-------- ------------------------------------------------------ */
1031
void ControllerEngine::scratchProcess(int timerId) {
1032
int deck = m_scratchTimers[timerId];
1033
PitchFilter* filter = m_pitchFilter[deck];
1034
QString group = QString("[Channel%1]").arg(deck);
1037
qWarning() << "Scratch filter pointer is null on deck" << deck;
1041
// Give the filter a data point:
1043
// If we're ramping to end scratching, feed fixed data
1045
filter->observation(m_rampTo[deck]*0.001);
1047
// This will (and should) be 0 if no net ticks have been accumulated
1048
// (i.e. the wheel is stopped)
1049
filter->observation(m_dx[deck] * m_intervalAccumulator[deck]);
1052
// Actually do the scratching
1053
ControlObjectThread *cot = getControlObjectThread(group, "scratch2");
1055
cot->slotSet(filter->currentPitch());
1058
// Reset accumulator
1059
m_intervalAccumulator[deck] = 0;
1061
// If we're ramping and the current pitch is really close to the rampTo
1062
// value, end scratching
1064
//if (m_ramp[deck]) qDebug() << "Ramping to" << m_rampTo[deck] << " Currently at:" << filter->currentPitch();
1065
if (m_ramp[deck] && fabs(m_rampTo[deck]-filter->currentPitch()) <= 0.00001) {
1066
// Not ramping no mo'
1067
m_ramp[deck] = false;
1069
// Clear scratch2_enable
1070
cot = getControlObjectThread(group, "scratch2_enable");
1077
m_scratchTimers.remove(timerId);
1083
/* -------- ------------------------------------------------------
1084
Purpose: Stops scratching the specified virtual deck
1085
Input: Virtual deck to stop scratching
1087
-------- ------------------------------------------------------ */
1088
void ControllerEngine::scratchDisable(int deck, bool ramp) {
1089
QString group = QString("[Channel%1]").arg(deck);
1091
m_rampTo[deck] = 0.0;
1093
// If no ramping is desired, disable scratching immediately
1095
// Clear scratch2_enable
1096
ControlObjectThread *cot = getControlObjectThread(group, "scratch2_enable");
1097
if(cot != NULL) cot->slotSet(0);
1098
// Can't return here because we need scratchProcess to stop the timer.
1099
// So it's still actually ramping, we just won't hear or see it.
1101
// See if deck is playing
1102
ControlObjectThread *cot = getControlObjectThread(group, "play");
1103
if (cot != NULL && cot->get() == 1) {
1104
// If so, set the target velocity to the playback speed
1106
// Get the pitch slider value
1107
cot = getControlObjectThread(group, "rate");
1112
// Get the pitch slider directions
1113
cot = getControlObjectThread(group, "rate_dir");
1114
if (cot != NULL && cot->get() == -1) {
1118
// Multiply by the pitch range
1119
cot = getControlObjectThread(group, "rateRange");
1121
rate = rate * cot->get();
1124
// Add 1 since the deck is playing
1127
// See if we're in reverse play
1128
cot = getControlObjectThread(group, "reverse");
1129
if (cot != NULL && cot->get() == 1) {
1133
m_rampTo[deck] = rate;
1137
m_ramp[deck] = true; // Activate the ramping in scratchProcess()
1140
/* -------- ------------------------------------------------------
1141
Purpose: [En/dis]ables soft-takeover status for a particular ControlObject
1142
Input: ControlObject group and key values,
1143
whether to set the soft-takeover status or not
1145
-------- ------------------------------------------------------ */
1146
void ControllerEngine::softTakeover(QString group, QString name, bool set) {
1147
ControlObject* pControl = ControlObject::getControl(ConfigKey(group, name));
1152
m_st.enable(pControl);
1154
m_st.disable(pControl);