diff --git a/src/audio/frame.h b/src/audio/frame.h index 1e3ffb1c8a33..55fd867a861b 100644 --- a/src/audio/frame.h +++ b/src/audio/frame.h @@ -120,6 +120,16 @@ class FramePos final { return util_isfinite(m_framePosition); } + // returns true if a is valid and is fairly close to target (within +/- 1 frame). + bool isNear(mixxx::audio::FramePos target) const { + VERIFY_OR_DEBUG_ASSERT(isValid()) { + return false; + } + return target.isValid() && + value() > target.value() - 1.0 && + value() < target.value() + 1.0; + }; + void setValue(value_t framePosition) { m_framePosition = framePosition; } diff --git a/src/engine/controls/bpmcontrol.cpp b/src/engine/controls/bpmcontrol.cpp index e249bb09bdf1..4116fd7231c7 100644 --- a/src/engine/controls/bpmcontrol.cpp +++ b/src/engine/controls/bpmcontrol.cpp @@ -641,64 +641,6 @@ double BpmControl::getBeatDistance(mixxx::audio::FramePos thisPosition) const { return beatPercentage; } -// static -bool BpmControl::getBeatContext( - const mixxx::BeatsPointer& pBeats, - mixxx::audio::FramePos position, - mixxx::audio::FramePos* pPrevBeatPosition, - mixxx::audio::FramePos* pNextBeatPosition, - mixxx::audio::FrameDiff_t* pBeatLengthFrames, - double* pBeatPercentage) { - if (!pBeats) { - return false; - } - - mixxx::audio::FramePos prevBeatPosition; - mixxx::audio::FramePos nextBeatPosition; - if (!pBeats->findPrevNextBeats(position, &prevBeatPosition, &nextBeatPosition, false)) { - return false; - } - - if (pPrevBeatPosition != nullptr) { - *pPrevBeatPosition = prevBeatPosition; - } - - if (pNextBeatPosition != nullptr) { - *pNextBeatPosition = nextBeatPosition; - } - - return getBeatContextNoLookup(position, - prevBeatPosition, - nextBeatPosition, - pBeatLengthFrames, - pBeatPercentage); -} - -// static -bool BpmControl::getBeatContextNoLookup( - mixxx::audio::FramePos position, - mixxx::audio::FramePos prevBeatPosition, - mixxx::audio::FramePos nextBeatPosition, - mixxx::audio::FrameDiff_t* pBeatLengthFrames, - double* pBeatPercentage) { - if (!prevBeatPosition.isValid() || !nextBeatPosition.isValid()) { - return false; - } - - const mixxx::audio::FrameDiff_t beatLengthFrames = nextBeatPosition - prevBeatPosition; - if (pBeatLengthFrames != nullptr) { - *pBeatLengthFrames = beatLengthFrames; - } - - if (pBeatPercentage != nullptr) { - *pBeatPercentage = (beatLengthFrames == 0.0) - ? 0.0 - : (position - prevBeatPosition) / beatLengthFrames; - } - - return true; -} - mixxx::audio::FramePos BpmControl::getNearestPositionInPhase( mixxx::audio::FramePos thisPosition, bool respectLoops, bool playing) { // Without a beatgrid, we don't know the phase offset. @@ -726,7 +668,7 @@ mixxx::audio::FramePos BpmControl::getNearestPositionInPhase( } // This happens if dThisPosition is the target position of a requested // seek command - if (!getBeatContext(pBeats, + if (!pBeats->getContext( thisPosition, &thisPrevBeatPosition, &thisNextBeatPosition, @@ -735,7 +677,8 @@ mixxx::audio::FramePos BpmControl::getNearestPositionInPhase( return thisPosition; } } else { - if (!getBeatContextNoLookup(thisPosition, + if (!mixxx::Beats::getContextNoLookup( + thisPosition, thisPrevBeatPosition, thisNextBeatPosition, &thisBeatLengthFrames, @@ -775,7 +718,7 @@ mixxx::audio::FramePos BpmControl::getNearestPositionInPhase( } const auto otherPosition = pOtherEngineBuffer->getExactPlayPos(); - if (!BpmControl::getBeatContext(otherBeats, + if (!otherBeats->getContext( otherPosition, nullptr, nullptr, @@ -923,8 +866,7 @@ mixxx::audio::FramePos BpmControl::getBeatMatchPosition( } // This happens if thisPosition is the target position of a requested // seek command. Get new prev and next beats for the calculation. - getBeatContext( - m_pBeats, + m_pBeats->getContext( thisPosition, &thisPrevBeatPosition, &thisNextBeatPosition, @@ -942,7 +884,7 @@ mixxx::audio::FramePos BpmControl::getBeatMatchPosition( } // We are between the previous and next beats so we can try a standard // lookup of the beat length. - getBeatContextNoLookup( + mixxx::Beats::getContextNoLookup( thisPosition, thisPrevBeatPosition, thisNextBeatPosition, @@ -979,8 +921,7 @@ mixxx::audio::FramePos BpmControl::getBeatMatchPosition( mixxx::audio::FramePos otherNextBeatPosition; mixxx::audio::FrameDiff_t otherBeatLengthFrames = -1; double otherBeatFraction = -1; - if (!BpmControl::getBeatContext( - otherBeats, + if (!otherBeats->getContext( otherPositionOfThisNextBeat, &otherPrevBeatPosition, &otherNextBeatPosition, @@ -1277,7 +1218,8 @@ void BpmControl::collectFeatures(GroupFeatureState* pGroupFeatures) const { m_pNextBeat.get()); mixxx::audio::FrameDiff_t beatLengthFrames; double beatFraction; - if (getBeatContextNoLookup(info.currentPosition, + if (mixxx::Beats::getContextNoLookup( + info.currentPosition, prevBeatPosition, nextBeatPosition, &beatLengthFrames, diff --git a/src/engine/controls/bpmcontrol.h b/src/engine/controls/bpmcontrol.h index ab6bf9c4afbf..9a91e5c41875 100644 --- a/src/engine/controls/bpmcontrol.h +++ b/src/engine/controls/bpmcontrol.h @@ -67,25 +67,6 @@ class BpmControl : public EngineControl { void collectFeatures(GroupFeatureState* pGroupFeatures) const; - // Calculates contextual information about beats: the previous beat, the - // next beat, the current beat length, and the beat ratio (how far dPosition - // lies within the current beat). Returns false if a previous or next beat - // does not exist. NULL arguments are safe and ignored. - static bool getBeatContext(const mixxx::BeatsPointer& pBeats, - mixxx::audio::FramePos position, - mixxx::audio::FramePos* pPrevBeatPosition, - mixxx::audio::FramePos* pNextBeatPosition, - mixxx::audio::FrameDiff_t* pBeatLengthFrames, - double* pBeatPercentage); - - // Alternative version that works if the next and previous beat positions - // are already known. - static bool getBeatContextNoLookup(mixxx::audio::FramePos position, - mixxx::audio::FramePos prevBeatPosition, - mixxx::audio::FramePos nextBeatPosition, - mixxx::audio::FrameDiff_t* pBeatLengthFrames, - double* pBeatPercentage); - // Returns the shortest change in percentage needed to achieve // target_percentage. // Example: shortestPercentageChange(0.99, 0.01) == 0.02 diff --git a/src/engine/controls/cuecontrol.cpp b/src/engine/controls/cuecontrol.cpp index f99a7816036a..a2e0eb3c73de 100644 --- a/src/engine/controls/cuecontrol.cpp +++ b/src/engine/controls/cuecontrol.cpp @@ -2356,10 +2356,13 @@ void CueControl::setCurrentSavedLoopControlAndActivate(HotcueControl* pControl) mixxx::CueType type = pCue->getType(); Cue::StartAndEndPositions pos = pCue->getStartAndEndPosition(); - VERIFY_OR_DEBUG_ASSERT( - type == mixxx::CueType::Loop && - pos.startPosition.isValid() && - pos.endPosition.isValid()) { + VERIFY_OR_DEBUG_ASSERT(type == mixxx::CueType::Loop) { + return; + } + VERIFY_OR_DEBUG_ASSERT(pos.startPosition.isValid()) { + return; + } + VERIFY_OR_DEBUG_ASSERT(pos.endPosition.isValid()) { return; } @@ -2380,14 +2383,15 @@ void CueControl::slotLoopEnabledChanged(bool enabled) { } DEBUG_ASSERT(pSavedLoopControl->getStatus() != HotcueControl::Status::Empty); - DEBUG_ASSERT(pSavedLoopControl->getCue() && - pSavedLoopControl->getCue()->getPosition() == - mixxx::audio::FramePos::fromEngineSamplePosMaybeInvalid( - m_pLoopStartPosition->get())); - DEBUG_ASSERT(pSavedLoopControl->getCue() && - pSavedLoopControl->getCue()->getEndPosition() == - mixxx::audio::FramePos::fromEngineSamplePosMaybeInvalid( - m_pLoopEndPosition->get())); + DEBUG_ASSERT(pSavedLoopControl->getCue()); + // Don't compare with == because there might be a tiny round-trip offset + // because LoopingControl::slotBeatLoop() uses beats to set the loop_in/_out COs + DEBUG_ASSERT(pSavedLoopControl->getCue()->getPosition().isNear( + mixxx::audio::FramePos::fromEngineSamplePosMaybeInvalid( + m_pLoopStartPosition->get()))); + DEBUG_ASSERT(pSavedLoopControl->getCue()->getEndPosition().isNear( + mixxx::audio::FramePos::fromEngineSamplePosMaybeInvalid( + m_pLoopEndPosition->get()))); if (enabled) { pSavedLoopControl->setStatus(HotcueControl::Status::Active); diff --git a/src/engine/controls/loopingcontrol.cpp b/src/engine/controls/loopingcontrol.cpp index fdb2cdc08911..2e79bec4240d 100644 --- a/src/engine/controls/loopingcontrol.cpp +++ b/src/engine/controls/loopingcontrol.cpp @@ -4,7 +4,6 @@ #include "control/controlobject.h" #include "control/controlpushbutton.h" -#include "engine/controls/bpmcontrol.h" #include "engine/controls/enginecontrol.h" #include "engine/controls/ratecontrol.h" #include "engine/enginebuffer.h" @@ -17,11 +16,6 @@ namespace { constexpr mixxx::audio::FrameDiff_t kMinimumAudibleLoopSizeFrames = 150; - -// returns true if a is valid and is fairly close to target (within +/- 1 frame). -bool positionNear(mixxx::audio::FramePos a, mixxx::audio::FramePos target) { - return a.isValid() && a > target - 1 && a < target + 1; -} } // namespace double LoopingControl::s_dBeatSizes[] = { 0.03125, 0.0625, 0.125, 0.25, 0.5, @@ -1400,7 +1394,7 @@ bool LoopingControl::currentLoopMatchesBeatloopSize(const LoopInfo& loopInfo) co const auto loopEndPosition = pBeats->findNBeatsFromPosition( loopInfo.startPosition, m_pCOBeatLoopSize->get()); - return positionNear(loopInfo.endPosition, loopEndPosition); + return loopEndPosition.isNear(loopInfo.endPosition); } bool LoopingControl::quantizeEnabledAndHasTrueTrackBeats() const { @@ -1423,7 +1417,9 @@ double LoopingControl::findBeatloopSizeForLoop( } } } - return -1; + + // No hit. Calculate the fractional beat length + return pBeats->numFractionalBeatsInRange(startPosition, endPosition); } void LoopingControl::updateBeatLoopingControls() { @@ -1646,8 +1642,8 @@ void LoopingControl::slotBeatLoop(double beats, // or if the endpoints are nearly the same, do not seek forward into the adjusted loop. if (!keepSetPoint || !(enable || m_bLoopingEnabled) || - (positionNear(newloopInfo.startPosition, loopInfo.startPosition) && - positionNear(newloopInfo.endPosition, loopInfo.endPosition))) { + (newloopInfo.startPosition.isNear(loopInfo.startPosition) && + newloopInfo.endPosition.isNear(loopInfo.endPosition))) { newloopInfo.seekMode = LoopSeekMode::MovedOut; } else { newloopInfo.seekMode = LoopSeekMode::Changed; @@ -1806,7 +1802,7 @@ void LoopingControl::slotLoopMove(double beats) { } FrameInfo info = frameInfo(); - if (BpmControl::getBeatContext(pBeats, + if (pBeats->getContext( info.currentPosition, nullptr, nullptr, diff --git a/src/test/bpmcontrol_test.cpp b/src/test/bpmcontrol_test.cpp index 044fa7781594..c6b0d6637657 100644 --- a/src/test/bpmcontrol_test.cpp +++ b/src/test/bpmcontrol_test.cpp @@ -44,7 +44,7 @@ TEST_F(BpmControlTest, BeatContext_BeatGrid) { mixxx::audio::FramePos nextBeatPosition; mixxx::audio::FrameDiff_t beatLengthFrames; double beatPercentage; - EXPECT_TRUE(BpmControl::getBeatContext(pBeats, + EXPECT_TRUE(pBeats->getContext( mixxx::audio::kStartFramePos, &prevBeatPosition, &nextBeatPosition, diff --git a/src/track/beats.cpp b/src/track/beats.cpp index 3e14671a1278..d0d8342fef63 100644 --- a/src/track/beats.cpp +++ b/src/track/beats.cpp @@ -615,6 +615,59 @@ mixxx::Bpm Beats::getBpmAroundPosition(audio::FramePos position, int n) const { return BeatUtils::calculateAverageBpm(2 * n, m_sampleRate, startPosition, endPosition); } +bool Beats::getContext( + mixxx::audio::FramePos position, + mixxx::audio::FramePos* pPrevBeatPosition, + mixxx::audio::FramePos* pNextBeatPosition, + mixxx::audio::FrameDiff_t* pBeatLengthFrames, + double* pBeatPercentage) const { + mixxx::audio::FramePos prevBeatPosition; + mixxx::audio::FramePos nextBeatPosition; + if (!findPrevNextBeats(position, &prevBeatPosition, &nextBeatPosition, false)) { + return false; + } + + if (pPrevBeatPosition != nullptr) { + *pPrevBeatPosition = prevBeatPosition; + } + + if (pNextBeatPosition != nullptr) { + *pNextBeatPosition = nextBeatPosition; + } + + return getContextNoLookup( + position, + prevBeatPosition, + nextBeatPosition, + pBeatLengthFrames, + pBeatPercentage); +} + +// static +bool Beats::getContextNoLookup( + mixxx::audio::FramePos position, + mixxx::audio::FramePos prevBeatPosition, + mixxx::audio::FramePos nextBeatPosition, + mixxx::audio::FrameDiff_t* pBeatLengthFrames, + double* pBeatPercentage) { + if (!prevBeatPosition.isValid() || !nextBeatPosition.isValid()) { + return false; + } + + const mixxx::audio::FrameDiff_t beatLengthFrames = nextBeatPosition - prevBeatPosition; + if (pBeatLengthFrames != nullptr) { + *pBeatLengthFrames = beatLengthFrames; + } + + if (pBeatPercentage != nullptr) { + *pBeatPercentage = (beatLengthFrames == 0.0) + ? 0.0 + : (position - prevBeatPosition) / beatLengthFrames; + } + + return true; +} + std::optional Beats::tryTranslate(audio::FrameDiff_t offsetFrames) const { std::vector markers; std::transform(m_markers.cbegin(), @@ -740,6 +793,29 @@ int Beats::numBeatsInRange(audio::FramePos startPosition, audio::FramePos endPos return i - 2; }; +double Beats::numFractionalBeatsInRange(audio::FramePos startPos, audio::FramePos endPos) const { + double pBeatPercentage; + // get the ratio of first beat / position: + // 1 - ((startPos - beat before range) / first beat length) + if (!getContext(startPos, nullptr, nullptr, nullptr, &pBeatPercentage)) { + return -1; + } + double numBeats = 1 - pBeatPercentage; + + // get the last beat ratio: + // (endPos - last beat in range) / last beat length + if (!getContext(endPos, nullptr, nullptr, nullptr, &pBeatPercentage)) { + return -1; + } + numBeats += pBeatPercentage; + + // get the beats inside the range + // subtract 1 because we already counted the first beat + numBeats += numBeatsInRange(findNextBeat(startPos), endPos) - 1; + + return numBeats; +} + audio::FramePos Beats::findNextBeat(audio::FramePos position) const { return findNthBeat(position, 1); } diff --git a/src/track/beats.h b/src/track/beats.h index ad0d7a0a59ce..43491bf66cfe 100644 --- a/src/track/beats.h +++ b/src/track/beats.h @@ -349,6 +349,8 @@ class Beats : private std::enable_shared_from_this { audio::FramePos snapPosToNearBeat(audio::FramePos position) const; int numBeatsInRange(audio::FramePos startPosition, audio::FramePos endPosition) const; + double numFractionalBeatsInRange(audio::FramePos startPosition, + audio::FramePos endPosition) const; /// Find the frame position N beats away from `position`. The number of beats may be /// negative and does not need to be an integer. In this case the returned position will @@ -370,6 +372,26 @@ class Beats : private std::enable_shared_from_this { /// The returned Bpm value may be invalid. mixxx::Bpm getBpmAroundPosition(audio::FramePos position, int n) const; + // Calculates contextual information about beats: the previous beat, the + // next beat, the current beat length, and the beat ratio (how far position + // lies within the current beat). Returns false if a previous or next beat + // does not exist. NULL arguments are safe and ignored. + bool getContext( + mixxx::audio::FramePos position, + mixxx::audio::FramePos* pPrevBeatPosition, + mixxx::audio::FramePos* pNextBeatPosition, + mixxx::audio::FrameDiff_t* pBeatLengthFrames, + double* pBeatPercentage) const; + + // Alternative version that works if the next and previous beat positions + // are already known. + static bool getContextNoLookup( + mixxx::audio::FramePos position, + mixxx::audio::FramePos prevBeatPosition, + mixxx::audio::FramePos nextBeatPosition, + mixxx::audio::FrameDiff_t* pBeatLengthFrames, + double* pBeatPercentage); + audio::SampleRate getSampleRate() const { return m_sampleRate; }