![]() |
Mixxx
|
00001 /*************************************************************************** 00002 midiscriptengine.cpp - description 00003 ------------------- 00004 begin : Fri Dec 12 2008 00005 copyright : (C) 2008-2010 by Sean M. Pappalardo 00006 "Holy crap, I wrote new code!" 00007 email : spappalardo@mixxx.org 00008 ***************************************************************************/ 00009 00010 /*************************************************************************** 00011 * * 00012 * This program is free software; you can redistribute it and/or modify * 00013 * it under the terms of the GNU General Public License as published by * 00014 * the Free Software Foundation; either version 2 of the License, or * 00015 * (at your option) any later version. * 00016 * * 00017 ***************************************************************************/ 00018 00019 #include "controlobject.h" 00020 #include "controlobjectthreadmain.h" 00021 #include "mididevice.h" 00022 #include "midiscriptengine.h" 00023 #include "errordialoghandler.h" 00024 00025 // #include <QScriptSyntaxCheckResult> 00026 00027 #ifdef _MSC_VER 00028 #include <float.h> // for _isnan() on VC++ 00029 #define isnan(x) _isnan(x) // VC++ uses _isnan() instead of isnan() 00030 #else 00031 #include <math.h> // for isnan() everywhere else 00032 #endif 00033 00034 00035 MidiScriptEngine::MidiScriptEngine(MidiDevice* midiDevice) : 00036 m_pMidiDevice(midiDevice), 00037 m_midiDebug(false), 00038 m_pEngine(NULL), 00039 m_midiPopups(false) { 00040 00041 // Handle error dialog buttons 00042 qRegisterMetaType<QMessageBox::StandardButton>("QMessageBox::StandardButton"); 00043 00044 // Pre-allocate arrays for average number of virtual decks 00045 int decks = 16; 00046 m_intervalAccumulator.resize(decks); 00047 m_dx.resize(decks); 00048 m_rampTo.resize(decks); 00049 m_ramp.resize(decks); 00050 m_pitchFilter.resize(decks); 00051 00052 // Initialize arrays used for testing and pointers 00053 for (int i=0; i < decks; i++) { 00054 m_dx[i] = 0; 00055 m_pitchFilter[i] = new PitchFilter(); // allocate RAM at startup 00056 m_ramp[i] = false; 00057 } 00058 } 00059 00060 MidiScriptEngine::~MidiScriptEngine() { 00061 // Clean up 00062 int decks = 16; // Must match value above 00063 for (int i=0; i < decks; i++) { 00064 delete m_pitchFilter[i]; 00065 m_pitchFilter[i] = NULL; 00066 } 00067 00068 // Delete the script engine, first clearing the pointer so that 00069 // other threads will not get the dead pointer after we delete it. 00070 if(m_pEngine != NULL) { 00071 QScriptEngine *engine = m_pEngine; 00072 m_pEngine = NULL; 00073 engine->deleteLater(); 00074 } 00075 00076 } 00077 00078 /* -------- ------------------------------------------------------ 00079 Purpose: Shuts down MIDI scripts in an orderly fashion 00080 (stops timers then executes shutdown functions) 00081 Input: - 00082 Output: - 00083 -------- ------------------------------------------------------ */ 00084 void MidiScriptEngine::gracefulShutdown(QList<QString> scriptFunctionPrefixes) { 00085 qDebug() << "MidiScriptEngine shutting down..."; 00086 00087 m_scriptEngineLock.lock(); 00088 // Clear the m_connectedControls hash so we stop responding 00089 // to signals. 00090 m_connectedControls.clear(); 00091 00092 // Disconnect the function call signal 00093 if (m_pMidiDevice) 00094 disconnect(m_pMidiDevice, SIGNAL(callMidiScriptFunction(QString, char, char, 00095 char, MidiStatusByte, QString)), 00096 this, SLOT(execute(QString, char, char, char, MidiStatusByte, QString))); 00097 00098 // Stop all timers 00099 stopAllTimers(); 00100 00101 // Call each script's shutdown function if it exists 00102 QListIterator<QString> prefixIt(scriptFunctionPrefixes); 00103 while (prefixIt.hasNext()) { 00104 QString shutName = prefixIt.next(); 00105 if (shutName!="") { 00106 shutName.append(".shutdown"); 00107 if (m_midiDebug) qDebug() << "MidiScriptEngine: Executing" << shutName; 00108 if (!internalExecute(QScriptValue(), shutName)) 00109 qWarning() << "MidiScriptEngine: No" << shutName << "function in script"; 00110 } 00111 } 00112 00113 // Prevents leaving decks in an unstable state 00114 // if the controller is shut down while scratching 00115 QHashIterator<int, int> i(m_scratchTimers); 00116 while (i.hasNext()) { 00117 i.next(); 00118 qDebug() << "Aborting scratching on deck" << i.value(); 00119 // Clear scratch2_enable 00120 QString group = QString("[Channel%1]").arg(i.value()); 00121 ControlObjectThread *cot = getControlObjectThread(group, "scratch2_enable"); 00122 if(cot != NULL) cot->slotSet(0); 00123 } 00124 00125 // Free all the control object threads 00126 QList<ConfigKey> keys = m_controlCache.keys(); 00127 QList<ConfigKey>::iterator it = keys.begin(); 00128 QList<ConfigKey>::iterator end = keys.end(); 00129 while(it != end) { 00130 ConfigKey key = *it; 00131 ControlObjectThread *cot = m_controlCache.take(key); 00132 delete cot; 00133 it++; 00134 } 00135 00136 m_scriptEngineLock.unlock(); 00137 00138 // Stop processing the event loop and terminate the thread. 00139 quit(); 00140 } 00141 00142 bool MidiScriptEngine::isReady() { 00143 m_scriptEngineLock.lock(); 00144 bool ret = m_pEngine != NULL; 00145 m_scriptEngineLock.unlock(); 00146 return ret; 00147 } 00148 00149 /* 00150 WARNING: must hold the lock to call this 00151 */ 00152 void MidiScriptEngine::initializeScriptEngine() { 00153 // Create the MidiScriptEngine 00154 m_pEngine = new QScriptEngine(this); 00155 00156 //qDebug() << "MidiScriptEngine::run() m_pEngine->parent() is " << m_pEngine->parent(); 00157 //qDebug() << "MidiScriptEngine::run() m_pEngine->thread() is " << m_pEngine->thread(); 00158 00159 // Make this MidiScriptEngine instance available to scripts as 00160 // 'engine'. 00161 QScriptValue engineGlobalObject = m_pEngine->globalObject(); 00162 engineGlobalObject.setProperty("engine", m_pEngine->newQObject(this)); 00163 00164 if (m_pMidiDevice) { 00165 qDebug() << "MIDI Device in script engine is:" << m_pMidiDevice->getName(); 00166 00167 // Make the MidiDevice instance available to scripts as 'midi'. 00168 engineGlobalObject.setProperty("midi", m_pEngine->newQObject(m_pMidiDevice)); 00169 00170 // Allow the MidiDevice to signal script function calls 00171 connect(m_pMidiDevice, SIGNAL(callMidiScriptFunction(QString, char, char, 00172 char, MidiStatusByte, QString)), 00173 this, SLOT(execute(QString, char, char, char, MidiStatusByte, QString))); 00174 } 00175 } 00176 00177 /* -------- ------------------------------------------------------ 00178 Purpose: Load all script files given in the shared list 00179 Input: - 00180 Output: - 00181 -------- ------------------------------------------------------ */ 00182 void MidiScriptEngine::loadScriptFiles(QList<QString> scriptFileNames) { 00183 00184 // Set the Midi Debug flag 00185 if (m_pMidiDevice) 00186 m_midiDebug = m_pMidiDevice->midiDebugging(); 00187 00188 qDebug() << "MidiScriptEngine: Loading & evaluating all MIDI script code"; 00189 00190 // scriptPaths holds the paths to search in when we're looking for scripts 00191 QList<QString> scriptPaths; 00192 scriptPaths.append(QDir::homePath().append("/").append(SETTINGS_PATH).append("presets/")); 00193 00194 ConfigObject<ConfigValue> *config = new ConfigObject<ConfigValue>(QDir::homePath().append("/").append(SETTINGS_PATH).append(SETTINGS_FILE)); 00195 scriptPaths.append(config->getConfigPath().append("midi/")); 00196 delete config; 00197 00198 QListIterator<QString> it(scriptFileNames); 00199 m_scriptEngineLock.lock(); 00200 while (it.hasNext()) { 00201 QString curScriptFileName = it.next(); 00202 safeEvaluate(curScriptFileName, scriptPaths); 00203 00204 if(m_scriptErrors.contains(curScriptFileName)) { 00205 qDebug() << "Errors occured while loading " << curScriptFileName; 00206 } 00207 } 00208 00209 m_scriptEngineLock.unlock(); 00210 emit(initialized()); 00211 } 00212 00213 /* -------- ------------------------------------------------------ 00214 Purpose: Run the initialization function for each loaded script 00215 if it exists 00216 Input: - 00217 Output: - 00218 -------- ------------------------------------------------------ */ 00219 void MidiScriptEngine::initializeScripts(QList<QString> scriptFunctionPrefixes) { 00220 m_scriptEngineLock.lock(); 00221 00222 QListIterator<QString> prefixIt(scriptFunctionPrefixes); 00223 while (prefixIt.hasNext()) { 00224 QString initName = prefixIt.next(); 00225 if (initName!="") { 00226 initName.append(".init"); 00227 if (m_midiDebug) qDebug() << "MidiScriptEngine: Executing" << initName; 00228 if (!safeExecute(initName, m_pMidiDevice->getName())) 00229 qWarning() << "MidiScriptEngine: No" << initName << "function in script"; 00230 } 00231 } 00232 m_scriptEngineLock.unlock(); 00233 emit(initialized()); 00234 } 00235 00236 /* -------- ------------------------------------------------------ 00237 Purpose: Create the MidiScriptEngine object (so it is owned in this 00238 thread, and start the Qt event loop for this thread via exec(). 00239 Input: - 00240 Output: - 00241 -------- ------------------------------------------------------ */ 00242 void MidiScriptEngine::run() { 00243 unsigned static id = 0; //the id of this thread, for debugging purposes //XXX copypasta (should factor this out somehow), -kousu 2/2009 00244 QThread::currentThread()->setObjectName(QString("MidiScriptEngine %1").arg(++id)); 00245 00246 // Prevent the script engine from strangling other parts of Mixxx 00247 // incase of a misbehaving script 00248 // - Should we perhaps not do this when running with --midiDebug so it's more 00249 // obvious if a script is taking too much CPU time? - Sean 4/19/10 00250 QThread::currentThread()->setPriority(QThread::LowPriority); 00251 00252 m_scriptEngineLock.lock(); 00253 initializeScriptEngine(); 00254 m_scriptEngineLock.unlock(); 00255 emit(initialized()); 00256 00257 // Run the Qt event loop indefinitely 00258 exec(); 00259 } 00260 00261 /* -------- ------------------------------------------------------ 00262 Purpose: Validate script syntax, then evaluate() it so the 00263 functions are registered & available for use. 00264 Input: - 00265 Output: - 00266 -------- ------------------------------------------------------ */ 00267 bool MidiScriptEngine::evaluate(QString filepath) { 00268 m_scriptEngineLock.lock(); 00269 QList<QString> dummy; 00270 bool ret = safeEvaluate(filepath, dummy); 00271 m_scriptEngineLock.unlock(); 00272 return ret; 00273 } 00274 00275 /* -------- ------------------------------------------------------ 00276 Purpose: Evaluate & call a script function 00277 Input: Function name 00278 Output: false if an invalid function or an exception 00279 -------- ------------------------------------------------------ */ 00280 bool MidiScriptEngine::execute(QString function) { 00281 m_scriptEngineLock.lock(); 00282 bool ret = safeExecute(function); 00283 if (!ret) qWarning() << "MidiScriptEngine: Invalid script function" << function; 00284 m_scriptEngineLock.unlock(); 00285 return ret; 00286 } 00287 00288 /* -------- ------------------------------------------------------ 00289 Purpose: Evaluate & call a script function 00290 Input: Function name, data string (e.g. device ID) 00291 Output: false if an invalid function or an exception 00292 -------- ------------------------------------------------------ */ 00293 bool MidiScriptEngine::execute(QString function, QString data) { 00294 m_scriptEngineLock.lock(); 00295 bool ret = safeExecute(function, data); 00296 if (!ret) qWarning() << "MidiScriptEngine: Invalid script function" << function; 00297 m_scriptEngineLock.unlock(); 00298 return ret; 00299 } 00300 00301 /* -------- ------------------------------------------------------ 00302 Purpose: Evaluate & call a script function 00303 Input: Function name, pointer to data buffer, length of buffer 00304 Output: false if an invalid function or an exception 00305 -------- ------------------------------------------------------ */ 00306 bool MidiScriptEngine::execute(QString function, const unsigned char data[], 00307 unsigned int length) { 00308 m_scriptEngineLock.lock(); 00309 bool ret = safeExecute(function, data, length); 00310 m_scriptEngineLock.unlock(); 00311 return ret; 00312 } 00313 00314 /* -------- ------------------------------------------------------ 00315 Purpose: Evaluate & call a script function 00316 Input: Function name, channel #, control #, value, status 00317 MixxxControl group 00318 Output: false if an invalid function or an exception 00319 -------- ------------------------------------------------------ */ 00320 bool MidiScriptEngine::execute(QString function, char channel, 00321 char control, char value, 00322 MidiStatusByte status, 00323 QString group) { 00324 m_scriptEngineLock.lock(); 00325 bool ret = safeExecute(function, channel, control, value, status, group); 00326 if (!ret) qWarning() << "MidiScriptEngine: Invalid script function" << function; 00327 m_scriptEngineLock.unlock(); 00328 return ret; 00329 } 00330 00331 /* -------- ------------------------------------------------------ 00332 Purpose: Evaluate & call a script function 00333 Input: Function name 00334 Output: false if an invalid function or an exception 00335 -------- ------------------------------------------------------ */ 00336 bool MidiScriptEngine::safeExecute(QString function) { 00337 //qDebug() << QString("MidiScriptEngine: Exec1 Thread ID=%1").arg(QThread::currentThreadId(),0,16); 00338 00339 if (m_pEngine == NULL) 00340 return false; 00341 00342 QScriptValue scriptFunction = m_pEngine->evaluate(function); 00343 00344 if (checkException()) 00345 return false; 00346 00347 if (!scriptFunction.isFunction()) 00348 return false; 00349 00350 scriptFunction.call(QScriptValue()); 00351 if (checkException()) 00352 return false; 00353 00354 return true; 00355 } 00356 00357 00358 /* -------- ------------------------------------------------------ 00359 Purpose: Evaluate & run script code 00360 Input: 'this' object if applicable, Code string 00361 Output: false if an exception 00362 -------- ------------------------------------------------------ */ 00363 bool MidiScriptEngine::internalExecute(QScriptValue thisObject, 00364 QString scriptCode) { 00365 // A special version of safeExecute since we're evaluating strings, not actual functions 00366 // (execute() would print an error that it's not a function every time a timer fires.) 00367 if(m_pEngine == NULL) 00368 return false; 00369 00370 // Check syntax 00371 QScriptSyntaxCheckResult result = m_pEngine->checkSyntax(scriptCode); 00372 QString error=""; 00373 switch (result.state()) { 00374 case (QScriptSyntaxCheckResult::Valid): break; 00375 case (QScriptSyntaxCheckResult::Intermediate): 00376 error = "Incomplete code"; 00377 break; 00378 case (QScriptSyntaxCheckResult::Error): 00379 error = "Syntax error"; 00380 break; 00381 } 00382 if (error != "") { 00383 error = QString("%1: %2 at line %3, column %4 of script code:\n%5\n") 00384 .arg(error) 00385 .arg(result.errorMessage()) 00386 .arg(result.errorLineNumber()) 00387 .arg(result.errorColumnNumber()) 00388 .arg(scriptCode); 00389 00390 if (m_midiDebug) qCritical() << "MidiScriptEngine:" << error; 00391 else scriptErrorDialog(error); 00392 return false; 00393 } 00394 00395 QScriptValue scriptFunction = m_pEngine->evaluate(scriptCode); 00396 00397 if (checkException()) 00398 return false; 00399 00400 // If it's not a function, we're done. 00401 if (!scriptFunction.isFunction()) 00402 return true; 00403 00404 // If it does happen to be a function, call it. 00405 scriptFunction.call(thisObject); 00406 if (checkException()) 00407 return false; 00408 00409 return true; 00410 } 00411 00412 /* -------- ------------------------------------------------------ 00413 Purpose: Evaluate & call a script function 00414 Input: Function name, data string (e.g. device ID) 00415 Output: false if an invalid function or an exception 00416 -------- ------------------------------------------------------ */ 00417 bool MidiScriptEngine::safeExecute(QString function, QString data) { 00418 //qDebug() << QString("MidiScriptEngine: Exec2 Thread ID=%1").arg(QThread::currentThreadId(),0,16); 00419 00420 if(m_pEngine == NULL) { 00421 return false; 00422 } 00423 00424 QScriptValue scriptFunction = m_pEngine->evaluate(function); 00425 00426 if (checkException()) 00427 return false; 00428 if (!scriptFunction.isFunction()) 00429 return false; 00430 00431 QScriptValueList args; 00432 args << QScriptValue(data); 00433 00434 scriptFunction.call(QScriptValue(), args); 00435 if (checkException()) 00436 return false; 00437 return true; 00438 } 00439 00440 /* -------- ------------------------------------------------------ 00441 Purpose: Evaluate & call a script function 00442 Input: Function name, ponter to data buffer, length of buffer 00443 Output: false if an invalid function or an exception 00444 -------- ------------------------------------------------------ */ 00445 bool MidiScriptEngine::safeExecute(QString function, const unsigned char data[], 00446 unsigned int length) { 00447 00448 if(m_pEngine == NULL) { 00449 return false; 00450 } 00451 00452 if (!m_pEngine->canEvaluate(function)) { 00453 qCritical() << "MidiScriptEngine: ?Syntax error in function " << function; 00454 return false; 00455 } 00456 00457 QScriptValue scriptFunction = m_pEngine->evaluate(function); 00458 00459 if (checkException()) 00460 return false; 00461 if (!scriptFunction.isFunction()) 00462 return false; 00463 00464 // These funky conversions are required in order to 00465 // get the byte array into ECMAScript complete and unharmed. 00466 // Don't change this or I will hurt you -- Sean 00467 QVector<QChar> temp(length); 00468 for (unsigned int i=0; i < length; i++) { 00469 temp[i]=data[i]; 00470 } 00471 QString buffer = QString(temp.constData(),length); 00472 QScriptValueList args; 00473 args << QScriptValue(buffer); 00474 args << QScriptValue(length); 00475 00476 scriptFunction.call(QScriptValue(), args); 00477 if (checkException()) 00478 return false; 00479 return true; 00480 } 00481 00482 /* -------- ------------------------------------------------------ 00483 Purpose: Evaluate & call a script function 00484 Input: Function name, channel #, control #, value, status 00485 Output: false if an invalid function or an exception 00486 -------- ------------------------------------------------------ */ 00487 bool MidiScriptEngine::safeExecute(QString function, char channel, 00488 char control, char value, 00489 MidiStatusByte status, 00490 QString group) { 00491 //qDebug() << QString("MidiScriptEngine: Exec2 Thread ID=%1").arg(QThread::currentThreadId(),0,16); 00492 00493 if(m_pEngine == NULL) { 00494 return false; 00495 } 00496 00497 QScriptValue scriptFunction = m_pEngine->evaluate(function); 00498 00499 if (checkException()) 00500 return false; 00501 if (!scriptFunction.isFunction()) 00502 return false; 00503 00504 QScriptValueList args; 00505 args << QScriptValue(channel); 00506 args << QScriptValue(control); 00507 args << QScriptValue(value); 00508 args << QScriptValue(status); 00509 args << QScriptValue(group); 00510 00511 scriptFunction.call(QScriptValue(), args); 00512 if (checkException()) 00513 return false; 00514 return true; 00515 } 00516 00517 /* -------- ------------------------------------------------------ 00518 Purpose: Evaluate & call a script function 00519 Input: This Object (context), Function name 00520 Output: false if an invalid function or an exception 00521 Notes: used for closure calls using native functions (ie: timers) 00522 -------- ------------------------------------------------------ */ 00523 bool MidiScriptEngine::safeExecute(QScriptValue thisObject, 00524 QScriptValue functionObject) { 00525 if (m_pEngine == NULL) { 00526 return false; 00527 } 00528 00529 QScriptValueList args; 00530 00531 functionObject.call(thisObject, args); 00532 if (checkException()) { 00533 return false; 00534 } 00535 return true; 00536 } 00537 00538 /* -------- ------------------------------------------------------ 00539 Purpose: Check to see if a script threw an exception 00540 Input: QScriptValue returned from call(scriptFunctionName) 00541 Output: true if there was an exception 00542 -------- ------------------------------------------------------ */ 00543 bool MidiScriptEngine::checkException() { 00544 if(m_pEngine == NULL) { 00545 return false; 00546 } 00547 00548 if (m_pEngine->hasUncaughtException()) { 00549 QScriptValue exception = m_pEngine->uncaughtException(); 00550 QString errorMessage = exception.toString(); 00551 int line = m_pEngine->uncaughtExceptionLineNumber(); 00552 QStringList backtrace = m_pEngine->uncaughtExceptionBacktrace(); 00553 QString filename = exception.property("fileName").toString(); 00554 00555 QStringList error; 00556 error << (filename.isEmpty() ? "" : filename) << errorMessage << QString(line); 00557 m_scriptErrors.insert((filename.isEmpty() ? "passed code" : filename), error); 00558 00559 QString errorText = QString(tr("Uncaught exception at line %1 in file %2: %3")) 00560 .arg(line) 00561 .arg((filename.isEmpty() ? "" : filename)) 00562 .arg(errorMessage); 00563 00564 if (filename.isEmpty()) 00565 errorText = QString(tr("Uncaught exception at line %1 in passed code: %2")) 00566 .arg(line) 00567 .arg(errorMessage); 00568 00569 if (m_midiDebug) 00570 qCritical() << "MidiScriptEngine:" << errorText 00571 << "\nBacktrace:\n" 00572 << backtrace; 00573 else scriptErrorDialog(errorText); 00574 return true; 00575 } 00576 return false; 00577 } 00578 00579 /* -------- ------------------------------------------------------ 00580 Purpose: Common error dialog creation code for run-time exceptions 00581 Allows users to ignore the error or reload the mappings 00582 Input: Detailed error string 00583 Output: - 00584 -------- ------------------------------------------------------ */ 00585 void MidiScriptEngine::scriptErrorDialog(QString detailedError) { 00586 qWarning() << "MidiScriptEngine:" << detailedError; 00587 ErrorDialogProperties* props = ErrorDialogHandler::instance()->newDialogProperties(); 00588 props->setType(DLG_WARNING); 00589 props->setTitle(tr("MIDI script error")); 00590 props->setText(tr("A MIDI control you just used is not working properly.")); 00591 props->setInfoText(tr("<html>(The MIDI script code needs to be fixed.)" 00592 "<br>For now, you can:<ul><li>Ignore this error for this session but you may experience erratic behavior</li>" 00593 "<li>Try to recover by resetting your controller</li></ul></html>")); 00594 props->setDetails(detailedError); 00595 props->setKey(detailedError); // To prevent multiple windows for the same error 00596 00597 // Allow user to suppress further notifications about this particular error 00598 props->addButton(QMessageBox::Ignore); 00599 00600 props->addButton(QMessageBox::Retry); 00601 props->addButton(QMessageBox::Close); 00602 props->setDefaultButton(QMessageBox::Close); 00603 props->setEscapeButton(QMessageBox::Close); 00604 props->setModal(false); 00605 00606 if (ErrorDialogHandler::instance()->requestErrorDialog(props)) { 00607 // Enable custom handling of the dialog buttons 00608 connect(ErrorDialogHandler::instance(), SIGNAL(stdButtonClicked(QString, QMessageBox::StandardButton)), 00609 this, SLOT(errorDialogButton(QString, QMessageBox::StandardButton))); 00610 } 00611 } 00612 00613 /* -------- ------------------------------------------------------ 00614 Purpose: Slot to handle custom button clicks in error dialogs 00615 Input: Key of dialog, StandardButton that was clicked 00616 Output: - 00617 -------- ------------------------------------------------------ */ 00618 void MidiScriptEngine::errorDialogButton(QString key, QMessageBox::StandardButton button) { 00619 00620 // Something was clicked, so disable this signal now 00621 disconnect(ErrorDialogHandler::instance(), SIGNAL(stdButtonClicked(QString, QMessageBox::StandardButton)), 00622 this, SLOT(errorDialogButton(QString, QMessageBox::StandardButton))); 00623 00624 if (button == QMessageBox::Retry) emit(resetController()); 00625 } 00626 00627 /* -------- ------------------------------------------------------ 00628 Purpose: Returns a list of functions available in the QtScript 00629 code 00630 Input: - 00631 Output: functionList QStringList 00632 -------- ------------------------------------------------------ */ 00633 QStringList MidiScriptEngine::getScriptFunctions() { 00634 m_scriptEngineLock.lock(); 00635 QStringList ret = m_scriptFunctions; 00636 m_scriptEngineLock.unlock(); 00637 return ret; 00638 } 00639 00640 void MidiScriptEngine::generateScriptFunctions(QString scriptCode) { 00641 00642 // QStringList functionList; 00643 QStringList codeLines = scriptCode.split("\n"); 00644 00645 // qDebug() << "MidiScriptEngine: m_scriptCode=" << m_scriptCode; 00646 00647 if (m_midiDebug) 00648 qDebug() << "MidiScriptEngine:" << codeLines.count() << "lines of code being searched for functions"; 00649 00650 // grep 'function' midi/midi-mappings-scripts.js|grep -i '(msg)'|sed -e 's/function \(.*\)(msg).*/\1/i' -e 's/[= ]//g' 00651 QRegExp rx("*.*function*(*)*"); // Find all lines with function names in them 00652 rx.setPatternSyntax(QRegExp::Wildcard); 00653 00654 int position = codeLines.indexOf(rx); 00655 00656 while (position != -1) { // While there are more matches 00657 00658 QString line = codeLines.takeAt(position); // Pull & remove the current match from the list. 00659 00660 if (line.indexOf('#') != 0 && line.indexOf("//") != 0) { // ignore commented out lines 00661 QStringList field = line.split(" "); 00662 if (m_midiDebug) qDebug() << "MidiScriptEngine: Found function:" << field[0] 00663 << "at line" << position; 00664 m_scriptFunctions.append(field[0]); 00665 } 00666 position = codeLines.indexOf(rx); 00667 } 00668 00669 } 00670 00671 ControlObjectThread* MidiScriptEngine::getControlObjectThread(QString group, QString name) { 00672 00673 ConfigKey key = ConfigKey(group, name); 00674 00675 ControlObjectThread *cot = NULL; 00676 if(!m_controlCache.contains(key)) { 00677 ControlObject *co = ControlObject::getControl(key); 00678 if(co != NULL) { 00679 cot = new ControlObjectThreadMain(co); 00680 m_controlCache.insert(key, cot); 00681 } 00682 } else { 00683 cot = m_controlCache.value(key); 00684 } 00685 00686 return cot; 00687 00688 } 00689 00690 /* -------- ------------------------------------------------------ 00691 Purpose: Returns the current value of a Mixxx control (for scripts) 00692 Input: Control group (e.g. [Channel1]), Key name (e.g. [filterHigh]) 00693 Output: The value 00694 -------- ------------------------------------------------------ */ 00695 double MidiScriptEngine::getValue(QString group, QString name) { 00696 00697 00698 // When this function runs, assert that somebody is holding the script 00699 // engine lock. 00700 bool lock = m_scriptEngineLock.tryLock(); 00701 Q_ASSERT(!lock); 00702 if(lock) { 00703 m_scriptEngineLock.unlock(); 00704 } 00705 00706 //qDebug() << QString("----------------------------------MidiScriptEngine: GetValue Thread ID=%1").arg(QThread::currentThreadId(),0,16); 00707 00708 ControlObjectThread *cot = getControlObjectThread(group, name); 00709 if (cot == NULL) { 00710 qWarning() << "MidiScriptEngine: Unknown control" << group << name; 00711 return 0.0; 00712 } 00713 00714 return cot->get(); 00715 } 00716 00717 /* -------- ------------------------------------------------------ 00718 Purpose: Sets new value of a Mixxx control (for scripts) 00719 Input: Control group, Key name, new value 00720 Output: - 00721 -------- ------------------------------------------------------ */ 00722 void MidiScriptEngine::setValue(QString group, QString name, double newValue) { 00723 00724 // When this function runs, assert that somebody is holding the script 00725 // engine lock. 00726 bool lock = m_scriptEngineLock.tryLock(); 00727 Q_ASSERT(!lock); 00728 if(lock) { 00729 m_scriptEngineLock.unlock(); 00730 } 00731 00732 if(isnan(newValue)) { 00733 qWarning() << "MidiScriptEngine: script setting [" << group << "," << name 00734 << "] to NotANumber, ignoring."; 00735 return; 00736 } 00737 00738 //qDebug() << QString("----------------------------------MidiScriptEngine: SetValue Thread ID=%1").arg(QThread::currentThreadId(),0,16); 00739 00740 ControlObjectThread *cot = getControlObjectThread(group, name); 00741 00742 if(cot != NULL && !m_st.ignore(group,name,newValue)) { 00743 cot->slotSet(newValue); 00744 // We call emitValueChanged so that script functions connected to this 00745 // control will get updates. 00746 cot->emitValueChanged(); 00747 } 00748 00749 } 00750 00751 /* -------- ------------------------------------------------------ 00752 Purpose: qDebugs script output so it ends up in mixxx.log 00753 Input: String to log 00754 Output: - 00755 -------- ------------------------------------------------------ */ 00756 void MidiScriptEngine::log(QString message) { 00757 00758 qDebug()<<message; 00759 } 00760 00761 /* -------- ------------------------------------------------------ 00762 Purpose: Calls script function(s) linked to a particular ControlObject 00763 so controller outputs update 00764 Input: ControlObject Group and Name strings 00765 Output: - 00766 -------- ------------------------------------------------------ */ 00767 void MidiScriptEngine::trigger(QString group, QString name) { 00768 // When this function runs, assert that somebody is holding the script 00769 // engine lock. 00770 bool lock = m_scriptEngineLock.tryLock(); 00771 Q_ASSERT(!lock); 00772 if(lock) { 00773 m_scriptEngineLock.unlock(); 00774 } 00775 00776 ControlObjectThread *cot = getControlObjectThread(group, name); 00777 00778 if (cot == NULL) { 00779 return; 00780 } 00781 00782 // ControlObject doesn't emit ValueChanged when set to the same value, 00783 // and ControlObjectThread::emitValueChanged also has no effect 00784 // so we have to call the function(s) manually with the current value 00785 ConfigKey key = ConfigKey(group,name); 00786 if(m_connectedControls.contains(key)) { 00787 QMultiHash<ConfigKey, QString>::iterator i = m_connectedControls.find(key); 00788 while (i != m_connectedControls.end() && i.key() == key) { 00789 QString function = i.value(); 00790 00791 QScriptValue function_value = m_pEngine->evaluate(function); 00792 QScriptValueList args; 00793 double value; 00794 00795 args << QScriptValue(cot->get()); 00796 args << QScriptValue(key.group); 00797 args << QScriptValue(key.item); 00798 QScriptValue result = function_value.call(QScriptValue(), args); 00799 if (result.isError()) { 00800 qWarning()<< "MidiScriptEngine: Call to" << function 00801 << "resulted in an error:" << result.toString(); 00802 } 00803 ++i; 00804 } 00805 } 00806 } 00807 00808 /* -------- ------------------------------------------------------ 00809 Purpose: (Dis)connects a ControlObject valueChanged() signal to/from a script function 00810 Input: Control group (e.g. [Channel1]), Key name (e.g. [filterHigh]), 00811 script function name, true if you want to disconnect 00812 Output: true if successful 00813 -------- ------------------------------------------------------ */ 00814 bool MidiScriptEngine::connectControl(QString group, QString name, QString function, bool disconnect) { 00815 ControlObjectThread* cobj = getControlObjectThread(group, name); 00816 ConfigKey key(group, name); 00817 00818 if (cobj == NULL) { 00819 qWarning() << "MidiScriptEngine: script connecting [" << group << "," << name 00820 << "], which is non-existent. ignoring."; 00821 return false; 00822 } 00823 00824 00825 // Don't add duplicates 00826 if (!disconnect && m_connectedControls.contains(key, function)) return true; 00827 00828 // When this function runs, assert that somebody is holding the script 00829 // engine lock. 00830 bool lock = m_scriptEngineLock.tryLock(); 00831 Q_ASSERT(!lock); 00832 if(lock) { 00833 m_scriptEngineLock.unlock(); 00834 } 00835 00836 //qDebug() << QString("MidiScriptEngine: Connect Thread ID=%1").arg(QThread::currentThreadId(),0,16); 00837 00838 00839 if(m_pEngine == NULL) { 00840 return false; 00841 } 00842 00843 QScriptValue slot = m_pEngine->evaluate(function); 00844 00845 if(!checkException() && slot.isFunction()) { 00846 if(disconnect) { 00847 // qDebug() << "MidiScriptEngine::connectControl disconnected " << group << name << " from " << function; 00848 m_connectedControls.remove(key, function); 00849 // Only disconnect the signal if there are no other instances of this control using it 00850 if (!m_connectedControls.contains(key)) { 00851 this->disconnect(cobj, SIGNAL(valueChanged(double)), 00852 this, SLOT(slotValueChanged(double))); 00853 } 00854 } else { 00855 // qDebug() << "MidiScriptEngine::connectControl connected " << group << name << " to " << function; 00856 connect(cobj, SIGNAL(valueChanged(double)), 00857 this, SLOT(slotValueChanged(double)), 00858 Qt::QueuedConnection); 00859 m_connectedControls.insert(key, function); 00860 } 00861 return true; 00862 } 00863 00864 return false; 00865 } 00866 00867 /* -------- ------------------------------------------------------ 00868 Purpose: Receives valueChanged() slots from ControlObjects, and 00869 fires off the appropriate script function. 00870 -------- ------------------------------------------------------ */ 00871 void MidiScriptEngine::slotValueChanged(double value) { 00872 m_scriptEngineLock.lock(); 00873 00874 ControlObjectThread* sender = dynamic_cast<ControlObjectThread*>(this->sender()); 00875 if(sender == NULL) { 00876 qWarning() << "MidiScriptEngine::slotValueChanged() Shouldn't happen -- sender == NULL"; 00877 m_scriptEngineLock.unlock(); 00878 return; 00879 } 00880 ControlObject* pSenderCO = sender->getControlObject(); 00881 if (pSenderCO == NULL) { 00882 return; 00883 } 00884 ConfigKey key = pSenderCO->getKey(); 00885 00886 //qDebug() << QString("MidiScriptEngine: slotValueChanged Thread ID=%1").arg(QThread::currentThreadId(),0,16); 00887 00888 if(m_connectedControls.contains(key)) { 00889 QMultiHash<ConfigKey, QString>::iterator i = m_connectedControls.find(key); 00890 while (i != m_connectedControls.end() && i.key() == key) { 00891 QString function = i.value(); 00892 00893 // qDebug() << "MidiScriptEngine::slotValueChanged() received signal from " << key.group << key.item << " ... firing : " << function; 00894 00895 // Could branch to safeExecute from here, but for now do it this way. 00896 QScriptValue function_value = m_pEngine->evaluate(function); 00897 QScriptValueList args; 00898 args << QScriptValue(value); 00899 args << QScriptValue(key.group); // Added by Math` 00900 args << QScriptValue(key.item); // Added by Math` 00901 QScriptValue result = function_value.call(QScriptValue(), args); 00902 if (result.isError()) { 00903 qWarning()<< "MidiScriptEngine: Call to " << function << " resulted in an error: " << result.toString(); 00904 } 00905 ++i; 00906 } 00907 } else { 00908 qWarning() << "MidiScriptEngine::slotValueChanged() Received signal from ControlObject that is not connected to a script function."; 00909 } 00910 00911 m_scriptEngineLock.unlock(); 00912 } 00913 00914 /* -------- ------------------------------------------------------ 00915 Purpose: Evaluate a script file 00916 Input: Script filename 00917 Output: false if the script file has errors or doesn't exist 00918 -------- ------------------------------------------------------ */ 00919 bool MidiScriptEngine::safeEvaluate(QString scriptName, QList<QString> scriptPaths) { 00920 if(m_pEngine == NULL) { 00921 return false; 00922 } 00923 00924 QString filename = ""; 00925 QFile input; 00926 00927 if (scriptPaths.length() == 0) { 00928 // If we aren't given any paths to search, assume that scriptName 00929 // contains the full file name 00930 filename = scriptName; 00931 input.setFileName(filename); 00932 } else { 00933 QListIterator<QString> it(scriptPaths); 00934 do { 00935 filename = it.next()+scriptName; 00936 input.setFileName(filename); 00937 } while (it.hasNext() && !input.exists()); 00938 } 00939 00940 qDebug() << "MidiScriptEngine: Loading" << filename; 00941 00942 // Read in the script file 00943 if (!input.open(QIODevice::ReadOnly)) { 00944 QString errorLog = 00945 QString("MidiScriptEngine: Problem opening the script file: %1, error # %2, %3") 00946 .arg(filename) 00947 .arg(input.error()) 00948 .arg(input.errorString()); 00949 00950 // GUI actions do not belong in the MSE. They should be passed to 00951 // the above layers, along with input.errorString(), and that layer 00952 // can take care of notifying the user. The script engine should do 00953 // one thign and one thign alone -- run the scripts. 00954 if (m_midiDebug) { 00955 qCritical() << errorLog; 00956 } else { 00957 qWarning() << errorLog; 00958 if (m_midiPopups) { 00959 ErrorDialogProperties* props = ErrorDialogHandler::instance()->newDialogProperties(); 00960 props->setType(DLG_WARNING); 00961 props->setTitle("MIDI script file problem"); 00962 props->setText(QString("There was a problem opening the MIDI script file %1.").arg(filename)); 00963 props->setInfoText(input.errorString()); 00964 00965 ErrorDialogHandler::instance()->requestErrorDialog(props); 00966 } 00967 } 00968 return false; 00969 } 00970 00971 QString scriptCode = ""; 00972 scriptCode.append(input.readAll()); 00973 scriptCode.append('\n'); 00974 input.close(); 00975 00976 // Check syntax 00977 QScriptSyntaxCheckResult result = m_pEngine->checkSyntax(scriptCode); 00978 QString error=""; 00979 switch (result.state()) { 00980 case (QScriptSyntaxCheckResult::Valid): break; 00981 case (QScriptSyntaxCheckResult::Intermediate): 00982 error = "Incomplete code"; 00983 break; 00984 case (QScriptSyntaxCheckResult::Error): 00985 error = "Syntax error"; 00986 break; 00987 } 00988 if (error!="") { 00989 error = QString("%1 at line %2, column %3 in file %4: %5") 00990 .arg(error) 00991 .arg(result.errorLineNumber()) 00992 .arg(result.errorColumnNumber()) 00993 .arg(filename) 00994 .arg(result.errorMessage()); 00995 00996 if (m_midiDebug) qCritical() << "MidiScriptEngine:" << error; 00997 else { 00998 qWarning() << "MidiScriptEngine:" << error; 00999 if (m_midiPopups) { 01000 ErrorDialogProperties* props = ErrorDialogHandler::instance()->newDialogProperties(); 01001 props->setType(DLG_WARNING); 01002 props->setTitle("MIDI script file error"); 01003 props->setText(QString("There was an error in the MIDI script file %1.").arg(filename)); 01004 props->setInfoText("The functionality provided by this script file will be disabled."); 01005 props->setDetails(error); 01006 01007 ErrorDialogHandler::instance()->requestErrorDialog(props); 01008 } 01009 } 01010 return false; 01011 } 01012 01013 // Evaluate the code 01014 QScriptValue scriptFunction = m_pEngine->evaluate(scriptCode, filename); 01015 01016 // Record errors 01017 if (checkException()) 01018 return false; 01019 01020 // Add the code we evaluated to our index 01021 generateScriptFunctions(scriptCode); 01022 01023 return true; 01024 } 01025 01026 /* 01027 * Check whether a source file that was evaluated()'d has errors. 01028 */ 01029 bool MidiScriptEngine::hasErrors(QString filename) { 01030 m_scriptEngineLock.lock(); 01031 bool ret = m_scriptErrors.contains(filename); 01032 m_scriptEngineLock.unlock(); 01033 return ret; 01034 } 01035 01036 /* 01037 * Get the errors for a source file that was evaluated()'d 01038 */ 01039 const QStringList MidiScriptEngine::getErrors(QString filename) { 01040 QStringList ret; 01041 m_scriptEngineLock.lock(); 01042 if(m_scriptErrors.contains(filename)) 01043 ret = m_scriptErrors.value(filename); 01044 m_scriptEngineLock.unlock(); 01045 return ret; 01046 } 01047 01048 01049 /* -------- ------------------------------------------------------ 01050 Purpose: Creates & starts a timer that runs some script code 01051 on timeout 01052 Input: Number of milliseconds, script function to call, 01053 whether it should fire just once 01054 Output: The timer's ID, 0 if starting it failed 01055 -------- ------------------------------------------------------ */ 01056 int MidiScriptEngine::beginTimer(int interval, QScriptValue timerCallback, 01057 bool oneShot) { 01058 // When this function runs, assert that somebody is holding the script 01059 // engine lock. 01060 bool lock = m_scriptEngineLock.tryLock(); 01061 Q_ASSERT(!lock); 01062 if (lock) { 01063 m_scriptEngineLock.unlock(); 01064 } 01065 01066 if (!timerCallback.isFunction() && !timerCallback.isString()) { 01067 qWarning() << "Invalid timer callback provided to beginTimer." 01068 << "Valid callbacks are strings and functions."; 01069 return 0; 01070 } 01071 01072 if (interval < 20) { 01073 qWarning() << "Timer request for" << interval 01074 << "ms is too short. Setting to the minimum of 20ms."; 01075 interval = 20; 01076 } 01077 // This makes use of every QObject's internal timer mechanism. Nice, clean, 01078 // and simple. See http://doc.trolltech.com/4.6/qobject.html#startTimer for 01079 // details 01080 int timerId = startTimer(interval); 01081 TimerInfo info; 01082 info.callback = timerCallback; 01083 QScriptContext *ctxt = m_pEngine->currentContext(); 01084 info.context = ctxt ? ctxt->thisObject() : QScriptValue(); 01085 info.oneShot = oneShot; 01086 m_timers[timerId] = info; 01087 if (timerId == 0) { 01088 qWarning() << "MIDI Script timer could not be created"; 01089 } else if (m_midiDebug) { 01090 if (oneShot) 01091 qDebug() << "Starting one-shot timer:" << timerId; 01092 else 01093 qDebug() << "Starting timer:" << timerId; 01094 } 01095 return timerId; 01096 } 01097 01098 /* -------- ------------------------------------------------------ 01099 Purpose: Stops & removes a timer 01100 Input: ID of timer to stop 01101 Output: - 01102 -------- ------------------------------------------------------ */ 01103 void MidiScriptEngine::stopTimer(int timerId) { 01104 // When this function runs, assert that somebody is holding the script 01105 // engine lock. 01106 bool lock = m_scriptEngineLock.tryLock(); 01107 Q_ASSERT(!lock); 01108 if(lock) m_scriptEngineLock.unlock(); 01109 01110 if (!m_timers.contains(timerId)) { 01111 qWarning() << "Killing timer" << timerId << ": That timer does not exist!"; 01112 return; 01113 } 01114 if (m_midiDebug) qDebug() << "Killing timer:" << timerId; 01115 01116 killTimer(timerId); 01117 m_timers.remove(timerId); 01118 } 01119 01120 /* -------- ------------------------------------------------------ 01121 Purpose: Stops & removes all timers (for shutdown) 01122 Input: - 01123 Output: - 01124 -------- ------------------------------------------------------ */ 01125 void MidiScriptEngine::stopAllTimers() { 01126 // When this function runs, assert that somebody is holding the script 01127 // engine lock. 01128 bool lock = m_scriptEngineLock.tryLock(); 01129 Q_ASSERT(!lock); 01130 if(lock) m_scriptEngineLock.unlock(); 01131 01132 QMutableHashIterator<int, TimerInfo> i(m_timers); 01133 while (i.hasNext()) { 01134 i.next(); 01135 stopTimer(i.key()); 01136 } 01137 } 01138 01139 /* -------- ------------------------------------------------------ 01140 Purpose: Runs the appropriate script code on timer events 01141 Input: - 01142 Output: - 01143 -------- ------------------------------------------------------ */ 01144 void MidiScriptEngine::timerEvent(QTimerEvent *event) { 01145 int timerId = event->timerId(); 01146 01147 m_scriptEngineLock.lock(); 01148 01149 // See if this is a scratching timer 01150 if (m_scratchTimers.contains(timerId)) { 01151 m_scriptEngineLock.unlock(); 01152 scratchProcess(timerId); 01153 return; 01154 } 01155 01156 if (!m_timers.contains(timerId)) { 01157 qWarning() << "Timer" << timerId << "fired but there's no function mapped to it!"; 01158 m_scriptEngineLock.unlock(); 01159 return; 01160 } 01161 01162 TimerInfo timerTarget = m_timers[timerId]; 01163 if (timerTarget.oneShot) stopTimer(timerId); 01164 01165 if (timerTarget.callback.isString()) { 01166 internalExecute(timerTarget.context, timerTarget.callback.toString()); 01167 } else if (timerTarget.callback.isFunction()) { 01168 safeExecute(timerTarget.context, timerTarget.callback); 01169 } 01170 m_scriptEngineLock.unlock(); 01171 } 01172 01173 /* -------- ------------------------------------------------------ 01174 Purpose: Enables scratching for relative controls 01175 Input: Virtual deck to scratch, 01176 Number of intervals per revolution of the controller wheel, 01177 RPM for the track at normal speed (usually 33+1/3), 01178 (optional) alpha value for the filter, 01179 (optional) beta value for the filter 01180 Output: - 01181 -------- ------------------------------------------------------ */ 01182 void MidiScriptEngine::scratchEnable(int deck, int intervalsPerRev, float rpm, float alpha, float beta) { 01183 // If we're already scratching this deck, override that with this request 01184 if (m_dx[deck]) { 01185 // qDebug() << "Already scratching deck" << deck << ". Overriding."; 01186 int timerId = m_scratchTimers.key(deck); 01187 killTimer(timerId); 01188 m_scratchTimers.remove(timerId); 01189 } 01190 01191 // Controller resolution in intervals per second at normal speed (rev/min * ints/rev * mins/sec) 01192 float intervalsPerSecond = (rpm * intervalsPerRev)/60; 01193 01194 m_dx[deck] = 1/intervalsPerSecond; 01195 m_intervalAccumulator[deck] = 0; 01196 m_ramp[deck] = false; 01197 01198 QString group = QString("[Channel%1]").arg(deck); 01199 01200 // Ramp 01201 float initVelocity = 0.0; // Default to stopped 01202 01203 // See if the deck is already being scratched 01204 ControlObjectThread *cot = getControlObjectThread(group, "scratch2_enable"); 01205 if (cot != NULL && cot->get() == 1) { 01206 // If so, set the filter's initial velocity to the scratch speed 01207 cot = getControlObjectThread(group, "scratch2"); 01208 if (cot != NULL) initVelocity=cot->get(); 01209 } else { 01210 // See if deck is playing 01211 cot = getControlObjectThread(group, "play"); 01212 if (cot != NULL && cot->get() == 1) { 01213 // If so, set the filter's initial velocity to the playback speed 01214 float rate=0; 01215 cot = getControlObjectThread(group, "rate"); 01216 if (cot != NULL) rate = cot->get(); 01217 cot = getControlObjectThread(group, "rateRange"); 01218 if (cot != NULL) rate = rate * cot->get(); 01219 // Add 1 since the deck is playing 01220 rate++; 01221 // See if we're in reverse play 01222 cot = getControlObjectThread(group, "reverse"); 01223 if (cot != NULL && cot->get() == 1) rate = -rate; 01224 01225 initVelocity = rate; 01226 } 01227 } 01228 01229 // Initialize pitch filter (0.001s = 1ms) 01230 // (We're assuming the OS actually gives us a 1ms timer below) 01231 if (alpha && beta) m_pitchFilter[deck]->init(0.001, initVelocity, alpha, beta); 01232 else m_pitchFilter[deck]->init(0.001, initVelocity); // Use filter's defaults if not specified 01233 01234 int timerId = startTimer(1); // 1ms is shortest possible, OS dependent 01235 // Associate this virtual deck with this timer for later processing 01236 m_scratchTimers[timerId] = deck; 01237 01238 // Set scratch2_enable 01239 cot = getControlObjectThread(group, "scratch2_enable"); 01240 if(cot != NULL) cot->slotSet(1); 01241 } 01242 01243 /* -------- ------------------------------------------------------ 01244 Purpose: Accumulates "ticks" of the controller wheel 01245 Input: Virtual deck to scratch, interval value (usually +1 or -1) 01246 Output: - 01247 -------- ------------------------------------------------------ */ 01248 void MidiScriptEngine::scratchTick(int deck, int interval) { 01249 m_intervalAccumulator[deck] += interval; 01250 } 01251 01252 /* -------- ------------------------------------------------------ 01253 Purpose: Applies the accumulated movement to the track speed 01254 Input: ID of timer for this deck 01255 Output: - 01256 -------- ------------------------------------------------------ */ 01257 void MidiScriptEngine::scratchProcess(int timerId) { 01258 int deck = m_scratchTimers[timerId]; 01259 PitchFilter* filter = m_pitchFilter[deck]; 01260 QString group = QString("[Channel%1]").arg(deck); 01261 01262 if (!filter) { 01263 qWarning() << "Scratch filter pointer is null on deck" << deck; 01264 return; 01265 } 01266 01267 // Give the filter a data point: 01268 01269 // If we're ramping to end scratching, feed fixed data 01270 if (m_ramp[deck]) filter->observation(m_rampTo[deck]*0.001); 01271 // This will (and should) be 0 if no net ticks have been accumulated (i.e. the wheel is stopped) 01272 else filter->observation(m_dx[deck] * m_intervalAccumulator[deck]); 01273 01274 // Actually do the scratching 01275 ControlObjectThread *cot = getControlObjectThread(group, "scratch2"); 01276 if(cot != NULL) cot->slotSet(filter->currentPitch()); 01277 01278 // Reset accumulator 01279 m_intervalAccumulator[deck] = 0; 01280 01281 // If we're ramping and the current pitch is really close to the rampTo value, 01282 // end scratching 01283 // if (m_ramp[deck]) qDebug() << "Ramping to" << m_rampTo[deck] << " Currently at:" << filter->currentPitch(); 01284 if (m_ramp[deck] && fabs(m_rampTo[deck]-filter->currentPitch()) <= 0.00001) { 01285 01286 m_ramp[deck] = false; // Not ramping no mo' 01287 01288 // Clear scratch2_enable 01289 cot = getControlObjectThread(group, "scratch2_enable"); 01290 if(cot != NULL) cot->slotSet(0); 01291 01292 // Remove timer 01293 killTimer(timerId); 01294 m_scratchTimers.remove(timerId); 01295 01296 m_dx[deck] = 0; 01297 } 01298 } 01299 01300 /* -------- ------------------------------------------------------ 01301 Purpose: Stops scratching the specified virtual deck 01302 Input: Virtual deck to stop scratching 01303 Output: - 01304 -------- ------------------------------------------------------ */ 01305 void MidiScriptEngine::scratchDisable(int deck) { 01306 QString group = QString("[Channel%1]").arg(deck); 01307 01308 // See if deck is playing 01309 ControlObjectThread *cot = getControlObjectThread(group, "play"); 01310 if (cot != NULL && cot->get() == 1) { 01311 // If so, set the target velocity to the playback speed 01312 float rate = 0; 01313 // Get the pitch slider value 01314 cot = getControlObjectThread(group, "rate"); 01315 if (cot != NULL) rate = cot->get(); 01316 // Multiply by the pitch range 01317 cot = getControlObjectThread(group, "rateRange"); 01318 if (cot != NULL) rate = rate * cot->get(); 01319 // Add 1 since the deck is playing 01320 rate++; 01321 // See if we're in reverse play 01322 cot = getControlObjectThread(group, "reverse"); 01323 if (cot != NULL && cot->get() == 1) rate = -rate; 01324 01325 m_rampTo[deck] = rate; 01326 } else { 01327 m_rampTo[deck] = 0.0; 01328 } 01329 01330 m_ramp[deck] = true; // Activate the ramping in scratchProcess() 01331 } 01332 01333 /* -------- ------------------------------------------------------ 01334 Purpose: [En/dis]ables soft-takeover status for a particular MixxxControl 01335 Input: MixxxControl group and key values, 01336 whether to set the soft-takeover status or not 01337 Output: - 01338 -------- ------------------------------------------------------ */ 01339 void MidiScriptEngine::softTakeover(QString group, QString name, bool set) { 01340 MixxxControl mc = MixxxControl(group, name); 01341 if (set) { 01342 m_st.enable(mc); 01343 } else { 01344 m_st.disable(mc); 01345 } 01346 }