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