diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b4f6f93a8cf..5877b834f55c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Cover art: Prevent wrong cover art display due to hash conflicts * Cover art: Add background color for quick cover art preview * Add Random Track Control to AutoDJ [#3076](https://github.com/mixxxdj/mixxx/pull/3076) +* Add support for saving loops as hotcues [#2194](https://github.com/mixxxdj/mixxx/pull/2194) [lp:1367159](https://bugs.launchpad.net/mixxx/+bug/1367159) ## [2.3.0](https://launchpad.net/mixxx/+milestone/2.3.0) (Unreleased) ### Hotcues ### diff --git a/CMakeLists.txt b/CMakeLists.txt index 385d90ae0674..cfb9b5b9bde9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1282,6 +1282,7 @@ add_executable(mixxx-test src/test/enginemicrophonetest.cpp src/test/enginesynctest.cpp src/test/globaltrackcache_test.cpp + src/test/hotcuecontrol_test.cpp src/test/imageutils_test.cpp src/test/indexrange_test.cpp src/test/keyutilstest.cpp diff --git a/res/skins/Deere/hotcue_button.xml b/res/skins/Deere/hotcue_button.xml index bbf386b73c92..2da3bac06396 100644 --- a/res/skins/Deere/hotcue_button.xml +++ b/res/skins/Deere/hotcue_button.xml @@ -17,7 +17,7 @@ true - 2 + 3 0 @@ -26,5 +26,9 @@ 1 + + 2 + + diff --git a/res/skins/LateNight/controls/button_hotcue.xml b/res/skins/LateNight/controls/button_hotcue.xml index fa2a9993bc77..8e7d0a0dfc5c 100644 --- a/res/skins/LateNight/controls/button_hotcue.xml +++ b/res/skins/LateNight/controls/button_hotcue.xml @@ -15,7 +15,7 @@ me,f - 2 + 3 0 @@ -27,6 +27,11 @@ skin://buttons/btn__square_set.svg skin://buttons/btn__square_active.svg + + 2 + skin:/buttons_/btn__square_set.svg + skin:/buttons_/btn__square_active.svg + diff --git a/res/skins/LateNight/palemoon/buttons/btn__1_loop.svg b/res/skins/LateNight/palemoon/buttons/btn__1_loop.svg new file mode 100644 index 000000000000..f4a66fb01a92 --- /dev/null +++ b/res/skins/LateNight/palemoon/buttons/btn__1_loop.svg @@ -0,0 +1,167 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + diff --git a/res/skins/LateNight/palemoon/buttons/btn__2_loop.svg b/res/skins/LateNight/palemoon/buttons/btn__2_loop.svg new file mode 100644 index 000000000000..501235396fd6 --- /dev/null +++ b/res/skins/LateNight/palemoon/buttons/btn__2_loop.svg @@ -0,0 +1,109 @@ + + + + + + + image/svg+xml + + + + + + + 2 + + + + + + + diff --git a/res/skins/LateNight/palemoon/buttons/btn__3_loop.svg b/res/skins/LateNight/palemoon/buttons/btn__3_loop.svg new file mode 100644 index 000000000000..e5327ba2c1b0 --- /dev/null +++ b/res/skins/LateNight/palemoon/buttons/btn__3_loop.svg @@ -0,0 +1,109 @@ + + + + + + + image/svg+xml + + + + + + + 3 + + + + + + + diff --git a/res/skins/LateNight/palemoon/buttons/btn__4_loop.svg b/res/skins/LateNight/palemoon/buttons/btn__4_loop.svg new file mode 100644 index 000000000000..2c2ce420c775 --- /dev/null +++ b/res/skins/LateNight/palemoon/buttons/btn__4_loop.svg @@ -0,0 +1,109 @@ + + + + + + + image/svg+xml + + + + + + + 4 + + + + + + + diff --git a/res/skins/LateNight/palemoon/buttons/btn__5_loop.svg b/res/skins/LateNight/palemoon/buttons/btn__5_loop.svg new file mode 100644 index 000000000000..7757dcc9ac7d --- /dev/null +++ b/res/skins/LateNight/palemoon/buttons/btn__5_loop.svg @@ -0,0 +1,109 @@ + + + + + + + image/svg+xml + + + + + + + 5 + + + + + + + diff --git a/res/skins/LateNight/palemoon/buttons/btn__6_loop.svg b/res/skins/LateNight/palemoon/buttons/btn__6_loop.svg new file mode 100644 index 000000000000..e284a940e41c --- /dev/null +++ b/res/skins/LateNight/palemoon/buttons/btn__6_loop.svg @@ -0,0 +1,109 @@ + + + + + + + image/svg+xml + + + + + + + 6 + + + + + + + diff --git a/res/skins/LateNight/palemoon/buttons/btn__7_loop.svg b/res/skins/LateNight/palemoon/buttons/btn__7_loop.svg new file mode 100644 index 000000000000..03f4ac0c1b46 --- /dev/null +++ b/res/skins/LateNight/palemoon/buttons/btn__7_loop.svg @@ -0,0 +1,109 @@ + + + + + + + image/svg+xml + + + + + + + 7 + + + + + + + diff --git a/res/skins/LateNight/palemoon/buttons/btn__8_loop.svg b/res/skins/LateNight/palemoon/buttons/btn__8_loop.svg new file mode 100644 index 000000000000..db05c078bdf0 --- /dev/null +++ b/res/skins/LateNight/palemoon/buttons/btn__8_loop.svg @@ -0,0 +1,159 @@ + + + + + + + image/svg+xml + + + + + + + 8 + + + + + + + + + + + + + diff --git a/res/skins/LateNight/style_palemoon.qss b/res/skins/LateNight/style_palemoon.qss index cc5759af0d90..55027c6b5a3a 100644 --- a/res/skins/LateNight/style_palemoon.qss +++ b/res/skins/LateNight/style_palemoon.qss @@ -1855,82 +1855,146 @@ WPushButton#RecButton[displayValue="1"], #Hotcue1 WPushButton[displayValue="0"] { image: url(skin:/palemoon/buttons/btn__1.svg) no-repeat center center; } - #Hotcue1 WPushButton[displayValue="1"][dark="false"] { + #Hotcue1 WPushButton[displayValue="1"][dark="false"], + #Hotcue1 WPushButton[displayValue="2"][dark="false"] { image: url(skin:/palemoon/buttons/btn__1_active.svg) no-repeat center center; } - #Hotcue1 WPushButton[displayValue="1"][dark="true"] { + #Hotcue1 WPushButton[displayValue="1"][dark="true"], + #Hotcue1 WPushButton[displayValue="2"][dark="true"] { image: url(skin:/palemoon/buttons/btn__1_active_dark.svg) no-repeat center center; } + #Hotcue1 WPushButton[type="loop"][displayValue="1"][dark="false"], + #Hotcue1 WPushButton[type="loop"][displayValue="2"][dark="false"], + #Hotcue1 WPushButton[type="loop"][displayValue="1"][dark="true"], + #Hotcue1 WPushButton[type="loop"][displayValue="2"][dark="true"] { + image: url(skin:/palemoon/buttons/btn__1_loop.svg) no-repeat center center; + } #Hotcue2 WPushButton[displayValue="0"] { image: url(skin:/palemoon/buttons/btn__2.svg) no-repeat center center; } - #Hotcue2 WPushButton[displayValue="1"] { + #Hotcue2 WPushButton[displayValue="1"], + #Hotcue2 WPushButton[displayValue="2"] { image: url(skin:/palemoon/buttons/btn__2_active.svg) no-repeat center center; } - #Hotcue2 WPushButton[displayValue="1"][dark="true"] { + #Hotcue2 WPushButton[displayValue="1"][dark="true"], + #Hotcue2 WPushButton[displayValue="2"][dark="true"] { image: url(skin:/palemoon/buttons/btn__2_active_dark.svg) no-repeat center center; } + #Hotcue2 WPushButton[type="loop"][displayValue="1"][dark="false"], + #Hotcue2 WPushButton[type="loop"][displayValue="2"][dark="false"], + #Hotcue2 WPushButton[type="loop"][displayValue="1"][dark="true"], + #Hotcue2 WPushButton[type="loop"][displayValue="2"][dark="true"] { + image: url(skin:/palemoon/buttons/btn__2_loop.svg) no-repeat center center; + } #Hotcue3 WPushButton[displayValue="0"] { image: url(skin:/palemoon/buttons/btn__3.svg) no-repeat center center; } - #Hotcue3 WPushButton[displayValue="1"] { + #Hotcue3 WPushButton[displayValue="1"], + #Hotcue3 WPushButton[displayValue="2"] { image: url(skin:/palemoon/buttons/btn__3_active.svg) no-repeat center center; } - #Hotcue3 WPushButton[displayValue="1"][dark="true"] { + #Hotcue3 WPushButton[displayValue="1"][dark="true"], + #Hotcue3 WPushButton[displayValue="2"][dark="true"] { image: url(skin:/palemoon/buttons/btn__3_active_dark.svg) no-repeat center center; } + #Hotcue3 WPushButton[type="loop"][displayValue="1"][dark="false"], + #Hotcue3 WPushButton[type="loop"][displayValue="2"][dark="false"], + #Hotcue3 WPushButton[type="loop"][displayValue="1"][dark="true"], + #Hotcue3 WPushButton[type="loop"][displayValue="2"][dark="true"] { + image: url(skin:/palemoon/buttons/btn__3_loop.svg) no-repeat center center; + } #Hotcue4 WPushButton[displayValue="0"] { image: url(skin:/palemoon/buttons/btn__4.svg) no-repeat center center; } - #Hotcue4 WPushButton[displayValue="1"] { + #Hotcue4 WPushButton[displayValue="1"], + #Hotcue4 WPushButton[displayValue="2"] { image: url(skin:/palemoon/buttons/btn__4_active.svg) no-repeat center center; } - #Hotcue4 WPushButton[displayValue="1"][dark="true"] { + #Hotcue4 WPushButton[displayValue="1"][dark="true"], + #Hotcue4 WPushButton[displayValue="2"][dark="true"] { image: url(skin:/palemoon/buttons/btn__4_active_dark.svg) no-repeat center center; } + #Hotcue4 WPushButton[type="loop"][displayValue="1"][dark="false"], + #Hotcue4 WPushButton[type="loop"][displayValue="2"][dark="false"], + #Hotcue4 WPushButton[type="loop"][displayValue="1"][dark="true"], + #Hotcue4 WPushButton[type="loop"][displayValue="2"][dark="true"] { + image: url(skin:/palemoon/buttons/btn__4_loop.svg) no-repeat center center; + } #Hotcue5 WPushButton[displayValue="0"] { image: url(skin:/palemoon/buttons/btn__5.svg) no-repeat center center; } - #Hotcue5 WPushButton[displayValue="1"] { + #Hotcue5 WPushButton[displayValue="1"], + #Hotcue5 WPushButton[displayValue="2"] { image: url(skin:/palemoon/buttons/btn__5_active.svg) no-repeat center center; } - #Hotcue5 WPushButton[displayValue="1"][dark="true"] { + #Hotcue5 WPushButton[displayValue="1"][dark="true"], + #Hotcue5 WPushButton[displayValue="2"][dark="true"] { image: url(skin:/palemoon/buttons/btn__5_active_dark.svg) no-repeat center center; } + #Hotcue5 WPushButton[type="loop"][displayValue="1"][dark="false"], + #Hotcue5 WPushButton[type="loop"][displayValue="2"][dark="false"], + #Hotcue5 WPushButton[type="loop"][displayValue="1"][dark="true"], + #Hotcue5 WPushButton[type="loop"][displayValue="2"][dark="true"] { + image: url(skin:/palemoon/buttons/btn__5_loop.svg) no-repeat center center; + } #Hotcue6 WPushButton[displayValue="0"] { image: url(skin:/palemoon/buttons/btn__6.svg) no-repeat center center; } - #Hotcue6 WPushButton[displayValue="1"] { + #Hotcue6 WPushButton[displayValue="1"], + #Hotcue6 WPushButton[displayValue="2"] { image: url(skin:/palemoon/buttons/btn__6_active.svg) no-repeat center center; } - #Hotcue6 WPushButton[displayValue="1"][dark="true"] { + #Hotcue6 WPushButton[displayValue="1"][dark="true"], + #Hotcue6 WPushButton[displayValue="2"][dark="true"] { image: url(skin:/palemoon/buttons/btn__6_active_dark.svg) no-repeat center center; } + #Hotcue6 WPushButton[type="loop"][displayValue="1"][dark="false"], + #Hotcue6 WPushButton[type="loop"][displayValue="2"][dark="false"], + #Hotcue6 WPushButton[type="loop"][displayValue="1"][dark="true"], + #Hotcue6 WPushButton[type="loop"][displayValue="2"][dark="true"] { + image: url(skin:/palemoon/buttons/btn__6_loop.svg) no-repeat center center; + } #Hotcue7 WPushButton[displayValue="0"] { image: url(skin:/palemoon/buttons/btn__7.svg) no-repeat center center; } - #Hotcue7 WPushButton[displayValue="1"] { + #Hotcue7 WPushButton[displayValue="1"], + #Hotcue7 WPushButton[displayValue="2"] { image: url(skin:/palemoon/buttons/btn__7_active.svg) no-repeat center center; } - #Hotcue7 WPushButton[displayValue="1"][dark="true"] { + #Hotcue7 WPushButton[displayValue="1"][dark="true"], + #Hotcue7 WPushButton[displayValue="2"][dark="true"] { image: url(skin:/palemoon/buttons/btn__7_active_dark.svg) no-repeat center center; } + #Hotcue7 WPushButton[type="loop"][displayValue="1"][dark="false"], + #Hotcue7 WPushButton[type="loop"][displayValue="2"][dark="false"], + #Hotcue7 WPushButton[type="loop"][displayValue="1"][dark="true"], + #Hotcue7 WPushButton[type="loop"][displayValue="2"][dark="true"] { + image: url(skin:/palemoon/buttons/btn__7_loop.svg) no-repeat center center; + } #Hotcue8 WPushButton[displayValue="0"] { image: url(skin:/palemoon/buttons/btn__8.svg) no-repeat center center; } - #Hotcue8 WPushButton[displayValue="1"] { + #Hotcue8 WPushButton[displayValue="1"], + #Hotcue8 WPushButton[displayValue="2"] { image: url(skin:/palemoon/buttons/btn__8_active.svg) no-repeat center center; } - #Hotcue8 WPushButton[displayValue="1"][dark="true"] { + #Hotcue8 WPushButton[displayValue="1"][dark="true"], + #Hotcue8 WPushButton[displayValue="2"][dark="true"] { image: url(skin:/palemoon/buttons/btn__8_active_dark.svg) no-repeat center center; } + #Hotcue8 WPushButton[type="loop"][displayValue="1"][dark="false"], + #Hotcue8 WPushButton[type="loop"][displayValue="2"][dark="false"], + #Hotcue8 WPushButton[type="loop"][displayValue="1"][dark="true"], + #Hotcue8 WPushButton[type="loop"][displayValue="2"][dark="true"] { + image: url(skin:/palemoon/buttons/btn__8_loop.svg) no-repeat center center; + } #SpecialCueButton_intro_start WPushButton[displayValue="0"] { image: url(skin:/palemoon/buttons/btn__intro_start.svg) no-repeat center center; diff --git a/res/skins/Tango/button_hotcue_deck.xml b/res/skins/Tango/button_hotcue_deck.xml index 62edc59087d6..9bef283bef14 100644 --- a/res/skins/Tango/button_hotcue_deck.xml +++ b/res/skins/Tango/button_hotcue_deck.xml @@ -16,7 +16,7 @@ Variables: me,f - 2 + 3 0 @@ -27,5 +27,10 @@ Variables: center + + 2 + + center + diff --git a/res/skins/Tango/button_hotcue_sam_pre.xml b/res/skins/Tango/button_hotcue_sam_pre.xml index ad95e22a4f39..a5cd146d5055 100644 --- a/res/skins/Tango/button_hotcue_sam_pre.xml +++ b/res/skins/Tango/button_hotcue_sam_pre.xml @@ -17,7 +17,7 @@ Variables: me,f - 2 + 3 0 @@ -28,5 +28,10 @@ Variables: center + + 2 + + center + diff --git a/src/engine/controls/cuecontrol.cpp b/src/engine/controls/cuecontrol.cpp index 5baba76efe89..c675385be300 100644 --- a/src/engine/controls/cuecontrol.cpp +++ b/src/engine/controls/cuecontrol.cpp @@ -58,6 +58,7 @@ CueControl::CueControl(QString group, m_bypassCueSetByPlay(false), m_iNumHotCues(NUM_HOT_CUES), m_pLoadedTrack(), + m_pCurrentSavedLoopControl(nullptr), m_mutex(QMutex::Recursive) { // To silence a compiler warning about CUE_MODE_PIONEER. Q_UNUSED(CUE_MODE_PIONEER); @@ -71,6 +72,11 @@ CueControl::CueControl(QString group, Qt::DirectConnection); m_pClosestBeat = ControlObject::getControl(ConfigKey(group, "beat_closest")); + m_pLoopStartPosition = make_parented(group, "loop_start_position", this); + m_pLoopEndPosition = make_parented(group, "loop_end_position", this); + m_pLoopEnabled = make_parented(group, "loop_enabled", this); + m_pBeatLoopActivate = make_parented(group, "beatloop_activate", this); + m_pBeatLoopSize = make_parented(group, "beatloop_size", this); m_pCuePoint = new ControlObject(ConfigKey(group, "cue_point")); m_pCuePoint->set(Cue::kNoPosition); @@ -290,26 +296,55 @@ void CueControl::createControls() { connect(pControl, &HotcueControl::hotcuePositionChanged, this, &CueControl::hotcuePositionChanged, Qt::DirectConnection); - connect(pControl, &HotcueControl::hotcueSet, - this, &CueControl::hotcueSet, + connect(pControl, + &HotcueControl::hotcueEndPositionChanged, + this, + &CueControl::hotcueEndPositionChanged, Qt::DirectConnection); - connect(pControl, &HotcueControl::hotcueGoto, - this, &CueControl::hotcueGoto, + connect(pControl, + &HotcueControl::hotcueSet, + this, + &CueControl::hotcueSet, Qt::DirectConnection); - connect(pControl, &HotcueControl::hotcueGotoAndPlay, - this, &CueControl::hotcueGotoAndPlay, + connect(pControl, + &HotcueControl::hotcueGoto, + this, + &CueControl::hotcueGoto, Qt::DirectConnection); - connect(pControl, &HotcueControl::hotcueGotoAndStop, - this, &CueControl::hotcueGotoAndStop, + connect(pControl, + &HotcueControl::hotcueGotoAndPlay, + this, + &CueControl::hotcueGotoAndPlay, Qt::DirectConnection); - connect(pControl, &HotcueControl::hotcueActivate, - this, &CueControl::hotcueActivate, + connect(pControl, + &HotcueControl::hotcueGotoAndStop, + this, + &CueControl::hotcueGotoAndStop, Qt::DirectConnection); - connect(pControl, &HotcueControl::hotcueActivatePreview, - this, &CueControl::hotcueActivatePreview, + connect(pControl, + &HotcueControl::hotcueGotoAndLoop, + this, + &CueControl::hotcueGotoAndLoop, Qt::DirectConnection); - connect(pControl, &HotcueControl::hotcueClear, - this, &CueControl::hotcueClear, + connect(pControl, + &HotcueControl::hotcueCueLoop, + this, + &CueControl::hotcueCueLoop, + Qt::DirectConnection); + connect(pControl, + &HotcueControl::hotcueActivate, + this, + &CueControl::hotcueActivate, + Qt::DirectConnection); + connect(pControl, + &HotcueControl::hotcueActivatePreview, + this, + &CueControl::hotcueActivatePreview, + Qt::DirectConnection); + connect(pControl, + &HotcueControl::hotcueClear, + this, + &CueControl::hotcueClear, Qt::DirectConnection); m_hotcueControls.append(pControl); @@ -321,8 +356,10 @@ void CueControl::attachCue(CuePointer pCue, HotcueControl* pControl) { return; } detachCue(pControl); - connect(pCue.get(), &Cue::updated, - this, &CueControl::cueUpdated, + connect(pCue.get(), + &Cue::updated, + this, + &CueControl::cueUpdated, Qt::DirectConnection); pControl->setCue(pCue); @@ -332,11 +369,17 @@ void CueControl::detachCue(HotcueControl* pControl) { VERIFY_OR_DEBUG_ASSERT(pControl) { return; } + CuePointer pCue(pControl->getCue()); if (!pCue) { return; } + disconnect(pCue.get(), 0, this, 0); + + if (m_pCurrentSavedLoopControl == pControl) { + m_pCurrentSavedLoopControl = nullptr; + } pControl->resetCue(); } @@ -368,10 +411,16 @@ void CueControl::trackLoaded(TrackPointer pNewTrack) { } m_pLoadedTrack = pNewTrack; - connect(m_pLoadedTrack.get(), &Track::analyzed, this, &CueControl::trackAnalyzed, Qt::DirectConnection); + connect(m_pLoadedTrack.get(), + &Track::analyzed, + this, + &CueControl::trackAnalyzed, + Qt::DirectConnection); - connect(m_pLoadedTrack.get(), &Track::cuesUpdated, - this, &CueControl::trackCuesUpdated, + connect(m_pLoadedTrack.get(), + &Track::cuesUpdated, + this, + &CueControl::trackCuesUpdated, Qt::DirectConnection); CuePointer pMainCue; @@ -422,7 +471,8 @@ void CueControl::trackLoaded(TrackPointer pNewTrack) { } break; case SeekOnLoadMode::FirstSound: { - CuePointer pAudibleSound = pNewTrack->findCueByType(mixxx::CueType::AudibleSound); + CuePointer pAudibleSound = + pNewTrack->findCueByType(mixxx::CueType::AudibleSound); double audibleSoundPosition = Cue::kNoPosition; if (pAudibleSound) { audibleSoundPosition = pAudibleSound->getPosition(); @@ -478,10 +528,11 @@ void CueControl::loadCuesFromTrack() { QSet active_hotcues; CuePointer pLoadCue, pIntroCue, pOutroCue; - if (!m_pLoadedTrack) + if (!m_pLoadedTrack) { return; + } - for (const CuePointer& pCue: m_pLoadedTrack->getCuePoints()) { + for (const CuePointer& pCue : m_pLoadedTrack->getCuePoints()) { switch (pCue->getType()) { case mixxx::CueType::MainCue: DEBUG_ASSERT(!pLoadCue); // There should be only one MainCue cue @@ -497,10 +548,6 @@ void CueControl::loadCuesFromTrack() { break; case mixxx::CueType::HotCue: case mixxx::CueType::Loop: { - // FIXME: While it's not possible to save Loops in Mixxx yet, we do - // support importing them from Serato and Rekordbox. For the time - // being we treat them like regular hotcues and ignore their end - // position until #2194 has been merged. if (pCue->getHotCue() == Cue::kNoHotCue) { continue; } @@ -522,7 +569,9 @@ void CueControl::loadCuesFromTrack() { } else { // If the old hotcue is the same, then we only need to update pControl->setPosition(pCue->getPosition()); + pControl->setEndPosition(pCue->getEndPosition()); pControl->setColor(pCue->getColor()); + pControl->setType(pCue->getType()); } // Add the hotcue to the list of active hotcues active_hotcues.insert(hotcue); @@ -538,9 +587,11 @@ void CueControl::loadCuesFromTrack() { double endPosition = pIntroCue->getEndPosition(); m_pIntroStartPosition->set(quantizeCuePoint(startPosition)); - m_pIntroStartEnabled->forceSet(startPosition == Cue::kNoPosition ? 0.0 : 1.0); + m_pIntroStartEnabled->forceSet( + startPosition == Cue::kNoPosition ? 0.0 : 1.0); m_pIntroEndPosition->set(quantizeCuePoint(endPosition)); - m_pIntroEndEnabled->forceSet(endPosition == Cue::kNoPosition ? 0.0 : 1.0); + m_pIntroEndEnabled->forceSet( + endPosition == Cue::kNoPosition ? 0.0 : 1.0); } else { m_pIntroStartPosition->set(Cue::kNoPosition); m_pIntroStartEnabled->forceSet(0.0); @@ -553,9 +604,11 @@ void CueControl::loadCuesFromTrack() { double endPosition = pOutroCue->getEndPosition(); m_pOutroStartPosition->set(quantizeCuePoint(startPosition)); - m_pOutroStartEnabled->forceSet(startPosition == Cue::kNoPosition ? 0.0 : 1.0); + m_pOutroStartEnabled->forceSet( + startPosition == Cue::kNoPosition ? 0.0 : 1.0); m_pOutroEndPosition->set(quantizeCuePoint(endPosition)); - m_pOutroEndEnabled->forceSet(endPosition == Cue::kNoPosition ? 0.0 : 1.0); + m_pOutroEndEnabled->forceSet( + endPosition == Cue::kNoPosition ? 0.0 : 1.0); } else { m_pOutroStartPosition->set(Cue::kNoPosition); m_pOutroStartEnabled->forceSet(0.0); @@ -641,7 +694,7 @@ void CueControl::quantizeChanged(double v) { } } -void CueControl::hotcueSet(HotcueControl* pControl, double value) { +void CueControl::hotcueSet(HotcueControl* pControl, double value, HotcueSetMode mode) { //qDebug() << "CueControl::hotcueSet" << value; if (value == 0) { @@ -649,8 +702,9 @@ void CueControl::hotcueSet(HotcueControl* pControl, double value) { } QMutexLocker lock(&m_mutex); - if (!m_pLoadedTrack) + if (!m_pLoadedTrack) { return; + } int hotcue = pControl->getHotcueNumber(); // Note: the cue is just detached from the hotcue control @@ -660,35 +714,98 @@ void CueControl::hotcueSet(HotcueControl* pControl, double value) { hotcueClear(pControl, value); CuePointer pCue(m_pLoadedTrack->createAndAddCue()); - double cuePosition = getQuantizedCurrentPosition(); - pCue->setStartPosition(cuePosition); - pCue->setHotCue(hotcue); - pCue->setLabel(); - pCue->setType(mixxx::CueType::HotCue); - const ColorPalette hotcueColorPalette = - m_colorPaletteSettings.getHotcueColorPalette(); - if (getConfig()->getValue(ConfigKey("[Controls]", "auto_hotcue_colors"), false)) { - pCue->setColor(hotcueColorPalette.colorForHotcueIndex(hotcue)); - } else { - int hotcueDefaultColorIndex = m_pConfig->getValue(ConfigKey("[Controls]", "HotcueDefaultColorIndex"), -1); - if (hotcueDefaultColorIndex < 0 || hotcueDefaultColorIndex >= hotcueColorPalette.size()) { - hotcueDefaultColorIndex = hotcueColorPalette.size() - 1; // default to last color (orange) + double cueStartPosition = Cue::kNoPosition; + double cueEndPosition = Cue::kNoPosition; + mixxx::CueType cueType = mixxx::CueType::Invalid; + + bool loopEnabled = m_pLoopEnabled->get(); + if (mode == HotcueSetMode::Auto) { + mode = loopEnabled ? HotcueSetMode::Loop : HotcueSetMode::Cue; + } + + switch (mode) { + case HotcueSetMode::Cue: { + // If no loop is enabled, just store regular jump cue + cueStartPosition = getQuantizedCurrentPosition(); + cueType = mixxx::CueType::HotCue; + break; + } + case HotcueSetMode::Loop: { + if (loopEnabled) { + // If a loop is enabled, save the current loop + cueStartPosition = m_pLoopStartPosition->get(); + cueEndPosition = m_pLoopEndPosition->get(); + } else { + // If no loop is enabled, save a loop starting from the current + // position and with the current beatloop size + cueStartPosition = getQuantizedCurrentPosition(); + double beatloopSize = m_pBeatLoopSize->get(); + const mixxx::BeatsPointer pBeats = m_pLoadedTrack->getBeats(); + if (beatloopSize <= 0 || !pBeats) { + return; + } + cueEndPosition = pBeats->findNBeatsFromSample(cueStartPosition, beatloopSize); } - pCue->setColor(hotcueColorPalette.at(hotcueDefaultColorIndex)); + cueType = mixxx::CueType::Loop; + break; + } + default: + DEBUG_ASSERT(!"Invalid HotcueSetMode"); + return; } + VERIFY_OR_DEBUG_ASSERT(cueType != mixxx::CueType::Invalid) { + return; + } + + // Abort if no position has been found. + VERIFY_OR_DEBUG_ASSERT(cueStartPosition != Cue::kNoPosition && + (cueType != mixxx::CueType::Loop || + cueEndPosition != Cue::kNoPosition)) { + return; + } + + pCue->setStartPosition(cueStartPosition); + pCue->setEndPosition(cueEndPosition); + pCue->setHotCue(hotcue); + pCue->setLabel(QString()); + pCue->setType(cueType); // TODO(XXX) deal with spurious signals attachCue(pCue, pControl); + if (cueType == mixxx::CueType::Loop) { + ConfigKey autoLoopColorsKey("[Controls]", "auto_loop_colors"); + if (getConfig()->getValue(autoLoopColorsKey, false)) { + auto hotcueColorPalette = + m_colorPaletteSettings.getHotcueColorPalette(); + pCue->setColor(hotcueColorPalette.colorForHotcueIndex(hotcue)); + } else { + pCue->setColor(mixxx::PredefinedColorPalettes::kDefaultLoopColor); + } + } else { + ConfigKey autoHotcueColorsKey("[Controls]", "auto_hotcue_colors"); + if (getConfig()->getValue(autoHotcueColorsKey, false)) { + auto hotcueColorPalette = + m_colorPaletteSettings.getHotcueColorPalette(); + pCue->setColor(hotcueColorPalette.colorForHotcueIndex(hotcue)); + } else { + pCue->setColor(mixxx::PredefinedColorPalettes::kDefaultCueColor); + } + } + + if (cueType == mixxx::CueType::Loop) { + setCurrentSavedLoopControlAndActivate(pControl); + } + // If quantize is enabled and we are not playing, jump to the cue point // since it's not necessarily where we currently are. TODO(XXX) is this // potentially invalid for vinyl control? bool playing = m_pPlay->toBool(); if (!playing && m_pQuantizeEnabled->toBool()) { - lock.unlock(); // prevent deadlock. + lock.unlock(); // prevent deadlock. // Enginebuffer will quantize more exactly than we can. - seekAbs(cuePosition); + seekAbs(cueStartPosition); } } @@ -721,8 +838,9 @@ void CueControl::hotcueGotoAndStop(HotcueControl* pControl, double value) { } QMutexLocker lock(&m_mutex); - if (!m_pLoadedTrack) + if (!m_pLoadedTrack) { return; + } CuePointer pCue(pControl->getCue()); @@ -758,7 +876,7 @@ void CueControl::hotcueGotoAndPlay(HotcueControl* pControl, double value) { if (position != Cue::kNoPosition) { seekAbs(position); if (!isPlayingByPlayButton()) { - // cueGoto is processed asynchrony. + // cueGoto is processed asynchronously. // avoid a wrong cue set if seek by cueGoto is still pending m_bPreviewing = false; m_iCurrentlyPreviewingHotcues = 0; @@ -770,7 +888,102 @@ void CueControl::hotcueGotoAndPlay(HotcueControl* pControl, double value) { } } -void CueControl::hotcueActivate(HotcueControl* pControl, double value) { +void CueControl::hotcueGotoAndLoop(HotcueControl* pControl, double value) { + if (value == 0) { + return; + } + + QMutexLocker lock(&m_mutex); + if (!m_pLoadedTrack) { + return; + } + + CuePointer pCue(pControl->getCue()); + + // Need to unlock before emitting any signals to prevent deadlock. + lock.unlock(); + + if (!pCue) { + return; + } + + double startPosition = pCue->getPosition(); + if (startPosition == Cue::kNoPosition) { + return; + } + + if (pCue->getType() == mixxx::CueType::Loop) { + seekAbs(startPosition); + setCurrentSavedLoopControlAndActivate(pControl); + } else if (pCue->getType() == mixxx::CueType::HotCue) { + seekAbs(startPosition); + setBeatLoop(startPosition, true); + } else { + return; + } + + if (!isPlayingByPlayButton()) { + // cueGoto is processed asynchronously. + // avoid a wrong cue set if seek by cueGoto is still pending + m_bPreviewing = false; + m_iCurrentlyPreviewingHotcues = 0; + // don't move the cue point to the hot cue point in DENON mode + m_bypassCueSetByPlay = true; + m_pPlay->set(1.0); + } + + m_pHotcueFocus->set(pControl->getHotcueNumber()); +} + +void CueControl::hotcueCueLoop(HotcueControl* pControl, double value) { + if (value == 0) { + return; + } + + if (!m_pLoadedTrack) { + return; + } + + CuePointer pCue = pControl->getCue(); + + if (!pCue || pCue->getPosition() == Cue::kNoPosition) { + hotcueSet(pControl, value, HotcueSetMode::Cue); + pCue = pControl->getCue(); + VERIFY_OR_DEBUG_ASSERT(pCue && pCue->getPosition() != Cue::kNoPosition) { + return; + } + } + + switch (pCue->getType()) { + case mixxx::CueType::Loop: { + // The hotcue_X_cueloop CO was invoked for a saved loop, set it as + // active the first time this happens and toggle the loop_enabled state + // on subsequent invocations. + if (m_pCurrentSavedLoopControl != pControl) { + setCurrentSavedLoopControlAndActivate(pControl); + } else { + bool loopActive = pControl->getStatus() == HotcueControl::Status::Active; + setLoop(pCue->getPosition(), pCue->getEndPosition(), !loopActive); + } + } break; + case mixxx::CueType::HotCue: { + // The hotcue_X_cueloop CO was invoked for a hotcue. In that case, + // create a beatloop starting at the hotcue position. This is useful for + // mapping the CUE LOOP mode labeled on some controllers. + setCurrentSavedLoopControlAndActivate(nullptr); + double startPosition = pCue->getPosition(); + bool loopActive = m_pLoopEnabled->get() && (startPosition == m_pLoopStartPosition->get()); + setBeatLoop(startPosition, !loopActive); + break; + } + default: + return; + } + + m_pHotcueFocus->set(pControl->getHotcueNumber()); +} + +void CueControl::hotcueActivate(HotcueControl* pControl, double value, HotcueSetMode mode) { //qDebug() << "CueControl::hotcueActivate" << value; QMutexLocker lock(&m_mutex); @@ -786,10 +999,25 @@ void CueControl::hotcueActivate(HotcueControl* pControl, double value) { if (pCue) { if (value != 0) { if (pCue->getPosition() == Cue::kNoPosition) { - hotcueSet(pControl, value); + hotcueSet(pControl, value, mode); } else { if (isPlayingByPlayButton()) { - hotcueGoto(pControl, value); + switch (pCue->getType()) { + case mixxx::CueType::HotCue: + hotcueGoto(pControl, value); + break; + case mixxx::CueType::Loop: + if (m_pCurrentSavedLoopControl != pControl) { + setCurrentSavedLoopControlAndActivate(pControl); + } else { + bool loopActive = pControl->getStatus() == + HotcueControl::Status::Active; + setLoop(pCue->getPosition(), pCue->getEndPosition(), !loopActive); + } + break; + default: + DEBUG_ASSERT(!"Invalid CueType!"); + } } else { hotcueActivatePreview(pControl, value); } @@ -803,7 +1031,7 @@ void CueControl::hotcueActivate(HotcueControl* pControl, double value) { // The cue is non-existent ... if (value != 0) { // set it to the current position - hotcueSet(pControl, value); + hotcueSet(pControl, value, mode); } else if (m_iCurrentlyPreviewingHotcues) { // yet we got a release for it and are // currently previewing a hotcue. This is indicative of a corner @@ -824,12 +1052,18 @@ void CueControl::hotcueActivatePreview(HotcueControl* pControl, double value) { CuePointer pCue(pControl->getCue()); if (value != 0) { - if (pCue && pCue->getPosition() != Cue::kNoPosition) { + if (pCue && pCue->getPosition() != Cue::kNoPosition && + pCue->getType() != mixxx::CueType::Invalid) { m_iCurrentlyPreviewingHotcues++; double position = pCue->getPosition(); m_bypassCueSetByPlay = true; - pControl->setPreviewing(true); + pControl->setPreviewingType(pCue->getType()); pControl->setPreviewingPosition(position); + if (pCue->getType() == mixxx::CueType::Loop) { + setCurrentSavedLoopControlAndActivate(pControl); + } else if (pControl->getStatus() == HotcueControl::Status::Set) { + pControl->setStatus(HotcueControl::Status::Active); + } // Need to unlock before emitting any signals to prevent deadlock. lock.unlock(); @@ -840,10 +1074,11 @@ void CueControl::hotcueActivatePreview(HotcueControl* pControl, double value) { } else if (m_iCurrentlyPreviewingHotcues) { // This is a activate release and we are previewing at least one // hotcue. If this hotcue is previewing: - if (pControl->isPreviewing()) { + mixxx::CueType cueType = pControl->getPreviewingType(); + if (cueType != mixxx::CueType::Invalid) { // Mark this hotcue as not previewing. double position = pControl->getPreviewingPosition(); - pControl->setPreviewing(false); + pControl->setPreviewingType(mixxx::CueType::Invalid); pControl->setPreviewingPosition(Cue::kNoPosition); // If this is the last hotcue to leave preview. @@ -851,6 +1086,11 @@ void CueControl::hotcueActivatePreview(HotcueControl* pControl, double value) { m_pPlay->set(0.0); // Need to unlock before emitting any signals to prevent deadlock. lock.unlock(); + if (cueType == mixxx::CueType::Loop) { + m_pLoopEnabled->set(0); + } else if (pControl->getStatus() == HotcueControl::Status::Active) { + pControl->setStatus(HotcueControl::Status::Set); + } seekExact(position); } } @@ -876,10 +1116,12 @@ void CueControl::hotcueClear(HotcueControl* pControl, double value) { m_pHotcueFocus->set(Cue::kNoHotCue); } -void CueControl::hotcuePositionChanged(HotcueControl* pControl, double newPosition) { +void CueControl::hotcuePositionChanged( + HotcueControl* pControl, double newPosition) { QMutexLocker lock(&m_mutex); - if (!m_pLoadedTrack) + if (!m_pLoadedTrack) { return; + } CuePointer pCue(pControl->getCue()); if (pCue) { @@ -887,11 +1129,37 @@ void CueControl::hotcuePositionChanged(HotcueControl* pControl, double newPositi if (newPosition == Cue::kNoPosition) { detachCue(pControl); } else if (newPosition > 0 && newPosition < m_pTrackSamples->get()) { + if (pCue->getType() == mixxx::CueType::Loop && newPosition >= pCue->getEndPosition()) { + return; + } pCue->setStartPosition(newPosition); } } } +void CueControl::hotcueEndPositionChanged( + HotcueControl* pControl, double newEndPosition) { + QMutexLocker lock(&m_mutex); + if (!m_pLoadedTrack) { + return; + } + + CuePointer pCue(pControl->getCue()); + if (pCue) { + // Setting the end position of a loop cue to Cue::kNoPosition converts + // it into a regular jump cue + if (pCue->getType() == mixxx::CueType::Loop && + newEndPosition == Cue::kNoPosition) { + pCue->setType(mixxx::CueType::HotCue); + pCue->setEndPosition(Cue::kNoPosition); + } else { + if (newEndPosition > pCue->getPosition()) { + pCue->setEndPosition(newEndPosition); + } + } + } +} + void CueControl::hintReader(HintVector* pHintList) { Hint cue_hint; double cuePoint = m_pCuePoint->get(); @@ -905,7 +1173,7 @@ void CueControl::hintReader(HintVector* pHintList) { // this is called from the engine thread // it is no locking required, because m_hotcueControl is filled during the // constructor and getPosition()->get() is a ControlObject - for (const auto& pControl: m_hotcueControls) { + for (const auto& pControl : m_hotcueControls) { double position = pControl->getPosition(); if (position != Cue::kNoPosition) { cue_hint.frame = SampleUtil::floorPlayPosToFrame(position); @@ -975,7 +1243,7 @@ void CueControl::cueGotoAndPlay(double value) { QMutexLocker lock(&m_mutex); // Start playing if not already if (!isPlayingByPlayButton()) { - // cueGoto is processed asynchrony. + // cueGoto is processed asynchronously. // avoid a wrong cue set if seek by cueGoto is still pending m_bPreviewing = false; m_iCurrentlyPreviewingHotcues = 0; @@ -1030,7 +1298,8 @@ void CueControl::cueCDJ(double value) { // If play is pressed while holding cue, the deck is now playing. (Handled in playFromCuePreview().) QMutexLocker lock(&m_mutex); - const auto freely_playing = m_pPlay->toBool() && !getEngineBuffer()->getScratching(); + const auto freely_playing = + m_pPlay->toBool() && !getEngineBuffer()->getScratching(); TrackAt trackAt = getTrackAt(); if (value != 0) { @@ -1065,7 +1334,7 @@ void CueControl::cueCDJ(double value) { // If quantize is enabled, jump to the cue point since it's not // necessarily where we currently are if (m_pQuantizeEnabled->toBool()) { - lock.unlock(); // prevent deadlock. + lock.unlock(); // prevent deadlock. // Enginebuffer will quantize more exactly than we can. seekAbs(m_pCuePoint->get()); } @@ -1140,9 +1409,9 @@ void CueControl::cuePlay(double value) { // If not freely playing (i.e. stopped or platter IS being touched), press to go to cue and stop. // On release, start playing from cue point. - QMutexLocker lock(&m_mutex); - const auto freely_playing = m_pPlay->toBool() && !getEngineBuffer()->getScratching(); + const auto freely_playing = + m_pPlay->toBool() && !getEngineBuffer()->getScratching(); TrackAt trackAt = getTrackAt(); // pressed @@ -1164,7 +1433,7 @@ void CueControl::cuePlay(double value) { // If quantize is enabled, jump to the cue point since it's not // necessarily where we currently are if (m_pQuantizeEnabled->toBool()) { - lock.unlock(); // prevent deadlock. + lock.unlock(); // prevent deadlock. // Enginebuffer will quantize more exactly than we can. seekAbs(m_pCuePoint->get()); } @@ -1226,15 +1495,18 @@ void CueControl::introStartSet(double value) { double outroStart = m_pOutroStartPosition->get(); double outroEnd = m_pOutroEndPosition->get(); if (introEnd != Cue::kNoPosition && position >= introEnd) { - qWarning() << "Trying to place intro start cue on or after intro end cue."; + qWarning() + << "Trying to place intro start cue on or after intro end cue."; return; } if (outroStart != Cue::kNoPosition && position >= outroStart) { - qWarning() << "Trying to place intro start cue on or after outro start cue."; + qWarning() << "Trying to place intro start cue on or after outro start " + "cue."; return; } if (outroEnd != Cue::kNoPosition && position >= outroEnd) { - qWarning() << "Trying to place intro start cue on or after outro end cue."; + qWarning() + << "Trying to place intro start cue on or after outro end cue."; return; } @@ -1304,15 +1576,18 @@ void CueControl::introEndSet(double value) { double outroStart = m_pOutroStartPosition->get(); double outroEnd = m_pOutroEndPosition->get(); if (introStart != Cue::kNoPosition && position <= introStart) { - qWarning() << "Trying to place intro end cue on or before intro start cue."; + qWarning() << "Trying to place intro end cue on or before intro start " + "cue."; return; } if (outroStart != Cue::kNoPosition && position >= outroStart) { - qWarning() << "Trying to place intro end cue on or after outro start cue."; + qWarning() + << "Trying to place intro end cue on or after outro start cue."; return; } if (outroEnd != Cue::kNoPosition && position >= outroEnd) { - qWarning() << "Trying to place intro end cue on or after outro end cue."; + qWarning() + << "Trying to place intro end cue on or after outro end cue."; return; } @@ -1382,15 +1657,18 @@ void CueControl::outroStartSet(double value) { double introEnd = m_pIntroEndPosition->get(); double outroEnd = m_pOutroEndPosition->get(); if (introStart != Cue::kNoPosition && position <= introStart) { - qWarning() << "Trying to place outro start cue on or before intro start cue."; + qWarning() << "Trying to place outro start cue on or before intro " + "start cue."; return; } if (introEnd != Cue::kNoPosition && position <= introEnd) { - qWarning() << "Trying to place outro start cue on or before intro end cue."; + qWarning() << "Trying to place outro start cue on or before intro end " + "cue."; return; } if (outroEnd != Cue::kNoPosition && position >= outroEnd) { - qWarning() << "Trying to place outro start cue on or after outro end cue."; + qWarning() + << "Trying to place outro start cue on or after outro end cue."; return; } @@ -1460,15 +1738,18 @@ void CueControl::outroEndSet(double value) { double introEnd = m_pIntroEndPosition->get(); double outroStart = m_pOutroStartPosition->get(); if (introStart != Cue::kNoPosition && position <= introStart) { - qWarning() << "Trying to place outro end cue on or before intro start cue."; + qWarning() << "Trying to place outro end cue on or before intro start " + "cue."; return; } if (introEnd != Cue::kNoPosition && position <= introEnd) { - qWarning() << "Trying to place outro end cue on or before intro end cue."; + qWarning() + << "Trying to place outro end cue on or before intro end cue."; return; } if (outroStart != Cue::kNoPosition && position <= outroStart) { - qWarning() << "Trying to place outro end cue on or before outro start cue."; + qWarning() << "Trying to place outro end cue on or before outro start " + "cue."; return; } @@ -1523,15 +1804,14 @@ void CueControl::outroEndActivate(double value) { } } -bool CueControl::updateIndicatorsAndModifyPlay(bool newPlay, bool playPossible) { +bool CueControl::updateIndicatorsAndModifyPlay( + bool newPlay, bool playPossible) { //qDebug() << "updateIndicatorsAndModifyPlay" << newPlay << playPossible // << m_iCurrentlyPreviewingHotcues << m_bPreviewing; QMutexLocker lock(&m_mutex); CueMode cueMode = static_cast(static_cast(m_pCueMode->get())); - if ((cueMode == CueMode::Denon || cueMode == CueMode::Numark) && - newPlay && playPossible && - !m_pPlay->toBool() && - !m_bypassCueSetByPlay) { + if ((cueMode == CueMode::Denon || cueMode == CueMode::Numark) && newPlay && + playPossible && !m_pPlay->toBool() && !m_bypassCueSetByPlay) { // in Denon mode each play from pause moves the cue point // if not previewing cueSet(1.0); @@ -1571,9 +1851,11 @@ bool CueControl::updateIndicatorsAndModifyPlay(bool newPlay, bool playPossible) m_pPlayIndicator->setBlinkValue(ControlIndicator::OFF); } else { // Flashing indicates that a following play would move cue point - m_pPlayIndicator->setBlinkValue(ControlIndicator::RATIO1TO1_500MS); + m_pPlayIndicator->setBlinkValue( + ControlIndicator::RATIO1TO1_500MS); } - } else if (cueMode == CueMode::Mixxx || cueMode == CueMode::MixxxNoBlinking || + } else if (cueMode == CueMode::Mixxx || + cueMode == CueMode::MixxxNoBlinking || cueMode == CueMode::Numark) { m_pPlayIndicator->setBlinkValue(ControlIndicator::OFF); } else { @@ -1587,12 +1869,14 @@ bool CueControl::updateIndicatorsAndModifyPlay(bool newPlay, bool playPossible) if (newPlay == 0.0 && trackAt == TrackAt::ElseWhere) { if (cueMode == CueMode::Mixxx) { // in Mixxx mode Cue Button is flashing slow if CUE will move Cue point - m_pCueIndicator->setBlinkValue(ControlIndicator::RATIO1TO1_500MS); + m_pCueIndicator->setBlinkValue( + ControlIndicator::RATIO1TO1_500MS); } else if (cueMode == CueMode::MixxxNoBlinking) { m_pCueIndicator->setBlinkValue(ControlIndicator::OFF); } else { // in Pioneer mode Cue Button is flashing fast if CUE will move Cue point - m_pCueIndicator->setBlinkValue(ControlIndicator::RATIO1TO1_250MS); + m_pCueIndicator->setBlinkValue( + ControlIndicator::RATIO1TO1_250MS); } } else { m_pCueIndicator->setBlinkValue(ControlIndicator::OFF); @@ -1626,7 +1910,8 @@ void CueControl::updateIndicators() { if (!playing) { if (trackAt != TrackAt::End && cueMode != CUE_MODE_NUMARK) { // Play will move cue point - m_pPlayIndicator->setBlinkValue(ControlIndicator::RATIO1TO1_500MS); + m_pPlayIndicator->setBlinkValue( + ControlIndicator::RATIO1TO1_500MS); } else { // At track end m_pPlayIndicator->setBlinkValue(ControlIndicator::OFF); @@ -1637,18 +1922,21 @@ void CueControl::updateIndicators() { // Here we have CUE_MODE_PIONEER or CUE_MODE_MIXXX // default to Pioneer mode if (!m_bPreviewing) { - const auto freely_playing = m_pPlay->toBool() && !getEngineBuffer()->getScratching(); + const auto freely_playing = + m_pPlay->toBool() && !getEngineBuffer()->getScratching(); if (!freely_playing) { switch (trackAt) { case TrackAt::ElseWhere: if (cueMode == CUE_MODE_MIXXX) { // in Mixxx mode Cue Button is flashing slow if CUE will move Cue point - m_pCueIndicator->setBlinkValue(ControlIndicator::RATIO1TO1_500MS); + m_pCueIndicator->setBlinkValue( + ControlIndicator::RATIO1TO1_500MS); } else if (cueMode == CUE_MODE_MIXXX_NO_BLINK) { m_pCueIndicator->setBlinkValue(ControlIndicator::OFF); } else { // in Pioneer mode Cue Button is flashing fast if CUE will move Cue point - m_pCueIndicator->setBlinkValue(ControlIndicator::RATIO1TO1_250MS); + m_pCueIndicator->setBlinkValue( + ControlIndicator::RATIO1TO1_250MS); } break; case TrackAt::End: @@ -1743,17 +2031,19 @@ double CueControl::quantizeCuePoint(double cuePos) { } bool CueControl::isTrackAtIntroCue() { - return (fabs(getSampleOfTrack().current - m_pIntroStartPosition->get()) < 1.0f); + return (fabs(getSampleOfTrack().current - m_pIntroStartPosition->get()) < + 1.0f); } bool CueControl::isPlayingByPlayButton() { - return m_pPlay->toBool() && - !m_iCurrentlyPreviewingHotcues && !m_bPreviewing; + return m_pPlay->toBool() && !m_iCurrentlyPreviewingHotcues && + !m_bPreviewing; } SeekOnLoadMode CueControl::getSeekOnLoadPreference() { - int configValue = getConfig()->getValue(ConfigKey("[Controls]", "CueRecall"), - static_cast(SeekOnLoadMode::IntroStart)); + int configValue = + getConfig()->getValue(ConfigKey("[Controls]", "CueRecall"), + static_cast(SeekOnLoadMode::IntroStart)); return static_cast(configValue); } @@ -1815,11 +2105,99 @@ void CueControl::hotcueFocusColorNext(double value) { pCue->setColor(colorPalette.nextColor(*color)); } +void CueControl::setCurrentSavedLoopControlAndActivate(HotcueControl* pControl) { + if (m_pCurrentSavedLoopControl && m_pCurrentSavedLoopControl != pControl) { + // Disable previous saved loop + DEBUG_ASSERT(m_pCurrentSavedLoopControl->getStatus() != HotcueControl::Status::Empty); + m_pCurrentSavedLoopControl->setStatus(HotcueControl::Status::Set); + m_pCurrentSavedLoopControl = nullptr; + } + + if (!pControl) { + return; + } + + if (!m_pLoadedTrack) { + return; + } + + CuePointer pCue(pControl->getCue()); + + VERIFY_OR_DEBUG_ASSERT(pCue && + pCue->getType() == mixxx::CueType::Loop && + pCue->getEndPosition() != Cue::kNoPosition) { + return; + } + + // Set new control as active + m_pCurrentSavedLoopControl = pControl; + setLoop(pCue->getPosition(), pCue->getEndPosition(), true); + pControl->setStatus(HotcueControl::Status::Active); +} + +void CueControl::slotLoopReset() { + setCurrentSavedLoopControlAndActivate(nullptr); +} + +void CueControl::slotLoopEnabledChanged(bool enabled) { + if (!m_pCurrentSavedLoopControl) { + return; + } + + DEBUG_ASSERT(m_pCurrentSavedLoopControl->getStatus() != HotcueControl::Status::Empty); + DEBUG_ASSERT( + m_pCurrentSavedLoopControl->getCue() && + m_pCurrentSavedLoopControl->getCue()->getPosition() == + m_pLoopStartPosition->get()); + DEBUG_ASSERT( + m_pCurrentSavedLoopControl->getCue() && + m_pCurrentSavedLoopControl->getCue()->getEndPosition() == + m_pLoopEndPosition->get()); + + if (enabled) { + m_pCurrentSavedLoopControl->setStatus(HotcueControl::Status::Active); + } else { + m_pCurrentSavedLoopControl->setStatus(HotcueControl::Status::Set); + } +} + +void CueControl::slotLoopUpdated(double startPosition, double endPosition) { + if (!m_pCurrentSavedLoopControl) { + return; + } + + if (!m_pLoadedTrack) { + return; + } + + if (m_pCurrentSavedLoopControl->getStatus() != HotcueControl::Status::Active) { + slotLoopReset(); + return; + } + + CuePointer pCue(m_pCurrentSavedLoopControl->getCue()); + + VERIFY_OR_DEBUG_ASSERT(pCue->getType() == mixxx::CueType::Loop) { + setCurrentSavedLoopControlAndActivate(nullptr); + return; + } + + DEBUG_ASSERT(startPosition != Cue::kNoPosition); + DEBUG_ASSERT(endPosition != Cue::kNoPosition); + DEBUG_ASSERT(startPosition < endPosition); + + DEBUG_ASSERT(m_pCurrentSavedLoopControl->getStatus() == HotcueControl::Status::Active); + pCue->setStartPosition(startPosition); + pCue->setEndPosition(endPosition); + DEBUG_ASSERT(m_pCurrentSavedLoopControl->getStatus() == HotcueControl::Status::Active); +} + ConfigKey HotcueControl::keyForControl(int hotcue, const char* name) { ConfigKey key; key.group = m_group; // Add one to hotcue so that we don't have a hotcue_0 - key.item = QLatin1String("hotcue_") % QString::number(hotcue+1) % "_" % name; + key.item = + QLatin1String("hotcue_") % QString::number(hotcue + 1) % "_" % name; return key; } @@ -1827,16 +2205,33 @@ HotcueControl::HotcueControl(QString group, int i) : m_group(group), m_iHotcueNumber(i), m_pCue(NULL), - m_bPreviewing(false), + m_previewingType(mixxx::CueType::Invalid), m_previewingPosition(-1) { m_hotcuePosition = new ControlObject(keyForControl(i, "position")); - connect(m_hotcuePosition, &ControlObject::valueChanged, - this, &HotcueControl::slotHotcuePositionChanged, + connect(m_hotcuePosition, + &ControlObject::valueChanged, + this, + &HotcueControl::slotHotcuePositionChanged, Qt::DirectConnection); m_hotcuePosition->set(Cue::kNoPosition); - m_hotcueEnabled = new ControlObject(keyForControl(i, "enabled")); - m_hotcueEnabled->setReadOnly(); + m_hotcueEndPosition = new ControlObject(keyForControl(i, "endposition")); + connect(m_hotcueEndPosition, + &ControlObject::valueChanged, + this, + &HotcueControl::slotHotcueEndPositionChanged, + Qt::DirectConnection); + m_hotcueEndPosition->set(Cue::kNoPosition); + + m_pHotcueStatus = new ControlObject(keyForControl(i, "status")); + m_pHotcueStatus->setReadOnly(); + + // Add an alias for the legacy hotcue_X_enabled CO + ControlDoublePrivate::insertAlias(keyForControl(i, "enabled"), + keyForControl(i, "status")); + + m_hotcueType = new ControlObject(keyForControl(i, "type")); + m_hotcueType->setReadOnly(); // The rgba value of the color assigned to this color. m_hotcueColor = new ControlObject(keyForControl(i, "color")); @@ -1855,6 +2250,20 @@ HotcueControl::HotcueControl(QString group, int i) this, &HotcueControl::slotHotcueSet, Qt::DirectConnection); + m_hotcueSetCue = new ControlPushButton(keyForControl(i, "setcue")); + connect(m_hotcueSetCue, + &ControlObject::valueChanged, + this, + &HotcueControl::slotHotcueSetCue, + Qt::DirectConnection); + + m_hotcueSetLoop = new ControlPushButton(keyForControl(i, "setloop")); + connect(m_hotcueSetLoop, + &ControlObject::valueChanged, + this, + &HotcueControl::slotHotcueSetLoop, + Qt::DirectConnection); + m_hotcueGoto = new ControlPushButton(keyForControl(i, "goto")); connect(m_hotcueGoto, &ControlObject::valueChanged, this, &HotcueControl::slotHotcueGoto, @@ -1870,11 +2279,41 @@ HotcueControl::HotcueControl(QString group, int i) this, &HotcueControl::slotHotcueGotoAndStop, Qt::DirectConnection); + m_hotcueGotoAndLoop = new ControlPushButton(keyForControl(i, "gotoandloop")); + connect(m_hotcueGotoAndLoop, + &ControlObject::valueChanged, + this, + &HotcueControl::slotHotcueGotoAndLoop, + Qt::DirectConnection); + + // Enable/disable the loop associated with this hotcue (either a saved loop + // or a beatloop from the hotcue position if this is a regular hotcue). + m_hotcueCueLoop = new ControlPushButton(keyForControl(i, "cueloop")); + connect(m_hotcueCueLoop, + &ControlObject::valueChanged, + this, + &HotcueControl::slotHotcueCueLoop, + Qt::DirectConnection); + m_hotcueActivate = new ControlPushButton(keyForControl(i, "activate")); connect(m_hotcueActivate, &ControlObject::valueChanged, this, &HotcueControl::slotHotcueActivate, Qt::DirectConnection); + m_hotcueActivateCue = new ControlPushButton(keyForControl(i, "activatecue")); + connect(m_hotcueActivateCue, + &ControlObject::valueChanged, + this, + &HotcueControl::slotHotcueActivateCue, + Qt::DirectConnection); + + m_hotcueActivateLoop = new ControlPushButton(keyForControl(i, "activateloop")); + connect(m_hotcueActivateLoop, + &ControlObject::valueChanged, + this, + &HotcueControl::slotHotcueActivateLoop, + Qt::DirectConnection); + m_hotcueActivatePreview = new ControlPushButton(keyForControl(i, "activate_preview")); connect(m_hotcueActivatePreview, &ControlObject::valueChanged, this, &HotcueControl::slotHotcueActivatePreview, @@ -1888,19 +2327,35 @@ HotcueControl::HotcueControl(QString group, int i) HotcueControl::~HotcueControl() { delete m_hotcuePosition; - delete m_hotcueEnabled; + delete m_hotcueEndPosition; + delete m_pHotcueStatus; + delete m_hotcueType; delete m_hotcueColor; delete m_hotcueSet; + delete m_hotcueSetCue; + delete m_hotcueSetLoop; delete m_hotcueGoto; delete m_hotcueGotoAndPlay; delete m_hotcueGotoAndStop; + delete m_hotcueGotoAndLoop; + delete m_hotcueCueLoop; delete m_hotcueActivate; + delete m_hotcueActivateCue; + delete m_hotcueActivateLoop; delete m_hotcueActivatePreview; delete m_hotcueClear; } void HotcueControl::slotHotcueSet(double v) { - emit hotcueSet(this, v); + emit hotcueSet(this, v, HotcueSetMode::Auto); +} + +void HotcueControl::slotHotcueSetCue(double v) { + emit hotcueSet(this, v, HotcueSetMode::Cue); +} + +void HotcueControl::slotHotcueSetLoop(double v) { + emit hotcueSet(this, v, HotcueSetMode::Loop); } void HotcueControl::slotHotcueGoto(double v) { @@ -1915,8 +2370,24 @@ void HotcueControl::slotHotcueGotoAndStop(double v) { emit hotcueGotoAndStop(this, v); } +void HotcueControl::slotHotcueGotoAndLoop(double v) { + emit hotcueGotoAndLoop(this, v); +} + +void HotcueControl::slotHotcueCueLoop(double v) { + emit hotcueCueLoop(this, v); +} + void HotcueControl::slotHotcueActivate(double v) { - emit hotcueActivate(this, v); + emit hotcueActivate(this, v, HotcueSetMode::Auto); +} + +void HotcueControl::slotHotcueActivateCue(double v) { + emit hotcueActivate(this, v, HotcueSetMode::Cue); +} + +void HotcueControl::slotHotcueActivateLoop(double v) { + emit hotcueActivate(this, v, HotcueSetMode::Loop); } void HotcueControl::slotHotcueActivatePreview(double v) { @@ -1928,10 +2399,14 @@ void HotcueControl::slotHotcueClear(double v) { } void HotcueControl::slotHotcuePositionChanged(double newPosition) { - m_hotcueEnabled->forceSet(newPosition == Cue::kNoPosition ? 0.0 : 1.0); + m_pHotcueStatus->forceSet(newPosition == Cue::kNoPosition ? 0.0 : 1.0); emit hotcuePositionChanged(this, newPosition); } +void HotcueControl::slotHotcueEndPositionChanged(double newEndPosition) { + emit hotcueEndPositionChanged(this, newEndPosition); +} + void HotcueControl::slotHotcueColorChangeRequest(double color) { if (color < 0 || color > 0xFFFFFF) { qWarning() << "slotHotcueColorChanged got invalid value:" << color; @@ -1958,9 +2433,18 @@ double HotcueControl::getPosition() const { return m_hotcuePosition->get(); } +double HotcueControl::getEndPosition() const { + return m_hotcueEndPosition->get(); +} + void HotcueControl::setCue(CuePointer pCue) { setPosition(pCue->getPosition()); + setEndPosition(pCue->getEndPosition()); setColor(pCue->getColor()); + setStatus((pCue->getType() == mixxx::CueType::Invalid) + ? HotcueControl::Status::Empty + : HotcueControl::Status::Set); + setType(pCue->getType()); // set pCue only if all other data is in place // because we have a null check for valid data else where in the code m_pCue = pCue; @@ -1979,9 +2463,29 @@ void HotcueControl::resetCue() { // in the code m_pCue.reset(); setPosition(Cue::kNoPosition); + setEndPosition(Cue::kNoPosition); + setType(mixxx::CueType::Invalid); + setStatus(Status::Empty); } void HotcueControl::setPosition(double position) { m_hotcuePosition->set(position); - m_hotcueEnabled->forceSet(position == Cue::kNoPosition ? 0.0 : 1.0); +} + +void HotcueControl::setEndPosition(double endPosition) { + m_hotcueEndPosition->set(endPosition); +} + +void HotcueControl::setType(mixxx::CueType type) { + m_hotcueType->forceSet(static_cast(type)); +} + +void HotcueControl::setStatus(HotcueControl::Status status) { + m_pHotcueStatus->forceSet(static_cast(status)); +} + +HotcueControl::Status HotcueControl::getStatus() const { + // Cast to int before casting to the int-based enum class because MSVC will + // throw a hissy fit otherwise. + return static_cast(static_cast(m_pHotcueStatus->get())); } diff --git a/src/engine/controls/cuecontrol.h b/src/engine/controls/cuecontrol.h index 6df689e4d0fb..3cede9511fdd 100644 --- a/src/engine/controls/cuecontrol.h +++ b/src/engine/controls/cuecontrol.h @@ -13,6 +13,7 @@ #include "preferences/usersettings.h" #include "track/cue.h" #include "track/track_decl.h" +#include "util/parented_ptr.h" #define NUM_HOT_CUES 37 @@ -36,60 +37,112 @@ enum class SeekOnLoadMode { IntroStart = 3, // Use intro start cue point }; +/// Used for requesting a specific hotcue type when activating/setting a +/// hotcue. Auto will make CueControl determine the type automatically (i.e. +/// create a loop cue if a loop is set, and a regular cue in all other cases). +enum class HotcueSetMode { + Auto = 0, + Cue = 1, + Loop = 2, +}; + inline SeekOnLoadMode seekOnLoadModeFromDouble(double value) { return static_cast(int(value)); } +/// A `HotcueControl` represents a hotcue slot. It can either be empty or have +/// a (hot-)cue attached to it. +/// +/// TODO(XXX): This class should be moved into a separate file. class HotcueControl : public QObject { Q_OBJECT public: + /// Describes the current status of the hotcue + enum class Status { + /// Hotuce not set + Empty = 0, + /// Hotcue is set and can be used + Set = 1, + /// Hotcue is currently active (this only applies to Saved Loop cues + /// while their loop is enabled). This status can be used by skins or + /// controller mappings to highlight a the cue control that has saved the current loop, + /// because resizing or moving the loop will make persistent changes to + /// the cue. + Active = 2, + }; + HotcueControl(QString group, int hotcueNumber); ~HotcueControl() override; - inline int getHotcueNumber() { return m_iHotcueNumber; } - inline CuePointer getCue() { return m_pCue; } - double getPosition() const; + int getHotcueNumber() const { + return m_iHotcueNumber; + } + + CuePointer getCue() const { + return m_pCue; + } void setCue(CuePointer pCue); void resetCue(); + + double getPosition() const; void setPosition(double position); + + double getEndPosition() const; + void setEndPosition(double endPosition); + + void setType(mixxx::CueType type); + + void setStatus(HotcueControl::Status status); + HotcueControl::Status getStatus() const; + void setColor(mixxx::RgbColor::optional_t newColor); mixxx::RgbColor::optional_t getColor() const; // Used for caching the preview state of this hotcue control. - inline bool isPreviewing() { - return m_bPreviewing; + mixxx::CueType getPreviewingType() const { + return m_previewingType; } - inline void setPreviewing(bool bPreviewing) { - m_bPreviewing = bPreviewing; + void setPreviewingType(mixxx::CueType type) { + m_previewingType = type; } - inline double getPreviewingPosition() { + double getPreviewingPosition() const { return m_previewingPosition; } - inline void setPreviewingPosition(double position) { + void setPreviewingPosition(double position) { m_previewingPosition = position; } private slots: void slotHotcueSet(double v); + void slotHotcueSetCue(double v); + void slotHotcueSetLoop(double v); void slotHotcueGoto(double v); void slotHotcueGotoAndPlay(double v); void slotHotcueGotoAndStop(double v); + void slotHotcueGotoAndLoop(double v); + void slotHotcueCueLoop(double v); void slotHotcueActivate(double v); + void slotHotcueActivateCue(double v); + void slotHotcueActivateLoop(double v); void slotHotcueActivatePreview(double v); void slotHotcueClear(double v); + void slotHotcueEndPositionChanged(double newPosition); void slotHotcuePositionChanged(double newPosition); void slotHotcueColorChangeRequest(double newColor); void slotHotcueColorChanged(double newColor); signals: - void hotcueSet(HotcueControl* pHotcue, double v); + void hotcueSet(HotcueControl* pHotcue, double v, HotcueSetMode mode); void hotcueGoto(HotcueControl* pHotcue, double v); void hotcueGotoAndPlay(HotcueControl* pHotcue, double v); void hotcueGotoAndStop(HotcueControl* pHotcue, double v); - void hotcueActivate(HotcueControl* pHotcue, double v); + void hotcueGotoAndLoop(HotcueControl* pHotcue, double v); + void hotcueCueLoop(HotcueControl* pHotcue, double v); + void hotcueActivate(HotcueControl* pHotcue, double v, HotcueSetMode mode); void hotcueActivatePreview(HotcueControl* pHotcue, double v); void hotcueClear(HotcueControl* pHotcue, double v); void hotcuePositionChanged(HotcueControl* pHotcue, double newPosition); + void hotcueEndPositionChanged(HotcueControl* pHotcue, double newEndPosition); void hotcueColorChanged(HotcueControl* pHotcue, double newColor); void hotcuePlay(double v); @@ -102,18 +155,26 @@ class HotcueControl : public QObject { // Hotcue state controls ControlObject* m_hotcuePosition; - ControlObject* m_hotcueEnabled; + ControlObject* m_hotcueEndPosition; + ControlObject* m_pHotcueStatus; + ControlObject* m_hotcueType; ControlObject* m_hotcueColor; // Hotcue button controls ControlObject* m_hotcueSet; + ControlObject* m_hotcueSetCue; + ControlObject* m_hotcueSetLoop; ControlObject* m_hotcueGoto; ControlObject* m_hotcueGotoAndPlay; ControlObject* m_hotcueGotoAndStop; + ControlObject* m_hotcueGotoAndLoop; + ControlObject* m_hotcueCueLoop; ControlObject* m_hotcueActivate; + ControlObject* m_hotcueActivateCue; + ControlObject* m_hotcueActivateLoop; ControlObject* m_hotcueActivatePreview; ControlObject* m_hotcueClear; - bool m_bPreviewing; + mixxx::CueType m_previewingType; double m_previewingPosition; }; @@ -135,20 +196,28 @@ class CueControl : public EngineControl { void trackLoaded(TrackPointer pNewTrack) override; void trackBeatsUpdated(mixxx::BeatsPointer pBeats) override; + public slots: + void slotLoopReset(); + void slotLoopEnabledChanged(bool enabled); + void slotLoopUpdated(double startPosition, double endPosition); + private slots: void quantizeChanged(double v); void cueUpdated(); void trackAnalyzed(); void trackCuesUpdated(); - void hotcueSet(HotcueControl* pControl, double v); + void hotcueSet(HotcueControl* pControl, double v, HotcueSetMode mode); void hotcueGoto(HotcueControl* pControl, double v); void hotcueGotoAndPlay(HotcueControl* pControl, double v); void hotcueGotoAndStop(HotcueControl* pControl, double v); - void hotcueActivate(HotcueControl* pControl, double v); + void hotcueGotoAndLoop(HotcueControl* pControl, double v); + void hotcueCueLoop(HotcueControl* pControl, double v); + void hotcueActivate(HotcueControl* pControl, double v, HotcueSetMode mode); void hotcueActivatePreview(HotcueControl* pControl, double v); void hotcueClear(HotcueControl* pControl, double v); void hotcuePositionChanged(HotcueControl* pControl, double newPosition); + void hotcueEndPositionChanged(HotcueControl* pControl, double newEndPosition); void hotcueFocusColorNext(double v); void hotcueFocusColorPrev(double v); @@ -190,6 +259,7 @@ class CueControl : public EngineControl { void createControls(); void attachCue(CuePointer pCue, HotcueControl* pControl); void detachCue(HotcueControl* pControl); + void setCurrentSavedLoopControlAndActivate(HotcueControl* pControl); void loadCuesFromTrack(); double quantizeCuePoint(double position); double getQuantizedCurrentPosition(); @@ -204,6 +274,11 @@ class CueControl : public EngineControl { int m_iCurrentlyPreviewingHotcues; ControlObject* m_pQuantizeEnabled; ControlObject* m_pClosestBeat; + parented_ptr m_pLoopStartPosition; + parented_ptr m_pLoopEndPosition; + parented_ptr m_pLoopEnabled; + parented_ptr m_pBeatLoopActivate; + parented_ptr m_pBeatLoopSize; bool m_bypassCueSetByPlay; ControlValueAtomic m_usedSeekOnLoadPosition; @@ -258,11 +333,25 @@ class CueControl : public EngineControl { ControlObject* m_pHotcueFocusColorPrev; TrackPointer m_pLoadedTrack; // is written from an engine worker thread + HotcueControl* m_pCurrentSavedLoopControl; // Tells us which controls map to which hotcue QMap m_controlMap; + // TODO(daschuer): It looks like the whole m_mutex is broken. Originally it + // ensured that the main cue really belongs to the loaded track. Now that + // we have hot cues that are altered outsite this guard this guarantee has + // become void. + // + // We have multiple cases where it locks m_pLoadedTrack and + // pControl->getCue(). This guards the hotcueClear() that could detach the + // cue call, but doesn't protect from cue changes via loadCuesFromTrack() + // which is called outside the mutex lock. + // + // We need to repair this. QMutex m_mutex; + + friend class HotcueControlTest; }; diff --git a/src/engine/controls/enginecontrol.cpp b/src/engine/controls/enginecontrol.cpp index a5d0a0555f27..51a6fab56f33 100644 --- a/src/engine/controls/enginecontrol.cpp +++ b/src/engine/controls/enginecontrol.cpp @@ -71,6 +71,18 @@ EngineBuffer* EngineControl::getEngineBuffer() { return m_pEngineBuffer; } +void EngineControl::setBeatLoop(double startPosition, bool enabled) { + if (m_pEngineBuffer) { + return m_pEngineBuffer->setBeatLoop(startPosition, enabled); + } +} + +void EngineControl::setLoop(double startPosition, double endPosition, bool enabled) { + if (m_pEngineBuffer) { + return m_pEngineBuffer->setLoop(startPosition, endPosition, enabled); + } +} + void EngineControl::seekAbs(double samplePosition) { if (m_pEngineBuffer) { m_pEngineBuffer->slotControlSeekAbs(samplePosition); diff --git a/src/engine/controls/enginecontrol.h b/src/engine/controls/enginecontrol.h index d8c4de38b179..ca98ca8286df 100644 --- a/src/engine/controls/enginecontrol.h +++ b/src/engine/controls/enginecontrol.h @@ -61,6 +61,9 @@ class EngineControl : public QObject { const double dTotalSamples, const double dTrackSampleRate); QString getGroup() const; + void setBeatLoop(double startPosition, bool enabled); + void setLoop(double startPosition, double endPosition, bool enabled); + // Called to collect player features for effects processing. virtual void collectFeatureState(GroupFeatureState* pGroupFeatures) const { Q_UNUSED(pGroupFeatures); diff --git a/src/engine/controls/loopingcontrol.cpp b/src/engine/controls/loopingcontrol.cpp index a7885325d5b2..4a206f9d7497 100644 --- a/src/engine/controls/loopingcontrol.cpp +++ b/src/engine/controls/loopingcontrol.cpp @@ -47,7 +47,7 @@ LoopingControl::LoopingControl(QString group, m_bAdjustingLoopInOld(false), m_bAdjustingLoopOutOld(false), m_bLoopOutPressedWhileLoopDisabled(false) { - m_oldLoopSamples = { kNoTrigger, kNoTrigger, false }; + m_oldLoopSamples = {kNoTrigger, kNoTrigger, LoopSeekMode::MovedOut}; m_loopSamples.setValue(m_oldLoopSamples); m_currentSample.setValue(0.0); m_pActiveBeatLoop = NULL; @@ -96,6 +96,9 @@ LoopingControl::LoopingControl(QString group, m_pCOLoopEnabled = new ControlObject(ConfigKey(group, "loop_enabled")); m_pCOLoopEnabled->set(0.0); + m_pCOLoopEnabled->connectValueChangeRequest(this, + &LoopingControl::slotLoopEnabledValueChangeRequest, + Qt::DirectConnection); m_pCOLoopStartPosition = new ControlObject(ConfigKey(group, "loop_start_position")); @@ -277,9 +280,12 @@ void LoopingControl::slotLoopScale(double scaleFactor) { } // Reseek if the loop shrank out from under the playposition. - loopSamples.seek = (m_bLoopingEnabled && scaleFactor < 1.0); + loopSamples.seekMode = (m_bLoopingEnabled && scaleFactor < 1.0) + ? LoopSeekMode::Changed + : LoopSeekMode::MovedOut; m_loopSamples.setValue(loopSamples); + emit loopUpdated(loopSamples.start, loopSamples.end); // Update CO for loop end marker m_pCOLoopEndPosition->set(loopSamples.end); @@ -322,7 +328,7 @@ void LoopingControl::process(const double dRate, if (loopSamples.start != m_oldLoopSamples.start || loopSamples.end != m_oldLoopSamples.end) { // bool seek is only valid after the loop has changed - if (loopSamples.seek) { + if (loopSamples.seekMode == LoopSeekMode::Changed) { // here the loop has changed and the play position // should be moved with it double target = seekInsideAdjustedLoop(currentSample, @@ -387,12 +393,16 @@ double LoopingControl::nextTrigger(bool reverse, if (loopSamples.start != m_oldLoopSamples.start || loopSamples.end != m_oldLoopSamples.end) { // bool seek is only valid after the loop has changed - if (loopSamples.seek) { + switch (loopSamples.seekMode) { + case LoopSeekMode::Changed: // here the loop has changed and the play position // should be moved with it *pTarget = seekInsideAdjustedLoop(currentSample, - m_oldLoopSamples.start, loopSamples.start, loopSamples.end); - } else { + m_oldLoopSamples.start, + loopSamples.start, + loopSamples.end); + break; + case LoopSeekMode::MovedOut: { bool movedOut = false; // Check if we have moved out of the loop, before we could enable it if (reverse) { @@ -406,8 +416,17 @@ double LoopingControl::nextTrigger(bool reverse, } if (movedOut) { *pTarget = seekInsideAdjustedLoop(currentSample, - loopSamples.start, loopSamples.start, loopSamples.end); + loopSamples.start, + loopSamples.start, + loopSamples.end); } + break; + } + case LoopSeekMode::None: + // Nothing to do here. This is used for enabling saved loops + // which we want to do without jumping to the loop start + // position. + break; } m_oldLoopSamples = loopSamples; if (*pTarget != kNoTrigger) { @@ -509,6 +528,58 @@ double LoopingControl::getSyncPositionInsideLoop(double dRequestedPlaypos, doubl return dSyncedPlayPos; } +void LoopingControl::setBeatLoop(double startPosition, bool enabled) { + VERIFY_OR_DEBUG_ASSERT(startPosition != Cue::kNoPosition) { + return; + } + + mixxx::BeatsPointer pBeats = m_pBeats; + if (!pBeats) { + return; + } + + double beatloopSize = m_pCOBeatLoopSize->get(); + + // TODO(XXX): This is not realtime safe. See this Zulip discussion for details: + // https://mixxx.zulipchat.com/#narrow/stream/109171-development/topic/getting.20locks.20out.20of.20Beats + double endPosition = pBeats->findNBeatsFromSample(startPosition, beatloopSize); + + setLoop(startPosition, endPosition, enabled); +} + +void LoopingControl::setLoop(double startPosition, double endPosition, bool enabled) { + VERIFY_OR_DEBUG_ASSERT(startPosition != Cue::kNoPosition && + endPosition != Cue::kNoPosition && startPosition < endPosition) { + return; + } + + LoopSamples loopSamples = m_loopSamples.getValue(); + if (loopSamples.start != startPosition || loopSamples.end != endPosition) { + // Copy saved loop parameters to active loop + loopSamples.start = startPosition; + loopSamples.end = endPosition; + loopSamples.seekMode = LoopSeekMode::None; + clearActiveBeatLoop(); + m_loopSamples.setValue(loopSamples); + m_pCOLoopStartPosition->set(loopSamples.start); + m_pCOLoopEndPosition->set(loopSamples.end); + } + setLoopingEnabled(enabled); + + // Seek back to loop in position if we're already behind the loop end. + // + // TODO(Holzhaus): This needs to be reverted as soon as GUI controls for + // controlling saved loop behaviour are in place, because this change makes + // saved loops very risky to use and might potentially mess up your mix. + // See https://github.com/mixxxdj/mixxx/pull/2194#issuecomment-721847833 + // for details. + if (enabled && m_currentSample.getValue() > loopSamples.end) { + slotLoopInGoto(1); + } + + m_pCOBeatLoopSize->setAndConfirm(findBeatloopSizeForLoop(startPosition, endPosition)); +} + void LoopingControl::setLoopInToCurrentPosition() { // set loop-in position mixxx::BeatsPointer pBeats = m_pBeats; @@ -562,9 +633,9 @@ void LoopingControl::setLoopInToCurrentPosition() { if (loopSamples.start != kNoTrigger && loopSamples.end != kNoTrigger) { setLoopingEnabled(true); - loopSamples.seek = true; + loopSamples.seekMode = LoopSeekMode::Changed; } else { - loopSamples.seek = false; + loopSamples.seekMode = LoopSeekMode::MovedOut; } if (m_pQuantizeEnabled->toBool() @@ -596,8 +667,15 @@ void LoopingControl::slotLoopIn(double pressed) { } else { setLoopInToCurrentPosition(); m_bAdjustingLoopIn = false; + LoopSamples loopSamples = m_loopSamples.getValue(); + if (loopSamples.start < loopSamples.end) { + emit loopUpdated(loopSamples.start, loopSamples.end); + } else { + emit loopReset(); + } } } else { + emit loopReset(); if (pressed > 0.0) { setLoopInToCurrentPosition(); } @@ -662,9 +740,9 @@ void LoopingControl::setLoopOutToCurrentPosition() { if (loopSamples.start != kNoTrigger && loopSamples.end != kNoTrigger) { setLoopingEnabled(true); - loopSamples.seek = true; + loopSamples.seekMode = LoopSeekMode::Changed; } else { - loopSamples.seek = false; + loopSamples.seekMode = LoopSeekMode::MovedOut; } if (m_pQuantizeEnabled->toBool() && pBeats) { @@ -701,12 +779,19 @@ void LoopingControl::slotLoopOut(double pressed) { // loop out point when the button is released. if (!m_bLoopOutPressedWhileLoopDisabled) { setLoopOutToCurrentPosition(); + LoopSamples loopSamples = m_loopSamples.getValue(); + if (loopSamples.start < loopSamples.end) { + emit loopUpdated(loopSamples.start, loopSamples.end); + } else { + emit loopReset(); + } m_bAdjustingLoopOut = false; } else { m_bLoopOutPressedWhileLoopDisabled = false; } } } else { + emit loopReset(); if (pressed > 0.0) { setLoopOutToCurrentPosition(); m_bLoopOutPressedWhileLoopDisabled = true; @@ -733,6 +818,47 @@ void LoopingControl::slotLoopExit(double val) { } } +void LoopingControl::slotLoopEnabledValueChangeRequest(double value) { + if (!m_pTrack) { + return; + } + + if (value) { + // Requested to set loop_enabled to 1 + if (m_bLoopingEnabled) { + VERIFY_OR_DEBUG_ASSERT(m_pCOLoopEnabled->get()) { + m_pCOLoopEnabled->setAndConfirm(1.0); + } + } else { + // Looping is currently disabled, try to enable the loop. In + // contrast to the reloop_toggle CO, we jump in no case. + LoopSamples loopSamples = m_loopSamples.getValue(); + if (loopSamples.start != kNoTrigger && loopSamples.end != kNoTrigger && + loopSamples.start <= loopSamples.end) { + // setAndConfirm is called by setLoopingEnabled + setLoopingEnabled(true); + } + } + } else { + // Requested to set loop_enabled to 0 + if (m_bLoopingEnabled) { + // Looping is currently enabled, disable the loop. If loop roll + // was active, also disable slip. + if (m_bLoopRollActive) { + m_pSlipEnabled->set(0); + m_bLoopRollActive = false; + m_activeLoopRolls.clear(); + } + // setAndConfirm is called by setLoopingEnabled + setLoopingEnabled(false); + } else { + VERIFY_OR_DEBUG_ASSERT(!m_pCOLoopEnabled->get()) { + m_pCOLoopEnabled->setAndConfirm(0.0); + } + } + } +} + void LoopingControl::slotReloopToggle(double val) { if (!m_pTrack || val <= 0.0) { return; @@ -784,15 +910,17 @@ void LoopingControl::slotLoopStartPos(double pos) { clearActiveBeatLoop(); if (pos == kNoTrigger) { + emit loopReset(); setLoopingEnabled(false); } - loopSamples.seek = false; + loopSamples.seekMode = LoopSeekMode::MovedOut; loopSamples.start = pos; m_pCOLoopStartPosition->set(pos); if (loopSamples.end != kNoTrigger && loopSamples.end <= loopSamples.start) { + emit loopReset(); loopSamples.end = kNoTrigger; m_pCOLoopEndPosition->set(kNoTrigger); setLoopingEnabled(false); @@ -819,11 +947,12 @@ void LoopingControl::slotLoopEndPos(double pos) { clearActiveBeatLoop(); - if (pos == -1.0) { + if (pos == kNoTrigger) { + emit loopReset(); setLoopingEnabled(false); } loopSamples.end = pos; - loopSamples.seek = false; + loopSamples.seekMode = LoopSeekMode::MovedOut; m_pCOLoopEndPosition->set(pos); m_loopSamples.setValue(loopSamples); } @@ -852,8 +981,12 @@ void LoopingControl::notifySeek(double dNewPlaypos) { } void LoopingControl::setLoopingEnabled(bool enabled) { + if (m_bLoopingEnabled == enabled) { + return; + } + m_bLoopingEnabled = enabled; - m_pCOLoopEnabled->set(enabled); + m_pCOLoopEnabled->setAndConfirm(enabled ? 1.0 : 0.0); BeatLoopingControl* pActiveBeatLoop = atomicLoadRelaxed(m_pActiveBeatLoop); if (pActiveBeatLoop != nullptr) { if (enabled) { @@ -862,6 +995,8 @@ void LoopingControl::setLoopingEnabled(bool enabled) { pActiveBeatLoop->deactivate(); } } + + emit loopEnabledChanged(enabled); } bool LoopingControl::isLoopingEnabled() { @@ -1009,6 +1144,11 @@ void LoopingControl::updateBeatLoopingControls() { } void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable) { + // If this is a "new" loop, stop tracking saved loop changes + if (!keepStartPoint) { + emit loopReset(); + } + // if a seek was queued in the engine buffer move the current sample to its position double p_seekPosition = 0; if (getEngineBuffer()->getQueuedSeekPosition(&p_seekPosition)) { @@ -1038,7 +1178,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, false}; + LoopSamples newloopSamples = {kNoTrigger, kNoTrigger, LoopSeekMode::MovedOut}; LoopSamples loopSamples = m_loopSamples.getValue(); double currentSample = m_currentSample.getValue(); @@ -1142,9 +1282,12 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable // If resizing an inactive loop by changing beatloop_size, // do not seek to the adjusted loop. - newloopSamples.seek = (keepStartPoint && (enable || m_bLoopingEnabled)); + newloopSamples.seekMode = (keepStartPoint && (enable || m_bLoopingEnabled)) + ? LoopSeekMode::Changed + : LoopSeekMode::MovedOut; m_loopSamples.setValue(newloopSamples); + emit loopUpdated(newloopSamples.start, newloopSamples.end); m_pCOLoopStartPosition->set(newloopSamples.start); m_pCOLoopEndPosition->set(newloopSamples.end); @@ -1250,11 +1393,12 @@ void LoopingControl::slotLoopMove(double 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.seekMode = m_bLoopingEnabled ? LoopSeekMode::Changed : LoopSeekMode::MovedOut; loopSamples.start = new_loop_in; loopSamples.end = new_loop_out; m_loopSamples.setValue(loopSamples); + emit loopUpdated(loopSamples.start, loopSamples.end); m_pCOLoopStartPosition->set(new_loop_in); m_pCOLoopEndPosition->set(new_loop_out); } diff --git a/src/engine/controls/loopingcontrol.h b/src/engine/controls/loopingcontrol.h index 3faaea34ea12..d7bdaed1bd76 100644 --- a/src/engine/controls/loopingcontrol.h +++ b/src/engine/controls/loopingcontrol.h @@ -13,6 +13,7 @@ #include "engine/controls/ratecontrol.h" #include "preferences/usersettings.h" #include "track/beats.h" +#include "track/cue.h" #include "track/track_decl.h" #define MINIMUM_AUDIBLE_LOOP_SIZE 300 // In samples @@ -51,12 +52,20 @@ class LoopingControl : public EngineControl { double getSyncPositionInsideLoop(double dRequestedPlaypos, double dSyncedPlayPos); void notifySeek(double dNewPlaypos) override; + + void setBeatLoop(double startPosition, bool enabled); + void setLoop(double startPosition, double endPosition, bool enabled); void setRateControl(RateControl* rateControl); bool isLoopingEnabled(); void trackLoaded(TrackPointer pNewTrack) override; void trackBeatsUpdated(mixxx::BeatsPointer pBeats) override; + signals: + void loopReset(); + void loopEnabledChanged(bool enabled); + void loopUpdated(double startPosition, double endPosition); + public slots: void slotLoopIn(double pressed); void slotLoopInGoto(double); @@ -91,12 +100,20 @@ class LoopingControl : public EngineControl { void slotLoopDouble(double pressed); void slotLoopHalve(double pressed); + private slots: + void slotLoopEnabledValueChangeRequest(double enabled); + private: + enum class LoopSeekMode { + Changed, // force the playposition to be inside the loop after adjusting it. + MovedOut, + None, + }; struct LoopSamples { double start; double end; - bool seek; // force the playposition to be inside the loop after adjusting it. + LoopSeekMode seekMode; }; void setLoopingEnabled(bool enabled); @@ -125,6 +142,7 @@ class LoopingControl : public EngineControl { ControlPushButton* m_pLoopOutButton; ControlPushButton* m_pLoopOutGotoButton; ControlPushButton* m_pLoopExitButton; + ControlPushButton* m_pLoopToggleButton; ControlPushButton* m_pReloopToggleButton; ControlPushButton* m_pReloopAndStopButton; ControlObject* m_pCOLoopScale; diff --git a/src/engine/enginebuffer.cpp b/src/engine/enginebuffer.cpp index 49d09689f6f4..5c724263e726 100644 --- a/src/engine/enginebuffer.cpp +++ b/src/engine/enginebuffer.cpp @@ -232,6 +232,22 @@ EngineBuffer::EngineBuffer(const QString& group, m_pCueControl = new CueControl(group, pConfig); addControl(m_pCueControl); + connect(m_pLoopingControl, + &LoopingControl::loopReset, + m_pCueControl, + &CueControl::slotLoopReset, + Qt::DirectConnection); + connect(m_pLoopingControl, + &LoopingControl::loopUpdated, + m_pCueControl, + &CueControl::slotLoopUpdated, + Qt::DirectConnection); + connect(m_pLoopingControl, + &LoopingControl::loopEnabledChanged, + m_pCueControl, + &CueControl::slotLoopEnabledChanged, + Qt::DirectConnection); + m_pReadAheadManager = new ReadAheadManager(m_pReader, m_pLoopingControl); m_pReadAheadManager->addRateControl(m_pRateControl); @@ -351,6 +367,14 @@ double EngineBuffer::getLocalBpm() const { return m_pBpmControl->getLocalBpm(); } +void EngineBuffer::setBeatLoop(double startPosition, bool enabled) { + return m_pLoopingControl->setBeatLoop(startPosition, enabled); +} + +void EngineBuffer::setLoop(double startPosition, double endPositon, bool enabled) { + return m_pLoopingControl->setLoop(startPosition, endPositon, enabled); +} + void EngineBuffer::setEngineMaster(EngineMaster* pEngineMaster) { for (const auto& pControl: qAsConst(m_engineControls)) { pControl->setEngineMaster(pEngineMaster); @@ -661,8 +685,9 @@ bool EngineBuffer::updateIndicatorsAndModifyPlay(bool newPlay) { bool playPossible = true; if ((!m_pCurrentTrack && atomicLoadRelaxed(m_iTrackLoading) == 0) || (m_pCurrentTrack && atomicLoadRelaxed(m_iTrackLoading) == 0 && - m_filepos_play >= m_pTrackSamples->get() && - !atomicLoadRelaxed(m_iSeekQueued)) || m_pPassthroughEnabled->toBool()) { + m_filepos_play >= m_pTrackSamples->get() && + !atomicLoadRelaxed(m_iSeekQueued)) || + m_pPassthroughEnabled->toBool()) { // play not possible playPossible = false; } diff --git a/src/engine/enginebuffer.h b/src/engine/enginebuffer.h index 592c4da8464c..15140c6bcbbc 100644 --- a/src/engine/enginebuffer.h +++ b/src/engine/enginebuffer.h @@ -126,6 +126,10 @@ class EngineBuffer : public EngineObject { double getBpm() const; // Returns the BPM of the loaded track around the current position (not thread-safe) double getLocalBpm() const; + /// Sets a beatloop for the loaded track (not thread safe) + void setBeatLoop(double startPosition, bool enabled); + /// Sets a loop for the loaded track (not thread safe) + void setLoop(double startPosition, double endPositon, bool enabled); // Sets pointer to other engine buffer/channel void setEngineMaster(EngineMaster*); @@ -251,6 +255,7 @@ class EngineBuffer : public EngineObject { UserSettingsPointer m_pConfig; friend class CueControlTest; + friend class HotcueControlTest; LoopingControl* m_pLoopingControl; // used for testes FRIEND_TEST(LoopingControlTest, LoopScale_HalvesLoop); diff --git a/src/mixer/basetrackplayer.cpp b/src/mixer/basetrackplayer.cpp index 57e5a41151f9..d19420f53b54 100644 --- a/src/mixer/basetrackplayer.cpp +++ b/src/mixer/basetrackplayer.cpp @@ -28,7 +28,7 @@ const double kShiftCuesOffsetSmallMillis = 1; inline double trackColorToDouble(mixxx::RgbColor::optional_t color) { return (color ? static_cast(*color) : kNoTrackColor); } -} +} // namespace BaseTrackPlayer::BaseTrackPlayer(QObject* pParent, const QString& group) : BasePlayer(pParent, group) { diff --git a/src/preferences/dialog/dlgprefcolors.cpp b/src/preferences/dialog/dlgprefcolors.cpp index f13111512610..b3f55801dea4 100644 --- a/src/preferences/dialog/dlgprefcolors.cpp +++ b/src/preferences/dialog/dlgprefcolors.cpp @@ -17,9 +17,13 @@ namespace { constexpr int kHotcueDefaultColorIndex = -1; +constexpr int kLoopDefaultColorIndex = -1; constexpr QSize kPalettePreviewSize = QSize(108, 16); const ConfigKey kAutoHotcueColorsConfigKey("[Controls]", "auto_hotcue_colors"); +const ConfigKey kAutoLoopColorsConfigKey("[Controls]", "auto_loop_colors"); const ConfigKey kHotcueDefaultColorIndexConfigKey("[Controls]", "HotcueDefaultColorIndex"); +const ConfigKey kLoopDefaultColorIndexConfigKey("[Controls]", "LoopDefaultColorIndex"); + } // anonymous namespace DlgPrefColors::DlgPrefColors( @@ -126,6 +130,24 @@ void DlgPrefColors::loadSettings() { comboBoxHotcueDefaultColor->setCurrentIndex( hotcueDefaultColorIndex + 1); } + + bool autoLoopColors = m_pConfig->getValue(kAutoLoopColorsConfigKey, false); + if (autoLoopColors) { + comboBoxLoopDefaultColor->setCurrentIndex(0); + } else { + int loopDefaultColorIndex = m_pConfig->getValue( + kLoopDefaultColorIndexConfigKey, kLoopDefaultColorIndex); + if (loopDefaultColorIndex < 0 || + loopDefaultColorIndex >= hotcuePalette.size()) { + loopDefaultColorIndex = + hotcuePalette.size() - 2; // default to second last color + if (loopDefaultColorIndex < 0) { + loopDefaultColorIndex = 0; + } + } + comboBoxLoopDefaultColor->setCurrentIndex( + loopDefaultColorIndex + 1); + } } // Set the default values for all the widgets @@ -138,6 +160,8 @@ void DlgPrefColors::slotResetToDefaults() { .getName()); comboBoxHotcueDefaultColor->setCurrentIndex( mixxx::PredefinedColorPalettes::kDefaultTrackColorPalette.size()); + comboBoxLoopDefaultColor->setCurrentIndex( + mixxx::PredefinedColorPalettes::kDefaultTrackColorPalette.size() - 1); slotApply(); } @@ -174,15 +198,25 @@ void DlgPrefColors::slotApply() { m_colorPaletteSettings.getTrackColorPalette())); } - int index = comboBoxHotcueDefaultColor->currentIndex(); + int hotcueColorIndex = comboBoxHotcueDefaultColor->currentIndex(); - if (index > 0) { + if (hotcueColorIndex > 0) { m_pConfig->setValue(kAutoHotcueColorsConfigKey, false); - m_pConfig->setValue(kHotcueDefaultColorIndexConfigKey, index - 1); + m_pConfig->setValue(kHotcueDefaultColorIndexConfigKey, hotcueColorIndex - 1); } else { m_pConfig->setValue(kAutoHotcueColorsConfigKey, true); m_pConfig->setValue(kHotcueDefaultColorIndexConfigKey, -1); } + + int loopColorIndex = comboBoxLoopDefaultColor->currentIndex(); + + if (loopColorIndex > 0) { + m_pConfig->setValue(kAutoLoopColorsConfigKey, false); + m_pConfig->setValue(kLoopDefaultColorIndexConfigKey, loopColorIndex - 1); + } else { + m_pConfig->setValue(kAutoLoopColorsConfigKey, true); + m_pConfig->setValue(kLoopDefaultColorIndexConfigKey, -1); + } } void DlgPrefColors::slotReplaceCueColorClicked() { @@ -259,33 +293,48 @@ void DlgPrefColors::slotHotcuePaletteIndexChanged(int paletteIndex) { ColorPalette palette = m_colorPaletteSettings.getHotcueColorPalette(paletteName); - int defaultColor = comboBoxHotcueDefaultColor->currentIndex(); + int defaultHotcueColor = comboBoxHotcueDefaultColor->currentIndex(); comboBoxHotcueDefaultColor->clear(); + int defaultLoopColor = comboBoxLoopDefaultColor->currentIndex(); + comboBoxLoopDefaultColor->clear(); + + QIcon paletteIcon = drawHotcueColorByPaletteIcon(paletteName); + comboBoxHotcueDefaultColor->addItem(tr("By hotcue number"), -1); - QIcon icon = drawHotcueColorByPaletteIcon(paletteName); - comboBoxHotcueDefaultColor->setItemIcon(0, icon); + comboBoxHotcueDefaultColor->setItemIcon(0, paletteIcon); + + comboBoxLoopDefaultColor->addItem(tr("By hotcue number"), -1); + comboBoxLoopDefaultColor->setItemIcon(0, paletteIcon); QPixmap pixmap(16, 16); for (int i = 0; i < palette.size(); ++i) { QColor color = mixxx::RgbColor::toQColor(palette.at(i)); - comboBoxHotcueDefaultColor->addItem( - tr("Color") + - QStringLiteral(" ") + - QString::number(i + 1) + - QStringLiteral(": ") + - color.name(), - i); pixmap.fill(color); - comboBoxHotcueDefaultColor->setItemIcon(i + 1, QIcon(pixmap)); + QIcon icon(pixmap); + QString item = tr("Color") + QStringLiteral(" ") + + QString::number(i + 1) + QStringLiteral(": ") + color.name(); + + comboBoxHotcueDefaultColor->addItem(item, i); + comboBoxHotcueDefaultColor->setItemIcon(i + 1, icon); + + comboBoxLoopDefaultColor->addItem(item, i); + comboBoxLoopDefaultColor->setItemIcon(i + 1, icon); } - if (comboBoxHotcueDefaultColor->count() > defaultColor) { - comboBoxHotcueDefaultColor->setCurrentIndex(defaultColor); + if (comboBoxHotcueDefaultColor->count() > defaultHotcueColor) { + comboBoxHotcueDefaultColor->setCurrentIndex(defaultHotcueColor); } else { comboBoxHotcueDefaultColor->setCurrentIndex( comboBoxHotcueDefaultColor->count() - 1); } + + if (comboBoxLoopDefaultColor->count() > defaultLoopColor) { + comboBoxLoopDefaultColor->setCurrentIndex(defaultLoopColor); + } else { + comboBoxLoopDefaultColor->setCurrentIndex( + comboBoxLoopDefaultColor->count() - 1); + } } void DlgPrefColors::slotEditTrackPaletteClicked() { @@ -326,39 +375,49 @@ void DlgPrefColors::openColorPaletteEditor( void DlgPrefColors::trackPaletteUpdated(const QString& trackColors) { QString hotcueColors = comboBoxHotcueColors->currentText(); - int defaultColor = comboBoxHotcueDefaultColor->currentIndex(); + int defaultHotcueColor = comboBoxHotcueDefaultColor->currentIndex(); + int defaultLoopColor = comboBoxLoopDefaultColor->currentIndex(); slotUpdate(); - restoreComboBoxes(hotcueColors, trackColors, defaultColor); + restoreComboBoxes(hotcueColors, trackColors, defaultHotcueColor, defaultLoopColor); } void DlgPrefColors::hotcuePaletteUpdated(const QString& hotcueColors) { QString trackColors = comboBoxTrackColors->currentText(); - int defaultColor = comboBoxHotcueDefaultColor->currentIndex(); + int defaultHotcueColor = comboBoxHotcueDefaultColor->currentIndex(); + int defaultLoopColor = comboBoxLoopDefaultColor->currentIndex(); slotUpdate(); - restoreComboBoxes(hotcueColors, trackColors, defaultColor); + restoreComboBoxes(hotcueColors, trackColors, defaultHotcueColor, defaultLoopColor); } void DlgPrefColors::palettesUpdated() { QString hotcueColors = comboBoxHotcueColors->currentText(); QString trackColors = comboBoxTrackColors->currentText(); - int defaultColor = comboBoxHotcueDefaultColor->currentIndex(); + int defaultHotcueColor = comboBoxHotcueDefaultColor->currentIndex(); + int defaultLoopColor = comboBoxLoopDefaultColor->currentIndex(); slotUpdate(); - restoreComboBoxes(hotcueColors, trackColors, defaultColor); + restoreComboBoxes(hotcueColors, trackColors, defaultHotcueColor, defaultLoopColor); } void DlgPrefColors::restoreComboBoxes( const QString& hotcueColors, const QString& trackColors, - int defaultColor) { + int defaultHotcueColor, + int defaultLoopColor) { comboBoxHotcueColors->setCurrentText(hotcueColors); comboBoxTrackColors->setCurrentText(trackColors); - if (comboBoxHotcueDefaultColor->count() > defaultColor) { - comboBoxHotcueDefaultColor->setCurrentIndex(defaultColor); + if (comboBoxHotcueDefaultColor->count() > defaultHotcueColor) { + comboBoxHotcueDefaultColor->setCurrentIndex(defaultHotcueColor); } else { comboBoxHotcueDefaultColor->setCurrentIndex( comboBoxHotcueDefaultColor->count() - 1); } + if (comboBoxLoopDefaultColor->count() > defaultLoopColor) { + comboBoxLoopDefaultColor->setCurrentIndex(defaultLoopColor); + } else { + comboBoxLoopDefaultColor->setCurrentIndex( + comboBoxLoopDefaultColor->count() - 1); + } } diff --git a/src/preferences/dialog/dlgprefcolors.h b/src/preferences/dialog/dlgprefcolors.h index ee6fada3023f..058523cc8b12 100644 --- a/src/preferences/dialog/dlgprefcolors.h +++ b/src/preferences/dialog/dlgprefcolors.h @@ -50,7 +50,8 @@ class DlgPrefColors : public DlgPreferencePage, public Ui::DlgPrefColorsDlg { void restoreComboBoxes( const QString& hotcueColors, const QString& trackColors, - int defaultColor); + int defaultHotcueColor, + int defaultLoopColor); const UserSettingsPointer m_pConfig; ColorPaletteSettings m_colorPaletteSettings; diff --git a/src/preferences/dialog/dlgprefcolorsdlg.ui b/src/preferences/dialog/dlgprefcolorsdlg.ui index 1cbea8478791..22abf4d01789 100644 --- a/src/preferences/dialog/dlgprefcolorsdlg.ui +++ b/src/preferences/dialog/dlgprefcolorsdlg.ui @@ -26,6 +26,20 @@ Colors + + + + Hotcue default color + + + + + + + Replace… + + + @@ -43,13 +57,6 @@ - - - - Track palette - - - @@ -67,10 +74,10 @@ - - + + - Hotcue default color + Track palette @@ -84,13 +91,16 @@ - - + + - Replace… + Loop default color + + + diff --git a/src/test/hotcuecontrol_test.cpp b/src/test/hotcuecontrol_test.cpp new file mode 100644 index 000000000000..001308ec68f3 --- /dev/null +++ b/src/test/hotcuecontrol_test.cpp @@ -0,0 +1,1297 @@ +#include "engine/controls/cuecontrol.h" +#include "test/signalpathtest.h" + +namespace { +double getBeatLengthSamples(TrackPointer pTrack) { + double beatLengthSecs = 60.0 / pTrack->getBpm(); + return beatLengthSecs * (pTrack->getSampleRate() * mixxx::kEngineChannelCount); +} +} // anonymous namespace + +class HotcueControlTest : public BaseSignalPathTest { + protected: + void SetUp() override { + BaseSignalPathTest::SetUp(); + + m_pPlay = std::make_unique(m_sGroup1, "play"); + m_pBeatloopActivate = std::make_unique(m_sGroup1, "beatloop_activate"); + m_pBeatloopSize = std::make_unique(m_sGroup1, "beatloop_size"); + m_pLoopStartPosition = std::make_unique(m_sGroup1, "loop_start_position"); + m_pLoopEndPosition = std::make_unique(m_sGroup1, "loop_end_position"); + m_pLoopEnabled = std::make_unique(m_sGroup1, "loop_enabled"); + m_pLoopDouble = std::make_unique(m_sGroup1, "loop_double"); + m_pLoopHalve = std::make_unique(m_sGroup1, "loop_halve"); + m_pLoopMove = std::make_unique(m_sGroup1, "loop_move"); + m_pHotcue1Activate = std::make_unique(m_sGroup1, "hotcue_1_activate"); + m_pHotcue1ActivateCue = std::make_unique(m_sGroup1, "hotcue_1_activatecue"); + m_pHotcue1ActivateLoop = std::make_unique(m_sGroup1, "hotcue_1_activateloop"); + m_pHotcue1Set = std::make_unique(m_sGroup1, "hotcue_1_set"); + m_pHotcue1SetCue = std::make_unique(m_sGroup1, "hotcue_1_setcue"); + m_pHotcue1SetLoop = std::make_unique(m_sGroup1, "hotcue_1_setloop"); + m_pHotcue1Goto = std::make_unique(m_sGroup1, "hotcue_1_goto"); + m_pHotcue1GotoAndPlay = std::make_unique(m_sGroup1, "hotcue_1_gotoandplay"); + m_pHotcue1GotoAndLoop = std::make_unique(m_sGroup1, "hotcue_1_gotoandloop"); + m_pHotcue1CueLoop = std::make_unique(m_sGroup1, "hotcue_1_cueloop"); + m_pHotcue1Position = std::make_unique(m_sGroup1, "hotcue_1_position"); + m_pHotcue1EndPosition = std::make_unique(m_sGroup1, "hotcue_1_endposition"); + m_pHotcue1Enabled = std::make_unique(m_sGroup1, "hotcue_1_enabled"); + m_pHotcue1Clear = std::make_unique(m_sGroup1, "hotcue_1_clear"); + m_pQuantizeEnabled = std::make_unique(m_sGroup1, "quantize"); + } + + TrackPointer createTestTrack() const { + const QString kTrackLocationTest = QDir::currentPath() + "/src/test/sine-30.wav"; + const auto pTrack = Track::newTemporary(kTrackLocationTest, SecurityTokenPointer()); + pTrack->setAudioProperties( + mixxx::audio::ChannelCount(2), + mixxx::audio::SampleRate(44100), + mixxx::audio::Bitrate(), + mixxx::Duration::fromSeconds(180)); + return pTrack; + } + + void loadTrack(TrackPointer pTrack) { + BaseSignalPathTest::loadTrack(m_pMixerDeck1, pTrack); + ProcessBuffer(); + } + + TrackPointer loadTestTrackWithBpm(double bpm) { + DEBUG_ASSERT(!m_pPlay->get()); + // Setup fake track with 120 bpm can calculate loop size + TrackPointer pTrack = createTestTrack(); + pTrack->setBpm(bpm); + + loadTrack(pTrack); + ProcessBuffer(); + + return pTrack; + } + + TrackPointer createAndLoadFakeTrack() { + return m_pMixerDeck1->loadFakeTrack(false, 0.0); + } + + void unloadTrack() { + m_pMixerDeck1->slotLoadTrack(TrackPointer(), false); + } + + double currentSamplePosition() { + return m_pChannel1->getEngineBuffer()->m_pCueControl->getSampleOfTrack().current; + } + + void setCurrentSamplePosition(double sample) { + m_pChannel1->getEngineBuffer()->queueNewPlaypos(sample, EngineBuffer::SEEK_STANDARD); + ProcessBuffer(); + } + + std::unique_ptr m_pPlay; + std::unique_ptr m_pBeatloopActivate; + std::unique_ptr m_pBeatloopSize; + std::unique_ptr m_pLoopStartPosition; + std::unique_ptr m_pLoopEndPosition; + std::unique_ptr m_pLoopEnabled; + std::unique_ptr m_pLoopDouble; + std::unique_ptr m_pLoopHalve; + std::unique_ptr m_pLoopMove; + std::unique_ptr m_pHotcue1Activate; + std::unique_ptr m_pHotcue1ActivateCue; + std::unique_ptr m_pHotcue1ActivateLoop; + std::unique_ptr m_pHotcue1Set; + std::unique_ptr m_pHotcue1SetCue; + std::unique_ptr m_pHotcue1SetLoop; + std::unique_ptr m_pHotcue1Goto; + std::unique_ptr m_pHotcue1GotoAndPlay; + std::unique_ptr m_pHotcue1GotoAndLoop; + std::unique_ptr m_pHotcue1CueLoop; + std::unique_ptr m_pHotcue1Position; + std::unique_ptr m_pHotcue1EndPosition; + std::unique_ptr m_pHotcue1Enabled; + std::unique_ptr m_pHotcue1Clear; + std::unique_ptr m_pQuantizeEnabled; +}; + +TEST_F(HotcueControlTest, DefautltControlValues) { + TrackPointer pTrack = createTestTrack(); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + loadTrack(pTrack); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); +} + +TEST_F(HotcueControlTest, NoTrackLoaded) { + TrackPointer pTrack = createTestTrack(); + + m_pHotcue1Set->slotSet(1); + m_pHotcue1Set->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + m_pHotcue1SetCue->slotSet(1); + m_pHotcue1SetCue->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + m_pHotcue1Activate->slotSet(1); + m_pHotcue1Activate->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + m_pHotcue1ActivateCue->slotSet(1); + m_pHotcue1ActivateCue->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + m_pHotcue1ActivateLoop->slotSet(1); + m_pHotcue1ActivateLoop->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); +} + +TEST_F(HotcueControlTest, SetCueAuto) { + createAndLoadFakeTrack(); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + m_pQuantizeEnabled->slotSet(0); + setCurrentSamplePosition(100); + ProcessBuffer(); + + m_pHotcue1Set->slotSet(1); + m_pHotcue1Set->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(100, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); +} + +TEST_F(HotcueControlTest, SetCueManual) { + createAndLoadFakeTrack(); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + m_pQuantizeEnabled->slotSet(0); + setCurrentSamplePosition(100); + + m_pHotcue1SetCue->slotSet(1); + m_pHotcue1SetCue->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(100, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); +} + +TEST_F(HotcueControlTest, SetLoopAuto) { + createAndLoadFakeTrack(); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + m_pChannel1->getEngineBuffer()->setLoop(100, 200, true); + + m_pHotcue1Set->slotSet(1); + m_pHotcue1Set->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(100, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(200, m_pHotcue1EndPosition->get()); +} + +TEST_F(HotcueControlTest, SetLoopManualWithLoop) { + createAndLoadFakeTrack(); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + m_pChannel1->getEngineBuffer()->setLoop(100, 200, true); + + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(100, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(200, m_pHotcue1EndPosition->get()); +} + +TEST_F(HotcueControlTest, SetLoopManualWithoutLoop) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + const double beatLengthSamples = getBeatLengthSamples(pTrack); + m_pBeatloopSize->slotSet(4); + const double beatloopLengthSamples = m_pBeatloopSize->get() * getBeatLengthSamples(pTrack); + + setCurrentSamplePosition(8 * beatLengthSamples); + ProcessBuffer(); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(currentSamplePosition(), m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(currentSamplePosition() + beatloopLengthSamples, m_pHotcue1EndPosition->get()); +} + +TEST_F(HotcueControlTest, SetLoopManualWithoutLoopOrBeats) { + createAndLoadFakeTrack(); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); +} + +TEST_F(HotcueControlTest, CueGoto) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + const double cuePositionSamples = 8 * getBeatLengthSamples(pTrack); + + // Seek to cue Position (8th beat) + setCurrentSamplePosition(cuePositionSamples); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(cuePositionSamples, currentSamplePosition()); + + m_pHotcue1SetCue->slotSet(1); + m_pHotcue1SetCue->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Seek to start of track + setCurrentSamplePosition(0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(0, currentSamplePosition()); + + m_pHotcue1Goto->slotSet(1); + m_pHotcue1Goto->slotSet(0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(cuePositionSamples, currentSamplePosition()); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + EXPECT_DOUBLE_EQ(0.0, m_pLoopEnabled->get()); +} + +TEST_F(HotcueControlTest, CueGotoAndPlay) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + const double cuePositionSamples = 8 * getBeatLengthSamples(pTrack); + + // Seek to cue Position (8th beat) + setCurrentSamplePosition(cuePositionSamples); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(cuePositionSamples, currentSamplePosition()); + + m_pHotcue1SetCue->slotSet(1); + m_pHotcue1SetCue->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Seek to start of track + setCurrentSamplePosition(0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(0, currentSamplePosition()); + + m_pHotcue1GotoAndPlay->slotSet(1); + m_pHotcue1GotoAndPlay->slotSet(0); + ProcessBuffer(); + EXPECT_LE(cuePositionSamples, currentSamplePosition()); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + EXPECT_DOUBLE_EQ(0.0, m_pLoopEnabled->get()); +} + +TEST_F(HotcueControlTest, CueGotoAndLoop) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + const double beatLengthSamples = getBeatLengthSamples(pTrack); + const double cuePositionSamples = 8 * beatLengthSamples; + m_pBeatloopSize->slotSet(4); + const double beatloopLengthSamples = m_pBeatloopSize->get() * getBeatLengthSamples(pTrack); + + // Seek to cue Position (8th beat) + setCurrentSamplePosition(cuePositionSamples); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(cuePositionSamples, currentSamplePosition()); + + m_pHotcue1SetCue->slotSet(1); + m_pHotcue1SetCue->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Seek to start of track + setCurrentSamplePosition(0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(0, currentSamplePosition()); + + m_pHotcue1GotoAndLoop->slotSet(1); + m_pHotcue1GotoAndLoop->slotSet(0); + ProcessBuffer(); + EXPECT_LE(cuePositionSamples, currentSamplePosition()); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + EXPECT_DOUBLE_EQ(1.0, m_pLoopEnabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pLoopStartPosition->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples + beatloopLengthSamples, m_pLoopEndPosition->get()); +} + +TEST_F(HotcueControlTest, SavedLoopGoto) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + m_pBeatloopSize->slotSet(4); + const double beatLengthSamples = getBeatLengthSamples(pTrack); + const double loopLengthSamples = m_pBeatloopSize->get() * beatLengthSamples; + const double cuePositionSamples = 8 * beatLengthSamples; + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Seek to cue Position (8th beat) + setCurrentSamplePosition(cuePositionSamples); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(cuePositionSamples, currentSamplePosition()); + + // Set a beatloop this position + m_pBeatloopActivate->slotSet(1); + m_pBeatloopActivate->slotSet(0); + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + EXPECT_DOUBLE_EQ(1.0, m_pLoopEnabled->get()); + + // Save loop to hotcue slot 1 + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Disable loop + m_pLoopEnabled->slotSet(0); + EXPECT_DOUBLE_EQ(0.0, m_pLoopEnabled->get()); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + + // Seek to start of track + setCurrentSamplePosition(0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(0, currentSamplePosition()); + + m_pHotcue1Goto->slotSet(1); + m_pHotcue1Goto->slotSet(0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(cuePositionSamples, currentSamplePosition()); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + EXPECT_DOUBLE_EQ(0.0, m_pLoopEnabled->get()); +} + +TEST_F(HotcueControlTest, SavedLoopGotoAndPlay) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + m_pBeatloopSize->slotSet(4); + const double beatLengthSamples = getBeatLengthSamples(pTrack); + const double loopLengthSamples = m_pBeatloopSize->get() * beatLengthSamples; + const double cuePositionSamples = 8 * beatLengthSamples; + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Seek to cue Position (8th beat) + setCurrentSamplePosition(cuePositionSamples); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(cuePositionSamples, currentSamplePosition()); + + // Set a beatloop this position + m_pBeatloopActivate->slotSet(1); + m_pBeatloopActivate->slotSet(0); + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + EXPECT_DOUBLE_EQ(1.0, m_pLoopEnabled->get()); + + // Save loop to hotcue slot 1 + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Disable loop + m_pLoopEnabled->slotSet(0); + EXPECT_DOUBLE_EQ(0.0, m_pLoopEnabled->get()); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + + // Seek to start of track + setCurrentSamplePosition(0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(0, currentSamplePosition()); + + m_pHotcue1GotoAndPlay->slotSet(1); + m_pHotcue1GotoAndPlay->slotSet(0); + ProcessBuffer(); + EXPECT_LE(cuePositionSamples, currentSamplePosition()); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + EXPECT_DOUBLE_EQ(0.0, m_pLoopEnabled->get()); +} + +TEST_F(HotcueControlTest, SavedLoopGotoAndLoop) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + m_pBeatloopSize->slotSet(4); + const double beatLengthSamples = getBeatLengthSamples(pTrack); + const double loopLengthSamples = m_pBeatloopSize->get() * beatLengthSamples; + const double cuePositionSamples = 8 * beatLengthSamples; + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Seek to cue Position (8th beat) + setCurrentSamplePosition(cuePositionSamples); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(cuePositionSamples, currentSamplePosition()); + + // Set a beatloop this position + m_pBeatloopActivate->slotSet(1); + m_pBeatloopActivate->slotSet(0); + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + EXPECT_DOUBLE_EQ(1.0, m_pLoopEnabled->get()); + + // Save loop to hotcue slot 1 + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Disable loop + m_pLoopEnabled->slotSet(0); + EXPECT_DOUBLE_EQ(0.0, m_pLoopEnabled->get()); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + + // Seek to start of track + setCurrentSamplePosition(0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(0, currentSamplePosition()); + + m_pHotcue1GotoAndLoop->slotSet(1); + m_pHotcue1GotoAndLoop->slotSet(0); + ProcessBuffer(); + EXPECT_LE(cuePositionSamples, currentSamplePosition()); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + EXPECT_DOUBLE_EQ(1.0, m_pLoopEnabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pLoopStartPosition->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples + loopLengthSamples, m_pLoopEndPosition->get()); +} + +TEST_F(HotcueControlTest, SavedLoopStatus) { + createAndLoadFakeTrack(); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + m_pChannel1->getEngineBuffer()->setLoop(100, 200, true); + + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + EXPECT_DOUBLE_EQ(1.0, m_pLoopEnabled->get()); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(100, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(200, m_pHotcue1EndPosition->get()); + + // Disable Loop + m_pLoopEnabled->slotSet(0); + + EXPECT_DOUBLE_EQ(0.0, m_pLoopEnabled->get()); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(100, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(200, m_pHotcue1EndPosition->get()); + + // Re-Enable Loop + m_pLoopEnabled->slotSet(1); + + EXPECT_DOUBLE_EQ(1.0, m_pLoopEnabled->get()); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(100, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(200, m_pHotcue1EndPosition->get()); + + m_pHotcue1Clear->slotSet(1); + m_pHotcue1Clear->slotSet(0); + + EXPECT_DOUBLE_EQ(1.0, m_pLoopEnabled->get()); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); +} + +TEST_F(HotcueControlTest, SavedLoopScale) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + m_pBeatloopSize->slotSet(4); + const double beatLengthSamples = getBeatLengthSamples(pTrack); + const double loopLengthSamples = m_pBeatloopSize->get() * beatLengthSamples; + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Set a beatloop (4 beats) + m_pBeatloopActivate->slotSet(1); + m_pBeatloopActivate->slotSet(0); + m_pPlay->slotSet(1); + ProcessBuffer(); + + // Save currently active loop to hotcue slot 1 + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Double loop size (4 => 8 beats) + m_pLoopDouble->slotSet(1); + m_pLoopDouble->slotSet(0); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(2 * loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Halve loop size (8 => 4 beats) + m_pLoopHalve->slotSet(1); + m_pLoopHalve->slotSet(0); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Halve loop size (4 => 2 beats) + m_pLoopHalve->slotSet(1); + m_pLoopHalve->slotSet(0); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples / 2, m_pHotcue1EndPosition->get()); + + m_pPlay->slotSet(0); +} + +TEST_F(HotcueControlTest, SavedLoopMove) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + constexpr double loopSize = 4; + m_pBeatloopSize->slotSet(loopSize); + const double beatLengthSamples = getBeatLengthSamples(pTrack); + const double loopLengthSamples = loopSize * beatLengthSamples; + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Set a beatloop at position 0 + m_pBeatloopActivate->slotSet(1); + m_pBeatloopActivate->slotSet(0); + m_pPlay->slotSet(1); + + // Save currently active loop to hotcue slot 1 + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Move loop right (0 => 4 beats) + m_pLoopMove->slotSet(loopSize); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(2 * loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Move loop left (4 => 0 beats) + m_pLoopMove->slotSet(-loopSize); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Move loop left (0 => -4 beats) + m_pLoopMove->slotSet(-loopSize); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(-loopLengthSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(0, m_pHotcue1EndPosition->get()); + + m_pPlay->slotSet(0); +} + +TEST_F(HotcueControlTest, SavedLoopNoScaleIfDisabled) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + m_pBeatloopSize->slotSet(4); + const double beatLengthSamples = getBeatLengthSamples(pTrack); + const double loopLengthSamples = m_pBeatloopSize->get() * beatLengthSamples; + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Set a beatloop (4 beats) + m_pBeatloopActivate->slotSet(1); + m_pBeatloopActivate->slotSet(0); + m_pPlay->slotSet(1); + ProcessBuffer(); + + // Save currently active loop to hotcue slot 1 + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Disable loop + m_pLoopEnabled->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + + // Double loop size (4 => 8 beats) while saved loop is disabled + m_pLoopDouble->slotSet(1); + m_pLoopDouble->slotSet(0); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Halve loop size (8 => 4 beats) while saved loop is disabled + m_pLoopHalve->slotSet(1); + m_pLoopHalve->slotSet(0); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Halve loop size (4 => 2 beats) while saved loop is disabled + m_pLoopHalve->slotSet(1); + m_pLoopHalve->slotSet(0); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1EndPosition->get()); + + m_pPlay->slotSet(0); +} + +TEST_F(HotcueControlTest, SavedLoopNoMoveIfDisabled) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + constexpr double loopSize = 4; + m_pBeatloopSize->slotSet(loopSize); + const double beatLengthSamples = getBeatLengthSamples(pTrack); + const double loopLengthSamples = loopSize * beatLengthSamples; + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Set a beatloop at position 0 + m_pBeatloopActivate->slotSet(1); + m_pBeatloopActivate->slotSet(0); + m_pPlay->slotSet(1); + + // Save currently active loop to hotcue slot 1 + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Disable Loop + m_pLoopEnabled->slotSet(0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + + // Move loop right (0 => 4 beats) while saved loop is disabled + m_pLoopMove->slotSet(loopSize); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Move loop left (4 => 0 beats) while saved loop is disabled + m_pLoopMove->slotSet(-loopSize); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Move loop left (0 => -4 beats) while saved loop is disabled + m_pLoopMove->slotSet(-loopSize); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1EndPosition->get()); + + m_pPlay->slotSet(0); +} + +TEST_F(HotcueControlTest, SavedLoopReset) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + m_pBeatloopSize->slotSet(4); + const double beatLengthSamples = getBeatLengthSamples(pTrack); + const double loopLengthSamples = m_pBeatloopSize->get() * beatLengthSamples; + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Set a beatloop + m_pBeatloopActivate->slotSet(1); + m_pBeatloopActivate->slotSet(0); + ProcessBuffer(); + + // Save currently active loop to hotcue slot 1 + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Set a new beatloop + setCurrentSamplePosition(loopLengthSamples); + m_pBeatloopActivate->slotSet(1); + m_pBeatloopActivate->slotSet(0); + ProcessBuffer(); + + // Check if setting the new beatloop disabled the current saved loop + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1EndPosition->get()); +} + +TEST_F(HotcueControlTest, SavedLoopCueLoopWithExistingLoop) { + createAndLoadFakeTrack(); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + m_pChannel1->getEngineBuffer()->setLoop(100, 200, true); + + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + EXPECT_DOUBLE_EQ(1.0, m_pLoopEnabled->get()); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(100, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(200, m_pHotcue1EndPosition->get()); + + // Disable Loop + m_pHotcue1CueLoop->slotSet(1); + m_pHotcue1CueLoop->slotSet(0); + + EXPECT_DOUBLE_EQ(0.0, m_pLoopEnabled->get()); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(100, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(200, m_pHotcue1EndPosition->get()); + + // Re-Enable Loop + m_pHotcue1CueLoop->slotSet(1); + m_pHotcue1CueLoop->slotSet(0); + + EXPECT_DOUBLE_EQ(1.0, m_pLoopEnabled->get()); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(100, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(200, m_pHotcue1EndPosition->get()); +} + +TEST_F(HotcueControlTest, CueLoopWithoutHotcueSetsHotcue) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + m_pHotcue1CueLoop->slotSet(1); + m_pHotcue1CueLoop->slotSet(0); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + EXPECT_TRUE(m_pLoopEnabled->toBool()); +} + +TEST_F(HotcueControlTest, CueLoopWithSavedLoopToggles) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_NE(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + EXPECT_TRUE(m_pLoopEnabled->toBool()); + + m_pHotcue1CueLoop->slotSet(1); + m_pHotcue1CueLoop->slotSet(0); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_NE(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + EXPECT_FALSE(m_pLoopEnabled->toBool()); + + m_pHotcue1CueLoop->slotSet(1); + m_pHotcue1CueLoop->slotSet(0); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_NE(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + EXPECT_TRUE(m_pLoopEnabled->toBool()); +} + +TEST_F(HotcueControlTest, CueLoopWithoutLoopOrBeats) { + createAndLoadFakeTrack(); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + m_pHotcue1CueLoop->slotSet(1); + m_pHotcue1CueLoop->slotSet(0); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + EXPECT_FALSE(m_pLoopEnabled->toBool()); +} + +TEST_F(HotcueControlTest, SavedLoopToggleDoesNotSeek) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + constexpr double loopSize = 4; + const double beatLengthSamples = getBeatLengthSamples(pTrack); + const double loopLengthSamples = loopSize * beatLengthSamples; + + const double beforeLoopPositionSamples = 0; + const double loopStartPositionSamples = 8 * beatLengthSamples; + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Seek to loop start position + setCurrentSamplePosition(loopStartPositionSamples); + ProcessBuffer(); + + m_pPlay->slotSet(1); + + // Set a beatloop + m_pBeatloopActivate->slotSet(1); + m_pBeatloopActivate->slotSet(0); + + // Save currently active loop to hotcue slot 1 + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Seek to start of track + setCurrentSamplePosition(beforeLoopPositionSamples); + EXPECT_NEAR(beforeLoopPositionSamples, currentSamplePosition(), 2048); + + // Check that the previous seek disabled the loop + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Re-Enable loop + m_pHotcue1Activate->slotSet(1); + m_pHotcue1Activate->slotSet(0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Check that re-enabling loop didn't seek + EXPECT_NEAR(beforeLoopPositionSamples, currentSamplePosition(), 2048); + + // Disable loop + m_pHotcue1Activate->slotSet(1); + m_pHotcue1Activate->slotSet(0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); +} + +TEST_F(HotcueControlTest, SavedLoopActivate) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + m_pBeatloopSize->slotSet(4); + const double beatLengthSamples = getBeatLengthSamples(pTrack); + const double loopLengthSamples = m_pBeatloopSize->get() * beatLengthSamples; + + const double beforeLoopPositionSamples = 0; + const double loopStartPositionSamples = 8 * beatLengthSamples; + const double afterLoopPositionSamples = 16 * beatLengthSamples; + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Seek to loop start position + setCurrentSamplePosition(loopStartPositionSamples); + ProcessBuffer(); + + // Set a beatloop + m_pBeatloopActivate->slotSet(1); + m_pBeatloopActivate->slotSet(0); + + m_pPlay->set(1); + + // Save currently active loop to hotcue slot 1 + m_pHotcue1Activate->slotSet(1); + m_pHotcue1Activate->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Seek to start of track + setCurrentSamplePosition(beforeLoopPositionSamples); + double positionBeforeActivate = currentSamplePosition(); + EXPECT_NEAR(beforeLoopPositionSamples, currentSamplePosition(), 2000); + + // Check that the previous seek disabled the loop + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Activate saved loop (does not imply seeking to loop start) + m_pHotcue1Activate->slotSet(1); + m_pHotcue1Activate->slotSet(0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + EXPECT_NEAR(positionBeforeActivate, currentSamplePosition(), 2000); + + // Seek to position after saved loop + setCurrentSamplePosition(afterLoopPositionSamples); + ProcessBuffer(); + + // Check that the previous seek disabled the loop + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + + positionBeforeActivate = currentSamplePosition(); + + // Activate saved loop (usually doesn't imply seeking to loop start, but in this case it does + // because the play position is behind the loop end position) + m_pHotcue1Activate->slotSet(1); + m_pHotcue1Activate->slotSet(0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopStartPositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + EXPECT_NEAR(loopStartPositionSamples, currentSamplePosition(), 2000); +} + +TEST_F(HotcueControlTest, SavedLoopActivateWhilePlayingTogglesLoop) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + m_pBeatloopSize->slotSet(4); + const double beatLengthSamples = getBeatLengthSamples(pTrack); + const double loopLengthSamples = m_pBeatloopSize->get() * beatLengthSamples; + const double loopStartPosition = 8 * beatLengthSamples; + const double loopEndPosition = loopStartPosition + loopLengthSamples; + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Set a beatloop + setCurrentSamplePosition(loopStartPosition); + ProcessBuffer(); + m_pBeatloopActivate->slotSet(1); + m_pBeatloopActivate->slotSet(0); + + m_pQuantizeEnabled->slotSet(1); + m_pPlay->slotSet(1); + ProcessBuffer(); + + // Save currently active loop to hotcue slot 1 + m_pHotcue1Activate->slotSet(1); + m_pHotcue1Activate->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(loopStartPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopEndPosition, m_pHotcue1EndPosition->get()); + EXPECT_DOUBLE_EQ(1.0, m_pLoopEnabled->get()); + EXPECT_DOUBLE_EQ(m_pHotcue1Position->get(), m_pLoopStartPosition->get()); + EXPECT_DOUBLE_EQ(m_pHotcue1EndPosition->get(), m_pLoopEndPosition->get()); + + m_pHotcue1Activate->slotSet(1); + m_pHotcue1Activate->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(0.0, m_pLoopEnabled->get()); + + m_pHotcue1Activate->slotSet(1); + m_pHotcue1Activate->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(1.0, m_pLoopEnabled->get()); +} + +TEST_F(HotcueControlTest, SavedLoopBeatLoopSizeRestore) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + constexpr double savedLoopSize = 8; + m_pBeatloopSize->slotSet(savedLoopSize); + const double beatLengthSamples = getBeatLengthSamples(pTrack); + const double loopLengthSamples = m_pBeatloopSize->get() * beatLengthSamples; + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Set a beatloop + m_pBeatloopActivate->slotSet(1); + m_pBeatloopActivate->slotSet(0); + + m_pPlay->set(1); + + // Save currently active loop to hotcue slot 1 + m_pHotcue1ActivateLoop->slotSet(1); + m_pHotcue1ActivateLoop->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Disable loop + m_pLoopEnabled->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Set new beatloop size + m_pBeatloopSize->slotSet(savedLoopSize / 2); + + // Re-enabled saved loop + m_pHotcue1ActivateLoop->slotSet(1); + m_pHotcue1ActivateLoop->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Check that saved loop's beatloop size has been restored + EXPECT_DOUBLE_EQ(savedLoopSize, m_pBeatloopSize->get()); +} + +TEST_F(HotcueControlTest, SavedLoopBeatLoopSizeRestoreDoesNotJump) { + // Setup fake track with 120 bpm and calculate loop size + TrackPointer pTrack = loadTestTrackWithBpm(120.0); + + constexpr double savedLoopSize = 4; + m_pBeatloopSize->slotSet(savedLoopSize); + const double beatLengthSamples = getBeatLengthSamples(pTrack); + const double loopLengthSamples = m_pBeatloopSize->get() * beatLengthSamples; + const double cuePositionSamples = 8 * beatLengthSamples; + const double beforeLoopPositionSamples = 0; + const double afterLoopPositionSamples = beatLengthSamples; + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Seek to cue Position (8th beat) + setCurrentSamplePosition(cuePositionSamples); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(cuePositionSamples, currentSamplePosition()); + + // Set a beatloop + m_pBeatloopActivate->slotSet(1); + m_pBeatloopActivate->slotSet(0); + + // Save currently active loop to hotcue slot 1 + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + + m_pPlay->set(1); + + // Check 1: Play position before saved loop + + // Disable loop + m_pLoopEnabled->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Set new beatloop size + m_pBeatloopSize->slotSet(m_pBeatloopSize->get() / 2); + + // Seek to position before saved loop + setCurrentSamplePosition(beforeLoopPositionSamples); + + // Re-enable saved loop + m_pHotcue1Activate->slotSet(1); + m_pHotcue1Activate->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Check that saved loop's beatloop size has been restored + EXPECT_DOUBLE_EQ(savedLoopSize, m_pBeatloopSize->get()); + + // Check that enabling the loop didn't cause a jump + EXPECT_NEAR(beforeLoopPositionSamples, currentSamplePosition(), 2000); + + // Check 2: Play position after saved loop + + // Disable loop + m_pLoopEnabled->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Set), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Set new beatloop size + m_pBeatloopSize->slotSet(m_pBeatloopSize->get() / 2); + + // Seek to position after saved loop + setCurrentSamplePosition(afterLoopPositionSamples); + + // Re-enable saved loop + m_pHotcue1Activate->slotSet(1); + m_pHotcue1Activate->slotSet(0); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(cuePositionSamples + loopLengthSamples, m_pHotcue1EndPosition->get()); + + // Check that saved loop's beatloop size has been restored + EXPECT_DOUBLE_EQ(savedLoopSize, m_pBeatloopSize->get()); + + // Check that enabling the loop didn't cause a jump + EXPECT_NEAR(afterLoopPositionSamples, currentSamplePosition(), 2000); +} + +TEST_F(HotcueControlTest, SavedLoopUnloadTrackWhileActive) { + // Setup fake track with 120 bpm + qWarning() << "Loading first track"; + loadTestTrackWithBpm(120.0); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Set a beatloop + m_pBeatloopActivate->slotSet(1); + m_pBeatloopActivate->slotSet(0); + ProcessBuffer(); + + // Save currently active loop to hotcue slot 1 + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_NE(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_NE(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Setup another fake track with 130 bpm + unloadTrack(); + qWarning() << "Loading second track"; + loadTestTrackWithBpm(130.0); + ProcessBuffer(); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); +} + +TEST_F(HotcueControlTest, SavedLoopUseLoopInOutWhileActive) { + std::unique_ptr pLoopIn = std::make_unique(m_sGroup1, "loop_in"); + std::unique_ptr pLoopOut = std::make_unique(m_sGroup1, "loop_out"); + + // Setup fake track with 120 bpm + loadTestTrackWithBpm(120.0); + + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Empty), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + // Set a beatloop + m_pBeatloopActivate->slotSet(1); + m_pBeatloopActivate->slotSet(0); + ProcessBuffer(); + + // Save currently active loop to hotcue slot 1 + m_pHotcue1SetLoop->slotSet(1); + m_pHotcue1SetLoop->slotSet(0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_NE(Cue::kNoPosition, m_pHotcue1Position->get()); + EXPECT_NE(Cue::kNoPosition, m_pHotcue1EndPosition->get()); + + setCurrentSamplePosition(0); + + pLoopIn->slotSet(1); + pLoopIn->slotSet(0); + ProcessBuffer(); + + setCurrentSamplePosition(1000); + + pLoopOut->slotSet(1); + pLoopOut->slotSet(0); + + ProcessBuffer(); + EXPECT_DOUBLE_EQ(static_cast(HotcueControl::Status::Active), m_pHotcue1Enabled->get()); + EXPECT_DOUBLE_EQ(0, m_pHotcue1Position->get()); + EXPECT_DOUBLE_EQ(1000, m_pHotcue1EndPosition->get()); +} diff --git a/src/track/cue.h b/src/track/cue.h index 471104f2e680..dfdccc16db53 100644 --- a/src/track/cue.h +++ b/src/track/cue.h @@ -56,8 +56,7 @@ class Cue : public QObject { int hotCue = kNoHotCue); QString getLabel() const; - void setLabel( - QString label = QString()); + void setLabel(QString label); mixxx::RgbColor getColor() const; void setColor(mixxx::RgbColor color); diff --git a/src/util/color/predefinedcolorpalettes.cpp b/src/util/color/predefinedcolorpalettes.cpp index 19f9e355835f..d953686824ba 100644 --- a/src/util/color/predefinedcolorpalettes.cpp +++ b/src/util/color/predefinedcolorpalettes.cpp @@ -315,4 +315,7 @@ const QList PredefinedColorPalettes::kPalettes{ const mixxx::RgbColor PredefinedColorPalettes::kDefaultCueColor = kSchemaMigrationReplacementColor; +const mixxx::RgbColor PredefinedColorPalettes::kDefaultLoopColor = + kColorMixxxWhite; + } // namespace mixxx diff --git a/src/util/color/predefinedcolorpalettes.h b/src/util/color/predefinedcolorpalettes.h index 482afb3729a8..bbcd7ee63e20 100644 --- a/src/util/color/predefinedcolorpalettes.h +++ b/src/util/color/predefinedcolorpalettes.h @@ -20,6 +20,7 @@ class PredefinedColorPalettes { static const QList kPalettes; static const mixxx::RgbColor kDefaultCueColor; + static const mixxx::RgbColor kDefaultLoopColor; }; } // namespace mixxx diff --git a/src/waveform/renderers/waveformmark.cpp b/src/waveform/renderers/waveformmark.cpp index 12fe909e62a0..dce3b975a693 100644 --- a/src/waveform/renderers/waveformmark.cpp +++ b/src/waveform/renderers/waveformmark.cpp @@ -56,20 +56,26 @@ WaveformMark::WaveformMark(const QString& group, const WaveformSignalColors& signalColors, int hotCue) : m_iHotCue(hotCue) { - QString control; + QString positionControl; + QString endPositionControl; if (hotCue != Cue::kNoHotCue) { - control = "hotcue_" + QString::number(hotCue + 1) + "_position"; + positionControl = "hotcue_" + QString::number(hotCue + 1) + "_position"; + endPositionControl = "hotcue_" + QString::number(hotCue + 1) + "_endposition"; } else { - control = context.selectString(node, "Control"); + positionControl = context.selectString(node, "Control"); } - if (!control.isEmpty()) { - m_pPointCos = std::make_unique(group, control); + + if (!positionControl.isEmpty()) { + m_pPositionCO = std::make_unique(group, positionControl); + } + if (!endPositionControl.isEmpty()) { + m_pEndPositionCO = std::make_unique(group, endPositionControl); } QString visibilityControl = context.selectString(node, "VisibilityControl"); if (!visibilityControl.isEmpty()) { ConfigKey key = ConfigKey::parseCommaSeparated(visibilityControl); - m_pVisibleCos = std::make_unique(key); + m_pVisibleCO = std::make_unique(key); } QColor color(context.selectString(node, "Color")); diff --git a/src/waveform/renderers/waveformmark.h b/src/waveform/renderers/waveformmark.h index 9face72cabcb..caa6b3fa814d 100644 --- a/src/waveform/renderers/waveformmark.h +++ b/src/waveform/renderers/waveformmark.h @@ -30,28 +30,48 @@ class WaveformMark { int getHotCue() const { return m_iHotCue; }; - //The m_pPointCos related function - bool isValid() const { return m_pPointCos && m_pPointCos->valid(); } + //The m_pPositionCO related function + bool isValid() const { + return m_pPositionCO && m_pPositionCO->valid(); + } template void connectSamplePositionChanged(Receiver receiver, Slot slot) const { - m_pPointCos->connectValueChanged(receiver, slot, Qt::AutoConnection); + m_pPositionCO->connectValueChanged(receiver, slot, Qt::AutoConnection); + }; + template + void connectSampleEndPositionChanged(Receiver receiver, Slot slot) const { + if (m_pEndPositionCO) { + m_pEndPositionCO->connectValueChanged(receiver, slot, Qt::AutoConnection); + } }; - double getSamplePosition() const { return m_pPointCos->get(); } - QString getItem() const { return m_pPointCos->getKey().item; } + double getSamplePosition() const { + return m_pPositionCO->get(); + } + double getSampleEndPosition() const { + if (m_pEndPositionCO) { + return m_pEndPositionCO->get(); + } + return Cue::kNoPosition; + } + QString getItem() const { + return m_pPositionCO->getKey().item; + } - // The m_pVisibleCos related function - bool hasVisible() const { return m_pVisibleCos && m_pVisibleCos->valid(); } + // The m_pVisibleCO related function + bool hasVisible() const { + return m_pVisibleCO && m_pVisibleCO->valid(); + } bool isVisible() const { if (!hasVisible()) { return true; } - return m_pVisibleCos->toBool(); + return m_pVisibleCO->toBool(); } template void connectVisibleChanged(Receiver receiver, Slot slot) const { - m_pVisibleCos->connectValueChanged(receiver, slot, Qt::AutoConnection); + m_pVisibleCO->connectValueChanged(receiver, slot, Qt::AutoConnection); } // Sets the appropriate mark colors based on the base color @@ -79,8 +99,9 @@ class WaveformMark { WaveformMarkLabel m_label; private: - std::unique_ptr m_pPointCos; - std::unique_ptr m_pVisibleCos; + std::unique_ptr m_pPositionCO; + std::unique_ptr m_pEndPositionCO; + std::unique_ptr m_pVisibleCO; int m_iHotCue; QImage m_image; diff --git a/src/waveform/renderers/waveformrendermark.cpp b/src/waveform/renderers/waveformrendermark.cpp index ba974cd5e793..ae9b6366de1f 100644 --- a/src/waveform/renderers/waveformrendermark.cpp +++ b/src/waveform/renderers/waveformrendermark.cpp @@ -57,30 +57,95 @@ void WaveformRenderMark::draw(QPainter* painter, QPaintEvent* /*event*/) { generateMarkImage(pMark); } - double samplePosition = pMark->getSamplePosition(); - if (samplePosition != -1.0) { - double currentMarkPoint = + const double samplePosition = pMark->getSamplePosition(); + if (samplePosition != Cue::kNoPosition) { + const double currentMarkPoint = m_waveformRenderer->transformSamplePositionInRendererWorld(samplePosition); + const double sampleEndPosition = pMark->getSampleEndPosition(); if (m_waveformRenderer->getOrientation() == Qt::Horizontal) { // NOTE: vRince I guess image width is odd to display the center on the exact line ! // external image should respect that ... const int markHalfWidth = static_cast(pMark->m_image.width() / 2.0 / m_waveformRenderer->getDevicePixelRatio()); + const int drawOffset = static_cast(currentMarkPoint) - markHalfWidth; + bool visible = false; // Check if the current point needs to be displayed. if (currentMarkPoint > -markHalfWidth && currentMarkPoint < m_waveformRenderer->getWidth() + markHalfWidth) { - const int drawOffset = static_cast(currentMarkPoint) - markHalfWidth; painter->drawImage(drawOffset, 0, pMark->m_image); + visible = true; + } + + // Check if the range needs to be displayed. + if (sampleEndPosition != Cue::kNoPosition) { + DEBUG_ASSERT(samplePosition < sampleEndPosition); + const double currentMarkEndPoint = + m_waveformRenderer->transformSamplePositionInRendererWorld( + sampleEndPosition); + if (visible || currentMarkEndPoint > 0) { + QColor color = pMark->fillColor(); + color.setAlphaF(0.4); + + QLinearGradient gradient(QPointF(0, 0), + QPointF(0, m_waveformRenderer->getHeight())); + gradient.setColorAt(0, color); + gradient.setColorAt(0.25, QColor(Qt::transparent)); + gradient.setColorAt(0.75, QColor(Qt::transparent)); + gradient.setColorAt(1, color); + painter->fillRect( + QRectF(QPointF(currentMarkPoint, 0), + QPointF(currentMarkEndPoint, + m_waveformRenderer + ->getHeight())), + QBrush(gradient)); + visible = true; + } + } + + if (visible) { marksOnScreen[pMark] = drawOffset; } } else { const int markHalfHeight = static_cast(pMark->m_image.height() / 2.0); + const int drawOffset = static_cast(currentMarkPoint) - markHalfHeight; + + bool visible = false; + // Check if the current point needs to be displayed. if (currentMarkPoint > -markHalfHeight && currentMarkPoint < m_waveformRenderer->getHeight() + markHalfHeight) { - const int drawOffset = static_cast(currentMarkPoint) - markHalfHeight; - painter->drawImage(0, drawOffset, pMark->m_image); + painter->drawImage(drawOffset, 0, pMark->m_image); + visible = true; + } + + // Check if the range needs to be displayed. + if (sampleEndPosition != Cue::kNoPosition) { + DEBUG_ASSERT(samplePosition < sampleEndPosition); + double currentMarkEndPoint = + m_waveformRenderer + ->transformSamplePositionInRendererWorld( + sampleEndPosition); + if (currentMarkEndPoint < m_waveformRenderer->getHeight()) { + QColor color = pMark->fillColor(); + color.setAlphaF(0.4); + + QLinearGradient gradient(QPointF(0, 0), + QPointF(m_waveformRenderer->getWidth(), 0)); + gradient.setColorAt(0, color); + gradient.setColorAt(0.25, QColor(Qt::transparent)); + gradient.setColorAt(0.75, QColor(Qt::transparent)); + gradient.setColorAt(1, color); + painter->fillRect( + QRectF(QPointF(0, currentMarkPoint), + QPointF(m_waveformRenderer->getWidth(), + currentMarkEndPoint)), + QBrush(gradient)); + visible = true; + } + } + + if (visible) { marksOnScreen[pMark] = drawOffset; } } diff --git a/src/widget/whotcuebutton.cpp b/src/widget/whotcuebutton.cpp index 667984c40e46..b5eb6584a4aa 100644 --- a/src/widget/whotcuebutton.cpp +++ b/src/widget/whotcuebutton.cpp @@ -57,6 +57,13 @@ void WHotcueButton::setup(const QDomNode& node, const SkinContext& context) { m_pCoColor->connectValueChanged(this, &WHotcueButton::slotColorChanged); slotColorChanged(m_pCoColor->get()); + m_pCoType = make_parented( + createConfigKey(QStringLiteral("type")), + this, + ControlFlag::NoAssertIfMissing); + m_pCoType->connectValueChanged(this, &WHotcueButton::slotTypeChanged); + slotTypeChanged(m_pCoType->get()); + auto pLeftConnection = new ControlParameterWidgetConnection( this, createConfigKey(QStringLiteral("activate")), @@ -83,7 +90,7 @@ void WHotcueButton::setup(const QDomNode& node, const SkinContext& context) { void WHotcueButton::mousePressEvent(QMouseEvent* e) { const bool rightClick = e->button() == Qt::RightButton; if (rightClick) { - if (readDisplayValue() == 1) { + if (readDisplayValue()) { // hot cue is set TrackPointer pTrack = PlayerInfo::instance().getTrackInfo(m_group); if (!pTrack) { @@ -132,13 +139,17 @@ void WHotcueButton::slotColorChanged(double color) { m_bCueColorDimmed = Color::isDimColorCustom(cueColor, m_cueColorDimThreshold); QString style = - QStringLiteral("WWidget[displayValue=\"1\"] { background-color: ") + + QStringLiteral( + "WWidget[displayValue=\"1\"], " + "WWidget[displayValue=\"2\"] { background-color: ") + cueColor.name() + QStringLiteral("; }"); if (m_hoverCueColor) { style += - QStringLiteral("WWidget[displayValue=\"1\"]:hover { background-color: ") + + QStringLiteral( + "WWidget[displayValue=\"1\"]:hover, " + "WWidget[displayValue=\"2\"]:hover { background-color: ") + cueColor.lighter(m_bCueColorDimmed ? 120 : 80).name() + QStringLiteral("; }"); } @@ -147,6 +158,46 @@ void WHotcueButton::slotColorChanged(double color) { restyleAndRepaint(); } +void WHotcueButton::slotTypeChanged(double type) { + // If the cast is put directly into the switch case, this seems to trigger + // a false positive warning on gcc 7.5.0 on Ubuntu 18.04.4 Bionic, so we cast + // it to int first and save to a local const variable. + const mixxx::CueType cueType = static_cast(static_cast(type)); + switch (cueType) { + case mixxx::CueType::Invalid: + m_type = QStringLiteral(""); + break; + case mixxx::CueType::HotCue: + m_type = QStringLiteral("hotcue"); + break; + case mixxx::CueType::MainCue: + m_type = QStringLiteral("maincue"); + break; + case mixxx::CueType::Beat: + m_type = QStringLiteral("beat"); + break; + case mixxx::CueType::Loop: + m_type = QStringLiteral("loop"); + break; + case mixxx::CueType::Jump: + m_type = QStringLiteral("jump"); + break; + case mixxx::CueType::Intro: + m_type = QStringLiteral("intro"); + break; + case mixxx::CueType::Outro: + m_type = QStringLiteral("outro"); + break; + case mixxx::CueType::AudibleSound: + m_type = QStringLiteral("audiblesound"); + break; + default: + DEBUG_ASSERT(!"Unknown cue type!"); + m_type = QStringLiteral(""); + } + restyleAndRepaint(); +} + void WHotcueButton::restyleAndRepaint() { if (readDisplayValue()) { // Adjust properties for Qss file diff --git a/src/widget/whotcuebutton.h b/src/widget/whotcuebutton.h index d7a3d218d48b..4afd4a706852 100644 --- a/src/widget/whotcuebutton.h +++ b/src/widget/whotcuebutton.h @@ -2,6 +2,7 @@ #include #include +#include #include #include "skin/skincontext.h" @@ -18,6 +19,7 @@ class WHotcueButton : public WPushButton { Q_PROPERTY(bool light MEMBER m_bCueColorIsLight); Q_PROPERTY(bool dark MEMBER m_bCueColorIsDark); + Q_PROPERTY(QString type MEMBER m_type); protected: void mousePressEvent(QMouseEvent* e) override; @@ -25,6 +27,7 @@ class WHotcueButton : public WPushButton { private slots: void slotColorChanged(double color); + void slotTypeChanged(double type); private: ConfigKey createConfigKey(const QString& name); @@ -34,9 +37,11 @@ class WHotcueButton : public WPushButton { int m_hotcue; bool m_hoverCueColor; parented_ptr m_pCoColor; + parented_ptr m_pCoType; parented_ptr m_pCueMenuPopup; int m_cueColorDimThreshold; bool m_bCueColorDimmed; bool m_bCueColorIsLight; bool m_bCueColorIsDark; + QString m_type; }; diff --git a/src/widget/woverview.cpp b/src/widget/woverview.cpp index 1f2416642574..2d3b082ce218 100644 --- a/src/widget/woverview.cpp +++ b/src/widget/woverview.cpp @@ -165,6 +165,8 @@ void WOverview::setup(const QDomNode& node, const SkinContext& context) { if (pMark->isValid()) { pMark->connectSamplePositionChanged(this, &WOverview::onMarkChanged); + pMark->connectSampleEndPositionChanged(this, + &WOverview::onMarkChanged); } if (pMark->hasVisible()) { pMark->connectVisibleChanged(this, @@ -395,7 +397,9 @@ void WOverview::updateCues(const QList &loadedCues) { } int hotcueNumber = currentCue->getHotCue(); - if (currentCue->getType() == mixxx::CueType::HotCue && hotcueNumber != Cue::kNoHotCue) { + if ((currentCue->getType() == mixxx::CueType::HotCue || + currentCue->getType() == mixxx::CueType::Loop) && + hotcueNumber != Cue::kNoHotCue) { // Prepend the hotcue number to hotcues' labels QString newLabel = currentCue->getLabel(); if (newLabel.isEmpty()) { @@ -810,8 +814,9 @@ void WOverview::drawMarks(QPainter* pPainter, const float offset, const float ga WaveformMarkPointer pMark = m_marksToRender.at(i); PainterScope painterScope(pPainter); + double samplePosition = m_marksToRender.at(i)->getSamplePosition(); const float markPosition = math_clamp( - offset + static_cast(m_marksToRender.at(i)->getSamplePosition()) * gain, + offset + static_cast(samplePosition) * gain, 0.0f, static_cast(width())); pMark->m_linePosition = markPosition; @@ -826,12 +831,33 @@ void WOverview::drawMarks(QPainter* pPainter, const float offset, const float ga bgLine.setLine(0.0, markPosition - 1.0, width(), markPosition - 1.0); } + QRectF rect; + double sampleEndPosition = m_marksToRender.at(i)->getSampleEndPosition(); + if (sampleEndPosition > 0) { + const float markEndPosition = math_clamp( + offset + static_cast(sampleEndPosition) * gain, + 0.0f, + static_cast(width())); + + if (m_orientation == Qt::Horizontal) { + rect.setCoords(markPosition, 0, markEndPosition, height()); + } else { + rect.setCoords(0, markPosition, width(), markEndPosition); + } + } + pPainter->setPen(pMark->borderColor()); pPainter->drawLine(bgLine); pPainter->setPen(pMark->fillColor()); pPainter->drawLine(line); + if (rect.isValid()) { + QColor loopColor = pMark->fillColor(); + loopColor.setAlphaF(0.5); + pPainter->fillRect(rect, loopColor); + } + if (!pMark->m_text.isEmpty()) { Qt::Alignment halign = pMark->m_align & Qt::AlignHorizontal_Mask; Qt::Alignment valign = pMark->m_align & Qt::AlignVertical_Mask;