Skip to content

Commit

Permalink
Merge pull request #12961 from ronso0/loop-jump-seconds-with-no-beats
Browse files Browse the repository at this point in the history
Looping/Beatjump: use seconds if track has no beats
  • Loading branch information
JoergAtGithub authored May 28, 2024
2 parents 006c5f4 + 761c7d6 commit d0f03e4
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 79 deletions.
30 changes: 22 additions & 8 deletions src/controllers/controlpickermenu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -639,21 +639,27 @@ ControlPickerMenu::ControlPickerMenu(QWidget* pParent)
QMenu* pLoopMenu = addSubmenu(tr("Looping"));
// add beatloop_activate and beatlooproll_activate to both the
// Loop and Beat-Loop menus to make sure users can find them.
QString noBeatsSeconds = QChar('(') +
tr("if the track has no beats the unit is seconds") + QChar(')');
QString beatloopActivateTitle = tr("Loop Selected Beats");
QString beatloopActivateDescription = tr("Create a beat loop of selected beat size");
QString beatloopActivateDescription =
tr("Create a beat loop of selected beat size") + noBeatsSeconds;
QString beatloopRollActivateTitle = tr("Loop Roll Selected Beats");
QString beatloopRollActivateDescription = tr("Create a rolling beat loop of selected beat size");
QString beatloopRollActivateDescription =
tr("Create a rolling beat loop of selected beat size") + noBeatsSeconds;
QString beatLoopTitle = tr("Loop %1 Beats");
QString reverseBeatLoopTitle = tr("Loop %1 Beats set from its end point");
QString beatLoopRollTitle = tr("Loop Roll %1 Beats");
QString reverseBeatLoopRollTitle = tr("Loop Roll %1 Beats set from its end point");
QString beatLoopDescription = tr("Create %1-beat loop");
QString reverseBeatLoopDescription = tr(
"Create %1-beat loop with the current play position as loop end");
QString beatLoopRollDescription = tr("Create temporary %1-beat loop roll");
QString beatLoopRollDescription =
tr("Create temporary %1-beat loop roll") + noBeatsSeconds;
QString reverseBeatLoopRollDescription =
tr("Create temporary %1-beat loop roll with the current play "
"position as loop end");
"position as loop end") +
noBeatsSeconds;

QList<double> beatSizes = LoopingControl::getBeatSizes();

Expand Down Expand Up @@ -735,8 +741,14 @@ ControlPickerMenu::ControlPickerMenu(QWidget* pParent)
QMenu* pBeatJumpMenu = addSubmenu(tr("Beat Jump / Loop Move"));
QString beatJumpForwardTitle = tr("Jump / Move Loop Forward %1 Beats");
QString beatJumpBackwardTitle = tr("Jump / Move Loop Backward %1 Beats");
QString beatJumpForwardDescription = tr("Jump forward by %1 beats, or if a loop is enabled, move the loop forward %1 beats");
QString beatJumpBackwardDescription = tr("Jump backward by %1 beats, or if a loop is enabled, move the loop backward %1 beats");
QString beatJumpForwardDescription =
tr("Jump forward by %1 beats, or if a loop is enabled, move the "
"loop forward %1 beats") +
noBeatsSeconds;
QString beatJumpBackwardDescription =
tr("Jump backward by %1 beats, or if a loop is enabled, move the "
"loop backward %1 beats") +
noBeatsSeconds;
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 "
Expand Down Expand Up @@ -777,8 +789,10 @@ ControlPickerMenu::ControlPickerMenu(QWidget* pParent)
// Loop moving
QString loopMoveForwardTitle = tr("Move Loop +%1 Beats");
QString loopMoveBackwardTitle = tr("Move Loop -%1 Beats");
QString loopMoveForwardDescription = tr("Move loop forward by %1 beats");
QString loopMoveBackwardDescription = tr("Move loop backward by %1 beats");
QString loopMoveForwardDescription = tr("Move loop forward by %1 beats") +
noBeatsSeconds;
QString loopMoveBackwardDescription = tr("Move loop backward by %1 beats") +
noBeatsSeconds;

