diff --git a/build/depends.py b/build/depends.py index 4fc24c183e1b..0095fe523ee4 100644 --- a/build/depends.py +++ b/build/depends.py @@ -826,6 +826,7 @@ def sources(self, build): "widget/wlabel.cpp", "widget/wtracktext.cpp", "widget/wnumber.cpp", + "widget/wbeatspinbox.cpp", "widget/wnumberdb.cpp", "widget/wnumberpos.cpp", "widget/wnumberrate.cpp", diff --git a/src/engine/bpmcontrol.cpp b/src/engine/bpmcontrol.cpp index 82fd990d8724..7965ea013263 100644 --- a/src/engine/bpmcontrol.cpp +++ b/src/engine/bpmcontrol.cpp @@ -400,7 +400,7 @@ double BpmControl::calcSyncedRate(double userTweak) { // Now that we have our beat distance we can also check how large the // current loop is. If we are in a <1 beat loop, don't worry about offset. - const bool loop_enabled = m_pLoopEnabled->get() > 0.0; + const bool loop_enabled = m_pLoopEnabled->toBool(); const double loop_size = (m_pLoopEndPosition->get() - m_pLoopStartPosition->get()) / dBeatLength; @@ -567,14 +567,14 @@ bool BpmControl::getBeatContextNoLookup( return true; } -double BpmControl::getPhaseOffset(double dThisPosition) { +double BpmControl::getNearestPositionInPhase(double dThisPosition, bool respectLoops, bool playing) { // Without a beatgrid, we don't know the phase offset. if (!m_pBeats) { - return 0; + return dThisPosition; } // Master buffer is always in sync! if (getSyncMode() == SYNC_MASTER) { - return 0; + return dThisPosition; } // Get the current position of this deck. @@ -588,13 +588,13 @@ double BpmControl::getPhaseOffset(double dThisPosition) { if (!getBeatContext(m_pBeats, dThisPosition, &dThisPrevBeat, &dThisNextBeat, &dThisBeatLength, NULL)) { - return 0; + return dThisPosition; } } else { if (!getBeatContextNoLookup(dThisPosition, dThisPrevBeat, dThisNextBeat, &dThisBeatLength, NULL)) { - return 0; + return dThisPosition; } } @@ -606,7 +606,15 @@ double BpmControl::getPhaseOffset(double dThisPosition) { // If not, we have to figure it out EngineBuffer* pOtherEngineBuffer = pickSyncTarget(); if (pOtherEngineBuffer == NULL) { - return 0; + return dThisPosition; + } + + if (playing) { + // "this" track is playing, or just starting + // only match phase if the sync target is playing as well + if (pOtherEngineBuffer->getSpeed() == 0.0) { + return dThisPosition; + } } TrackPointer otherTrack = pOtherEngineBuffer->getLoadedTrack(); @@ -614,7 +622,7 @@ double BpmControl::getPhaseOffset(double dThisPosition) { // If either track does not have beats, then we can't adjust the phase. if (!otherBeats) { - return 0; + return dThisPosition; } double dOtherLength = ControlObject::getControl( @@ -624,7 +632,7 @@ double BpmControl::getPhaseOffset(double dThisPosition) { if (!BpmControl::getBeatContext(otherBeats, dOtherPosition, NULL, NULL, NULL, &dOtherBeatFraction)) { - return 0.0; + return dThisPosition; } } @@ -658,51 +666,55 @@ double BpmControl::getPhaseOffset(double dThisPosition) { dNewPlaypos += dThisPrevBeat; } - // We might be seeking outside the loop. - const bool loop_enabled = m_pLoopEnabled->get() > 0.0; - const double loop_start_position = m_pLoopStartPosition->get(); - const double loop_end_position = m_pLoopEndPosition->get(); - - // Cases for sanity: - // - // CASE 1 - // Two identical 1-beat loops, out of phase by X samples. - // Other deck is at its loop start. - // This deck is half way through. We want to jump forward X samples to the loop end point. - // - // Two identical 1-beat loop, out of phase by X samples. - // Other deck is - - // If sync target is 50% through the beat, - // If we are at the loop end point and hit sync, jump forward X samples. - - - // TODO(rryan): Revise this with something that keeps a broader number of - // cases in sync. This at least prevents breaking out of the loop. - if (loop_enabled) { - const double loop_length = loop_end_position - loop_start_position; - if (loop_length <= 0.0) { - return false; - } - - // TODO(rryan): If loop_length is not a multiple of dThisBeatLength should - // we bail and not sync phase? - - // Syncing to after the loop end. - double end_delta = dNewPlaypos - loop_end_position; - if (end_delta > 0) { - int i = end_delta / loop_length; - dNewPlaypos = loop_start_position + end_delta - i * loop_length; - } - - // Syncing to before the loop beginning. - double start_delta = loop_start_position - dNewPlaypos; - if (start_delta > 0) { - int i = start_delta / loop_length; - dNewPlaypos = loop_end_position - start_delta + i * loop_length; + if (respectLoops) { + // We might be seeking outside the loop. + const bool loop_enabled = m_pLoopEnabled->toBool(); + const double loop_start_position = m_pLoopStartPosition->get(); + const double loop_end_position = m_pLoopEndPosition->get(); + + // Cases for sanity: + // + // CASE 1 + // Two identical 1-beat loops, out of phase by X samples. + // Other deck is at its loop start. + // This deck is half way through. We want to jump forward X samples to the loop end point. + // + // Two identical 1-beat loop, out of phase by X samples. + // Other deck is + + // If sync target is 50% through the beat, + // If we are at the loop end point and hit sync, jump forward X samples. + + + // TODO(rryan): Revise this with something that keeps a broader number of + // cases in sync. This at least prevents breaking out of the loop. + if (loop_enabled && + dThisPosition <= loop_end_position) { + const double loop_length = loop_end_position - loop_start_position; + const double end_delta = dNewPlaypos - loop_end_position; + + // Syncing to after the loop end. + if (end_delta > 0 && loop_length > 0.0) { + int i = end_delta / loop_length; + dNewPlaypos = loop_start_position + end_delta - i * loop_length; + + // Move new position after loop jump into phase as well. + // This is a recursive call, called only twice because of + // respectLoops = false + dNewPlaypos = getNearestPositionInPhase(dNewPlaypos, false, playing); + } + + // Note: Syncing to before the loop beginning is allowed, because + // loops are catching } } + return dNewPlaypos; +} + +double BpmControl::getPhaseOffset(double dThisPosition) { + // This does not respect looping + double dNewPlaypos = getNearestPositionInPhase(dThisPosition, false, false); return dNewPlaypos - dThisPosition; } diff --git a/src/engine/bpmcontrol.h b/src/engine/bpmcontrol.h index adc3978fd32b..38a8541555c8 100644 --- a/src/engine/bpmcontrol.h +++ b/src/engine/bpmcontrol.h @@ -33,7 +33,8 @@ class BpmControl : public EngineControl { // out of sync. double calcSyncedRate(double userTweak); // Get the phase offset from the specified position. - double getPhaseOffset(double reference_position); + double getNearestPositionInPhase(double dThisPosition, bool respectLoops, bool playing); + double getPhaseOffset(double dThisPosition); double getBeatDistance(double dThisPosition) const; double getPreviousSample() const { return m_dPreviousSample; } diff --git a/src/engine/enginebuffer.cpp b/src/engine/enginebuffer.cpp index 88bd1dc5dfc5..fdc1efdbedb0 100644 --- a/src/engine/enginebuffer.cpp +++ b/src/engine/enginebuffer.cpp @@ -1165,7 +1165,7 @@ void EngineBuffer::processSeek(bool paused) { } if ((seekType & SEEK_PHASE) && !paused && m_pQuantize->toBool()) { - position += m_pBpmControl->getPhaseOffset(position); + position = m_pBpmControl->getNearestPositionInPhase(position, true, true); } double newPlayFrame = position / kSamplesPerFrame; diff --git a/src/engine/enginebuffer.h b/src/engine/enginebuffer.h index afbbf995d914..98faffb223e4 100644 --- a/src/engine/enginebuffer.h +++ b/src/engine/enginebuffer.h @@ -231,9 +231,12 @@ class EngineBuffer : public EngineObject { UserSettingsPointer m_pConfig; LoopingControl* m_pLoopingControl; - FRIEND_TEST(LoopingControlTest, LoopHalveButton_HalvesLoop); + FRIEND_TEST(LoopingControlTest, LoopScale_HalvesLoop); FRIEND_TEST(LoopingControlTest, LoopMoveTest); FRIEND_TEST(LoopingControlTest, LoopResizeSeek); + FRIEND_TEST(LoopingControlTest, ReloopToggleButton_DoesNotJumpAhead); + FRIEND_TEST(LoopingControlTest, ReloopAndStopButton); + FRIEND_TEST(LoopingControlTest, Beatjump_JumpsByBeats); FRIEND_TEST(SyncControlTest, TestDetermineBpmMultiplier); FRIEND_TEST(EngineSyncTest, HalfDoubleBpmTest); FRIEND_TEST(EngineSyncTest, HalfDoubleThenPlay); diff --git a/src/engine/loopingcontrol.cpp b/src/engine/loopingcontrol.cpp index f3858e37308f..88cda8636007 100644 --- a/src/engine/loopingcontrol.cpp +++ b/src/engine/loopingcontrol.cpp @@ -17,7 +17,7 @@ #include "track/beats.h" double LoopingControl::s_dBeatSizes[] = { 0.03125, 0.0625, 0.125, 0.25, 0.5, - 1, 2, 4, 8, 16, 32, 64 }; + 1, 2, 4, 8, 16, 32, 64, 128, 256, 512 }; // Used to generate the beatloop_%SIZE, beatjump_%SIZE, and loop_move_%SIZE CO // ConfigKeys. @@ -40,8 +40,6 @@ QList LoopingControl::getBeatSizes() { LoopingControl::LoopingControl(QString group, UserSettingsPointer pConfig) : EngineControl(group, pConfig) { - m_bLoopingEnabled = false; - m_bLoopRollActive = false; LoopSamples loopSamples = { kNoTrigger, kNoTrigger }; m_loopSamples.setValue(loopSamples); m_iCurrentSample = 0; @@ -54,23 +52,40 @@ LoopingControl::LoopingControl(QString group, Qt::DirectConnection); m_pLoopInButton->set(0); + m_pLoopInGotoButton = new ControlPushButton(ConfigKey(group, "loop_in_goto")); + connect(m_pLoopInGotoButton, SIGNAL(valueChanged(double)), + this, SLOT(slotLoopInGoto(double))); + m_pLoopOutButton = new ControlPushButton(ConfigKey(group, "loop_out")); connect(m_pLoopOutButton, SIGNAL(valueChanged(double)), this, SLOT(slotLoopOut(double)), Qt::DirectConnection); m_pLoopOutButton->set(0); + m_pLoopOutGotoButton = new ControlPushButton(ConfigKey(group, "loop_out_goto")); + connect(m_pLoopOutGotoButton, SIGNAL(valueChanged(double)), + this, SLOT(slotLoopOutGoto(double))); + + m_pLoopExitButton = new ControlPushButton(ConfigKey(group, "loop_exit")); connect(m_pLoopExitButton, SIGNAL(valueChanged(double)), this, SLOT(slotLoopExit(double)), Qt::DirectConnection); m_pLoopExitButton->set(0); - m_pReloopExitButton = new ControlPushButton(ConfigKey(group, "reloop_exit")); - connect(m_pReloopExitButton, SIGNAL(valueChanged(double)), - this, SLOT(slotReloopExit(double)), + m_pReloopToggleButton = new ControlPushButton(ConfigKey(group, "reloop_toggle")); + connect(m_pReloopToggleButton, SIGNAL(valueChanged(double)), + this, SLOT(slotReloopToggle(double)), + Qt::DirectConnection); + m_pReloopToggleButton->set(0); + // The old reloop_exit name was confusing. This CO does both entering and exiting. + ControlDoublePrivate::insertAlias(ConfigKey(group, "reloop_exit"), + ConfigKey(group, "reloop_toggle")); + + m_pReloopAndStopButton = new ControlPushButton(ConfigKey(group, "reloop_andstop")); + connect(m_pReloopAndStopButton, SIGNAL(valueChanged(double)), + this, SLOT(slotReloopAndStop(double)), Qt::DirectConnection); - m_pReloopExitButton->set(0); m_pCOLoopEnabled = new ControlObject(ConfigKey(group, "loop_enabled")); m_pCOLoopEnabled->set(0.0); @@ -91,17 +106,27 @@ LoopingControl::LoopingControl(QString group, m_pQuantizeEnabled = ControlObject::getControl(ConfigKey(group, "quantize")); m_pNextBeat = ControlObject::getControl(ConfigKey(group, "beat_next")); + m_pPreviousBeat = ControlObject::getControl(ConfigKey(group, "beat_prev")); m_pClosestBeat = ControlObject::getControl(ConfigKey(group, "beat_closest")); m_pTrackSamples = ControlObject::getControl(ConfigKey(group, "track_samples")); m_pSlipEnabled = ControlObject::getControl(ConfigKey(group, "slip_enabled")); - // Connect beatloop, which can flexibly handle different values. - // Using this CO directly is meant to be used internally and by scripts, - // or anything else that can pass in arbitrary values. + // DEPRECATED: Use beatloop_size and beatloop_set instead. + // Activates a beatloop of a specified number of beats. m_pCOBeatLoop = new ControlObject(ConfigKey(group, "beatloop"), false); connect(m_pCOBeatLoop, SIGNAL(valueChanged(double)), this, SLOT(slotBeatLoop(double)), Qt::DirectConnection); + m_pCOBeatLoopSize = new ControlObject(ConfigKey(group, "beatloop_size"), + true, false, false, 4.0); + m_pCOBeatLoopSize->connectValueChangeRequest(this, + SLOT(slotBeatLoopSizeChangeRequest(double)), Qt::DirectConnection); + m_pCOBeatLoopActivate = new ControlPushButton(ConfigKey(group, "beatloop_activate")); + connect(m_pCOBeatLoopActivate, SIGNAL(valueChanged(double)), + this, SLOT(slotBeatLoopToggle(double))); + m_pCOBeatLoopRollActivate = new ControlPushButton(ConfigKey(group, "beatlooproll_activate")); + connect(m_pCOBeatLoopRollActivate, SIGNAL(valueChanged(double)), + this, SLOT(slotBeatLoopRollActivate(double))); // Here we create corresponding beatloop_(SIZE) CO's which all call the same // BeatControl, but with a set value. @@ -125,6 +150,14 @@ LoopingControl::LoopingControl(QString group, m_pCOBeatJump = new ControlObject(ConfigKey(group, "beatjump"), false); connect(m_pCOBeatJump, SIGNAL(valueChanged(double)), this, SLOT(slotBeatJump(double)), Qt::DirectConnection); + m_pCOBeatJumpSize = new ControlObject(ConfigKey(group, "beatjump_size"), + true, false, false, 4.0); + m_pCOBeatJumpForward = new ControlPushButton(ConfigKey(group, "beatjump_forward")); + connect(m_pCOBeatJumpForward, SIGNAL(valueChanged(double)), + this, SLOT(slotBeatJumpForward(double))); + m_pCOBeatJumpBackward = new ControlPushButton(ConfigKey(group, "beatjump_backward")); + connect(m_pCOBeatJumpBackward, SIGNAL(valueChanged(double)), + this, SLOT(slotBeatJumpBackward(double))); // Create beatjump_(SIZE) CO's which all call beatjump, but with a set // value. @@ -159,13 +192,18 @@ LoopingControl::LoopingControl(QString group, m_pLoopDoubleButton = new ControlPushButton(ConfigKey(group, "loop_double")); connect(m_pLoopDoubleButton, SIGNAL(valueChanged(double)), this, SLOT(slotLoopDouble(double))); + + m_pPlayButton = ControlObject::getControl(ConfigKey(group, "play")); } LoopingControl::~LoopingControl() { delete m_pLoopOutButton; + delete m_pLoopOutGotoButton; delete m_pLoopInButton; + delete m_pLoopInGotoButton; delete m_pLoopExitButton; - delete m_pReloopExitButton; + delete m_pReloopToggleButton; + delete m_pReloopAndStopButton; delete m_pCOLoopEnabled; delete m_pCOLoopStartPosition; delete m_pCOLoopEndPosition; @@ -178,8 +216,14 @@ LoopingControl::~LoopingControl() { BeatLoopingControl* pBeatLoop = m_beatLoops.takeLast(); delete pBeatLoop; } + delete m_pCOBeatLoopSize; + delete m_pCOBeatLoopActivate; + delete m_pCOBeatLoopRollActivate; delete m_pCOBeatJump; + delete m_pCOBeatJumpSize; + delete m_pCOBeatJumpForward; + delete m_pCOBeatJumpBackward; while (!m_beatJumps.isEmpty()) { BeatJumpControl* pBeatJump = m_beatJumps.takeLast(); delete pBeatJump; @@ -192,7 +236,7 @@ LoopingControl::~LoopingControl() { } } -void LoopingControl::slotLoopScale(double scale) { +void LoopingControl::slotLoopScale(double scaleFactor) { LoopSamples loopSamples = m_loopSamples.getValue(); if (loopSamples.start == kNoTrigger || loopSamples.end == kNoTrigger) { return; @@ -200,7 +244,7 @@ void LoopingControl::slotLoopScale(double scale) { int loop_length = loopSamples.end - loopSamples.start; int old_loop_end = loopSamples.end; int samples = m_pTrackSamples->get(); - loop_length *= scale; + loop_length *= scaleFactor; // Abandon loops that are too short of extend beyond the end of the file. if (loop_length < MINIMUM_AUDIBLE_LOOP_SIZE || @@ -232,79 +276,32 @@ void LoopingControl::slotLoopScale(double scale) { } m_loopSamples.setValue(loopSamples); - + // Update CO for loop end marker m_pCOLoopEndPosition->set(loopSamples.end); // Reseek if the loop shrank out from under the playposition. - if (m_bLoopingEnabled && scale < 1.0) { + if (m_bLoopingEnabled && scaleFactor < 1.0) { seekInsideAdjustedLoop( loopSamples.start, old_loop_end, loopSamples.start, loopSamples.end); } } -void LoopingControl::slotLoopHalve(double v) { - LoopSamples loopSamples = m_loopSamples.getValue(); - if (loopSamples.start == kNoTrigger || - loopSamples.end == kNoTrigger || - v <= 0.0) { +void LoopingControl::slotLoopHalve(double pressed) { + if (pressed <= 0.0) { return; } - // If a beatloop is active then halve should deactive the current - // beatloop and activate the previous one. - BeatLoopingControl* pActiveBeatLoop = m_pActiveBeatLoop; - if (pActiveBeatLoop != nullptr) { - int active_index = m_beatLoops.indexOf(pActiveBeatLoop); - if (active_index - 1 >= 0) { - if (m_bLoopingEnabled) { - // If the current position is outside the range of the new loop, - // take the current position and subtract the length of the new loop until - // it fits. - int old_loop_in = loopSamples.start; - int old_loop_out = loopSamples.end; - slotBeatLoopActivate(m_beatLoops[active_index - 1]); - loopSamples = m_loopSamples.getValue(); - seekInsideAdjustedLoop( - old_loop_in, old_loop_out, - loopSamples.start, loopSamples.end); - } else { - // Calling scale clears the active beatloop. - slotLoopScale(0.5); - m_pActiveBeatLoop = m_beatLoops[active_index - 1]; - } - } - } else { - slotLoopScale(0.5); - } + slotBeatLoop(m_pCOBeatLoopSize->get() / 2.0, true, false); } -void LoopingControl::slotLoopDouble(double v) { - LoopSamples loopSamples = m_loopSamples.getValue(); - if (loopSamples.start == kNoTrigger || - loopSamples.end == kNoTrigger || - v <= 0.0) { +void LoopingControl::slotLoopDouble(double pressed) { + if (pressed <= 0.0) { return; } - // If a beatloop is active then double should deactive the current - // beatloop and activate the next one. - BeatLoopingControl* pActiveBeatLoop = m_pActiveBeatLoop; - if (pActiveBeatLoop != NULL) { - int active_index = m_beatLoops.indexOf(pActiveBeatLoop); - if (active_index + 1 < m_beatLoops.size()) { - if (m_bLoopingEnabled) { - slotBeatLoopActivate(m_beatLoops[active_index + 1]); - } else { - // Calling scale clears the active beatloop. - slotLoopScale(2.0); - m_pActiveBeatLoop = m_beatLoops[active_index + 1]; - } - } - } else { - slotLoopScale(2.0); - } + slotBeatLoop(m_pCOBeatLoopSize->get() * 2.0, true, false); } double LoopingControl::process(const double dRate, @@ -330,10 +327,20 @@ double LoopingControl::process(const double dRate, bool outsideLoop = currentSample >= loopSamples.end || currentSample <= loopSamples.start; if (outsideLoop) { - retval = reverse ? loopSamples.end : loopSamples.start; + if (!m_bReloopCatchUpcomingLoop && !m_bAdjustingLoopIn && !m_bAdjustingLoopOut) { + retval = reverse ? loopSamples.end : loopSamples.start; + } + } else { + m_bReloopCatchUpcomingLoop = false; } } + if (m_bAdjustingLoopIn) { + setLoopInToCurrentPosition(); + } else if (m_bAdjustingLoopOut) { + setLoopOutToCurrentPosition(); + } + return retval; } @@ -347,7 +354,8 @@ double LoopingControl::nextTrigger(const double dRate, bool bReverse = dRate < 0; LoopSamples loopSamples = m_loopSamples.getValue(); - if (m_bLoopingEnabled) { + if (m_bLoopingEnabled && !m_bReloopCatchUpcomingLoop && + !m_bAdjustingLoopIn && !m_bAdjustingLoopOut) { if (bReverse) { return loopSamples.start; } else { @@ -367,7 +375,8 @@ double LoopingControl::getTrigger(const double dRate, bool bReverse = dRate < 0; LoopSamples loopSamples = m_loopSamples.getValue(); - if (m_bLoopingEnabled) { + if (m_bLoopingEnabled && !m_bReloopCatchUpcomingLoop && + !m_bAdjustingLoopIn && !m_bAdjustingLoopOut) { if (bReverse) { return loopSamples.end; } else { @@ -410,24 +419,33 @@ void LoopingControl::hintReader(HintVector* pHintList) { } } -void LoopingControl::slotLoopIn(double val) { - if (!m_pTrack || val <= 0.0) { - return; - } - +void LoopingControl::setLoopInToCurrentPosition() { clearActiveBeatLoop(); // set loop-in position LoopSamples loopSamples = m_loopSamples.getValue(); - double closestBeat = -1; + double quantizedBeat = -1; int pos = m_iCurrentSample; - if (m_pQuantizeEnabled->toBool()) { - closestBeat = m_pClosestBeat->get(); - if (closestBeat != -1) { - pos = static_cast(floor(closestBeat)); + if (m_pQuantizeEnabled->toBool() && m_pBeats != nullptr) { + if (m_bAdjustingLoopIn) { + double closestBeat = m_pClosestBeat->get(); + if (closestBeat == getCurrentSample()) { + quantizedBeat = closestBeat; + } else { + quantizedBeat = m_pPreviousBeat->get(); + } + } else { + quantizedBeat = m_pClosestBeat->get(); + } + if (quantizedBeat != -1) { + pos = static_cast(floor(quantizedBeat)); } } + if (pos != -1 && !even(pos)) { + pos--; + } + // Reset the loop out position if it is before the loop in so that loops // cannot be inverted. if (loopSamples.end != kNoTrigger && @@ -441,8 +459,8 @@ void LoopingControl::slotLoopIn(double val) { // pre-defined beatloop size instead (when possible) if (loopSamples.end != kNoTrigger && (loopSamples.end - pos) < MINIMUM_AUDIBLE_LOOP_SIZE) { - if (closestBeat != -1 && m_pBeats) { - pos = static_cast(floor(m_pBeats->findNthBeat(closestBeat, -2))); + if (quantizedBeat != -1 && m_pBeats) { + pos = static_cast(floor(m_pBeats->findNthBeat(quantizedBeat, -2))); if (pos == -1 || (loopSamples.end - pos) < MINIMUM_AUDIBLE_LOOP_SIZE) { pos = loopSamples.end - MINIMUM_AUDIBLE_LOOP_SIZE; } @@ -451,30 +469,73 @@ void LoopingControl::slotLoopIn(double val) { } } - if (pos != -1 && !even(pos)) { - pos--; - } - loopSamples.start = pos; m_pCOLoopStartPosition->set(loopSamples.start); + if (m_pQuantizeEnabled->toBool() + && loopSamples.start < loopSamples.end + && m_pBeats != nullptr) { + m_pCOBeatLoopSize->setAndConfirm( + m_pBeats->numBeatsInRange(loopSamples.start, loopSamples.end)); + } + m_loopSamples.setValue(loopSamples); //qDebug() << "set loop_in to " << loopSamples.start; } -void LoopingControl::slotLoopOut(double val) { - if (!m_pTrack || val <= 0.0) { +void LoopingControl::slotLoopIn(double pressed) { + if (m_pTrack == nullptr) { return; } + // If loop is enabled, suspend looping and set the loop in point + // when this button is released. + if (m_bLoopingEnabled) { + if (pressed > 0.0) { + m_bAdjustingLoopIn = true; + // Adjusting both the in and out point at the same time makes no sense + m_bAdjustingLoopOut = false; + } else { + setLoopInToCurrentPosition(); + m_bAdjustingLoopIn = false; + } + } else { + if (pressed > 0.0) { + setLoopInToCurrentPosition(); + } + m_bAdjustingLoopIn = false; + } +} + +void LoopingControl::slotLoopInGoto(double pressed) { + if (pressed > 0.0) { + seekAbs(static_cast( + m_loopSamples.getValue().start)); + } +} + +void LoopingControl::setLoopOutToCurrentPosition() { LoopSamples loopSamples = m_loopSamples.getValue(); - double closestBeat = -1; + double quantizedBeat = -1; int pos = m_iCurrentSample; - if (m_pQuantizeEnabled->toBool()) { - closestBeat = m_pClosestBeat->get(); - if (closestBeat != -1) { - pos = static_cast(floor(closestBeat)); + if (m_pQuantizeEnabled->toBool() && m_pBeats != nullptr) { + if (m_bAdjustingLoopOut) { + double closestBeat = m_pClosestBeat->get(); + if (closestBeat == getCurrentSample()) { + quantizedBeat = closestBeat; + } else { + quantizedBeat = m_pNextBeat->get(); + } + } else { + quantizedBeat = m_pClosestBeat->get(); } + if (quantizedBeat != -1) { + pos = static_cast(floor(quantizedBeat)); + } + } + + if (pos != -1 && !even(pos)) { + pos++; // Increment to avoid shortening too-short loops } // If the user is trying to set a loop-out before the loop in or without @@ -487,8 +548,8 @@ void LoopingControl::slotLoopOut(double val) { // inaudible (which can happen easily with quantize-to-beat enabled,) // use the smallest pre-defined beatloop instead (when possible) if ((pos - loopSamples.start) < MINIMUM_AUDIBLE_LOOP_SIZE) { - if (closestBeat != -1 && m_pBeats) { - pos = static_cast(floor(m_pBeats->findNthBeat(closestBeat, 2))); + if (quantizedBeat != -1 && m_pBeats) { + pos = static_cast(floor(m_pBeats->findNthBeat(quantizedBeat, 2))); if (pos == -1 || (pos - loopSamples.start) < MINIMUM_AUDIBLE_LOOP_SIZE) { pos = loopSamples.start + MINIMUM_AUDIBLE_LOOP_SIZE; } @@ -497,16 +558,16 @@ void LoopingControl::slotLoopOut(double val) { } } - if (pos != -1 && !even(pos)) { - pos++; // Increment to avoid shortening too-short loops - } - clearActiveBeatLoop(); // set loop out position loopSamples.end = pos; m_pCOLoopEndPosition->set(loopSamples.end); m_loopSamples.setValue(loopSamples); + if (m_pQuantizeEnabled->toBool() && m_pBeats != nullptr) { + m_pCOBeatLoopSize->setAndConfirm( + m_pBeats->numBeatsInRange(loopSamples.start, loopSamples.end)); + } // start looping if (loopSamples.start != kNoTrigger && @@ -516,18 +577,57 @@ void LoopingControl::slotLoopOut(double val) { //qDebug() << "set loop_out to " << loopSamples.end; } +void LoopingControl::slotLoopOut(double pressed) { + if (m_pTrack == nullptr) { + return; + } + + // If loop is enabled, suspend looping and set the loop out point + // when this button is released. + if (m_bLoopingEnabled) { + if (pressed > 0.0) { + m_bAdjustingLoopOut = true; + // Adjusting both the in and out point at the same time makes no sense + m_bAdjustingLoopIn = false; + } else { + // If this button was pressed to set the loop out point when loop + // was disabled, that will enable looping, so avoid moving the + // loop out point when the button is released. + if (!m_bLoopOutPressedWhileLoopDisabled) { + setLoopOutToCurrentPosition(); + m_bAdjustingLoopOut = false; + } else { + m_bLoopOutPressedWhileLoopDisabled = false; + } + } + } else { + if (pressed > 0.0) { + setLoopOutToCurrentPosition(); + m_bLoopOutPressedWhileLoopDisabled = true; + } + m_bAdjustingLoopOut = false; + } +} + +void LoopingControl::slotLoopOutGoto(double pressed) { + if (pressed > 0.0) { + seekAbs(static_cast( + m_loopSamples.getValue().end)); + } +} + void LoopingControl::slotLoopExit(double val) { if (!m_pTrack || val <= 0.0) { return; } - + // If we're looping, stop looping if (m_bLoopingEnabled) { setLoopingEnabled(false); } } -void LoopingControl::slotReloopExit(double val) { +void LoopingControl::slotReloopToggle(double val) { if (!m_pTrack || val <= 0.0) { return; } @@ -540,15 +640,33 @@ void LoopingControl::slotReloopExit(double val) { m_bLoopRollActive = false; } setLoopingEnabled(false); - //qDebug() << "reloop_exit looping off"; + //qDebug() << "reloop_toggle looping off"; } else { - // If we're not looping, jump to the loop-in point and start looping + // If we're not looping, enable the loop. If the loop is ahead of the + // current play position, do not jump to it. LoopSamples loopSamples = m_loopSamples.getValue(); if (loopSamples.start != kNoTrigger && loopSamples.end != kNoTrigger && loopSamples.start <= loopSamples.end) { + if (getCurrentSample() < loopSamples.start) { + m_bReloopCatchUpcomingLoop = true; + } setLoopingEnabled(true); + // If we're not playing, jump to the loop in point so the waveform + // shows where it will play from when playback resumes. + if (!m_pPlayButton->toBool() && !m_bReloopCatchUpcomingLoop) { + slotLoopInGoto(1); + } } - //qDebug() << "reloop_exit looping on"; + //qDebug() << "reloop_toggle looping on"; + } +} + +void LoopingControl::slotReloopAndStop(double pressed) { + if (pressed > 0) { + m_pPlayButton->set(0.0); + seekAbs(static_cast( + m_loopSamples.getValue().start)); + setLoopingEnabled(true); } } @@ -624,7 +742,9 @@ void LoopingControl::slotLoopEndPos(double pos) { void LoopingControl::notifySeek(double dNewPlaypos) { LoopSamples loopSamples = m_loopSamples.getValue(); if (m_bLoopingEnabled) { - if (dNewPlaypos < loopSamples.start || dNewPlaypos > loopSamples.end) { + // Disable loop when we jump after it, using hot cues or waveform overview + // If we jump before, the loop it is kept enabled as catching loop + if (dNewPlaypos > loopSamples.end) { setLoopingEnabled(false); } } @@ -645,6 +765,7 @@ void LoopingControl::setLoopingEnabled(bool enabled) { void LoopingControl::trackLoaded(TrackPointer pNewTrack, TrackPointer pOldTrack) { Q_UNUSED(pOldTrack); + if (m_pTrack) { disconnect(m_pTrack.get(), SIGNAL(beatsUpdated()), this, SLOT(slotUpdatedTrackBeats())); @@ -679,7 +800,7 @@ void LoopingControl::slotBeatLoopActivate(BeatLoopingControl* pBeatLoopControl) // looping. slotBeatLoop will update m_pActiveBeatLoop if applicable. Note, // this used to only maintain the current start point if a beatloop was // enabled. See Bug #1159243. - slotBeatLoop(pBeatLoopControl->getSize(), m_bLoopingEnabled); + slotBeatLoop(pBeatLoopControl->getSize(), m_bLoopingEnabled, true); } void LoopingControl::slotBeatLoopActivateRoll(BeatLoopingControl* pBeatLoopControl) { @@ -689,7 +810,7 @@ void LoopingControl::slotBeatLoopActivateRoll(BeatLoopingControl* pBeatLoopContr // Disregard existing loops. m_pSlipEnabled->set(1); - slotBeatLoop(pBeatLoopControl->getSize(), false); + slotBeatLoop(pBeatLoopControl->getSize(), false, true); m_bLoopRollActive = true; } @@ -701,8 +822,12 @@ void LoopingControl::slotBeatLoopDeactivate(BeatLoopingControl* pBeatLoopControl void LoopingControl::slotBeatLoopDeactivateRoll(BeatLoopingControl* pBeatLoopControl) { Q_UNUSED(pBeatLoopControl); setLoopingEnabled(false); - m_pSlipEnabled->set(0); - m_bLoopRollActive = false; + // Make sure slip mode is not turned off if it was turned on + // by something that was not a rolling beatloop. + if (m_bLoopRollActive) { + m_pSlipEnabled->set(0); + m_bLoopRollActive = false; + } } void LoopingControl::clearActiveBeatLoop() { @@ -712,120 +837,96 @@ void LoopingControl::clearActiveBeatLoop() { } } -void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint) { - int samples = m_pTrackSamples->get(); - if (!m_pTrack || samples == 0) { - clearActiveBeatLoop(); - return; +bool LoopingControl::currentLoopMatchesBeatloopSize() { + if (m_pBeats == nullptr) { + return false; } - if (!m_pBeats) { - clearActiveBeatLoop(); - return; + LoopSamples loopSamples = m_loopSamples.getValue(); + + // Calculate where the loop out point would be if it is a beatloop + int beatLoopOutPoint = + m_pBeats->findNBeatsFromSample(loopSamples.start, m_pCOBeatLoopSize->get()); + if (!even(beatLoopOutPoint)) { + beatLoopOutPoint--; } - // For now we do not handle negative beatloops. + return loopSamples.end == beatLoopOutPoint; +} + +void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable) { + double maxBeatSize = s_dBeatSizes[sizeof(s_dBeatSizes)/sizeof(s_dBeatSizes[0]) - 1]; + double minBeatSize = s_dBeatSizes[0]; if (beats < 0) { + // For now we do not handle negative beatloops. clearActiveBeatLoop(); return; + } else if (beats > maxBeatSize) { + beats = maxBeatSize; + } else if (beats < minBeatSize) { + beats = minBeatSize; } - // O(n) search, but there are only ~10-ish beatloop controls so this is - // fine. - for (BeatLoopingControl* pBeatLoopControl: m_beatLoops) { - if (pBeatLoopControl->getSize() == beats) { - pBeatLoopControl->activate(); - BeatLoopingControl* pOldBeatLoop = - m_pActiveBeatLoop.fetchAndStoreRelease(pBeatLoopControl); - if (pOldBeatLoop != nullptr && pOldBeatLoop != pBeatLoopControl) { - pOldBeatLoop->deactivate(); - } - break; - } + int samples = m_pTrackSamples->get(); + if (!m_pTrack || samples == 0 + || !m_pBeats) { + clearActiveBeatLoop(); + m_pCOBeatLoopSize->setAndConfirm(beats); + return; } + // Calculate the new loop start and end samples // give start and end defaults so we can detect problems LoopSamples newloopSamples = {kNoTrigger, kNoTrigger}; LoopSamples loopSamples = m_loopSamples.getValue(); - - // For positive numbers we start from the current position/closest beat and + // Start from the current position/closest beat and // create the loop around X beats from there. - if (beats > 0) { - if (keepStartPoint) { + if (keepStartPoint) { + if (loopSamples.start != kNoTrigger) { newloopSamples.start = loopSamples.start; } else { - // loop_in is set to the previous beat if quantize is on. The - // closest beat might be ahead of play position which would cause a seek. - // TODO: If in reverse, should probably choose nextBeat. - double cur_pos = getCurrentSample(); - double prevBeat; - double nextBeat; - m_pBeats->findPrevNextBeats(cur_pos, &prevBeat, &nextBeat); - - if (m_pQuantizeEnabled->get() > 0.0 && prevBeat != -1) { - if (beats >= 1.0) { - newloopSamples.start = prevBeat; - } else { - // In case of beat length less then 1 beat: - // (| - beats, ^ - current track's position): - // - // ...|...................^........|... - // - // If we press 1/2 beatloop we want loop from 50% to 100%, - // If I press 1/4 beatloop, we want loop from 50% to 75% etc - double beat_len = nextBeat - prevBeat; - double loops_per_beat = 1.0 / beats; - double beat_pos = cur_pos - prevBeat; - int beat_frac = - static_cast(floor((beat_pos / beat_len) * - loops_per_beat)); - newloopSamples.start = prevBeat + beat_len / loops_per_beat * beat_frac; - } - + newloopSamples.start = getCurrentSample(); + } + } else { + // loop_in is set to the previous beat if quantize is on. The + // closest beat might be ahead of play position which would cause a seek. + // TODO: If in reverse, should probably choose nextBeat. + double cur_pos = getCurrentSample(); + double prevBeat; + double nextBeat; + m_pBeats->findPrevNextBeats(cur_pos, &prevBeat, &nextBeat); + + if (m_pQuantizeEnabled->toBool() && prevBeat != -1) { + if (beats >= 1.0) { + newloopSamples.start = prevBeat; } else { - newloopSamples.start = floor(cur_pos); - } - - - if (!even(newloopSamples.start)) { - newloopSamples.start--; + // In case of beat length less then 1 beat: + // (| - beats, ^ - current track's position): + // + // ...|...................^........|... + // + // If we press 1/2 beatloop we want loop from 50% to 100%, + // If I press 1/4 beatloop, we want loop from 50% to 75% etc + double beat_len = nextBeat - prevBeat; + double loops_per_beat = 1.0 / beats; + double beat_pos = cur_pos - prevBeat; + int beat_frac = + static_cast(floor((beat_pos / beat_len) * + loops_per_beat)); + newloopSamples.start = prevBeat + beat_len / loops_per_beat * beat_frac; } - } - - int fullbeats = static_cast(beats); - double fracbeats = beats - static_cast(fullbeats); - - // Now we need to calculate the length of the beatloop. We do this by - // taking the current beat and the fullbeats'th beat and measuring the - // distance between them. - newloopSamples.end = newloopSamples.start; - - if (fullbeats > 0) { - // Add the length between this beat and the fullbeats'th beat to the - // loop_out position; - // TODO: figure out how to convert this to a findPrevNext call. - double this_beat = m_pBeats->findNthBeat(newloopSamples.start, 1); - double nth_beat = m_pBeats->findNthBeat(newloopSamples.start, 1 + fullbeats); - newloopSamples.end += (nth_beat - this_beat); - } - if (fracbeats > 0) { - // Add the fraction of the beat following the current loop_out - // position to loop out. - // TODO: figure out how to convert this to a findPrevNext call. - double loop_out_beat = m_pBeats->findNthBeat(newloopSamples.end, 1); - double loop_out_next_beat = m_pBeats->findNthBeat(newloopSamples.end, 2); - newloopSamples.end += (loop_out_next_beat - loop_out_beat) * fracbeats; + } else { + newloopSamples.start = floor(cur_pos); } } - if ((newloopSamples.start == kNoTrigger) || (newloopSamples.end == kNoTrigger)) - return; - if (!even(newloopSamples.start)) { newloopSamples.start--; } + + newloopSamples.end = m_pBeats->findNBeatsFromSample(newloopSamples.start, beats); if (!even(newloopSamples.end)) { newloopSamples.end--; } @@ -836,12 +937,66 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint) { } else { newloopSamples.end += 2; } - } else if (newloopSamples.end > samples) { - // Do not allow beat loops to go beyond the end of the track - newloopSamples.end = samples; } - if (keepStartPoint) { + // Do not allow beat loops to go beyond the end of the track + if (newloopSamples.end > samples) { + // 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 + // a smaller size, but not get larger. + double previousBeatloopSize = m_pCOBeatLoopSize->get(); + double previousBeatloopOutPoint = + m_pBeats->findNBeatsFromSample(newloopSamples.start, previousBeatloopSize); + if (previousBeatloopOutPoint < newloopSamples.start + && beats < previousBeatloopSize) { + m_pCOBeatLoopSize->setAndConfirm(beats); + } + return; + } + + // When loading a new track or after setting a manual loop without quantize, + // do not resize the existing loop until beatloop_size matches + // the size of the existing loop. + // Do not return immediately so beatloop_size can be updated. + bool avoidResize = false; + if (!currentLoopMatchesBeatloopSize() && !enable) { + avoidResize = true; + } + + if (m_pCOBeatLoopSize->get() != beats) { + m_pCOBeatLoopSize->setAndConfirm(beats); + } + + // This check happens after setting m_pCOBeatLoopSize so + // beatloop_size can be prepared without having a track loaded. + if ((newloopSamples.start == kNoTrigger) || (newloopSamples.end == kNoTrigger)) { + return; + } + + if (avoidResize) { + return; + } + + // O(n) search, but there are only ~10-ish beatloop controls so this is + // fine. + for (BeatLoopingControl* pBeatLoopControl: m_beatLoops) { + if (pBeatLoopControl->getSize() == beats) { + if (enable || m_bLoopingEnabled) { + pBeatLoopControl->activate(); + } + BeatLoopingControl* pOldBeatLoop = + m_pActiveBeatLoop.fetchAndStoreRelease(pBeatLoopControl); + if (pOldBeatLoop != nullptr && pOldBeatLoop != pBeatLoopControl) { + pOldBeatLoop->deactivate(); + } + break; + } + } + + // If resizing an inactive loop by changing beatloop_size, + // do not seek to the adjusted loop. + if (keepStartPoint && (enable || m_bLoopingEnabled)) { seekInsideAdjustedLoop(loopSamples.start, loopSamples.end, newloopSamples.start, newloopSamples.end); } @@ -849,7 +1004,47 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint) { m_loopSamples.setValue(newloopSamples); m_pCOLoopStartPosition->set(newloopSamples.start); m_pCOLoopEndPosition->set(newloopSamples.end); - setLoopingEnabled(true); + if (enable) { + setLoopingEnabled(true); + } +} + +void LoopingControl::slotBeatLoopSizeChangeRequest(double beats) { + // slotBeatLoop will call m_pCOBeatLoopSize->setAndConfirm if + // new beatloop_size is valid + slotBeatLoop(beats, true, false); +} + +void LoopingControl::slotBeatLoopToggle(double pressed) { + if (pressed > 0) { + slotBeatLoop(m_pCOBeatLoopSize->get()); + } +} + +void LoopingControl::slotBeatLoopRollActivate(double pressed) { + if (pressed > 0.0) { + if (m_bLoopingEnabled) { + setLoopingEnabled(false); + // Make sure slip mode is not turned off if it was turned on + // by something that was not a rolling beatloop. + if (m_bLoopRollActive) { + m_pSlipEnabled->set(0.0); + m_bLoopRollActive = false; + } + } else { + m_pSlipEnabled->set(1.0); + slotBeatLoop(m_pCOBeatLoopSize->get()); + m_bLoopRollActive = true; + } + } else { + setLoopingEnabled(false); + // Make sure slip mode is not turned off if it was turned on + // by something that was not a rolling beatloop. + if (m_bLoopRollActive) { + m_pSlipEnabled->set(0.0); + m_bLoopRollActive = false; + } + } } void LoopingControl::slotBeatJump(double beats) { @@ -857,16 +1052,27 @@ void LoopingControl::slotBeatJump(double beats) { return; } - double dPosition = getCurrentSample(); - double dBeatLength; - if (BpmControl::getBeatContext(m_pBeats, dPosition, - NULL, NULL, &dBeatLength, NULL)) { - seekAbs(dPosition + beats * dBeatLength); + if (m_bLoopingEnabled && !m_bAdjustingLoopIn && !m_bAdjustingLoopOut) { + slotLoopMove(beats); + } else { + seekAbs(m_pBeats->findNBeatsFromSample(getCurrentSample(), beats)); + } +} + +void LoopingControl::slotBeatJumpForward(double pressed) { + if (pressed) { + slotBeatJump(m_pCOBeatJumpSize->get()); + } +} + +void LoopingControl::slotBeatJumpBackward(double pressed) { + if (pressed) { + slotBeatJump(-1.0 * m_pCOBeatJumpSize->get()); } } void LoopingControl::slotLoopMove(double beats) { - if (!m_pTrack || !m_pBeats) { + if (m_pTrack == nullptr || m_pBeats == nullptr || beats == 0) { return; } LoopSamples loopSamples = m_loopSamples.getValue(); @@ -874,14 +1080,12 @@ void LoopingControl::slotLoopMove(double beats) { return; } - double dPosition = getCurrentSample(); - double dBeatLength; - if (BpmControl::getBeatContext(m_pBeats, dPosition, - NULL, NULL, &dBeatLength, NULL)) { + if (BpmControl::getBeatContext(m_pBeats, getCurrentSample(), + nullptr, nullptr, nullptr, nullptr)) { int old_loop_in = loopSamples.start; int old_loop_out = loopSamples.end; - int new_loop_in = old_loop_in + (beats * dBeatLength); - int new_loop_out = old_loop_out + (beats * dBeatLength); + int new_loop_in = m_pBeats->findNBeatsFromSample(old_loop_in, beats); + int new_loop_out = m_pBeats->findNBeatsFromSample(old_loop_out, beats); if (!even(new_loop_in)) { --new_loop_in; } @@ -968,14 +1172,14 @@ BeatJumpControl::~BeatJumpControl() { delete m_pJumpBackward; } -void BeatJumpControl::slotJumpBackward(double v) { - if (v > 0) { +void BeatJumpControl::slotJumpBackward(double pressed) { + if (pressed > 0) { emit(beatJump(-m_dBeatJumpSize)); } } -void BeatJumpControl::slotJumpForward(double v) { - if (v > 0) { +void BeatJumpControl::slotJumpForward(double pressed) { + if (pressed > 0) { emit(beatJump(m_dBeatJumpSize)); } } @@ -1045,6 +1249,7 @@ BeatLoopingControl::BeatLoopingControl(QString group, double size) // An indicator control which is 1 if the beatloop is enabled and 0 if not. m_pEnabled = new ControlObject( keyForControl(group, "beatloop_%1_enabled", size)); + m_pEnabled->setReadOnly(); } BeatLoopingControl::~BeatLoopingControl() { @@ -1058,7 +1263,7 @@ BeatLoopingControl::~BeatLoopingControl() { void BeatLoopingControl::deactivate() { if (m_bActive) { m_bActive = false; - m_pEnabled->set(0); + m_pEnabled->forceSet(0); m_pLegacy->set(0); } } @@ -1066,7 +1271,7 @@ void BeatLoopingControl::deactivate() { void BeatLoopingControl::activate() { if (!m_bActive) { m_bActive = true; - m_pEnabled->set(1); + m_pEnabled->forceSet(1); m_pLegacy->set(1); } } diff --git a/src/engine/loopingcontrol.h b/src/engine/loopingcontrol.h index 41cd5d6d1ec9..c419e1f90213 100644 --- a/src/engine/loopingcontrol.h +++ b/src/engine/loopingcontrol.h @@ -59,10 +59,13 @@ class LoopingControl : public EngineControl { virtual void notifySeek(double dNewPlaypos); public slots: - void slotLoopIn(double); - void slotLoopOut(double); + void slotLoopIn(double pressed); + void slotLoopInGoto(double); + void slotLoopOut(double pressed); + void slotLoopOutGoto(double); void slotLoopExit(double); - void slotReloopExit(double); + void slotReloopToggle(double); + void slotReloopAndStop(double); void slotLoopStartPos(double); void slotLoopEndPos(double); virtual void trackLoaded(TrackPointer pNewTrack, TrackPointer pOldTrack) override; @@ -70,7 +73,10 @@ 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); + void slotBeatLoop(double loopSize, bool keepStartPoint=false, bool enable=true); + void slotBeatLoopSizeChangeRequest(double beats); + void slotBeatLoopToggle(double pressed); + void slotBeatLoopRollActivate(double pressed); void slotBeatLoopActivate(BeatLoopingControl* pBeatLoopControl); void slotBeatLoopActivateRoll(BeatLoopingControl* pBeatLoopControl); void slotBeatLoopDeactivate(BeatLoopingControl* pBeatLoopControl); @@ -78,13 +84,15 @@ class LoopingControl : public EngineControl { // Jump forward or backward by beats. void slotBeatJump(double beats); + void slotBeatJumpForward(double pressed); + void slotBeatJumpBackward(double pressed); // Move the loop by beats. void slotLoopMove(double beats); - void slotLoopScale(double); - void slotLoopDouble(double); - void slotLoopHalve(double); + void slotLoopScale(double scaleFactor); + void slotLoopDouble(double pressed); + void slotLoopHalve(double pressed); private: @@ -94,44 +102,64 @@ class LoopingControl : public EngineControl { }; void setLoopingEnabled(bool enabled); + void setLoopInToCurrentPosition(); + void setLoopOutToCurrentPosition(); void clearActiveBeatLoop(); + int calculateEndOfBeatloop(int startSample, double beatloopSizeInBeats); + bool currentLoopMatchesBeatloopSize(); // When a loop changes size such that the playposition is outside of the loop, // we can figure out the best place in the new loop to seek to maintain // the beat. It will even keep multi-bar phrasing correct with 4/4 tracks. void seekInsideAdjustedLoop(int old_loop_in, int old_loop_out, int new_loop_in, int new_loop_out); + ControlPushButton* m_pCOBeatLoopActivate; + ControlPushButton* m_pCOBeatLoopRollActivate; ControlObject* m_pCOLoopStartPosition; ControlObject* m_pCOLoopEndPosition; ControlObject* m_pCOLoopEnabled; ControlPushButton* m_pLoopInButton; + ControlPushButton* m_pLoopInGotoButton; ControlPushButton* m_pLoopOutButton; + ControlPushButton* m_pLoopOutGotoButton; ControlPushButton* m_pLoopExitButton; - ControlPushButton* m_pReloopExitButton; + ControlPushButton* m_pReloopToggleButton; + ControlPushButton* m_pReloopAndStopButton; ControlObject* m_pCOLoopScale; ControlPushButton* m_pLoopHalveButton; ControlPushButton* m_pLoopDoubleButton; ControlObject* m_pSlipEnabled; - - bool m_bLoopingEnabled; - bool m_bLoopRollActive; + ControlObject* m_pPlayButton; + + bool m_bLoopingEnabled = false; + bool m_bLoopRollActive = false; + bool m_bLoopManualTogglePressedToExitLoop = false; + bool m_bReloopCatchUpcomingLoop = false; + bool m_bAdjustingLoopIn = false; + bool m_bAdjustingLoopOut = false; + bool m_bLoopOutPressedWhileLoopDisabled = false; // TODO(DSC) Make the following values double ControlValueAtomic m_loopSamples; QAtomicInt m_iCurrentSample; ControlObject* m_pQuantizeEnabled; ControlObject* m_pNextBeat; + ControlObject* m_pPreviousBeat; ControlObject* m_pClosestBeat; ControlObject* m_pTrackSamples; QAtomicPointer m_pActiveBeatLoop; // Base BeatLoop Control Object. ControlObject* m_pCOBeatLoop; + ControlObject* m_pCOBeatLoopSize; // Different sizes for Beat Loops/Seeks. static double s_dBeatSizes[]; // Array of BeatLoopingControls, one for each size. QList m_beatLoops; ControlObject* m_pCOBeatJump; + ControlObject* m_pCOBeatJumpSize; + ControlPushButton* m_pCOBeatJumpForward; + ControlPushButton* m_pCOBeatJumpBackward; QList m_beatJumps; ControlObject* m_pCOLoopMove; @@ -174,8 +202,8 @@ class BeatJumpControl : public QObject { void beatJump(double beats); public slots: - void slotJumpForward(double value); - void slotJumpBackward(double value); + void slotJumpForward(double pressed); + void slotJumpBackward(double pressed); private: double m_dBeatJumpSize; diff --git a/src/engine/sync/enginesync.cpp b/src/engine/sync/enginesync.cpp index dd2e38aaea52..955ef7c52a2c 100644 --- a/src/engine/sync/enginesync.cpp +++ b/src/engine/sync/enginesync.cpp @@ -381,7 +381,7 @@ EngineChannel* EngineSync::pickNonSyncSyncTarget(EngineChannel* pDontPick) const EngineBuffer* pBuffer = pChannel->getEngineBuffer(); if (pBuffer && pBuffer->getBpm() > 0) { // If the deck is playing then go with it immediately. - if (fabs(pBuffer->getSpeed()) > 0) { + if (pBuffer->getSpeed() != 0.0) { return pChannel; } // Otherwise hold out for a deck that might be playing but diff --git a/src/mixer/basetrackplayer.cpp b/src/mixer/basetrackplayer.cpp index 09da2c454272..8aa82087bcd1 100644 --- a/src/mixer/basetrackplayer.cpp +++ b/src/mixer/basetrackplayer.cpp @@ -203,6 +203,30 @@ void BaseTrackPlayerImpl::slotLoadTrack(TrackPointer pNewTrack, bool bPlay) { m_pLoadedTrack = pNewTrack; if (m_pLoadedTrack) { + // Clear loop + // It seems that the trick is to first clear the loop out point, and then + // the loop in point. If we first clear the loop in point, the loop out point + // does not get cleared. + m_pLoopOutPoint->set(-1); + m_pLoopInPoint->set(-1); + + // The loop in and out points must be set here and not in slotTrackLoaded + // so LoopingControl::trackLoaded can access them. + const QList trackCues(pNewTrack->getCuePoints()); + QListIterator it(trackCues); + while (it.hasNext()) { + CuePointer pCue(it.next()); + if (pCue->getType() == Cue::LOOP) { + double loopStart = pCue->getPosition(); + double loopEnd = loopStart + pCue->getLength(); + if (loopStart != -1 && loopEnd != -1) { + m_pLoopInPoint->set(loopStart); + m_pLoopOutPoint->set(loopEnd); + break; + } + } + } + // Listen for updates to the file's BPM connect(m_pLoadedTrack.get(), SIGNAL(bpmUpdated(double)), m_pBPM.get(), SLOT(set(double))); @@ -279,27 +303,6 @@ void BaseTrackPlayerImpl::slotTrackLoaded(TrackPointer pNewTrack, m_pKey->set(m_pLoadedTrack->getKey()); setReplayGain(m_pLoadedTrack->getReplayGain().getRatio()); - // Clear loop - // It seems that the trick is to first clear the loop out point, and then - // the loop in point. If we first clear the loop in point, the loop out point - // does not get cleared. - m_pLoopOutPoint->set(-1); - m_pLoopInPoint->set(-1); - - const QList trackCues(pNewTrack->getCuePoints()); - QListIterator it(trackCues); - while (it.hasNext()) { - CuePointer pCue(it.next()); - if (pCue->getType() == Cue::LOOP) { - double loopStart = pCue->getPosition(); - double loopEnd = loopStart + pCue->getLength(); - if (loopStart != -1 && loopEnd != -1) { - m_pLoopInPoint->set(loopStart); - m_pLoopOutPoint->set(loopEnd); - break; - } - } - } if(m_pConfig->getValue( ConfigKey("[Mixer Profile]", "EqAutoReset"), false)) { if (m_pLowFilter != NULL) { diff --git a/src/skin/legacyskinparser.cpp b/src/skin/legacyskinparser.cpp index 9cb6b4726b7e..5d1e707b2741 100644 --- a/src/skin/legacyskinparser.cpp +++ b/src/skin/legacyskinparser.cpp @@ -60,6 +60,7 @@ #include "widget/weffectparameter.h" #include "widget/weffectbuttonparameter.h" #include "widget/weffectparameterbase.h" +#include "widget/wbeatspinbox.h" #include "widget/woverviewlmh.h" #include "widget/woverviewhsv.h" #include "widget/woverviewrgb.h" @@ -517,6 +518,8 @@ QList LegacySkinParser::parseNode(const QDomElement& node) { result = wrapWidget(parseStandardWidget(node)); } else if (nodeName == "Display") { result = wrapWidget(parseStandardWidget(node)); + } else if (nodeName == "BeatSpinBox") { + result = wrapWidget(parseBeatSpinBox(node)); } else if (nodeName == "NumberRate") { result = wrapWidget(parseNumberRate(node)); } else if (nodeName == "NumberPos") { @@ -1096,6 +1099,21 @@ QWidget* LegacySkinParser::parseEngineKey(const QDomElement& node) { return pEngineKey; } +QWidget* LegacySkinParser::parseBeatSpinBox(const QDomElement& node) { + bool createdValueControl = false; + ControlObject* valueControl = controlFromConfigNode(node.toElement(), "Value", &createdValueControl); + + WBeatSpinBox* pSpinbox = new WBeatSpinBox(m_pParent, valueControl); + commonWidgetSetup(node, pSpinbox); + pSpinbox->setup(node, *m_pContext); + + if (createdValueControl && valueControl != nullptr) { + valueControl->setParent(pSpinbox); + } + + return pSpinbox; +} + QWidget* LegacySkinParser::parseBattery(const QDomElement& node) { WBattery *p = new WBattery(m_pParent); setupBaseWidget(node, p); diff --git a/src/skin/legacyskinparser.h b/src/skin/legacyskinparser.h index c5b2d67b0e7d..7175afe04e56 100644 --- a/src/skin/legacyskinparser.h +++ b/src/skin/legacyskinparser.h @@ -77,6 +77,7 @@ class LegacySkinParser : public QObject, public SkinParser { QWidget* parseNumberRate(const QDomElement& node); QWidget* parseNumberPos(const QDomElement& node); QWidget* parseEngineKey(const QDomElement& node); + QWidget* parseBeatSpinBox(const QDomElement& node); QWidget* parseEffectChainName(const QDomElement& node); QWidget* parseEffectName(const QDomElement& node); QWidget* parseEffectParameterName(const QDomElement& node); diff --git a/src/skin/tooltips.cpp b/src/skin/tooltips.cpp index efc977c4555c..fcc813ed3fd0 100644 --- a/src/skin/tooltips.cpp +++ b/src/skin/tooltips.cpp @@ -30,6 +30,8 @@ void Tooltips::addStandardTooltips() { QString leftClick = tr("Left-click"); QString rightClick = tr("Right-click"); QString scrollWheel = tr("Scroll-wheel"); + QString loopActive = "(" + tr("loop active") + ")"; + QString loopInactive = "(" + tr("loop inactive") + ")"; QString effectsWithinChain = tr("Effects within the chain must be enabled to hear them."); add("waveform_overview") @@ -554,13 +556,21 @@ void Tooltips::addStandardTooltips() { add("loop_in") << tr("Loop-In Marker") - << tr("Sets the deck loop-in position to the current play position.") - << quantizeSnap; + << QString("%1: %2").arg(leftClick + " " + loopInactive, + tr("Sets the track Loop-In Marker to the current play position.")) + << quantizeSnap + << QString("%1: %2").arg(leftClick + " " + loopActive, + tr("Press and hold to move Loop-In Marker.")) + << QString("%1: %2").arg(rightClick, tr("Jump to Loop-In Marker.")); add("loop_out") << tr("Loop-Out Marker") - << tr("Sets the deck loop-out position to the current play position.") - << quantizeSnap; + << QString("%1: %2").arg(leftClick + " " + loopInactive, + tr("Sets the track Loop-Out Marker to the current play position.")) + << quantizeSnap + << QString("%1: %2").arg(leftClick + " " + loopActive, + tr("Press and hold to move Loop-Out Marker.")) + << QString("%1: %2").arg(rightClick, tr("Jump to Loop-Out Marker.")); add("loop_halve") << tr("Loop Halve") @@ -571,31 +581,54 @@ void Tooltips::addStandardTooltips() { << tr("Loop Double") << tr("Doubles the current loop's length by moving the end marker."); + add("beatloop_size") + << tr("Beatloop Size") + << tr("Select the size of the loop in beats to set with the Beatloop button.") + << tr("Changing this resizes the loop if the loop already matches this size."); + + add("beatloop_halve") + << tr("Halve the size of an existing beatloop, or halve the size of the next beatloop set with the Beatloop button."); + + add("beatloop_double") + << tr("Double the size of an existing beatloop, or double the size of the next beatloop set with the Beatloop button."); + //beatloop and beatlooproll - add("beatloop") + add("beatloop_activate") << tr("Beatloop") - << QString("%1: %2").arg(leftClick, tr("Setup a loop over the set number of beats.")) + << QString("%1: %2").arg(leftClick, tr("Start a loop over the set number of beats.")) << quantizeSnap - << QString("%1: %2").arg(rightClick, tr("Temporarily setup a rolling loop over the set number of beats.")) + << 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("beatjump") - << tr("Beatjump") - << QString("%1: %2").arg(leftClick, tr("Jump forward or backward by the set number of beats.")); + 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."); + + 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.")); - add("loop_move") - << tr("Loop Move") - << QString("%1: %2").arg(leftClick, tr("Adjust the loop in and out points by the set number of beats.")); + 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.")); add("loop_exit") << tr("Loop Exit") << tr("Turns the current loop off.") << tr("Works only if Loop-In and Loop-Out marker are set."); - add("reloop_exit") - << tr("Reloop/Exit") - << tr("Toggles the current loop on or off.") - << tr("Works only if Loop-In and Loop-Out marker are set."); + add("reloop_toggle") + << tr("Reloop") + << QString("%1: %2").arg(leftClick, tr("Toggles the current loop on or off.")) + << tr("If the loop is ahead of the current position, looping will start when the loop is reached.") + << tr("Works only if Loop-In and Loop-Out Marker are set.") + << QString("%1: %2").arg(rightClick, tr("Enable loop, jump to Loop-In Marker, and stop playback.")); add("slip_mode") << tr("Slip Mode") diff --git a/src/test/looping_control_test.cpp b/src/test/looping_control_test.cpp index 63d9173766df..33f87443ed5b 100644 --- a/src/test/looping_control_test.cpp +++ b/src/test/looping_control_test.cpp @@ -14,7 +14,7 @@ class LoopingControlTest : public MockedEngineBackendTest { public: LoopingControlTest() - : kTrackLengthSamples(3000) { + : kTrackLengthSamples(300000000) { } protected: @@ -32,16 +32,29 @@ class LoopingControlTest : public MockedEngineBackendTest { m_pButtonLoopIn = std::make_unique(m_sGroup1, "loop_in"); m_pButtonLoopOut = std::make_unique(m_sGroup1, "loop_out"); m_pButtonLoopExit = std::make_unique(m_sGroup1, "loop_exit"); - m_pButtonReloopExit = std::make_unique(m_sGroup1, "reloop_exit"); + m_pButtonReloopToggle = std::make_unique(m_sGroup1, "reloop_toggle"); + m_pButtonReloopAndStop = std::make_unique(m_sGroup1, "reloop_andstop"); m_pButtonLoopDouble = std::make_unique(m_sGroup1, "loop_double"); m_pButtonLoopHalve = std::make_unique(m_sGroup1, "loop_halve"); m_pLoopEnabled = std::make_unique(m_sGroup1, "loop_enabled"); m_pLoopStartPoint = std::make_unique(m_sGroup1, "loop_start_position"); m_pLoopEndPoint = std::make_unique(m_sGroup1, "loop_end_position"); + m_pLoopScale = std::make_unique(m_sGroup1, "loop_scale"); + m_pButtonPlay = std::make_unique(m_sGroup1, "play"); m_pPlayPosition = std::make_unique(m_sGroup1, "playposition"); m_pButtonBeatMoveForward = std::make_unique(m_sGroup1, "loop_move_1_forward"); m_pButtonBeatMoveBackward = std::make_unique(m_sGroup1, "loop_move_1_backward"); m_pButtonBeatLoop2Activate = std::make_unique(m_sGroup1, "beatloop_2_activate"); + m_pButtonBeatLoop4Activate = std::make_unique(m_sGroup1, "beatloop_4_activate"); + m_pBeatLoop2Enabled = std::make_unique(m_sGroup1, "beatloop_2_enabled"); + m_pBeatLoop4Enabled = std::make_unique(m_sGroup1, "beatloop_4_enabled"); + m_pBeatLoop64Enabled = std::make_unique(m_sGroup1, "beatloop_64_enabled"); + m_pBeatLoop = std::make_unique(m_sGroup1, "beatloop"); + m_pBeatLoopSize = std::make_unique(m_sGroup1, "beatloop_size"); + m_pButtonBeatLoopActivate = std::make_unique(m_sGroup1, "beatloop_activate"); + m_pBeatJumpSize = std::make_unique(m_sGroup1, "beatjump_size"); + m_pButtonBeatJumpForward = std::make_unique(m_sGroup1, "beatjump_forward"); + m_pButtonBeatJumpBackward = std::make_unique(m_sGroup1, "beatjump_backward"); } bool isLoopEnabled() { @@ -61,16 +74,29 @@ class LoopingControlTest : public MockedEngineBackendTest { std::unique_ptr m_pButtonLoopIn; std::unique_ptr m_pButtonLoopOut; std::unique_ptr m_pButtonLoopExit; - std::unique_ptr m_pButtonReloopExit; + std::unique_ptr m_pButtonReloopToggle; + std::unique_ptr m_pButtonReloopAndStop; std::unique_ptr m_pButtonLoopDouble; std::unique_ptr m_pButtonLoopHalve; std::unique_ptr m_pLoopEnabled; std::unique_ptr m_pLoopStartPoint; std::unique_ptr m_pLoopEndPoint; + std::unique_ptr m_pLoopScale; std::unique_ptr m_pPlayPosition; + std::unique_ptr m_pButtonPlay; std::unique_ptr m_pButtonBeatMoveForward; std::unique_ptr m_pButtonBeatMoveBackward; std::unique_ptr m_pButtonBeatLoop2Activate; + std::unique_ptr m_pButtonBeatLoop4Activate; + std::unique_ptr m_pBeatLoop2Enabled; + std::unique_ptr m_pBeatLoop4Enabled; + std::unique_ptr m_pBeatLoop64Enabled; + std::unique_ptr m_pBeatLoop; + std::unique_ptr m_pBeatLoopSize; + std::unique_ptr m_pButtonBeatLoopActivate; + std::unique_ptr m_pBeatJumpSize; + std::unique_ptr m_pButtonBeatJumpForward; + std::unique_ptr m_pButtonBeatJumpBackward; }; TEST_F(LoopingControlTest, LoopSet) { @@ -93,8 +119,8 @@ TEST_F(LoopingControlTest, LoopSetOddSamples) { TEST_F(LoopingControlTest, LoopInSetInsideLoopContinues) { m_pLoopStartPoint->slotSet(0); m_pLoopEndPoint->slotSet(100); - m_pButtonReloopExit->slotSet(1); - m_pButtonReloopExit->slotSet(0); + m_pButtonReloopToggle->slotSet(1); + m_pButtonReloopToggle->slotSet(0); seekToSampleAndProcess(50); EXPECT_TRUE(isLoopEnabled()); EXPECT_EQ(0, m_pLoopStartPoint->get()); @@ -108,8 +134,8 @@ TEST_F(LoopingControlTest, LoopInSetInsideLoopContinues) { TEST_F(LoopingControlTest, LoopInSetAfterLoopOutStops) { m_pLoopStartPoint->slotSet(0); m_pLoopEndPoint->slotSet(100); - m_pButtonReloopExit->slotSet(1); - m_pButtonReloopExit->slotSet(0); + m_pButtonReloopToggle->slotSet(1); + m_pButtonReloopToggle->slotSet(0); seekToSampleAndProcess(50); EXPECT_TRUE(isLoopEnabled()); EXPECT_EQ(0, m_pLoopStartPoint->get()); @@ -123,8 +149,8 @@ TEST_F(LoopingControlTest, LoopInSetAfterLoopOutStops) { TEST_F(LoopingControlTest, LoopOutSetInsideLoopContinues) { m_pLoopStartPoint->slotSet(0); m_pLoopEndPoint->slotSet(100); - m_pButtonReloopExit->slotSet(1); - m_pButtonReloopExit->slotSet(0); + m_pButtonReloopToggle->slotSet(1); + m_pButtonReloopToggle->slotSet(0); seekToSampleAndProcess(50); EXPECT_TRUE(isLoopEnabled()); EXPECT_EQ(0, m_pLoopStartPoint->get()); @@ -138,8 +164,8 @@ TEST_F(LoopingControlTest, LoopOutSetInsideLoopContinues) { TEST_F(LoopingControlTest, LoopOutSetBeforeLoopInIgnored) { m_pLoopStartPoint->slotSet(10); m_pLoopEndPoint->slotSet(100); - m_pButtonReloopExit->slotSet(1); - m_pButtonReloopExit->slotSet(0); + m_pButtonReloopToggle->slotSet(1); + m_pButtonReloopToggle->slotSet(0); seekToSampleAndProcess(50); EXPECT_TRUE(isLoopEnabled()); EXPECT_EQ(10, m_pLoopStartPoint->get()); @@ -171,14 +197,26 @@ TEST_F(LoopingControlTest, LoopInButton_QuantizeEnabledNoBeats) { EXPECT_EQ(50, m_pLoopStartPoint->get()); } -TEST_F(LoopingControlTest, LoopInButton_QuantizeEnabledClosestBeat) { - m_pQuantizeEnabled->set(1); - m_pClosestBeat->set(100); - m_pNextBeat->set(110); +TEST_F(LoopingControlTest, LoopInButton_AdjustLoopInPointOutsideLoop) { + m_pLoopStartPoint->slotSet(1000); + m_pLoopEndPoint->slotSet(2000); + m_pButtonReloopToggle->slotSet(1); + m_pButtonReloopToggle->slotSet(0); + m_pButtonLoopIn->slotSet(1); seekToSampleAndProcess(50); + m_pButtonLoopIn->slotSet(0); + EXPECT_EQ(50, m_pLoopStartPoint->get()); +} + +TEST_F(LoopingControlTest, LoopInButton_AdjustLoopInPointInsideLoop) { + m_pLoopStartPoint->slotSet(1000); + m_pLoopEndPoint->slotSet(2000); + m_pButtonReloopToggle->slotSet(1); + m_pButtonReloopToggle->slotSet(0); m_pButtonLoopIn->slotSet(1); + seekToSampleAndProcess(1500); m_pButtonLoopIn->slotSet(0); - EXPECT_EQ(100, m_pLoopStartPoint->get()); + EXPECT_EQ(1500, m_pLoopStartPoint->get()); } TEST_F(LoopingControlTest, LoopOutButton_QuantizeDisabled) { @@ -203,18 +241,47 @@ TEST_F(LoopingControlTest, LoopOutButton_QuantizeEnabledNoBeats) { EXPECT_EQ(500, m_pLoopEndPoint->get()); } -TEST_F(LoopingControlTest, LoopOutButton_QuantizeEnabledClosestBeat) { +TEST_F(LoopingControlTest, LoopOutButton_AdjustLoopOutPointOutsideLoop) { + m_pLoopStartPoint->slotSet(1000); + m_pLoopEndPoint->slotSet(2000); + m_pButtonReloopToggle->slotSet(1); + m_pButtonReloopToggle->slotSet(0); + m_pButtonLoopOut->slotSet(1); + seekToSampleAndProcess(3000); + m_pButtonLoopOut->slotSet(0); + EXPECT_EQ(3000, m_pLoopEndPoint->get()); +} + +TEST_F(LoopingControlTest, LoopOutButton_AdjustLoopOutPointInsideLoop) { + m_pLoopStartPoint->slotSet(100); + m_pLoopEndPoint->slotSet(2000); + m_pButtonReloopToggle->slotSet(1); + m_pButtonReloopToggle->slotSet(0); + m_pButtonLoopOut->slotSet(1); + seekToSampleAndProcess(1500); + m_pButtonLoopOut->slotSet(0); + EXPECT_EQ(1500, m_pLoopEndPoint->get()); +} + +TEST_F(LoopingControlTest, LoopInOutButtons_QuantizeEnabled) { + m_pTrack1->setBpm(60.0); m_pQuantizeEnabled->set(1); - m_pClosestBeat->set(1000); - m_pNextBeat->set(1100); seekToSampleAndProcess(500); - m_pLoopStartPoint->slotSet(0); + m_pButtonLoopIn->slotSet(1); + m_pButtonLoopIn->slotSet(0); + EXPECT_EQ(m_pClosestBeat->get(), m_pLoopStartPoint->get()); + m_pBeatJumpSize->set(13); + m_pButtonBeatJumpForward->set(1); + m_pButtonBeatJumpForward->set(0); + ProcessBuffer(); m_pButtonLoopOut->slotSet(1); m_pButtonLoopOut->slotSet(0); - EXPECT_EQ(1000, m_pLoopEndPoint->get()); + ProcessBuffer(); + EXPECT_EQ(m_pClosestBeat->get(), m_pLoopEndPoint->get()); + // FIXME: EXPECT_EQ(13.0, m_pBeatLoopSize->get()); } -TEST_F(LoopingControlTest, ReloopExitButton_TogglesLoop) { +TEST_F(LoopingControlTest, ReloopToggleButton_TogglesLoop) { m_pQuantizeEnabled->set(0); m_pClosestBeat->set(-1); m_pNextBeat->set(-1); @@ -225,13 +292,13 @@ TEST_F(LoopingControlTest, ReloopExitButton_TogglesLoop) { EXPECT_TRUE(isLoopEnabled()); EXPECT_EQ(0, m_pLoopStartPoint->get()); EXPECT_EQ(500, m_pLoopEndPoint->get()); - m_pButtonReloopExit->slotSet(1); - m_pButtonReloopExit->slotSet(0); + m_pButtonReloopToggle->slotSet(1); + m_pButtonReloopToggle->slotSet(0); EXPECT_FALSE(isLoopEnabled()); EXPECT_EQ(0, m_pLoopStartPoint->get()); EXPECT_EQ(500, m_pLoopEndPoint->get()); - m_pButtonReloopExit->slotSet(1); - m_pButtonReloopExit->slotSet(0); + m_pButtonReloopToggle->slotSet(1); + m_pButtonReloopToggle->slotSet(0); EXPECT_TRUE(isLoopEnabled()); EXPECT_EQ(0, m_pLoopStartPoint->get()); EXPECT_EQ(500, m_pLoopEndPoint->get()); @@ -245,35 +312,48 @@ TEST_F(LoopingControlTest, ReloopExitButton_TogglesLoop) { EXPECT_FALSE(isLoopEnabled()); } -TEST_F(LoopingControlTest, LoopDoubleButton_DoublesLoop) { +TEST_F(LoopingControlTest, ReloopToggleButton_DoesNotJumpAhead) { + m_pLoopStartPoint->slotSet(1000); + m_pLoopEndPoint->slotSet(2000); + seekToSampleAndProcess(0); + + m_pButtonReloopToggle->slotSet(1); + m_pButtonReloopToggle->slotSet(0); seekToSampleAndProcess(50); - m_pLoopStartPoint->slotSet(0); - m_pLoopEndPoint->slotSet(500); + EXPECT_LE(m_pChannel1->getEngineBuffer()->m_pLoopingControl->getCurrentSample(), m_pLoopStartPoint->get()); +} + +TEST_F(LoopingControlTest, ReloopAndStopButton) { + m_pLoopStartPoint->slotSet(1000); + m_pLoopEndPoint->slotSet(2000); + seekToSampleAndProcess(1500); + m_pButtonReloopToggle->slotSet(1); + m_pButtonReloopToggle->slotSet(0); + m_pButtonReloopAndStop->slotSet(1); + m_pButtonReloopAndStop->slotSet(0); + ProcessBuffer(); + EXPECT_EQ(m_pChannel1->getEngineBuffer()->m_pLoopingControl->getCurrentSample(), m_pLoopStartPoint->get()); + EXPECT_TRUE(m_pLoopEnabled->toBool()); +} + +TEST_F(LoopingControlTest, LoopScale_DoublesLoop) { + seekToSampleAndProcess(0); + m_pButtonLoopIn->set(1); + m_pButtonLoopIn->set(0); + seekToSampleAndProcess(500); + m_pButtonLoopOut->set(1); + m_pButtonLoopOut->set(0); EXPECT_EQ(0, m_pLoopStartPoint->get()); EXPECT_EQ(500, m_pLoopEndPoint->get()); - m_pButtonLoopDouble->slotSet(1); - m_pButtonLoopDouble->slotSet(0); + m_pLoopScale->set(2.0); EXPECT_EQ(0, m_pLoopStartPoint->get()); EXPECT_EQ(1000, m_pLoopEndPoint->get()); - m_pButtonLoopDouble->slotSet(1); - m_pButtonLoopDouble->slotSet(0); + m_pLoopScale->set(2.0); EXPECT_EQ(0, m_pLoopStartPoint->get()); EXPECT_EQ(2000, m_pLoopEndPoint->get()); } -TEST_F(LoopingControlTest, LoopDoubleButton_IgnoresPastTrackEnd) { - seekToSampleAndProcess(50); - m_pLoopStartPoint->slotSet(0); - m_pLoopEndPoint->slotSet(1600); - EXPECT_EQ(0, m_pLoopStartPoint->get()); - EXPECT_EQ(1600, m_pLoopEndPoint->get()); - m_pButtonLoopDouble->slotSet(1); - m_pButtonLoopDouble->slotSet(0); - EXPECT_EQ(0, m_pLoopStartPoint->get()); - EXPECT_EQ(1600, m_pLoopEndPoint->get()); -} - -TEST_F(LoopingControlTest, LoopHalveButton_HalvesLoop) { +TEST_F(LoopingControlTest, LoopScale_HalvesLoop) { m_pLoopStartPoint->slotSet(0); m_pLoopEndPoint->slotSet(2000); seekToSampleAndProcess(1800); @@ -281,8 +361,7 @@ TEST_F(LoopingControlTest, LoopHalveButton_HalvesLoop) { EXPECT_EQ(2000, m_pLoopEndPoint->get()); EXPECT_EQ(1800, m_pChannel1->getEngineBuffer()->m_pLoopingControl->getCurrentSample()); EXPECT_FALSE(isLoopEnabled()); - m_pButtonLoopHalve->slotSet(1); - m_pButtonLoopHalve->slotSet(0); + m_pLoopScale->set(0.5); ProcessBuffer(); EXPECT_EQ(0, m_pLoopStartPoint->get()); EXPECT_EQ(1000, m_pLoopEndPoint->get()); @@ -291,10 +370,9 @@ TEST_F(LoopingControlTest, LoopHalveButton_HalvesLoop) { // even though it is outside the loop. EXPECT_EQ(1800, m_pChannel1->getEngineBuffer()->m_pLoopingControl->getCurrentSample()); - m_pButtonReloopExit->slotSet(1); + m_pButtonReloopToggle->slotSet(1); EXPECT_TRUE(isLoopEnabled()); - m_pButtonLoopHalve->slotSet(1); - m_pButtonLoopHalve->slotSet(0); + m_pLoopScale->set(0.5); ProcessBuffer(); EXPECT_EQ(0, m_pLoopStartPoint->get()); EXPECT_EQ(500, m_pLoopEndPoint->get()); @@ -303,6 +381,57 @@ TEST_F(LoopingControlTest, LoopHalveButton_HalvesLoop) { EXPECT_EQ(300, m_pChannel1->getEngineBuffer()->m_pLoopingControl->getCurrentSample()); } +TEST_F(LoopingControlTest, LoopDoubleButton_IgnoresPastTrackEnd) { + seekToSampleAndProcess(50); + m_pLoopStartPoint->slotSet(kTrackLengthSamples / 2.0); + m_pLoopEndPoint->slotSet(kTrackLengthSamples); + EXPECT_EQ(kTrackLengthSamples / 2.0, m_pLoopStartPoint->get()); + EXPECT_EQ(kTrackLengthSamples, m_pLoopEndPoint->get()); + m_pButtonLoopDouble->slotSet(1); + m_pButtonLoopDouble->slotSet(0); + EXPECT_EQ(kTrackLengthSamples / 2.0, m_pLoopStartPoint->get()); + EXPECT_EQ(kTrackLengthSamples, m_pLoopEndPoint->get()); +} + +TEST_F(LoopingControlTest, LoopDoubleButton_DoublesBeatloopSize) { + m_pTrack1->setBpm(120.0); + m_pBeatLoopSize->set(16.0); + m_pButtonBeatLoopActivate->set(1.0); + m_pButtonBeatLoopActivate->set(0.0); + m_pButtonLoopDouble->set(1.0); + m_pButtonLoopDouble->set(0.0); + EXPECT_EQ(32.0, m_pBeatLoopSize->get()); +} + +TEST_F(LoopingControlTest, LoopDoubleButton_DoesNotResizeManualLoop) { + seekToSampleAndProcess(500); + m_pButtonLoopIn->set(1.0); + m_pButtonLoopIn->set(0.0); + seekToSampleAndProcess(1000); + m_pButtonLoopOut->set(1.0); + m_pButtonLoopOut->set(0.0); + EXPECT_EQ(500, m_pLoopStartPoint->get()); + EXPECT_EQ(1000, m_pLoopEndPoint->get()); + m_pButtonLoopDouble->slotSet(1); + m_pButtonLoopDouble->slotSet(0); + EXPECT_EQ(500, m_pLoopStartPoint->get()); + EXPECT_EQ(1000, m_pLoopEndPoint->get()); +} + +TEST_F(LoopingControlTest, LoopDoubleButton_UpdatesNumberedBeatloopActivationControls) { + m_pTrack1->setBpm(120.0); + m_pBeatLoopSize->set(2.0); + m_pButtonBeatLoopActivate->set(1.0); + m_pButtonBeatLoopActivate->set(0.0); + EXPECT_TRUE(m_pBeatLoop2Enabled->toBool()); + EXPECT_FALSE(m_pBeatLoop4Enabled->toBool()); + + m_pButtonLoopDouble->set(1.0); + m_pButtonLoopDouble->set(0.0); + EXPECT_FALSE(m_pBeatLoop2Enabled->toBool()); + EXPECT_TRUE(m_pBeatLoop4Enabled->toBool()); +} + TEST_F(LoopingControlTest, LoopHalveButton_IgnoresTooSmall) { ProcessBuffer(); m_pLoopStartPoint->slotSet(0); @@ -315,13 +444,51 @@ TEST_F(LoopingControlTest, LoopHalveButton_IgnoresTooSmall) { EXPECT_EQ(40, m_pLoopEndPoint->get()); } +TEST_F(LoopingControlTest, LoopHalveButton_HalvesBeatloopSize) { + m_pTrack1->setBpm(120.0); + m_pBeatLoopSize->set(64.0); + m_pButtonBeatLoopActivate->set(1.0); + m_pButtonBeatLoopActivate->set(0.0); + m_pButtonLoopHalve->slotSet(1); + m_pButtonLoopHalve->slotSet(0); + EXPECT_EQ(32.0, m_pBeatLoopSize->get()); +} + +TEST_F(LoopingControlTest, LoopHalveButton_DoesNotResizeManualLoop) { + seekToSampleAndProcess(500); + m_pButtonLoopIn->set(1.0); + m_pButtonLoopIn->set(0.0); + seekToSampleAndProcess(1000); + m_pButtonLoopOut->set(1.0); + m_pButtonLoopOut->set(0.0); + EXPECT_EQ(500, m_pLoopStartPoint->get()); + EXPECT_EQ(1000, m_pLoopEndPoint->get()); + m_pButtonLoopHalve->slotSet(1); + m_pButtonLoopHalve->slotSet(0); + EXPECT_EQ(500, m_pLoopStartPoint->get()); + EXPECT_EQ(1000, m_pLoopEndPoint->get()); +} + +TEST_F(LoopingControlTest, LoopHalveButton_UpdatesNumberedBeatloopActivationControls) { + m_pTrack1->setBpm(120.0); + m_pBeatLoopSize->set(4.0); + m_pButtonBeatLoopActivate->set(1.0); + m_pButtonBeatLoopActivate->set(0.0); + EXPECT_FALSE(m_pBeatLoop2Enabled->toBool()); + EXPECT_TRUE(m_pBeatLoop4Enabled->toBool()); + + m_pButtonLoopHalve->set(1.0); + m_pButtonLoopHalve->set(0.0); + EXPECT_TRUE(m_pBeatLoop2Enabled->toBool()); + EXPECT_FALSE(m_pBeatLoop4Enabled->toBool()); +} + TEST_F(LoopingControlTest, LoopMoveTest) { - // Set a crazy bpm so our super-short track of 1000 samples has a couple beats in it. - m_pTrack1->setBpm(23520); + m_pTrack1->setBpm(120); m_pLoopStartPoint->slotSet(0); m_pLoopEndPoint->slotSet(300); seekToSampleAndProcess(10); - m_pButtonReloopExit->slotSet(1); + m_pButtonReloopToggle->slotSet(1); EXPECT_TRUE(isLoopEnabled()); EXPECT_EQ(0, m_pLoopStartPoint->get()); EXPECT_EQ(300, m_pLoopEndPoint->get()); @@ -331,12 +498,13 @@ TEST_F(LoopingControlTest, LoopMoveTest) { m_pButtonBeatMoveForward->set(1.0); m_pButtonBeatMoveForward->set(0.0); ProcessBuffer(); - EXPECT_EQ(224, m_pLoopStartPoint->get()); - EXPECT_EQ(524, m_pLoopEndPoint->get()); - EXPECT_EQ(310, m_pChannel1->getEngineBuffer()->m_pLoopingControl->getCurrentSample()); + EXPECT_EQ(44100, m_pLoopStartPoint->get()); + EXPECT_EQ(44400, m_pLoopEndPoint->get()); + // Should seek to the corresponding offset within the moved loop + EXPECT_EQ(44110, m_pChannel1->getEngineBuffer()->m_pLoopingControl->getCurrentSample()); - // Move backward so that the current position is off the end of the loop. - m_pChannel1->getEngineBuffer()->queueNewPlaypos(500, EngineBuffer::SEEK_STANDARD); + // Move backward so that the current position is outside the new location of the loop + m_pChannel1->getEngineBuffer()->queueNewPlaypos(44300, EngineBuffer::SEEK_STANDARD); ProcessBuffer(); m_pButtonBeatMoveBackward->set(1.0); m_pButtonBeatMoveBackward->set(0.0); @@ -347,18 +515,19 @@ TEST_F(LoopingControlTest, LoopMoveTest) { // Now repeat the test with looping disabled (should not affect the // playhead). - m_pButtonReloopExit->slotSet(1); + m_pButtonReloopToggle->slotSet(1); EXPECT_FALSE(isLoopEnabled()); // Move the loop out from under the playposition. m_pButtonBeatMoveForward->set(1.0); m_pButtonBeatMoveForward->set(0.0); ProcessBuffer(); - EXPECT_EQ(224, m_pLoopStartPoint->get()); - EXPECT_EQ(524, m_pLoopEndPoint->get()); + EXPECT_EQ(44100, m_pLoopStartPoint->get()); + EXPECT_EQ(44400, m_pLoopEndPoint->get()); + // Should not seek inside the moved loop when the loop is disabled EXPECT_EQ(200, m_pChannel1->getEngineBuffer()->m_pLoopingControl->getCurrentSample()); - // Move backward so that the current position is off the end of the loop. + // Move backward so that the current position is outside the new location of the loop m_pChannel1->getEngineBuffer()->queueNewPlaypos(500, EngineBuffer::SEEK_STANDARD); ProcessBuffer(); m_pButtonBeatMoveBackward->set(1.0); @@ -381,7 +550,7 @@ TEST_F(LoopingControlTest, LoopResizeSeek) { m_pLoopStartPoint->slotSet(0); m_pLoopEndPoint->slotSet(600); seekToSampleAndProcess(500); - m_pButtonReloopExit->slotSet(1); + m_pButtonReloopToggle->slotSet(1); EXPECT_TRUE(isLoopEnabled()); EXPECT_EQ(0, m_pLoopStartPoint->get()); EXPECT_EQ(600, m_pLoopEndPoint->get()); @@ -402,7 +571,7 @@ TEST_F(LoopingControlTest, LoopResizeSeek) { m_pLoopStartPoint->slotSet(0); m_pLoopEndPoint->slotSet(600); seekToSampleAndProcess(500); - m_pButtonReloopExit->slotSet(1); + m_pButtonReloopToggle->slotSet(1); EXPECT_FALSE(isLoopEnabled()); EXPECT_EQ(0, m_pLoopStartPoint->get()); EXPECT_EQ(600, m_pLoopEndPoint->get()); @@ -415,3 +584,237 @@ TEST_F(LoopingControlTest, LoopResizeSeek) { EXPECT_EQ(950, m_pLoopEndPoint->get()); EXPECT_EQ(500, m_pChannel1->getEngineBuffer()->m_pLoopingControl->getCurrentSample()); } + +TEST_F(LoopingControlTest, BeatLoopSize_SetAndToggle) { + m_pTrack1->setBpm(120.0); + // Setting beatloop_size should not activate a loop + m_pBeatLoopSize->set(2.0); + EXPECT_FALSE(m_pLoopEnabled->toBool()); + + m_pButtonBeatLoopActivate->set(1.0); + m_pButtonBeatLoopActivate->set(0.0); + EXPECT_TRUE(m_pLoopEnabled->toBool()); + EXPECT_TRUE(m_pBeatLoop2Enabled->toBool()); + + m_pButtonBeatLoopActivate->set(1.0); + m_pButtonBeatLoopActivate->set(0.0); + EXPECT_TRUE(m_pLoopEnabled->toBool()); + EXPECT_TRUE(m_pBeatLoop2Enabled->toBool()); +} + +TEST_F(LoopingControlTest, BeatLoopSize_SetWithoutTrackLoaded) { + // Eject the track that is automatically loaded by the testing framework + m_pChannel1->getEngineBuffer()->slotEjectTrack(1.0); + m_pBeatLoopSize->set(5.0); + EXPECT_EQ(5.0, m_pBeatLoopSize->get()); +} + +TEST_F(LoopingControlTest, BeatLoopSize_IgnoresPastTrackEnd) { + // TODO: actually calculate that the beatloop would go beyond + // the end of the track + m_pTrack1->setBpm(60.0); + seekToSampleAndProcess(m_pTrackSamples->get() - 400); + m_pBeatLoopSize->set(64.0); + EXPECT_NE(64.0, m_pBeatLoopSize->get()); + EXPECT_FALSE(m_pBeatLoop64Enabled->toBool()); +} + +TEST_F(LoopingControlTest, BeatLoopSize_SetsNumberedControls) { + m_pTrack1->setBpm(120.0); + m_pBeatLoopSize->set(2.0); + m_pButtonBeatLoopActivate->set(1.0); + m_pButtonBeatLoopActivate->set(0.0); + EXPECT_TRUE(m_pBeatLoop2Enabled->toBool()); + EXPECT_FALSE(m_pBeatLoop4Enabled->toBool()); + + m_pBeatLoopSize->set(4.0); + EXPECT_FALSE(m_pBeatLoop2Enabled->toBool()); + EXPECT_TRUE(m_pBeatLoop4Enabled->toBool()); + EXPECT_TRUE(m_pLoopEnabled->toBool()); +} + +TEST_F(LoopingControlTest, BeatLoopSize_IsSetByNumberedControl) { + m_pTrack1->setBpm(120.0); + m_pBeatLoopSize->set(4.0); + m_pButtonBeatLoop2Activate->set(1.0); + m_pButtonBeatLoop2Activate->set(0.0); + EXPECT_TRUE(m_pBeatLoop2Enabled->toBool()); + EXPECT_TRUE(m_pLoopEnabled->toBool()); + EXPECT_EQ(2.0, m_pBeatLoopSize->get()); + + m_pButtonBeatLoopActivate->set(1.0); + m_pButtonBeatLoopActivate->set(0.0); + EXPECT_TRUE(m_pBeatLoop2Enabled->toBool()); + EXPECT_TRUE(m_pLoopEnabled->toBool()); + EXPECT_EQ(2.0, m_pBeatLoopSize->get()); +} + +TEST_F(LoopingControlTest, BeatLoopSize_SetDoesNotStartLoop) { + m_pTrack1->setBpm(120.0); + m_pBeatLoopSize->set(16.0); + EXPECT_FALSE(m_pLoopEnabled->toBool()); +} + +TEST_F(LoopingControlTest, BeatLoopSize_ResizeKeepsStartPosition) { + seekToSampleAndProcess(50); + m_pTrack1->setBpm(160.0); + m_pBeatLoopSize->set(2.0); + m_pButtonBeatLoopActivate->set(1.0); + m_pButtonBeatLoopActivate->set(0.0); + double oldStart = m_pLoopStartPoint->get(); + + ProcessBuffer(); + + m_pBeatLoopSize->set(1.0); + EXPECT_TRUE(m_pLoopEnabled->toBool()); + double newStart = m_pLoopStartPoint->get(); + EXPECT_TRUE(oldStart == newStart); +} + +TEST_F(LoopingControlTest, BeatLoopSize_ValueChangeDoesNotActivateLoop) { + seekToSampleAndProcess(50); + m_pTrack1->setBpm(160.0); + m_pBeatLoopSize->set(2.0); + m_pButtonBeatLoopActivate->set(1.0); + m_pButtonBeatLoopActivate->set(0.0); + EXPECT_TRUE(m_pLoopEnabled->toBool()); + + m_pButtonReloopToggle->set(1.0); + m_pButtonReloopToggle->set(0.0); + EXPECT_FALSE(m_pLoopEnabled->toBool()); + m_pBeatLoopSize->set(4.0); + EXPECT_FALSE(m_pLoopEnabled->toBool()); + EXPECT_FALSE(m_pBeatLoop4Enabled->toBool()); +} + +TEST_F(LoopingControlTest, BeatLoopSize_ValueChangeResizesBeatLoop) { + seekToSampleAndProcess(50); + m_pTrack1->setBpm(160.0); + m_pBeatLoopSize->set(2.0); + m_pButtonBeatLoopActivate->set(1.0); + m_pButtonBeatLoopActivate->set(0.0); + EXPECT_TRUE(m_pLoopEnabled->toBool()); + double oldLoopStart = m_pLoopStartPoint->get(); + double oldLoopEnd = m_pLoopEndPoint->get(); + double oldLoopLength = oldLoopEnd - oldLoopStart; + + m_pButtonReloopToggle->set(1.0); + m_pButtonReloopToggle->set(0.0); + EXPECT_FALSE(m_pLoopEnabled->toBool()); + m_pBeatLoopSize->set(4.0); + + double newLoopStart = m_pLoopStartPoint->get(); + double newLoopEnd = m_pLoopEndPoint->get(); + double newLoopLength = newLoopEnd - newLoopStart; + EXPECT_EQ(oldLoopStart, newLoopStart); + EXPECT_NE(oldLoopEnd, newLoopEnd); + EXPECT_EQ(oldLoopLength * 2, newLoopLength); +} + +TEST_F(LoopingControlTest, BeatLoopSize_ValueChangeDoesNotResizeManualLoop) { + seekToSampleAndProcess(50); + m_pTrack1->setBpm(160.0); + m_pQuantizeEnabled->set(0); + m_pBeatLoopSize->set(4.0); + m_pButtonLoopIn->slotSet(1); + m_pButtonLoopIn->slotSet(0); + seekToSampleAndProcess(500); + m_pButtonLoopOut->slotSet(1); + m_pButtonLoopOut->slotSet(0); + double oldLoopStart = m_pLoopStartPoint->get(); + double oldLoopEnd = m_pLoopEndPoint->get(); + + m_pBeatLoopSize->set(8.0); + double newLoopStart = m_pLoopStartPoint->get(); + double newLoopEnd = m_pLoopEndPoint->get(); + EXPECT_EQ(oldLoopStart, newLoopStart); + EXPECT_EQ(oldLoopEnd, newLoopEnd); +} + +TEST_F(LoopingControlTest, LegacyBeatLoopControl) { + m_pTrack1->setBpm(120.0); + m_pBeatLoop->set(2.0); + EXPECT_TRUE(m_pBeatLoop2Enabled->toBool()); + EXPECT_TRUE(m_pLoopEnabled->toBool()); + EXPECT_EQ(2.0, m_pBeatLoopSize->get()); + + m_pButtonReloopToggle->set(1.0); + m_pButtonReloopToggle->set(0.0); + EXPECT_FALSE(m_pBeatLoop2Enabled->toBool()); + EXPECT_FALSE(m_pLoopEnabled->toBool()); + EXPECT_EQ(2.0, m_pBeatLoopSize->get()); + + ProcessBuffer(); + + m_pBeatLoop->set(6.0); + EXPECT_TRUE(m_pLoopEnabled->toBool()); + EXPECT_EQ(6.0, m_pBeatLoopSize->get()); +} + +TEST_F(LoopingControlTest, Beatjump_JumpsByBeats) { + m_pTrack1->setBpm(120.0); + double beatLength = m_pNextBeat->get(); + EXPECT_NE(0, beatLength); + + m_pBeatJumpSize->set(4.0); + m_pButtonBeatJumpForward->set(1.0); + m_pButtonBeatJumpForward->set(0.0); + ProcessBuffer(); + EXPECT_EQ(beatLength * 4, m_pChannel1->getEngineBuffer()->m_pLoopingControl->getCurrentSample()); + m_pButtonBeatJumpBackward->set(1.0); + m_pButtonBeatJumpBackward->set(0.0); + ProcessBuffer(); + EXPECT_EQ(0, m_pChannel1->getEngineBuffer()->m_pLoopingControl->getCurrentSample()); +} + +TEST_F(LoopingControlTest, Beatjump_MovesActiveLoop) { + m_pTrack1->setBpm(120.0); + m_pBeatLoopSize->set(4.0); + m_pButtonBeatLoopActivate->set(1.0); + m_pButtonBeatLoopActivate->set(0.0); + double beatLength = m_pNextBeat->get(); + EXPECT_EQ(0, m_pLoopStartPoint->get()); + EXPECT_EQ(beatLength * 4, m_pLoopEndPoint->get()); + + m_pBeatJumpSize->set(1.0); + m_pButtonBeatJumpForward->set(1.0); + m_pButtonBeatJumpForward->set(0.0); + EXPECT_EQ(beatLength, m_pLoopStartPoint->get()); + EXPECT_EQ(beatLength * 5, m_pLoopEndPoint->get()); + + m_pButtonBeatJumpBackward->set(1.0); + m_pButtonBeatJumpBackward->set(0.0); + EXPECT_EQ(0, m_pLoopStartPoint->get()); + EXPECT_EQ(beatLength * 4, m_pLoopEndPoint->get()); +} + +TEST_F(LoopingControlTest, Beatjump_MovesLoopBoundaries) { + // Holding down the loop in/out buttons and using beatjump should + // move only the loop in/out point, but not shift the entire loop forward/backward + m_pTrack1->setBpm(120.0); + m_pBeatLoopSize->set(4.0); + m_pButtonBeatLoopActivate->set(1.0); + m_pButtonBeatLoopActivate->set(0.0); + double beatLength = m_pNextBeat->get(); + EXPECT_EQ(0, m_pLoopStartPoint->get()); + EXPECT_EQ(beatLength * 4, m_pLoopEndPoint->get()); + + m_pButtonLoopIn->set(1.0); + m_pBeatJumpSize->set(1.0); + m_pButtonBeatJumpForward->set(1.0); + m_pButtonBeatJumpForward->set(0.0); + ProcessBuffer(); + m_pButtonLoopIn->set(0.0); + ProcessBuffer(); + EXPECT_EQ(beatLength, m_pLoopStartPoint->get()); + EXPECT_EQ(beatLength * 4, m_pLoopEndPoint->get()); + + m_pButtonLoopOut->set(1.0); + m_pButtonBeatJumpForward->set(1.0); + m_pButtonBeatJumpForward->set(0.0); + ProcessBuffer(); + m_pButtonLoopOut->set(0.0); + ProcessBuffer(); + EXPECT_EQ(beatLength, m_pLoopStartPoint->get()); + EXPECT_EQ(beatLength * 2, m_pLoopEndPoint->get()); +} diff --git a/src/track/beats.h b/src/track/beats.h index 32d167d23556..112dd9861946 100644 --- a/src/track/beats.h +++ b/src/track/beats.h @@ -104,6 +104,46 @@ class Beats { // then dSamples is returned. If no beat can be found, returns -1. virtual double findNthBeat(double dSamples, int n) const = 0; + inline int numBeatsInRange(double dStartSample, double dEndSample) { + double dLastCountedBeat = 0.0; + int iBeatsCounter; + for (iBeatsCounter = 1; dLastCountedBeat < dEndSample; iBeatsCounter++) { + dLastCountedBeat = findNthBeat(dStartSample, iBeatsCounter); + if (dLastCountedBeat == -1) { + break; + } + } + return iBeatsCounter - 2; + }; + + // Find the sample N beats away from dSample. The number of beats may be + // negative and does not need to be an integer. + inline double findNBeatsFromSample(double dSample, double beats) const { + double endSample = dSample; + double thisBeat = 0.0, nthBeat = 0.0; + int fullBeats = static_cast(beats); + // Add the length between this beat and the fullbeats'th beat + // to the end position + if (beats > 0) { + thisBeat = findNthBeat(dSample, 1); + nthBeat = findNthBeat(dSample, fullBeats + 1); + } else if (beats < 0) { + thisBeat = findNthBeat(dSample, -1); + nthBeat = findNthBeat(dSample, fullBeats - 1); + } + endSample += nthBeat - thisBeat; + + // Add the fraction of the beat + double fractionBeats = beats - static_cast(fullBeats); + if (fractionBeats != 0) { + double endNextBeat, endPrevBeat; + findPrevNextBeats(endSample, &endPrevBeat, &endNextBeat); + endSample += (endNextBeat - endPrevBeat) * fractionBeats; + } + + return endSample; + }; + // Adds to pBeatsList the position in samples of every beat occuring between // startPosition and endPosition. BeatIterator must be iterated while // holding a strong references to the Beats object to ensure that the Beats diff --git a/src/widget/wbeatspinbox.cpp b/src/widget/wbeatspinbox.cpp new file mode 100644 index 000000000000..762fbe995615 --- /dev/null +++ b/src/widget/wbeatspinbox.cpp @@ -0,0 +1,282 @@ +#include + +#include "widget/wbeatspinbox.h" + +#include "control/controlobject.h" +#include "control/controlproxy.h" +#include "util/math.h" + +QRegExp WBeatSpinBox::s_regexpBlacklist("[^0-9.,/ ]"); + +WBeatSpinBox::WBeatSpinBox(QWidget * parent, ControlObject* pValueControl, + int decimals, double minimum, double maximum) + : WBaseWidget(parent), + m_valueControl( + pValueControl ? + pValueControl->getKey() : ConfigKey(), this + ), + m_scaleFactor(1.0) { + setDecimals(decimals); + setMinimum(minimum); + setMaximum(maximum); + setKeyboardTracking(false); + // Prevent this widget from getting focused with tab + // to avoid interfering with using the library via keyboard. + setFocusPolicy(Qt::ClickFocus); + + setValue(m_valueControl.get()); + connect(this, SIGNAL(valueChanged(double)), + this, SLOT(slotSpinboxValueChanged(double))); + m_valueControl.connectValueChanged(SLOT(slotControlValueChanged(double))); +} + +void WBeatSpinBox::setup(const QDomNode& node, const SkinContext& context) { + Q_UNUSED(node); + m_scaleFactor = context.getScaleFactor(); +} + +void WBeatSpinBox::stepBy(int steps) { + double oldValue = m_valueControl.get(); + double newValue; + QString temp = text(); + int cursorPos = lineEdit()->cursorPosition(); + if (validate(temp, cursorPos) == QValidator::Acceptable) { + double editValue = valueFromText(temp); + newValue = editValue * pow(2, steps); + if (newValue < minimum() || newValue > maximum()) { + // don't clamp the value here to not fall out of a measure + newValue = editValue; + } + } else { + // here we have an unacceptable edit, going back to the old value first + newValue = oldValue; + } + // Do not call QDoubleSpinBox::setValue directly in case + // the new value of the ControlObject needs to be confirmed. + // Curiously, m_valueControl.set() does not cause slotControlValueChanged + // to execute for beatjump_size, so call QDoubleSpinBox::setValue in this function. + m_valueControl.set(newValue); + double coValue = m_valueControl.get(); + if (coValue != value()) { + setValue(coValue); + } + selectAll(); +} + +void WBeatSpinBox::slotSpinboxValueChanged(double newValue) { + // Do not call QDoubleSpinBox::setValue directly in case + // the new value of the ControlObject needs to be confirmed. + m_valueControl.set(newValue); +} + +void WBeatSpinBox::slotControlValueChanged(double newValue) { + if (value() != newValue) { + setValue(newValue); + } +} + +QString WBeatSpinBox::fractionString(int numerator, int denominator) const { + return QString("%1/%2").arg(numerator).arg(denominator); +} + +QString WBeatSpinBox::textFromValue(double value) const { + double dWholePart, dFracPart; + dFracPart = modf(value, &dWholePart); + + QString sFracPart; + if (dFracPart == 0.5) { + sFracPart = fractionString(1, 2); + } else if (dFracPart == 0.25) { + sFracPart = fractionString(1, 4); + } else if (dFracPart == 0.75) { + sFracPart = fractionString(3, 4); + } else if (dFracPart == 0.33333) { + sFracPart = fractionString(1, 3); + } else if (dFracPart == 0.66667) { + sFracPart = fractionString(2, 3); + } else if (dFracPart == 0.125) { + sFracPart = fractionString(1, 8); + } else if (dFracPart == 0.375) { + sFracPart = fractionString(3, 8); + } else if (dFracPart == 0.625) { + sFracPart = fractionString(5, 8); + } else if (dFracPart == 0.875) { + sFracPart = fractionString(7, 8); + } else if (dFracPart == 0.0625) { + sFracPart = fractionString(1, 16); + } else if (dFracPart == 0.1875) { + sFracPart = fractionString(3, 16); + } else if (dFracPart == 0.3125) { + sFracPart = fractionString(5, 16); + } else if (dFracPart == 0.4375) { + sFracPart = fractionString(7, 16); + } else if (dFracPart == 0.5625) { + sFracPart = fractionString(9, 16); + } else if (dFracPart == 0.6875) { + sFracPart = fractionString(11, 16); + } else if (dFracPart == 0.8125) { + sFracPart = fractionString(13, 16); + } else if (dFracPart == 0.9375) { + sFracPart = fractionString(15, 16); + } else if (dFracPart == 0.03125) { + sFracPart = fractionString(1, 32); + } else if (dFracPart == 0.09375) { + sFracPart = fractionString(3, 32); + } else if (dFracPart == 0.15625) { + sFracPart = fractionString(5, 32); + } else if (dFracPart == 0.21875) { + sFracPart = fractionString(7, 32); + } else if (dFracPart == 0.28125) { + sFracPart = fractionString(9, 32); + } else if (dFracPart == 0.34375) { + sFracPart = fractionString(11, 32); + } else if (dFracPart == 0.40625) { + sFracPart = fractionString(13, 32); + } else if (dFracPart == 0.46875) { + sFracPart = fractionString(15, 32); + } else if (dFracPart == 0.53125) { + sFracPart = fractionString(17, 32); + } else if (dFracPart == 0.59375) { + sFracPart = fractionString(19, 32); + } else if (dFracPart == 0.65625) { + sFracPart = fractionString(21, 32); + } else if (dFracPart == 0.71875) { + sFracPart = fractionString(23, 32); + } else if (dFracPart == 0.78125) { + sFracPart = fractionString(25, 32); + } else if (dFracPart == 0.84375) { + sFracPart = fractionString(27, 32); + } else if (dFracPart == 0.90625) { + sFracPart = fractionString(29, 32); + } else if (dFracPart == 0.96875) { + sFracPart = fractionString(31, 32); + } else { + return locale().toString(value, 'g', 5); + } + + if (dWholePart > 0) { + return locale().toString(dWholePart, 'f', 0) + " " + sFracPart; + } + return sFracPart; +} + +double WBeatSpinBox::valueFromText(const QString& text) const { + if (text.count(" ") > 1 || text.count("/") > 1) { + return value(); + } + + bool conversionWorked = false; + double dValue; + dValue = locale().toDouble(text, &conversionWorked); + if (conversionWorked) { + return dValue; + } + + QString sIntPart, sFracPart, sNumerator, sDenominator; + + if (text.contains(" ")) { + QStringList numberParts = text.split(" "); + sIntPart = numberParts.at(0); + sFracPart = numberParts.at(1); + } else if (text.contains("/")) { + sFracPart = text; + } + + QStringList splitFraction = sFracPart.split("/"); + sNumerator = splitFraction.at(0); + sDenominator = splitFraction.at(1); + + return locale().toDouble(sIntPart) + + locale().toDouble(sNumerator) / locale().toDouble(sDenominator); +} + +QValidator::State WBeatSpinBox::validate(QString& input, int& pos) const { + Q_UNUSED(pos); + if (input.contains(s_regexpBlacklist)) { + return QValidator::Invalid; + } + if (input.isEmpty()) { + return QValidator::Intermediate; + } + + if (input.count(" ") > 1 || input.count("/") > 1) { + return QValidator::Invalid; + } + + bool conversionWorked = false; + locale().toDouble(input, &conversionWorked); + if (conversionWorked) { + return QValidator::Acceptable; + } + + QString sIntPart, sFracPart, sNumerator, sDenominator; + + if (input.contains(" ")) { + // Whole number + fraction, for example "1 1/2" + QStringList numberParts = input.split(" "); + sIntPart = numberParts.at(0); + sFracPart = numberParts.at(1); + // Whole number + trailing space + if (sFracPart.isEmpty()) { + return QValidator::Intermediate; + } + } else if (input.contains("/")) { + // Fraction without whole number, for example "1/2" + sFracPart = input; + } + + if (!sIntPart.isEmpty()) { + conversionWorked = false; + sIntPart.toInt(&conversionWorked); + if (!conversionWorked) { + return QValidator::Invalid; + } + } + + if (!sFracPart.contains("/")) { + return QValidator::Intermediate; + } + QStringList fracParts = sFracPart.split("/"); + sNumerator = fracParts.at(0); + if (sNumerator.isEmpty()) { + // when deleting the numerator, for example "/32" + return QValidator::Intermediate; + } + sDenominator = fracParts.at(1); + + conversionWorked = false; + sNumerator.toInt(&conversionWorked); + if (!conversionWorked) { + return QValidator::Invalid; + } + + if (sDenominator.isEmpty()) { + return QValidator::Intermediate; + } + conversionWorked = false; + sDenominator.toInt(&conversionWorked); + if (!conversionWorked) { + return QValidator::Invalid; + } + + return QValidator::Acceptable; +} + +bool WBeatSpinBox::event(QEvent* pEvent) { + if (pEvent->type() == QEvent::ToolTip) { + updateTooltip(); + } else if (pEvent->type() == QEvent::FontChange) { + const QFont& fonti = font(); + qDebug() << "WBeatSpinBox::event QEvent::FontChange" << fonti.pixelSize() * m_scaleFactor; + // Change the new font on the fly by casting away its constancy + // using setFont() here, would results into a recursive loop + // resetting the font to the original css values. + // Only scale pixel size fonts, point size fonts are scaled by the OS + if (fonti.pixelSize() > 0) { + const_cast(fonti).setPixelSize(fonti.pixelSize() * m_scaleFactor); + } + // TODO(Be): Figure out how to apply newly scaled font??? + } + + return QDoubleSpinBox::event(pEvent); +} diff --git a/src/widget/wbeatspinbox.h b/src/widget/wbeatspinbox.h new file mode 100644 index 000000000000..dcd4c9a15de1 --- /dev/null +++ b/src/widget/wbeatspinbox.h @@ -0,0 +1,42 @@ +#ifndef WBEATSPINBOX_H +#define WBEATSPINBOX_H + +#include "control/controlobject.h" +#include "control/controlproxy.h" +#include "widget/wbasewidget.h" +#include "skin/skincontext.h" +#include + +class ControlProxy; + +class WBeatSpinBox : public QDoubleSpinBox, public WBaseWidget { + Q_OBJECT + public: + WBeatSpinBox(QWidget *parent=nullptr, + ControlObject* pValueControl=nullptr, + int decimals=5, + double minimum=0.03125, double maximum=512.00); + + void setup(const QDomNode& node, const SkinContext& context); + + private slots: + void slotSpinboxValueChanged(double newValue); + void slotControlValueChanged(double newValue); + + QString textFromValue(double value) const override; + double valueFromText(const QString& text) const override; + QValidator::State validate(QString& input, int& pos) const override; + + private: + void stepBy(int steps) override; + QString fractionString(int numerator, int denominator) const; + + ControlProxy m_valueControl; + static QRegExp s_regexpBlacklist; + + // for font scaling + bool event(QEvent* pEvent) override; + double m_scaleFactor; +}; + +#endif