diff --git a/res/skins/LateNight/classic/buttons/btn__beatloop_anchor_end.svg b/res/skins/LateNight/classic/buttons/btn__beatloop_anchor_end.svg
new file mode 100644
index 00000000000..13983d2fba2
--- /dev/null
+++ b/res/skins/LateNight/classic/buttons/btn__beatloop_anchor_end.svg
@@ -0,0 +1,67 @@
+
+
diff --git a/res/skins/LateNight/classic/buttons/btn__beatloop_anchor_start.svg b/res/skins/LateNight/classic/buttons/btn__beatloop_anchor_start.svg
new file mode 100644
index 00000000000..74cf1c64157
--- /dev/null
+++ b/res/skins/LateNight/classic/buttons/btn__beatloop_anchor_start.svg
@@ -0,0 +1,95 @@
+
+
diff --git a/res/skins/LateNight/controls/button_2state_display.xml b/res/skins/LateNight/controls/button_2state_display.xml
new file mode 100644
index 00000000000..52cfa56853f
--- /dev/null
+++ b/res/skins/LateNight/controls/button_2state_display.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+ 2
+ false
+
+ 0
+
+
+ skins:LateNight//buttons/btn__.svg
+ skins:LateNight//buttons/btn___active.svg
+
+
+ 1
+
+
+ skins:LateNight//buttons/btn___active.svg
+ skins:LateNight//buttons/btn___active.svg
+
+
+
+ LeftButton
+
+
+
+ false
+
+
+
diff --git a/res/skins/LateNight/decks/row_5_transportLoopJump.xml b/res/skins/LateNight/decks/row_5_transportLoopJump.xml
index 8a5c8ac5a9c..028dd403068 100644
--- a/res/skins/LateNight/decks/row_5_transportLoopJump.xml
+++ b/res/skins/LateNight/decks/row_5_transportLoopJump.xml
@@ -230,7 +230,7 @@
vertical
min,min
78,52
- 86,52
+ 104,52
AlignLeftTop
@@ -290,6 +290,14 @@
,loop_out
,loop_out_goto
+
+
+ beatloop_anchor
+ BeatLoopAnchor
+ 26f,26f
+ ,beatloop_anchor_toggle
+ ,beatloop_anchor
+
diff --git a/res/skins/LateNight/style_palemoon.qss b/res/skins/LateNight/style_palemoon.qss
index 915e54e762a..f2d17717d91 100644
--- a/res/skins/LateNight/style_palemoon.qss
+++ b/res/skins/LateNight/style_palemoon.qss
@@ -1669,6 +1669,7 @@ QPushButton#pushButtonAnalyze:checked {
#SyncLeader[value="1"],
WPushButton#LoopIn[pressed="true"],
WPushButton#LoopOut[pressed="true"],
+WPushButton#BeatLoopAnchor[pressed="true"],
#BeatjumpControls WPushButton[value="1"],
#RateControls WPushButton[value="1"],
#BeatgridControls WPushButton[pressed="true"]/*,
@@ -2076,6 +2077,12 @@ WPushButton#PlayDeck[value="0"] {
#LoopOut[pressed="true"] {
image: url(skins:LateNight/palemoon/buttons/btn__loop_out_active.svg) no-repeat center center;
}
+#BeatLoopAnchor[displayValue="0"] {
+ image: url(skins:LateNight/classic/buttons/btn__beatloop_anchor_start.svg) no-repeat center center;
+ }
+ #BeatLoopAnchor[displayValue="1"] {
+ image: url(skins:LateNight/classic/buttons/btn__beatloop_anchor_end.svg) no-repeat center center;
+ }
#JumpForward {
image: url(skins:LateNight/palemoon/buttons/btn__beatjump_right.svg) no-repeat center center;
diff --git a/src/controllers/controlpickermenu.cpp b/src/controllers/controlpickermenu.cpp
index d5a2de9f85d..32beb65952c 100644
--- a/src/controllers/controlpickermenu.cpp
+++ b/src/controllers/controlpickermenu.cpp
@@ -628,6 +628,15 @@ ControlPickerMenu::ControlPickerMenu(QWidget* pParent)
QString beatJumpBackwardDescription = tr("Jump backward by %1 beats, or if a loop is enabled, move the loop backward %1 beats");
addDeckControl("beatjump_forward", tr("Beat Jump / Loop Move Forward Selected Beats"), tr("Jump forward by the selected number of beats, or if a loop is enabled, move the loop forward by the selected number of beats"), beatJumpMenu);
addDeckControl("beatjump_backward", tr("Beat Jump / Loop Move Backward Selected Beats"), tr("Jump backward by the selected number of beats, or if a loop is enabled, move the loop backward by the selected number of beats"), beatJumpMenu);
+ addDeckControl("beatjump_anchor",
+ tr("Beat Jump"),
+ tr("Indicate which loop marker remain static when adjusting the "
+ "size or is inherited from the current position"),
+ beatJumpMenu);
+ addDeckControl("beatjump_anchor_toggle",
+ tr("Beat Jump"),
+ tr("Toggle the loop marker anchor"),
+ beatJumpMenu);
beatJumpMenu->addSeparator();
QMenu* beatjumpFwdSubmenu = addSubmenu(tr("Beat Jump / Loop Move Forward"), beatJumpMenu);
diff --git a/src/engine/controls/loopingcontrol.cpp b/src/engine/controls/loopingcontrol.cpp
index 84b2f5718c2..6f7a9db3f53 100644
--- a/src/engine/controls/loopingcontrol.cpp
+++ b/src/engine/controls/loopingcontrol.cpp
@@ -136,6 +136,29 @@ LoopingControl::LoopingControl(const QString& group,
this,
[this](double value) { slotBeatLoop(value); },
Qt::DirectConnection);
+ m_pCOBeatLoopAnchor = new ControlObject(ConfigKey(group, "beatloop_anchor"),
+ true,
+ false,
+ false,
+ static_cast(LoopAnchorPoint::Start));
+ m_pCOBeatLoopAnchor->connectValueChangeRequest(this,
+ &LoopingControl::slotBeatLoopAnchorChangeRequest,
+ Qt::DirectConnection);
+
+ m_pCOBeatLoopAnchorToggle = new ControlObject(
+ ConfigKey(group, "beatloop_anchor_toggle"), false);
+ connect(
+ m_pCOBeatLoopAnchorToggle,
+ &ControlObject::valueChanged,
+ this,
+ [this](double value) {
+ if (value > 0) {
+ slotBeatLoopAnchorChangeRequest(static_cast(
+ static_cast(m_pCOBeatLoopAnchor->get() + 1.0) %
+ 2));
+ }
+ },
+ Qt::DirectConnection);
m_pCOBeatLoopSize = new ControlObject(ConfigKey(group, "beatloop_size"),
true, false, false, 4.0);
@@ -269,6 +292,8 @@ LoopingControl::~LoopingControl() {
delete pBeatLoop;
}
delete m_pCOBeatLoopSize;
+ delete m_pCOBeatLoopAnchor;
+ delete m_pCOBeatLoopAnchorToggle;
delete m_pCOBeatLoopActivate;
delete m_pCOBeatLoopRollActivate;
@@ -1396,9 +1421,9 @@ mixxx::audio::FramePos LoopingControl::findQuantizedBeatloopStart(
return previousFractionBeatPosition + loopLength;
}
-void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable) {
+void LoopingControl::slotBeatLoop(double beats, bool keepSetPoint, bool enable) {
// If this is a "new" loop, stop tracking saved loop changes
- if (!keepStartPoint) {
+ if (!keepSetPoint) {
emit loopReset();
}
@@ -1430,6 +1455,7 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable
return;
}
+ const LoopAnchorPoint loopAnchor = static_cast(m_pCOBeatLoopAnchor->get());
// Calculate the new loop start and end positions
// give start and end defaults so we can detect problems
LoopInfo newloopInfo = {mixxx::audio::kInvalidFramePos,
@@ -1439,12 +1465,18 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable
mixxx::audio::FramePos currentPosition = info.currentPosition;
// Start from the current position/closest beat and
// create the loop around X beats from there.
- if (keepStartPoint) {
- if (loopInfo.startPosition.isValid()) {
- newloopInfo.startPosition = loopInfo.startPosition;
- } else {
- newloopInfo.startPosition =
- math_min(info.currentPosition, info.trackEndPosition);
+ if (keepSetPoint) {
+ switch (loopAnchor) {
+ case LoopAnchorPoint::Start:
+ newloopInfo.startPosition = loopInfo.startPosition.isValid()
+ ? loopInfo.startPosition
+ : math_min(info.currentPosition, info.trackEndPosition);
+ break;
+ case LoopAnchorPoint::End:
+ newloopInfo.endPosition = loopInfo.endPosition.isValid()
+ ? loopInfo.endPosition
+ : math_min(info.currentPosition, info.trackEndPosition);
+ break;
}
} else {
// If running reverse, move the loop one loop size to the left.
@@ -1458,23 +1490,42 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable
currentPosition = pBeats->findNBeatsFromPosition(currentPosition, -beats);
}
- if (!m_pQuantizeEnabled->toBool()) {
- newloopInfo.startPosition = currentPosition;
- } else {
- // loop_in is set to the closest beat if quantize is on and the loop size is >= 1 beat.
- // The closest beat might be ahead of play position and will cause a catching loop.
- newloopInfo.startPosition = findQuantizedBeatloopStart(pBeats, currentPosition, beats);
+ bool quantize = m_pQuantizeEnabled->toBool();
+ // loop_in is set to the closest beat if quantize is on and the loop size is >= 1 beat.
+ // The closest beat might be ahead of play position and will cause a catching loop.
+ switch (loopAnchor) {
+ case LoopAnchorPoint::Start:
+ newloopInfo.startPosition = !quantize
+ ? currentPosition
+ : findQuantizedBeatloopStart(
+ pBeats, currentPosition, beats);
+ break;
+ case LoopAnchorPoint::End:
+ newloopInfo.endPosition = !quantize
+ ? currentPosition
+ : findQuantizedBeatloopStart(
+ pBeats, currentPosition, beats);
+ break;
}
}
- newloopInfo.endPosition = pBeats->findNBeatsFromPosition(newloopInfo.startPosition, beats);
+ switch (loopAnchor) {
+ case LoopAnchorPoint::Start:
+ newloopInfo.endPosition = pBeats->findNBeatsFromPosition(newloopInfo.startPosition, beats);
+ break;
+ case LoopAnchorPoint::End:
+ newloopInfo.startPosition = pBeats->findNBeatsFromPosition(newloopInfo.endPosition, -beats);
+ break;
+ }
if (!newloopInfo.startPosition.isValid() ||
!newloopInfo.endPosition.isValid() ||
newloopInfo.startPosition >=
newloopInfo.endPosition // happens when the call above fails
- || newloopInfo.endPosition >
- trackEndPosition) { // Do not allow beat loops to go beyond the end of the track
+ || (newloopInfo.endPosition > trackEndPosition &&
+ (enable || m_bLoopingEnabled))) { // Do not allow beat
+ // loops to go beyond
+ // the end of the track
// If a track is loaded with beatloop_size larger than
// the distance between the loop in point and
// the end of the track, let beatloop_size be set to
@@ -1513,7 +1564,8 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable
// If the start point has changed, or the loop is not enabled,
// or if the endpoints are nearly the same, do not seek forward into the adjusted loop.
- if (!keepStartPoint ||
+ if (!keepSetPoint ||
+ loopAnchor == LoopAnchorPoint::End ||
!(enable || m_bLoopingEnabled) ||
(positionNear(newloopInfo.startPosition, loopInfo.startPosition) &&
positionNear(newloopInfo.endPosition, loopInfo.endPosition))) {
@@ -1532,6 +1584,17 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable
updateBeatLoopingControls();
}
+void LoopingControl::slotBeatLoopAnchorChangeRequest(double anchor) {
+ if (anchor == 0.0) {
+ m_pCOBeatLoopAnchor->setAndConfirm(static_cast(LoopAnchorPoint::Start));
+ } else if (anchor == 1.0) {
+ m_pCOBeatLoopAnchor->setAndConfirm(static_cast(LoopAnchorPoint::End));
+ } else {
+ qWarning()
+ << anchor << "is not valid value for 'beatloop_anchor'";
+ }
+}
+
void LoopingControl::slotBeatLoopSizeChangeRequest(double beats) {
// slotBeatLoop will call m_pCOBeatLoopSize->setAndConfirm if
// new beatloop_size is valid
diff --git a/src/engine/controls/loopingcontrol.h b/src/engine/controls/loopingcontrol.h
index 96167a41927..51049d75291 100644
--- a/src/engine/controls/loopingcontrol.h
+++ b/src/engine/controls/loopingcontrol.h
@@ -63,6 +63,13 @@ class LoopingControl : public EngineControl {
None,
};
+ enum class LoopAnchorPoint {
+ Start, // The loop has been defined by its start point. Adjusting the
+ // size will move the end point
+ End, // The loop has been defined by its end point. Adjusting the size
+ // will move the end point
+ };
+
struct LoopInfo {
mixxx::audio::FramePos startPosition;
mixxx::audio::FramePos endPosition;
@@ -114,8 +121,9 @@ class LoopingControl : public EngineControl {
// Generate a loop of 'beats' length. It can also do fractions for a
// beatslicing effect.
- void slotBeatLoop(double loopSize, bool keepStartPoint=false, bool enable=true);
+ void slotBeatLoop(double loopSize, bool keepSetPoint = false, bool enable = true);
void slotBeatLoopSizeChangeRequest(double beats);
+ void slotBeatLoopAnchorChangeRequest(double anchor);
void slotBeatLoopToggle(double pressed);
void slotBeatLoopRollActivate(double pressed);
void slotBeatLoopActivate(BeatLoopingControl* pBeatLoopControl);
@@ -208,6 +216,8 @@ class LoopingControl : public EngineControl {
// Base BeatLoop Control Object.
ControlObject* m_pCOBeatLoop;
ControlObject* m_pCOBeatLoopSize;
+ ControlObject* m_pCOBeatLoopAnchor;
+ ControlObject* m_pCOBeatLoopAnchorToggle;
// Different sizes for Beat Loops/Seeks.
static double s_dBeatSizes[];
// Array of BeatLoopingControls, one for each size.
diff --git a/src/skin/legacy/tooltips.cpp b/src/skin/legacy/tooltips.cpp
index 388f8b49da8..b65637ab44f 100644
--- a/src/skin/legacy/tooltips.cpp
+++ b/src/skin/legacy/tooltips.cpp
@@ -741,6 +741,11 @@ void Tooltips::addStandardTooltips() {
<< QString("%1: %2").arg(rightClick, tr("Temporarily enable a rolling loop over the set number of beats."))
<< tr("Playback will resume where the track would have been if it had not entered the loop.");
+ add("beatloop_anchor")
+ << tr("Beatloop")
+ << tr("Define whether the loop is defined and adjusted from its "
+ "staring point or ending point.");
+
add("beatjump_size")
<< tr("Beatjump/Loop Move Size")
<< tr("Select the number of beats to jump or move the loop with the Beatjump Forward/Backward buttons.");
diff --git a/src/test/controller_mapping_validation_test.cpp b/src/test/controller_mapping_validation_test.cpp
index 29be36d171c..21efd2aff98 100644
--- a/src/test/controller_mapping_validation_test.cpp
+++ b/src/test/controller_mapping_validation_test.cpp
@@ -123,7 +123,7 @@ bool LegacyControllerMappingValidationTest::testLoadMapping(const MappingInfo& m
FakeController controller;
controller.setMapping(pMapping);
- bool result = controller.applyMapping();
+ bool result = controller.applyMapping("");
controller.stopEngine();
return result;
}
diff --git a/src/test/looping_control_test.cpp b/src/test/looping_control_test.cpp
index ff3eeafd0fc..04e7bcd05cc 100644
--- a/src/test/looping_control_test.cpp
+++ b/src/test/looping_control_test.cpp
@@ -716,12 +716,28 @@ TEST_F(LoopingControlTest, BeatLoopSize_IgnoresPastTrackEnd) {
m_pTrack1->trySetBpm(60.0);
setCurrentPosition(mixxx::audio::FramePos::fromEngineSamplePosMaybeInvalid(
m_pTrackSamples->get()) -
- 200);
+ 44100);
+ m_pBeatLoopSize->set(0.5);
+ m_pButtonBeatLoopActivate->set(1.0);
+ m_pButtonBeatLoopActivate->set(0.0);
+ EXPECT_TRUE(m_pLoopEnabled->toBool());
m_pBeatLoopSize->set(64.0);
EXPECT_NE(64.0, m_pBeatLoopSize->get());
EXPECT_FALSE(m_pBeatLoop64Enabled->toBool());
}
+TEST_F(LoopingControlTest, BeatLoopSize_SetPastTrackEndIfLoopInactive) {
+ // TODO: actually calculate that the beatloop would go beyond
+ // the end of the track
+ m_pTrack1->trySetBpm(60.0);
+ setCurrentPosition(mixxx::audio::FramePos::fromEngineSamplePosMaybeInvalid(
+ m_pTrackSamples->get()) -
+ 44100);
+ m_pBeatLoopSize->set(64.0);
+ EXPECT_EQ(64.0, m_pBeatLoopSize->get());
+ EXPECT_FALSE(m_pBeatLoop64Enabled->toBool());
+}
+
TEST_F(LoopingControlTest, BeatLoopSize_SetsNumberedControls) {
m_pTrack1->trySetBpm(120.0);
m_pBeatLoopSize->set(2.0);