diff --git a/src/controllers/controllerengine.cpp b/src/controllers/controllerengine.cpp index 187ee5263d2..59a1ec5e25a 100644 --- a/src/controllers/controllerengine.cpp +++ b/src/controllers/controllerengine.cpp @@ -23,6 +23,8 @@ // (closure compatible version of connectControl) #include +#define SCRATCH_DEBUG_OUTPUT false + namespace { constexpr int kDecks = 16; @@ -30,6 +32,8 @@ constexpr int kDecks = 16; // timer. constexpr int kScratchTimerMs = 1; constexpr double kAlphaBetaDt = kScratchTimerMs / 1000.0; +// stop ramping at a rate which doesn't produce any audible output anymore +constexpr double kBrakeRampToRate = 0.01; } // namespace ControllerEngine::ControllerEngine( @@ -51,6 +55,7 @@ ControllerEngine::ControllerEngine( m_scratchFilters.resize(kDecks); m_rampFactor.resize(kDecks); m_brakeActive.resize(kDecks); + m_spinbackActive.resize(kDecks); m_softStartActive.resize(kDecks); // Initialize arrays used for testing and pointers for (int i = 0; i < kDecks; ++i) { @@ -1160,7 +1165,7 @@ int ControllerEngine::beginTimer(int interval, const QScriptValue& timerCallback Input: ID of timer to stop Output: - -------- ------------------------------------------------------ */ -void ControllerEngine::stopTimer(int timerId) { +void ControllerEngine::stopTimer(const int timerId) { if (!m_timers.contains(timerId)) { qWarning() << "Killing timer" << timerId << ": That timer does not exist!"; return; @@ -1170,6 +1175,16 @@ void ControllerEngine::stopTimer(int timerId) { m_timers.remove(timerId); } +void ControllerEngine::stopScratchTimer(const int timerId) { + if (!m_scratchTimers.contains(timerId)) { + qWarning() << "Killing scratch timer" << timerId << ": That timer does not exist!"; + return; + } + controllerDebug("Killing scratch timer:" << timerId); + killTimer(timerId); + m_scratchTimers.remove(timerId); +} + void ControllerEngine::stopAllTimers() { QMutableHashIterator i(m_timers); while (i.hasNext()) { @@ -1229,12 +1244,36 @@ bool ControllerEngine::isDeckPlaying(const QString& group) { ControlObjectScript* pPlay = getControlObjectScript(group, "play"); if (pPlay == nullptr) { - QString error = QString("Could not getControlObjectScript()"); - scriptErrorDialog(error, error); - return false; + QString error = QString("Could not get ControlObjectScript(%1, play)").arg(group); + scriptErrorDialog(error, error); + return false; + } + + return pPlay->toBool(); +} + +void ControllerEngine::stopDeck(const QString& group) { + ControlObjectScript* pPlay = getControlObjectScript(group, "play"); + + if (pPlay == nullptr) { + QString error = QString("Could not get ControlObjectScript(%1, play)").arg(group); + scriptErrorDialog(error, error); + return; + } + + pPlay->set(0.0); +} + +bool ControllerEngine::isTrackLoaded(const QString& group) { + ControlObjectScript* pTrackLoaded = getControlObjectScript(group, "track_loaded"); + + if (pTrackLoaded == nullptr) { + QString error = QString("Could not get ControlObjectScript(%1, track_loaded)").arg(group); + scriptErrorDialog(error, error); + return false; } - return pPlay->get() > 0.0; + return pTrackLoaded->toBool(); } /* -------- ------------------------------------------------------ @@ -1333,31 +1372,47 @@ void ControllerEngine::scratchTick(int deck, int interval) { Input: ID of timer for this deck Output: - -------- ------------------------------------------------------ */ -void ControllerEngine::scratchProcess(int timerId) { - int deck = m_scratchTimers[timerId]; +void ControllerEngine::scratchProcess(const int timerId) { +#if SCRATCH_DEBUG_OUTPUT + qDebug() << " ."; + qDebug() << " ControllerEngine::scratchProcess"; +#endif + const int deck = m_scratchTimers[timerId]; // PlayerManager::groupForDeck is 0-indexed. - QString group = PlayerManager::groupForDeck(deck - 1); + const QString group = PlayerManager::groupForDeck(deck - 1); AlphaBetaFilter* filter = m_scratchFilters[deck]; if (!filter) { qWarning() << "Scratch filter pointer is null on deck" << deck; return; } +#if SCRATCH_DEBUG_OUTPUT const double oldRate = filter->predictedVelocity(); +#endif // Give the filter a data point: // If we're ramping to end scratching and the wheel hasn't been turned very // recently (spinback after lift-off,) feed fixed data + qDebug() << "."; if (m_ramp[deck] && !m_softStartActive[deck] && ((mixxx::Time::elapsed() - m_lastMovement[deck]) >= mixxx::Duration::fromMillis(1))) { +#if SCRATCH_DEBUG_OUTPUT + qDebug() << " ramp && !softStart"; +#endif filter->observation(m_rampTo[deck] * m_rampFactor[deck]); // Once this code path is run, latch so it always runs until reset //m_lastMovement[deck] += mixxx::Duration::fromSeconds(1); } else if (m_softStartActive[deck]) { +#if SCRATCH_DEBUG_OUTPUT + qDebug() << " softStart"; +#endif // pretend we have moved by (desired rate*default distance) - filter->observation(m_rampTo[deck]*kAlphaBetaDt); + filter->observation(m_rampTo[deck] * kAlphaBetaDt); } else { +#if SCRATCH_DEBUG_OUTPUT + qDebug() << " else"; +#endif // This will (and should) be 0 if no net ticks have been accumulated // (i.e. the wheel is stopped) filter->observation(m_dx[deck] * m_intervalAccumulator[deck]); @@ -1375,24 +1430,32 @@ void ControllerEngine::scratchProcess(int timerId) { // Reset accumulator m_intervalAccumulator[deck] = 0; +#if SCRATCH_DEBUG_OUTPUT + qDebug() << "."; + qDebug() << " oldRate " << oldRate; + qDebug() << " newRate " << newRate; + qDebug() << " fabs " << fabs(trunc((m_rampTo[deck] - newRate) * 100000) / 100000); + qDebug() << "."; +#endif // End scratching if we're ramping and the current rate is really close to the rampTo value if ((m_ramp[deck] && fabs(m_rampTo[deck] - newRate) <= 0.00001) || - // or if we brake or softStart and have crossed over the desired value, - ((m_brakeActive[deck] || m_softStartActive[deck]) && ( - (oldRate > m_rampTo[deck] && newRate < m_rampTo[deck]) || - (oldRate < m_rampTo[deck] && newRate > m_rampTo[deck]))) || - // or if the deck was stopped manually during brake or softStart - ((m_brakeActive[deck] || m_softStartActive[deck]) && (!isDeckPlaying(group)))) { + // or if we brake, spin back or softstart and have crossed over the desired value, + (m_brakeActive[deck] && newRate < m_rampTo[deck]) || + ((m_spinbackActive[deck] || m_softStartActive[deck]) && newRate > m_rampTo[deck]) || + // or if the deck was stopped manually during brake or softStart + ((m_brakeActive[deck] || m_softStartActive[deck]) && (!isDeckPlaying(group))) || + // or if there is no track loaded (anymore) + !isTrackLoaded(group)) { // Not ramping no mo' m_ramp[deck] = false; - if (m_brakeActive[deck]) { - // If in brake mode, set scratch2 rate to 0 and turn off the play button. + if (m_brakeActive[deck] || m_spinbackActive[deck]) { +#if SCRATCH_DEBUG_OUTPUT + qDebug() << " brake || spinback, stop scratching, stop deck"; +#endif + // If in brake mode, set scratch2 rate to 0 and stop the deck. pScratch2->slotSet(0.0); - ControlObjectScript* pPlay = getControlObjectScript(group, "play"); - if (pPlay != nullptr) { - pPlay->slotSet(0.0); - } + stopDeck(group); } // Clear scratch2_enable to end scratching. @@ -1403,13 +1466,16 @@ void ControllerEngine::scratchProcess(int timerId) { } pScratch2Enable->slotSet(0); - // Remove timer - killTimer(timerId); - m_scratchTimers.remove(timerId); + stopScratchTimer(timerId); m_dx[deck] = 0.0; m_brakeActive[deck] = false; + m_spinbackActive[deck] = false; m_softStartActive[deck] = false; +#if SCRATCH_DEBUG_OUTPUT + qDebug() << " DONE scratching"; + qDebug() << " ."; +#endif } } @@ -1502,9 +1568,11 @@ void ControllerEngine::softTakeoverIgnoreNextValue( rate (optional) Output: - -------- ------------------------------------------------------ */ -void ControllerEngine::spinback(int deck, bool activate, double factor, double rate) { - // defaults for args set in header file - brake(deck, activate, factor, rate); +void ControllerEngine::spinback( + const int deck, bool activate, const double factor, const double rate) { + qDebug() << "ControllerEngine::spinback(deck:" << deck << ", activate:" << activate + << ", factor:" << factor << ", rate:" << rate; + brake(deck, activate, -factor, rate); } /* -------- ------------------------------------------------------ @@ -1513,35 +1581,49 @@ void ControllerEngine::spinback(int deck, bool activate, double factor, double r rate (optional, necessary for spinback) Output: - -------- ------------------------------------------------------ */ -void ControllerEngine::brake(int deck, bool activate, double factor, double rate) { +void ControllerEngine::brake(const int deck, bool activate, double factor, const double rate) { + qDebug() << "ControllerEngine::brake(deck:" << deck << ", activate:" << activate + << ", factor:" << factor << ", rate:" << rate; // PlayerManager::groupForDeck is 0-indexed. - QString group = PlayerManager::groupForDeck(deck - 1); - - // kill timer when both enabling or disabling - int timerId = m_scratchTimers.key(deck); - killTimer(timerId); - m_scratchTimers.remove(timerId); - + const QString group = PlayerManager::groupForDeck(deck - 1); // enable/disable scratch2 mode ControlObjectScript* pScratch2Enable = getControlObjectScript(group, "scratch2_enable"); if (pScratch2Enable != nullptr) { pScratch2Enable->slotSet(activate ? 1 : 0); } - // used in scratchProcess for the different timer behavior we need - m_brakeActive[deck] = activate; - double initRate = rate; - - if (activate) { - // store the new values for this spinback/brake effect - if (initRate == 1.0) {// then rate is really 1.0 or was set to default - // in /res/common-controller-scripts.js so check for real value, - // taking pitch into account - initRate = getDeckRate(group); + // Used for killing the current timer when both enabling or disabling + // Don't kill timer yet! This may be a brake init while currently spinning back + // and we don't want to interrupt that. + int timerId = m_scratchTimers.key(deck); + + if (!activate) { + m_brakeActive[deck] = false; + m_spinbackActive[deck] = false; + stopScratchTimer(timerId); + return; + } + // Distinguish spinback and brake. Both ramp to a very low rate to avoid a long, + // inaudible run out. For spinback that rate is negative so we don't cross 0. + // Spinback and brake also require different handling in scratchProcess. + double initRate; + if (rate < -kBrakeRampToRate) { // spinback + m_spinbackActive[deck] = true; + m_brakeActive[deck] = false; + m_rampTo[deck] = -kBrakeRampToRate; + initRate = rate; + } else if (rate > kBrakeRampToRate) { // brake + // It just doesn't make sense to allow brake to interrupt spinback or an + // already running brake process + if (m_spinbackActive[deck] || m_brakeActive[deck]) { + return; } - // stop ramping at a rate which doesn't produce any audible output anymore - m_rampTo[deck] = 0.01; - // if we are currently softStart()ing, stop it + m_brakeActive[deck] = true; + m_spinbackActive[deck] = false; + m_rampTo[deck] = kBrakeRampToRate; + // Let's fetch the current rate to create a seamless brake process + initRate = getDeckRate(group); + // If we are currently softStart'ing adopt the current scratch rate if (m_softStartActive[deck]) { m_softStartActive[deck] = false; AlphaBetaFilter* filter = m_scratchFilters[deck]; @@ -1549,31 +1631,41 @@ void ControllerEngine::brake(int deck, bool activate, double factor, double rate initRate = filter->predictedVelocity(); } } + } else { // -kBrakeRampToRate <= rate <= kBrakeRampToRate + // This filters stopped deck and rare case of very low initial rates + m_brakeActive[deck] = false; + m_spinbackActive[deck] = false; + stopScratchTimer(timerId); + stopDeck(group); + return; + } + stopScratchTimer(timerId); - // setup timer and set scratch2 - timerId = startTimer(kScratchTimerMs); - m_scratchTimers[timerId] = deck; + // If we want to brake or spin back and are currently softStart'ing, stop it. - ControlObjectScript* pScratch2 = getControlObjectScript(group, "scratch2"); - if (pScratch2 != nullptr) { - pScratch2->slotSet(initRate); - } + // setup timer and set scratch2 + timerId = startTimer(kScratchTimerMs); + m_scratchTimers[timerId] = deck; - // setup the filter with default alpha and beta*factor - double alphaBrake = 1.0/512; - // avoid decimals for fine adjusting - if (factor>1) { - factor = ((factor-1)/10)+1; - } - double betaBrake = ((1.0/512)/1024)*factor; // default*factor - AlphaBetaFilter* filter = m_scratchFilters[deck]; - if (filter != nullptr) { - filter->init(kAlphaBetaDt, initRate, alphaBrake, betaBrake); - } + ControlObjectScript* pScratch2 = getControlObjectScript(group, "scratch2"); + if (pScratch2 != nullptr) { + pScratch2->slotSet(initRate); + } - // activate the ramping in scratchProcess() - m_ramp[deck] = true; + // setup the filter with default alpha and beta*factor + double alphaBrake = 1.0 / 512; + // avoid decimals for fine adjusting + if (factor > 1) { + factor = ((factor - 1) / 10) + 1; } + double betaBrake = ((1.0 / 512) / 1024) * factor; // default*factor + AlphaBetaFilter* filter = m_scratchFilters[deck]; + if (filter != nullptr) { + filter->init(kAlphaBetaDt, initRate, alphaBrake, betaBrake); + } + + // activate the ramping in scratchProcess() + m_ramp[deck] = true; } /* -------- ------------------------------------------------------ @@ -1581,14 +1673,15 @@ void ControllerEngine::brake(int deck, bool activate, double factor, double rate Input: deck, activate/deactivate, factor (optional) Output: - -------- ------------------------------------------------------ */ -void ControllerEngine::softStart(int deck, bool activate, double factor) { +void ControllerEngine::softStart(const int deck, bool activate, double factor) { + qDebug() << "ControllerEngine::softStart(deck:" << deck << ", activate:" << activate + << ", factor:" << factor; // PlayerManager::groupForDeck is 0-indexed. - QString group = PlayerManager::groupForDeck(deck - 1); + const QString group = PlayerManager::groupForDeck(deck - 1); // kill timer when both enabling or disabling int timerId = m_scratchTimers.key(deck); - killTimer(timerId); - m_scratchTimers.remove(timerId); + stopScratchTimer(timerId); // enable/disable scratch2 mode ControlObjectScript* pScratch2Enable = getControlObjectScript(group, "scratch2_enable"); @@ -1598,49 +1691,52 @@ void ControllerEngine::softStart(int deck, bool activate, double factor) { // used in scratchProcess for the different timer behavior we need m_softStartActive[deck] = activate; - double initRate = 0.0; - - if (activate) { - // acquire deck rate - m_rampTo[deck] = getDeckRate(group); - // if brake()ing, get current rate from filter - if (m_brakeActive[deck]) { - m_brakeActive[deck] = false; + if (!activate) { + return; + } - AlphaBetaFilter* filter = m_scratchFilters[deck]; - if (filter != nullptr) { - initRate = filter->predictedVelocity(); - } - } + double initRate = 0.0; + // acquire deck rate + m_rampTo[deck] = getDeckRate(group); - // setup timer, start playing and set scratch2 - timerId = startTimer(kScratchTimerMs); - m_scratchTimers[timerId] = deck; + // if braking or spinning back, get current rate from filter + if (m_brakeActive[deck] || m_spinbackActive[deck]) { + m_brakeActive[deck] = false; + m_spinbackActive[deck] = false; - ControlObjectScript* pPlay = getControlObjectScript(group, "play"); - if (pPlay != nullptr) { - pPlay->slotSet(1.0); + AlphaBetaFilter* filter = m_scratchFilters[deck]; + if (filter != nullptr) { + initRate = filter->predictedVelocity(); } + } - ControlObjectScript* pScratch2 = getControlObjectScript(group, "scratch2"); - if (pScratch2 != nullptr) { - pScratch2->slotSet(initRate); - } + // setup timer, start playing and set scratch2 + timerId = startTimer(kScratchTimerMs); + m_scratchTimers[timerId] = deck; - // setup the filter like in brake(), with default alpha and beta*factor - double alphaSoft = 1.0/512; - // avoid decimals for fine adjusting - if (factor>1) { - factor = ((factor-1)/10)+1; - } - double betaSoft = ((1.0/512)/1024)*factor; // default: (1.0/512)/1024 - AlphaBetaFilter* filter = m_scratchFilters[deck]; - if (filter != nullptr) { // kAlphaBetaDt = 1/1000 seconds - filter->init(kAlphaBetaDt, initRate, alphaSoft, betaSoft); - } + ControlObjectScript* pPlay = getControlObjectScript(group, "play"); + if (pPlay != nullptr) { + pPlay->slotSet(1.0); + } + + ControlObjectScript* pScratch2 = getControlObjectScript(group, "scratch2"); + if (pScratch2 != nullptr) { + pScratch2->slotSet(initRate); + } - // activate the ramping in scratchProcess() - m_ramp[deck] = true; + // setup the filter like in brake(), with default alpha and beta*factor + double alphaSoft = 1.0 / 512; + // avoid decimals for fine adjusting + if (factor > 1) { + factor = ((factor - 1) / 10) + 1; } + double betaSoft = ((1.0 / 512) / 1024) * factor; // default: (1.0/512)/1024 + AlphaBetaFilter* filter = m_scratchFilters[deck]; + if (filter != nullptr) { // kAlphaBetaDt = 1/1000 seconds + filter->init(kAlphaBetaDt, initRate, alphaSoft, betaSoft); + } + + // activate the ramping in scratchProcess() + m_ramp[deck] = true; } diff --git a/src/controllers/controllerengine.h b/src/controllers/controllerengine.h index 9caab019b7e..291e4ce2315 100644 --- a/src/controllers/controllerengine.h +++ b/src/controllers/controllerengine.h @@ -132,9 +132,15 @@ class ControllerEngine : public QObject { Q_INVOKABLE bool isScratching(int deck); Q_INVOKABLE void softTakeover(const QString& group, const QString& name, bool set); Q_INVOKABLE void softTakeoverIgnoreNextValue(const QString& group, const QString& name); - Q_INVOKABLE void brake(int deck, bool activate, double factor=1.0, double rate=1.0); - Q_INVOKABLE void spinback(int deck, bool activate, double factor=1.8, double rate=-10.0); - Q_INVOKABLE void softStart(int deck, bool activate, double factor=1.0); + Q_INVOKABLE void brake(const int deck, + bool activate, + double factor = 1.0, + const double rate = 1.0); + Q_INVOKABLE void spinback(const int deck, + bool activate, + double factor = 1.8, + const double rate = -10.0); + Q_INVOKABLE void softStart(const int deck, bool activate, double factor = 1.0); // Handler for timers that scripts set. virtual void timerEvent(QTimerEvent *event); @@ -192,9 +198,12 @@ class ControllerEngine : public QObject { ControlObjectScript* getControlObjectScript(const QString& group, const QString& name); // Scratching functions & variables - void scratchProcess(int timerId); + void scratchProcess(const int timerId); + void stopScratchTimer(const int timerId); bool isDeckPlaying(const QString& group); + void stopDeck(const QString& group); + bool isTrackLoaded(const QString& group); double getDeckRate(const QString& group); Controller* m_pController; @@ -216,7 +225,7 @@ class ControllerEngine : public QObject { QVarLengthArray m_intervalAccumulator; QVarLengthArray m_lastMovement; QVarLengthArray m_dx, m_rampTo, m_rampFactor; - QVarLengthArray m_ramp, m_brakeActive, m_softStartActive; + QVarLengthArray m_ramp, m_brakeActive, m_spinbackActive, m_softStartActive; QVarLengthArray m_scratchFilters; QHash m_scratchTimers; QHash m_scriptWrappedFunctionCache;