1 #include "engine/controls/ratecontrol.h"
2 
3 #include <QtDebug>
4 
5 #include "control/controlobject.h"
6 #include "control/controlpotmeter.h"
7 #include "control/controlproxy.h"
8 #include "control/controlpushbutton.h"
9 #include "control/controlttrotary.h"
10 #include "engine/controls/bpmcontrol.h"
11 #include "engine/controls/enginecontrol.h"
12 #include "engine/positionscratchcontroller.h"
13 #include "moc_ratecontrol.cpp"
14 #include "util/rotary.h"
15 #include "vinylcontrol/defs_vinylcontrol.h"
16 
17 namespace {
18 constexpr int kRateSensitivityMin = 100;
19 constexpr int kRateSensitivityMax = 2500;
20 } // namespace
21 
22 // Static default values for rate buttons (percents)
23 ControlValueAtomic<double> RateControl::m_dTemporaryRateChangeCoarse;
24 ControlValueAtomic<double> RateControl::m_dTemporaryRateChangeFine;
25 ControlValueAtomic<double> RateControl::m_dPermanentRateChangeCoarse;
26 ControlValueAtomic<double> RateControl::m_dPermanentRateChangeFine;
27 int RateControl::m_iRateRampSensitivity;
28 RateControl::RampMode RateControl::m_eRateRampMode;
29 
30 const double RateControl::kWheelMultiplier = 40.0;
31 const double RateControl::kPausedJogMultiplier = 18.0;
32 
RateControl(const QString & group,UserSettingsPointer pConfig)33 RateControl::RateControl(const QString& group,
34         UserSettingsPointer pConfig)
35         : EngineControl(group, pConfig),
36           m_pBpmControl(nullptr),
37           m_bTempStarted(false),
38           m_tempRateRatio(0.0),
39           m_dRateTempRampChange(0.0) {
40     m_pScratchController = new PositionScratchController(group);
41 
42     // This is the resulting rate ratio that can be used for display or calculations.
43     // The track original rate ratio is 1.
44     m_pRateRatio = new ControlObject(ConfigKey(group, "rate_ratio"),
45                   true, false, false, 1.0);
46     connect(m_pRateRatio, &ControlObject::valueChanged,
47             this, &RateControl::slotRateRatioChanged,
48             Qt::DirectConnection);
49 
50     m_pRateDir = new ControlObject(ConfigKey(group, "rate_dir"));
51     connect(m_pRateDir, &ControlObject::valueChanged,
52             this, &RateControl::slotRateRangeChanged,
53             Qt::DirectConnection);
54     m_pRateRange = new ControlPotmeter(
55             ConfigKey(group, "rateRange"), 0.01, 4.00);
56     connect(m_pRateRange, &ControlObject::valueChanged,
57             this, &RateControl::slotRateRangeChanged,
58             Qt::DirectConnection);
59 
60     // Allow rate slider to go out of bounds so that master sync rate
61     // adjustments are not capped.
62     m_pRateSlider = new ControlPotmeter(
63             ConfigKey(group, "rate"), -1.0, 1.0, true);
64     connect(m_pRateSlider, &ControlObject::valueChanged,
65             this, &RateControl::slotRateSliderChanged,
66             Qt::DirectConnection);
67 
68     // Search rate. Rate used when searching in sound. This overrules the
69     // playback rate
70     m_pRateSearch = new ControlPotmeter(ConfigKey(group, "rateSearch"), -300., 300.);
71 
72     // Reverse button
73     m_pReverseButton = new ControlPushButton(ConfigKey(group, "reverse"));
74     m_pReverseButton->set(0);
75 
76     // Forward button
77     m_pForwardButton = new ControlPushButton(ConfigKey(group, "fwd"));
78     connect(m_pForwardButton, &ControlObject::valueChanged,
79             this, &RateControl::slotControlFastForward,
80             Qt::DirectConnection);
81     m_pForwardButton->set(0);
82 
83     // Back button
84     m_pBackButton = new ControlPushButton(ConfigKey(group, "back"));
85     connect(m_pBackButton, &ControlObject::valueChanged,
86             this, &RateControl::slotControlFastBack,
87             Qt::DirectConnection);
88     m_pBackButton->set(0);
89 
90     m_pReverseRollButton = new ControlPushButton(ConfigKey(group, "reverseroll"));
91     connect(m_pReverseRollButton, &ControlObject::valueChanged,
92             this, &RateControl::slotReverseRollActivate,
93             Qt::DirectConnection);
94 
95     m_pSlipEnabled = new ControlProxy(group, "slip_enabled", this);
96 
97     m_pVCEnabled = ControlObject::getControl(ConfigKey(getGroup(), "vinylcontrol_enabled"));
98     m_pVCScratching = ControlObject::getControl(ConfigKey(getGroup(), "vinylcontrol_scratching"));
99     m_pVCMode = ControlObject::getControl(ConfigKey(getGroup(), "vinylcontrol_mode"));
100 
101     // Permanent rate-change buttons
102     m_pButtonRatePermDown =
103         new ControlPushButton(ConfigKey(group,"rate_perm_down"));
104     connect(m_pButtonRatePermDown, &ControlObject::valueChanged,
105             this, &RateControl::slotControlRatePermDown,
106             Qt::DirectConnection);
107 
108     m_pButtonRatePermDownSmall =
109         new ControlPushButton(ConfigKey(group,"rate_perm_down_small"));
110     connect(m_pButtonRatePermDownSmall, &ControlObject::valueChanged,
111             this, &RateControl::slotControlRatePermDownSmall,
112             Qt::DirectConnection);
113 
114     m_pButtonRatePermUp =
115         new ControlPushButton(ConfigKey(group,"rate_perm_up"));
116     connect(m_pButtonRatePermUp, &ControlObject::valueChanged,
117             this, &RateControl::slotControlRatePermUp,
118             Qt::DirectConnection);
119 
120     m_pButtonRatePermUpSmall =
121         new ControlPushButton(ConfigKey(group,"rate_perm_up_small"));
122     connect(m_pButtonRatePermUpSmall, &ControlObject::valueChanged,
123             this, &RateControl::slotControlRatePermUpSmall,
124             Qt::DirectConnection);
125 
126     // Temporary rate-change buttons
127     m_pButtonRateTempDown =
128         new ControlPushButton(ConfigKey(group,"rate_temp_down"));
129     m_pButtonRateTempDownSmall =
130         new ControlPushButton(ConfigKey(group,"rate_temp_down_small"));
131     m_pButtonRateTempUp =
132         new ControlPushButton(ConfigKey(group,"rate_temp_up"));
133     m_pButtonRateTempUpSmall =
134         new ControlPushButton(ConfigKey(group,"rate_temp_up_small"));
135 
136     // We need the sample rate so we can guesstimate something close
137     // what latency is.
138     m_pSampleRate = ControlObject::getControl(ConfigKey("[Master]","samplerate"));
139 
140     // Wheel to control playback position/speed
141     m_pWheel = new ControlTTRotary(ConfigKey(group, "wheel"));
142 
143     // Scratch controller, this is an accumulator which is useful for
144     // controllers that return individual +1 or -1s, these get added up and
145     // cleared when we read
146     m_pScratch2 = new ControlObject(ConfigKey(group, "scratch2"));
147 
148     // Scratch enable toggle
149     m_pScratch2Enable = new ControlPushButton(ConfigKey(group, "scratch2_enable"));
150     m_pScratch2Enable->set(0);
151 
152     m_pScratch2Scratching = new ControlPushButton(ConfigKey(group,
153                                                             "scratch2_indicates_scratching"));
154     // Enable by default, because it was always scratching before introducing
155     // this control.
156     m_pScratch2Scratching->set(1.0);
157 
158 
159     m_pJog = new ControlObject(ConfigKey(group, "jog"));
160     m_pJogFilter = new Rotary();
161     // FIXME: This should be dependent on sample rate/block size or something
162     m_pJogFilter->setFilterLength(25);
163 
164 //     // Update Internal Settings
165 //     // Set Pitchbend Mode
166 //     m_eRateRampMode = static_cast<RampMode>(
167 //         getConfig()->getValue(ConfigKey("[Controls]","RateRamp"),
168 //                               static_cast<int>(RampMode::Stepping)));
169 
170 //     // Set the Sensitivity
171 //     m_iRateRampSensitivity =
172 //             getConfig()->getValueString(ConfigKey("[Controls]","RateRampSensitivity")).toInt();
173 
174     m_pSyncMode = new ControlProxy(group, "sync_mode", this);
175 }
176 
~RateControl()177 RateControl::~RateControl() {
178     delete m_pRateRatio;
179     delete m_pRateSlider;
180     delete m_pRateRange;
181     delete m_pRateDir;
182     delete m_pSyncMode;
183 
184     delete m_pRateSearch;
185 
186     delete m_pReverseButton;
187     delete m_pReverseRollButton;
188     delete m_pForwardButton;
189     delete m_pBackButton;
190 
191     delete m_pButtonRateTempDown;
192     delete m_pButtonRateTempDownSmall;
193     delete m_pButtonRateTempUp;
194     delete m_pButtonRateTempUpSmall;
195     delete m_pButtonRatePermDown;
196     delete m_pButtonRatePermDownSmall;
197     delete m_pButtonRatePermUp;
198     delete m_pButtonRatePermUpSmall;
199 
200     delete m_pWheel;
201     delete m_pScratch2;
202     delete m_pScratch2Scratching;
203     delete m_pScratch2Enable;
204     delete m_pJog;
205     delete m_pJogFilter;
206     delete m_pScratchController;
207 }
208 
setBpmControl(BpmControl * bpmcontrol)209 void RateControl::setBpmControl(BpmControl* bpmcontrol) {
210     m_pBpmControl = bpmcontrol;
211 }
212 
213 //static
setRateRampMode(RampMode mode)214 void RateControl::setRateRampMode(RampMode mode) {
215     m_eRateRampMode = mode;
216 }
217 
218 //static
getRateRampMode()219 RateControl::RampMode RateControl::getRateRampMode() {
220     return m_eRateRampMode;
221 }
222 
223 //static
setRateRampSensitivity(int sense)224 void RateControl::setRateRampSensitivity(int sense) {
225     // Reverse the actual sensitivity value passed.
226     // That way the gui works in an intuitive manner.
227     sense = kRateSensitivityMax - sense + kRateSensitivityMin;
228     if (sense < kRateSensitivityMin) {
229         m_iRateRampSensitivity = kRateSensitivityMin;
230     } else if (sense > kRateSensitivityMax) {
231         m_iRateRampSensitivity = kRateSensitivityMax;
232     } else {
233         m_iRateRampSensitivity = sense;
234     }
235 }
236 
237 //static
setTemporaryRateChangeCoarseAmount(double v)238 void RateControl::setTemporaryRateChangeCoarseAmount(double v) {
239     m_dTemporaryRateChangeCoarse.setValue(v);
240 }
241 
242 //static
setTemporaryRateChangeFineAmount(double v)243 void RateControl::setTemporaryRateChangeFineAmount(double v) {
244     m_dTemporaryRateChangeFine.setValue(v);
245 }
246 
247 //static
setPermanentRateChangeCoarseAmount(double v)248 void RateControl::setPermanentRateChangeCoarseAmount(double v) {
249     m_dPermanentRateChangeCoarse.setValue(v);
250 }
251 
252 //static
setPermanentRateChangeFineAmount(double v)253 void RateControl::setPermanentRateChangeFineAmount(double v) {
254     m_dPermanentRateChangeFine.setValue(v);
255 }
256 
257 //static
getTemporaryRateChangeCoarseAmount()258 double RateControl::getTemporaryRateChangeCoarseAmount() {
259     return m_dTemporaryRateChangeCoarse.getValue();
260 }
261 
262 //static
getTemporaryRateChangeFineAmount()263 double RateControl::getTemporaryRateChangeFineAmount() {
264     return m_dTemporaryRateChangeFine.getValue();
265 }
266 
267 //static
getPermanentRateChangeCoarseAmount()268 double RateControl::getPermanentRateChangeCoarseAmount() {
269     return m_dPermanentRateChangeCoarse.getValue();
270 }
271 
272 //static
getPermanentRateChangeFineAmount()273 double RateControl::getPermanentRateChangeFineAmount() {
274     return m_dPermanentRateChangeFine.getValue();
275 }
276 
slotRateRangeChanged(double)277 void RateControl::slotRateRangeChanged(double) {
278     // update RateSlider with the new Range value butdo not change m_pRateRatio
279     slotRateRatioChanged(m_pRateRatio->get());
280 }
281 
slotRateSliderChanged(double v)282 void RateControl::slotRateSliderChanged(double v) {
283     double rateRatio = 1.0 + m_pRateDir->get() * m_pRateRange->get() * v;
284     m_pRateRatio->set(rateRatio);
285 }
286 
slotRateRatioChanged(double v)287 void RateControl::slotRateRatioChanged(double v) {
288     double rateRange = m_pRateRange->get();
289     if (rateRange > 0.0) {
290         double newRate = m_pRateDir->get() * (v - 1) / rateRange;
291         m_pRateSlider->set(newRate);
292     } else {
293         m_pRateSlider->set(0);
294     }
295 }
296 
slotReverseRollActivate(double v)297 void RateControl::slotReverseRollActivate(double v) {
298     if (v > 0.0) {
299         m_pSlipEnabled->set(1);
300         m_pReverseButton->set(1);
301     } else {
302         m_pReverseButton->set(0);
303         m_pSlipEnabled->set(0);
304     }
305 }
306 
slotControlFastForward(double v)307 void RateControl::slotControlFastForward(double v) {
308     //qDebug() << "slotControlFastForward(" << v << ")";
309     if (v > 0.0) {
310         m_pRateSearch->set(4.0);
311     } else {
312         m_pRateSearch->set(0.0);
313     }
314 }
315 
slotControlFastBack(double v)316 void RateControl::slotControlFastBack(double v) {
317     //qDebug() << "slotControlFastBack(" << v << ")";
318     if (v > 0.0) {
319         m_pRateSearch->set(-4.0);
320     } else {
321         m_pRateSearch->set(0.0);
322     }
323 }
324 
slotControlRatePermDown(double v)325 void RateControl::slotControlRatePermDown(double v) {
326     // Adjusts temp rate down if button pressed
327     if (v > 0.0) {
328         m_pRateSlider->set(m_pRateSlider->get() -
329                 m_pRateDir->get() * m_dPermanentRateChangeCoarse.getValue() / (100 * m_pRateRange->get()));
330         slotRateSliderChanged(m_pRateSlider->get());
331     }
332 }
333 
slotControlRatePermDownSmall(double v)334 void RateControl::slotControlRatePermDownSmall(double v) {
335     // Adjusts temp rate down if button pressed
336     if (v > 0.0) {
337         m_pRateSlider->set(m_pRateSlider->get() -
338                 m_pRateDir->get() * m_dPermanentRateChangeFine.getValue() / (100. * m_pRateRange->get()));
339         slotRateSliderChanged(m_pRateSlider->get());
340     }
341 }
342 
slotControlRatePermUp(double v)343 void RateControl::slotControlRatePermUp(double v) {
344     // Adjusts temp rate up if button pressed
345     if (v > 0.0) {
346         m_pRateSlider->set(m_pRateSlider->get() +
347                 m_pRateDir->get() * m_dPermanentRateChangeCoarse.getValue() / (100. * m_pRateRange->get()));
348         slotRateSliderChanged(m_pRateSlider->get());
349     }
350 }
351 
slotControlRatePermUpSmall(double v)352 void RateControl::slotControlRatePermUpSmall(double v) {
353     // Adjusts temp rate up if button pressed
354     if (v > 0.0) {
355         m_pRateSlider->set(m_pRateSlider->get() +
356                            m_pRateDir->get() * m_dPermanentRateChangeFine.getValue() / (100. * m_pRateRange->get()));
357         slotRateSliderChanged(m_pRateSlider->get());
358     }
359 }
360 
getWheelFactor() const361 double RateControl::getWheelFactor() const {
362     return m_pWheel->get();
363 }
364 
getJogFactor() const365 double RateControl::getJogFactor() const {
366     // FIXME: Sensitivity should be configurable separately?
367     const double jogSensitivity = 0.1;  // Nudges during playback
368     double jogValue = m_pJog->get();
369 
370     // Since m_pJog is an accumulator, reset it since we've used its value.
371     if (jogValue != 0.) {
372         m_pJog->set(0.);
373     }
374 
375     double jogValueFiltered = m_pJogFilter->filter(jogValue);
376     double jogFactor = jogValueFiltered * jogSensitivity;
377 
378     if (util_isnan(jogValue) || util_isnan(jogFactor)) {
379         jogFactor = 0.0;
380     }
381 
382     return jogFactor;
383 }
384 
getSyncMode() const385 SyncMode RateControl::getSyncMode() const {
386     return syncModeFromDouble(m_pSyncMode->get());
387 }
388 
calculateSpeed(double baserate,double speed,bool paused,int iSamplesPerBuffer,bool * pReportScratching,bool * pReportReverse)389 double RateControl::calculateSpeed(double baserate, double speed, bool paused,
390                                    int iSamplesPerBuffer,
391                                    bool* pReportScratching,
392                                    bool* pReportReverse) {
393     *pReportScratching = false;
394     *pReportReverse = false;
395 
396     processTempRate(iSamplesPerBuffer);
397 
398     double rate;
399     const double searching = m_pRateSearch->get();
400     if (searching != 0) {
401         // If searching is in progress, it overrides everything else
402         rate = searching;
403     } else {
404         double wheelFactor = getWheelFactor();
405         double jogFactor = getJogFactor();
406         bool bVinylControlEnabled = m_pVCEnabled && m_pVCEnabled->toBool();
407         bool useScratch2Value = m_pScratch2Enable->toBool();
408 
409         // By default scratch2_enable is enough to determine if the user is
410         // scratching or not. Moving platter controllers have to disable
411         // "scratch2_indicates_scratching" if they are not scratching,
412         // to allow things like key-lock.
413         if (useScratch2Value && m_pScratch2Scratching->toBool()) {
414             *pReportScratching = true;
415         }
416 
417         if (bVinylControlEnabled) {
418             if (m_pVCScratching->toBool()) {
419                 *pReportScratching = true;
420             }
421             rate = speed;
422         } else {
423             double scratchFactor = m_pScratch2->get();
424             // Don't trust values from m_pScratch2
425             if (util_isnan(scratchFactor)) {
426                 scratchFactor = 0.0;
427             }
428             if (paused) {
429                 // Stopped. Wheel, jog and scratch controller all scrub through audio.
430                 if (useScratch2Value) {
431                     rate = scratchFactor + jogFactor + wheelFactor * kWheelMultiplier;
432                 } else {
433                     rate = jogFactor * kPausedJogMultiplier + wheelFactor;
434                 }
435             } else {
436                 // The buffer is playing, so calculate the buffer rate.
437 
438                 // There are four rate effects we apply: wheel, scratch, jog and temp.
439                 // Wheel: a linear additive effect (no spring-back)
440                 // Scratch: a rate multiplier
441                 // Jog: a linear additive effect whose value is filtered (springs back)
442                 // Temp: pitch bend
443 
444                 // New scratch behavior - overrides playback speed (and old behavior)
445                 if (useScratch2Value) {
446                     rate = scratchFactor;
447                 } else {
448                     // add temp rate, but don't go backwards
449                     rate = math_max(speed + getTempRate(), 0.0);
450                     rate += wheelFactor;
451                 }
452                 rate += jogFactor;
453             }
454         }
455 
456         double currentSample = getSampleOfTrack().current;
457         m_pScratchController->process(currentSample, rate, iSamplesPerBuffer, baserate);
458 
459         // If waveform scratch is enabled, override all other controls
460         if (m_pScratchController->isEnabled()) {
461             rate = m_pScratchController->getRate();
462             *pReportScratching = true;
463         } else {
464             // If master sync is on, respond to it -- but vinyl and scratch mode always override.
465             if (getSyncMode() == SYNC_FOLLOWER && !paused &&
466                     !bVinylControlEnabled && !useScratch2Value) {
467                 if (m_pBpmControl == nullptr) {
468                     qDebug() << "ERROR: calculateRate m_pBpmControl is null during master sync";
469                     return 1.0;
470                 }
471 
472                 double userTweak = 0.0;
473                 if (!*pReportScratching) {
474                     // Only report user tweak if the user is not scratching.
475                     userTweak = getTempRate() + wheelFactor + jogFactor;
476                 }
477                 rate = m_pBpmControl->calcSyncedRate(userTweak);
478             }
479             // If we are reversing (and not scratching,) flip the rate.  This is ok even when syncing.
480             // Reverse with vinyl is only ok if absolute mode isn't on.
481             int vcmode = m_pVCMode ? static_cast<int>(m_pVCMode->get()) : MIXXX_VCMODE_ABSOLUTE;
482             // TODO(owen): Instead of just ignoring reverse mode, should we
483             // disable absolute mode instead?
484             if (m_pReverseButton->toBool() && !m_pScratch2Enable->toBool() &&
485                     (!bVinylControlEnabled ||
486                             vcmode != MIXXX_VCMODE_ABSOLUTE)) {
487                 rate = -rate;
488                 *pReportReverse = true;
489             }
490         }
491     }
492     return rate;
493 }
494 
processTempRate(const int bufferSamples)495 void RateControl::processTempRate(const int bufferSamples) {
496     // Code to handle temporary rate change buttons.
497     // We support two behaviors, the standard ramped pitch bending
498     // and pitch shift stepping, which is the old behavior.
499 
500     RampDirection rampDirection = RampDirection::None;
501     if (m_pButtonRateTempUp->toBool()) {
502         rampDirection = RampDirection::Up;
503     } else if (m_pButtonRateTempDown->toBool()) {
504         rampDirection = RampDirection::Down;
505     } else if (m_pButtonRateTempUpSmall->toBool()) {
506         rampDirection = RampDirection::UpSmall;
507     } else if (m_pButtonRateTempDownSmall->toBool()) {
508         rampDirection = RampDirection::DownSmall;
509     }
510 
511     if (rampDirection != RampDirection::None) {
512         if (m_eRateRampMode == RampMode::Stepping) {
513             if (!m_bTempStarted) {
514                 m_bTempStarted = true;
515                 // old temporary pitch shift behavior
516                 double change = m_dTemporaryRateChangeCoarse.getValue() / 100.0;
517                 double csmall = m_dTemporaryRateChangeFine.getValue() / 100.0;
518 
519                 switch (rampDirection) {
520                 case RampDirection::Up:
521                     setRateTemp(change);
522                     break;
523                 case RampDirection::Down:
524                     setRateTemp(-change);
525                     break;
526                 case RampDirection::UpSmall:
527                     setRateTemp(csmall);
528                     break;
529                 case RampDirection::DownSmall:
530                     setRateTemp(-csmall);
531                     break;
532                 case RampDirection::None:
533                 default:
534                     DEBUG_ASSERT(false);
535                 }
536             }
537         } else if (m_eRateRampMode == RampMode::Linear) {
538             if (!m_bTempStarted) {
539                 m_bTempStarted = true;
540                 double latrate = bufferSamples / m_pSampleRate->get();
541                 m_dRateTempRampChange = latrate / (m_iRateRampSensitivity / 100.0);
542             }
543 
544             switch (rampDirection) {
545             case RampDirection::Up:
546             case RampDirection::UpSmall:
547                 addRateTemp(m_dRateTempRampChange * m_pRateRange->get());
548                 break;
549             case RampDirection::Down:
550             case RampDirection::DownSmall:
551                 subRateTemp(m_dRateTempRampChange * m_pRateRange->get());
552                 break;
553             case RampDirection::None:
554             default:
555                 DEBUG_ASSERT(false);
556             }
557         }
558     } else if (m_bTempStarted) {
559         m_bTempStarted = false;
560         resetRateTemp();
561     }
562 }
563 
getTempRate()564 double RateControl::getTempRate() {
565     // qDebug() << m_tempRateRatio;
566     return m_tempRateRatio;
567 }
568 
setRateTemp(double v)569 void RateControl::setRateTemp(double v) {
570     m_tempRateRatio = v;
571     if (m_tempRateRatio < -1.0) {
572         m_tempRateRatio = -1.0;
573     } else if (m_tempRateRatio > 1.0) {
574         m_tempRateRatio = 1.0;
575     } else if (util_isnan(m_tempRateRatio)) {
576         m_tempRateRatio = 0;
577     }
578 }
579 
addRateTemp(double v)580 void RateControl::addRateTemp(double v)
581 {
582     setRateTemp(m_tempRateRatio + v);
583 }
584 
subRateTemp(double v)585 void RateControl::subRateTemp(double v)
586 {
587     setRateTemp(m_tempRateRatio - v);
588 }
589 
resetRateTemp(void)590 void RateControl::resetRateTemp(void)
591 {
592     setRateTemp(0.0);
593 }
594 
notifySeek(double playPos)595 void RateControl::notifySeek(double playPos) {
596     m_pScratchController->notifySeek(playPos);
597     EngineControl::notifySeek(playPos);
598 }
599 
isReverseButtonPressed()600 bool RateControl::isReverseButtonPressed() {
601     if (m_pReverseButton) {
602         return m_pReverseButton->toBool();
603     }
604     return false;
605 }
606