diff --git a/build/depends.py b/build/depends.py index 178e10f8c9b4..73982b505bf7 100644 --- a/build/depends.py +++ b/build/depends.py @@ -1062,6 +1062,7 @@ def sources(self, build): "track/beatgrid.cpp", "track/beatmap.cpp", "track/beatutils.cpp", + "track/beats.cpp", "track/bpm.cpp", "track/keyfactory.cpp", "track/keys.cpp", diff --git a/src/engine/bpmcontrol.cpp b/src/engine/bpmcontrol.cpp index a8666c8b16fd..ae2167e81ad3 100644 --- a/src/engine/bpmcontrol.cpp +++ b/src/engine/bpmcontrol.cpp @@ -792,17 +792,6 @@ void BpmControl::setCurrentSample(const double dCurrentSample, const double dTot EngineControl::setCurrentSample(dCurrentSample, dTotalSamples); } -double BpmControl::process(const double dRate, - const double dCurrentSample, - const double dTotalSamples, - const int iBufferSize) { - Q_UNUSED(dRate); - Q_UNUSED(dCurrentSample); - Q_UNUSED(dTotalSamples); - Q_UNUSED(iBufferSize); - return kNoTrigger; -} - double BpmControl::updateLocalBpm() { double prev_local_bpm = m_pLocalBpm->get(); double local_bpm = 0; diff --git a/src/engine/bpmcontrol.h b/src/engine/bpmcontrol.h index 38a8541555c8..7c1ed553dbdf 100644 --- a/src/engine/bpmcontrol.h +++ b/src/engine/bpmcontrol.h @@ -20,7 +20,7 @@ class BpmControl : public EngineControl { public: BpmControl(QString group, UserSettingsPointer pConfig); - virtual ~BpmControl(); + ~BpmControl() override; double getBpm() const; double getLocalBpm() const { return m_pLocalBpm ? m_pLocalBpm->get() : 0.0; } @@ -38,11 +38,7 @@ class BpmControl : public EngineControl { double getBeatDistance(double dThisPosition) const; double getPreviousSample() const { return m_dPreviousSample; } - void setCurrentSample(const double dCurrentSample, const double dTotalSamples); - double process(const double dRate, - const double dCurrentSample, - const double dTotalSamples, - const int iBufferSize); + void setCurrentSample(const double dCurrentSample, const double dTotalSamples) override; void setTargetBeatDistance(double beatDistance); void setInstantaneousBpm(double instantaneousBpm); void resetSyncAdjustment(); diff --git a/src/engine/clockcontrol.cpp b/src/engine/clockcontrol.cpp index b6f03f5bfe6c..841b4662159a 100644 --- a/src/engine/clockcontrol.cpp +++ b/src/engine/clockcontrol.cpp @@ -46,7 +46,7 @@ void ClockControl::slotBeatsUpdated() { } } -double ClockControl::process(const double dRate, +void ClockControl::process(const double dRate, const double currentSample, const double totalSamples, const int iBuffersize) { @@ -66,6 +66,4 @@ double ClockControl::process(const double dRate, double distanceToClosestBeat = fabs(currentSample - closestBeat); m_pCOBeatActive->set(distanceToClosestBeat < blinkIntervalSamples / 2.0); } - - return kNoTrigger; } diff --git a/src/engine/clockcontrol.h b/src/engine/clockcontrol.h index 56ec15561635..3aa83a87d663 100644 --- a/src/engine/clockcontrol.h +++ b/src/engine/clockcontrol.h @@ -16,10 +16,10 @@ class ClockControl: public EngineControl { ClockControl(QString group, UserSettingsPointer pConfig); - virtual ~ClockControl(); + ~ClockControl() override; - double process(const double dRate, const double currentSample, - const double totalSamples, const int iBufferSize); + void process(const double dRate, const double currentSample, + const double totalSamples, const int iBufferSize) override; public slots: void trackLoaded(TrackPointer pNewTrack, TrackPointer pOldTrack) override; diff --git a/src/engine/cuecontrol.h b/src/engine/cuecontrol.h index b6356681fc87..e6d61ffe7d1a 100644 --- a/src/engine/cuecontrol.h +++ b/src/engine/cuecontrol.h @@ -86,9 +86,9 @@ class CueControl : public EngineControl { public: CueControl(QString group, UserSettingsPointer pConfig); - virtual ~CueControl(); + ~CueControl() override; - virtual void hintReader(HintVector* pHintList); + virtual void hintReader(HintVector* pHintList) override; bool updateIndicatorsAndModifyPlay(bool newPlay, bool playPossible); void updateIndicators(); bool isTrackAtCue(); diff --git a/src/engine/enginecontrol.cpp b/src/engine/enginecontrol.cpp index 045a987abd0e..9e8f1e9f28c1 100644 --- a/src/engine/enginecontrol.cpp +++ b/src/engine/enginecontrol.cpp @@ -19,7 +19,7 @@ EngineControl::EngineControl(QString group, EngineControl::~EngineControl() { } -double EngineControl::process(const double dRate, +void EngineControl::process(const double dRate, const double dCurrentSample, const double dTotalSamples, const int iBufferSize) { @@ -27,29 +27,6 @@ double EngineControl::process(const double dRate, Q_UNUSED(dCurrentSample); Q_UNUSED(dTotalSamples); Q_UNUSED(iBufferSize); - return kNoTrigger; -} - -double EngineControl::nextTrigger(const double dRate, - const double currentSample, - const double totalSamples, - const int iBufferSize) { - Q_UNUSED(dRate); - Q_UNUSED(currentSample); - Q_UNUSED(totalSamples); - Q_UNUSED(iBufferSize); - return kNoTrigger; -} - -double EngineControl::getTrigger(const double dRate, - const double currentSample, - const double totalSamples, - const int iBufferSize) { - Q_UNUSED(dRate); - Q_UNUSED(currentSample); - Q_UNUSED(totalSamples); - Q_UNUSED(iBufferSize); - return kNoTrigger; } void EngineControl::trackLoaded(TrackPointer pNewTrack, TrackPointer pOldTrack) { diff --git a/src/engine/enginecontrol.h b/src/engine/enginecontrol.h index a444be13945c..550f16094bc7 100644 --- a/src/engine/enginecontrol.h +++ b/src/engine/enginecontrol.h @@ -36,29 +36,17 @@ class EngineControl : public QObject { public: EngineControl(QString group, UserSettingsPointer pConfig); - virtual ~EngineControl(); + ~EngineControl() override; // Called by EngineBuffer::process every latency period. See the above // comments for information about guarantees that hold during this call. An // EngineControl can perform any upkeep operations that are necessary during - // this call. If the EngineControl would like to request the playback - // position to be altered, it should return the sample to seek to from this - // method. Otherwise it should return kNoTrigger. - virtual double process(const double dRate, + // this call. + virtual void process(const double dRate, const double dCurrentSample, const double dTotalSamples, const int iBufferSize); - virtual double nextTrigger(const double dRate, - const double dCurrentSample, - const double dTotalSamples, - const int iBufferSize); - - virtual double getTrigger(const double dRate, - const double dCurrentSample, - const double dTotalSamples, - const int iBufferSize); - // hintReader allows the EngineControl to provide hints to the reader to // indicate that the given portion of a song is a potential imminent seek // target. diff --git a/src/engine/keycontrol.h b/src/engine/keycontrol.h index b3fc6dea0a85..b97f3567c240 100644 --- a/src/engine/keycontrol.h +++ b/src/engine/keycontrol.h @@ -25,7 +25,7 @@ class KeyControl : public EngineControl { }; KeyControl(QString group, UserSettingsPointer pConfig); - virtual ~KeyControl(); + ~KeyControl() override; // Returns a struct, with the results of the last pitch and tempo calculations KeyControl::PitchTempoRatio getPitchTempoRatio(); diff --git a/src/engine/loopingcontrol.cpp b/src/engine/loopingcontrol.cpp index 6c162fcbaf47..af981f6fe3e1 100644 --- a/src/engine/loopingcontrol.cpp +++ b/src/engine/loopingcontrol.cpp @@ -39,10 +39,17 @@ QList LoopingControl::getBeatSizes() { LoopingControl::LoopingControl(QString group, UserSettingsPointer pConfig) - : EngineControl(group, pConfig) { - LoopSamples loopSamples = { kNoTrigger, kNoTrigger }; - m_loopSamples.setValue(loopSamples); - m_iCurrentSample = 0; + : EngineControl(group, pConfig), + m_bLoopingEnabled(false), + m_bLoopRollActive(false), + m_bAdjustingLoopIn(false), + m_bAdjustingLoopOut(false), + m_bAdjustingLoopInOld(false), + m_bAdjustingLoopOutOld(false), + m_bLoopOutPressedWhileLoopDisabled(false) { + m_oldLoopSamples = { kNoTrigger, kNoTrigger, false }; + m_loopSamples.setValue(m_oldLoopSamples); + m_currentSample.setValue(0.0); m_pActiveBeatLoop = NULL; //Create loop-in, loop-out, loop-exit, and reloop/exit ControlObjects @@ -241,23 +248,18 @@ void LoopingControl::slotLoopScale(double scaleFactor) { if (loopSamples.start == kNoTrigger || loopSamples.end == kNoTrigger) { return; } - int loop_length = loopSamples.end - loopSamples.start; - int old_loop_end = loopSamples.end; - int samples = m_pTrackSamples->get(); + double loop_length = loopSamples.end - loopSamples.start; + int trackSamples = m_pTrackSamples->get(); loop_length *= scaleFactor; // Abandon loops that are too short of extend beyond the end of the file. if (loop_length < MINIMUM_AUDIBLE_LOOP_SIZE || - loopSamples.start + loop_length > samples) { + loopSamples.start + loop_length > trackSamples) { return; } loopSamples.end = loopSamples.start + loop_length; - if (!even(loopSamples.end)) { - loopSamples.end--; - } - // TODO(XXX) we could be smarter about taking the active beatloop, scaling // it by the desired amount and trying to find another beatloop that matches // it, but for now we just clear the active beat loop if somebody scales. @@ -265,27 +267,23 @@ void LoopingControl::slotLoopScale(double scaleFactor) { // Don't allow 0 samples loop, so one can still manipulate it if (loopSamples.end == loopSamples.start) { - if ((loopSamples.end + 2) >= samples) + if ((loopSamples.end + 2) >= trackSamples) loopSamples.start -= 2; else loopSamples.end += 2; } // Do not allow loops to go past the end of the song - else if (loopSamples.end > samples) { - loopSamples.end = samples; + else if (loopSamples.end > trackSamples) { + loopSamples.end = trackSamples; } + // Reseek if the loop shrank out from under the playposition. + loopSamples.seek = (m_bLoopingEnabled && scaleFactor < 1.0); + m_loopSamples.setValue(loopSamples); // Update CO for loop end marker m_pCOLoopEndPosition->set(loopSamples.end); - - // Reseek if the loop shrank out from under the playposition. - if (m_bLoopingEnabled && scaleFactor < 1.0) { - seekInsideAdjustedLoop( - loopSamples.start, old_loop_end, - loopSamples.start, loopSamples.end); - } } void LoopingControl::slotLoopHalve(double pressed) { @@ -304,34 +302,41 @@ void LoopingControl::slotLoopDouble(double pressed) { slotBeatLoop(m_pCOBeatLoopSize->get() * 2.0, true, false); } -double LoopingControl::process(const double dRate, +void LoopingControl::process(const double dRate, const double currentSample, const double totalSamples, const int iBufferSize) { Q_UNUSED(totalSamples); Q_UNUSED(iBufferSize); + Q_UNUSED(dRate); - int currentSampleEven = static_cast(currentSample); - if (!even(currentSampleEven)) { - currentSampleEven--; - } - m_iCurrentSample = currentSampleEven; - - bool reverse = dRate < 0; + double oldCurrentSample = m_currentSample.getValue(); - double retval = kNoTrigger; - LoopSamples loopSamples = m_loopSamples.getValue(); - if (m_bLoopingEnabled && + if (oldCurrentSample != currentSample) { + m_currentSample.setValue(currentSample); + } else { + // no transport, so we have to do scheduled seeks here + LoopSamples loopSamples = m_loopSamples.getValue(); + if (m_bLoopingEnabled && + !m_bAdjustingLoopIn && !m_bAdjustingLoopOut && loopSamples.start != kNoTrigger && loopSamples.end != kNoTrigger) { - bool outsideLoop = currentSample >= loopSamples.end || - currentSample <= loopSamples.start; - if (outsideLoop) { - if (!m_bReloopCatchUpcomingLoop && !m_bAdjustingLoopIn && !m_bAdjustingLoopOut) { - retval = reverse ? loopSamples.end : loopSamples.start; + + if (loopSamples.start != m_oldLoopSamples.start || + loopSamples.end != m_oldLoopSamples.end) { + // bool seek is only valid after the loop has changed + if (loopSamples.seek) { + // here the loop has changed and the play position + // should be moved with it + double target = seekInsideAdjustedLoop(currentSample, + m_oldLoopSamples.start, loopSamples.start, loopSamples.end); + if (target != kNoTrigger) { + // jump immediately + seekAbs(target); + } + } + m_oldLoopSamples = loopSamples; } - } else { - m_bReloopCatchUpcomingLoop = false; } } @@ -340,47 +345,62 @@ double LoopingControl::process(const double dRate, } else if (m_bAdjustingLoopOut) { setLoopOutToCurrentPosition(); } - - return retval; } -double LoopingControl::nextTrigger(const double dRate, - const double currentSample, - const double totalSamples, - const int iBufferSize) { - Q_UNUSED(currentSample); - Q_UNUSED(totalSamples); - Q_UNUSED(iBufferSize); - bool bReverse = dRate < 0; +double LoopingControl::nextTrigger(bool reverse, + const double currentSample, + double *pTarget) { + *pTarget = kNoTrigger; + LoopSamples loopSamples = m_loopSamples.getValue(); - if (m_bLoopingEnabled && !m_bReloopCatchUpcomingLoop && - !m_bAdjustingLoopIn && !m_bAdjustingLoopOut) { - if (bReverse) { - return loopSamples.start; - } else { - return loopSamples.end; + if (m_bAdjustingLoopInOld != m_bAdjustingLoopIn) { + m_bAdjustingLoopInOld = m_bAdjustingLoopIn; + if (reverse && !m_bAdjustingLoopIn) { + m_oldLoopSamples = loopSamples; + *pTarget = loopSamples.end; + return currentSample; } } - return kNoTrigger; -} -double LoopingControl::getTrigger(const double dRate, - const double currentSample, - const double totalSamples, - const int iBufferSize) { - Q_UNUSED(currentSample); - Q_UNUSED(totalSamples); - Q_UNUSED(iBufferSize); - bool bReverse = dRate < 0; - LoopSamples loopSamples = m_loopSamples.getValue(); + if (m_bAdjustingLoopOutOld != m_bAdjustingLoopOut) { + m_bAdjustingLoopOutOld = m_bAdjustingLoopOut; + if (!reverse && !m_bAdjustingLoopOut) { + m_oldLoopSamples = loopSamples; + *pTarget = loopSamples.start; + return currentSample; + } + } - if (m_bLoopingEnabled && !m_bReloopCatchUpcomingLoop && - !m_bAdjustingLoopIn && !m_bAdjustingLoopOut) { - if (bReverse) { - return loopSamples.end; - } else { + if (m_bLoopingEnabled && + !m_bAdjustingLoopIn && !m_bAdjustingLoopOut && + loopSamples.start != kNoTrigger && + loopSamples.end != kNoTrigger) { + + if (loopSamples.start != m_oldLoopSamples.start || + loopSamples.end != m_oldLoopSamples.end) { + // bool seek is only valid after the loop has changed + if (loopSamples.seek) { + // here the loop has changed and the play position + // should be moved with it + *pTarget = seekInsideAdjustedLoop(currentSample, + m_oldLoopSamples.start, loopSamples.start, loopSamples.end); + } + m_oldLoopSamples = loopSamples; + if (*pTarget != kNoTrigger) { + // jump immediately + qDebug() << currentSample << + m_oldLoopSamples.start << loopSamples.start << loopSamples.end; + return currentSample; + } + } + + if (reverse) { + *pTarget = loopSamples.end; return loopSamples.start; + } else { + *pTarget = loopSamples.start; + return loopSamples.end; } } return kNoTrigger; @@ -423,7 +443,7 @@ void LoopingControl::setLoopInToCurrentPosition() { // set loop-in position LoopSamples loopSamples = m_loopSamples.getValue(); double quantizedBeat = -1; - int pos = m_iCurrentSample; + double pos = m_currentSample.getValue(); if (m_pQuantizeEnabled->toBool() && m_pBeats != nullptr) { if (m_bAdjustingLoopIn) { double closestBeat = m_pClosestBeat->get(); @@ -436,14 +456,10 @@ void LoopingControl::setLoopInToCurrentPosition() { quantizedBeat = m_pClosestBeat->get(); } if (quantizedBeat != -1) { - pos = static_cast(floor(quantizedBeat)); + pos = quantizedBeat; } } - if (pos != -1 && !even(pos)) { - pos--; - } - // Reset the loop out position if it is before the loop in so that loops // cannot be inverted. if (loopSamples.end != kNoTrigger && @@ -458,7 +474,7 @@ void LoopingControl::setLoopInToCurrentPosition() { if (loopSamples.end != kNoTrigger && (loopSamples.end - pos) < MINIMUM_AUDIBLE_LOOP_SIZE) { if (quantizedBeat != -1 && m_pBeats) { - pos = static_cast(floor(m_pBeats->findNthBeat(quantizedBeat, -2))); + pos = m_pBeats->findNthBeat(quantizedBeat, -2); if (pos == -1 || (loopSamples.end - pos) < MINIMUM_AUDIBLE_LOOP_SIZE) { pos = loopSamples.end - MINIMUM_AUDIBLE_LOOP_SIZE; } @@ -468,8 +484,18 @@ void LoopingControl::setLoopInToCurrentPosition() { } loopSamples.start = pos; + m_pCOLoopStartPosition->set(loopSamples.start); + // start looping + if (loopSamples.start != kNoTrigger && + loopSamples.end != kNoTrigger) { + setLoopingEnabled(true); + loopSamples.seek = true; + } else { + loopSamples.seek = false; + } + if (m_pQuantizeEnabled->toBool() && loopSamples.start < loopSamples.end && m_pBeats != nullptr) { @@ -518,7 +544,7 @@ void LoopingControl::slotLoopInGoto(double pressed) { void LoopingControl::setLoopOutToCurrentPosition() { LoopSamples loopSamples = m_loopSamples.getValue(); double quantizedBeat = -1; - int pos = m_iCurrentSample; + int pos = m_currentSample.getValue(); if (m_pQuantizeEnabled->toBool() && m_pBeats != nullptr) { if (m_bAdjustingLoopOut) { double closestBeat = m_pClosestBeat->get(); @@ -531,14 +557,10 @@ void LoopingControl::setLoopOutToCurrentPosition() { quantizedBeat = m_pClosestBeat->get(); } if (quantizedBeat != -1) { - pos = static_cast(floor(quantizedBeat)); + pos = quantizedBeat; } } - if (pos != -1 && !even(pos)) { - pos++; // Increment to avoid shortening too-short loops - } - // If the user is trying to set a loop-out before the loop in or without // having a loop-in, then ignore it. if (loopSamples.start == kNoTrigger || pos < loopSamples.start) { @@ -561,13 +583,16 @@ void LoopingControl::setLoopOutToCurrentPosition() { // set loop out position loopSamples.end = pos; + m_pCOLoopEndPosition->set(loopSamples.end); - m_loopSamples.setValue(loopSamples); // start looping if (loopSamples.start != kNoTrigger && loopSamples.end != kNoTrigger) { setLoopingEnabled(true); + loopSamples.seek = true; + } else { + loopSamples.seek = false; } if (m_pQuantizeEnabled->toBool() && m_pBeats != nullptr) { @@ -578,6 +603,8 @@ void LoopingControl::setLoopOutToCurrentPosition() { clearActiveBeatLoop(); } //qDebug() << "set loop_out to " << loopSamples.end; + + m_loopSamples.setValue(loopSamples); } void LoopingControl::slotLoopOut(double pressed) { @@ -650,13 +677,8 @@ void LoopingControl::slotReloopToggle(double val) { LoopSamples loopSamples = m_loopSamples.getValue(); if (loopSamples.start != kNoTrigger && loopSamples.end != kNoTrigger && loopSamples.start <= loopSamples.end) { - if (getCurrentSample() < loopSamples.start) { - m_bReloopCatchUpcomingLoop = true; - } setLoopingEnabled(true); - // If we're not playing, jump to the loop in point so the waveform - // shows where it will play from when playback resumes. - if (!m_pPlayButton->toBool() && !m_bReloopCatchUpcomingLoop) { + if (getCurrentSample() > loopSamples.end) { slotLoopInGoto(1); } } @@ -674,18 +696,10 @@ void LoopingControl::slotReloopAndStop(double pressed) { } void LoopingControl::slotLoopStartPos(double pos) { - if (!m_pTrack) { - return; - } - - int newpos = pos; - if (newpos != kNoTrigger && !even(newpos)) { - newpos--; - } - + // This slot is called before trackLoaded() for a new Track LoopSamples loopSamples = m_loopSamples.getValue(); - if (loopSamples.start == newpos) { + if (loopSamples.start == pos) { //nothing to do return; } @@ -696,8 +710,9 @@ void LoopingControl::slotLoopStartPos(double pos) { setLoopingEnabled(false); } - loopSamples.start = newpos; - m_pCOLoopStartPosition->set(newpos); + loopSamples.seek = false; + loopSamples.start = pos; + m_pCOLoopStartPosition->set(pos); if (loopSamples.end != kNoTrigger && loopSamples.end <= loopSamples.start) { @@ -709,17 +724,10 @@ void LoopingControl::slotLoopStartPos(double pos) { } void LoopingControl::slotLoopEndPos(double pos) { - if (!m_pTrack) { - return; - } - - int newpos = pos; - if (newpos != -1 && !even(newpos)) { - newpos--; - } + // This slot is called before trackLoaded() for a new Track LoopSamples loopSamples = m_loopSamples.getValue(); - if (loopSamples.end == newpos) { + if (loopSamples.end == pos) { //nothing to do return; } @@ -727,7 +735,7 @@ void LoopingControl::slotLoopEndPos(double pos) { // Reject if the loop-in is not set, or if the new position is before the // start point (but not -1). if (loopSamples.start == kNoTrigger || - (newpos != kNoTrigger && newpos <= loopSamples.start)) { + (pos != kNoTrigger && pos <= loopSamples.start)) { m_pCOLoopEndPosition->set(loopSamples.end); return; } @@ -737,17 +745,28 @@ void LoopingControl::slotLoopEndPos(double pos) { if (pos == -1.0) { setLoopingEnabled(false); } - loopSamples.end = newpos; - m_pCOLoopEndPosition->set(newpos); + loopSamples.end = pos; + loopSamples.seek = false; + m_pCOLoopEndPosition->set(pos); m_loopSamples.setValue(loopSamples); } +// This is called from the engine thread void LoopingControl::notifySeek(double dNewPlaypos) { LoopSamples loopSamples = m_loopSamples.getValue(); + double currentSample = m_currentSample.getValue(); if (m_bLoopingEnabled) { - // Disable loop when we jump after it, using hot cues or waveform overview - // If we jump before, the loop it is kept enabled as catching loop - if (dNewPlaypos > loopSamples.end) { + // Disable loop when we jumping out, or over a catching loop, + // using hot cues or waveform overview. + if (currentSample >= loopSamples.start && + currentSample <= loopSamples.end && + dNewPlaypos < loopSamples.start) { + // jumping out of loop in backwards + setLoopingEnabled(false); + } + if (currentSample <= loopSamples.end && + dNewPlaypos > loopSamples.end) { + // jumping out a loop or over a catching loop forward setLoopingEnabled(false); } } @@ -848,13 +867,11 @@ bool LoopingControl::currentLoopMatchesBeatloopSize() { LoopSamples loopSamples = m_loopSamples.getValue(); // Calculate where the loop out point would be if it is a beatloop - int beatLoopOutPoint = + double beatLoopOutPoint = m_pBeats->findNBeatsFromSample(loopSamples.start, m_pCOBeatLoopSize->get()); - if (!even(beatLoopOutPoint)) { - beatLoopOutPoint--; - } - return loopSamples.end == beatLoopOutPoint; + return loopSamples.end > beatLoopOutPoint - 2 && + loopSamples.end < beatLoopOutPoint + 2; } void LoopingControl::updateBeatLoopingControls() { @@ -902,7 +919,7 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable // Calculate the new loop start and end samples // give start and end defaults so we can detect problems - LoopSamples newloopSamples = {kNoTrigger, kNoTrigger}; + LoopSamples newloopSamples = {kNoTrigger, kNoTrigger, false}; LoopSamples loopSamples = m_loopSamples.getValue(); // Start from the current position/closest beat and @@ -943,18 +960,11 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable } } else { - newloopSamples.start = floor(cur_pos); + newloopSamples.start = cur_pos; } } - if (!even(newloopSamples.start)) { - newloopSamples.start--; - } - newloopSamples.end = m_pBeats->findNBeatsFromSample(newloopSamples.start, beats); - if (!even(newloopSamples.end)) { - newloopSamples.end--; - } if (newloopSamples.start == newloopSamples.end) { if ((newloopSamples.end + 2) > samples) { @@ -971,8 +981,8 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable // the end of the track, let beatloop_size be set to // a smaller size, but not get larger. double previousBeatloopSize = m_pCOBeatLoopSize->get(); - double previousBeatloopOutPoint = - m_pBeats->findNBeatsFromSample(newloopSamples.start, previousBeatloopSize); + double previousBeatloopOutPoint = m_pBeats->findNBeatsFromSample( + newloopSamples.start, previousBeatloopSize); if (previousBeatloopOutPoint < newloopSamples.start && beats < previousBeatloopSize) { m_pCOBeatLoopSize->setAndConfirm(beats); @@ -984,9 +994,9 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable // do not resize the existing loop until beatloop_size matches // the size of the existing loop. // Do not return immediately so beatloop_size can be updated. - bool avoidResize = false; + bool omitResize = false; if (!currentLoopMatchesBeatloopSize() && !enable) { - avoidResize = true; + omitResize = true; } if (m_pCOBeatLoopSize->get() != beats) { @@ -999,20 +1009,18 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable return; } - if (avoidResize) { + if (omitResize) { return; } // If resizing an inactive loop by changing beatloop_size, // do not seek to the adjusted loop. - if (keepStartPoint && (enable || m_bLoopingEnabled)) { - seekInsideAdjustedLoop(loopSamples.start, loopSamples.end, - newloopSamples.start, newloopSamples.end); - } + newloopSamples.seek = (keepStartPoint && (enable || m_bLoopingEnabled)); m_loopSamples.setValue(newloopSamples); m_pCOLoopStartPosition->set(newloopSamples.start); m_pCOLoopEndPosition->set(newloopSamples.end); + if (enable) { setLoopingEnabled(true); } @@ -1062,7 +1070,13 @@ void LoopingControl::slotBeatJump(double beats) { return; } - if (m_bLoopingEnabled && !m_bAdjustingLoopIn && !m_bAdjustingLoopOut) { + LoopSamples loopSamples = m_loopSamples.getValue(); + double currentSample = m_currentSample.getValue(); + + if (m_bLoopingEnabled && !m_bAdjustingLoopIn && !m_bAdjustingLoopOut && + loopSamples.start <= currentSample && + loopSamples.end >= currentSample) { + // If inside an active loop, move loop slotLoopMove(beats); } else { seekAbs(m_pBeats->findNBeatsFromSample(getCurrentSample(), beats)); @@ -1092,51 +1106,41 @@ void LoopingControl::slotLoopMove(double beats) { if (BpmControl::getBeatContext(m_pBeats, getCurrentSample(), nullptr, nullptr, nullptr, nullptr)) { - int old_loop_in = loopSamples.start; - int old_loop_out = loopSamples.end; - int new_loop_in = m_pBeats->findNBeatsFromSample(old_loop_in, beats); - int new_loop_out = m_pBeats->findNBeatsFromSample(old_loop_out, beats); - if (!even(new_loop_in)) { - --new_loop_in; - } - if (!even(new_loop_out)) { - --new_loop_out; - } + double new_loop_in = m_pBeats->findNBeatsFromSample(loopSamples.start, beats); + double new_loop_out = currentLoopMatchesBeatloopSize() ? + m_pBeats->findNBeatsFromSample(new_loop_in, m_pCOBeatLoopSize->get()) : + m_pBeats->findNBeatsFromSample(loopSamples.end, beats); + + // If we are looping make sure that the play head does not leave the + // loop as a result of our adjustment. + loopSamples.seek = m_bLoopingEnabled; loopSamples.start = new_loop_in; - m_pCOLoopStartPosition->set(new_loop_in); loopSamples.end = new_loop_out; - m_pCOLoopEndPosition->set(new_loop_out); m_loopSamples.setValue(loopSamples); - - // If we are looping make sure that the play head does not leave the - // loop as a result of our adjustment. - if (m_bLoopingEnabled) { - seekInsideAdjustedLoop(old_loop_in, old_loop_out, - new_loop_in, new_loop_out); - } + m_pCOLoopStartPosition->set(new_loop_in); + m_pCOLoopEndPosition->set(new_loop_out); } } -void LoopingControl::seekInsideAdjustedLoop(int old_loop_in, int old_loop_out, - int new_loop_in, int new_loop_out) { +// Must be called from the engine thread only +int LoopingControl::seekInsideAdjustedLoop( + double currentSample, double old_loop_in, + double new_loop_in, double new_loop_out) { // Copy on stack since m_iCurrentSample sample can change under us. - int currentSample = m_iCurrentSample; if (currentSample >= new_loop_in && currentSample <= new_loop_out) { - return; - } - - int new_loop_size = new_loop_out - new_loop_in; - if (!even(new_loop_size)) { - --new_loop_size; + // playposition already is inside the loop + return kNoTrigger; } - if (new_loop_size > old_loop_out - old_loop_in) { - // Could this happen if the user grows a loop and then also shifts it? - qWarning() << "seekInsideAdjustedLoop called for loop that got larger -- ignoring"; - return; + if (currentSample < old_loop_in && currentSample <= new_loop_out) { + // Playposition was before a catching loop and is still a catching loop + // nothing to do + return kNoTrigger; } - int adjusted_position = currentSample; + double new_loop_size = new_loop_out - new_loop_in; + DEBUG_ASSERT(new_loop_size > 0); + double adjusted_position = currentSample; while (adjusted_position > new_loop_out) { adjusted_position -= new_loop_size; VERIFY_OR_DEBUG_ASSERT(adjusted_position > new_loop_in) { @@ -1158,8 +1162,9 @@ void LoopingControl::seekInsideAdjustedLoop(int old_loop_in, int old_loop_out, } } if (adjusted_position != currentSample) { - m_iCurrentSample = adjusted_position; - seekAbs(static_cast(adjusted_position)); + return adjusted_position; + } else { + return kNoTrigger; } } diff --git a/src/engine/loopingcontrol.h b/src/engine/loopingcontrol.h index 39aa977119bd..42141abde17f 100644 --- a/src/engine/loopingcontrol.h +++ b/src/engine/loopingcontrol.h @@ -28,35 +28,27 @@ class LoopingControl : public EngineControl { static QList getBeatSizes(); LoopingControl(QString group, UserSettingsPointer pConfig); - virtual ~LoopingControl(); + ~LoopingControl() override; // process() updates the internal state of the LoopingControl to reflect the // correct current sample. If a loop should be taken LoopingControl returns // the sample that should be seeked to. Otherwise it returns currentSample. - virtual double process(const double dRate, + void process(const double dRate, const double currentSample, const double totalSamples, - const int iBufferSize); + const int iBufferSize) override; // nextTrigger returns the sample at which the engine will be triggered to // take a loop, given the value of currentSample and dRate. - virtual double nextTrigger(const double dRate, + virtual double nextTrigger(bool reverse, const double currentSample, - const double totalSamples, - const int iBufferSize); - - // getTrigger returns the sample that the engine will next be triggered to - // loop to, given the value of currentSample and dRate. - virtual double getTrigger(const double dRate, - const double currentSample, - const double totalSamples, - const int iBufferSize); + double *pTarget); // hintReader will add to hintList hints both the loop in and loop out // sample, if set. - virtual void hintReader(HintVector* pHintList); + void hintReader(HintVector* pHintList) override; - virtual void notifySeek(double dNewPlaypos); + void notifySeek(double dNewPlaypos) override; public slots: void slotLoopIn(double pressed); @@ -97,8 +89,9 @@ class LoopingControl : public EngineControl { private: struct LoopSamples { - int start; - int end; + double start; + double end; + bool seek; // force the playposition to be inside the loop after adjusting it. }; void setLoopingEnabled(bool enabled); @@ -111,8 +104,8 @@ class LoopingControl : public EngineControl { // When a loop changes size such that the playposition is outside of the loop, // we can figure out the best place in the new loop to seek to maintain // the beat. It will even keep multi-bar phrasing correct with 4/4 tracks. - void seekInsideAdjustedLoop(int old_loop_in, int old_loop_out, - int new_loop_in, int new_loop_out); + int seekInsideAdjustedLoop(double currentSample, + double old_loop_in, double new_loop_in, double new_loop_out); ControlPushButton* m_pCOBeatLoopActivate; ControlPushButton* m_pCOBeatLoopRollActivate; @@ -132,16 +125,17 @@ class LoopingControl : public EngineControl { ControlObject* m_pSlipEnabled; ControlObject* m_pPlayButton; - bool m_bLoopingEnabled = false; - bool m_bLoopRollActive = false; - bool m_bLoopManualTogglePressedToExitLoop = false; - bool m_bReloopCatchUpcomingLoop = false; - bool m_bAdjustingLoopIn = false; - bool m_bAdjustingLoopOut = false; - bool m_bLoopOutPressedWhileLoopDisabled = false; + bool m_bLoopingEnabled; + bool m_bLoopRollActive; + bool m_bAdjustingLoopIn; + bool m_bAdjustingLoopOut; + bool m_bAdjustingLoopInOld; + bool m_bAdjustingLoopOutOld; + bool m_bLoopOutPressedWhileLoopDisabled; // TODO(DSC) Make the following values double ControlValueAtomic m_loopSamples; - QAtomicInt m_iCurrentSample; + LoopSamples m_oldLoopSamples; + ControlValueAtomic m_currentSample; ControlObject* m_pQuantizeEnabled; ControlObject* m_pNextBeat; ControlObject* m_pPreviousBeat; diff --git a/src/engine/quantizecontrol.h b/src/engine/quantizecontrol.h index a76321683d05..76baef63ebf0 100644 --- a/src/engine/quantizecontrol.h +++ b/src/engine/quantizecontrol.h @@ -16,10 +16,10 @@ class QuantizeControl : public EngineControl { Q_OBJECT public: QuantizeControl(QString group, UserSettingsPointer pConfig); - virtual ~QuantizeControl(); + ~QuantizeControl() override; - virtual void setCurrentSample(const double dCurrentSample, - const double dTotalSamples); + void setCurrentSample(const double dCurrentSample, + const double dTotalSamples) override; public slots: void trackLoaded(TrackPointer pNewTrack, TrackPointer pOldTrack) override; diff --git a/src/engine/ratecontrol.cpp b/src/engine/ratecontrol.cpp index 7fe644353a55..c5ddefb466cf 100644 --- a/src/engine/ratecontrol.cpp +++ b/src/engine/ratecontrol.cpp @@ -512,7 +512,7 @@ double RateControl::calculateSpeed(double baserate, double speed, bool paused, return rate; } -double RateControl::process(const double rate, +void RateControl::process(const double rate, const double currentSample, const double totalSamples, const int bufferSamples) @@ -547,7 +547,7 @@ double RateControl::process(const double rate, // Avoid Division by Zero if (range == 0) { qDebug() << "Avoiding a Division by Zero in RATERAMP_STEP code"; - return kNoTrigger; + return; } double change = m_pRateDir->get() * m_dTemp / @@ -611,8 +611,6 @@ double RateControl::process(const double rate, resetRateTemp(); } } - - return kNoTrigger; } double RateControl::getTempRate() { diff --git a/src/engine/ratecontrol.h b/src/engine/ratecontrol.h index 3341b7f51f79..99f6d0d4a03c 100644 --- a/src/engine/ratecontrol.h +++ b/src/engine/ratecontrol.h @@ -32,15 +32,15 @@ class RateControl : public EngineControl { Q_OBJECT public: RateControl(QString group, UserSettingsPointer pConfig); - virtual ~RateControl(); + ~RateControl() override; void setBpmControl(BpmControl* bpmcontrol); // Must be called during each callback of the audio thread so that // RateControl has a chance to update itself. - double process(const double dRate, + void process(const double dRate, const double currentSample, const double totalSamples, - const int bufferSamples); + const int bufferSamples) override; // Returns the current engine rate. "reportScratching" is used to tell // the caller that the user is currently scratching, and this is used to // disable keylock. @@ -61,7 +61,7 @@ class RateControl : public EngineControl { static void setRateRamp(bool); // Set Rate Ramp Sensitivity static void setRateRampSensitivity(int); - virtual void notifySeek(double dNewPlaypos); + void notifySeek(double dNewPlaypos) override; public slots: void slotReverseRollActivate(double); diff --git a/src/engine/readaheadmanager.cpp b/src/engine/readaheadmanager.cpp index 915329cae480..e10ae22e8b74 100644 --- a/src/engine/readaheadmanager.cpp +++ b/src/engine/readaheadmanager.cpp @@ -47,30 +47,31 @@ SINT ReadAheadManager::getNextSamples(double dRate, CSAMPLE* pOutput, //qDebug() << "start" << start_sample << requested_samples; - + double target; // A loop will only limit the amount we can read in one shot. const double loop_trigger = m_pLoopingControl->nextTrigger( - dRate, m_currentPosition, 0, 0); - bool loop_active = loop_trigger != kNoTrigger; + in_reverse, m_currentPosition, &target); + SINT preloop_samples = 0; double samplesToLoopTrigger = 0.0; + bool reachedTrigger = false; + SINT samples_from_reader = requested_samples; - if (loop_active) { + if (loop_trigger != kNoTrigger) { samplesToLoopTrigger = in_reverse ? m_currentPosition - loop_trigger : loop_trigger - m_currentPosition; - if (samplesToLoopTrigger < 0) { - // We have already passed the loop trigger - samples_from_reader = 0; - } else { + if (samplesToLoopTrigger >= 0.0) { // We can only read whole frames from the reader. // Use ceil here, to be sure to reach the loop trigger. - preloop_samples = SampleUtil::ceilPlayPosToFrameStart(samplesToLoopTrigger, - kNumChannels); + preloop_samples = SampleUtil::ceilPlayPosToFrameStart( + samplesToLoopTrigger, kNumChannels); // clamp requested samples from the caller to the loop trigger point - samples_from_reader = math_clamp(requested_samples, - static_cast(0), preloop_samples); + if (preloop_samples <= requested_samples) { + reachedTrigger = true; + samples_from_reader = preloop_samples; + } } } @@ -102,48 +103,44 @@ SINT ReadAheadManager::getNextSamples(double dRate, CSAMPLE* pOutput, } // Activate on this trigger if necessary - if (loop_active) { - // LoopingControl makes the decision about whether we should loop or - // not. - const double loop_target = m_pLoopingControl->process( - dRate, m_currentPosition, 0, 0); - - if (loop_target != kNoTrigger) { - m_currentPosition = loop_target; - if (preloop_samples > 0) { - // we are up to one frame ahead of the loop trigger - double overshoot = preloop_samples - samplesToLoopTrigger; - // start the loop later accordingly to be sure the loop length is as desired - // e.g. exactly one bar. - m_currentPosition += overshoot; - - // Example in frames; - // loop start 1.1 loop end 3.3 loop length 2.2 - // m_currentPosition samplesToLoopTrigger preloop_samples - // 2.0 1.3 2 - // 1.8 1.5 2 - // 1.6 1.7 2 - // 1.4 1.9 2 - // 1.2 2.1 3 - // Average preloop_samples = 2.2 - } + if (reachedTrigger) { + DEBUG_ASSERT(target != kNoTrigger); + + // Jump to other end of loop. + m_currentPosition = target; + if (preloop_samples > 0) { + // we are up to one frame ahead of the loop trigger + double overshoot = preloop_samples - samplesToLoopTrigger; + // start the loop later accordingly to be sure the loop length is as desired + // e.g. exactly one bar. + m_currentPosition += overshoot; + + // Example in frames; + // loop start 1.1 loop end 3.3 loop length 2.2 + // m_currentPosition samplesToLoopTrigger preloop_samples + // 2.0 1.3 2 + // 1.8 1.5 2 + // 1.6 1.7 2 + // 1.4 1.9 2 + // 1.2 2.1 3 + // Average preloop_samples = 2.2 + } - // start reading before the loop start point, to crossfade these samples - // with the samples we need to the loop end - int loop_read_position = SampleUtil::roundPlayPosToFrameStart( - m_currentPosition + (in_reverse ? preloop_samples : -preloop_samples), kNumChannels); + // start reading before the loop start point, to crossfade these samples + // with the samples we need to the loop end + int loop_read_position = SampleUtil::roundPlayPosToFrameStart( + m_currentPosition + (in_reverse ? preloop_samples : -preloop_samples), kNumChannels); - int looping_samples_read = m_pReader->read( - loop_read_position, samples_read, in_reverse, m_pCrossFadeBuffer); + int looping_samples_read = m_pReader->read( + loop_read_position, samples_read, in_reverse, m_pCrossFadeBuffer); - if (looping_samples_read != samples_read) { - qDebug() << "ERROR: Couldn't get all needed samples for crossfade."; - } + if (looping_samples_read != samples_read) { + qDebug() << "ERROR: Couldn't get all needed samples for crossfade."; + } - // do crossfade from the current buffer into the new loop beginning - if (samples_read != 0) { // avoid division by zero - SampleUtil::linearCrossfadeBuffers(pOutput, pOutput, m_pCrossFadeBuffer, samples_read); - } + // do crossfade from the current buffer into the new loop beginning + if (samples_read != 0) { // avoid division by zero + SampleUtil::linearCrossfadeBuffers(pOutput, pOutput, m_pCrossFadeBuffer, samples_read); } } @@ -216,8 +213,9 @@ void ReadAheadManager::addReadLogEntry(double virtualPlaypositionStart, } // Not thread-save, call from engine thread only -double ReadAheadManager::getFilePlaypositionFromLog(double currentFilePlayposition, - double numConsumedSamples) { +double ReadAheadManager::getFilePlaypositionFromLog( + double currentFilePlayposition, double numConsumedSamples) { + if (numConsumedSamples == 0) { return currentFilePlayposition; } @@ -236,8 +234,8 @@ double ReadAheadManager::getFilePlaypositionFromLog(double currentFilePlaypositi // Notify EngineControls that we have taken a seek. // Every new entry start with a seek + // (Not looping control) if (shouldNotifySeek) { - m_pLoopingControl->notifySeek(entry.virtualPlaypositionStart); if (m_pRateControl) { m_pRateControl->notifySeek(entry.virtualPlaypositionStart); } diff --git a/src/engine/sync/synccontrol.h b/src/engine/sync/synccontrol.h index 446ed9ea85c2..7af10a30489c 100644 --- a/src/engine/sync/synccontrol.h +++ b/src/engine/sync/synccontrol.h @@ -22,7 +22,7 @@ class SyncControl : public EngineControl, public Syncable { static const double kBpmDouble; SyncControl(const QString& group, UserSettingsPointer pConfig, EngineChannel* pChannel, SyncableListener* pEngineSync); - virtual ~SyncControl(); + ~SyncControl() override; const QString& getGroup() const { return m_sGroup; } EngineChannel* getChannel() const { return m_pChannel; } diff --git a/src/test/looping_control_test.cpp b/src/test/looping_control_test.cpp index 526bf1a6eb6f..5515b5733490 100644 --- a/src/test/looping_control_test.cpp +++ b/src/test/looping_control_test.cpp @@ -11,6 +11,10 @@ #include "test/mockedenginebackendtest.h" #include "util/memory.h" +// Due to rounding errors loop positions should be compared with EXPECT_NEAR instead of EXPECT_EQ. +// NOTE(uklotzde, 2017-12-10): The rounding errors currently only appeared with GCC 7.2.1. +constexpr double kLoopPositionMaxAbsError = 0.000000001; + class LoopingControlTest : public MockedEngineBackendTest { public: LoopingControlTest() @@ -110,10 +114,10 @@ TEST_F(LoopingControlTest, LoopSet) { TEST_F(LoopingControlTest, LoopSetOddSamples) { m_pLoopStartPoint->slotSet(1); - m_pLoopEndPoint->slotSet(101); + m_pLoopEndPoint->slotSet(101.5); seekToSampleAndProcess(50); - EXPECT_EQ(0, m_pLoopStartPoint->get()); - EXPECT_EQ(100, m_pLoopEndPoint->get()); + EXPECT_EQ(1, m_pLoopStartPoint->get()); + EXPECT_EQ(101.5, m_pLoopEndPoint->get()); } TEST_F(LoopingControlTest, LoopInSetInsideLoopContinues) { @@ -390,7 +394,11 @@ TEST_F(LoopingControlTest, LoopScale_HalvesLoop) { EXPECT_EQ(500, m_pLoopEndPoint->get()); // Since the current sample was out of range of the new loop, // the current sample should reseek based on the new loop size. - EXPECT_EQ(300, m_pChannel1->getEngineBuffer()->m_pLoopingControl->getCurrentSample()); + double target; + double trigger = m_pChannel1->getEngineBuffer()->m_pLoopingControl->nextTrigger( + false, 1800, &target); + EXPECT_EQ(300, target); + EXPECT_EQ(1800, trigger); } TEST_F(LoopingControlTest, LoopDoubleButton_IgnoresPastTrackEnd) { @@ -512,6 +520,7 @@ TEST_F(LoopingControlTest, LoopMoveTest) { ProcessBuffer(); EXPECT_EQ(44100, m_pLoopStartPoint->get()); EXPECT_EQ(44400, m_pLoopEndPoint->get()); + ProcessBuffer(); // Should seek to the corresponding offset within the moved loop EXPECT_EQ(44110, m_pChannel1->getEngineBuffer()->m_pLoopingControl->getCurrentSample()); @@ -522,10 +531,11 @@ TEST_F(LoopingControlTest, LoopMoveTest) { m_pButtonBeatMoveBackward->set(0.0); ProcessBuffer(); EXPECT_EQ(0, m_pLoopStartPoint->get()); - EXPECT_EQ(300, m_pLoopEndPoint->get()); + EXPECT_NEAR(300, m_pLoopEndPoint->get(), kLoopPositionMaxAbsError); + ProcessBuffer(); EXPECT_EQ(200, m_pChannel1->getEngineBuffer()->m_pLoopingControl->getCurrentSample()); - // Now repeat the test with looping disabled (should not affect the + // Now repeat the test with looping disabled (should not affect the // playhead). m_pButtonReloopToggle->slotSet(1); EXPECT_FALSE(isLoopEnabled()); @@ -546,7 +556,7 @@ TEST_F(LoopingControlTest, LoopMoveTest) { m_pButtonBeatMoveBackward->set(0.0); ProcessBuffer(); EXPECT_EQ(0, m_pLoopStartPoint->get()); - EXPECT_EQ(300, m_pLoopEndPoint->get()); + EXPECT_NEAR(300, m_pLoopEndPoint->get(), kLoopPositionMaxAbsError); EXPECT_EQ(500, m_pChannel1->getEngineBuffer()->m_pLoopingControl->getCurrentSample()); } @@ -577,6 +587,7 @@ TEST_F(LoopingControlTest, LoopResizeSeek) { // loop. EXPECT_EQ(0, m_pLoopStartPoint->get()); EXPECT_EQ(450, m_pLoopEndPoint->get()); + ProcessBuffer(); EXPECT_EQ(50, m_pChannel1->getEngineBuffer()->m_pLoopingControl->getCurrentSample()); // But if looping is not enabled, no warping occurs. @@ -794,6 +805,13 @@ TEST_F(LoopingControlTest, Beatjump_MovesActiveLoop) { EXPECT_EQ(beatLength, m_pLoopStartPoint->get()); EXPECT_EQ(beatLength * 5, m_pLoopEndPoint->get()); + // jump backward with playposition outside the loop should not move the loop + m_pButtonBeatJumpBackward->set(1.0); + m_pButtonBeatJumpBackward->set(0.0); + EXPECT_EQ(beatLength, m_pLoopStartPoint->get()); + EXPECT_EQ(beatLength * 5, m_pLoopEndPoint->get()); + + seekToSampleAndProcess(beatLength); m_pButtonBeatJumpBackward->set(1.0); m_pButtonBeatJumpBackward->set(0.0); EXPECT_EQ(0, m_pLoopStartPoint->get()); @@ -830,3 +848,23 @@ TEST_F(LoopingControlTest, Beatjump_MovesLoopBoundaries) { EXPECT_EQ(beatLength, m_pLoopStartPoint->get()); EXPECT_EQ(beatLength * 2, m_pLoopEndPoint->get()); } + +TEST_F(LoopingControlTest, LoopEscape) { + m_pLoopStartPoint->slotSet(100); + m_pLoopEndPoint->slotSet(200); + m_pButtonReloopToggle->set(1.0); + m_pButtonReloopToggle->set(0.0); + ProcessBuffer(); + EXPECT_TRUE(isLoopEnabled()); + // seek outside a loop schould disable it + seekToSampleAndProcess(300); + EXPECT_FALSE(isLoopEnabled()); + + m_pButtonReloopToggle->set(1.0); + m_pButtonReloopToggle->set(0.0); + ProcessBuffer(); + EXPECT_TRUE(isLoopEnabled()); + // seek outside a loop schould disable it + seekToSampleAndProcess(50); + EXPECT_FALSE(isLoopEnabled()); +} diff --git a/src/test/readaheadmanager_test.cpp b/src/test/readaheadmanager_test.cpp index afdb5dd90c23..be03d6d8a8f8 100644 --- a/src/test/readaheadmanager_test.cpp +++ b/src/test/readaheadmanager_test.cpp @@ -35,53 +35,28 @@ class StubLoopControl : public LoopingControl { m_triggerReturnValues.push_back(value); } - void pushProcessReturnValue(double value) { - m_processReturnValues.push_back(value); + void pushTargetReturnValue(double value) { + m_targetReturnValues.push_back(value); } - double nextTrigger(const double dRate, + double nextTrigger(bool reverse, const double currentSample, - const double totalSamples, - const int iBufferSize) override { - Q_UNUSED(dRate); + double* pTarget) override { + Q_UNUSED(reverse); Q_UNUSED(currentSample); - Q_UNUSED(totalSamples); - Q_UNUSED(iBufferSize); + Q_UNUSED(pTarget); + RELEASE_ASSERT(!m_targetReturnValues.isEmpty()); + *pTarget = m_targetReturnValues.takeFirst(); RELEASE_ASSERT(!m_triggerReturnValues.isEmpty()); return m_triggerReturnValues.takeFirst(); } - double process(const double dRate, - const double dCurrentSample, - const double dTotalSamples, - const int iBufferSize) override { - Q_UNUSED(dRate); - Q_UNUSED(dCurrentSample); - Q_UNUSED(dTotalSamples); - Q_UNUSED(iBufferSize); - RELEASE_ASSERT(!m_processReturnValues.isEmpty()); - return m_processReturnValues.takeFirst(); - } - - // getTrigger returns the sample that the engine will next be triggered to - // loop to, given the value of currentSample and dRate. - double getTrigger(const double dRate, - const double currentSample, - const double totalSamples, - const int iBufferSize) override { - Q_UNUSED(dRate); - Q_UNUSED(currentSample); - Q_UNUSED(totalSamples); - Q_UNUSED(iBufferSize); - return kNoTrigger; - } - // hintReader has no effect in this stubbed class void hintReader(HintVector* pHintList) override { Q_UNUSED(pHintList); } - void notifySeek(double dNewPlaypos) { + void notifySeek(double dNewPlaypos) override { Q_UNUSED(dNewPlaypos); } @@ -93,7 +68,7 @@ class StubLoopControl : public LoopingControl { protected: QList m_triggerReturnValues; - QList m_processReturnValues; + QList m_targetReturnValues; }; class ReadAheadManagerTest : public MixxxTest { @@ -114,30 +89,6 @@ class ReadAheadManagerTest : public MixxxTest { CSAMPLE* m_pBuffer; }; -TEST_F(ReadAheadManagerTest, LoopEnableSeekBackward) { - // If a loop is enabled and the current playposition is ahead of the loop, - // we should seek to the beginning of the loop. - m_pReadAheadManager->notifySeek(110); - // Trigger value means, the sample that triggers the loop (loop out) - m_pLoopControl->pushTriggerReturnValue(100); - // Process value is the sample we should seek to - m_pLoopControl->pushProcessReturnValue(10); - EXPECT_EQ(0, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 100)); - EXPECT_EQ(10, m_pReadAheadManager->getPlaypos()); -} - -TEST_F(ReadAheadManagerTest, InReverseLoopEnableSeekForward) { - // If we are in reverse, a loop is enabled, and the current playposition - // is before of the loop, we should seek to the out point of the loop. - m_pReadAheadManager->notifySeek(1); - // Trigger value means, the sample that triggers the loop (loop in) - m_pLoopControl->pushTriggerReturnValue(10); - // Process value is the sample we should seek to. - m_pLoopControl->pushProcessReturnValue(100); - EXPECT_EQ(0, m_pReadAheadManager->getNextSamples(-1.0, m_pBuffer, 100)); - EXPECT_EQ(100, m_pReadAheadManager->getPlaypos()); -} - TEST_F(ReadAheadManagerTest, FractionalFrameLoop) { // If we are in reverse, a loop is enabled, and the current playposition // is before of the loop, we should seek to the out point of the loop. @@ -150,12 +101,12 @@ TEST_F(ReadAheadManagerTest, FractionalFrameLoop) { m_pLoopControl->pushTriggerReturnValue(20.2); m_pLoopControl->pushTriggerReturnValue(20.2); // Process value is the sample we should seek to. - m_pLoopControl->pushProcessReturnValue(3.3); - m_pLoopControl->pushProcessReturnValue(3.3); - m_pLoopControl->pushProcessReturnValue(3.3); - m_pLoopControl->pushProcessReturnValue(3.3); - m_pLoopControl->pushProcessReturnValue(3.3); - m_pLoopControl->pushProcessReturnValue(kNoTrigger); + m_pLoopControl->pushTargetReturnValue(3.3); + m_pLoopControl->pushTargetReturnValue(3.3); + m_pLoopControl->pushTargetReturnValue(3.3); + m_pLoopControl->pushTargetReturnValue(3.3); + m_pLoopControl->pushTargetReturnValue(3.3); + m_pLoopControl->pushTargetReturnValue(kNoTrigger); // read from start to loop trigger, overshoot 0.3 EXPECT_EQ(20, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 100)); // read loop diff --git a/src/track/beats.cpp b/src/track/beats.cpp new file mode 100644 index 000000000000..80510c4e5da2 --- /dev/null +++ b/src/track/beats.cpp @@ -0,0 +1,55 @@ + +#include "track/beats.h" + + + +int Beats::numBeatsInRange(double dStartSample, double dEndSample) { + double dLastCountedBeat = 0.0; + int iBeatsCounter; + for (iBeatsCounter = 1; dLastCountedBeat < dEndSample; iBeatsCounter++) { + dLastCountedBeat = findNthBeat(dStartSample, iBeatsCounter); + if (dLastCountedBeat == -1) { + break; + } + } + return iBeatsCounter - 2; +}; + +double Beats::findNBeatsFromSample(double fromSample, double beats) const { + double nthBeat; + double prevBeat; + double nextBeat; + + if (!findPrevNextBeats(fromSample, &prevBeat, &nextBeat)) { + return fromSample; + } + double fromFractionBeats = (fromSample - prevBeat) / (nextBeat - prevBeat); + double beatsFromPrevBeat = fromFractionBeats + beats; + + int fullBeats = static_cast(beatsFromPrevBeat); + double fractionBeats = beatsFromPrevBeat - fullBeats; + + // Add the length between this beat and the fullbeats'th beat + // to the end position + if (fullBeats > 0) { + nthBeat = findNthBeat(nextBeat, fullBeats); + } else { + nthBeat = findNthBeat(prevBeat, fullBeats - 1); + } + + if (nthBeat == -1) { + return fromSample; + } + + // Add the fraction of the beat + if (fractionBeats != 0) { + nextBeat = findNthBeat(nthBeat, 2); + if (nextBeat == -1) { + return fromSample; + } + nthBeat += (nextBeat - nthBeat) * fractionBeats; + } + + return nthBeat; +}; + diff --git a/src/track/beats.h b/src/track/beats.h index 112dd9861946..ecca3b5036db 100644 --- a/src/track/beats.h +++ b/src/track/beats.h @@ -104,45 +104,12 @@ class Beats { // then dSamples is returned. If no beat can be found, returns -1. virtual double findNthBeat(double dSamples, int n) const = 0; - inline int numBeatsInRange(double dStartSample, double dEndSample) { - double dLastCountedBeat = 0.0; - int iBeatsCounter; - for (iBeatsCounter = 1; dLastCountedBeat < dEndSample; iBeatsCounter++) { - dLastCountedBeat = findNthBeat(dStartSample, iBeatsCounter); - if (dLastCountedBeat == -1) { - break; - } - } - return iBeatsCounter - 2; - }; + int numBeatsInRange(double dStartSample, double dEndSample); // Find the sample N beats away from dSample. The number of beats may be // negative and does not need to be an integer. - inline double findNBeatsFromSample(double dSample, double beats) const { - double endSample = dSample; - double thisBeat = 0.0, nthBeat = 0.0; - int fullBeats = static_cast(beats); - // Add the length between this beat and the fullbeats'th beat - // to the end position - if (beats > 0) { - thisBeat = findNthBeat(dSample, 1); - nthBeat = findNthBeat(dSample, fullBeats + 1); - } else if (beats < 0) { - thisBeat = findNthBeat(dSample, -1); - nthBeat = findNthBeat(dSample, fullBeats - 1); - } - endSample += nthBeat - thisBeat; - - // Add the fraction of the beat - double fractionBeats = beats - static_cast(fullBeats); - if (fractionBeats != 0) { - double endNextBeat, endPrevBeat; - findPrevNextBeats(endSample, &endPrevBeat, &endNextBeat); - endSample += (endNextBeat - endPrevBeat) * fractionBeats; - } - - return endSample; - }; + double findNBeatsFromSample(double fromSample, double beats) const; + // Adds to pBeatsList the position in samples of every beat occuring between // startPosition and endPosition. BeatIterator must be iterated while