QMenu* pLoopmoveFwdSubmenu = addSubmenu(tr("Loop Move Forward"), pBeatJumpMenu);
foreach (double beats, beatSizes) {
Expand Down
52 changes: 34 additions & 18 deletions src/engine/controls/loopingcontrol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ LoopingControl::LoopingControl(const QString& group,
m_bAdjustingLoopInOld(false),
m_bAdjustingLoopOutOld(false),
m_bLoopOutPressedWhileLoopDisabled(false),
m_prevLoopSize(-1) {
m_prevLoopSize(-1),
m_trueTrackBeats(false) {
m_currentPosition.setValue(mixxx::audio::kStartFramePos);
m_pActiveBeatLoop = nullptr;
m_pRateControl = nullptr;
Expand Down Expand Up @@ -420,7 +421,7 @@ mixxx::audio::FramePos LoopingControl::nextTrigger(bool reverse,
// When the LoopIn button is released in reverse mode we jump to the end of the loop to not fall out and disable the active loop
// This must not happen in quantized mode. The newly set start is always ahead (in time, but behind spacially) of the current position so we don't jump.
// Jumping to the end is then handled when the loop's start is reached later in this function.
if (reverse && !m_bAdjustingLoopIn && !m_pQuantizeEnabled->toBool()) {
if (reverse && !m_bAdjustingLoopIn && !(quantizeEnabledAndHasTrueTrackBeats())) {
m_oldLoopInfo = loopInfo;
*pTargetPosition = loopInfo.endPosition;
return currentPosition;
Expand All @@ -434,7 +435,8 @@ mixxx::audio::FramePos LoopingControl::nextTrigger(bool reverse,
// When the LoopOut button is released in forward mode we jump to the start of the loop to not fall out and disable the active loop
// This must not happen in quantized mode. The newly set end is always ahead of the current position so we don't jump.
// Jumping to the start is then handled when the loop's end is reached later in this function.
if (!reverse && !m_bAdjustingLoopOut && !m_pQuantizeEnabled->toBool()) {
if (!reverse && !m_bAdjustingLoopOut &&
!(quantizeEnabledAndHasTrueTrackBeats())) {
m_oldLoopInfo = loopInfo;
*pTargetPosition = loopInfo.startPosition;
return currentPosition;
Expand Down Expand Up @@ -716,7 +718,7 @@ void LoopingControl::setLoopInToCurrentPosition() {
// silence of the last buffer. This position might be not reachable in
// a future runs, depending on the buffering.
mixxx::audio::FramePos position = math_min(info.currentPosition, info.trackEndPosition);
if (m_pQuantizeEnabled->toBool() && pBeats) {
if (quantizeEnabledAndHasTrueTrackBeats()) {
mixxx::audio::FramePos prevBeatPosition;
mixxx::audio::FramePos nextBeatPosition;
if (pBeats->findPrevNextBeats(position, &prevBeatPosition, &nextBeatPosition, false)) {
Expand Down Expand Up @@ -781,9 +783,10 @@ void LoopingControl::setLoopInToCurrentPosition() {
loopInfo.seekMode = LoopSeekMode::MovedOut;
}

if (m_pQuantizeEnabled->toBool() && loopInfo.startPosition.isValid() &&
if (quantizeEnabledAndHasTrueTrackBeats() &&
loopInfo.startPosition.isValid() &&
loopInfo.endPosition.isValid() &&
loopInfo.startPosition < loopInfo.endPosition && pBeats) {
loopInfo.startPosition < loopInfo.endPosition) {
m_pCOBeatLoopSize->setAndConfirm(pBeats->numBeatsInRange(
loopInfo.startPosition, loopInfo.endPosition));
updateBeatLoopingControls();
Expand Down Expand Up @@ -875,7 +878,7 @@ void LoopingControl::setLoopOutToCurrentPosition() {
// silence of the last buffer. This position might be not reachable in
// a future runs, depending on the buffering.
mixxx::audio::FramePos position = math_min(info.currentPosition, info.trackEndPosition);
if (m_pQuantizeEnabled->toBool() && pBeats) {
if (quantizeEnabledAndHasTrueTrackBeats()) {
mixxx::audio::FramePos prevBeatPosition;
mixxx::audio::FramePos nextBeatPosition;
if (pBeats->findPrevNextBeats(position, &prevBeatPosition, &nextBeatPosition, false)) {
Expand Down Expand Up @@ -951,7 +954,7 @@ void LoopingControl::setLoopOutToCurrentPosition() {
loopInfo.seekMode = LoopSeekMode::MovedOut;
}

if (m_pQuantizeEnabled->toBool() && pBeats) {
if (quantizeEnabledAndHasTrueTrackBeats()) {
m_pCOBeatLoopSize->setAndConfirm(pBeats->numBeatsInRange(
loopInfo.startPosition, loopInfo.endPosition));
updateBeatLoopingControls();
Expand Down Expand Up @@ -1249,15 +1252,24 @@ void LoopingControl::trackLoaded(TrackPointer pNewTrack) {

void LoopingControl::trackBeatsUpdated(mixxx::BeatsPointer pBeats) {
clearActiveBeatLoop();
m_pBeats = pBeats;
if (m_pBeats) {
LoopInfo loopInfo = m_loopInfo.getValue();
if (loopInfo.startPosition.isValid() && loopInfo.endPosition.isValid()) {
double loaded_loop_size = findBeatloopSizeForLoop(
loopInfo.startPosition, loopInfo.endPosition);
if (loaded_loop_size != -1) {
m_pCOBeatLoopSize->setAndConfirm(loaded_loop_size);
}
if (pBeats) {
m_pBeats = pBeats;
m_trueTrackBeats = true;
} else if (m_pTrack) {
// no beats, use fake beats so we can use seconds as beat unit
m_pBeats = getFake60BpmBeats();
m_trueTrackBeats = false;
} else {
// no track, no beats
m_pBeats = pBeats;
m_trueTrackBeats = false;
}
LoopInfo loopInfo = m_loopInfo.getValue();
if (loopInfo.startPosition.isValid() && loopInfo.endPosition.isValid()) {
double loaded_loop_size = findBeatloopSizeForLoop(
loopInfo.startPosition, loopInfo.endPosition);
if (loaded_loop_size != -1) {
m_pCOBeatLoopSize->setAndConfirm(loaded_loop_size);
}
}
}
Expand Down Expand Up @@ -1391,6 +1403,10 @@ bool LoopingControl::currentLoopMatchesBeatloopSize(const LoopInfo& loopInfo) co
return positionNear(loopInfo.endPosition, loopEndPosition);
}

bool LoopingControl::quantizeEnabledAndHasTrueTrackBeats() const {
return m_pQuantizeEnabled->toBool() && m_trueTrackBeats;
}

double LoopingControl::findBeatloopSizeForLoop(
mixxx::audio::FramePos startPosition,
mixxx::audio::FramePos endPosition) const {
Expand Down Expand Up @@ -1549,7 +1565,7 @@ void LoopingControl::slotBeatLoop(double beats,
currentPosition = pBeats->findNBeatsFromPosition(currentPosition, -beats);
}

bool quantize = m_pQuantizeEnabled->toBool();
bool quantize = quantizeEnabledAndHasTrueTrackBeats();
// 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) {
Expand Down
14 changes: 14 additions & 0 deletions src/engine/controls/loopingcontrol.h
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,17 @@ class LoopingControl : public EngineControl {
void clearLoopInfoAndControls();
void updateBeatLoopingControls();
bool currentLoopMatchesBeatloopSize(const LoopInfo& loopInfo) const;
bool quantizeEnabledAndHasTrueTrackBeats() const;

// Fake beats that allow using looping/beatjump controls with no beats:
// one 'beat' = one second
mixxx::BeatsPointer getFake60BpmBeats() {
auto fakeBeats = mixxx::Beats::fromConstTempo(
frameInfo().sampleRate,
mixxx::audio::kStartFramePos,
mixxx::Bpm(60.0));
return fakeBeats;
}

// Given loop in and out points, determine if this is a beatloop of a particular
// size.
Expand Down Expand Up @@ -252,6 +263,9 @@ class LoopingControl : public EngineControl {
// objects below are written from an engine worker thread
TrackPointer m_pTrack;
mixxx::BeatsPointer m_pBeats;
// Flag that allows to act quantized only if we have true track beats.
// See quantizeEnabledAndHasTrueTrackBeats()
bool m_trueTrackBeats;

friend class LoopingControlTest;
};
Expand Down
31 changes: 23 additions & 8 deletions src/skin/legacy/tooltips.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -786,9 +786,12 @@ void Tooltips::addStandardTooltips() {
<< tr("Loop Double")
<< tr("Doubles the current loop's length by moving the end marker.");

QString noBeatsSeconds = tr("If the track has no beats the unit is seconds.");

add("beatloop_size")
<< tr("Beatloop Size")
<< tr("Select the size of the loop in beats to set with the Beatloop button.")
<< noBeatsSeconds
<< tr("Changing this resizes the loop if the loop already matches this size.");

add("beatloop_halve")
Expand All @@ -801,6 +804,7 @@ void Tooltips::addStandardTooltips() {
add("beatloop_activate")
<< tr("Beatloop")
<< QString("%1: %2").arg(leftClick, tr("Start a loop over the set number of beats."))
<< noBeatsSeconds
<< quantizeSnap
<< 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.");
Expand All @@ -812,21 +816,32 @@ void Tooltips::addStandardTooltips() {

add("beatjump_size")
<< tr("Beatjump/Loop Move Size")
<< noBeatsSeconds
<< tr("Select the number of beats to jump or move the loop with the Beatjump Forward/Backward buttons.");

add("beatjump_forward")
<< tr("Beatjump Forward")
<< QString("%1: %2").arg(leftClick + " " + loopInactive, tr("Jump forward by the set number of beats."))
<< QString("%1: %2").arg(leftClick + " " + loopActive, tr("Move the loop forward by the set number of beats."))
<< QString("%1: %2").arg(rightClick + " " + loopInactive, tr("Jump forward by 1 beat."))
<< QString("%1: %2").arg(rightClick + " " + loopActive, tr("Move the loop forward by 1 beat."));
<< QString("%1: %2").arg(leftClick + " " + loopInactive,
tr("Jump forward by the set number of beats."))
<< QString("%1: %2").arg(leftClick + " " + loopActive,
tr("Move the loop forward by the set number of beats."))
<< QString("%1: %2").arg(rightClick + " " + loopInactive,
tr("Jump forward by 1 beat."))
<< QString("%1: %2").arg(rightClick + " " + loopActive,
tr("Move the loop forward by 1 beat."))
<< noBeatsSeconds;

add("beatjump_backward")
<< tr("Beatjump Backward")
<< QString("%1: %2").arg(leftClick + " " + loopInactive, tr("Jump backward by the set number of beats."))
<< QString("%1: %2").arg(leftClick + " " + loopActive, tr("Move the loop backward by the set number of beats."))
<< QString("%1: %2").arg(rightClick + " " + loopInactive, tr("Jump backward by 1 beat."))
<< QString("%1: %2").arg(rightClick + " " + loopActive, tr("Move the loop backward by 1 beat."));
<< QString("%1: %2").arg(leftClick + " " + loopInactive,
tr("Jump backward by the set number of beats."))
<< QString("%1: %2").arg(leftClick + " " + loopActive,
tr("Move the loop backward by the set number of beats."))
<< QString("%1: %2").arg(rightClick + " " + loopInactive,
tr("Jump backward by 1 beat."))
<< QString("%1: %2").arg(rightClick + " " + loopActive,
tr("Move the loop backward by 1 beat."))
<< noBeatsSeconds;

add("loop_exit")
<< tr("Loop Exit")
Expand Down
22 changes: 0 additions & 22 deletions src/test/hotcuecontrol_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1092,28 +1092,6 @@ TEST_F(HotcueControlTest, CueLoopWithSavedLoopToggles) {
EXPECT_TRUE(m_pLoopEnabled->toBool());
}

TEST_F(HotcueControlTest, CueLoopWithoutLoopOrBeats) {
createAndLoadFakeTrack();

EXPECT_DOUBLE_EQ(static_cast<double>(HotcueControl::Status::Empty), m_pHotcue1Enabled->get());
EXPECT_FALSE(mixxx::audio::FramePos::fromEngineSamplePosMaybeInvalid(
m_pHotcue1Position->get())
.isValid());
EXPECT_FALSE(mixxx::audio::FramePos::fromEngineSamplePosMaybeInvalid(
m_pHotcue1EndPosition->get())
.isValid());

m_pHotcue1CueLoop->set(1);
m_pHotcue1CueLoop->set(0);

EXPECT_DOUBLE_EQ(static_cast<double>(HotcueControl::Status::Set), m_pHotcue1Enabled->get());
EXPECT_FRAMEPOS_EQ_CONTROL(mixxx::audio::kStartFramePos, m_pHotcue1Position);
EXPECT_FALSE(mixxx::audio::FramePos::fromEngineSamplePosMaybeInvalid(
m_pHotcue1EndPosition->get())
.isValid());
EXPECT_FALSE(m_pLoopEnabled->toBool());
}

TEST_F(HotcueControlTest, SavedLoopToggleDoesNotSeek) {
// Setup fake track with 120 bpm and calculate loop size
TrackPointer pTrack = loadTestTrackWithBpm(120.0);
Expand Down
Loading

0 comments on commit d0f03e4

Please sign in to comment.