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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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;