summaryrefslogtreecommitdiffstats
path: root/src/engine/controls/loopingcontrol.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/engine/controls/loopingcontrol.cpp')
-rw-r--r--src/engine/controls/loopingcontrol.cpp1356
1 files changed, 1356 insertions, 0 deletions
diff --git a/src/engine/controls/loopingcontrol.cpp b/src/engine/controls/loopingcontrol.cpp
new file mode 100644
index 0000000000..98734b4eb4
--- /dev/null
+++ b/src/engine/controls/loopingcontrol.cpp
@@ -0,0 +1,1356 @@
+// loopingcontrol.cpp
+// Created on Sep 23, 2008
+// Author: asantoni, rryan
+
+#include <QtDebug>
+
+#include "control/controlobject.h"
+#include "preferences/usersettings.h"
+#include "control/controlpushbutton.h"
+#include "engine/controls/loopingcontrol.h"
+#include "engine/controls/bpmcontrol.h"
+#include "engine/controls/enginecontrol.h"
+#include "util/compatibility.h"
+#include "util/math.h"
+#include "util/sample.h"
+
+#include "track/track.h"
+#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, 128, 256, 512 };
+
+// Used to generate the beatloop_%SIZE, beatjump_%SIZE, and loop_move_%SIZE CO
+// ConfigKeys.
+ConfigKey keyForControl(QString group, QString ctrlName, double num) {
+ ConfigKey key;
+ key.group = group;
+ key.item = ctrlName.arg(num);
+ return key;
+}
+
+// static
+QList<double> LoopingControl::getBeatSizes() {
+ QList<double> result;
+ for (unsigned int i = 0; i < (sizeof(s_dBeatSizes) / sizeof(s_dBeatSizes[0])); ++i) {
+ result.append(s_dBeatSizes[i]);
+ }
+ return result;
+}
+
+LoopingControl::LoopingControl(QString group,
+ UserSettingsPointer pConfig)
+ : EngineControl(group, pConfig),
+ m_bLoopingEnabled(false),
+ m_bLoopRollActive(false),
+ m_bAdjustingLoopIn(false),
+ m_bAdjustingLoopOut(false),
+ m_bAdjustingLoopInOld(false),
+ m_bAdjustingLoopOutOld(false),
+ m_bLoopOutPressedWhileLoopDisabled(false) {
+ m_oldLoopSamples = { kNoTrigger, kNoTrigger, false };
+ m_loopSamples.setValue(m_oldLoopSamples);
+ m_currentSample.setValue(0.0);
+ m_pActiveBeatLoop = NULL;
+
+ //Create loop-in, loop-out, loop-exit, and reloop/exit ControlObjects
+ m_pLoopInButton = new ControlPushButton(ConfigKey(group, "loop_in"));
+ connect(m_pLoopInButton, &ControlObject::valueChanged,
+ this, &LoopingControl::slotLoopIn,
+ Qt::DirectConnection);
+ m_pLoopInButton->set(0);
+
+ m_pLoopInGotoButton = new ControlPushButton(ConfigKey(group, "loop_in_goto"));
+ connect(m_pLoopInGotoButton, &ControlObject::valueChanged,
+ this, &LoopingControl::slotLoopInGoto);
+
+ m_pLoopOutButton = new ControlPushButton(ConfigKey(group, "loop_out"));
+ connect(m_pLoopOutButton, &ControlObject::valueChanged,
+ this, &LoopingControl::slotLoopOut,
+ Qt::DirectConnection);
+ m_pLoopOutButton->set(0);
+
+ m_pLoopOutGotoButton = new ControlPushButton(ConfigKey(group, "loop_out_goto"));
+ connect(m_pLoopOutGotoButton, &ControlObject::valueChanged,
+ this, &LoopingControl::slotLoopOutGoto);
+
+
+ m_pLoopExitButton = new ControlPushButton(ConfigKey(group, "loop_exit"));
+ connect(m_pLoopExitButton, &ControlObject::valueChanged,
+ this, &LoopingControl::slotLoopExit,
+ Qt::DirectConnection);
+ m_pLoopExitButton->set(0);
+
+ m_pReloopToggleButton = new ControlPushButton(ConfigKey(group, "reloop_toggle"));
+ connect(m_pReloopToggleButton, &ControlObject::valueChanged,
+ this, &LoopingControl::slotReloopToggle,
+ 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, &ControlObject::valueChanged,
+ this, &LoopingControl::slotReloopAndStop,
+ Qt::DirectConnection);
+
+ m_pCOLoopEnabled = new ControlObject(ConfigKey(group, "loop_enabled"));
+ m_pCOLoopEnabled->set(0.0);
+
+ m_pCOLoopStartPosition =
+ new ControlObject(ConfigKey(group, "loop_start_position"));
+ m_pCOLoopStartPosition->set(kNoTrigger);
+ connect(m_pCOLoopStartPosition, &ControlObject::valueChanged,
+ this, &LoopingControl::slotLoopStartPos,
+ Qt::DirectConnection);
+
+ m_pCOLoopEndPosition =
+ new ControlObject(ConfigKey(group, "loop_end_position"));
+ m_pCOLoopEndPosition->set(kNoTrigger);
+ connect(m_pCOLoopEndPosition, &ControlObject::valueChanged,
+ this, &LoopingControl::slotLoopEndPos,
+ Qt::DirectConnection);
+
+ 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"));
+
+ // 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, &ControlObject::valueChanged, this,
+ [=](double value){slotBeatLoop(value);}, Qt::DirectConnection);
+
+ m_pCOBeatLoopSize = new ControlObject(ConfigKey(group, "beatloop_size"),
+ true, false, false, 4.0);
+ m_pCOBeatLoopSize->connectValueChangeRequest(this,
+ &LoopingControl::slotBeatLoopSizeChangeRequest, Qt::DirectConnection);
+ m_pCOBeatLoopActivate = new ControlPushButton(ConfigKey(group, "beatloop_activate"));
+ connect(m_pCOBeatLoopActivate, &ControlObject::valueChanged,
+ this, &LoopingControl::slotBeatLoopToggle);
+ m_pCOBeatLoopRollActivate = new ControlPushButton(ConfigKey(group, "beatlooproll_activate"));
+ connect(m_pCOBeatLoopRollActivate, &ControlObject::valueChanged,
+ this, &LoopingControl::slotBeatLoopRollActivate);
+
+ // Here we create corresponding beatloop_(SIZE) CO's which all call the same
+ // BeatControl, but with a set value.
+ for (unsigned int i = 0; i < (sizeof(s_dBeatSizes) / sizeof(s_dBeatSizes[0])); ++i) {
+ BeatLoopingControl* pBeatLoop = new BeatLoopingControl(group, s_dBeatSizes[i]);
+ connect(pBeatLoop, &BeatLoopingControl::activateBeatLoop,
+ this, &LoopingControl::slotBeatLoopActivate,
+ Qt::DirectConnection);
+ connect(pBeatLoop, &BeatLoopingControl::activateBeatLoopRoll,
+ this, &LoopingControl::slotBeatLoopActivateRoll,
+ Qt::DirectConnection);
+ connect(pBeatLoop, &BeatLoopingControl::deactivateBeatLoop,
+ this, &LoopingControl::slotBeatLoopDeactivate,
+ Qt::DirectConnection);
+ connect(pBeatLoop, &BeatLoopingControl::deactivateBeatLoopRoll,
+ this, &LoopingControl::slotBeatLoopDeactivateRoll,
+ Qt::DirectConnection);
+ m_beatLoops.append(pBeatLoop);
+ }
+
+ m_pCOBeatJump = new ControlObject(ConfigKey(group, "beatjump"), false);
+ connect(m_pCOBeatJump, &ControlObject::valueChanged,
+ this, &LoopingControl::slotBeatJump, 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, &ControlObject::valueChanged,
+ this, &LoopingControl::slotBeatJumpForward);
+ m_pCOBeatJumpBackward = new ControlPushButton(ConfigKey(group, "beatjump_backward"));
+ connect(m_pCOBeatJumpBackward, &ControlObject::valueChanged,
+ this, &LoopingControl::slotBeatJumpBackward);
+
+ // Create beatjump_(SIZE) CO's which all call beatjump, but with a set
+ // value.
+ for (unsigned int i = 0; i < (sizeof(s_dBeatSizes) / sizeof(s_dBeatSizes[0])); ++i) {
+ BeatJumpControl* pBeatJump = new BeatJumpControl(group, s_dBeatSizes[i]);
+ connect(pBeatJump, &BeatJumpControl::beatJump,
+ this, &LoopingControl::slotBeatJump,
+ Qt::DirectConnection);
+ m_beatJumps.append(pBeatJump);
+ }
+
+ m_pCOLoopMove = new ControlObject(ConfigKey(group, "loop_move"), false);
+ connect(m_pCOLoopMove, &ControlObject::valueChanged,
+ this, &LoopingControl::slotLoopMove, Qt::DirectConnection);
+
+ // Create loop_move_(SIZE) CO's which all call loop_move, but with a set
+ // value.
+ for (unsigned int i = 0; i < (sizeof(s_dBeatSizes) / sizeof(s_dBeatSizes[0])); ++i) {
+ LoopMoveControl* pLoopMove = new LoopMoveControl(group, s_dBeatSizes[i]);
+ connect(pLoopMove, &LoopMoveControl::loopMove,
+ this, &LoopingControl::slotLoopMove,
+ Qt::DirectConnection);
+ m_loopMoves.append(pLoopMove);
+ }
+
+ m_pCOLoopScale = new ControlObject(ConfigKey(group, "loop_scale"), false);
+ connect(m_pCOLoopScale, &ControlObject::valueChanged,
+ this, &LoopingControl::slotLoopScale);
+ m_pLoopHalveButton = new ControlPushButton(ConfigKey(group, "loop_halve"));
+ connect(m_pLoopHalveButton, &ControlObject::valueChanged,
+ this, &LoopingControl::slotLoopHalve);
+ m_pLoopDoubleButton = new ControlPushButton(ConfigKey(group, "loop_double"));
+ connect(m_pLoopDoubleButton, &ControlObject::valueChanged,
+ this, &LoopingControl::slotLoopDouble);
+
+ 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_pReloopToggleButton;
+ delete m_pReloopAndStopButton;
+ delete m_pCOLoopEnabled;
+ delete m_pCOLoopStartPosition;
+ delete m_pCOLoopEndPosition;
+ delete m_pCOLoopScale;
+ delete m_pLoopHalveButton;
+ delete m_pLoopDoubleButton;
+
+ delete m_pCOBeatLoop;
+ while (!m_beatLoops.isEmpty()) {
+ 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;
+ }
+
+ delete m_pCOLoopMove;
+ while (!m_loopMoves.isEmpty()) {
+ LoopMoveControl* pLoopMove = m_loopMoves.takeLast();
+ delete pLoopMove;
+ }
+}
+
+void LoopingControl::slotLoopScale(double scaleFactor) {
+ LoopSamples loopSamples = m_loopSamples.getValue();
+ if (loopSamples.start == kNoTrigger || loopSamples.end == kNoTrigger) {
+ return;
+ }
+ double loop_length = loopSamples.end - loopSamples.start;
+ int trackSamples = m_pTrackSamples->get();
+ loop_length *= scaleFactor;
+
+ // Abandon loops that are too short of extend beyond the end of the file.
+ if (loop_length < MINIMUM_AUDIBLE_LOOP_SIZE ||
+ loopSamples.start + loop_length > trackSamples) {
+ return;
+ }
+
+ loopSamples.end = loopSamples.start + loop_length;
+
+ // TODO(XXX) we could be smarter about taking the active beatloop, scaling
+ // it by the desired amount and trying to find another beatloop that matches
+ // it, but for now we just clear the active beat loop if somebody scales.
+ clearActiveBeatLoop();
+
+ // Don't allow 0 samples loop, so one can still manipulate it
+ if (loopSamples.end == loopSamples.start) {
+ if ((loopSamples.end + 2) >= trackSamples)
+ loopSamples.start -= 2;
+ else
+ loopSamples.end += 2;
+ }
+ // Do not allow loops to go past the end of the song
+ else if (loopSamples.end > trackSamples) {
+ loopSamples.end = trackSamples;
+ }
+
+ // Reseek if the loop shrank out from under the playposition.
+ loopSamples.seek = (m_bLoopingEnabled && scaleFactor < 1.0);
+
+ m_loopSamples.setValue(loopSamples);
+
+ // Update CO for loop end marker
+ m_pCOLoopEndPosition->set(loopSamples.end);
+}
+
+void LoopingControl::slotLoopHalve(double pressed) {
+ if (pressed <= 0.0) {
+ return;
+ }
+
+ slotBeatLoop(m_pCOBeatLoopSize->get() / 2.0, true, false);
+}
+
+void LoopingControl::slotLoopDouble(double pressed) {
+ if (pressed <= 0.0) {
+ return;
+ }
+
+ slotBeatLoop(m_pCOBeatLoopSize->get() * 2.0, true, false);
+}
+
+void LoopingControl::process(const double dRate,
+ const double currentSample,
+ const int iBufferSize) {
+ Q_UNUSED(iBufferSize);
+ Q_UNUSED(dRate);
+
+ double oldCurrentSample = m_currentSample.getValue();
+
+ if (oldCurrentSample != currentSample) {
+ m_currentSample.setValue(currentSample);
+ } else {
+ // no transport, so we have to do scheduled seeks here
+ LoopSamples loopSamples = m_loopSamples.getValue();
+ if (m_bLoopingEnabled &&
+ !m_bAdjustingLoopIn && !m_bAdjustingLoopOut &&
+ loopSamples.start != kNoTrigger &&
+ loopSamples.end != kNoTrigger) {
+
+ if (loopSamples.start != m_oldLoopSamples.start ||
+ loopSamples.end != m_oldLoopSamples.end) {
+ // bool seek is only valid after the loop has changed
+ if (loopSamples.seek) {
+ // here the loop has changed and the play position
+ // should be moved with it
+ double target = seekInsideAdjustedLoop(currentSample,
+ m_oldLoopSamples.start, loopSamples.start, loopSamples.end);
+ if (target != kNoTrigger) {
+ // jump immediately
+ seekAbs(target);
+ }
+ }
+ m_oldLoopSamples = loopSamples;
+ }
+ }
+ }
+
+ if (m_bAdjustingLoopIn) {
+ setLoopInToCurrentPosition();
+ } else if (m_bAdjustingLoopOut) {
+ setLoopOutToCurrentPosition();
+ }
+}
+
+double LoopingControl::nextTrigger(bool reverse,
+ const double currentSample,
+ double *pTarget) {
+ *pTarget = kNoTrigger;
+
+ LoopSamples loopSamples = m_loopSamples.getValue();
+
+ if (m_bAdjustingLoopInOld != m_bAdjustingLoopIn) {
+ m_bAdjustingLoopInOld = m_bAdjustingLoopIn;
+ if (reverse && !m_bAdjustingLoopIn) {
+ m_oldLoopSamples = loopSamples;
+ *pTarget = loopSamples.end;
+ return currentSample;
+ }
+ }
+
+ if (m_bAdjustingLoopOutOld != m_bAdjustingLoopOut) {
+ m_bAdjustingLoopOutOld = m_bAdjustingLoopOut;
+ if (!reverse && !m_bAdjustingLoopOut) {
+ m_oldLoopSamples = loopSamples;
+ *pTarget = loopSamples.start;
+ return currentSample;
+ }
+ }
+
+ if (m_bLoopingEnabled &&
+ !m_bAdjustingLoopIn && !m_bAdjustingLoopOut &&
+ loopSamples.start != kNoTrigger &&
+ loopSamples.end != kNoTrigger) {
+
+ if (loopSamples.start != m_oldLoopSamples.start ||
+ loopSamples.end != m_oldLoopSamples.end) {
+ // bool seek is only valid after the loop has changed
+ if (loopSamples.seek) {
+ // here the loop has changed and the play position
+ // should be moved with it
+ *pTarget = seekInsideAdjustedLoop(currentSample,
+ m_oldLoopSamples.start, loopSamples.start, loopSamples.end);
+ } else {
+ bool movedOut = false;
+ // Check if we have moved out of the loop, before we could enable it
+ if (reverse) {
+ if (loopSamples.start > currentSample) {
+ movedOut = true;
+ }
+ } else {
+ if (loopSamples.end < currentSample) {
+ movedOut = true;
+ }
+ }
+ if (movedOut) {
+ *pTarget = seekInsideAdjustedLoop(currentSample,
+ loopSamples.start, loopSamples.start, loopSamples.end);
+ }
+ }
+ m_oldLoopSamples = loopSamples;
+ if (*pTarget != kNoTrigger) {
+ // jump immediately
+ return currentSample;
+ }
+ }
+
+ if (reverse) {
+ *pTarget = loopSamples.end;
+ return loopSamples.start;
+ } else {
+ *pTarget = loopSamples.start;
+ return loopSamples.end;
+ }
+ }
+ return kNoTrigger;
+}
+
+void LoopingControl::hintReader(HintVector* pHintList) {
+ LoopSamples loopSamples = m_loopSamples.getValue();
+ Hint loop_hint;
+ // If the loop is enabled, then this is high priority because we will loop
+ // sometime potentially very soon! The current audio itself is priority 1,
+ // but we will issue ourselves at priority 2.
+ if (m_bLoopingEnabled) {
+ // If we're looping, hint the loop in and loop out, in case we reverse
+ // into it. We could save information from process to tell which
+ // direction we're going in, but that this is much simpler, and hints
+ // aren't that bad to make anyway.
+ if (loopSamples.start >= 0) {
+ loop_hint.priority = 2;
+ loop_hint.frame = SampleUtil::floorPlayPosToFrame(loopSamples.start);
+ loop_hint.frameCount = Hint::kFrameCountForward;
+ pHintList->append(loop_hint);
+ }
+ if (loopSamples.end >= 0) {
+ loop_hint.priority = 10;
+ loop_hint.frame = SampleUtil::ceilPlayPosToFrame(loopSamples.end);
+ loop_hint.frameCount = Hint::kFrameCountBackward;
+ pHintList->append(loop_hint);
+ }
+ } else {
+ if (loopSamples.start >= 0) {
+ loop_hint.priority = 10;
+ loop_hint.frame = SampleUtil::floorPlayPosToFrame(loopSamples.start);
+ loop_hint.frameCount = Hint::kFrameCountForward;
+ pHintList->append(loop_hint);
+ }
+ }
+}
+
+void LoopingControl::setLoopInToCurrentPosition() {
+ // set loop-in position
+ BeatsPointer pBeats = m_pBeats;
+ LoopSamples loopSamples = m_loopSamples.getValue();
+ double quantizedBeat = -1;
+ double pos = m_currentSample.getValue();
+ if (m_pQuantizeEnabled->toBool() && pBeats) {
+ if (m_bAdjustingLoopIn) {
+ double closestBeat = m_pClosestBeat->get();
+ if (closestBeat == m_currentSample.getValue()) {
+ quantizedBeat = closestBeat;
+ } else {
+ quantizedBeat = m_pPreviousBeat->get();
+ }
+ } else {
+ quantizedBeat = m_pClosestBeat->get();
+ }
+ if (quantizedBeat != -1) {
+ pos = quantizedBeat;
+ }
+ }
+
+ // Reset the loop out position if it is before the loop in so that loops
+ // cannot be inverted.
+ if (loopSamples.end != kNoTrigger &&
+ loopSamples.end < pos) {
+ loopSamples.end = kNoTrigger;
+ m_pCOLoopEndPosition->set(kNoTrigger);
+ }
+
+ // If we're looping and the loop-in and out points are now so close
+ // that the loop would be inaudible, set the in point to the smallest
+ // pre-defined beatloop size instead (when possible)
+ if (loopSamples.end != kNoTrigger &&
+ (loopSamples.end - pos) < MINIMUM_AUDIBLE_LOOP_SIZE) {
+ if (quantizedBeat != -1 && pBeats) {
+ pos = pBeats->findNthBeat(quantizedBeat, -2);
+ if (pos == -1 || (loopSamples.end - pos) < MINIMUM_AUDIBLE_LOOP_SIZE) {
+ pos = loopSamples.end - MINIMUM_AUDIBLE_LOOP_SIZE;
+ }
+ } else {
+ pos = loopSamples.end - MINIMUM_AUDIBLE_LOOP_SIZE;
+ }
+ }
+
+ loopSamples.start = pos;
+
+ m_pCOLoopStartPosition->set(loopSamples.start);
+
+ // start looping
+ if (loopSamples.start != kNoTrigger &&
+ loopSamples.end != kNoTrigger) {
+ setLoopingEnabled(true);
+ loopSamples.seek = true;
+ } else {
+ loopSamples.seek = false;
+ }
+
+ if (m_pQuantizeEnabled->toBool()
+ && loopSamples.start < loopSamples.end
+ && pBeats) {
+ m_pCOBeatLoopSize->setAndConfirm(
+ pBeats->numBeatsInRange(loopSamples.start, loopSamples.end));
+ updateBeatLoopingControls();
+ } else {
+ clearActiveBeatLoop();
+ }
+
+ m_loopSamples.setValue(loopSamples);
+ //qDebug() << "set loop_in to " << loopSamples.start;
+}
+
+void LoopingControl::slotLoopIn(double pressed) {
+ if (!m_pTrack) {
+ 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<double>(
+ m_loopSamples.getValue().start));
+ }
+}
+
+void LoopingControl::setLoopOutToCurrentPosition() {
+ BeatsPointer pBeats = m_pBeats;
+ LoopSamples loopSamples = m_loopSamples.getValue();
+ double quantizedBeat = -1;
+ int pos = m_currentSample.getValue();
+ if (m_pQuantizeEnabled->toBool() && pBeats) {
+ if (m_bAdjustingLoopOut) {
+ double closestBeat = m_pClosestBeat->get();
+ if (closestBeat == m_currentSample.getValue()) {
+ quantizedBeat = closestBeat;
+ } else {
+ quantizedBeat = m_pNextBeat->get();
+ }
+ } else {
+ quantizedBeat = m_pClosestBeat->get();
+ }
+ if (quantizedBeat != -1) {
+ pos = quantizedBeat;
+ }
+ }
+
+ // If the user is trying to set a loop-out before the loop in or without
+ // having a loop-in, then ignore it.
+ if (loopSamples.start == kNoTrigger || pos < loopSamples.start) {
+ return;
+ }
+
+ // If the loop-in and out points are set so close that the loop would be
+ // 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 (quantizedBeat != -1 && pBeats) {
+ pos = static_cast<int>(floor(pBeats->findNthBeat(quantizedBeat, 2)));
+ if (pos == -1 || (pos - loopSamples.start) < MINIMUM_AUDIBLE_LOOP_SIZE) {
+ pos = loopSamples.start + MINIMUM_AUDIBLE_LOOP_SIZE;
+ }
+ } else {
+ pos = loopSamples.start + MINIMUM_AUDIBLE_LOOP_SIZE;
+ }
+ }
+
+ // set loop out position
+ loopSamples.end = pos;
+
+ m_pCOLoopEndPosition->set(loopSamples.end);
+
+ // start looping
+ if (loopSamples.start != kNoTrigger &&
+ loopSamples.end != kNoTrigger) {
+ setLoopingEnabled(true);
+ loopSamples.seek = true;
+ } else {
+ loopSamples.seek = false;
+ }
+
+ if (m_pQuantizeEnabled->toBool() && pBeats) {
+ m_pCOBeatLoopSize->setAndConfirm(
+ pBeats->numBeatsInRange(loopSamples.start, loopSamples.end));
+ updateBeatLoopingControls();
+ } else {
+ clearActiveBeatLoop();
+ }
+ //qDebug() << "set loop_out to " << loopSamples.end;
+
+ m_loopSamples.setValue(loopSamples);
+}
+
+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<double>(
+ 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::slotReloopToggle(double val) {
+ if (!m_pTrack || val <= 0.0) {
+ return;
+ }
+
+ // If we're looping, stop looping
+ if (m_bLoopingEnabled) {
+ // If loop roll was active, also disable slip.
+ if (m_bLoopRollActive) {
+ m_pSlipEnabled->set(0);
+ m_bLoopRollActive = false;
+ m_activeLoopRolls.clear();
+ }
+ setLoopingEnabled(false);
+ //qDebug() << "reloop_toggle looping off";
+ } else {
+ // 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) {
+ setLoopingEnabled(true);
+ if (m_currentSample.getValue() > loopSamples.end) {
+ slotLoopInGoto(1);
+ }
+ }
+ //qDebug() << "reloop_toggle looping on";
+ }
+}
+
+void LoopingControl::slotReloopAndStop(double pressed) {
+ if (pressed > 0) {
+ m_pPlayButton->set(0.0);
+ seekAbs(static_cast<double>(
+ m_loopSamples.getValue().start));
+ setLoopingEnabled(true);
+ }
+}
+
+void LoopingControl::slotLoopStartPos(double pos) {
+ // This slot is called before trackLoaded() for a new Track
+ LoopSamples loopSamples = m_loopSamples.getValue();
+
+ if (loopSamples.start == pos) {
+ //nothing to do
+ return;
+ }
+
+ clearActiveBeatLoop();
+
+ if (pos == kNoTrigger) {
+ setLoopingEnabled(false);
+ }
+
+ loopSamples.seek = false;
+ loopSamples.start = pos;
+ m_pCOLoopStartPosition->set(pos);
+
+ if (loopSamples.end != kNoTrigger &&
+ loopSamples.end <= loopSamples.start) {
+ loopSamples.end = kNoTrigger;
+ m_pCOLoopEndPosition->set(kNoTrigger);
+ setLoopingEnabled(false);
+ }
+ m_loopSamples.setValue(loopSamples);
+}
+
+void LoopingControl::slotLoopEndPos(double pos) {
+ // This slot is called before trackLoaded() for a new Track
+
+ LoopSamples loopSamples = m_loopSamples.getValue();
+ if (loopSamples.end == pos) {
+ //nothing to do
+ return;
+ }
+
+ // Reject if the loop-in is not set, or if the new position is before the
+ // start point (but not -1).
+ if (loopSamples.start == kNoTrigger ||
+ (pos != kNoTrigger && pos <= loopSamples.start)) {
+ m_pCOLoopEndPosition->set(loopSamples.end);
+ return;
+ }
+
+ clearActiveBeatLoop();
+
+ if (pos == -1.0) {
+ setLoopingEnabled(false);
+ }
+ loopSamples.end = pos;
+ loopSamples.seek = false;
+ m_pCOLoopEndPosition->set(pos);
+ m_loopSamples.setValue(loopSamples);
+}
+
+// This is called from the engine thread
+void LoopingControl::notifySeek(double dNewPlaypos, bool adjustingPhase) {
+ LoopSamples loopSamples = m_loopSamples.getValue();
+ double currentSample = m_currentSample.getValue();
+ if (m_bLoopingEnabled && !adjustingPhase) {
+ // Disable loop when we jumping out, or over a catching loop,
+ // using hot cues or waveform overview.
+ // Do not jump out of a loop if we adjust a phase (lp1743010)
+ if (currentSample >= loopSamples.start &&
+ currentSample <= loopSamples.end &&
+ dNewPlaypos < loopSamples.start) {
+ // jumping out of loop in backwards
+ setLoopingEnabled(false);
+ }
+ if (currentSample <= loopSamples.end &&
+ dNewPlaypos > loopSamples.end) {
+ // jumping out a loop or over a catching loop forward
+ setLoopingEnabled(false);
+ }
+ }
+}
+
+void LoopingControl::setLoopingEnabled(bool enabled) {
+ m_bLoopingEnabled = enabled;
+ m_pCOLoopEnabled->set(enabled);
+ BeatLoopingControl* pActiveBeatLoop = load_atomic_pointer(m_pActiveBeatLoop);
+ if (pActiveBeatLoop != nullptr) {
+ if (enabled) {
+ pActiveBeatLoop->activate();
+ } else {
+ pActiveBeatLoop->deactivate();
+ }
+ }
+}
+
+void LoopingControl::trackLoaded(TrackPointer pNewTrack) {
+ if (m_pTrack) {
+ disconnect(m_pTrack.get(), &Track::beatsUpdated,
+ this, &LoopingControl::slotUpdatedTrackBeats);
+ }
+
+ clearActiveBeatLoop();
+
+ if (pNewTrack) {
+ m_pTrack = pNewTrack;
+ m_pBeats = m_pTrack->getBeats();
+ connect(m_pTrack.get(), &Track::beatsUpdated,
+ this, &LoopingControl::slotUpdatedTrackBeats);
+ } else {
+ m_pTrack.reset();
+ m_pBeats.clear();
+ }
+}
+
+void LoopingControl::slotUpdatedTrackBeats() {
+ TrackPointer pTrack = m_pTrack;
+ if (pTrack) {
+ m_pBeats = pTrack->getBeats();
+ }
+}
+
+void LoopingControl::slotBeatLoopActivate(BeatLoopingControl* pBeatLoopControl) {
+ if (!m_pTrack) {
+ return;
+ }
+
+ // Maintain the current start point if there is an active loop currently
+ // 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, true);
+}
+
+void LoopingControl::slotBeatLoopActivateRoll(BeatLoopingControl* pBeatLoopControl) {
+ if (!m_pTrack) {
+ return;
+ }
+
+ // Disregard existing loops (except beatlooprolls).
+ m_pSlipEnabled->set(1);
+ slotBeatLoop(pBeatLoopControl->getSize(), m_bLoopRollActive, true);
+ m_bLoopRollActive = true;
+ m_activeLoopRolls.push(pBeatLoopControl->getSize());
+}
+
+void LoopingControl::slotBeatLoopDeactivate(BeatLoopingControl* pBeatLoopControl) {
+ Q_UNUSED(pBeatLoopControl);
+ setLoopingEnabled(false);
+}
+
+void LoopingControl::slotBeatLoopDeactivateRoll(BeatLoopingControl* pBeatLoopControl) {
+ pBeatLoopControl->deactivate();
+ const double size = pBeatLoopControl->getSize();
+ auto i = m_activeLoopRolls.begin();
+ while (i != m_activeLoopRolls.end()) {
+ if (size == *i) {
+ i = m_activeLoopRolls.erase(i);
+ } else {
+ ++i;
+ }
+ }
+
+ // 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_activeLoopRolls.empty()) {
+ setLoopingEnabled(false);
+ m_pSlipEnabled->set(0);
+ m_bLoopRollActive = false;
+ }
+
+ // Return to the previous beatlooproll if necessary.
+ if (!m_activeLoopRolls.empty()) {
+ slotBeatLoop(m_activeLoopRolls.top(), m_bLoopRollActive, true);
+ }
+}
+
+void LoopingControl::clearActiveBeatLoop() {
+ BeatLoopingControl* pOldBeatLoop = m_pActiveBeatLoop.fetchAndStoreAcquire(nullptr);
+ if (pOldBeatLoop != nullptr) {
+ pOldBeatLoop->deactivate();
+ }
+}
+
+bool LoopingControl::currentLoopMatchesBeatloopSize() {
+ BeatsPointer pBeats = m_pBeats;
+ if (!pBeats) {
+ return false;
+ }
+
+ LoopSamples loopSamples = m_loopSamples.getValue();
+
+ // Calculate where the loop out point would be if it is a beatloop
+ double beatLoopOutPoint =
+ pBeats->findNBeatsFromSample(loopSamples.start, m_pCOBeatLoopSize->get());
+
+ return loopSamples.end > beatLoopOutPoint - 2 &&
+ loopSamples.end < beatLoopOutPoint + 2;
+}
+
+void LoopingControl::updateBeatLoopingControls() {
+ // O(n) search, but there are only ~10-ish beatloop controls so this is
+ // fine.
+ double dBeatloopSize = m_pCOBeatLoopSize->get();
+ for (BeatLoopingControl* pBeatLoopControl: qAsConst(m_beatLoops)) {
+ if (pBeatLoopControl->getSize() == dBeatloopSize) {
+ if (m_bLoopingEnabled) {
+ pBeatLoopControl->activate();
+ }
+ BeatLoopingControl* pOldBeatLoop =
+ m_pActiveBeatLoop.fetchAndStoreRelease(pBeatLoopControl);
+ if (pOldBeatLoop != nullptr && pOldBeatLoop != pBeatLoopControl) {
+ pOldBeatLoop->deactivate();
+ }
+ return;
+ }
+ }
+ // If the loop did not return from the function yet, dBeatloopSize does
+ // not match any of the BeatLoopingControls' sizes.
+ clearActiveBeatLoop();
+}
+
+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;
+ }
+
+ int samples = m_pTrackSamples->get();
+ BeatsPointer pBeats = m_pBeats;
+ if (samples == 0 || !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, false};
+ LoopSamples loopSamples = m_loopSamples.getValue();
+ double currentSample = m_currentSample.getValue();
+
+ // Start from the current position/closest beat and
+ // create the loop around X beats from there.
+ if (keepStartPoint) {
+ if (loopSamples.start != kNoTrigger) {
+ newloopSamples.start = loopSamples.start;
+ } else {
+ newloopSamples.start = currentSample;
+ }
+ } 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 prevBeat;
+ double nextBeat;
+ pBeats->findPrevNextBeats(currentSample, &prevBeat, &nextBeat);
+
+ if (m_pQuantizeEnabled->toBool() && 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