From 4257db41cccd9f5e0f1730aea86edbb1be240170 Mon Sep 17 00:00:00 2001 From: Be Date: Sun, 6 Jan 2019 10:40:02 -0600 Subject: move EngineControls to their own folder --- build/depends.py | 17 +- build/features.py | 2 +- src/controllers/controlpickermenu.cpp | 4 +- src/engine/bpmcontrol.cpp | 884 ----------------- src/engine/bpmcontrol.h | 169 ---- src/engine/clockcontrol.cpp | 68 -- src/engine/clockcontrol.h | 37 - src/engine/controls/bpmcontrol.cpp | 884 +++++++++++++++++ src/engine/controls/bpmcontrol.h | 169 ++++ src/engine/controls/clockcontrol.cpp | 68 ++ src/engine/controls/clockcontrol.h | 37 + src/engine/controls/cuecontrol.cpp | 1141 ++++++++++++++++++++++ src/engine/controls/cuecontrol.h | 182 ++++ src/engine/controls/enginecontrol.cpp | 109 +++ src/engine/controls/enginecontrol.h | 108 +++ src/engine/controls/keycontrol.cpp | 374 ++++++++ src/engine/controls/keycontrol.h | 81 ++ src/engine/controls/loopingcontrol.cpp | 1356 +++++++++++++++++++++++++++ src/engine/controls/loopingcontrol.h | 244 +++++ src/engine/controls/quantizecontrol.cpp | 129 +++ src/engine/controls/quantizecontrol.h | 45 + src/engine/controls/ratecontrol.cpp | 664 +++++++++++++ src/engine/controls/ratecontrol.h | 192 ++++ src/engine/controls/vinylcontrolcontrol.cpp | 167 ++++ src/engine/controls/vinylcontrolcontrol.h | 44 + src/engine/cuecontrol.cpp | 1141 ---------------------- src/engine/cuecontrol.h | 182 ---- src/engine/enginebuffer.cpp | 18 +- src/engine/enginecontrol.cpp | 109 --- src/engine/enginecontrol.h | 108 --- src/engine/keycontrol.cpp | 374 -------- src/engine/keycontrol.h | 81 -- src/engine/loopingcontrol.cpp | 1356 --------------------------- src/engine/loopingcontrol.h | 244 ----- src/engine/quantizecontrol.cpp | 129 --- src/engine/quantizecontrol.h | 45 - src/engine/ratecontrol.cpp | 664 ------------- src/engine/ratecontrol.h | 192 ---- src/engine/readaheadmanager.cpp | 4 +- src/engine/sync/synccontrol.cpp | 4 +- src/engine/sync/synccontrol.h | 2 +- src/engine/vinylcontrolcontrol.cpp | 167 ---- src/engine/vinylcontrolcontrol.h | 44 - src/mixer/basetrackplayer.cpp | 2 +- src/preferences/dialog/dlgprefdeck.cpp | 2 +- src/preferences/dialog/dlgprefdeck.h | 2 +- src/test/bpmcontrol_test.cpp | 2 +- src/test/enginesynctest.cpp | 2 +- src/test/looping_control_test.cpp | 2 +- src/test/mockedenginebackendtest.h | 2 +- src/test/readaheadmanager_test.cpp | 2 +- src/test/signalpathtest.h | 2 +- src/waveform/renderers/waveformmarkset.cpp | 2 +- 53 files changed, 6029 insertions(+), 6030 deletions(-) delete mode 100644 src/engine/bpmcontrol.cpp delete mode 100644 src/engine/bpmcontrol.h delete mode 100644 src/engine/clockcontrol.cpp delete mode 100644 src/engine/clockcontrol.h create mode 100644 src/engine/controls/bpmcontrol.cpp create mode 100644 src/engine/controls/bpmcontrol.h create mode 100644 src/engine/controls/clockcontrol.cpp create mode 100644 src/engine/controls/clockcontrol.h create mode 100644 src/engine/controls/cuecontrol.cpp create mode 100644 src/engine/controls/cuecontrol.h create mode 100644 src/engine/controls/enginecontrol.cpp create mode 100644 src/engine/controls/enginecontrol.h create mode 100644 src/engine/controls/keycontrol.cpp create mode 100644 src/engine/controls/keycontrol.h create mode 100644 src/engine/controls/loopingcontrol.cpp create mode 100644 src/engine/controls/loopingcontrol.h create mode 100644 src/engine/controls/quantizecontrol.cpp create mode 100644 src/engine/controls/quantizecontrol.h create mode 100644 src/engine/controls/ratecontrol.cpp create mode 100644 src/engine/controls/ratecontrol.h create mode 100644 src/engine/controls/vinylcontrolcontrol.cpp create mode 100644 src/engine/controls/vinylcontrolcontrol.h delete mode 100644 src/engine/cuecontrol.cpp delete mode 100644 src/engine/cuecontrol.h delete mode 100644 src/engine/enginecontrol.cpp delete mode 100644 src/engine/enginecontrol.h delete mode 100644 src/engine/keycontrol.cpp delete mode 100644 src/engine/keycontrol.h delete mode 100644 src/engine/loopingcontrol.cpp delete mode 100644 src/engine/loopingcontrol.h delete mode 100644 src/engine/quantizecontrol.cpp delete mode 100644 src/engine/quantizecontrol.h delete mode 100644 src/engine/ratecontrol.cpp delete mode 100644 src/engine/ratecontrol.h delete mode 100644 src/engine/vinylcontrolcontrol.cpp delete mode 100644 src/engine/vinylcontrolcontrol.h diff --git a/build/depends.py b/build/depends.py index 678c253a67..516edc4362 100644 --- a/build/depends.py +++ b/build/depends.py @@ -818,16 +818,15 @@ class MixxxCore(Feature): "src/engine/enginedeck.cpp", "src/engine/engineaux.cpp", "src/engine/channelmixer_autogen.cpp", - - "src/engine/enginecontrol.cpp", - "src/engine/ratecontrol.cpp", "src/engine/positionscratchcontroller.cpp", - "src/engine/loopingcontrol.cpp", - "src/engine/bpmcontrol.cpp", - "src/engine/keycontrol.cpp", - "src/engine/cuecontrol.cpp", - "src/engine/quantizecontrol.cpp", - "src/engine/clockcontrol.cpp", + "src/engine/controls/bpmcontrol.cpp", + "src/engine/controls/clockcontrol.cpp", + "src/engine/controls/cuecontrol.cpp", + "src/engine/controls/enginecontrol.cpp", + "src/engine/controls/keycontrol.cpp", + "src/engine/controls/loopingcontrol.cpp", + "src/engine/controls/quantizecontrol.cpp", + "src/engine/controls/ratecontrol.cpp", "src/engine/readaheadmanager.cpp", "src/engine/enginetalkoverducking.cpp", "src/engine/cachingreader.cpp", diff --git a/build/features.py b/build/features.py index 399ce035d3..48060fcd5f 100644 --- a/build/features.py +++ b/build/features.py @@ -318,7 +318,7 @@ class VinylControl(Feature): 'src/vinylcontrol/vinylcontrolmanager.cpp', 'src/vinylcontrol/vinylcontrolprocessor.cpp', 'src/vinylcontrol/steadypitch.cpp', - 'src/engine/vinylcontrolcontrol.cpp', ] + 'src/engine/controls/vinylcontrolcontrol.cpp', ] if build.platform_is_windows: sources.append("lib/xwax/timecoder_win32.cpp") sources.append("lib/xwax/lut_win32.cpp") diff --git a/src/controllers/controlpickermenu.cpp b/src/controllers/controlpickermenu.cpp index 1c5fdb18c6..a989d3af43 100644 --- a/src/controllers/controlpickermenu.cpp +++ b/src/controllers/controlpickermenu.cpp @@ -2,8 +2,8 @@ #include "vinylcontrol/defs_vinylcontrol.h" #include "mixer/playermanager.h" -#include "engine/cuecontrol.h" -#include "engine/loopingcontrol.h" +#include "engine/controls/cuecontrol.h" +#include "engine/controls/loopingcontrol.h" #include "effects/effectrack.h" #include "effects/effectchainslot.h" #include "effects/effectslot.h" diff --git a/src/engine/bpmcontrol.cpp b/src/engine/bpmcontrol.cpp deleted file mode 100644 index 013fe53213..0000000000 --- a/src/engine/bpmcontrol.cpp +++ /dev/null @@ -1,884 +0,0 @@ -#include - -#include "control/controlobject.h" -#include "control/controlpushbutton.h" -#include "control/controllinpotmeter.h" - -#include "engine/enginebuffer.h" -#include "engine/bpmcontrol.h" -#include "waveform/visualplayposition.h" -#include "engine/enginechannel.h" -#include "engine/enginemaster.h" -#include "control/controlproxy.h" -#include "util/assert.h" -#include "util/math.h" -#include "util/duration.h" - -namespace { -const int kMinBpm = 30; -// Maximum allowed interval between beats (calculated from kMinBpm). -const mixxx::Duration kMaxInterval = mixxx::Duration::fromMillis(1000.0 * (60.0 / kMinBpm)); -const int kFilterLength = 5; -// The local_bpm is calculated forward and backward this number of beats, so -// the actual number of beats is this x2. -const int kLocalBpmSpan = 4; -const SINT kSamplesPerFrame = 2; -} - -BpmControl::BpmControl(QString group, - UserSettingsPointer pConfig) - : EngineControl(group, pConfig), - m_tapFilter(this, kFilterLength, kMaxInterval), - m_dSyncInstantaneousBpm(0.0), - m_dLastSyncAdjustment(1.0), - m_sGroup(group) { - m_dSyncTargetBeatDistance.setValue(0.0); - m_dUserOffset.setValue(0.0); - - m_pPlayButton = new ControlProxy(group, "play", this); - m_pReverseButton = new ControlProxy(group, "reverse", this); - m_pRateSlider = new ControlProxy(group, "rate", this); - m_pRateSlider->connectValueChanged(this, &BpmControl::slotUpdateEngineBpm, - Qt::DirectConnection); - m_pQuantize = ControlObject::getControl(group, "quantize"); - m_pRateRange = new ControlProxy(group, "rateRange", this); - m_pRateRange->connectValueChanged(this, &BpmControl::slotUpdateRateSlider, - Qt::DirectConnection); - m_pRateDir = new ControlProxy(group, "rate_dir", this); - m_pRateDir->connectValueChanged(this, &BpmControl::slotUpdateEngineBpm, - Qt::DirectConnection); - - m_pPrevBeat.reset(new ControlProxy(group, "beat_prev")); - m_pNextBeat.reset(new ControlProxy(group, "beat_next")); - m_pClosestBeat.reset(new ControlProxy(group, "beat_closest")); - - m_pLoopEnabled = new ControlProxy(group, "loop_enabled", this); - m_pLoopStartPosition = new ControlProxy(group, "loop_start_position", this); - m_pLoopEndPosition = new ControlProxy(group, "loop_end_position", this); - - m_pFileBpm = new ControlObject(ConfigKey(group, "file_bpm")); - connect(m_pFileBpm, &ControlObject::valueChanged, - this, &BpmControl::slotFileBpmChanged, - Qt::DirectConnection); - m_pLocalBpm = new ControlObject(ConfigKey(group, "local_bpm")); - m_pAdjustBeatsFaster = new ControlPushButton(ConfigKey(group, "beats_adjust_faster"), false); - connect(m_pAdjustBeatsFaster, &ControlObject::valueChanged, - this, &BpmControl::slotAdjustBeatsFaster, - Qt::DirectConnection); - m_pAdjustBeatsSlower = new ControlPushButton(ConfigKey(group, "beats_adjust_slower"), false); - connect(m_pAdjustBeatsSlower, &ControlObject::valueChanged, - this, &BpmControl::slotAdjustBeatsSlower, - Qt::DirectConnection); - m_pTranslateBeatsEarlier = new ControlPushButton(ConfigKey(group, "beats_translate_earlier"), false); - connect(m_pTranslateBeatsEarlier, &ControlObject::valueChanged, - this, &BpmControl::slotTranslateBeatsEarlier, - Qt::DirectConnection); - m_pTranslateBeatsLater = new ControlPushButton(ConfigKey(group, "beats_translate_later"), false); - connect(m_pTranslateBeatsLater, &ControlObject::valueChanged, - this, &BpmControl::slotTranslateBeatsLater, - Qt::DirectConnection); - - // Pick a wide range (1 to 200) and allow out of bounds sets. This lets you - // map a soft-takeover MIDI knob to the BPM. This also creates bpm_up and - // bpm_down controls. - // bpm_up / bpm_down steps by 1 - // bpm_up_small / bpm_down_small steps by 0.1 - m_pEngineBpm = new ControlLinPotmeter(ConfigKey(group, "bpm"), 1, 200, 1, 0.1, true); - connect(m_pEngineBpm, &ControlObject::valueChanged, - this, &BpmControl::slotUpdateRateSlider, - Qt::DirectConnection); - - m_pButtonTap = new ControlPushButton(ConfigKey(group, "bpm_tap")); - connect(m_pButtonTap, &ControlObject::valueChanged, - this, &BpmControl::slotBpmTap, - Qt::DirectConnection); - - // Beat sync (scale buffer tempo relative to tempo of other buffer) - m_pButtonSync = new ControlPushButton(ConfigKey(group, "beatsync")); - connect(m_pButtonSync, &ControlObject::valueChanged, - this, &BpmControl::slotControlBeatSync, - Qt::DirectConnection); - - m_pButtonSyncPhase = new ControlPushButton(ConfigKey(group, "beatsync_phase")); - connect(m_pButtonSyncPhase, &ControlObject::valueChanged, - this, &BpmControl::slotControlBeatSyncPhase, - Qt::DirectConnection); - - m_pButtonSyncTempo = new ControlPushButton(ConfigKey(group, "beatsync_tempo")); - connect(m_pButtonSyncTempo, &ControlObject::valueChanged, - this, &BpmControl::slotControlBeatSyncTempo, - Qt::DirectConnection); - - m_pTranslateBeats = new ControlPushButton(ConfigKey(group, "beats_translate_curpos")); - connect(m_pTranslateBeats, &ControlObject::valueChanged, - this, &BpmControl::slotBeatsTranslate, - Qt::DirectConnection); - - m_pBeatsTranslateMatchAlignment = new ControlPushButton(ConfigKey(group, "beats_translate_match_alignment")); - connect(m_pBeatsTranslateMatchAlignment, &ControlObject::valueChanged, - this, &BpmControl::slotBeatsTranslateMatchAlignment, - Qt::DirectConnection); - - connect(&m_tapFilter, &TapFilter::tapped, - this, &BpmControl::slotTapFilter, - Qt::DirectConnection); - - // Measures distance from last beat in percentage: 0.5 = half-beat away. - m_pThisBeatDistance = new ControlProxy(group, "beat_distance", this); - m_pSyncMode = new ControlProxy(group, "sync_mode", this); -} - -BpmControl::~BpmControl() { - delete m_pFileBpm; - delete m_pLocalBpm; - delete m_pEngineBpm; - delete m_pButtonTap; - delete m_pButtonSync; - delete m_pButtonSyncPhase; - delete m_pButtonSyncTempo; - delete m_pTranslateBeats; - delete m_pBeatsTranslateMatchAlignment; - delete m_pTranslateBeatsEarlier; - delete m_pTranslateBeatsLater; - delete m_pAdjustBeatsFaster; - delete m_pAdjustBeatsSlower; -} - -double BpmControl::getBpm() const { - return m_pEngineBpm->get(); -} - -void BpmControl::slotFileBpmChanged(double bpm) { - Q_UNUSED(bpm); - // Adjust the file-bpm with the current setting of the rate to get the - // engine BPM. We only do this for SYNC_NONE decks because EngineSync will - // set our BPM if the file BPM changes. See SyncControl::fileBpmChanged(). - BeatsPointer pBeats = m_pBeats; - if (pBeats) { - const double beats_bpm = - pBeats->getBpmAroundPosition( - getSampleOfTrack().current, kLocalBpmSpan); - if (beats_bpm != -1) { - m_pLocalBpm->set(beats_bpm); - } else { - m_pLocalBpm->set(bpm); - } - } else { - m_pLocalBpm->set(bpm); - } - if (getSyncMode() == SYNC_NONE) { - slotUpdateEngineBpm(); - } - resetSyncAdjustment(); -} - -void BpmControl::slotAdjustBeatsFaster(double v) { - BeatsPointer pBeats = m_pBeats; - if (v > 0 && pBeats && (pBeats->getCapabilities() & Beats::BEATSCAP_SETBPM)) { - double new_bpm = math_min(200.0, pBeats->getBpm() + .01); - pBeats->setBpm(new_bpm); - } -} - -void BpmControl::slotAdjustBeatsSlower(double v) { - BeatsPointer pBeats = m_pBeats; - if (v > 0 && pBeats && (pBeats->getCapabilities() & Beats::BEATSCAP_SETBPM)) { - double new_bpm = math_max(10.0, pBeats->getBpm() - .01); - pBeats->setBpm(new_bpm); - } -} - -void BpmControl::slotTranslateBeatsEarlier(double v) { - BeatsPointer pBeats = m_pBeats; - if (v > 0 && pBeats && - (pBeats->getCapabilities() & Beats::BEATSCAP_TRANSLATE)) { - const int translate_dist = getSampleOfTrack().rate * -.01; - pBeats->translate(translate_dist); - } -} - -void BpmControl::slotTranslateBeatsLater(double v) { - BeatsPointer pBeats = m_pBeats; - if (v > 0 && pBeats && - (pBeats->getCapabilities() & Beats::BEATSCAP_TRANSLATE)) { - // TODO(rryan): Track::getSampleRate is possibly inaccurate! - const int translate_dist = getSampleOfTrack().rate * .01; - pBeats->translate(translate_dist); - } -} - -void BpmControl::slotBpmTap(double v) { - if (v > 0) { - m_tapFilter.tap(); - } -} - -void BpmControl::slotTapFilter(double averageLength, int numSamples) { - // averageLength is the average interval in milliseconds tapped over - // numSamples samples. Have to convert to BPM now: - - if (averageLength <= 0) - return; - - if (numSamples < 4) - return; - - BeatsPointer pBeats = m_pBeats; - if (!pBeats) - return; - - // (60 seconds per minute) * (1000 milliseconds per second) / (X millis per - // beat) = Y beats/minute - double averageBpm = 60.0 * 1000.0 / averageLength / calcRateRatio(); - pBeats->setBpm(averageBpm); -} - -void BpmControl::slotControlBeatSyncPhase(double v) { - if (!v) return; - getEngineBuffer()->requestSyncPhase(); -} - -void BpmControl::slotControlBeatSyncTempo(double v) { - if (!v) return; - syncTempo(); -} - -void BpmControl::slotControlBeatSync(double v) { - if (!v) return; - if (!syncTempo()) { - // syncTempo failed, nothing else to do - return; - } - - // Also sync phase if quantize is enabled. - // this is used from controller scripts, where the latching behaviour of - // the sync_enable CO cannot be used - if (m_pPlayButton->toBool() && m_pQuantize->toBool()) { - getEngineBuffer()->requestSyncPhase(); - } -} - -bool BpmControl::syncTempo() { - EngineBuffer* pOtherEngineBuffer = pickSyncTarget(); - - if (!pOtherEngineBuffer) { - return false; - } - - double fThisBpm = m_pEngineBpm->get(); - double fThisLocalBpm = m_pLocalBpm->get(); - - double fOtherBpm = pOtherEngineBuffer->getBpm(); - double fOtherLocalBpm = pOtherEngineBuffer->getLocalBpm(); - - //qDebug() << "this" << "bpm" << fThisBpm << "filebpm" << fThisFileBpm; - //qDebug() << "other" << "bpm" << fOtherBpm << "filebpm" << fOtherFileBpm; - - //////////////////////////////////////////////////////////////////////////// - // Rough proof of how syncing works -- rryan 3/2011 - // ------------------------------------------------ - // - // Let this and other denote this deck versus the sync-target deck. - // - // The goal is for this deck's effective BPM to equal the other decks. - // - // thisBpm = otherBpm - // - // The overall rate is the product of range, direction, and scale plus 1: - // - // rate = 1.0 + rateDir * rateRange * rateScale - // - // An effective BPM is the file-bpm times the rate: - // - // bpm = fileBpm * rate - // - // So our goal is to tweak thisRate such that this equation is true: - // - // thisFileBpm * (1.0 + thisRate) = otherFileBpm * (1.0 + otherRate) - // - // so rearrange this equation in terms of thisRate: - // - // thisRate = (otherFileBpm * (1.0 + otherRate)) / thisFileBpm - 1.0 - // - // So the new rateScale to set is: - // - // thisRateScale = ((otherFileBpm * (1.0 + otherRate)) / thisFileBpm - 1.0) / (thisRateDir * thisRateRange) - - if (fOtherBpm > 0.0 && fThisBpm > 0.0) { - // The desired rate is the other decks effective rate divided by this - // deck's file BPM. This gives us the playback rate that will produce an - // effective BPM equivalent to the other decks. - double desiredRate = fOtherBpm / fThisLocalBpm; - - // Test if this buffer's bpm is the double of the other one, and adjust - // the rate scale. I believe this is intended to account for our BPM - // algorithm sometimes finding double or half BPMs. This avoids drastic - // scales. - - float fFileBpmDelta = fabs(fThisLocalBpm - fOtherLocalBpm); - if (fabs(fThisLocalBpm * 2.0 - fOtherLocalBpm) < fFileBpmDelta) { - desiredRate /= 2.0; - } else if (fabs(fThisLocalBpm - 2.0 * fOtherLocalBpm) < fFileBpmDelta) { - desiredRate *= 2.0; - } - - // Subtract the base 1.0, now fDesiredRate is the percentage - // increase/decrease in playback rate, not the playback rate. - double desiredRateShift = desiredRate - 1.0; - - // Ensure the rate is within reasonable boundaries. Remember, this is the - // percent to scale the rate, not the rate itself. If fDesiredRate was -1, - // that would mean the deck would be completely stopped. If fDesiredRate - // is 1, that means it is playing at 2x speed. This limit enforces that - // we are scaled between 0.5x and 2x. - if (desiredRateShift < 1.0 && desiredRateShift > -0.5) - { - m_pEngineBpm->set(m_pLocalBpm->get() * desiredRate); - - - // Adjust the rateScale. We have to divide by the range and - // direction to get the correct rateScale. - double desiredRateSlider = desiredRateShift / (m_pRateRange->get() * m_pRateDir->get()); - // And finally, set the slider - m_pRateSlider->set(desiredRateSlider); - - return true; - } - } - return false; -} - -// static -double BpmControl::shortestPercentageChange(const double& current_percentage, - const double& target_percentage) { - if (current_percentage == target_percentage) { - return 0.0; - } else if (current_percentage < target_percentage) { - // Invariant: forwardDistance - backwardsDistance == 1.0 - - // my: 0.01 target:0.99 forwards: 0.98 - // my: 0.25 target: 0.5 forwards: 0.25 - // my: 0.25 target: 0.75 forwards: 0.5 - // my: 0.98 target: 0.99 forwards: 0.01 - const double forwardDistance = target_percentage - current_percentage; - - // my: 0.01 target:0.99 backwards: -0.02 - // my: 0.25 target: 0.5 backwards: -0.75 - // my: 0.25 target: 0.75 backwards: -0.5 - // my: 0.98 target: 0.99 backwards: -0.99 - const double backwardsDistance = target_percentage - current_percentage - 1.0; - - return (fabs(forwardDistance) < fabs(backwardsDistance)) ? - forwardDistance : backwardsDistance; - } else { // current_percentage > target_percentage - // Invariant: forwardDistance - backwardsDistance == 1.0 - - // my: 0.99 target: 0.01 forwards: 0.02 - const double forwardDistance = 1.0 - current_percentage + target_percentage; - - // my: 0.99 target:0.01 backwards: -0.98 - const double backwardsDistance = target_percentage - current_percentage; - - return (fabs(forwardDistance) < fabs(backwardsDistance)) ? - forwardDistance : backwardsDistance; - } -} - -double BpmControl::calcSyncedRate(double userTweak) { - double rate = 1.0; - // Don't know what to do if there's no bpm. - if (m_pLocalBpm->get() != 0.0) { - rate = m_dSyncInstantaneousBpm / m_pLocalBpm->get(); - } - - // If we are not quantized, or there are no beats, or we're master, - // or we're in reverse, just return the rate as-is. - if (!m_pQuantize->get() || getSyncMode() == SYNC_MASTER || - !m_pBeats || m_pReverseButton->get()) { - m_resetSyncAdjustment = true; - return rate + userTweak; - } - - // Now we need to get our beat distance so we can figure out how - // out of phase we are. - double dThisPosition = getSampleOfTrack().current; - double dBeatLength; - double my_percentage; - if (!BpmControl::getBeatContextNoLookup(dThisPosition, - m_pPrevBeat->get(), m_pNextBeat->get(), - &dBeatLength, &my_percentage)) { - m_resetSyncAdjustment = true; - return rate + 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->toBool(); - const double loop_size = (m_pLoopEndPosition->get() - - m_pLoopStartPosition->get()) / - dBeatLength; - if (loop_enabled && loop_size < 1.0 && loop_size > 0) { - m_resetSyncAdjustment = true; - return rate + userTweak; - } - - // Now we have all we need to calculate the sync adjustment if any. - double adjustment = calcSyncAdjustment(my_percentage, userTweak != 0.0); - return (rate + userTweak) * adjustment; -} - -double BpmControl::calcSyncAdjustment(double my_percentage, bool userTweakingSync) { - int resetSyncAdjustment = m_resetSyncAdjustment.fetchAndStoreRelaxed(0); - if (resetSyncAdjustment) { - m_dLastSyncAdjustment = 1.0; - } - - // Either shortest distance is directly to the master or backwards. - - // TODO(rryan): This is kind of backwards because we are measuring distance - // from master to my percentage. All of the control code below is based on - // this point of reference so I left it this way but I think we should think - // about things in terms of "my percentage-offset setpoint" that the control - // loop should aim to maintain. - // TODO(rryan): All of this code is based on the assumption that a track - // can't pass through multiple beats in one engine callback. Instead our - // setpoint should be tracking the true offset in "samples traveled" rather - // than modular 1.0 beat fractions. This will allow sync to work across loop - // boundaries too. - - double master_percentage = m_dSyncTargetBeatDistance.getValue(); - double shortest_distance = shortestPercentageChange( - master_percentage, my_percentage); - - /*qDebug() << m_sGroup << m_dUserOffset; - qDebug() << "master beat distance:" << master_percentage; - qDebug() << "my beat distance:" << my_percentage; - qDebug() << "error :" << (shortest_distance - m_dUserOffset); - qDebug() << "user offset :" << m_dUserOffset;*/ - - double adjustment = 1.0; - - if (userTweakingSync) { - // Don't do anything else, leave it - adjustment = 1.0; - m_dUserOffset.setValue(shortest_distance); - } else { - double error = shortest_distance - m_dUserOffset.getValue(); - // Threshold above which we do sync adjustment. - const double kErrorThreshold = 0.01; - // Threshold above which sync is really, really bad, so much so that we - // don't even know if we're ahead or behind. This can occur when quantize was - // off, but then it gets turned on. - const double kTrainWreckThreshold = 0.2; - const double kSyncAdjustmentCap = 0.05; - if (fabs(error) > kTrainWreckThreshold) { - // Assume poor reflexes (late button push) -- speed up to catch the other track. - adjustment = 1.0 + kSyncAdjustmentCap; - } else if (fabs(error) > kErrorThreshold) { - // Proportional control constant. The higher this is, the more we - // influence sync. - const double kSyncAdjustmentProportional = 0.7; - const double kSyncDeltaCap = 0.02; - - // TODO(owilliams): There are a lot of "1.0"s in this code -- can we eliminate them? - const double adjust = 1.0 + (-error * kSyncAdjustmentProportional); - // Cap the difference between the last adjustment and this one. - double delta = adjust - m_dLastSyncAdjustment; - delta = math_clamp(delta, -kSyncDeltaCap, kSyncDeltaCap); - - // Cap the adjustment between -kSyncAdjustmentCap and +kSyncAdjustmentCap - adjustment = 1.0 + math_clamp( - m_dLastSyncAdjustment - 1.0 + delta, - -kSyncAdjustmentCap, kSyncAdjustmentCap); - } else { - // We are in sync, no adjustment needed. - adjustment = 1.0; - } - } - m_dLastSyncAdjustment = adjustment; - return adjustment; -} - -double BpmControl::getBeatDistance(double dThisPosition) const { - // We have to adjust our reported beat distance by the user offset to - // preserve comparisons of beat distances. Specifically, this beat distance - // is used in synccontrol to update the internal clock beat distance, and if - // we don't adjust the reported distance the track will try to adjust - // sync against itself. - double dPrevBeat = m_pPrevBeat->get(); - double dNextBeat = m_pNextBeat->get(); - - if (dPrevBeat == -1 || dNextBeat == -1) { - return 0.0 - m_dUserOffset.getValue(); - } - - double dBeatLength = dNextBeat - dPrevBeat; - double dBeatPercentage = dBeatLength == 0.0 ? 0.0 : - (dThisPosition - dPrevBeat) / dBeatLength; - // Because findNext and findPrev have an epsilon built in, sometimes - // the beat percentage is out of range. Fix it. - if (dBeatPercentage < 0) ++dBeatPercentage; - if (dBeatPercentage > 1) --dBeatPercentage; - - return dBeatPercentage - m_dUserOffset.getValue(); -} - -// static -bool BpmControl::getBeatContext(const BeatsPointer& pBeats, - const double dPosition, - double* dpPrevBeat, - double* dpNextBeat, - double* dpBeatLength, - double* dpBeatPercentage) { - if (!pBeats) { - return false; - } - - double dPrevBeat; - double dNextBeat; - if (!pBeats->findPrevNextBeats(dPosition, &dPrevBeat, &dNextBeat)) { - return false; - } - - if (dpPrevBeat != NULL) { - *dpPrevBeat = dPrevBeat; - } - - if (dpNextBeat != NULL) { - *dpNextBeat = dNextBeat; - } - - return getBeatContextNoLookup(dPosition, dPrevBeat, dNextBeat, - dpBeatLength, dpBeatPercentage); -} - -// static -bool BpmControl::getBeatContextNoLookup( - const double dPosition, - const double dPrevBeat, - const double dNextBeat, - double* dpBeatLength, - double* dpBeatPercentage) { - if (dPrevBeat == -1 || dNextBeat == -1) { - return false; - } - - double dBeatLength = dNextBeat - dPrevBeat; - if (dpBeatLength != NULL) { - *dpBeatLength = dBeatLength; - } - - if (dpBeatPercentage != NULL) { - *dpBeatPercentage = dBeatLength == 0.0 ? 0.0 : - (dPosition - dPrevBeat) / dBeatLength; - // Because findNext and findPrev have an epsilon built in, sometimes - // the beat percentage is out of range. Fix it. - if (*dpBeatPercentage < 0) ++*dpBeatPercentage; - if (*dpBeatPercentage > 1) --*dpBeatPercentage; - } - - return true; -} - -double BpmControl::getNearestPositionInPhase( - double dThisPosition, bool respectLoops, bool playing) { - // Without a beatgrid, we don't know the phase offset. - BeatsPointer pBeats = m_pBeats; - if (!pBeats) { - return dThisPosition; - } - // Master buffer is always in sync! - if (getSyncMode() == SYNC_MASTER) { - return dThisPosition; - } - - // Get the current position of this deck. - double dThisPrevBeat = m_pPrevBeat->get(); - double dThisNextBeat = m_pNextBeat->get(); - double dThisBeatLength; - if (dThisPosition > dThisNextBeat || dThisPosition < dThisPrevBeat) { - // There's a chance the COs might be out of date, so do a lookup. - // TODO: figure out a way so that quantized control can take care of - // this so this call isn't necessary. - if (!getBeatContext(pBeats, dThisPosition, - &dThisPrevBeat, &dThisNextBeat, - &dThisBeatLength, NULL)) { - return dThisPosition; - } - } else { - if (!getBeatContextNoLookup(dThisPosition, - dThisPrevBeat, dThisNextBeat, - &dThisBeatLength, NULL)) { - return dThisPosition; - } - } - - double dOtherBeatFraction; - if (getSyncMode() == SYNC_FOLLOWER) { - // If we're a follower, it's easy to get the other beat fraction - dOtherBeatFraction = m_dSyncTargetBeatDistance.getValue(); - } else { - // If not, we have to figure it out - EngineBuffer* pOtherEngineBuffer = pickSyncTarget(); - if (pOtherEngineBuffer == NULL) { - 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(); - BeatsPointer otherBeats = otherTrack ? otherTrack->getBeats() : BeatsPointer(); - - // If either track does not have beats, then we can't adjust the phase. - if (!otherBeats) { - return dThisPosition; - } - - double dOtherLength = ControlObject::getControl( - ConfigKey(pOtherEngineBuffer->getGroup(), "track_samples"))->get(); - double dOtherEnginePlayPos = pOtherEngineBuffer->getVisualPlayPos(); - double dOtherPosition = dOtherLength * dOtherEnginePlayPos; - - if (!BpmControl::getBeatContext(otherBeats, dOtherPosition, - NULL, NULL, NULL, &dOtherBeatFraction)) { - return dThisPosition; - } - } - - bool this_near_next = dThisNextBeat - dThisPosition <= dThisPosition - dThisPrevBeat; - bool other_near_next = dOtherBeatFraction >= 0.5; - - // We want our beat fraction to be identical to theirs. - - // If the two tracks have similar alignment, adjust phase is straight- - // forward. Use the same fraction for both beats, starting from the previous - // beat. But if This track is nearer to the next beat and the Other track - // is nearer to the previous beat, use This Next beat as the starting point - // for the phase. (ie, we pushed the sync button late). If This track - // is nearer to the previous beat, but the Other track is nearer to the - // next beat, we pushed the sync button early so use the double-previous - // beat as the basis for the adjustment. - // - // This makes way more sense when you're actually mixing. - // - // TODO(XXX) Revisit this logic once we move away from tempo-locked, - // infinite beatgrids because the assumption that findNthBeat(-2) always - // works will be wrong then. - - double dNewPlaypos = (dOtherBeatFraction + m_dUserOffset.getValue()) * dThisBeatLength; - if (this_near_next == other_near_next) { - dNewPlaypos += dThisPrevBeat; - } else if (this_near_next && !other_near_next) { - dNewPlaypos += dThisNextBeat; - } else { //!this_near_next && other_near_next - dThisPrevBeat = pBeats->findNthBeat(dThisPosition, -2); - dNewPlaypos += dThisPrevBeat; - } - - 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; -} - -void BpmControl::slotUpdateEngineBpm(double value) { - Q_UNUSED(value); - // Adjust playback bpm in response to a change in the rate slider. - double dRate = calcRateRatio(); - m_pEngineBpm->set(m_pLocalBpm->get() * dRate); -} - -void BpmControl::slotUpdateRateSlider(double value) { - Q_UNUSED(value); - // Adjust rate slider position to reflect change in rate range. - double localBpm = m_pLocalBpm->get(); - double rateScale = m_pRateDir->get() * m_pRateRange->get(); - if (localBpm == 0.0 || rateScale == 0.0) { - return; - } - - double dRateSlider = (m_pEngineBpm->get() / localBpm - 1.0) / rateScale; - m_pRateSlider->set(dRateSlider); -} - -// called from an engine worker thread -void BpmControl::trackLoaded(TrackPointer pNewTrack) { - if (m_pTrack) { - disconnect(m_pTrack.get(), &Track::beatsUpdated, - this, &BpmControl::slotUpdatedTrackBeats); - } - - // reset for a new track - resetSyncAdjustment(); - - if (pNewTrack) { - m_pTrack = pNewTrack; - m_pBeats = m_pTrack->getBeats(); - connect(m_pTrack.get(), &Track::beatsUpdated, - this, &BpmControl::slotUpdatedTrackBeats); - } else { - m_pTrack.reset(); - m_pBeats.clear(); - } -} - -void BpmControl::slotUpdatedTrackBeats() { - TrackPointer pTrack = m_pTrack; - if (pTrack) { - resetSyncAdjustment(); - m_pBeats = pTrack->getBeats(); - } -} - -void BpmControl::slotBeatsTranslate(double v) { - BeatsPointer pBeats = m_pBeats; - if (v > 0 && pBeats && (pBeats->getCapabilities() & Beats::BEATSCAP_TRANSLATE)) { - double currentSample = getSampleOfTrack().current; - double closestBeat = pBeats->findClosestBeat(currentSample); - int delta = currentSample - closestBeat; - if (delta % 2 != 0) { - delta--; - } - pBeats->translate(delta); - } -} - -void BpmControl::slotBeatsTranslateMatchAlignment(double v) { - BeatsPointer pBeats = m_pBeats; - if (v > 0 && pBeats && (pBeats->getCapabilities() & Beats::BEATSCAP_TRANSLATE)) { - // Must reset the user offset *before* calling getPhaseOffset(), - // otherwise it will always return 0 if master sync is active. - m_dUserOffset.setValue(0.0); - - double offset = getPhaseOffset(getSampleOfTrack().current); - pBeats->translate(-offset); - } -} - -double BpmControl::updateLocalBpm() { - double prev_local_bpm = m_pLocalBpm->get(); - double local_bpm = 0; - BeatsPointer pBeats = m_pBeats; - if (pBeats) { - local_bpm = pBeats->getBpmAroundPosition( - getSampleOfTrack().current, kLocalBpmSpan); - if (local_bpm == -1) { - local_bpm = m_pFileBpm->get(); - } - } else { - local_bpm = m_pFileBpm->get(); - } - if (local_bpm != prev_local_bpm) { - m_pLocalBpm->set(local_bpm); - slotUpdateEngineBpm(); - } - return local_bpm; -} - -double BpmControl::updateBeatDistance() { - double beat_distance = getBeatDistance(getSampleOfTrack().current); - m_pThisBeatDistance->set(beat_distance); - if (getSyncMode() == SYNC_NONE) { - m_dUserOffset.setValue(0.0); - } - return beat_distance; -} - -void BpmControl::setTargetBeatDistance(double beatDistance) { - m_dSyncTargetBeatDistance.setValue(beatDistance); -} - -void BpmControl::setInstantaneousBpm(double instantaneousBpm) { - m_dSyncInstantaneousBpm = instantaneousBpm; -} - -void BpmControl::resetSyncAdjustment() { - // Immediately edit the beat distance to reflect the new reality. - double new_distance = m_pThisBeatDistance->get() + m_dUserOffset.getValue(); - m_pThisBeatDistance->set(new_distance); - m_dUserOffset.setValue(0.0); - m_resetSyncAdjustment = true; -} - -void BpmControl::collectFeatures(GroupFeatureState* pGroupFeatures) const { - // Without a beatgrid we don't know any beat details. - SampleOfTrack sot = getSampleOfTrack(); - if (!sot.rate || !m_pBeats) { - return; - } - - // Get the current position of this deck. - double dThisPrevBeat = m_pPrevBeat->get(); - double dThisNextBeat = m_pNextBeat->get(); - double dThisBeatLength; - double dThisBeatFraction; - if (getBeatContextNoLookup(sot.current, - dThisPrevBeat, dThisNextBeat, - &dThisBeatLength, &dThisBeatFraction)) { - pGroupFeatures->has_beat_length_sec = true; - - // Note: dThisBeatLength is fractional frames count * 2 (stereo samples) - pGroupFeatures->beat_length_sec = dThisBeatLength / kSamplesPerFrame - / sot.rate / calcRateRatio(); - - pGroupFeatures->has_beat_fraction = true; - pGroupFeatures->beat_fraction = dThisBeatFraction; - } -} - -double BpmControl::calcRateRatio() const { - return std::max(1e-6, - 1.0 + m_pRateDir->get() * m_pRateRange->get() * m_pRateSlider->get()); -} diff --git a/src/engine/bpmcontrol.h b/src/engine/bpmcontrol.h deleted file mode 100644 index a715179ee0..0000000000 --- a/src/engine/bpmcontrol.h +++ /dev/null @@ -1,169 +0,0 @@ -#ifndef BPMCONTROL_H -#define BPMCONTROL_H - -#include - -#include "control/controlobject.h" -#include "engine/enginecontrol.h" -#include "engine/sync/syncable.h" -#include "util/tapfilter.h" - -class ControlObject; -class ControlLinPotmeter; -class ControlProxy; -class ControlPushButton; -class EngineBuffer; -class SyncControl; - -class BpmControl : public EngineControl { - Q_OBJECT - - public: - BpmControl(QString group, UserSettingsPointer pConfig); - ~BpmControl() override; - - double getBpm() const; - double getLocalBpm() const { return m_pLocalBpm ? m_pLocalBpm->get() : 0.0; } - // When in master sync mode, ratecontrol calls calcSyncedRate to figure out - // how fast the track should play back. The returned rate is usually just - // the correct pitch to match bpms. The usertweak argument represents - // how much the user is nudging the pitch to get two tracks into sync, and - // that value is added to the rate by bpmcontrol. The rate may be - // further adjusted if bpmcontrol discovers that the tracks have fallen - // out of sync. - double calcSyncedRate(double userTweak); - // Get the phase offset from the specified position. - double getNearestPositionInPhase(double dThisPosition, bool respectLoops, bool playing); - double getPhaseOffset(double dThisPosition); - double getBeatDistance(double dThisPosition) const; - - void setTargetBeatDistance(double beatDistance); - void setInstantaneousBpm(double instantaneousBpm); - void resetSyncAdjustment(); - double updateLocalBpm(); - double updateBeatDistance(); - - void collectFeatures(GroupFeatureState* pGroupFeatures) const; - - // Calculates contextual information about beats: the previous beat, the - // next beat, the current beat length, and the beat ratio (how far dPosition - // lies within the current beat). Returns false if a previous or next beat - // does not exist. NULL arguments are safe and ignored. - static bool getBeatContext(const BeatsPointer& pBeats, - const double dPosition, - double* dpPrevBeat, - double* dpNextBeat, - double* dpBeatLength, - double* dpBeatPercentage); - - // Alternative version that works if the next and previous beat positions - // are already known. - static bool getBeatContextNoLookup( - const double dPosition, - const double dPrevBeat, - const double dNextBeat, - double* dpBeatLength, - double* dpBeatPercentage); - - // Returns the shortest change in percentage needed to achieve - // target_percentage. - // Example: shortestPercentageChange(0.99, 0.01) == 0.02 - static double shortestPercentageChange(const double& current_percentage, - const double& target_percentage); - void trackLoaded(TrackPointer pNewTrack) override; - - private slots: - void slotFileBpmChanged(double); - void slotAdjustBeatsFaster(double); - void slotAdjustBeatsSlower(double); - void slotTranslateBeatsEarlier(double); - void slotTranslateBeatsLater(double); - void slotControlBeatSync(double); - void slotControlBeatSyncPhase(double); - void slotControlBeatSyncTempo(double); - void slotTapFilter(double,int); - void slotBpmTap(double); - void slotUpdateRateSlider(double v = 0.0); - void slotUpdateEngineBpm(double v = 0.0); - void slotUpdatedTrackBeats(); - void slotBeatsTranslate(double); - void slotBeatsTranslateMatchAlignment(double); - - private: - SyncMode getSyncMode() const { - return syncModeFromDouble(m_pSyncMode->get()); - } - bool syncTempo(); - double calcSyncAdjustment(double my_percentage, bool userTweakingSync); - double calcRateRatio() const; - - friend class SyncControl; - - // ControlObjects that come from EngineBuffer - ControlProxy* m_pPlayButton; - QAtomicInt m_oldPlayButton; - ControlProxy* m_pReverseButton; - ControlProxy* m_pRateSlider; - ControlObject* m_pQuantize; - ControlProxy* m_pRateRange; - ControlProxy* m_pRateDir; - - // ControlObjects that come from QuantizeControl - QScopedPointer m_pNextBeat; - QScopedPointer m_pPrevBeat; - QScopedPointer m_pClosestBeat; - - // ControlObjects that come from LoopingControl - ControlProxy* m_pLoopEnabled; - ControlProxy* m_pLoopStartPosition; - ControlProxy* m_pLoopEndPosition; - - // The current loaded file's detected BPM - ControlObject* m_pFileBpm; - // The average bpm around the current playposition; - ControlObject* m_pLocalBpm; - ControlPushButton* m_pAdjustBeatsFaster; - ControlPushButton* m_pAdjustBeatsSlower; - ControlPushButton* m_pTranslateBeatsEarlier; - ControlPushButton* m_pTranslateBeatsLater; - - // The current effective BPM of the engine - ControlLinPotmeter* m_pEngineBpm; - - // Used for bpm tapping from GUI and MIDI - ControlPushButton* m_pButtonTap; - - // Button for sync'ing with the other EngineBuffer - ControlPushButton* m_pButtonSync; - ControlPushButton* m_pButtonSyncPhase; - ControlPushButton* m_pButtonSyncTempo; - - // Button that translates the beats so the nearest beat is on the current - // playposition. - ControlPushButton* m_pTranslateBeats; - // Button that translates beats to match another playing deck - ControlPushButton* m_pBeatsTranslateMatchAlignment; - - ControlProxy* m_pThisBeatDistance; - ControlValueAtomic m_dSyncTargetBeatDistance; - ControlValueAtomic m_dUserOffset; - QAtomicInt m_resetSyncAdjustment; - ControlProxy* m_pSyncMode; - - TapFilter m_tapFilter; // threadsave - - // used in the engine thread only - double m_dSyncInstantaneousBpm; - double m_dLastSyncAdjustment; - - // objects below are written from an engine worker thread - TrackPointer m_pTrack; - BeatsPointer m_pBeats; - - const QString m_sGroup; - - FRIEND_TEST(EngineSyncTest, UserTweakBeatDistance); -}; - - -#endif // BPMCONTROL_H diff --git a/src/engine/clockcontrol.cpp b/src/engine/clockcontrol.cpp deleted file mode 100644 index b5ed799467..0000000000 --- a/src/engine/clockcontrol.cpp +++ /dev/null @@ -1,68 +0,0 @@ -#include "engine/clockcontrol.h" - -#include "control/controlobject.h" -#include "preferences/usersettings.h" -#include "engine/enginecontrol.h" -#include "control/controlproxy.h" - -ClockControl::ClockControl(QString group, UserSettingsPointer pConfig) - : EngineControl(group, pConfig) { - m_pCOBeatActive = new ControlObject(ConfigKey(group, "beat_active")); - m_pCOBeatActive->set(0.0); - m_pCOSampleRate = new ControlProxy("[Master]","samplerate"); -} - -ClockControl::~ClockControl() { - delete m_pCOBeatActive; - delete m_pCOSampleRate; -} - -// called from an engine worker thread -void ClockControl::trackLoaded(TrackPointer pNewTrack) { - // Clear on-beat control - m_pCOBeatActive->set(0.0); - - // Disconnect any previously loaded track/beats - if (m_pTrack) { - disconnect(m_pTrack.get(), &Track::beatsUpdated, - this, &ClockControl::slotBeatsUpdated); - } - if (pNewTrack) { - m_pTrack = pNewTrack; - m_pBeats = m_pTrack->getBeats(); - connect(m_pTrack.get(), &Track::beatsUpdated, - this, &ClockControl::slotBeatsUpdated); - } else { - m_pBeats.clear(); - m_pTrack.reset(); - } - -} - -void ClockControl::slotBeatsUpdated() { - TrackPointer pTrack = m_pTrack; - if(pTrack) { - m_pBeats = pTrack->getBeats(); - } -} - -void ClockControl::process(const double dRate, - const double currentSample, - const int iBuffersize) { - Q_UNUSED(iBuffersize); - double samplerate = m_pCOSampleRate->get(); - - // TODO(XXX) should this be customizable, or latency dependent? - const double blinkSeconds = 0.100; - - // Multiply by two to get samples from frames. Interval is scaled linearly - // by the rate. - const double blinkIntervalSamples = 2.0 * samplerate * (1.0 * dRate) * blinkSeconds; - - BeatsPointer pBeats = m_pBeats; - if (pBeats) { - double closestBeat = pBeats->findClosestBeat(currentSample); - double distanceToClosestBeat = fabs(currentSample - closestBeat); - m_pCOBeatActive->set(distanceToClosestBeat < blinkIntervalSamples / 2.0); - } -} diff --git a/src/engine/clockcontrol.h b/src/engine/clockcontrol.h deleted file mode 100644 index 132edd454b..0000000000 --- a/src/engine/clockcontrol.h +++ /dev/null @@ -1,37 +0,0 @@ -#ifndef CLOCKCONTROL_H -#define CLOCKCONTROL_H - -#include "preferences/usersettings.h" -#include "engine/enginecontrol.h" - -#include "track/track.h" -#include "track/beats.h" - -class ControlProxy; -class ControlObject; - -class ClockControl: public EngineControl { - Q_OBJECT - public: - ClockControl(QString group, - UserSettingsPointer pConfig); - - ~ClockControl() override; - - void process(const double dRate, const double currentSample, - const int iBufferSize) override; - - public slots: - void trackLoaded(TrackPointer pNewTrack) override; - void slotBeatsUpdated(); - - private: - ControlObject* m_pCOBeatActive; - ControlProxy* m_pCOSampleRate; - - // objects below are written from an engine worker thread - TrackPointer m_pTrack; - BeatsPointer m_pBeats; -}; - -#endif /* CLOCKCONTROL_H */ diff --git a/src/engine/controls/bpmcontrol.cpp b/src/engine/controls/bpmcontrol.cpp new file mode 100644 index 0000000000..87d4278b6a --- /dev/null +++ b/src/engine/controls/bpmcontrol.cpp @@ -0,0 +1,884 @@ +#include + +#include "control/controlobject.h" +#include "control/controlpushbutton.h" +#include "control/controllinpotmeter.h" + +#include "engine/enginebuffer.h" +#include "engine/controls/bpmcontrol.h" +#include "waveform/visualplayposition.h" +#include "engine/enginechannel.h" +#include "engine/enginemaster.h" +#include "control/controlproxy.h" +#include "util/assert.h" +#include "util/math.h" +#include "util/duration.h" + +namespace { +const int kMinBpm = 30; +// Maximum allowed interval between beats (calculated from kMinBpm). +const mixxx::Duration kMaxInterval = mixxx::Duration::fromMillis(1000.0 * (60.0 / kMinBpm)); +const int kFilterLength = 5; +// The local_bpm is calculated forward and backward this number of beats, so +// the actual number of beats is this x2. +const int kLocalBpmSpan = 4; +const SINT kSamplesPerFrame = 2; +} + +BpmControl::BpmControl(QString group, + UserSettingsPointer pConfig) + : EngineControl(group, pConfig), + m_tapFilter(this, kFilterLength, kMaxInterval), + m_dSyncInstantaneousBpm(0.0), + m_dLastSyncAdjustment(1.0), + m_sGroup(group) { + m_dSyncTargetBeatDistance.setValue(0.0); + m_dUserOffset.setValue(0.0); + + m_pPlayButton = new ControlProxy(group, "play", this); + m_pReverseButton = new ControlProxy(group, "reverse", this); + m_pRateSlider = new ControlProxy(group, "rate", this); + m_pRateSlider->connectValueChanged(this, &BpmControl::slotUpdateEngineBpm, + Qt::DirectConnection); + m_pQuantize = ControlObject::getControl(group, "quantize"); + m_pRateRange = new ControlProxy(group, "rateRange", this); + m_pRateRange->connectValueChanged(this, &BpmControl::slotUpdateRateSlider, + Qt::DirectConnection); + m_pRateDir = new ControlProxy(group, "rate_dir", this); + m_pRateDir->connectValueChanged(this, &BpmControl::slotUpdateEngineBpm, + Qt::DirectConnection); + + m_pPrevBeat.reset(new ControlProxy(group, "beat_prev")); + m_pNextBeat.reset(new ControlProxy(group, "beat_next")); + m_pClosestBeat.reset(new ControlProxy(group, "beat_closest")); + + m_pLoopEnabled = new ControlProxy(group, "loop_enabled", this); + m_pLoopStartPosition = new ControlProxy(group, "loop_start_position", this); + m_pLoopEndPosition = new ControlProxy(group, "loop_end_position", this); + + m_pFileBpm = new ControlObject(ConfigKey(group, "file_bpm")); + connect(m_pFileBpm, &ControlObject::valueChanged, + this, &BpmControl::slotFileBpmChanged, + Qt::DirectConnection); + m_pLocalBpm = new ControlObject(ConfigKey(group, "local_bpm")); + m_pAdjustBeatsFaster = new ControlPushButton(ConfigKey(group, "beats_adjust_faster"), false); + connect(m_pAdjustBeatsFaster, &ControlObject::valueChanged, + this, &BpmControl::slotAdjustBeatsFaster, + Qt::DirectConnection); + m_pAdjustBeatsSlower = new ControlPushButton(ConfigKey(group, "beats_adjust_slower"), false); + connect(m_pAdjustBeatsSlower, &ControlObject::valueChanged, + this, &BpmControl::slotAdjustBeatsSlower, + Qt::DirectConnection); + m_pTranslateBeatsEarlier = new ControlPushButton(ConfigKey(group, "beats_translate_earlier"), false); + connect(m_pTranslateBeatsEarlier, &ControlObject::valueChanged, + this, &BpmControl::slotTranslateBeatsEarlier, + Qt::DirectConnection); + m_pTranslateBeatsLater = new ControlPushButton(ConfigKey(group, "beats_translate_later"), false); + connect(m_pTranslateBeatsLater, &ControlObject::valueChanged, + this, &BpmControl::slotTranslateBeatsLater, + Qt::DirectConnection); + + // Pick a wide range (1 to 200) and allow out of bounds sets. This lets you + // map a soft-takeover MIDI knob to the BPM. This also creates bpm_up and + // bpm_down controls. + // bpm_up / bpm_down steps by 1 + // bpm_up_small / bpm_down_small steps by 0.1 + m_pEngineBpm = new ControlLinPotmeter(ConfigKey(group, "bpm"), 1, 200, 1, 0.1, true); + connect(m_pEngineBpm, &ControlObject::valueChanged, + this, &BpmControl::slotUpdateRateSlider, + Qt::DirectConnection); + + m_pButtonTap = new ControlPushButton(ConfigKey(group, "bpm_tap")); + connect(m_pButtonTap, &ControlObject::valueChanged, + this, &BpmControl::slotBpmTap, + Qt::DirectConnection); + + // Beat sync (scale buffer tempo relative to tempo of other buffer) + m_pButtonSync = new ControlPushButton(ConfigKey(group, "beatsync")); + connect(m_pButtonSync, &ControlObject::valueChanged, + this, &BpmControl::slotControlBeatSync, + Qt::DirectConnection); + + m_pButtonSyncPhase = new ControlPushButton(ConfigKey(group, "beatsync_phase")); + connect(m_pButtonSyncPhase, &ControlObject::valueChanged, + this, &BpmControl::slotControlBeatSyncPhase, + Qt::DirectConnection); + + m_pButtonSyncTempo = new ControlPushButton(ConfigKey(group, "beatsync_tempo")); + connect(m_pButtonSyncTempo, &ControlObject::valueChanged, + this, &BpmControl::slotControlBeatSyncTempo, + Qt::DirectConnection); + + m_pTranslateBeats = new ControlPushButton(ConfigKey(group, "beats_translate_curpos")); + connect(m_pTranslateBeats, &ControlObject::valueChanged, + this, &BpmControl::slotBeatsTranslate, + Qt::DirectConnection); + + m_pBeatsTranslateMatchAlignment = new ControlPushButton(ConfigKey(group, "beats_translate_match_alignment")); + connect(m_pBeatsTranslateMatchAlignment, &ControlObject::valueChanged, + this, &BpmControl::slotBeatsTranslateMatchAlignment, + Qt::DirectConnection); + + connect(&m_tapFilter, &TapFilter::tapped, + this, &BpmControl::slotTapFilter, + Qt::DirectConnection); + + // Measures distance from last beat in percentage: 0.5 = half-beat away. + m_pThisBeatDistance = new ControlProxy(group, "beat_distance", this); + m_pSyncMode = new ControlProxy(group, "sync_mode", this); +} + +BpmControl::~BpmControl() { + delete m_pFileBpm; + delete m_pLocalBpm; + delete m_pEngineBpm; + delete m_pButtonTap; + delete m_pButtonSync; + delete m_pButtonSyncPhase; + delete m_pButtonSyncTempo; + delete m_pTranslateBeats; + delete m_pBeatsTranslateMatchAlignment; + delete m_pTranslateBeatsEarlier; + delete m_pTranslateBeatsLater; + delete m_pAdjustBeatsFaster; + delete m_pAdjustBeatsSlower; +} + +double BpmControl::getBpm() const { + return m_pEngineBpm->get(); +} + +void BpmControl::slotFileBpmChanged(double bpm) { + Q_UNUSED(bpm); + // Adjust the file-bpm with the current setting of the rate to get the + // engine BPM. We only do this for SYNC_NONE decks because EngineSync will + // set our BPM if the file BPM changes. See SyncControl::fileBpmChanged(). + BeatsPointer pBeats = m_pBeats; + if (pBeats) { + const double beats_bpm = + pBeats->getBpmAroundPosition( + getSampleOfTrack().current, kLocalBpmSpan); + if (beats_bpm != -1) { + m_pLocalBpm->set(beats_bpm); + } else { + m_pLocalBpm->set(bpm); + } + } else { + m_pLocalBpm->set(bpm); + } + if (getSyncMode() == SYNC_NONE) { + slotUpdateEngineBpm(); + } + resetSyncAdjustment(); +} + +void BpmControl::slotAdjustBeatsFaster(double v) { + BeatsPointer pBeats = m_pBeats; + if (v > 0 && pBeats && (pBeats->getCapabilities() & Beats::BEATSCAP_SETBPM)) { + double new_bpm = math_min(200.0, pBeats->getBpm() + .01); + pBeats->setBpm(new_bpm); + } +} + +void BpmControl::slotAdjustBeatsSlower(double v) { + BeatsPointer pBeats = m_pBeats; + if (v > 0 && pBeats && (pBeats->getCapabilities() & Beats::BEATSCAP_SETBPM)) { + double new_bpm = math_max(10.0, pBeats->getBpm() - .01); + pBeats->setBpm(new_bpm); + } +} + +void BpmControl::slotTranslateBeatsEarlier(double v) { + BeatsPointer pBeats = m_pBeats; + if (v > 0 && pBeats && + (pBeats->getCapabilities() & Beats::BEATSCAP_TRANSLATE)) { + const int translate_dist = getSampleOfTrack().rate * -.01; + pBeats->translate(translate_dist); + } +} + +void BpmControl::slotTranslateBeatsLater(double v) { + BeatsPointer pBeats = m_pBeats; + if (v > 0 && pBeats && + (pBeats->getCapabilities() & Beats::BEATSCAP_TRANSLATE)) { + // TODO(rryan): Track::getSampleRate is possibly inaccurate! + const int translate_dist = getSampleOfTrack().rate * .01; + pBeats->translate(translate_dist); + } +} + +void BpmControl::slotBpmTap(double v) { + if (v > 0) { + m_tapFilter.tap(); + } +} + +void BpmControl::slotTapFilter(double averageLength, int numSamples) { + // averageLength is the average interval in milliseconds tapped over + // numSamples samples. Have to convert to BPM now: + + if (averageLength <= 0) + return; + + if (numSamples < 4) + return; + + BeatsPointer pBeats = m_pBeats; + if (!pBeats) + return; + + // (60 seconds per minute) * (1000 milliseconds per second) / (X millis per + // beat) = Y beats/minute + double averageBpm = 60.0 * 1000.0 / averageLength / calcRateRatio(); + pBeats->setBpm(averageBpm); +} + +void BpmControl::slotControlBeatSyncPhase(double v) { + if (!v) return; + getEngineBuffer()->requestSyncPhase(); +} + +void BpmControl::slotControlBeatSyncTempo(double v) { + if (!v) return; + syncTempo(); +} + +void BpmControl::slotControlBeatSync(double v) { + if (!v) return; + if (!syncTempo()) { + // syncTempo failed, nothing else to do + return; + } + + // Also sync phase if quantize is enabled. + // this is used from controller scripts, where the latching behaviour of + // the sync_enable CO cannot be used + if (m_pPlayButton->toBool() && m_pQuantize->toBool()) { + getEngineBuffer()->requestSyncPhase(); + } +} + +bool BpmControl::syncTempo() { + EngineBuffer* pOtherEngineBuffer = pickSyncTarget(); + + if (!pOtherEngineBuffer) { + return false; + } + + double fThisBpm = m_pEngineBpm->get(); + double fThisLocalBpm = m_pLocalBpm->get(); + + double fOtherBpm = pOtherEngineBuffer->getBpm(); + double fOtherLocalBpm = pOtherEngineBuffer->getLocalBpm(); + + //qDebug() << "this" << "bpm" << fThisBpm << "filebpm" << fThisFileBpm; + //qDebug() << "other" << "bpm" << fOtherBpm << "filebpm" << fOtherFileBpm; + + //////////////////////////////////////////////////////////////////////////// + // Rough proof of how syncing works -- rryan 3/2011 + // ------------------------------------------------ + // + // Let this and other denote this deck versus the sync-target deck. + // + // The goal is for this deck's effective BPM to equal the other decks. + // + // thisBpm = otherBpm + // + // The overall rate is the product of range, direction, and scale plus 1: + // + // rate = 1.0 + rateDir * rateRange * rateScale + // + // An effective BPM is the file-bpm times the rate: + // + // bpm = fileBpm * rate + // + // So our goal is to tweak thisRate such that this equation is true: + // + // thisFileBpm * (1.0 + thisRate) = otherFileBpm * (1.0 + otherRate) + // + // so rearrange this equation in terms of thisRate: + // + // thisRate = (otherFileBpm * (1.0 + otherRate)) / thisFileBpm - 1.0 + // + // So the new rateScale to set is: + // + // thisRateScale = ((otherFileBpm * (1.0 + otherRate)) / thisFileBpm - 1.0) / (thisRateDir * thisRateRange) + + if (fOtherBpm > 0.0 && fThisBpm > 0.0) { + // The desired rate is the other de