diff --git a/src/engine/controls/ratecontrol.cpp b/src/engine/controls/ratecontrol.cpp index 8e1c065ccda..29389d678ff 100644 --- a/src/engine/controls/ratecontrol.cpp +++ b/src/engine/controls/ratecontrol.cpp @@ -30,142 +30,172 @@ RateControl::RampMode RateControl::m_eRateRampMode; const double RateControl::kWheelMultiplier = 40.0; const double RateControl::kPausedJogMultiplier = 18.0; -RateControl::RateControl(const QString& group, - UserSettingsPointer pConfig) +RateControl::RateControl(const QString& group, UserSettingsPointer pConfig) : EngineControl(group, pConfig), m_pBpmControl(nullptr), + m_pSampleRate(QStringLiteral("[App]"), QStringLiteral("samplerate")), + m_pRateRatio(std::make_unique( + ConfigKey(group, QStringLiteral("rate_ratio")), + true, + false, + false, + 1.0)), + m_pRateDir(std::make_unique( + ConfigKey(group, QStringLiteral("rate_dir")))), + m_pRateRange(std::make_unique( + ConfigKey(group, QStringLiteral("rateRange")), 0.01, 4.00)), + // We need the sample rate so we can guesstimate something close + // what latency is. + // Allow rate slider to go out of bounds so that sync lock rate + // adjustments are not capped. + m_pRateSlider(std::make_unique( + ConfigKey(group, QStringLiteral("rate")), -1.0, 1.0, true)), + // Search rate. Rate used when searching in sound. This overrules the + // playback rate + m_pRateSearch(std::make_unique( + ConfigKey(group, QStringLiteral("rateSearch")), -300., 300.)), + // Temporary rate-change buttons + m_pButtonRateTempDown(std::make_unique( + ConfigKey(group, QStringLiteral("rate_temp_down")))), + m_pButtonRateTempDownSmall(std::make_unique( + ConfigKey(group, QStringLiteral("rate_temp_down_small")))), + m_pButtonRateTempUp(std::make_unique( + ConfigKey(group, QStringLiteral("rate_temp_up")))), + m_pButtonRateTempUpSmall(std::make_unique( + ConfigKey(group, QStringLiteral("rate_temp_up_small")))), + // Permanent rate-change buttons + m_pButtonRatePermDown(std::make_unique( + ConfigKey(group, QStringLiteral("rate_perm_down")))), + m_pButtonRatePermDownSmall(std::make_unique( + ConfigKey(group, QStringLiteral("rate_perm_down_small")))), + m_pButtonRatePermUp(std::make_unique( + ConfigKey(group, QStringLiteral("rate_perm_up")))), + m_pButtonRatePermUpSmall(std::make_unique( + ConfigKey(group, QStringLiteral("rate_perm_up_small")))), + // Reverse button + m_pReverseButton(std::make_unique( + ConfigKey(group, QStringLiteral("reverse")))), + m_pReverseRollButton(std::make_unique( + ConfigKey(group, QStringLiteral("reverseroll")))), + m_pForwardButton(std::make_unique( + ConfigKey(group, QStringLiteral("fwd")))), + m_pBackButton(std::make_unique( + ConfigKey(group, QStringLiteral("back")))), + // Wheel to control playback position/speed + m_pWheel(std::make_unique( + ConfigKey(group, QStringLiteral("wheel")))), + // Scratch controller, this is an accumulator which is useful for + // controllers that return individual +1 or -1s, these get added up + // and cleared when we read + m_pScratch2(std::make_unique( + ConfigKey(group, QStringLiteral("scratch2")))), + m_pScratch2Enable(std::make_unique( + ConfigKey(group, QStringLiteral("scratch2_enable")))), + m_pScratch2Scratching(std::make_unique(ConfigKey( + group, QStringLiteral("scratch2_indicates_scratching")))), + m_pScratchController( + std::make_unique(group)), + m_pJog(std::make_unique( + ConfigKey(group, QStringLiteral("jog")))), + m_pJogFilter(std::make_unique()), + // Vinyl control + m_pVCEnabled(ControlObject::getControl(ConfigKey( + getGroup(), QStringLiteral("vinylcontrol_enabled")))), + m_pVCScratching(ControlObject::getControl(ConfigKey( + getGroup(), QStringLiteral("vinylcontrol_scratching")))), + m_pVCMode(ControlObject::getControl( + ConfigKey(getGroup(), QStringLiteral("vinylcontrol_mode")))), + m_syncMode(group, QStringLiteral("sync_mode")), + m_slipEnabled(group, QStringLiteral("slip_enabled")), m_wrapAroundCount(0), m_jumpPos(mixxx::audio::FramePos()), m_targetPos(mixxx::audio::FramePos()), m_bTempStarted(false), m_tempRateRatio(0.0), m_dRateTempRampChange(0.0) { - m_pScratchController = new PositionScratchController(group); - // This is the resulting rate ratio that can be used for display or calculations. // The track original rate ratio is 1. - m_pRateRatio = new ControlObject(ConfigKey(group, "rate_ratio"), - true, false, false, 1.0); - connect(m_pRateRatio, &ControlObject::valueChanged, - this, &RateControl::slotRateRatioChanged, + connect(m_pRateRatio.get(), + &ControlObject::valueChanged, + this, + &RateControl::slotRateRatioChanged, Qt::DirectConnection); - m_pRateDir = new ControlObject(ConfigKey(group, "rate_dir")); - connect(m_pRateDir, &ControlObject::valueChanged, - this, &RateControl::slotRateRangeChanged, + connect(m_pRateDir.get(), + &ControlObject::valueChanged, + this, + &RateControl::slotRateRangeChanged, Qt::DirectConnection); - m_pRateRange = new ControlPotmeter( - ConfigKey(group, "rateRange"), 0.01, 4.00); - connect(m_pRateRange, &ControlObject::valueChanged, - this, &RateControl::slotRateRangeChanged, + connect(m_pRateRange.get(), + &ControlObject::valueChanged, + this, + &RateControl::slotRateRangeChanged, Qt::DirectConnection); - // Allow rate slider to go out of bounds so that sync lock rate - // adjustments are not capped. - m_pRateSlider = new ControlPotmeter( - ConfigKey(group, "rate"), -1.0, 1.0, true); - connect(m_pRateSlider, &ControlObject::valueChanged, - this, &RateControl::slotRateSliderChanged, + connect(m_pRateSlider.get(), + &ControlObject::valueChanged, + this, + &RateControl::slotRateSliderChanged, Qt::DirectConnection); - // Search rate. Rate used when searching in sound. This overrules the - // playback rate - m_pRateSearch = new ControlPotmeter(ConfigKey(group, "rateSearch"), -300., 300.); - - // Reverse button - m_pReverseButton = new ControlPushButton(ConfigKey(group, "reverse")); m_pReverseButton->set(0); - // Forward button - m_pForwardButton = new ControlPushButton(ConfigKey(group, "fwd")); - connect(m_pForwardButton, &ControlObject::valueChanged, - this, &RateControl::slotControlFastForward, + connect(m_pForwardButton.get(), + &ControlObject::valueChanged, + this, + &RateControl::slotControlFastForward, Qt::DirectConnection); m_pForwardButton->set(0); - // Back button - m_pBackButton = new ControlPushButton(ConfigKey(group, "back")); - connect(m_pBackButton, &ControlObject::valueChanged, - this, &RateControl::slotControlFastBack, + connect(m_pBackButton.get(), + &ControlObject::valueChanged, + this, + &RateControl::slotControlFastBack, Qt::DirectConnection); m_pBackButton->set(0); - m_pReverseRollButton = new ControlPushButton(ConfigKey(group, "reverseroll")); - connect(m_pReverseRollButton, &ControlObject::valueChanged, - this, &RateControl::slotReverseRollActivate, + connect(m_pReverseRollButton.get(), + &ControlObject::valueChanged, + this, + &RateControl::slotReverseRollActivate, Qt::DirectConnection); - m_pSlipEnabled = new ControlProxy(group, "slip_enabled", this); - - m_pVCEnabled = ControlObject::getControl(ConfigKey(getGroup(), "vinylcontrol_enabled")); - m_pVCScratching = ControlObject::getControl(ConfigKey(getGroup(), "vinylcontrol_scratching")); - m_pVCMode = ControlObject::getControl(ConfigKey(getGroup(), "vinylcontrol_mode")); - // Permanent rate-change buttons - m_pButtonRatePermDown = - new ControlPushButton(ConfigKey(group,"rate_perm_down")); - connect(m_pButtonRatePermDown, &ControlObject::valueChanged, - this, &RateControl::slotControlRatePermDown, - Qt::DirectConnection); m_pButtonRatePermDown->setKbdRepeatable(true); - - m_pButtonRatePermDownSmall = - new ControlPushButton(ConfigKey(group,"rate_perm_down_small")); - connect(m_pButtonRatePermDownSmall, &ControlObject::valueChanged, - this, &RateControl::slotControlRatePermDownSmall, + connect(m_pButtonRatePermDown.get(), + &ControlObject::valueChanged, + this, + &RateControl::slotControlRatePermDown, Qt::DirectConnection); - m_pButtonRatePermDownSmall->setKbdRepeatable(true); - m_pButtonRatePermUp = - new ControlPushButton(ConfigKey(group,"rate_perm_up")); - connect(m_pButtonRatePermUp, &ControlObject::valueChanged, - this, &RateControl::slotControlRatePermUp, + m_pButtonRatePermDownSmall->setKbdRepeatable(true); + connect(m_pButtonRatePermDownSmall.get(), + &ControlObject::valueChanged, + this, + &RateControl::slotControlRatePermDownSmall, Qt::DirectConnection); - m_pButtonRatePermUp->setKbdRepeatable(true); - m_pButtonRatePermUpSmall = - new ControlPushButton(ConfigKey(group,"rate_perm_up_small")); - connect(m_pButtonRatePermUpSmall, &ControlObject::valueChanged, - this, &RateControl::slotControlRatePermUpSmall, + m_pButtonRatePermUp->setKbdRepeatable(true); + connect(m_pButtonRatePermUp.get(), + &ControlObject::valueChanged, + this, + &RateControl::slotControlRatePermUp, Qt::DirectConnection); - m_pButtonRatePermUpSmall->setKbdRepeatable(true); - // Temporary rate-change buttons - m_pButtonRateTempDown = - new ControlPushButton(ConfigKey(group,"rate_temp_down")); - m_pButtonRateTempDownSmall = - new ControlPushButton(ConfigKey(group,"rate_temp_down_small")); - m_pButtonRateTempUp = - new ControlPushButton(ConfigKey(group,"rate_temp_up")); - m_pButtonRateTempUpSmall = - new ControlPushButton(ConfigKey(group,"rate_temp_up_small")); - - // We need the sample rate so we can guesstimate something close - // what latency is. - m_pSampleRate = ControlObject::getControl( - ConfigKey(QStringLiteral("[App]"), QStringLiteral("samplerate"))); - - // Wheel to control playback position/speed - m_pWheel = new ControlTTRotary(ConfigKey(group, "wheel")); - - // Scratch controller, this is an accumulator which is useful for - // controllers that return individual +1 or -1s, these get added up and - // cleared when we read - m_pScratch2 = new ControlObject(ConfigKey(group, "scratch2")); + m_pButtonRatePermUpSmall->setKbdRepeatable(true); + connect(m_pButtonRatePermUpSmall.get(), + &ControlObject::valueChanged, + this, + &RateControl::slotControlRatePermUpSmall, + Qt::DirectConnection); // Scratch enable toggle - m_pScratch2Enable = new ControlPushButton(ConfigKey(group, "scratch2_enable")); m_pScratch2Enable->set(0); - m_pScratch2Scratching = new ControlPushButton(ConfigKey(group, - "scratch2_indicates_scratching")); // Enable by default, because it was always scratching before introducing // this control. m_pScratch2Scratching->set(1.0); - - m_pJog = new ControlObject(ConfigKey(group, "jog")); - m_pJogFilter = new Rotary(); // FIXME: This should be dependent on sample rate/block size or something m_pJogFilter->setFilterLength(25); @@ -178,40 +208,6 @@ RateControl::RateControl(const QString& group, // // Set the Sensitivity // m_iRateRampSensitivity = // getConfig()->getValueString(ConfigKey("[Controls]","RateRampSensitivity")).toInt(); - - m_pSyncMode = new ControlProxy(group, "sync_mode", this); -} - -RateControl::~RateControl() { - delete m_pRateRatio; - delete m_pRateSlider; - delete m_pRateRange; - delete m_pRateDir; - delete m_pSyncMode; - - delete m_pRateSearch; - - delete m_pReverseButton; - delete m_pReverseRollButton; - delete m_pForwardButton; - delete m_pBackButton; - - delete m_pButtonRateTempDown; - delete m_pButtonRateTempDownSmall; - delete m_pButtonRateTempUp; - delete m_pButtonRateTempUpSmall; - delete m_pButtonRatePermDown; - delete m_pButtonRatePermDownSmall; - delete m_pButtonRatePermUp; - delete m_pButtonRatePermUpSmall; - - delete m_pWheel; - delete m_pScratch2; - delete m_pScratch2Scratching; - delete m_pScratch2Enable; - delete m_pJog; - delete m_pJogFilter; - delete m_pScratchController; } void RateControl::setBpmControl(BpmControl* bpmcontrol) { @@ -304,11 +300,11 @@ void RateControl::slotRateRatioChanged(double v) { void RateControl::slotReverseRollActivate(double v) { if (v > 0.0) { - m_pSlipEnabled->set(1); + m_slipEnabled.set(1); m_pReverseButton->set(1); } else { m_pReverseButton->set(0); - m_pSlipEnabled->set(0); + m_slipEnabled.set(0); } } @@ -391,7 +387,7 @@ double RateControl::getJogFactor() const { } SyncMode RateControl::getSyncMode() const { - return syncModeFromDouble(m_pSyncMode->get()); + return syncModeFromDouble(m_syncMode.get()); } double RateControl::calculateSpeed(double baserate, double speed, bool paused, @@ -555,7 +551,7 @@ void RateControl::processTempRate(const int bufferSamples) { } else if (m_eRateRampMode == RampMode::Linear) { if (!m_bTempStarted) { m_bTempStarted = true; - double latrate = bufferSamples / m_pSampleRate->get(); + double latrate = bufferSamples / m_pSampleRate.get(); m_dRateTempRampChange = latrate / (m_iRateRampSensitivity / 100.0); } @@ -628,3 +624,7 @@ void RateControl::notifyWrapAround(mixxx::audio::FramePos triggerPos, m_jumpPos = triggerPos; m_targetPos = targetPos; } + +void RateControl::notifySeek(mixxx::audio::FramePos position) { + m_pScratchController->notifySeek(position); +} diff --git a/src/engine/controls/ratecontrol.h b/src/engine/controls/ratecontrol.h index 951772f8406..25fc3957308 100644 --- a/src/engine/controls/ratecontrol.h +++ b/src/engine/controls/ratecontrol.h @@ -2,9 +2,10 @@ #include -#include "preferences/usersettings.h" +#include "control/pollingcontrolproxy.h" #include "engine/controls/enginecontrol.h" #include "engine/sync/syncable.h" +#include "preferences/usersettings.h" class BpmControl; class Rotary; @@ -22,7 +23,6 @@ class RateControl : public EngineControl { Q_OBJECT public: RateControl(const QString& group, UserSettingsPointer pConfig); - ~RateControl() override; // Enumerations which hold the state of the pitchbend buttons. // These enumerations can be used like a bitmask. @@ -76,6 +76,7 @@ class RateControl : public EngineControl { // PositionScratchController can correctly interpret the sample position delta. void notifyWrapAround(mixxx::audio::FramePos triggerPos, mixxx::audio::FramePos targetPos); + void notifySeek(mixxx::audio::FramePos position) override; public slots: void slotRateRangeChanged(double); @@ -106,51 +107,53 @@ public slots: // Get the 'Raw' Temp Rate double getTempRate(void); - // Values used when temp and perm rate buttons are pressed - static ControlValueAtomic m_dTemporaryRateChangeCoarse; - static ControlValueAtomic m_dTemporaryRateChangeFine; - static ControlValueAtomic m_dPermanentRateChangeCoarse; - static ControlValueAtomic m_dPermanentRateChangeFine; + // For Sync Lock + BpmControl* m_pBpmControl; + + PollingControlProxy m_pSampleRate; + std::unique_ptr m_pRateRatio; + std::unique_ptr m_pRateDir; + std::unique_ptr m_pRateRange; + std::unique_ptr m_pRateSlider; + std::unique_ptr m_pRateSearch; + + std::unique_ptr m_pButtonRateTempDown; + std::unique_ptr m_pButtonRateTempDownSmall; + std::unique_ptr m_pButtonRateTempUp; + std::unique_ptr m_pButtonRateTempUpSmall; + + std::unique_ptr m_pButtonRatePermDown; + std::unique_ptr m_pButtonRatePermDownSmall; + std::unique_ptr m_pButtonRatePermUp; + std::unique_ptr m_pButtonRatePermUpSmall; + + std::unique_ptr m_pReverseButton; + std::unique_ptr m_pReverseRollButton; + std::unique_ptr m_pForwardButton; + std::unique_ptr m_pBackButton; + + std::unique_ptr m_pWheel; + std::unique_ptr m_pScratch2; + std::unique_ptr m_pScratch2Enable; + std::unique_ptr m_pScratch2Scratching; + + std::unique_ptr m_pScratchController; + + std::unique_ptr m_pJog; + std::unique_ptr m_pJogFilter; - ControlPushButton* m_pButtonRateTempDown; - ControlPushButton* m_pButtonRateTempDownSmall; - ControlPushButton* m_pButtonRateTempUp; - ControlPushButton* m_pButtonRateTempUpSmall; - - ControlPushButton* m_pButtonRatePermDown; - ControlPushButton* m_pButtonRatePermDownSmall; - ControlPushButton* m_pButtonRatePermUp; - ControlPushButton* m_pButtonRatePermUpSmall; - - ControlObject* m_pRateRatio; - ControlObject* m_pRateDir; - ControlObject* m_pRateRange; - ControlPotmeter* m_pRateSlider; - ControlPotmeter* m_pRateSearch; - ControlPushButton* m_pReverseButton; - ControlPushButton* m_pReverseRollButton; - ControlObject* m_pBackButton; - ControlObject* m_pForwardButton; - - ControlTTRotary* m_pWheel; - ControlObject* m_pScratch2; - PositionScratchController* m_pScratchController; - - ControlPushButton* m_pScratch2Enable; - ControlObject* m_pJog; ControlObject* m_pVCEnabled; ControlObject* m_pVCScratching; ControlObject* m_pVCMode; - ControlObject* m_pScratch2Scratching; - Rotary* m_pJogFilter; - - ControlObject* m_pSampleRate; - // For Sync Lock - BpmControl* m_pBpmControl; + PollingControlProxy m_syncMode; + PollingControlProxy m_slipEnabled; - ControlProxy* m_pSyncMode; - ControlProxy* m_pSlipEnabled; + // Values used when temp and perm rate buttons are pressed + static ControlValueAtomic m_dTemporaryRateChangeCoarse; + static ControlValueAtomic m_dTemporaryRateChangeFine; + static ControlValueAtomic m_dPermanentRateChangeCoarse; + static ControlValueAtomic m_dPermanentRateChangeFine; int m_wrapAroundCount; mixxx::audio::FramePos m_jumpPos; diff --git a/src/engine/positionscratchcontroller.cpp b/src/engine/positionscratchcontroller.cpp index d4540b7fabc..077f788752d 100644 --- a/src/engine/positionscratchcontroller.cpp +++ b/src/engine/positionscratchcontroller.cpp @@ -1,19 +1,20 @@ #include "engine/positionscratchcontroller.h" #include "control/controlobject.h" +#include "control/controlproxy.h" #include "engine/bufferscalers/enginebufferscale.h" // for MIN_SEEK_SPEED #include "moc_positionscratchcontroller.cpp" #include "preferences/configobject.h" // for ConfigKey #include "util/math.h" +#include "util/time.h" class VelocityController { public: VelocityController() - : m_last_error(0.0), - m_p(0.0), - m_d(0.0) { + : m_last_error(0.0), + m_p(0.0), + m_d(0.0) { } - void setPD(double p, double d) { m_p = p; m_d = d; @@ -37,8 +38,8 @@ class VelocityController { class RateIIFilter { public: RateIIFilter() - : m_factor(1.0), - m_last_rate(0.0) { + : m_factor(1.0), + m_last_rate(0.0) { } void setFactor(double factor) { @@ -52,7 +53,7 @@ class RateIIFilter { double filter(double rate) { if (fabs(rate) - fabs(m_last_rate) > -0.1) { m_last_rate = m_last_rate * (1 - m_factor) + rate * m_factor; - } else { + } else { // do not filter strong decelerations to avoid overshooting m_last_rate = rate; } @@ -64,8 +65,31 @@ class RateIIFilter { double m_last_rate; }; +namespace { + +constexpr double kDefaultSampleInterval = 0.016; +// The max wait time when no new position has been set +constexpr double kMoveDelayMax = 0.04; +// The rate threshold above which disabling position scratching will enable +// an 'inertia' mode. +constexpr double kThrowThreshold = 2.5; +// Max velocity we would like to stop in a given time period. +constexpr double kMaxVelocity = 100; +// Seconds to stop a throw at the max velocity. +constexpr double kTimeToStop = 1.0; + +} // anonymous namespace + PositionScratchController::PositionScratchController(const QString& group) : m_group(group), + m_pScratchEnable(std::make_unique( + ConfigKey(group, QStringLiteral("scratch_position_enable")))), + m_pScratchPos(std::make_unique( + ConfigKey(group, QStringLiteral("scratch_position")))), + m_pMainSampleRate(std::make_unique( + ConfigKey(QStringLiteral("[App]"), QStringLiteral("samplerate")))), + m_pVelocityController(std::make_unique()), + m_pRateIIFilter(std::make_unique()), m_isScratching(false), m_inertiaEnabled(false), m_prevSamplePos(0), @@ -74,25 +98,46 @@ PositionScratchController::PositionScratchController(const QString& group) m_scratchStartPos(0), m_rate(0), m_moveDelay(0), - m_mouseSampleTime(0) { - m_pScratchEnable = new ControlObject(ConfigKey(group, "scratch_position_enable")); - m_pScratchPos = new ControlObject(ConfigKey(group, "scratch_position")); - m_pMainSampleRate = ControlObject::getControl( - ConfigKey(QStringLiteral("[App]"), QStringLiteral("samplerate"))); - m_pVelocityController = new VelocityController(); - m_pRateIIFilter = new RateIIFilter; + m_mouseSampleTime(0), + m_bufferSize(-1), // ? + m_dt(1), + m_callsPerDt(1), + m_p(1), + m_d(1), + m_f(0.4) { + m_pMainSampleRate->connectValueChanged(this, + &PositionScratchController::slotUpdateFilterParameters); } PositionScratchController::~PositionScratchController() { - delete m_pRateIIFilter; - delete m_pVelocityController; - delete m_pScratchPos; - delete m_pScratchEnable; +} + +void PositionScratchController::slotUpdateFilterParameters(double sampleRate) { + // The latency or time difference between process calls. + m_dt = static_cast(m_bufferSize) / sampleRate / 2; + + // Sample Mouse with fixed timing intervals to iron out significant jitters + // that are added on the way from mouse to engine thread + // Normally the Mouse is sampled every 8 ms so with this 16 ms window we + // have 0 ... 3 samples. The remaining jitter is ironed by the following IIR + // lowpass filter + m_callsPerDt = static_cast(ceil(kDefaultSampleInterval / m_dt)); + + // Tweak PD controller for different latencies + m_p = 0.3; + m_d = m_p / -2; + m_f = 0.4; + if (m_dt > kDefaultSampleInterval * 2) { + m_f = 1; + } + + m_pVelocityController->setPD(m_p, m_d); + m_pRateIIFilter->setFactor(m_f); } void PositionScratchController::process(double currentSamplePos, double releaseRate, - int iBufferSize, + int bufferSize, double baseSampleRate, int wrappedAround, mixxx::audio::FramePos trigger, @@ -105,33 +150,18 @@ void PositionScratchController::process(double currentSamplePos, return; } - // The latency or time difference between process calls. - const double dt = static_cast(iBufferSize) / m_pMainSampleRate->get() / 2; + if (bufferSize != m_bufferSize) { + m_bufferSize = bufferSize; + slotUpdateFilterParameters(m_pMainSampleRate->get()); + } - // Sample Mouse with fixed timing intervals to iron out significant jitters - // that are added on the way from mouse to engine thread - // Normally the Mouse is sampled every 8 ms so with this 16 ms window we - // have 0 ... 3 samples. The remaining jitter is ironed by the following IIR - // lowpass filter - const double m_mouseSampleInterval = 0.016; - const auto callsPerDt = static_cast(ceil(m_mouseSampleInterval / dt)); double scratchPosition = 0; - m_mouseSampleTime += dt; - if (m_mouseSampleTime >= m_mouseSampleInterval || !m_isScratching) { + m_mouseSampleTime += m_dt; + if (m_mouseSampleTime >= kDefaultSampleInterval || !m_isScratching) { scratchPosition = m_pScratchPos->get(); m_mouseSampleTime = 0; } - // Tweak PD controller for different latencies - double p = 0.3; - double d = p/-2; - double f = 0.4; - if (dt > m_mouseSampleInterval * 2) { - f = 1; - } - m_pVelocityController->setPD(p, d); - m_pRateIIFilter->setFactor(f); - if (m_isScratching) { if (m_inertiaEnabled) { // If we got here then we're not scratching and we're in inertia @@ -145,19 +175,14 @@ void PositionScratchController::process(double currentSamplePos, decayThreshold = MIN_SEEK_SPEED; } - // Max velocity we would like to stop in a given time period. - constexpr double kMaxVelocity = 100; - // Seconds to stop a throw at the max velocity. - constexpr double kTimeToStop = 1.0; - // We calculate the exponential decay constant based on the above // constants. Roughly we backsolve what the decay should be if we want to // stop a throw of max velocity kMaxVelocity in kTimeToStop seconds. Here is // the derivation: // kMaxVelocity * alpha ^ (# callbacks to stop in) = decayThreshold - // # callbacks = kTimeToStop / dt - // alpha = (decayThreshold / kMaxVelocity) ^ (dt / kTimeToStop) - const double kExponentialDecay = pow(decayThreshold / kMaxVelocity, dt / kTimeToStop); + // # callbacks = kTimeToStop / m_dt + // alpha = (decayThreshold / kMaxVelocity) ^ (m_dt / kTimeToStop) + const double kExponentialDecay = pow(decayThreshold / kMaxVelocity, m_dt / kTimeToStop); m_rate *= kExponentialDecay; @@ -167,7 +192,7 @@ void PositionScratchController::process(double currentSamplePos, m_inertiaEnabled = false; m_isScratching = false; } - // qDebug() << m_rate << kExponentialDecay << dt; + // qDebug() << m_rate << kExponentialDecay << m_dt; } else if (scratchEnable) { // If we're scratching, clear the inertia flag. This case should // have been caught by the 'enable' case below, but just to make @@ -197,33 +222,35 @@ void PositionScratchController::process(double currentSamplePos, sampleDelta = currentSamplePos - m_prevSamplePos; } - // Measure the total distance traveled since last frame and add - // it to the running total. This is required to scratch within loop - // boundaries. And normalize to one buffer - m_samplePosDeltaSum += (sampleDelta) / (iBufferSize * baseSampleRate); + // Measure the total distance traveled since last call, add it to the + // running total and normalize to one buffer. + // This is required to scratch within loop boundaries. + m_samplePosDeltaSum += (sampleDelta) / (bufferSize * baseSampleRate); - // Continue with the last rate if we do not have a new - // Mouse position + // If we may have a new position, calculate scratch parameters and + // eventually the new rate. + // Else, continue with the last rate. if (m_mouseSampleTime == 0) { // Set the scratch target to the current set position // and normalize to one buffer double scratchTargetDelta = (scratchPosition - m_scratchStartPos) / - (iBufferSize * baseSampleRate); + (bufferSize * baseSampleRate); bool calcRate = true; if (m_scratchTargetDelta == scratchTargetDelta) { - // we get here, if the next mouse position is delayed - // the mouse is stopped or moves slow. Since we don't know the case - // we assume delayed mouse updates for 40 ms - m_moveDelay += dt * callsPerDt; - if (m_moveDelay < 0.04) { + // We get here if the next mouse position is delayed, the mouse + // is stopped or moves very slowly. Since we don't know the case + // we assume delayed mouse updates for 40 ms. + // TODO Make threshold configurable for controller use? + m_moveDelay += m_dt * m_callsPerDt; + if (m_moveDelay < kMoveDelayMax) { // Assume a missing Mouse Update and continue with the // previously calculated rate. calcRate = false; } else { // Mouse has stopped - m_pVelocityController->setPD(p, 0); + m_pVelocityController->setPD(m_p, 0); if (scratchTargetDelta == 0) { // Mouse was not moved at all // Stop immediately by restarting the controller @@ -242,10 +269,10 @@ void PositionScratchController::process(double currentSamplePos, double ctrlError = m_pRateIIFilter->filter( scratchTargetDelta - m_samplePosDeltaSum); m_rate = m_pVelocityController->observation(ctrlError); - m_rate /= ceil(m_mouseSampleInterval / dt); + m_rate /= ceil(kDefaultSampleInterval / m_dt); // Note: The following SoundTouch changes the also rate by a ramp // This looks like average of the new and the old rate independent - // from dt. Ramping is disabled when direction changes or rate = 0; + // from m_dt. Ramping is disabled when direction changes or rate = 0; // (determined experimentally) if (fabs(m_rate) < MIN_SEEK_SPEED) { // we cannot get closer @@ -253,17 +280,12 @@ void PositionScratchController::process(double currentSamplePos, } } - // qDebug() << m_rate << scratchTargetDelta << m_samplePosDeltaSum << dt; + // qDebug() << m_rate << scratchTargetDelta << m_samplePosDeltaSum << m_dt; } } else { // We were previously in scratch mode and are no longer in scratch // mode. Disable everything, or optionally enable inertia mode if // the previous rate was high enough to count as a 'throw' - - // The rate threshold above which disabling position scratching will enable - // an 'inertia' mode. - constexpr double kThrowThreshold = 2.5; - if (fabs(m_rate) > kThrowThreshold) { m_inertiaEnabled = true; } else { @@ -279,8 +301,8 @@ void PositionScratchController::process(double currentSamplePos, m_moveDelay = 0; // Set up initial values, in a way that the system is settled m_rate = releaseRate; - m_samplePosDeltaSum = -(releaseRate / p) * - callsPerDt; // Set to the remaining error of a p controller + m_samplePosDeltaSum = -(releaseRate / m_p) * + m_callsPerDt; // Set to the remaining error of a p controller m_pVelocityController->reset(-m_samplePosDeltaSum); m_pRateIIFilter->reset(-m_samplePosDeltaSum); m_scratchStartPos = scratchPosition; @@ -289,15 +311,6 @@ void PositionScratchController::process(double currentSamplePos, m_prevSamplePos = currentSamplePos; } -bool PositionScratchController::isEnabled() { - // return true only if m_rate is valid. - return m_isScratching; -} - -double PositionScratchController::getRate() { - return m_rate; -} - void PositionScratchController::notifySeek(mixxx::audio::FramePos position) { DEBUG_ASSERT(position.isValid()); // scratching continues after seek due to calculating the relative distance traveled diff --git a/src/engine/positionscratchcontroller.h b/src/engine/positionscratchcontroller.h index 9a15e1f7f4a..0f2acc5ac2a 100644 --- a/src/engine/positionscratchcontroller.h +++ b/src/engine/positionscratchcontroller.h @@ -6,33 +6,44 @@ #include "audio/frame.h" class ControlObject; -class RateIIFilter; +class ControlProxy; class VelocityController; +class RateIIFilter; class PositionScratchController : public QObject { Q_OBJECT public: PositionScratchController(const QString& group); - virtual ~PositionScratchController(); + // required for the forward-declarations of uniquq_pointers of + // VelocityController and RateIIFilter + ~PositionScratchController() override; void process(double currentSample, double releaseRate, - int iBufferSize, + int bufferSize, double baseSampleRate, int wrappedAround, mixxx::audio::FramePos trigger, mixxx::audio::FramePos target); - bool isEnabled(); - double getRate(); + bool isEnabled() const { + // TODO return true only if m_rate is valid. + return m_isScratching; + } + double getRate() const { + return m_rate; + } void notifySeek(mixxx::audio::FramePos position); + private slots: + void slotUpdateFilterParameters(double sampleRate); + private: const QString m_group; - ControlObject* m_pScratchEnable; - ControlObject* m_pScratchPos; - ControlObject* m_pMainSampleRate; - VelocityController* m_pVelocityController; - RateIIFilter* m_pRateIIFilter; + std::unique_ptr m_pScratchEnable; + std::unique_ptr m_pScratchPos; + std::unique_ptr m_pMainSampleRate; + std::unique_ptr m_pVelocityController; + std::unique_ptr m_pRateIIFilter; bool m_isScratching; bool m_inertiaEnabled; double m_prevSamplePos; @@ -42,4 +53,13 @@ class PositionScratchController : public QObject { double m_rate; double m_moveDelay; double m_mouseSampleTime; + + int m_bufferSize; + + double m_dt; + double m_callsPerDt; + + double m_p; + double m_d; + double m_f; };