diff --git a/CMakeLists.txt b/CMakeLists.txt index a444543ce47..40cdb2a4d02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -713,8 +713,6 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/sources/soundsourcesndfile.cpp src/track/albuminfo.cpp src/track/beatfactory.cpp - src/track/beatgrid.cpp - src/track/beatmap.cpp src/track/beats.cpp src/track/beatutils.cpp src/track/bpm.cpp @@ -1223,8 +1221,7 @@ add_executable(mixxx-test src/test/audiotaperpot_test.cpp src/test/autodjprocessor_test.cpp src/test/baseeffecttest.cpp - src/test/beatgridtest.cpp - src/test/beatmaptest.cpp + src/test/beatstest.cpp src/test/beatstranslatetest.cpp src/test/bpmcontrol_test.cpp src/test/broadcastprofile_test.cpp diff --git a/build/depends.py b/build/depends.py index 8fe2422d0d6..c282fc7a868 100644 --- a/build/depends.py +++ b/build/depends.py @@ -1222,8 +1222,6 @@ def sources(self, build): "src/skin/launchimage.cpp", "src/track/beatfactory.cpp", - "src/track/beatgrid.cpp", - "src/track/beatmap.cpp", "src/track/beatutils.cpp", "src/track/beats.cpp", "src/track/bpm.cpp", diff --git a/res/skins/Tango/buttons/btn_beats_curpos.svg b/res/skins/Tango/buttons/btn_beats_curpos.svg index 6dabfab5a4f..98de1217ec6 100644 --- a/res/skins/Tango/buttons/btn_beats_curpos.svg +++ b/res/skins/Tango/buttons/btn_beats_curpos.svg @@ -1,2 +1,7 @@ - + + + diff --git a/src/analyzer/analyzerbeats.cpp b/src/analyzer/analyzerbeats.cpp index cdbdbf50128..96f9368abb4 100644 --- a/src/analyzer/analyzerbeats.cpp +++ b/src/analyzer/analyzerbeats.cpp @@ -9,7 +9,7 @@ #include "analyzer/plugins/analyzerqueenmarybeats.h" #include "analyzer/plugins/analyzersoundtouchbeats.h" #include "track/beatfactory.h" -#include "track/beatmap.h" +#include "track/beats.h" #include "track/beatutils.h" #include "track/track.h" @@ -43,7 +43,7 @@ AnalyzerBeats::AnalyzerBeats(UserSettingsPointer pConfig, bool enforceBpmDetecti m_iMaxBpm(9999) { } -bool AnalyzerBeats::initialize(TrackPointer tio, int sampleRate, int totalSamples) { +bool AnalyzerBeats::initialize(TrackPointer pTrack, int sampleRate, int totalSamples) { if (totalSamples == 0) { return false; } @@ -55,7 +55,7 @@ bool AnalyzerBeats::initialize(TrackPointer tio, int sampleRate, int totalSample return false; } - bool bpmLock = tio->isBpmLocked(); + bool bpmLock = pTrack->isBpmLocked(); if (bpmLock) { qDebug() << "Track is BpmLocked: Beat calculation will not start"; return false; @@ -101,8 +101,7 @@ bool AnalyzerBeats::initialize(TrackPointer tio, int sampleRate, int totalSample m_iCurrentSample = 0; // if we can load a stored track don't reanalyze it - bool bShouldAnalyze = shouldAnalyze(tio); - + bool bShouldAnalyze = shouldAnalyze(pTrack); DEBUG_ASSERT(!m_pPlugin); if (bShouldAnalyze) { @@ -131,11 +130,11 @@ bool AnalyzerBeats::initialize(TrackPointer tio, int sampleRate, int totalSample return bShouldAnalyze; } -bool AnalyzerBeats::shouldAnalyze(TrackPointer tio) const { +bool AnalyzerBeats::shouldAnalyze(TrackPointer pTrack) const { int iMinBpm = m_bpmSettings.getBpmRangeStart(); int iMaxBpm = m_bpmSettings.getBpmRangeEnd(); - bool bpmLock = tio->isBpmLocked(); + bool bpmLock = pTrack->isBpmLocked(); if (bpmLock) { qDebug() << "Track is BpmLocked: Beat calculation will not start"; return false; @@ -148,11 +147,11 @@ bool AnalyzerBeats::shouldAnalyze(TrackPointer tio) const { // If the track already has a Beats object then we need to decide whether to // analyze this track or not. - mixxx::BeatsPointer pBeats = tio->getBeats(); + mixxx::BeatsPointer pBeats = pTrack->getBeats(); if (!pBeats) { return true; } - if (!mixxx::Bpm::isValidValue(pBeats->getBpm())) { + if (!mixxx::Bpm::isValidValue(pBeats->getBpm().getValue())) { // Tracks with an invalid bpm <= 0 should be re-analyzed, // independent of the preference settings. We expect that // all tracks have a bpm > 0 when analyzed. Users that want @@ -161,7 +160,7 @@ bool AnalyzerBeats::shouldAnalyze(TrackPointer tio) const { qDebug() << "Re-analyzing track with invalid BPM despite preference settings."; return true; } - if (pBeats->findNextBeat(0) <= 0.0) { + if (pBeats->findNextBeat(mixxx::FramePos(0)) <= mixxx::FramePos(0.0)) { qDebug() << "First beat is 0 for grid so analyzing track to find first beat."; return true; } @@ -188,8 +187,8 @@ bool AnalyzerBeats::shouldAnalyze(TrackPointer tio) const { // Beat grid exists but version and settings differ if (!m_bPreferencesReanalyzeOldBpm) { qDebug() << "Beat calculation skips analyzing because the track has" - << "a BPM computed by a previous Mixxx version and user" - << "preferences indicate we should not change it."; + << "a BPM computed by a previous Mixxx version and user" + << "preferences indicate we should not change it."; return false; } @@ -213,7 +212,7 @@ void AnalyzerBeats::cleanup() { m_pPlugin.reset(); } -void AnalyzerBeats::storeResults(TrackPointer tio) { +void AnalyzerBeats::storeResults(TrackPointer pTrack) { VERIFY_OR_DEBUG_ASSERT(m_pPlugin) { return; } @@ -229,57 +228,58 @@ void AnalyzerBeats::storeResults(TrackPointer tio) { QHash extraVersionInfo = getExtraVersionInfo( m_pluginId, m_bPreferencesFastAnalysis); pBeats = BeatFactory::makePreferredBeats( - *tio, + pTrack, beats, extraVersionInfo, m_bPreferencesFixedTempo, m_bPreferencesOffsetCorrection, - m_iSampleRate, m_iTotalSamples, m_iMinBpm, m_iMaxBpm); qDebug() << "AnalyzerBeats plugin detected" << beats.size() - << "beats. Average BPM:" << (pBeats ? pBeats->getBpm() : 0.0); + << "beats. Average BPM:" << (pBeats ? pBeats->getBpm().getValue() : 0.0); } else { - float bpm = m_pPlugin->getBpm(); + mixxx::Bpm bpm = mixxx::Bpm(m_pPlugin->getBpm()); qDebug() << "AnalyzerBeats plugin detected constant BPM: " << bpm; - pBeats = BeatFactory::makeBeatGrid(*tio, bpm, 0.0f); + pBeats = std::make_shared(pTrack.get()); + pBeats->setGrid(bpm); + pTrack->setBeats(pBeats); } - mixxx::BeatsPointer pCurrentBeats = tio->getBeats(); + mixxx::BeatsPointer pCurrentBeats = pTrack->getBeats(); // If the track has no beats object then set our newly generated one // regardless of beat lock. if (!pCurrentBeats) { - tio->setBeats(pBeats); + pTrack->setBeats(pBeats); return; } // If the track received the beat lock while we were analyzing it then we // abort setting it. - if (tio->isBpmLocked()) { + if (pTrack->isBpmLocked()) { qDebug() << "Track was BPM-locked as we were analyzing it. Aborting analysis."; return; } // If the user prefers to replace old beatgrids with newly generated ones or // the old beatgrid has 0-bpm then we replace it. - bool zeroCurrentBpm = pCurrentBeats->getBpm() == 0.0; + bool zeroCurrentBpm = pCurrentBeats->getBpm().getValue() == 0.0; if (m_bPreferencesReanalyzeOldBpm || zeroCurrentBpm) { if (zeroCurrentBpm) { qDebug() << "Replacing 0-BPM beatgrid with a" << pBeats->getBpm() << "beatgrid."; } - tio->setBeats(pBeats); + pTrack->setBeats(pBeats); return; } // If we got here then the user doesn't want to replace the beatgrid but // since the first beat is zero we'll apply the offset we just detected. - double currentFirstBeat = pCurrentBeats->findNextBeat(0); - double newFirstBeat = pBeats->findNextBeat(0); - if (currentFirstBeat == 0.0 && newFirstBeat > 0) { - pCurrentBeats->translate(newFirstBeat); + mixxx::FramePos currentFirstBeat = pCurrentBeats->findNextBeat(mixxx::FramePos(0)); + mixxx::FramePos newFirstBeat = pBeats->findNextBeat(mixxx::FramePos(0)); + if (currentFirstBeat == mixxx::FramePos(0.0) && newFirstBeat > mixxx::FramePos(0)) { + pCurrentBeats->translate(newFirstBeat - currentFirstBeat); } } diff --git a/src/analyzer/analyzerbeats.h b/src/analyzer/analyzerbeats.h index 5dbb9a7f5f4..a82b7966900 100644 --- a/src/analyzer/analyzerbeats.h +++ b/src/analyzer/analyzerbeats.h @@ -27,13 +27,13 @@ class AnalyzerBeats : public Analyzer { static QList availablePlugins(); static mixxx::AnalyzerPluginInfo defaultPlugin(); - bool initialize(TrackPointer tio, int sampleRate, int totalSamples) override; + bool initialize(TrackPointer pTrack, int sampleRate, int totalSamples) override; bool processSamples(const CSAMPLE *pIn, const int iLen) override; - void storeResults(TrackPointer tio) override; + void storeResults(TrackPointer pTrack) override; void cleanup() override; private: - bool shouldAnalyze(TrackPointer tio) const; + bool shouldAnalyze(TrackPointer pTrack) const; static QHash getExtraVersionInfo( QString pluginId, bool bPreferencesFastAnalysis); diff --git a/src/controllers/controlpickermenu.cpp b/src/controllers/controlpickermenu.cpp index 6ddfd3e6ef2..75ed45c58ca 100644 --- a/src/controllers/controlpickermenu.cpp +++ b/src/controllers/controlpickermenu.cpp @@ -27,8 +27,18 @@ ControlPickerMenu::ControlPickerMenu(QWidget* pParent) // Master Controls QMenu* mixerMenu = addSubmenu(tr("Mixer")); - addControl("[Master]", "balance", tr("Main Output Balance"), tr("Main output balance"), mixerMenu, true); - addControl("[Master]", "delay", tr("Main Output Delay"), tr("Main output delay"), mixerMenu, true); + addControl("[Master]", + "balance", + tr("Main Output Balance"), + tr("Main output balance"), + mixerMenu, + true); + addControl("[Master]", + "delay", + tr("Main Output Delay"), + tr("Main output delay"), + mixerMenu, + true); addControl("[Master]", "crossfader", tr("Crossfader"), tr("Crossfader"), mixerMenu, true); addControl("[Master]", "gain", tr("Main Output Gain"), tr("Main output gain"), mixerMenu, true); addControl("[Master]", "headGain", tr("Headphone Gain"), tr("Headphone gain"), mixerMenu, true); diff --git a/src/engine/controls/bpmcontrol.cpp b/src/engine/controls/bpmcontrol.cpp index c2baca7c263..284f24a8d8c 100644 --- a/src/engine/controls/bpmcontrol.cpp +++ b/src/engine/controls/bpmcontrol.cpp @@ -2,15 +2,14 @@ #include -#include "control/controllinpotmeter.h" #include "control/controlobject.h" #include "control/controlproxy.h" -#include "control/controlpushbutton.h" #include "engine/channels/enginechannel.h" #include "engine/enginebuffer.h" #include "engine/enginemaster.h" #include "util/assert.h" #include "util/duration.h" +#include "util/frameadapter.h" #include "util/logger.h" #include "util/math.h" #include "waveform/visualplayposition.h" @@ -35,7 +34,7 @@ constexpr int kBpmTapFilterLength = 5; // The local_bpm is calculated forward and backward this number of beats, so // the actual number of beats is this x2. constexpr int kLocalBpmSpan = 4; -constexpr SINT kSamplesPerFrame = 2; +constexpr double kSlightBeatsTranslateFactor = 0.01; } BpmControl::BpmControl(QString group, @@ -48,37 +47,49 @@ BpmControl::BpmControl(QString 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_pRateRatio = new ControlProxy(group, "rate_ratio", this); + m_pPlayButton = make_parented(group, "play", this); + m_pReverseButton = make_parented(group, "reverse", this); + m_pRateRatio = make_parented(group, "rate_ratio", this); m_pRateRatio->connectValueChanged(this, &BpmControl::slotUpdateEngineBpm, Qt::DirectConnection); m_pQuantize = ControlObject::getControl(group, "quantize"); - m_pPrevBeat.reset(new ControlProxy(group, "beat_prev")); - m_pNextBeat.reset(new ControlProxy(group, "beat_next")); + m_pPrevBeat = make_parented(group, "beat_prev", this); + m_pNextBeat = make_parented(group, "beat_next", this); - 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_pLoopEnabled = make_parented(group, "loop_enabled", this); + m_pLoopStartPosition = make_parented(group, "loop_start_position", this); + m_pLoopEndPosition = make_parented(group, "loop_end_position", this); - 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, + m_pLocalBpm = std::make_unique(ConfigKey(group, "local_bpm")); + m_pAdjustBeatsFaster = std::make_unique( + ConfigKey(group, "beats_adjust_faster"), false); + connect(m_pAdjustBeatsFaster.get(), + &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, + m_pAdjustBeatsSlower = std::make_unique( + ConfigKey(group, "beats_adjust_slower"), false); + connect(m_pAdjustBeatsSlower.get(), + &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, + m_pTranslateBeatsEarlier = std::make_unique( + ConfigKey(group, "beats_translate_earlier"), false); + connect(m_pTranslateBeatsEarlier.get(), + &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, + m_pTranslateBeatsLater = std::make_unique( + ConfigKey(group, "beats_translate_later"), false); + connect(m_pTranslateBeatsLater.get(), + &ControlObject::valueChanged, + this, + &BpmControl::slotTranslateBeatsLater, Qt::DirectConnection); // Pick a wide range (kBpmRangeMin to kBpmRangeMax) and allow out of bounds sets. This lets you @@ -86,71 +97,78 @@ BpmControl::BpmControl(QString group, // bpm_down controls. // bpm_up / bpm_down steps by kBpmRangeStep // bpm_up_small / bpm_down_small steps by kBpmRangeSmallStep - m_pEngineBpm = new ControlLinPotmeter( - ConfigKey(group, "bpm"), + m_pEngineBpm = std::make_unique(ConfigKey(group, "bpm"), kBpmRangeMin, kBpmRangeMax, kBpmRangeStep, kBpmRangeSmallStep, true); m_pEngineBpm->set(0.0); - connect(m_pEngineBpm, &ControlObject::valueChanged, - this, &BpmControl::slotUpdateRateSlider, + connect(m_pEngineBpm.get(), + &ControlObject::valueChanged, + this, + &BpmControl::slotUpdateRateSlider, Qt::DirectConnection); - m_pButtonTap = new ControlPushButton(ConfigKey(group, "bpm_tap")); - connect(m_pButtonTap, &ControlObject::valueChanged, - this, &BpmControl::slotBpmTap, + m_pButtonTap = + std::make_unique(ConfigKey(group, "bpm_tap")); + connect(m_pButtonTap.get(), + &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, + m_pButtonSync = + std::make_unique(ConfigKey(group, "beatsync")); + connect(m_pButtonSync.get(), + &ControlObject::valueChanged, + this, + &BpmControl::slotControlBeatSync, Qt::DirectConnection); - m_pButtonSyncPhase = new ControlPushButton(ConfigKey(group, "beatsync_phase")); - connect(m_pButtonSyncPhase, &ControlObject::valueChanged, - this, &BpmControl::slotControlBeatSyncPhase, + m_pButtonSyncPhase = std::make_unique( + ConfigKey(group, "beatsync_phase")); + connect(m_pButtonSyncPhase.get(), + &ControlObject::valueChanged, + this, + &BpmControl::slotControlBeatSyncPhase, Qt::DirectConnection); - m_pButtonSyncTempo = new ControlPushButton(ConfigKey(group, "beatsync_tempo")); - connect(m_pButtonSyncTempo, &ControlObject::valueChanged, - this, &BpmControl::slotControlBeatSyncTempo, + m_pButtonSyncTempo = std::make_unique( + ConfigKey(group, "beatsync_tempo")); + connect(m_pButtonSyncTempo.get(), + &ControlObject::valueChanged, + this, + &BpmControl::slotControlBeatSyncTempo, Qt::DirectConnection); - m_pTranslateBeats = new ControlPushButton(ConfigKey(group, "beats_translate_curpos")); - connect(m_pTranslateBeats, &ControlObject::valueChanged, - this, &BpmControl::slotBeatsTranslate, + m_pTranslateBeats = std::make_unique( + ConfigKey(group, "beats_translate_curpos")); + connect(m_pTranslateBeats.get(), + &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, + m_pBeatsTranslateMatchAlignment = std::make_unique( + ConfigKey(group, "beats_translate_match_alignment")); + connect(m_pBeatsTranslateMatchAlignment.get(), + &ControlObject::valueChanged, + this, + &BpmControl::slotBeatsTranslateMatchAlignment, Qt::DirectConnection); - connect(&m_tapFilter, &TapFilter::tapped, - this, &BpmControl::slotTapFilter, + 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_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; + m_pThisBeatDistance = + make_parented(group, "beat_distance", this); + m_pSyncMode = make_parented(group, "sync_mode", this); } double BpmControl::getBpm() const { @@ -158,39 +176,35 @@ double BpmControl::getBpm() const { } void BpmControl::slotAdjustBeatsFaster(double v) { - mixxx::BeatsPointer pBeats = m_pBeats; - if (v > 0 && pBeats && (pBeats->getCapabilities() & mixxx::Beats::BEATSCAP_SETBPM)) { - double bpm = pBeats->getBpm(); - double adjustedBpm = bpm + kBpmAdjustStep; - pBeats->setBpm(adjustedBpm); + if (v > 0 && m_pBeats) { + auto bpm = m_pBeats->getBpm(); + auto adjustedBpm = bpm + kBpmAdjustStep; + m_pBeats->setBpm(adjustedBpm); } } void BpmControl::slotAdjustBeatsSlower(double v) { - mixxx::BeatsPointer pBeats = m_pBeats; - if (v > 0 && pBeats && (pBeats->getCapabilities() & mixxx::Beats::BEATSCAP_SETBPM)) { - double bpm = pBeats->getBpm(); - double adjustedBpm = math_max(kBpmAdjustMin, bpm - kBpmAdjustStep); - pBeats->setBpm(adjustedBpm); + if (v > 0 && m_pBeats) { + auto bpm = m_pBeats->getBpm(); + auto adjustedBpm = mixxx::Bpm(math_max(kBpmAdjustMin, (bpm - kBpmAdjustStep).getValue())); + m_pBeats->setBpm(adjustedBpm); } } void BpmControl::slotTranslateBeatsEarlier(double v) { - mixxx::BeatsPointer pBeats = m_pBeats; - if (v > 0 && pBeats && - (pBeats->getCapabilities() & mixxx::Beats::BEATSCAP_TRANSLATE)) { - const int translate_dist = getSampleOfTrack().rate * -.01; - pBeats->translate(translate_dist); + if (v > 0 && m_pBeats) { + const mixxx::FrameDiff_t translateDistFrames = samplesToFrames( + getSampleOfTrack().rate * -kSlightBeatsTranslateFactor); + m_pBeats->translate(translateDistFrames); } } void BpmControl::slotTranslateBeatsLater(double v) { - mixxx::BeatsPointer pBeats = m_pBeats; - if (v > 0 && pBeats && - (pBeats->getCapabilities() & mixxx::Beats::BEATSCAP_TRANSLATE)) { + if (v > 0 && m_pBeats) { // TODO(rryan): Track::getSampleRate is possibly inaccurate! - const int translate_dist = getSampleOfTrack().rate * .01; - pBeats->translate(translate_dist); + const mixxx::FrameDiff_t translateDistFrames = samplesToFrames( + getSampleOfTrack().rate * kSlightBeatsTranslateFactor); + m_pBeats->translate(translateDistFrames); } } @@ -208,8 +222,7 @@ void BpmControl::slotTapFilter(double averageLength, int numSamples) { return; } - mixxx::BeatsPointer pBeats = m_pBeats; - if (!pBeats) { + if (!m_pBeats) { return; } @@ -220,12 +233,13 @@ void BpmControl::slotTapFilter(double averageLength, int numSamples) { // (60 seconds per minute) * (1000 milliseconds per second) / (X millis per // beat) = Y beats/minute - double averageBpm = 60.0 * 1000.0 / averageLength / rateRatio; - pBeats->setBpm(averageBpm); + mixxx::Bpm averageBpm(60.0 * 1000.0 / averageLength / rateRatio); + m_pBeats->setBpm(averageBpm); } void BpmControl::slotControlBeatSyncPhase(double v) { - if (!v) return; + if (!v) + return; if (isSynchronized()) { m_dUserOffset.setValue(0.0); @@ -234,12 +248,14 @@ void BpmControl::slotControlBeatSyncPhase(double v) { } void BpmControl::slotControlBeatSyncTempo(double v) { - if (!v) return; + if (!v) + return; syncTempo(); } void BpmControl::slotControlBeatSync(double v) { - if (!v) return; + if (!v) + return; if (!syncTempo()) { // syncTempo failed, nothing else to do return; @@ -326,8 +342,8 @@ bool BpmControl::syncTempo() { } // static -double BpmControl::shortestPercentageChange(const double& current_percentage, - const double& target_percentage) { +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) { @@ -343,27 +359,32 @@ double BpmControl::shortestPercentageChange(const double& current_percentage, // 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; + const double backwardsDistance = + target_percentage - current_percentage - 1.0; - return (fabs(forwardDistance) < fabs(backwardsDistance)) ? - forwardDistance : backwardsDistance; + 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; + 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; + return (fabs(forwardDistance) < fabs(backwardsDistance)) + ? forwardDistance + : backwardsDistance; } } double BpmControl::calcSyncedRate(double userTweak) { if (kLogger.traceEnabled()) { - kLogger.trace() << getGroup() << "BpmControl::calcSyncedRate, tweak " << userTweak; + kLogger.trace() << getGroup() << "BpmControl::calcSyncedRate, tweak " + << userTweak; } m_dUserTweakingSync = userTweak != 0.0; double rate = 1.0; @@ -374,8 +395,8 @@ double BpmControl::calcSyncedRate(double userTweak) { // 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() || isMaster(getSyncMode()) || - !m_pBeats || m_pReverseButton->get()) { + if (!m_pQuantize->get() || isMaster(getSyncMode()) || !m_pBeats || + m_pReverseButton->get()) { m_resetSyncAdjustment = true; return rate + userTweak; } @@ -393,9 +414,9 @@ 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->toBool(); - const double loop_size = (m_pLoopEndPosition->get() - - m_pLoopStartPosition->get()) / - dBeatLength; + 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; @@ -427,9 +448,10 @@ double BpmControl::calcSyncAdjustment(bool userTweakingSync) { double syncTargetBeatDistance = m_dSyncTargetBeatDistance.getValue(); // We want the untweaked beat distance, so we have to add the offset here. - double thisBeatDistance = m_pThisBeatDistance->get() + m_dUserOffset.getValue(); - double shortest_distance = shortestPercentageChange( - syncTargetBeatDistance, thisBeatDistance); + double thisBeatDistance = + m_pThisBeatDistance->get() + m_dUserOffset.getValue(); + double shortest_distance = + shortestPercentageChange(syncTargetBeatDistance, thisBeatDistance); if (kLogger.traceEnabled()) { kLogger.trace() << m_group << "****************"; @@ -471,9 +493,10 @@ double BpmControl::calcSyncAdjustment(bool userTweakingSync) { 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); + adjustment = 1.0 + + math_clamp(m_dLastSyncAdjustment - 1.0 + delta, + -kSyncAdjustmentCap, + kSyncAdjustmentCap); } else { // We are in sync, no adjustment needed. adjustment = 1.0; @@ -490,7 +513,8 @@ double BpmControl::getBeatDistance(double dThisPosition) const { // we don't adjust the reported distance the track will try to adjust // sync against itself. if (kLogger.traceEnabled()) { - kLogger.trace() << getGroup() << "BpmControl::getBeatDistance" << dThisPosition; + kLogger.trace() << getGroup() << "BpmControl::getBeatDistance" + << dThisPosition; } double dPrevBeat = m_pPrevBeat->get(); double dNextBeat = m_pNextBeat->get(); @@ -500,109 +524,122 @@ double BpmControl::getBeatDistance(double dThisPosition) const { } double dBeatLength = dNextBeat - dPrevBeat; - double dBeatPercentage = dBeatLength == 0.0 ? 0.0 : - (dThisPosition - dPrevBeat) / dBeatLength; + 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; + if (dBeatPercentage < 0) + ++dBeatPercentage; + if (dBeatPercentage > 1) + --dBeatPercentage; return dBeatPercentage - m_dUserOffset.getValue(); } // static bool BpmControl::getBeatContext(const mixxx::BeatsPointer& pBeats, - const double dPosition, - double* dpPrevBeat, - double* dpNextBeat, - double* dpBeatLength, + const mixxx::FramePos position, + mixxx::FramePos* pPrevBeat, + mixxx::FramePos* pNextBeat, + mixxx::FrameDiff_t* dpBeatLength, double* dpBeatPercentage) { if (!pBeats) { return false; } - double dPrevBeat; - double dNextBeat; - if (!pBeats->findPrevNextBeats(dPosition, &dPrevBeat, &dNextBeat)) { + mixxx::FramePos pPrevBeatInner, pNextBeatInner; + if (!pBeats->findPrevNextBeats( + position, &pPrevBeatInner, &pNextBeatInner)) { return false; } - if (dpPrevBeat != NULL) { - *dpPrevBeat = dPrevBeat; + if (pPrevBeat != nullptr) { + *pPrevBeat = pPrevBeatInner; } - if (dpNextBeat != NULL) { - *dpNextBeat = dNextBeat; + if (pNextBeat != nullptr) { + *pNextBeat = pNextBeatInner; } - return getBeatContextNoLookup(dPosition, dPrevBeat, dNextBeat, - dpBeatLength, dpBeatPercentage); + return getBeatContextNoLookup(position, + pPrevBeatInner, + pNextBeatInner, + dpBeatLength, + dpBeatPercentage); } // static bool BpmControl::getBeatContextNoLookup( - const double dPosition, - const double dPrevBeat, - const double dNextBeat, - double* dpBeatLength, - double* dpBeatPercentage) { - if (dPrevBeat == -1 || dNextBeat == -1) { + const mixxx::FramePos position, + mixxx::FramePos pPrevBeat, + mixxx::FramePos pNextBeat, + mixxx::FrameDiff_t* dpBeatLength, + double* dpBeatPercentage) { + if (pPrevBeat == mixxx::kInvalidFramePos || pNextBeat == mixxx::kInvalidFramePos) { return false; } - double dBeatLength = dNextBeat - dPrevBeat; + mixxx::FrameDiff_t dBeatLength = pNextBeat - pPrevBeat; if (dpBeatLength != NULL) { *dpBeatLength = dBeatLength; } if (dpBeatPercentage != NULL) { - *dpBeatPercentage = dBeatLength == 0.0 ? 0.0 : - (dPosition - dPrevBeat) / dBeatLength; + *dpBeatPercentage = dBeatLength == 0.0 + ? 0.0 + : (position - pPrevBeat) / 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; + if (*dpBeatPercentage < 0) + ++*dpBeatPercentage; + if (*dpBeatPercentage > 1) + --*dpBeatPercentage; } return true; } -double BpmControl::getNearestPositionInPhase( - double dThisPosition, bool respectLoops, bool playing) { +mixxx::FramePos BpmControl::getNearestPositionInPhase( + mixxx::FramePos thisPosition, bool respectLoops, bool playing) { // Without a beatgrid, we don't know the phase offset. - mixxx::BeatsPointer pBeats = m_pBeats; - if (!pBeats) { - return dThisPosition; + if (!m_pBeats) { + return thisPosition; } - SyncMode syncMode = getSyncMode(); // Explicit master buffer is always in sync! if (syncMode == SYNC_MASTER_EXPLICIT) { - return dThisPosition; + return thisPosition; } // Get the current position of this deck. - double dThisPrevBeat = m_pPrevBeat->get(); - double dThisNextBeat = m_pNextBeat->get(); - double dThisBeatLength; - if (dThisPosition > dThisNextBeat || dThisPosition < dThisPrevBeat) { + mixxx::FramePos thisPrevBeat = samplePosToFramePos(m_pPrevBeat->get()); + mixxx::FramePos thisNextBeat = samplePosToFramePos(m_pNextBeat->get()); + mixxx::FrameDiff_t dThisBeatLengthFrames; + if (thisPosition > thisNextBeat || thisPosition < thisPrevBeat) { if (kLogger.traceEnabled()) { - kLogger.trace() << "BpmControl::getNearestPositionInPhase out of date" - << dThisPosition << dThisNextBeat << dThisPrevBeat; + kLogger.trace() + << "BpmControl::getNearestPositionInPhase out of date" + << thisPosition << thisNextBeat << thisPrevBeat; } // This happens if dThisPosition is the target position of a requested // seek command - if (!getBeatContext(pBeats, dThisPosition, - &dThisPrevBeat, &dThisNextBeat, - &dThisBeatLength, NULL)) { - return dThisPosition; + if (!getBeatContext(m_pBeats, + thisPosition, + &thisPrevBeat, + &thisNextBeat, + &dThisBeatLengthFrames, + NULL)) { + return thisPosition; } } else { - if (!getBeatContextNoLookup(dThisPosition, - dThisPrevBeat, dThisNextBeat, - &dThisBeatLength, NULL)) { - return dThisPosition; + if (!getBeatContextNoLookup(thisPosition, + thisPrevBeat, + thisNextBeat, + &dThisBeatLengthFrames, + NULL)) { + return thisPosition; } } @@ -624,7 +661,7 @@ double BpmControl::getNearestPositionInPhase( if (!pOtherEngineBuffer) { // no suitable sync buffer found - return dThisPosition; + return thisPosition; } TrackPointer otherTrack = pOtherEngineBuffer->getLoadedTrack(); @@ -633,23 +670,24 @@ double BpmControl::getNearestPositionInPhase( // If either track does not have beats, then we can't adjust the phase. if (!otherBeats) { - return dThisPosition; + return thisPosition; } - double dOtherPosition = pOtherEngineBuffer->getExactPlayPos(); + mixxx::FramePos otherPosition = + samplePosToFramePos(pOtherEngineBuffer->getExactPlayPos()); if (!BpmControl::getBeatContext(otherBeats, - dOtherPosition, + otherPosition, NULL, NULL, NULL, &dOtherBeatFraction)) { - return dThisPosition; + return thisPosition; } } bool this_near_next = - dThisNextBeat - dThisPosition <= dThisPosition - dThisPrevBeat; + thisNextBeat - thisPosition <= thisPosition - thisPrevBeat; bool other_near_next = dOtherBeatFraction >= 0.5; // We want our beat fraction to be identical to theirs. @@ -669,22 +707,22 @@ double BpmControl::getNearestPositionInPhase( // infinite beatgrids because the assumption that findNthBeat(-2) always // works will be wrong then. - double dNewPlaypos = - (dOtherBeatFraction + m_dUserOffset.getValue()) * dThisBeatLength; + mixxx::FramePos newPlaypos = mixxx::FramePos(dThisBeatLengthFrames) * + (dOtherBeatFraction + m_dUserOffset.getValue()); if (this_near_next == other_near_next) { - dNewPlaypos += dThisPrevBeat; + newPlaypos += thisPrevBeat.getValue(); } else if (this_near_next && !other_near_next) { - dNewPlaypos += dThisNextBeat; + newPlaypos += thisNextBeat.getValue(); } else { //!this_near_next && other_near_next - dThisPrevBeat = pBeats->findNthBeat(dThisPosition, -2); - dNewPlaypos += dThisPrevBeat; + thisPrevBeat = m_pBeats->findNthBeat(thisPosition, -1); + newPlaypos += thisPrevBeat.getValue(); } 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(); + mixxx::FramePos loopStartPositionFrames = samplePosToFramePos(m_pLoopStartPosition->get()); + mixxx::FramePos loopEndPositionFrames = samplePosToFramePos(m_pLoopEndPosition->get()); // Cases for sanity: // @@ -699,23 +737,22 @@ double BpmControl::getNearestPositionInPhase( // 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; + if (loop_enabled && thisPosition <= loopEndPositionFrames) { + mixxx::FrameDiff_t loopLength = loopEndPositionFrames - loopStartPositionFrames; + mixxx::FrameDiff_t endDelta = newPlaypos - loopEndPositionFrames; // 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; + if (endDelta > 0 && loopLength > 0.0) { + int i = endDelta / loopLength; + newPlaypos = loopStartPositionFrames + endDelta - i * loopLength; // 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); + newPlaypos = + getNearestPositionInPhase(newPlaypos, false, playing); } // Note: Syncing to before the loop beginning is allowed, because @@ -723,18 +760,18 @@ double BpmControl::getNearestPositionInPhase( } } - return dNewPlaypos; + return newPlaypos; } -double BpmControl::getBeatMatchPosition( - double dThisPosition, bool respectLoops, bool playing) { +mixxx::FramePos BpmControl::getBeatMatchPosition( + mixxx::FramePos thisPosition, bool respectLoops, bool playing) { // Without a beatgrid, we don't know the phase offset. if (!m_pBeats) { - return dThisPosition; + return thisPosition; } // Explicit master buffer is always in sync! if (getSyncMode() == SYNC_MASTER_EXPLICIT) { - return dThisPosition; + return thisPosition; } EngineBuffer* pOtherEngineBuffer = nullptr; @@ -750,78 +787,75 @@ double BpmControl::getBeatMatchPosition( pOtherEngineBuffer = getEngineBuffer(); } } else if (!pOtherEngineBuffer) { - return dThisPosition; + return thisPosition; } // Get the current position of this deck. - double dThisPrevBeat = m_pPrevBeat->get(); - double dThisNextBeat = m_pNextBeat->get(); - double dThisBeatLength = -1; + mixxx::FramePos thisPrevBeat = samplePosToFramePos(m_pPrevBeat->get()); + mixxx::FramePos thisNextBeat = samplePosToFramePos(m_pNextBeat->get()); + mixxx::FrameDiff_t dThisBeatLengthFrames = -1; // Look up the next beat and beat length for the new position - if (dThisNextBeat == -1 || - dThisPosition > dThisNextBeat || - (dThisPrevBeat != -1 && dThisPosition < dThisPrevBeat)) { + if (thisNextBeat == mixxx::kInvalidFramePos || thisPosition > thisNextBeat || + (thisPrevBeat != mixxx::kInvalidFramePos && thisPosition < thisPrevBeat)) { if (kLogger.traceEnabled()) { kLogger.trace() << "BpmControl::getBeatMatchPosition out of date" - << dThisPrevBeat << dThisPosition << dThisNextBeat; + << thisPrevBeat << thisPosition << thisNextBeat; } // This happens if dThisPosition is the target position of a requested // seek command. Get new prev and next beats for the calculation. - getBeatContext( - m_pBeats, - dThisPosition, - &dThisPrevBeat, - &dThisNextBeat, - &dThisBeatLength, + getBeatContext(m_pBeats, + thisPosition, + &thisPrevBeat, + &thisNextBeat, + &dThisBeatLengthFrames, nullptr); // now we either have a useful next beat or there is none - if (dThisNextBeat == -1) { + if (thisNextBeat == mixxx::kInvalidFramePos) { // We can't match the next beat, give up. - return dThisPosition; + return thisPosition; } } else { if (kLogger.traceEnabled()) { kLogger.trace() << "BpmControl::getBeatMatchPosition up to date" - << dThisPrevBeat << dThisPosition << dThisNextBeat; + << thisPrevBeat << thisPosition << thisNextBeat; } // We are between the previous and next beats so we can try a standard // lookup of the beat length. - getBeatContextNoLookup( - dThisPosition, - dThisPrevBeat, - dThisNextBeat, - &dThisBeatLength, + getBeatContextNoLookup(thisPosition, + thisPrevBeat, + thisNextBeat, + &dThisBeatLengthFrames, nullptr); } TrackPointer otherTrack = pOtherEngineBuffer->getLoadedTrack(); - mixxx::BeatsPointer otherBeats = otherTrack ? otherTrack->getBeats() : mixxx::BeatsPointer(); + mixxx::BeatsPointer otherBeats = + otherTrack ? otherTrack->getBeats() : mixxx::BeatsPointer(); // If either track does not have beats, then we can't adjust the phase. if (!otherBeats) { - return dThisPosition; + return thisPosition; } - double dOtherPosition = pOtherEngineBuffer->getExactPlayPos(); + mixxx::FramePos otherPosition = samplePosToFramePos(pOtherEngineBuffer->getExactPlayPos()); - double dOtherPrevBeat = -1; - double dOtherNextBeat = -1; - double dOtherBeatLength = -1; + mixxx::FramePos otherPrevBeat = mixxx::kInvalidFramePos; + mixxx::FramePos otherNextBeat = mixxx::kInvalidFramePos; + mixxx::FrameDiff_t dOtherBeatLength = -1; double dOtherBeatFraction = -1; - if (!BpmControl::getBeatContext( - otherBeats, - dOtherPosition, - &dOtherPrevBeat, - &dOtherNextBeat, + if (!BpmControl::getBeatContext(otherBeats, + otherPosition, + &otherPrevBeat, + &otherNextBeat, &dOtherBeatLength, &dOtherBeatFraction)) { - return dThisPosition; + return thisPosition; } if (dOtherBeatLength == -1 || dOtherBeatFraction == -1) { // the other Track has no usable beat info, do not seek. - return dThisPosition; + return thisPosition; } double dThisSampleRate = m_pBeats->getSampleRate(); @@ -830,8 +864,8 @@ double BpmControl::getBeatMatchPosition( // Seek our next beat to the other next beat // This is the only thing we can do if the track has different BPM, // playing the next beat together. - double thisDivSec = (dThisNextBeat - dThisPosition) / - dThisSampleRate / dThisRateRatio; + double thisDivSec = + (thisNextBeat - thisPosition) / dThisSampleRate / dThisRateRatio; if (dOtherBeatFraction < 1.0 / 8) { // the user has probably pressed play too late, sync the previous beat @@ -839,29 +873,29 @@ double BpmControl::getBeatMatchPosition( } dOtherBeatFraction += m_dUserOffset.getValue(); - double otherDivSec = (1 - dOtherBeatFraction) * - dOtherBeatLength / otherBeats->getSampleRate() / pOtherEngineBuffer->getRateRatio(); + double otherDivSec = (1 - dOtherBeatFraction) * dOtherBeatLength / + otherBeats->getSampleRate() / pOtherEngineBuffer->getRateRatio(); // This matches the next beat in of both tracks. - double seekMatch = (thisDivSec - otherDivSec) * - dThisSampleRate * dThisRateRatio; + double seekMatch = + (thisDivSec - otherDivSec) * dThisSampleRate * dThisRateRatio; - if (dThisBeatLength > 0) { - if (dThisBeatLength / 2 < seekMatch) { + if (dThisBeatLengthFrames > 0) { + if (dThisBeatLengthFrames / 2 < seekMatch) { // seek to previous beat, because of shorter distance - seekMatch -= dThisBeatLength; - } else if (dThisBeatLength / 2 < -seekMatch) { + seekMatch -= dThisBeatLengthFrames; + } else if (dThisBeatLengthFrames / 2 < -seekMatch) { // seek to beat after next, because of shorter distance - seekMatch += dThisBeatLength; + seekMatch += dThisBeatLengthFrames; } } - double dNewPlaypos = dThisPosition + seekMatch; + mixxx::FramePos newPlaypos = thisPosition + seekMatch; 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(); + mixxx::FramePos loopStartPositionFrames = samplePosToFramePos(m_pLoopStartPosition->get()); + mixxx::FramePos loopEndPositionFrames = samplePosToFramePos(m_pLoopEndPosition->get()); // Cases for sanity: // @@ -878,33 +912,34 @@ double BpmControl::getBeatMatchPosition( // 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; + if (loop_enabled && thisPosition <= loopEndPositionFrames) { + const mixxx::FrameDiff_t loopLength = loopEndPositionFrames - loopStartPositionFrames; + const mixxx::FrameDiff_t endDelta = newPlaypos - loopEndPositionFrames; // 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; + if (endDelta > 0 && loopLength > 0.0) { + int i = endDelta / loopLength; + newPlaypos = loopStartPositionFrames + endDelta - i * loopLength; // 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); + newPlaypos = + getNearestPositionInPhase(newPlaypos, false, playing); } // Note: Syncing to before the loop beginning is allowed, because // loops are catching } } - return dNewPlaypos; + return newPlaypos; } -double BpmControl::getPhaseOffset(double dThisPosition) { +mixxx::FrameDiff_t BpmControl::getPhaseOffset(mixxx::FramePos thisPosition) { // This does not respect looping - double dNewPlaypos = getNearestPositionInPhase(dThisPosition, false, false); - return dNewPlaypos - dThisPosition; + mixxx::FramePos newPlayposFrames = getNearestPositionInPhase( + thisPosition, false, false); + return newPlayposFrames - thisPosition; } void BpmControl::slotUpdateEngineBpm(double value) { @@ -944,7 +979,7 @@ void BpmControl::trackLoaded(TrackPointer pNewTrack) { void BpmControl::trackBeatsUpdated(mixxx::BeatsPointer pBeats) { if (kLogger.traceEnabled()) { kLogger.trace() << getGroup() << "BpmControl::trackBeatsUpdated" - << (pBeats ? pBeats->getBpm() : 0.0); + << (pBeats ? pBeats->getBpm() : mixxx::Bpm(0.0)); } m_pBeats = pBeats; updateLocalBpm(); @@ -952,49 +987,43 @@ void BpmControl::trackBeatsUpdated(mixxx::BeatsPointer pBeats) { } void BpmControl::slotBeatsTranslate(double v) { - mixxx::BeatsPointer pBeats = m_pBeats; - if (v > 0 && pBeats && (pBeats->getCapabilities() & mixxx::Beats::BEATSCAP_TRANSLATE)) { - double currentSample = getSampleOfTrack().current; - double closestBeat = pBeats->findClosestBeat(currentSample); - int delta = currentSample - closestBeat; - if (delta % 2 != 0) { - delta--; - } - pBeats->translate(delta); + if (v > 0 && m_pBeats) { + mixxx::FramePos currentFrame = getFrameOfTrack().currentFrame; + mixxx::FramePos closestBeat = m_pBeats->findClosestBeat(currentFrame); + mixxx::FrameDiff_t delta = currentFrame - closestBeat; + m_pBeats->translate(delta); } } void BpmControl::slotBeatsTranslateMatchAlignment(double v) { - mixxx::BeatsPointer pBeats = m_pBeats; - if (v > 0 && pBeats && (pBeats->getCapabilities() & mixxx::Beats::BEATSCAP_TRANSLATE)) { + if (v > 0 && m_pBeats) { // 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); + mixxx::FrameDiff_t offsetFrames = getPhaseOffset(getFrameOfTrack().currentFrame); + m_pBeats->translate(-offsetFrames); } } double BpmControl::updateLocalBpm() { - double prev_local_bpm = m_pLocalBpm->get(); - double local_bpm = 0; - mixxx::BeatsPointer pBeats = m_pBeats; - if (pBeats) { - local_bpm = pBeats->getBpmAroundPosition( - getSampleOfTrack().current, kLocalBpmSpan); - if (local_bpm == -1) { - local_bpm = pBeats->getBpm(); + mixxx::Bpm prev_local_bpm(m_pLocalBpm->get()); + mixxx::Bpm local_bpm; + if (m_pBeats) { + local_bpm = m_pBeats->getBpmAroundPosition( + getFrameOfTrack().currentFrame, kLocalBpmSpan); + if (local_bpm.getValue() == -1) { + local_bpm = m_pBeats->getBpm(); } } if (local_bpm != prev_local_bpm) { if (kLogger.traceEnabled()) { kLogger.trace() << getGroup() << "BpmControl::updateLocalBpm" << local_bpm; } - m_pLocalBpm->set(local_bpm); + m_pLocalBpm->set(local_bpm.getValue()); slotUpdateEngineBpm(); } - return local_bpm; + return local_bpm.getValue(); } double BpmControl::updateBeatDistance() { @@ -1030,26 +1059,27 @@ void BpmControl::resetSyncAdjustment() { void BpmControl::collectFeatures(GroupFeatureState* pGroupFeatures) const { // Without a beatgrid we don't know any beat details. - SampleOfTrack sot = getSampleOfTrack(); - if (!sot.rate || !m_pBeats) { + FrameOfTrack frameOfTrack = getFrameOfTrack(); + if (!frameOfTrack.sampleRate || !m_pBeats) { return; } // Get the current position of this deck. - double dThisPrevBeat = m_pPrevBeat->get(); - double dThisNextBeat = m_pNextBeat->get(); - double dThisBeatLength; + mixxx::FramePos thisPrevBeat = samplePosToFramePos(m_pPrevBeat->get()); + mixxx::FramePos thisNextBeat = samplePosToFramePos(m_pNextBeat->get()); + mixxx::FrameDiff_t dThisBeatLengthFrames; double dThisBeatFraction; - if (getBeatContextNoLookup(sot.current, - dThisPrevBeat, dThisNextBeat, - &dThisBeatLength, &dThisBeatFraction)) { - + if (getBeatContextNoLookup(frameOfTrack.currentFrame, + thisPrevBeat, + thisNextBeat, + &dThisBeatLengthFrames, + &dThisBeatFraction)) { // Note: dThisBeatLength is fractional frames count * 2 (stereo samples) - double sotPerSec = kSamplesPerFrame * sot.rate * m_pRateRatio->get(); - if (sotPerSec != 0.0) { - pGroupFeatures->beat_length_sec = dThisBeatLength / sotPerSec; + mixxx::FrameDiff_t framesOfTrackPerSec = frameOfTrack.sampleRate * m_pRateRatio->get(); + if (framesOfTrackPerSec != 0.0) { + pGroupFeatures->beat_length_sec = dThisBeatLengthFrames / framesOfTrackPerSec; pGroupFeatures->has_beat_length_sec = true; - } else { + } else { pGroupFeatures->has_beat_length_sec = false; } diff --git a/src/engine/controls/bpmcontrol.h b/src/engine/controls/bpmcontrol.h index d191f77ef73..173afaac5c2 100644 --- a/src/engine/controls/bpmcontrol.h +++ b/src/engine/controls/bpmcontrol.h @@ -3,15 +3,16 @@ #include +#include "control/controllinpotmeter.h" #include "control/controlobject.h" +#include "control/controlpushbutton.h" #include "engine/controls/enginecontrol.h" #include "engine/sync/syncable.h" +#include "util/parented_ptr.h" #include "util/tapfilter.h" class ControlObject; -class ControlLinPotmeter; class ControlProxy; -class ControlPushButton; class EngineBuffer; class SyncControl; @@ -23,7 +24,7 @@ class BpmControl : public EngineControl { public: BpmControl(QString group, UserSettingsPointer pConfig); - ~BpmControl() override; + ~BpmControl() override = default; double getBpm() const; double getLocalBpm() const { return m_pLocalBpm ? m_pLocalBpm->get() : 0.0; } @@ -36,9 +37,11 @@ class BpmControl : public EngineControl { // out of sync. double calcSyncedRate(double userTweak); // Get the phase offset from the specified position. - double getNearestPositionInPhase(double dThisPosition, bool respectLoops, bool playing); - double getBeatMatchPosition(double dThisPosition, bool respectLoops, bool playing); - double getPhaseOffset(double dThisPosition); + mixxx::FramePos getNearestPositionInPhase( + mixxx::FramePos thisPosition, bool respectLoops, bool playing); + mixxx::FramePos getBeatMatchPosition( + mixxx::FramePos thisPosition, bool respectLoops, bool playing); + mixxx::FrameDiff_t getPhaseOffset(mixxx::FramePos thisPosition); /// getBeatDistance is adjusted to include the user offset so it's /// transparent to other decks. double getBeatDistance(double dThisPosition) const; @@ -58,20 +61,20 @@ class BpmControl : public EngineControl { // 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 mixxx::BeatsPointer& pBeats, - const double dPosition, - double* dpPrevBeat, - double* dpNextBeat, - double* dpBeatLength, + const mixxx::FramePos position, + mixxx::FramePos* pPrevBeat, + mixxx::FramePos* pNextBeat, + mixxx::FrameDiff_t* 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); + const mixxx::FramePos position, + mixxx::FramePos pPrevBeat, + mixxx::FramePos pNextBeat, + mixxx::FrameDiff_t* dpBeatLength, + double* dpBeatPercentage); // Returns the shortest change in percentage needed to achieve // target_percentage. @@ -111,50 +114,53 @@ class BpmControl : public EngineControl { friend class SyncControl; // ControlObjects that come from EngineBuffer - ControlProxy* m_pPlayButton; + parented_ptr m_pPlayButton; QAtomicInt m_oldPlayButton; - ControlProxy* m_pReverseButton; - ControlProxy* m_pRateRatio; + parented_ptr m_pReverseButton; + parented_ptr m_pRateRatio; ControlObject* m_pQuantize; // ControlObjects that come from QuantizeControl - QScopedPointer m_pNextBeat; - QScopedPointer m_pPrevBeat; + parented_ptr m_pNextBeat; + parented_ptr m_pPrevBeat; + parented_ptr m_pClosestBeat; // ControlObjects that come from LoopingControl - ControlProxy* m_pLoopEnabled; - ControlProxy* m_pLoopStartPosition; - ControlProxy* m_pLoopEndPosition; + parented_ptr m_pLoopEnabled; + parented_ptr m_pLoopStartPosition; + parented_ptr m_pLoopEndPosition; // The average bpm around the current playposition; - ControlObject* m_pLocalBpm; - ControlPushButton* m_pAdjustBeatsFaster; - ControlPushButton* m_pAdjustBeatsSlower; - ControlPushButton* m_pTranslateBeatsEarlier; - ControlPushButton* m_pTranslateBeatsLater; + std::unique_ptr m_pLocalBpm; + std::unique_ptr m_pAdjustBeatsFaster; + std::unique_ptr m_pAdjustBeatsSlower; + std::unique_ptr m_pTranslateBeatsEarlier; + std::unique_ptr m_pTranslateBeatsLater; // The current effective BPM of the engine - ControlLinPotmeter* m_pEngineBpm; + std::unique_ptr m_pEngineBpm; // Used for bpm tapping from GUI and MIDI - ControlPushButton* m_pButtonTap; + std::unique_ptr m_pButtonTap; // Button for sync'ing with the other EngineBuffer - ControlPushButton* m_pButtonSync; - ControlPushButton* m_pButtonSyncPhase; - ControlPushButton* m_pButtonSyncTempo; + std::unique_ptr m_pButtonSync; + std::unique_ptr m_pButtonSyncPhase; + std::unique_ptr m_pButtonSyncTempo; // Button that translates the beats so the nearest beat is on the current // playposition. - ControlPushButton* m_pTranslateBeats; + std::unique_ptr m_pTranslateBeats; // Button that translates beats to match another playing deck - ControlPushButton* m_pBeatsTranslateMatchAlignment; + std::unique_ptr m_pBeatsTranslateMatchAlignment; + // Button to set the nearest beat as a downbeat + std::unique_ptr m_pBeatsSetDownbeat; - ControlProxy* m_pThisBeatDistance; + parented_ptr m_pThisBeatDistance; ControlValueAtomic m_dSyncTargetBeatDistance; ControlValueAtomic m_dUserOffset; QAtomicInt m_resetSyncAdjustment; - ControlProxy* m_pSyncMode; + parented_ptr m_pSyncMode; TapFilter m_tapFilter; // threadsafe diff --git a/src/engine/controls/clockcontrol.cpp b/src/engine/controls/clockcontrol.cpp index b560de06d9b..509f684bd38 100644 --- a/src/engine/controls/clockcontrol.cpp +++ b/src/engine/controls/clockcontrol.cpp @@ -47,8 +47,8 @@ void ClockControl::process(const double dRate, mixxx::BeatsPointer pBeats = m_pBeats; if (pBeats) { - double closestBeat = pBeats->findClosestBeat(currentSample); - double distanceToClosestBeat = fabs(currentSample - closestBeat); + mixxx::FramePos closestBeat = pBeats->findClosestBeat(mixxx::FramePos(currentSample / 2.0)); + double distanceToClosestBeat = fabs(currentSample - closestBeat.getValue() * 2.0); m_pCOBeatActive->set(distanceToClosestBeat < blinkIntervalSamples / 2.0); } } diff --git a/src/engine/controls/cuecontrol.cpp b/src/engine/controls/cuecontrol.cpp index 314bcf7a7d8..4c285f11a72 100644 --- a/src/engine/controls/cuecontrol.cpp +++ b/src/engine/controls/cuecontrol.cpp @@ -1704,11 +1704,11 @@ double CueControl::quantizeCuePoint(double cuePos) { return cuePos; } - double closestBeat = pBeats->findClosestBeat(cuePos); + mixxx::FramePos closestBeat = pBeats->findClosestBeat(mixxx::FramePos(cuePos / 2.0)); // The closest beat can be an unreachable interpolated beat past the end of // the track. - if (closestBeat != -1.0 && closestBeat <= total) { - return closestBeat; + if (closestBeat != mixxx::FramePos(-1.0) && closestBeat <= mixxx::FramePos(total / 2.0)) { + return closestBeat.getValue() * 2; } return cuePos; diff --git a/src/engine/controls/enginecontrol.cpp b/src/engine/controls/enginecontrol.cpp index a5d0a0555f2..f841b0d61af 100644 --- a/src/engine/controls/enginecontrol.cpp +++ b/src/engine/controls/enginecontrol.cpp @@ -2,10 +2,12 @@ // Created 7/5/2009 by RJ Ryan (rryan@mit.edu) #include "engine/controls/enginecontrol.h" -#include "engine/enginemaster.h" + #include "engine/enginebuffer.h" +#include "engine/enginemaster.h" #include "engine/sync/enginesync.h" #include "mixer/playermanager.h" +#include "util/frameadapter.h" EngineControl::EngineControl(QString group, UserSettingsPointer pConfig) @@ -115,3 +117,11 @@ EngineBuffer* EngineControl::pickSyncTarget() { } return nullptr; } + +EngineControl::FrameOfTrack EngineControl::getFrameOfTrack() const { + FrameOfTrack frameOfTrack{ + samplePosToFramePos(getSampleOfTrack().current), + samplePosToFramePos(getSampleOfTrack().total), + getSampleOfTrack().rate}; + return frameOfTrack; +} diff --git a/src/engine/controls/enginecontrol.h b/src/engine/controls/enginecontrol.h index 1fe3371e5e5..6fbea61f794 100644 --- a/src/engine/controls/enginecontrol.h +++ b/src/engine/controls/enginecontrol.h @@ -76,10 +76,16 @@ class EngineControl : public QObject { double total; double rate; }; + struct FrameOfTrack { + mixxx::FramePos currentFrame; + mixxx::FramePos totalFrames; + double sampleRate; + }; SampleOfTrack getSampleOfTrack() const { return m_sampleOfTrack.getValue(); } + FrameOfTrack getFrameOfTrack() const; void seek(double fractionalPosition); void seekAbs(double sample); // Seek to an exact sample and don't allow quantizing adjustment. diff --git a/src/engine/controls/loopingcontrol.cpp b/src/engine/controls/loopingcontrol.cpp index fbce5c8a8d5..fc0bb96123d 100644 --- a/src/engine/controls/loopingcontrol.cpp +++ b/src/engine/controls/loopingcontrol.cpp @@ -2,22 +2,23 @@ // Created on Sep 23, 2008 // Author: asantoni, rryan +#include "engine/controls/loopingcontrol.h" + #include #include "control/controlobject.h" #include "control/controlpushbutton.h" #include "engine/controls/bpmcontrol.h" #include "engine/controls/enginecontrol.h" -#include "engine/controls/loopingcontrol.h" #include "engine/enginebuffer.h" #include "preferences/usersettings.h" +#include "track/beats.h" +#include "track/track.h" #include "util/compatibility.h" +#include "util/frameadapter.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 }; @@ -517,12 +518,12 @@ void LoopingControl::setLoopInToCurrentPosition() { mixxx::BeatsPointer pBeats = m_pBeats; LoopSamples loopSamples = m_loopSamples.getValue(); double quantizedBeat = -1; - double pos = m_currentSample.getValue(); + double dPosSample = m_currentSample.getValue(); if (m_pQuantizeEnabled->toBool() && pBeats) { if (m_bAdjustingLoopIn) { - double closestBeat = m_pClosestBeat->get(); - if (closestBeat == m_currentSample.getValue()) { - quantizedBeat = closestBeat; + mixxx::FramePos closestBeat(m_pClosestBeat->get() / 2.0); + if (closestBeat.getValue() * 2.0 == m_currentSample.getValue()) { + quantizedBeat = closestBeat.getValue() * 2.0; } else { quantizedBeat = m_pPreviousBeat->get(); } @@ -530,14 +531,14 @@ void LoopingControl::setLoopInToCurrentPosition() { quantizedBeat = m_pClosestBeat->get(); } if (quantizedBeat != -1) { - pos = quantizedBeat; + dPosSample = 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 < dPosSample) { loopSamples.end = kNoTrigger; m_pCOLoopEndPosition->set(kNoTrigger); } @@ -546,18 +547,18 @@ void LoopingControl::setLoopInToCurrentPosition() { // 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) { + (loopSamples.end - dPosSample) < 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; + dPosSample = pBeats->findNthBeat(mixxx::FramePos(quantizedBeat / 2.0), -2).getValue(); + if (dPosSample == -1 || (loopSamples.end - dPosSample) < MINIMUM_AUDIBLE_LOOP_SIZE) { + dPosSample = loopSamples.end - MINIMUM_AUDIBLE_LOOP_SIZE; } } else { - pos = loopSamples.end - MINIMUM_AUDIBLE_LOOP_SIZE; + dPosSample = loopSamples.end - MINIMUM_AUDIBLE_LOOP_SIZE; } } - loopSamples.start = pos; + loopSamples.start = dPosSample; m_pCOLoopStartPosition->set(loopSamples.start); @@ -574,7 +575,8 @@ void LoopingControl::setLoopInToCurrentPosition() { && loopSamples.start < loopSamples.end && pBeats) { m_pCOBeatLoopSize->setAndConfirm( - pBeats->numBeatsInRange(loopSamples.start, loopSamples.end)); + pBeats->numBeatsInRange(mixxx::FramePos(loopSamples.start / 2.0), + mixxx::FramePos(loopSamples.end / 2.0))); updateBeatLoopingControls(); } else { clearActiveBeatLoop(); @@ -610,8 +612,7 @@ void LoopingControl::slotLoopIn(double pressed) { void LoopingControl::slotLoopInGoto(double pressed) { if (pressed > 0.0) { - seekAbs(static_cast( - m_loopSamples.getValue().start)); + seekAbs(static_cast(m_loopSamples.getValue().start)); } } @@ -647,8 +648,12 @@ void LoopingControl::setLoopOutToCurrentPosition() { // use the smallest pre-defined beatloop instead (when possible) if ((pos - loopSamples.start) < MINIMUM_AUDIBLE_LOOP_SIZE) { if (quantizedBeat != -1 && pBeats) { - pos = static_cast(floor(pBeats->findNthBeat(quantizedBeat, 2))); - if (pos == -1 || (pos - loopSamples.start) < MINIMUM_AUDIBLE_LOOP_SIZE) { + pos = static_cast(floor( + pBeats->findNthBeat(mixxx::FramePos(quantizedBeat / 2.0), 1) + .getValue() * + 2.0)); + if (pos == -1 || + (pos - loopSamples.start) < MINIMUM_AUDIBLE_LOOP_SIZE) { pos = loopSamples.start + MINIMUM_AUDIBLE_LOOP_SIZE; } } else { @@ -662,8 +667,7 @@ void LoopingControl::setLoopOutToCurrentPosition() { m_pCOLoopEndPosition->set(loopSamples.end); // start looping - if (loopSamples.start != kNoTrigger && - loopSamples.end != kNoTrigger) { + if (loopSamples.start != kNoTrigger && loopSamples.end != kNoTrigger) { setLoopingEnabled(true); loopSamples.seek = true; } else { @@ -672,7 +676,8 @@ void LoopingControl::setLoopOutToCurrentPosition() { if (m_pQuantizeEnabled->toBool() && pBeats) { m_pCOBeatLoopSize->setAndConfirm( - pBeats->numBeatsInRange(loopSamples.start, loopSamples.end)); + pBeats->numBeatsInRange(mixxx::FramePos(loopSamples.start / 2.0), + mixxx::FramePos(loopSamples.end / 2.0))); updateBeatLoopingControls(); } else { clearActiveBeatLoop(); @@ -720,8 +725,7 @@ void LoopingControl::slotLoopOut(double pressed) { void LoopingControl::slotLoopOutGoto(double pressed) { if (pressed > 0.0) { - seekAbs(static_cast( - m_loopSamples.getValue().end)); + seekAbs(static_cast(m_loopSamples.getValue().end)); } } @@ -769,8 +773,7 @@ void LoopingControl::slotReloopToggle(double val) { void LoopingControl::slotReloopAndStop(double pressed) { if (pressed > 0) { m_pPlayButton->set(0.0); - seekAbs(static_cast( - m_loopSamples.getValue().start)); + seekAbs(static_cast(m_loopSamples.getValue().start)); setLoopingEnabled(true); } } @@ -794,8 +797,7 @@ void LoopingControl::slotLoopStartPos(double pos) { loopSamples.start = pos; m_pCOLoopStartPosition->set(pos); - if (loopSamples.end != kNoTrigger && - loopSamples.end <= loopSamples.start) { + if (loopSamples.end != kNoTrigger && loopSamples.end <= loopSamples.start) { loopSamples.end = kNoTrigger; m_pCOLoopEndPosition->set(kNoTrigger); setLoopingEnabled(false); @@ -886,8 +888,8 @@ void LoopingControl::trackBeatsUpdated(mixxx::BeatsPointer pBeats) { if (m_pBeats) { LoopSamples loopSamples = m_loopSamples.getValue(); if (loopSamples.start != kNoTrigger && loopSamples.end != kNoTrigger) { - double loaded_loop_size = findBeatloopSizeForLoop( - loopSamples.start, loopSamples.end); + double loaded_loop_size = + findBeatloopSizeForLoop(loopSamples.start, loopSamples.end); if (loaded_loop_size != -1) { m_pCOBeatLoopSize->setAndConfirm(loaded_loop_size); } @@ -895,7 +897,8 @@ void LoopingControl::trackBeatsUpdated(mixxx::BeatsPointer pBeats) { } } -void LoopingControl::slotBeatLoopActivate(BeatLoopingControl* pBeatLoopControl) { +void LoopingControl::slotBeatLoopActivate( + BeatLoopingControl* pBeatLoopControl) { if (!m_pTrack) { return; } @@ -907,10 +910,11 @@ void LoopingControl::slotBeatLoopActivate(BeatLoopingControl* pBeatLoopControl) slotBeatLoop(pBeatLoopControl->getSize(), m_bLoopingEnabled, true); } -void LoopingControl::slotBeatLoopActivateRoll(BeatLoopingControl* pBeatLoopControl) { - if (!m_pTrack) { - return; - } +void LoopingControl::slotBeatLoopActivateRoll( + BeatLoopingControl* pBeatLoopControl) { + if (!m_pTrack) { + return; + } // Disregard existing loops (except beatlooprolls). m_pSlipEnabled->set(1); @@ -919,12 +923,14 @@ void LoopingControl::slotBeatLoopActivateRoll(BeatLoopingControl* pBeatLoopContr m_activeLoopRolls.push(pBeatLoopControl->getSize()); } -void LoopingControl::slotBeatLoopDeactivate(BeatLoopingControl* pBeatLoopControl) { +void LoopingControl::slotBeatLoopDeactivate( + BeatLoopingControl* pBeatLoopControl) { Q_UNUSED(pBeatLoopControl); setLoopingEnabled(false); } -void LoopingControl::slotBeatLoopDeactivateRoll(BeatLoopingControl* pBeatLoopControl) { +void LoopingControl::slotBeatLoopDeactivateRoll( + BeatLoopingControl* pBeatLoopControl) { pBeatLoopControl->deactivate(); const double size = pBeatLoopControl->getSize(); auto i = m_activeLoopRolls.begin(); @@ -951,7 +957,8 @@ void LoopingControl::slotBeatLoopDeactivateRoll(BeatLoopingControl* pBeatLoopCon } void LoopingControl::clearActiveBeatLoop() { - BeatLoopingControl* pOldBeatLoop = m_pActiveBeatLoop.fetchAndStoreAcquire(nullptr); + BeatLoopingControl* pOldBeatLoop = + m_pActiveBeatLoop.fetchAndStoreAcquire(nullptr); if (pOldBeatLoop != nullptr) { pOldBeatLoop->deactivate(); } @@ -966,11 +973,13 @@ bool LoopingControl::currentLoopMatchesBeatloopSize() { 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()); + mixxx::FramePos beatLoopOutPoint = pBeats->findNBeatsFromFrame( + mixxx::FramePos(loopSamples.start / 2.0), m_pCOBeatLoopSize->get()); - return loopSamples.end > beatLoopOutPoint - 2 && - loopSamples.end < beatLoopOutPoint + 2; + return mixxx::FramePos(loopSamples.end / 2.0) > + beatLoopOutPoint - 1 && + mixxx::FramePos(loopSamples.end / 2.0) < + beatLoopOutPoint + 1; } double LoopingControl::findBeatloopSizeForLoop(double start, double end) const { @@ -979,9 +988,14 @@ double LoopingControl::findBeatloopSizeForLoop(double start, double end) const { return -1; } - for (unsigned int i = 0; i < (sizeof(s_dBeatSizes) / sizeof(s_dBeatSizes[0])); ++i) { + for (unsigned int i = 0; + i < (sizeof(s_dBeatSizes) / sizeof(s_dBeatSizes[0])); + ++i) { double beatLoopOutPoint = - pBeats->findNBeatsFromSample(start, s_dBeatSizes[i]); + pBeats->findNBeatsFromFrame( + mixxx::FramePos(start / 2.0), s_dBeatSizes[i]) + .getValue() * + 2.0; if (end > beatLoopOutPoint - 2 && end < beatLoopOutPoint + 2) { return s_dBeatSizes[i]; } @@ -993,7 +1007,7 @@ 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)) { + for (BeatLoopingControl* pBeatLoopControl : qAsConst(m_beatLoops)) { if (pBeatLoopControl->getSize() == dBeatloopSize) { if (m_bLoopingEnabled) { pBeatLoopControl->activate(); @@ -1011,7 +1025,8 @@ void LoopingControl::updateBeatLoopingControls() { clearActiveBeatLoop(); } -void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable) { +void LoopingControl::slotBeatLoop( + double beats, bool keepStartPoint, bool enable) { // if a seek was queued in the engine buffer move the current sample to its position double p_seekPosition = 0; if (getEngineBuffer()->getQueuedSeekPosition(&p_seekPosition)) { @@ -1019,7 +1034,8 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable m_currentSample.setValue(p_seekPosition); } - double maxBeatSize = s_dBeatSizes[sizeof(s_dBeatSizes)/sizeof(s_dBeatSizes[0]) - 1]; + 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. @@ -1043,7 +1059,7 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable // 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(); + mixxx::FramePos currentFramePos = getFrameOfTrack().currentFrame; // Start from the current position/closest beat and // create the loop around X beats from there. @@ -1051,22 +1067,24 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable if (loopSamples.start != kNoTrigger) { newloopSamples.start = loopSamples.start; } else { - newloopSamples.start = currentSample; + newloopSamples.start = framePosToSamplePos(currentFramePos); } } else { // loop_in is set to the closest beat if quantize is on and the loop size is >= 1 beat. // The closest beat might be ahead of play position and will cause a catching loop. - double prevBeat; - double nextBeat; - pBeats->findPrevNextBeats(currentSample, &prevBeat, &nextBeat); + mixxx::FramePos prevBeat; + mixxx::FramePos nextBeat; + pBeats->findPrevNextBeats( + currentFramePos, &prevBeat, &nextBeat); - if (m_pQuantizeEnabled->toBool() && prevBeat != -1) { - double beatLength = nextBeat - prevBeat; - double loopLength = beatLength * beats; + if (m_pQuantizeEnabled->toBool() && prevBeat.getValue() != -1) { + mixxx::FrameDiff_t beatLengthFrames = nextBeat - prevBeat; + mixxx::FrameDiff_t loopLengthFrames = beatLengthFrames * beats; - double closestBeat = pBeats->findClosestBeat(currentSample); + mixxx::FramePos closestBeatFramePos = + pBeats->findClosestBeat(currentFramePos); if (beats >= 1.0) { - newloopSamples.start = closestBeat; + newloopSamples.start = framePosToSamplePos(closestBeatFramePos); } else { // In case of beat length less then 1 beat: // (| - beats, ^ - current track's position): @@ -1075,17 +1093,21 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable // // 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 samplesSinceLastBeat = currentSample - prevBeat; + mixxx::FrameDiff_t framesSinceLastBeat = + currentFramePos - prevBeat; // find the previous beat fraction and check if the current position is closer to this or the next one // place the new loop start to the closer one - double previousFractionBeat = prevBeat + floor(samplesSinceLastBeat / loopLength) * loopLength; - double samplesSinceLastFractionBeat = currentSample - previousFractionBeat; + mixxx::FramePos previousFractionBeatFramePos = prevBeat + + floor(framesSinceLastBeat / loopLengthFrames) * loopLengthFrames; + mixxx::FrameDiff_t framesSinceLastFractionBeat = + currentFramePos - previousFractionBeatFramePos; - if (samplesSinceLastFractionBeat <= (loopLength / 2.0)) { - newloopSamples.start = previousFractionBeat; + if (framesSinceLastFractionBeat <= (loopLengthFrames / 2.0)) { + newloopSamples.start = framePosToSamplePos(previousFractionBeatFramePos); } else { - newloopSamples.start = previousFractionBeat + loopLength; + newloopSamples.start = framePosToSamplePos( + previousFractionBeatFramePos + loopLengthFrames); } } @@ -1096,25 +1118,33 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable reverse = m_pRateControl->isReverseButtonPressed(); } if (reverse) { - newloopSamples.start -= loopLength; + newloopSamples.start -= framesToSamples(loopLengthFrames); } } else { - newloopSamples.start = currentSample; + newloopSamples.start = framePosToSamplePos(currentFramePos); } } - newloopSamples.end = pBeats->findNBeatsFromSample(newloopSamples.start, beats); - if (newloopSamples.start >= newloopSamples.end // happens when the call above fails - || newloopSamples.end > samples) { // Do not allow beat loops to go beyond the end of the track + newloopSamples.end = + framePosToSamplePos(pBeats->findNBeatsFromFrame( + samplePosToFramePos(newloopSamples.start), beats)); + + if (newloopSamples.start >= + newloopSamples.end // happens when the call above fails + || newloopSamples.end > + samples) { // Do not allow beat loops to go beyond the end of the track // If a track is loaded with beatloop_size larger than // the distance between the loop in point and // the end of the track, let beatloop_size be set to // a smaller size, but not get larger. double previousBeatloopSize = m_pCOBeatLoopSize->get(); - double previousBeatloopOutPoint = pBeats->findNBeatsFromSample( - newloopSamples.start, previousBeatloopSize); - if (previousBeatloopOutPoint < newloopSamples.start - && beats < previousBeatloopSize) { + mixxx::FramePos previousBeatloopOutPointFramePos = + pBeats->findNBeatsFromFrame( + samplePosToFramePos(newloopSamples.start), + previousBeatloopSize); + + if (framePosToSamplePos(previousBeatloopOutPointFramePos) < newloopSamples.start && + beats < previousBeatloopSize) { m_pCOBeatLoopSize->setAndConfirm(beats); } return; @@ -1135,7 +1165,8 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable // 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)) { + if ((newloopSamples.start == kNoTrigger) || + (newloopSamples.end == kNoTrigger)) { return; } @@ -1212,7 +1243,10 @@ void LoopingControl::slotBeatJump(double beats) { // If inside an active loop, move loop slotLoopMove(beats); } else { - seekAbs(pBeats->findNBeatsFromSample(currentSample, beats)); + seekAbs(pBeats->findNBeatsFromFrame( + mixxx::FramePos(currentSample / 2.0), beats) + .getValue() * + 2); } } @@ -1238,12 +1272,26 @@ void LoopingControl::slotLoopMove(double beats) { return; } - if (BpmControl::getBeatContext(pBeats, m_currentSample.getValue(), - nullptr, nullptr, nullptr, nullptr)) { - double new_loop_in = pBeats->findNBeatsFromSample(loopSamples.start, beats); - double new_loop_out = currentLoopMatchesBeatloopSize() ? - pBeats->findNBeatsFromSample(new_loop_in, m_pCOBeatLoopSize->get()) : - pBeats->findNBeatsFromSample(loopSamples.end, beats); + if (BpmControl::getBeatContext(pBeats, + mixxx::FramePos(m_currentSample.getValue() / mixxx::kEngineChannelCount), + nullptr, + nullptr, + nullptr, + nullptr)) { + double new_loop_in = + pBeats->findNBeatsFromFrame( + mixxx::FramePos(loopSamples.start / 2.0), beats) + .getValue() * + 2; + double new_loop_out = currentLoopMatchesBeatloopSize() + ? pBeats->findNBeatsFromFrame(mixxx::FramePos(new_loop_in / 2.0), + m_pCOBeatLoopSize->get()) + .getValue() * + 2 + : pBeats->findNBeatsFromFrame( + mixxx::FramePos(loopSamples.end / 2.0), beats) + .getValue() * + 2; // If we are looping make sure that the play head does not leave the // loop as a result of our adjustment. diff --git a/src/engine/controls/quantizecontrol.cpp b/src/engine/controls/quantizecontrol.cpp index 0960edf22ac..a0b84072260 100644 --- a/src/engine/controls/quantizecontrol.cpp +++ b/src/engine/controls/quantizecontrol.cpp @@ -2,14 +2,16 @@ // Created on Sat 5, 2011 // Author: pwhelan +#include "engine/controls/quantizecontrol.h" + #include #include "control/controlobject.h" -#include "preferences/usersettings.h" #include "control/controlpushbutton.h" -#include "engine/controls/quantizecontrol.h" #include "engine/controls/enginecontrol.h" +#include "preferences/usersettings.h" #include "util/assert.h" +#include "util/frameadapter.h" QuantizeControl::QuantizeControl(QString group, UserSettingsPointer pConfig) @@ -40,7 +42,7 @@ void QuantizeControl::trackLoaded(TrackPointer pNewTrack) { lookupBeatPositions(0.0); updateClosestBeat(0.0); } else { - m_pBeats.clear(); + m_pBeats.reset(); m_pCOPrevBeat->set(-1); m_pCONextBeat->set(-1); m_pCOClosestBeat->set(-1); @@ -81,10 +83,10 @@ void QuantizeControl::playPosChanged(double dNewPlaypos) { void QuantizeControl::lookupBeatPositions(double dCurrentSample) { mixxx::BeatsPointer pBeats = m_pBeats; if (pBeats) { - double prevBeat, nextBeat; - pBeats->findPrevNextBeats(dCurrentSample, &prevBeat, &nextBeat); - m_pCOPrevBeat->set(prevBeat); - m_pCONextBeat->set(nextBeat); + mixxx::FramePos prevBeat, nextBeat; + pBeats->findPrevNextBeats(samplePosToFramePos(dCurrentSample), &prevBeat, &nextBeat); + m_pCOPrevBeat->set(framePosToSamplePos(prevBeat)); + m_pCONextBeat->set(framePosToSamplePos(nextBeat)); } } diff --git a/src/engine/enginebuffer.cpp b/src/engine/enginebuffer.cpp index cc08b50d618..4a9c1c1c76e 100644 --- a/src/engine/enginebuffer.cpp +++ b/src/engine/enginebuffer.cpp @@ -1197,7 +1197,15 @@ void EngineBuffer::processSeek(bool paused) { kLogger.trace() << "EngineBuffer::processSeek Seeking phase"; } double requestedPosition = position; - double syncPosition = m_pBpmControl->getBeatMatchPosition(position, true, true); + double syncPosition = + m_pBpmControl + ->getBeatMatchPosition( + mixxx::FramePos( + position / mixxx::kEngineChannelCount), + true, + true) + .getValue() * + mixxx::kEngineChannelCount; position = m_pLoopingControl->getSyncPositionInsideLoop(requestedPosition, syncPosition); if (kLogger.traceEnabled()) { kLogger.trace() diff --git a/src/engine/sync/synccontrol.cpp b/src/engine/sync/synccontrol.cpp index 8c319fac8d8..9bde4b1d4bd 100644 --- a/src/engine/sync/synccontrol.cpp +++ b/src/engine/sync/synccontrol.cpp @@ -486,7 +486,7 @@ void SyncControl::notifySeek(double dNewPlaypos) { double SyncControl::fileBpm() const { mixxx::BeatsPointer pBeats = m_pBeats; if (pBeats) { - return pBeats->getBpm(); + return pBeats->getBpm().getValue(); } return 0.0; } diff --git a/src/library/banshee/bansheeplaylistmodel.cpp b/src/library/banshee/bansheeplaylistmodel.cpp index c99460577fd..b8252213c57 100644 --- a/src/library/banshee/bansheeplaylistmodel.cpp +++ b/src/library/banshee/bansheeplaylistmodel.cpp @@ -278,7 +278,8 @@ TrackPointer BansheePlaylistModel::getTrack(const QModelIndex& index) const { pTrack->setComposer(getFieldString(index, CLM_COMPOSER)); // If the track has a BPM, then give it a static beatgrid. if (bpm > 0) { - mixxx::BeatsPointer pBeats = BeatFactory::makeBeatGrid(*pTrack, bpm, 0.0); + mixxx::BeatsPointer pBeats = std::make_shared(mixxx::Beats(pTrack.get())); + pBeats->setGrid(mixxx::Bpm(bpm)); pTrack->setBeats(pBeats); } } diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index 25d5a6691f5..8b97c8a563a 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -553,11 +553,11 @@ void bindTrackLibraryValues( QString beatsSubVersion; // Fall back on cached BPM double dBpm = trackInfo.getBpm().getValue(); - if (!pBeats.isNull()) { - beatsBlob = pBeats->toByteArray(); + if (pBeats) { + beatsBlob = pBeats->toProtobuf(); beatsVersion = pBeats->getVersion(); beatsSubVersion = pBeats->getSubVersion(); - dBpm = pBeats->getBpm(); + dBpm = pBeats->getBpm().getValue(); } pTrackLibraryQuery->bindValue(":bpm", dBpm); pTrackLibraryQuery->bindValue(":beats_version", beatsVersion); @@ -1181,7 +1181,7 @@ bool setTrackBeats(const QSqlRecord& record, const int column, QByteArray beatsBlob = record.value(column + 3).toByteArray(); bool bpmLocked = record.value(column + 4).toBool(); mixxx::BeatsPointer pBeats = BeatFactory::loadBeatsFromByteArray( - *pTrack, beatsVersion, beatsSubVersion, beatsBlob); + pTrack, beatsVersion, beatsSubVersion, beatsBlob); if (pBeats) { pTrack->setBeats(pBeats); } else { diff --git a/src/library/dlgtrackinfo.cpp b/src/library/dlgtrackinfo.cpp index 541fe0a0dde..f436c01c494 100644 --- a/src/library/dlgtrackinfo.cpp +++ b/src/library/dlgtrackinfo.cpp @@ -7,6 +7,7 @@ #include "library/coverartcache.h" #include "library/coverartutils.h" +#include "preferences/beatdetectionsettings.h" #include "preferences/colorpalettesettings.h" #include "sources/soundsourceproxy.h" #include "track/beatfactory.h" @@ -15,8 +16,8 @@ #include "track/keyutils.h" #include "util/color/colorpalette.h" #include "util/compatibility.h" -#include "util/desktophelper.h" #include "util/datetime.h" +#include "util/desktophelper.h" #include "util/duration.h" const int kFilterLength = 80; @@ -274,17 +275,20 @@ void DlgTrackInfo::populateFields(const Track& track) { void DlgTrackInfo::reloadTrackBeats(const Track& track) { mixxx::BeatsPointer pBeats = track.getBeats(); if (pBeats) { - spinBpm->setValue(pBeats->getBpm()); + spinBpm->setValue(pBeats->getBpm().getValue()); m_pBeatsClone = pBeats->clone(); } else { - m_pBeatsClone.clear(); + m_pBeatsClone.reset(); spinBpm->setValue(0.0); } - m_trackHasBeatMap = pBeats && !(pBeats->getCapabilities() & mixxx::Beats::BEATSCAP_SETBPM); - bpmConst->setChecked(!m_trackHasBeatMap); - bpmConst->setEnabled(m_trackHasBeatMap); // We cannot make turn a BeatGrid to a BeatMap - spinBpm->setEnabled(!m_trackHasBeatMap); // We cannot change bpm continuously or tab them - bpmTap->setEnabled(!m_trackHasBeatMap); // when we have a beatmap + + m_bpmIsConst = pBeats && + m_pConfig->getValue( + ConfigKey(BPM_CONFIG_KEY, BPM_FIXED_TEMPO_ASSUMPTION)); + bpmConst->setChecked(m_bpmIsConst); + bpmConst->setEnabled(!m_bpmIsConst); + spinBpm->setEnabled(m_bpmIsConst); // We cannot change bpm continuously or tap them + bpmTap->setEnabled(m_bpmIsConst); // when we have a variable bpm. if (track.isBpmLocked()) { tabBPM->setEnabled(false); @@ -445,7 +449,7 @@ void DlgTrackInfo::clear() { txtTrackNumber->setText(""); txtComment->setPlainText(""); spinBpm->setValue(0.0); - m_pBeatsClone.clear(); + m_pBeatsClone.reset(); m_keysClone = Keys(); txtDuration->setText(""); @@ -461,53 +465,53 @@ void DlgTrackInfo::clear() { } void DlgTrackInfo::slotBpmDouble() { - m_pBeatsClone->scale(mixxx::Beats::DOUBLE); + m_pBeatsClone->scale(mixxx::BeatsInternal::DOUBLE); // read back the actual value - double newValue = m_pBeatsClone->getBpm(); + double newValue = m_pBeatsClone->getBpm().getValue(); spinBpm->setValue(newValue); } void DlgTrackInfo::slotBpmHalve() { - m_pBeatsClone->scale(mixxx::Beats::HALVE); + m_pBeatsClone->scale(mixxx::BeatsInternal::HALVE); // read back the actual value - double newValue = m_pBeatsClone->getBpm(); + double newValue = m_pBeatsClone->getBpm().getValue(); spinBpm->setValue(newValue); } void DlgTrackInfo::slotBpmTwoThirds() { - m_pBeatsClone->scale(mixxx::Beats::TWOTHIRDS); + m_pBeatsClone->scale(mixxx::BeatsInternal::TWOTHIRDS); // read back the actual value - double newValue = m_pBeatsClone->getBpm(); + double newValue = m_pBeatsClone->getBpm().getValue(); spinBpm->setValue(newValue); } void DlgTrackInfo::slotBpmThreeFourth() { - m_pBeatsClone->scale(mixxx::Beats::THREEFOURTHS); + m_pBeatsClone->scale(mixxx::BeatsInternal::THREEFOURTHS); // read back the actual value - double newValue = m_pBeatsClone->getBpm(); + double newValue = m_pBeatsClone->getBpm().getValue(); spinBpm->setValue(newValue); } void DlgTrackInfo::slotBpmFourThirds() { - m_pBeatsClone->scale(mixxx::Beats::FOURTHIRDS); + m_pBeatsClone->scale(mixxx::BeatsInternal::FOURTHIRDS); // read back the actual value - double newValue = m_pBeatsClone->getBpm(); + double newValue = m_pBeatsClone->getBpm().getValue(); spinBpm->setValue(newValue); } void DlgTrackInfo::slotBpmThreeHalves() { - m_pBeatsClone->scale(mixxx::Beats::THREEHALVES); + m_pBeatsClone->scale(mixxx::BeatsInternal::THREEHALVES); // read back the actual value - double newValue = m_pBeatsClone->getBpm(); + double newValue = m_pBeatsClone->getBpm().getValue(); spinBpm->setValue(newValue); } void DlgTrackInfo::slotBpmClear() { spinBpm->setValue(0); - m_pBeatsClone.clear(); + m_pBeatsClone.reset(); bpmConst->setChecked(true); - bpmConst->setEnabled(m_trackHasBeatMap); + bpmConst->setEnabled(!m_bpmIsConst); spinBpm->setEnabled(true); bpmTap->setEnabled(true); } @@ -523,10 +527,12 @@ void DlgTrackInfo::slotBpmConstChanged(int state) { // The cue point should be set on a beat, so this seams // to be a good alternative CuePosition cue = m_pLoadedTrack->getCuePoint(); - m_pBeatsClone = BeatFactory::makeBeatGrid( - *m_pLoadedTrack, spinBpm->value(), cue.getPosition()); + m_pBeatsClone = std::make_shared(m_pLoadedTrack.get()); + // setGrid accepts position in frames thus dividing by 2 + m_pBeatsClone->setGrid(mixxx::Bpm(spinBpm->value()), + mixxx::FramePos(cue.getPosition() / 2.0)); } else { - m_pBeatsClone.clear(); + m_pBeatsClone.reset(); } spinBpm->setEnabled(true); bpmTap->setEnabled(true); @@ -551,27 +557,28 @@ void DlgTrackInfo::slotBpmTap(double averageLength, int numSamples) { void DlgTrackInfo::slotSpinBpmValueChanged(double value) { if (value <= 0) { - m_pBeatsClone.clear(); + m_pBeatsClone.reset(); return; } if (!m_pBeatsClone) { CuePosition cue = m_pLoadedTrack->getCuePoint(); - m_pBeatsClone = BeatFactory::makeBeatGrid( - *m_pLoadedTrack, value, cue.getPosition()); + m_pBeatsClone = std::make_shared(m_pLoadedTrack.get()); + m_pBeatsClone->setGrid(mixxx::Bpm(spinBpm->value()), + mixxx::FramePos(cue.getPosition() / 2.0)); } - double oldValue = m_pBeatsClone->getBpm(); + double oldValue = m_pBeatsClone->getBpm().getValue(); if (oldValue == value) { return; } - if (m_pBeatsClone->getCapabilities() & mixxx::Beats::BEATSCAP_SETBPM) { - m_pBeatsClone->setBpm(value); + if (m_bpmIsConst) { + m_pBeatsClone->setBpm(mixxx::Bpm(value)); } // read back the actual value - double newValue = m_pBeatsClone->getBpm(); + double newValue = m_pBeatsClone->getBpm().getValue(); spinBpm->setValue(newValue); } diff --git a/src/library/dlgtrackinfo.h b/src/library/dlgtrackinfo.h index d4a06eea2bc..6b702fe3cad 100644 --- a/src/library/dlgtrackinfo.h +++ b/src/library/dlgtrackinfo.h @@ -92,7 +92,7 @@ class DlgTrackInfo : public QDialog, public Ui::DlgTrackInfo { TrackPointer m_pLoadedTrack; mixxx::BeatsPointer m_pBeatsClone; Keys m_keysClone; - bool m_trackHasBeatMap; + bool m_bpmIsConst; QScopedPointer m_pTapFilter; QScopedPointer m_pTagFetcher; diff --git a/src/library/rekordbox/rekordboxfeature.cpp b/src/library/rekordbox/rekordboxfeature.cpp index c2ef77205d5..1a8c14ebf57 100644 --- a/src/library/rekordbox/rekordboxfeature.cpp +++ b/src/library/rekordbox/rekordboxfeature.cpp @@ -388,7 +388,9 @@ void insertTrack( query.bindValue(":bitrate", bitrate); query.bindValue(":analyze_path", anlzPath); query.bindValue(":device", device); - query.bindValue(":color", mixxx::RgbColor::toQVariant(colorFromID(static_cast(track->color_id())))); + query.bindValue(":color", + mixxx::RgbColor::toQVariant( + colorFromID(static_cast(track->color_id())))); if (!query.exec()) { LOG_FAILED_QUERY(query); @@ -396,13 +398,13 @@ void insertTrack( int trackID = -1; QSqlQuery finderQuery(database); - finderQuery.prepare("select id from " + kRekordboxLibraryTable + " where rb_id=:rb_id and device=:device"); + finderQuery.prepare("select id from " + kRekordboxLibraryTable + + " where rb_id=:rb_id and device=:device"); finderQuery.bindValue(":rb_id", rbID); finderQuery.bindValue(":device", device); if (!finderQuery.exec()) { - LOG_FAILED_QUERY(finderQuery) - << "rbID:" << rbID; + LOG_FAILED_QUERY(finderQuery) << "rbID:" << rbID; } if (finderQuery.next()) { @@ -415,13 +417,11 @@ void insertTrack( if (!queryInsertIntoDevicePlaylistTracks.exec()) { LOG_FAILED_QUERY(queryInsertIntoDevicePlaylistTracks) - << "trackID:" << trackID - << "position:" << audioFilesCount; + << "trackID:" << trackID << "position:" << audioFilesCount; } } -void buildPlaylistTree( - QSqlDatabase& database, +void buildPlaylistTree(QSqlDatabase& database, TreeItem* parent, uint32_t parentID, QMap& playlistNameMap, @@ -431,11 +431,13 @@ void buildPlaylistTree( QString playlistPath, QString device); -QString parseDeviceDB(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* deviceItem) { +QString parseDeviceDB( + mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* deviceItem) { QString device = deviceItem->getLabel(); QString devicePath = deviceItem->getData().toList()[0].toString(); - qDebug() << "parseDeviceDB device: " << device << " devicePath: " << devicePath; + qDebug() << "parseDeviceDB device: " << device + << " devicePath: " << devicePath; QString dbPath = devicePath + QStringLiteral("/") + kPdbPath; @@ -462,11 +464,11 @@ QString parseDeviceDB(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dev ScopedTransaction transaction(database); QSqlQuery query(database); - query.prepare( - "INSERT INTO " + kRekordboxLibraryTable + + query.prepare("INSERT INTO " + kRekordboxLibraryTable + " (rb_id, artist, title, album, year," "genre,comment,tracknumber,bpm, bitrate,duration, location," - "rating,key,analyze_path,device,color) VALUES (:rb_id, :artist, :title, :album, :year,:genre," + "rating,key,analyze_path,device,color) VALUES (:rb_id, :artist, " + ":title, :album, :year,:genre," ":comment, :tracknumber,:bpm, :bitrate,:duration, :location," ":rating,:key,:analyze_path,:device,:color)"); @@ -476,8 +478,8 @@ QString parseDeviceDB(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dev int playlistID = createDevicePlaylist(database, devicePath); QSqlQuery queryInsertIntoDevicePlaylistTracks(database); - queryInsertIntoDevicePlaylistTracks.prepare( - "INSERT INTO " + kRekordboxPlaylistTracksTable + + queryInsertIntoDevicePlaylistTracks.prepare("INSERT INTO " + + kRekordboxPlaylistTracksTable + " (playlist_id, track_id, position) " "VALUES (:playlist_id, :track_id, :position)"); @@ -518,25 +520,30 @@ QString parseDeviceDB(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dev bool folderOrPlaylistFound = false; - for (int tableOrderIndex = 0; tableOrderIndex < totalTables; tableOrderIndex++) { - for ( - std::vector::iterator table = reckordboxDB.tables()->begin(); + for (int tableOrderIndex = 0; tableOrderIndex < totalTables; + tableOrderIndex++) { + for (std::vector::iterator table = + reckordboxDB.tables()->begin(); table != reckordboxDB.tables()->end(); ++table) { if ((*table)->type() == tableOrder[tableOrderIndex]) { uint16_t lastIndex = (*table)->last_page()->index(); - rekordbox_pdb_t::page_ref_t* currentRef = (*table)->first_page(); + rekordbox_pdb_t::page_ref_t* currentRef = + (*table)->first_page(); while (true) { rekordbox_pdb_t::page_t* page = currentRef->body(); if (page->is_data_page()) { - for ( - std::vector::iterator rowGroup = page->row_groups()->begin(); + for (std::vector:: + iterator rowGroup = + page->row_groups()->begin(); rowGroup != page->row_groups()->end(); ++rowGroup) { - for ( - std::vector::iterator rowRef = (*rowGroup)->rows()->begin(); + for (std::vector:: + iterator rowRef = (*rowGroup) + ->rows() + ->begin(); rowRef != (*rowGroup)->rows()->end(); ++rowRef) { if ((*rowRef)->present()) { @@ -544,49 +551,89 @@ QString parseDeviceDB(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dev case rekordbox_pdb_t::PAGE_TYPE_KEYS: { // Key found, update map rekordbox_pdb_t::key_row_t* key = - static_cast((*rowRef)->body()); - keysMap[key->id()] = getText(key->name()); + static_cast( + (*rowRef)->body()); + keysMap[key->id()] = + getText(key->name()); } break; case rekordbox_pdb_t::PAGE_TYPE_GENRES: { // Genre found, update map rekordbox_pdb_t::genre_row_t* genre = - static_cast((*rowRef)->body()); - genresMap[genre->id()] = getText(genre->name()); + static_cast( + (*rowRef)->body()); + genresMap[genre->id()] = + getText(genre->name()); } break; case rekordbox_pdb_t::PAGE_TYPE_ARTISTS: { // Artist found, update map rekordbox_pdb_t::artist_row_t* artist = - static_cast((*rowRef)->body()); - artistsMap[artist->id()] = getText(artist->name()); + static_cast( + (*rowRef)->body()); + artistsMap[artist->id()] = + getText(artist->name()); } break; case rekordbox_pdb_t::PAGE_TYPE_ALBUMS: { // Album found, update map rekordbox_pdb_t::album_row_t* album = - static_cast((*rowRef)->body()); - albumsMap[album->id()] = getText(album->name()); + static_cast( + (*rowRef)->body()); + albumsMap[album->id()] = + getText(album->name()); } break; - case rekordbox_pdb_t::PAGE_TYPE_PLAYLIST_ENTRIES: { + case rekordbox_pdb_t:: + PAGE_TYPE_PLAYLIST_ENTRIES: { // Playlist to track mapping found, update map - rekordbox_pdb_t::playlist_entry_row_t* playlistEntry = - static_cast((*rowRef)->body()); - playlistTrackMap[playlistEntry->playlist_id()][playlistEntry->entry_index()] = - playlistEntry->track_id(); + rekordbox_pdb_t::playlist_entry_row_t* + playlistEntry = static_cast< + rekordbox_pdb_t:: + playlist_entry_row_t*>( + (*rowRef)->body()); + playlistTrackMap + [playlistEntry->playlist_id()] + [playlistEntry->entry_index()] = + playlistEntry + ->track_id(); } break; case rekordbox_pdb_t::PAGE_TYPE_TRACKS: { // Track found, insert into database - insertTrack( - database, static_cast((*rowRef)->body()), query, queryInsertIntoDevicePlaylistTracks, artistsMap, albumsMap, genresMap, keysMap, devicePath, device, audioFilesCount); + insertTrack(database, + static_cast( + (*rowRef)->body()), + query, + queryInsertIntoDevicePlaylistTracks, + artistsMap, + albumsMap, + genresMap, + keysMap, + devicePath, + device, + audioFilesCount); audioFilesCount++; } break; - case rekordbox_pdb_t::PAGE_TYPE_PLAYLIST_TREE: { + case rekordbox_pdb_t:: + PAGE_TYPE_PLAYLIST_TREE: { // Playlist tree node found, update map - rekordbox_pdb_t::playlist_tree_row_t* playlistTree = - static_cast((*rowRef)->body()); - - playlistNameMap[playlistTree->id()] = getText(playlistTree->name()); - playlistIsFolderMap[playlistTree->id()] = playlistTree->is_folder(); - playlistTreeMap[playlistTree->parent_id()][playlistTree->sort_order()] = playlistTree->id(); + rekordbox_pdb_t::playlist_tree_row_t* + playlistTree = static_cast< + rekordbox_pdb_t:: + playlist_tree_row_t*>( + (*rowRef)->body()); + + playlistNameMap[playlistTree->id()] = + getText(playlistTree->name()); + playlistIsFolderMap[playlistTree + ->id()] = + playlistTree->is_folder(); + playlistTreeMap + [playlistTree->parent_id()] + [playlistTree->sort_order()] = + playlistTree->id(); folderOrPlaylistFound = true; } break; @@ -611,18 +658,26 @@ QString parseDeviceDB(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dev if (audioFilesCount > 0 || folderOrPlaylistFound) { // If we have found anything, recursively build playlist/folder TreeItem children // for the original device TreeItem - buildPlaylistTree(database, deviceItem, 0, playlistNameMap, playlistIsFolderMap, playlistTreeMap, playlistTrackMap, devicePath, device); + buildPlaylistTree(database, + deviceItem, + 0, + playlistNameMap, + playlistIsFolderMap, + playlistTreeMap, + playlistTrackMap, + devicePath, + device); } - qDebug() << "Found: " << audioFilesCount << " audio files in Rekordbox device " << device; + qDebug() << "Found: " << audioFilesCount + << " audio files in Rekordbox device " << device; transaction.commit(); return devicePath; } -void buildPlaylistTree( - QSqlDatabase& database, +void buildPlaylistTree(QSqlDatabase& database, TreeItem* parent, uint32_t parentID, QMap& playlistNameMap, @@ -631,11 +686,14 @@ void buildPlaylistTree( QMap>& playlistTrackMap, QString playlistPath, QString device) { - for (uint32_t childIndex = 0; childIndex < (uint32_t)playlistTreeMap[parentID].size(); childIndex++) { + for (uint32_t childIndex = 0; + childIndex < (uint32_t)playlistTreeMap[parentID].size(); + childIndex++) { uint32_t childID = playlistTreeMap[parentID][childIndex]; QString playlistItemName = playlistNameMap[childID]; - QString currentPath = playlistPath + kPLaylistPathDelimiter + playlistItemName; + QString currentPath = + playlistPath + kPLaylistPathDelimiter + playlistItemName; QList data; @@ -646,8 +704,8 @@ void buildPlaylistTree( // Create a playlist for this child QSqlQuery queryInsertIntoPlaylist(database); - queryInsertIntoPlaylist.prepare( - "INSERT INTO " + kRekordboxPlaylistsTable + + queryInsertIntoPlaylist.prepare("INSERT INTO " + + kRekordboxPlaylistsTable + " (name) " "VALUES (:name)"); @@ -660,12 +718,12 @@ void buildPlaylistTree( } QSqlQuery idQuery(database); - idQuery.prepare("select id from " + kRekordboxPlaylistsTable + " where name=:path"); + idQuery.prepare("select id from " + kRekordboxPlaylistsTable + + " where name=:path"); idQuery.bindValue(":path", currentPath); if (!idQuery.exec()) { - LOG_FAILED_QUERY(idQuery) - << "currentPath" << currentPath; + LOG_FAILED_QUERY(idQuery) << "currentPath" << currentPath; return; } @@ -675,36 +733,42 @@ void buildPlaylistTree( } QSqlQuery queryInsertIntoPlaylistTracks(database); - queryInsertIntoPlaylistTracks.prepare( - "INSERT INTO " + kRekordboxPlaylistTracksTable + + queryInsertIntoPlaylistTracks.prepare("INSERT INTO " + + kRekordboxPlaylistTracksTable + " (playlist_id, track_id, position) " "VALUES (:playlist_id, :track_id, :position)"); if (playlistTrackMap.count(childID)) { // Add playlist tracks for children - for (uint32_t trackIndex = 1; trackIndex <= static_cast(playlistTrackMap[childID].size()); trackIndex++) { + for (uint32_t trackIndex = 1; trackIndex <= + static_cast(playlistTrackMap[childID].size()); + trackIndex++) { uint32_t rbTrackID = playlistTrackMap[childID][trackIndex]; int trackID = -1; QSqlQuery finderQuery(database); - finderQuery.prepare("select id from " + kRekordboxLibraryTable + " where rb_id=:rb_id and device=:device"); + finderQuery.prepare("select id from " + kRekordboxLibraryTable + + " where rb_id=:rb_id and device=:device"); finderQuery.bindValue(":rb_id", rbTrackID); finderQuery.bindValue(":device", device); if (!finderQuery.exec()) { LOG_FAILED_QUERY(finderQuery) - << "rbTrackID:" << rbTrackID - << "device:" << device; + << "rbTrackID:" << rbTrackID << "device:" << device; return; } if (finderQuery.next()) { - trackID = finderQuery.value(finderQuery.record().indexOf("id")).toInt(); + trackID = finderQuery + .value(finderQuery.record().indexOf("id")) + .toInt(); } - queryInsertIntoPlaylistTracks.bindValue(":playlist_id", playlistID); + queryInsertIntoPlaylistTracks.bindValue( + ":playlist_id", playlistID); queryInsertIntoPlaylistTracks.bindValue(":track_id", trackID); - queryInsertIntoPlaylistTracks.bindValue(":position", static_cast(trackIndex)); + queryInsertIntoPlaylistTracks.bindValue( + ":position", static_cast(trackIndex)); if (!queryInsertIntoPlaylistTracks.exec()) { LOG_FAILED_QUERY(queryInsertIntoPlaylistTracks) @@ -719,7 +783,15 @@ void buildPlaylistTree( if (playlistIsFolderMap[childID]) { // If this child is a folder (playlists are only leaf nodes), build playlist tree for it - buildPlaylistTree(database, child, childID, playlistNameMap, playlistIsFolderMap, playlistTreeMap, playlistTrackMap, currentPath, device); + buildPlaylistTree(database, + child, + childID, + playlistNameMap, + playlistIsFolderMap, + playlistTreeMap, + playlistTrackMap, + currentPath, + device); } } } @@ -730,34 +802,39 @@ void clearDeviceTables(QSqlDatabase& database, TreeItem* child) { int trackID = -1; int playlistID = -1; QSqlQuery tracksQuery(database); - tracksQuery.prepare("select id from " + kRekordboxLibraryTable + " where device=:device"); + tracksQuery.prepare("select id from " + kRekordboxLibraryTable + + " where device=:device"); tracksQuery.bindValue(":device", child->getLabel()); QSqlQuery deletePlaylistsQuery(database); - deletePlaylistsQuery.prepare("delete from " + kRekordboxPlaylistsTable + " where id=:id"); + deletePlaylistsQuery.prepare( + "delete from " + kRekordboxPlaylistsTable + " where id=:id"); QSqlQuery deletePlaylistTracksQuery(database); - deletePlaylistTracksQuery.prepare("delete from " + kRekordboxPlaylistTracksTable + " where playlist_id=:playlist_id"); + deletePlaylistTracksQuery.prepare("delete from " + + kRekordboxPlaylistTracksTable + " where playlist_id=:playlist_id"); if (!tracksQuery.exec()) { - LOG_FAILED_QUERY(tracksQuery) - << "device:" << child->getLabel(); + LOG_FAILED_QUERY(tracksQuery) << "device:" << child->getLabel(); } while (tracksQuery.next()) { trackID = tracksQuery.value(tracksQuery.record().indexOf("id")).toInt(); QSqlQuery playlistTracksQuery(database); - playlistTracksQuery.prepare("select playlist_id from " + kRekordboxPlaylistTracksTable + " where track_id=:track_id"); + playlistTracksQuery.prepare("select playlist_id from " + + kRekordboxPlaylistTracksTable + " where track_id=:track_id"); playlistTracksQuery.bindValue(":track_id", trackID); if (!playlistTracksQuery.exec()) { - LOG_FAILED_QUERY(playlistTracksQuery) - << "trackID:" << trackID; + LOG_FAILED_QUERY(playlistTracksQuery) << "trackID:" << trackID; } while (playlistTracksQuery.next()) { - playlistID = playlistTracksQuery.value(playlistTracksQuery.record().indexOf("playlist_id")).toInt(); + playlistID = playlistTracksQuery + .value(playlistTracksQuery.record().indexOf( + "playlist_id")) + .toInt(); deletePlaylistsQuery.bindValue(":id", playlistID); @@ -776,18 +853,23 @@ void clearDeviceTables(QSqlDatabase& database, TreeItem* child) { } QSqlQuery deleteTracksQuery(database); - deleteTracksQuery.prepare("delete from " + kRekordboxLibraryTable + " where device=:device"); + deleteTracksQuery.prepare( + "delete from " + kRekordboxLibraryTable + " where device=:device"); deleteTracksQuery.bindValue(":device", child->getLabel()); if (!deleteTracksQuery.exec()) { - LOG_FAILED_QUERY(deleteTracksQuery) - << "device:" << child->getLabel(); + LOG_FAILED_QUERY(deleteTracksQuery) << "device:" << child->getLabel(); } transaction.commit(); } -void setHotCue(TrackPointer track, double startPosition, double endPosition, int id, QString label, mixxx::RgbColor::optional_t color) { +void setHotCue(TrackPointer track, + double startPosition, + double endPosition, + int id, + QString label, + mixxx::RgbColor::optional_t color) { CuePointer pCue; bool hotCueFound = false; @@ -817,12 +899,17 @@ void setHotCue(TrackPointer track, double startPosition, double endPosition, int } } -void readAnalyze(TrackPointer track, double sampleRate, int timingOffset, bool ignoreCues, QString anlzPath) { +void readAnalyze(TrackPointer track, + double sampleRate, + int timingOffset, + bool ignoreCues, + QString anlzPath) { if (!QFile(anlzPath).exists()) { return; } - qDebug() << "Rekordbox ANLZ path:" << anlzPath << " for: " << track->getTitle(); + qDebug() << "Rekordbox ANLZ path:" << anlzPath + << " for: " << track->getTitle(); std::ifstream ifs(anlzPath.toStdString(), std::ifstream::binary); kaitai::kstream ks(&ifs); @@ -835,18 +922,26 @@ void readAnalyze(TrackPointer track, double sampleRate, int timingOffset, bool i QList memoryCuesAndLoops; int lastHotCueIndex = 0; - for (std::vector::iterator section = anlz.sections()->begin(); section != anlz.sections()->end(); ++section) { + for (std::vector::iterator section = + anlz.sections()->begin(); + section != anlz.sections()->end(); + ++section) { switch ((*section)->fourcc()) { case rekordbox_anlz_t::SECTION_TAGS_BEAT_GRID: { if (!ignoreCues) { break; } - rekordbox_anlz_t::beat_grid_tag_t* beatGridTag = static_cast((*section)->body()); + rekordbox_anlz_t::beat_grid_tag_t* beatGridTag = + static_cast( + (*section)->body()); QVector beats; - for (std::vector::iterator beat = beatGridTag->beats()->begin(); beat != beatGridTag->beats()->end(); ++beat) { + for (std::vector::iterator + beat = beatGridTag->beats()->begin(); + beat != beatGridTag->beats()->end(); + ++beat) { int time = static_cast((*beat)->time()) - timingOffset; // Ensure no offset times are less than 1 if (time < 1) { @@ -857,8 +952,14 @@ void readAnalyze(TrackPointer track, double sampleRate, int timingOffset, bool i QHash extraVersionInfo; - mixxx::BeatsPointer pBeats = BeatFactory::makePreferredBeats( - *track, beats, extraVersionInfo, false, false, sampleRate, 0, 0, 0); + mixxx::BeatsPointer pBeats = BeatFactory::makePreferredBeats(track, + beats, + extraVersionInfo, + false, + false, + 0, + 0, + 0); track->setBeats(pBeats); } break; @@ -867,9 +968,14 @@ void readAnalyze(TrackPointer track, double sampleRate, int timingOffset, bool i break; } - rekordbox_anlz_t::cue_tag_t* cuesTag = static_cast((*section)->body()); + rekordbox_anlz_t::cue_tag_t* cuesTag = + static_cast( + (*section)->body()); - for (std::vector::iterator cueEntry = cuesTag->cues()->begin(); cueEntry != cuesTag->cues()->end(); ++cueEntry) { + for (std::vector::iterator + cueEntry = cuesTag->cues()->begin(); + cueEntry != cuesTag->cues()->end(); + ++cueEntry) { int time = static_cast((*cueEntry)->time()) - timingOffset; // Ensure no offset times are less than 1 if (time < 1) { @@ -888,7 +994,9 @@ void readAnalyze(TrackPointer track, double sampleRate, int timingOffset, bool i memoryCuesAndLoops << memoryCue; } break; case rekordbox_anlz_t::CUE_ENTRY_TYPE_LOOP: { - int endTime = static_cast((*cueEntry)->loop_time()) - timingOffset; + int endTime = + static_cast((*cueEntry)->loop_time()) - + timingOffset; // Ensure no offset times are less than 1 if (endTime < 1) { endTime = 1; @@ -896,19 +1004,20 @@ void readAnalyze(TrackPointer track, double sampleRate, int timingOffset, bool i memory_cue_loop_t loop; loop.startPosition = position; - loop.endPosition = samples * static_cast(endTime); + loop.endPosition = + samples * static_cast(endTime); loop.color = mixxx::RgbColor::nullopt(); memoryCuesAndLoops << loop; } break; } } break; case rekordbox_anlz_t::CUE_LIST_TYPE_HOT_CUES: { - int hotCueIndex = static_cast((*cueEntry)->hot_cue() - 1); + int hotCueIndex = + static_cast((*cueEntry)->hot_cue() - 1); if (hotCueIndex > lastHotCueIndex) { lastHotCueIndex = hotCueIndex; } - setHotCue( - track, + setHotCue(track, position, Cue::kNoPosition, hotCueIndex, @@ -923,10 +1032,16 @@ void readAnalyze(TrackPointer track, double sampleRate, int timingOffset, bool i break; } - rekordbox_anlz_t::cue_extended_tag_t* cuesExtendedTag = static_cast((*section)->body()); + rekordbox_anlz_t::cue_extended_tag_t* cuesExtendedTag = + static_cast( + (*section)->body()); - for (std::vector::iterator cueExtendedEntry = cuesExtendedTag->cues()->begin(); cueExtendedEntry != cuesExtendedTag->cues()->end(); ++cueExtendedEntry) { - int time = static_cast((*cueExtendedEntry)->time()) - timingOffset; + for (std::vector::iterator + cueExtendedEntry = cuesExtendedTag->cues()->begin(); + cueExtendedEntry != cuesExtendedTag->cues()->end(); + ++cueExtendedEntry) { + int time = static_cast((*cueExtendedEntry)->time()) - + timingOffset; // Ensure no offset times are less than 1 if (time < 1) { time = 1; @@ -940,12 +1055,17 @@ void readAnalyze(TrackPointer track, double sampleRate, int timingOffset, bool i memory_cue_loop_t memoryCue; memoryCue.startPosition = position; memoryCue.endPosition = Cue::kNoPosition; - memoryCue.comment = toUnicode((*cueExtendedEntry)->comment()); - memoryCue.color = colorFromID(static_cast((*cueExtendedEntry)->color_id())); + memoryCue.comment = + toUnicode((*cueExtendedEntry)->comment()); + memoryCue.color = colorFromID(static_cast( + (*cueExtendedEntry)->color_id())); memoryCuesAndLoops << memoryCue; } break; case rekordbox_anlz_t::CUE_ENTRY_TYPE_LOOP: { - int endTime = static_cast((*cueExtendedEntry)->loop_time()) - timingOffset; + int endTime = + static_cast( + (*cueExtendedEntry)->loop_time()) - + timingOffset; // Ensure no offset times are less than 1 if (endTime < 1) { endTime = 1; @@ -953,15 +1073,19 @@ void readAnalyze(TrackPointer track, double sampleRate, int timingOffset, bool i memory_cue_loop_t loop; loop.startPosition = position; - loop.endPosition = samples * static_cast(endTime); - loop.comment = toUnicode((*cueExtendedEntry)->comment()); - loop.color = colorFromID(static_cast((*cueExtendedEntry)->color_id())); + loop.endPosition = + samples * static_cast(endTime); + loop.comment = + toUnicode((*cueExtendedEntry)->comment()); + loop.color = colorFromID(static_cast( + (*cueExtendedEntry)->color_id())); memoryCuesAndLoops << loop; } break; } } break; case rekordbox_anlz_t::CUE_LIST_TYPE_HOT_CUES: { - int hotCueIndex = static_cast((*cueExtendedEntry)->hot_cue() - 1); + int hotCueIndex = static_cast( + (*cueExtendedEntry)->hot_cue() - 1); if (hotCueIndex > lastHotCueIndex) { lastHotCueIndex = hotCueIndex; } @@ -987,20 +1111,26 @@ void readAnalyze(TrackPointer track, double sampleRate, int timingOffset, bool i } if (memoryCuesAndLoops.size() > 0) { - std::sort(memoryCuesAndLoops.begin(), memoryCuesAndLoops.end(), [](const memory_cue_loop_t& a, const memory_cue_loop_t& b) -> bool { - return a.startPosition < b.startPosition; - }); + std::sort(memoryCuesAndLoops.begin(), + memoryCuesAndLoops.end(), + [](const memory_cue_loop_t& a, const memory_cue_loop_t& b) + -> bool { return a.startPosition < b.startPosition; }); bool mainCueFound = false; // Add memory cues and loops - for (int memoryCueOrLoopIndex = 0; memoryCueOrLoopIndex < memoryCuesAndLoops.size(); memoryCueOrLoopIndex++) { - memory_cue_loop_t memoryCueOrLoop = memoryCuesAndLoops[memoryCueOrLoopIndex]; - - if (!mainCueFound && memoryCueOrLoop.endPosition == Cue::kNoPosition) { + for (int memoryCueOrLoopIndex = 0; + memoryCueOrLoopIndex < memoryCuesAndLoops.size(); + memoryCueOrLoopIndex++) { + memory_cue_loop_t memoryCueOrLoop = + memoryCuesAndLoops[memoryCueOrLoopIndex]; + + if (!mainCueFound && + memoryCueOrLoop.endPosition == Cue::kNoPosition) { // Set first chronological memory cue as Mixxx MainCue track->setCuePoint(CuePosition(memoryCueOrLoop.startPosition)); - CuePointer pMainCue = track->findCueByType(mixxx::CueType::MainCue); + CuePointer pMainCue = + track->findCueByType(mixxx::CueType::MainCue); pMainCue->setLabel(memoryCueOrLoop.comment); pMainCue->setColor(*memoryCueOrLoop.color); mainCueFound = true; @@ -1008,8 +1138,7 @@ void readAnalyze(TrackPointer track, double sampleRate, int timingOffset, bool i // Mixxx v2.4 will feature multiple loops, so these saved here will be usable // For 2.3, Mixxx treats them as hotcues and the first one will be loaded as the single loop Mixxx supports lastHotCueIndex++; - setHotCue( - track, + setHotCue(track, memoryCueOrLoop.startPosition, memoryCueOrLoop.endPosition, lastHotCueIndex, @@ -1025,7 +1154,12 @@ void readAnalyze(TrackPointer track, double sampleRate, int timingOffset, bool i RekordboxPlaylistModel::RekordboxPlaylistModel(QObject* parent, TrackCollectionManager* trackCollectionManager, QSharedPointer trackSource) - : BaseExternalPlaylistModel(parent, trackCollectionManager, "mixxx.db.model.rekordbox.playlistmodel", kRekordboxPlaylistsTable, kRekordboxPlaylistTracksTable, trackSource) { + : BaseExternalPlaylistModel(parent, + trackCollectionManager, + "mixxx.db.model.rekordbox.playlistmodel", + kRekordboxPlaylistsTable, + kRekordboxPlaylistTracksTable, + trackSource) { } void RekordboxPlaylistModel::initSortColumnMapping() { @@ -1034,35 +1168,67 @@ void RekordboxPlaylistModel::initSortColumnMapping() { m_columnIndexBySortColumnId[i] = -1; } - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ARTIST] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ARTIST); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TITLE] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TITLE); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ALBUM] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUM); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ALBUMARTIST] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_YEAR] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GENRE] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GENRE); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMPOSER] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GROUPING] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GROUPING); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TRACKNUMBER] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_FILETYPE] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_NATIVELOCATION] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMMENT] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMMENT); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DURATION] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BITRATE] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BPM] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_REPLAYGAIN] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DATETIMEADDED] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TIMESPLAYED] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_RATING] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_KEY] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_PREVIEW] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COLOR] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COLOR); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COVERART] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_POSITION] = fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ARTIST] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ARTIST); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TITLE] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TITLE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ALBUM] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUM); + m_columnIndexBySortColumnId + [TrackModel::SortColumnId::SORTCOLUMN_ALBUMARTIST] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_YEAR] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GENRE] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GENRE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMPOSER] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GROUPING] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GROUPING); + m_columnIndexBySortColumnId + [TrackModel::SortColumnId::SORTCOLUMN_TRACKNUMBER] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_FILETYPE] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE); + m_columnIndexBySortColumnId + [TrackModel::SortColumnId::SORTCOLUMN_NATIVELOCATION] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMMENT] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMMENT); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DURATION] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BITRATE] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BPM] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM); + m_columnIndexBySortColumnId + [TrackModel::SortColumnId::SORTCOLUMN_REPLAYGAIN] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN); + m_columnIndexBySortColumnId + [TrackModel::SortColumnId::SORTCOLUMN_DATETIMEADDED] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED); + m_columnIndexBySortColumnId + [TrackModel::SortColumnId::SORTCOLUMN_TIMESPLAYED] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_RATING] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_KEY] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_PREVIEW] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COLOR] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COLOR); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COVERART] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_POSITION] = + fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); m_sortColumnIdByColumnIndex.clear(); for (int i = 0; i < TrackModel::SortColumnId::NUM_SORTCOLUMNIDS; ++i) { - TrackModel::SortColumnId sortColumn = static_cast(i); - m_sortColumnIdByColumnIndex.insert(m_columnIndexBySortColumnId[sortColumn], sortColumn); + TrackModel::SortColumnId sortColumn = + static_cast(i); + m_sortColumnIdByColumnIndex.insert( + m_columnIndexBySortColumnId[sortColumn], sortColumn); } } @@ -1070,7 +1236,9 @@ TrackPointer RekordboxPlaylistModel::getTrack(const QModelIndex& index) const { qDebug() << "RekordboxTrackModel::getTrack"; TrackPointer track = BaseExternalPlaylistModel::getTrack(index); - QString location = index.sibling(index.row(), fieldIndex("location")).data().toString(); + QString location = index.sibling(index.row(), fieldIndex("location")) + .data() + .toString(); if (!QFile(location).exists()) { return track; @@ -1088,9 +1256,11 @@ TrackPointer RekordboxPlaylistModel::getTrack(const QModelIndex& index) const { int timingOffset = 0; if (location.toLower().endsWith(".mp3")) { - int timingShiftCase = mp3guessenc_timing_shift_case(location.toStdString().c_str()); + int timingShiftCase = + mp3guessenc_timing_shift_case(location.toStdString().c_str()); - qDebug() << "Timing shift case:" << timingShiftCase << "for MP3 file:" << location; + qDebug() << "Timing shift case:" << timingShiftCase + << "for MP3 file:" << location; switch (timingShiftCase) { #ifdef __COREAUDIO__ @@ -1127,7 +1297,9 @@ TrackPointer RekordboxPlaylistModel::getTrack(const QModelIndex& index) const { double sampleRate = static_cast(track->getSampleRate()); - QString anlzPath = index.sibling(index.row(), fieldIndex("analyze_path")).data().toString(); + QString anlzPath = index.sibling(index.row(), fieldIndex("analyze_path")) + .data() + .toString(); QString anlzPathExt = anlzPath.left(anlzPath.length() - 3) + "EXT"; if (QFile(anlzPathExt).exists()) { @@ -1140,9 +1312,12 @@ TrackPointer RekordboxPlaylistModel::getTrack(const QModelIndex& index) const { // Assume that the key of the file the has been analyzed in Recordbox is correct // and prevent the AnalyzerKey from re-analyzing. - track->setKeys(KeyFactory::makeBasicKeysFromText(index.sibling(index.row(), fieldIndex("key")).data().toString(), mixxx::track::io::key::USER)); + track->setKeys(KeyFactory::makeBasicKeysFromText( + index.sibling(index.row(), fieldIndex("key")).data().toString(), + mixxx::track::io::key::USER)); - track->setColor(mixxx::RgbColor::fromQVariant(index.sibling(index.row(), fieldIndex("color")).data())); + track->setColor(mixxx::RgbColor::fromQVariant( + index.sibling(index.row(), fieldIndex("color")).data())); return track; } @@ -1155,7 +1330,8 @@ bool RekordboxPlaylistModel::isColumnHiddenByDefault(int column) { } bool RekordboxPlaylistModel::isColumnInternal(int column) { - return column == fieldIndex(ColumnCache::COLUMN_REKORDBOX_ANALYZE_PATH) || BaseExternalPlaylistModel::isColumnInternal(column); + return column == fieldIndex(ColumnCache::COLUMN_REKORDBOX_ANALYZE_PATH) || + BaseExternalPlaylistModel::isColumnInternal(column); } RekordboxFeature::RekordboxFeature( diff --git a/src/library/searchquery.cpp b/src/library/searchquery.cpp index af176ed1977..a08734a316f 100644 --- a/src/library/searchquery.cpp +++ b/src/library/searchquery.cpp @@ -9,6 +9,13 @@ #include "util/db/dbconnection.h" #include "util/db/sqllikewildcards.h" +namespace { +constexpr int kRoundToDecimal = 10; +inline double reducePrecision(double num) { + return round(num * pow(10, kRoundToDecimal)) / pow(10, kRoundToDecimal); +} +} // namespace + QVariant getTrackValueForColumn(const TrackPointer& pTrack, const QString& column) { if (column == LIBRARYTABLE_ARTIST) { return pTrack->getArtist(); @@ -324,13 +331,34 @@ bool NumericFilterNode::match(const TrackPointer& pTrack) const { continue; } - double dValue = value.toDouble(); + const double dValue = value.toDouble(); + + // We are reducing the precision of double since an operation on Beats: getBpm + // results in a BPM that is dynamically calculated instead of using a stored + // precise value thus leading to a slight double precision error with comparison + // operations. + // This approximation can be removed with an implementation of beats + // that stores the BPM explicitly. + // TODO(hacksdump): Restore original functionality with BeatMarker implementation. + const double dValueReducedPrecision = reducePrecision(dValue); + const double dOperatorArgumentReducedPrecision = reducePrecision(m_dOperatorArgument); + if (m_bOperatorQuery) { - if ((m_operator == "=" && dValue == m_dOperatorArgument) || - (m_operator == "<" && dValue < m_dOperatorArgument) || - (m_operator == ">" && dValue > m_dOperatorArgument) || - (m_operator == "<=" && dValue <= m_dOperatorArgument) || - (m_operator == ">=" && dValue >= m_dOperatorArgument)) { + if ((m_operator == "=" && + dValueReducedPrecision == + dOperatorArgumentReducedPrecision) || + (m_operator == "<" && + dValueReducedPrecision < + dOperatorArgumentReducedPrecision) || + (m_operator == ">" && + dValueReducedPrecision > + dOperatorArgumentReducedPrecision) || + (m_operator == "<=" && + dValueReducedPrecision <= + dOperatorArgumentReducedPrecision) || + (m_operator == ">=" && + dValueReducedPrecision >= + dOperatorArgumentReducedPrecision)) { return true; } } else if (m_bRangeQuery && dValue >= m_dRangeLow && diff --git a/src/mixer/basetrackplayer.cpp b/src/mixer/basetrackplayer.cpp index 2058d66d6a4..c70fca9d919 100644 --- a/src/mixer/basetrackplayer.cpp +++ b/src/mixer/basetrackplayer.cpp @@ -14,7 +14,7 @@ #include "mixer/playerinfo.h" #include "mixer/playermanager.h" #include "sources/soundsourceproxy.h" -#include "track/beatgrid.h" +#include "track/beats.h" #include "track/track.h" #include "util/compatibility.h" #include "util/platform.h" @@ -32,7 +32,8 @@ const double kShiftCuesOffsetSmallMillis = 1; inline double trackColorToDouble(mixxx::RgbColor::optional_t color) { return (color ? static_cast(*color) : kNoTrackColor); } -} + +} // anonymous namespace BaseTrackPlayer::BaseTrackPlayer(QObject* pParent, const QString& group) : BasePlayer(pParent, group) { diff --git a/src/proto/beats.proto b/src/proto/beats.proto index 567b7f4e59d..99b83c563a6 100644 --- a/src/proto/beats.proto +++ b/src/proto/beats.proto @@ -1,3 +1,4 @@ +// This proto describes the beats information attached to a track. syntax = "proto2"; package mixxx.track.io; @@ -10,10 +11,42 @@ enum Source { USER = 2; } -message Beat { - optional int32 frame_position = 1; - optional bool enabled = 2 [ default = true ]; - optional Source source = 3 [ default = ANALYZER ]; +enum SectionType { + INTRO = 0; + OUTRO = 1; + OTHER = 2; +} + +// Setting grey as the default color +message Color { + optional int32 red = 1 [ default = 100 ]; + optional int32 green = 2 [ default = 100 ]; + optional int32 blue = 3 [ default = 100 ]; +} + +message TimeSignature { + optional int32 beats_per_bar = 1 [ default = 4]; + optional int32 note_value = 2 [ default = 4 ]; +} + +message TimeSignatureMarker { + optional int32 beat_index = 1 [ default = 0 ]; + optional TimeSignature time_signature = 2; +} + +message PhraseMarker { + optional int32 beat_index = 1 [ default = 0 ]; +} + +message Section { + optional string name = 1 [ default = "UNNAMED"]; + optional SectionType type = 2 [ default = OTHER ]; + optional Color color = 3; +} + +message SectionMarker { + optional int32 beat_index = 1 [default = 0]; + optional Section section = 2; } message Bpm { @@ -21,11 +54,31 @@ message Bpm { optional Source source = 2 [ default = ANALYZER ]; } -message BeatMap { +message Beat { + optional double frame_position = 1 [ default = 0 ]; + optional bool enabled = 2 [ default = true ]; + optional Source source = 3 [ default = ANALYZER ]; +} + +message Beats { repeated Beat beat = 1; + repeated TimeSignatureMarker time_signature_markers = 3; + repeated PhraseMarker phrase_markers = 4; + repeated SectionMarker section_markers = 5; } -message BeatGrid { - optional Bpm bpm = 1; - optional Beat first_beat = 2; +// These are legacy data structures for upgrading from Mixxx < 2.4 +message LegacyBeat { + optional int32 frame_position = 1; + optional bool enabled = 2 [ default = true ]; + optional Source source = 3 [ default = ANALYZER ]; } + +message LegacyBeatMap { + repeated LegacyBeat beat = 1; +} + +message LegacyBeatGrid { + optional Bpm bpm = 1; + optional LegacyBeat first_beat = 2; +} \ No newline at end of file diff --git a/src/skin/tooltips.cpp b/src/skin/tooltips.cpp index 11f10aa2c74..68f97cb3090 100644 --- a/src/skin/tooltips.cpp +++ b/src/skin/tooltips.cpp @@ -205,16 +205,17 @@ void Tooltips::addStandardTooltips() { << tr("Adjusts the headphone output gain.") << QString("%1: %2").arg(rightClick, resetToDefault); - add("headMix") - << tr("Headphone Mix") - << tr("Crossfades the headphone output between the main mix and cueing (PFL or Pre-Fader Listening) signal.") - << QString("%1: %2").arg(rightClick, resetToDefault); - - add("headSplit") - << tr("Headphone Split Cue") - << tr("If activated, the main mix signal plays in the right channel, while the cueing signal " - "plays in the left channel.") - << tr("Adjust the Headphone Mix so in the left channel is not the pure cueing signal."); + add("headMix") << tr("Headphone Mix") + << tr("Crossfades the headphone output between the main mix " + "and cueing (PFL or Pre-Fader Listening) signal.") + << QString("%1: %2").arg(rightClick, resetToDefault); + + add("headSplit") << tr("Headphone Split Cue") + << tr("If activated, the main mix signal plays in the " + "right channel, while the cueing signal " + "plays in the left channel.") + << tr("Adjust the Headphone Mix so in the left channel is " + "not the pure cueing signal."); add("orientation") << tr("Crossfader Orientation") @@ -486,121 +487,137 @@ void Tooltips::addStandardTooltips() { << tr("Sends the selected channel's audio to the headphone output, " "selected in Preferences -> Sound Hardware."); - add("mute") - << tr("Mute") - << tr("Mutes the selected channel's audio in the main output."); - - add("master_enable") - << tr("Main mix enable") - << tr("Hold or short click for latching to " - "mix this input into the main output."); - - add("back_start") - << tr("Fast Rewind") - << QString("%1: %2").arg(leftClick, tr("Fast rewind through the track.")) - << QString("%1: %2").arg(rightClick, tr("Jumps to the beginning of the track.")); - - add("fwd_end") - << tr("Fast Forward") - << QString("%1: %2").arg(leftClick, tr("Fast forward through the track.")) - << QString("%1: %2").arg(rightClick, tr("Jumps to the end of the track.")); - - // Ghetto-Sync (TM) - add("beatsync_beatsync_tempo") - << tr("Old Synchronize") - << tr("(This skin should be updated to use Master Sync!)") - << QString("%1: %2").arg(leftClick, tr("Syncs the tempo (BPM) and phase to that of the other track, " - "if BPM is detected on both.")) - << QString("%1: %2").arg(rightClick, tr("Syncs the tempo (BPM) to that of the other track, " - "if BPM is detected on both.")) - << tr("Syncs to the first deck (in numerical order) that is playing a track and has a BPM.") - << tr("If no deck is playing, syncs to the first deck that has a BPM.") - << tr("Decks can't sync to samplers and samplers can only sync to decks."); - - // Awesome-Sync (TM) - add("sync_enabled") - << tr("Enable Master Sync") - << tr("Tap to sync the tempo to other playing tracks or the master clock.") - << tr("Hold for at least a second to enable sync lock for this deck.") - << tr("Decks with sync locked will all play at the same tempo, and decks that also have " - "quantize enabled will always have their beats lined up."); - - // TODO(owen): find a better phrase for "the other deck" - add("sync_reset_key") - << tr("Sync and Reset Key") - << QString("%1: %2").arg(leftClick, tr("Sets the pitch to a key that allows a harmonic transition " - "from the other track. Requires a detected key on both involved decks.")) - << QString("%1: %2").arg(rightClick, tr("Resets the key to the original track key.")); - - add("sync_master") - << tr("Enable Sync Clock Master") - << tr("When enabled, this device will serve as the master clock for all other decks."); - - add("rate") - << tr("Speed Control") - << tr("Changes the track playback speed (affects both the tempo and the pitch). If keylock is enabled, only the tempo is affected.") - << QString("%1: %2").arg(rightClick, resetToDefault); - - add("pitch") - << tr("Pitch Control") - << tr("Changes the track pitch independent of the tempo.") - << QString("%1: %2").arg(rightClick, resetToDefault); - - add("pitch_up") - << tr("Pitch Control") - << tr("Changes the track pitch independent of the tempo.") - << QString("%1: %2").arg(leftClick, tr("Increases the pitch by one semitone.")) - << QString("%1: %2").arg(rightClick, tr("Increases the pitch by 10 cents.")); - - add("pitch_down") - << tr("Pitch Control") - << tr("Changes the track pitch independent of the tempo.") - << QString("%1: %2").arg(leftClick, tr("Decreases the pitch by one semitone.")) - << QString("%1: %2").arg(rightClick, tr("Decreases the pitch by 10 cents.")); - - add("pitch_adjust") - << tr("Pitch Adjust") - << tr("Adjust the pitch in addition to the speed slider pitch.") - << QString("%1: %2").arg(rightClick, resetToDefault); - - - add("rate_display") - << tr("Pitch Rate") - << tr("Displays the current playback rate of the track."); - - add("repeat") - << tr("Repeat") - << tr("When active the track will repeat if you go past the end or reverse before the start."); - - add("eject") - << tr("Eject") - << tr("Ejects track from the player."); - - add("hotcue") - << tr("Hotcue") - << QString("%1: %2").arg(leftClick, tr("If hotcue is set, jumps to the hotcue.")) - << tr("If hotcue is not set, sets the hotcue to the current play position.") - << quantizeSnap - << QString("%1: %2").arg(rightClick, tr("Opens a menu to clear hotcues or edit their labels and colors.")); - - // Status displays and toggle buttons - add("toggle_recording") - << tr("Record Mix") - << tr("Toggle mix recording."); - - // Status displays and toggle buttons - add("recording_duration") - << tr("Recording Duration") - << tr("Displays the duration of the running recording."); - - // For legacy reasons also add tooltips for "shoutcast_enabled". - for (const char* key : {"shoutcast_enabled", "broadcast_enabled"}) { - add(key) - << tr("Enable Live Broadcasting") - << tr("Stream your mix over the Internet.") - << tr("Provides visual feedback for Live Broadcasting status:") - << tr("disabled, connecting, connected, failure."); - } + add("mute") + << tr("Mute") + << tr("Mutes the selected channel's audio in the main output."); + + add("master_enable") + << tr("Main mix enable") + << tr("Hold or short click for latching to " + "mix this input into the main output."); + + add("back_start") + << tr("Fast Rewind") + << QString("%1: %2").arg(leftClick, tr("Fast rewind through the track.")) + << QString("%1: %2").arg(rightClick, tr("Jumps to the beginning of the track.")); + + add("fwd_end") + << tr("Fast Forward") + << QString("%1: %2").arg(leftClick, tr("Fast forward through the track.")) + << QString("%1: %2").arg(rightClick, tr("Jumps to the end of the track.")); + + // Ghetto-Sync (TM) + add("beatsync_beatsync_tempo") + << tr("Old Synchronize") + << tr("(This skin should be updated to use Master Sync!)") + << QString("%1: %2").arg(leftClick, + tr("Syncs the tempo (BPM) and phase to that of the " + "other track, " + "if BPM is detected on both.")) + << QString("%1: %2").arg(rightClick, + tr("Syncs the tempo (BPM) to that of the other track, " + "if BPM is detected on both.")) + << tr("Syncs to the first deck (in numerical order) that is " + "playing a track and has a BPM.") + << tr("If no deck is playing, syncs to the first deck that has a " + "BPM.") + << tr("Decks can't sync to samplers and samplers can only sync " + "to decks."); + + // Awesome-Sync (TM) + add("sync_enabled") + << tr("Enable Master Sync") + << tr("Tap to sync the tempo to other playing tracks or the " + "master clock.") + << tr("Hold for at least a second to enable sync lock for this " + "deck.") + << tr("Decks with sync locked will all play at the same tempo, " + "and decks that also have " + "quantize enabled will always have their beats lined up."); + + // TODO(owen): find a better phrase for "the other deck" + add("sync_reset_key") + << tr("Sync and Reset Key") + << QString("%1: %2").arg(leftClick, + tr("Sets the pitch to a key that allows a harmonic " + "transition " + "from the other track. Requires a detected key on " + "both involved decks.")) + << QString("%1: %2").arg(rightClick, + tr("Resets the key to the original track key.")); + + add("sync_master") << tr("Enable Sync Clock Master") + << tr("When enabled, this device will serve as the " + "master clock for all other decks."); + + add("rate") << tr("Speed Control") + << tr("Changes the track playback speed (affects both the " + "tempo and the pitch). If keylock is enabled, only the " + "tempo is affected.") + << QString("%1: %2").arg(rightClick, resetToDefault); + + add("pitch") << tr("Pitch Control") + << tr("Changes the track pitch independent of the tempo.") + << QString("%1: %2").arg(rightClick, resetToDefault); + + add("pitch_up") << tr("Pitch Control") + << tr("Changes the track pitch independent of the tempo.") + << QString("%1: %2").arg(leftClick, + tr("Increases the pitch by one semitone.")) + << QString("%1: %2").arg(rightClick, + tr("Increases the pitch by 10 cents.")); + + add("pitch_down") + << tr("Pitch Control") + << tr("Changes the track pitch independent of the tempo.") + << QString("%1: %2").arg( + leftClick, tr("Decreases the pitch by one semitone.")) + << QString("%1: %2").arg( + rightClick, tr("Decreases the pitch by 10 cents.")); + + add("pitch_adjust") + << tr("Pitch Adjust") + << tr("Adjust the pitch in addition to the speed slider pitch.") + << QString("%1: %2").arg(rightClick, resetToDefault); + + add("rate_display") + << tr("Pitch Rate") + << tr("Displays the current playback rate of the track."); + + add("repeat") << tr("Repeat") + << tr("When active the track will repeat if you go past " + "the end or reverse before the start."); + + add("eject") << tr("Eject") << tr("Ejects track from the player."); + + add("hotcue") << tr("Hotcue") + << QString("%1: %2").arg(leftClick, + tr("If hotcue is set, jumps to the hotcue.")) + << tr("If hotcue is not set, sets the hotcue to the " + "current play position.") + << quantizeSnap + << QString("%1: %2").arg(rightClick, + tr("Opens a menu to clear hotcues or edit their " + "labels and colors.")); + + // Status displays and toggle buttons + add("toggle_recording") + << tr("Record Mix") + << tr("Toggle mix recording."); + + // Status displays and toggle buttons + add("recording_duration") + << tr("Recording Duration") + << tr("Displays the duration of the running recording."); + + // For legacy reasons also add tooltips for "shoutcast_enabled". + for (const char* key : {"shoutcast_enabled", "broadcast_enabled"}) { + add(key) + << tr("Enable Live Broadcasting") + << tr("Stream your mix over the Internet.") + << tr("Provides visual feedback for Live Broadcasting status:") + << tr("disabled, connecting, connected, failure."); + } add("passthrough_enabled") << tr("Enable Passthrough") diff --git a/src/test/beatgridtest.cpp b/src/test/beatgridtest.cpp deleted file mode 100644 index d8c1f88c485..00000000000 --- a/src/test/beatgridtest.cpp +++ /dev/null @@ -1,196 +0,0 @@ -#include -#include - -#include "track/beatgrid.h" -#include "util/memory.h" - -using namespace mixxx; - -namespace { - -const double kMaxBeatError = 1e-9; - -TrackPointer newTrack(int sampleRate) { - TrackPointer pTrack(Track::newTemporary()); - pTrack->setAudioProperties( - mixxx::audio::ChannelCount(2), - mixxx::audio::SampleRate(sampleRate), - mixxx::audio::Bitrate(), - mixxx::Duration::fromSeconds(180)); - return pTrack; -} - -TEST(BeatGridTest, Scale) { - int sampleRate = 44100; - TrackPointer pTrack = newTrack(sampleRate); - - double bpm = 60.0; - pTrack->setBpm(bpm); - - auto pGrid = std::make_unique(*pTrack, 0); - pGrid->setBpm(bpm); - - EXPECT_DOUBLE_EQ(bpm, pGrid->getBpm()); - pGrid->scale(Beats::DOUBLE); - EXPECT_DOUBLE_EQ(2 * bpm, pGrid->getBpm()); - - pGrid->scale(Beats::HALVE); - EXPECT_DOUBLE_EQ(bpm, pGrid->getBpm()); - - pGrid->scale(Beats::TWOTHIRDS); - EXPECT_DOUBLE_EQ(bpm * 2 / 3, pGrid->getBpm()); - - pGrid->scale(Beats::THREEHALVES); - EXPECT_DOUBLE_EQ(bpm, pGrid->getBpm()); - - pGrid->scale(Beats::THREEFOURTHS); - EXPECT_DOUBLE_EQ(bpm * 3 / 4, pGrid->getBpm()); - - pGrid->scale(Beats::FOURTHIRDS); - EXPECT_DOUBLE_EQ(bpm, pGrid->getBpm()); -} - -TEST(BeatGridTest, TestNthBeatWhenOnBeat) { - int sampleRate = 44100; - TrackPointer pTrack = newTrack(sampleRate); - - double bpm = 60.1; - const int kFrameSize = 2; - pTrack->setBpm(bpm); - double beatLength = (60.0 * sampleRate / bpm) * kFrameSize; - - auto pGrid = std::make_unique(*pTrack, 0); - pGrid->setBpm(bpm); - // Pretend we're on the 20th beat; - double position = beatLength * 20; - - // The spec dictates that a value of 0 is always invalid and returns -1 - EXPECT_EQ(-1, pGrid->findNthBeat(position, 0)); - - // findNthBeat should return exactly the current beat if we ask for 1 or - // -1. For all other values, it should return n times the beat length. - for (int i = 1; i < 20; ++i) { - EXPECT_NEAR(position + beatLength * (i - 1), pGrid->findNthBeat(position, i), kMaxBeatError); - EXPECT_NEAR(position + beatLength * (-i + 1), pGrid->findNthBeat(position, -i), kMaxBeatError); - } - - // Also test prev/next beat calculation. - double prevBeat, nextBeat; - pGrid->findPrevNextBeats(position, &prevBeat, &nextBeat); - EXPECT_NEAR(position, prevBeat, kMaxBeatError); - EXPECT_NEAR(position + beatLength, nextBeat, kMaxBeatError); - - // Both previous and next beat should return the current position. - EXPECT_NEAR(position, pGrid->findNextBeat(position), kMaxBeatError); - EXPECT_NEAR(position, pGrid->findPrevBeat(position), kMaxBeatError); -} - -TEST(BeatGridTest, TestNthBeatWhenOnBeat_BeforeEpsilon) { - int sampleRate = 44100; - TrackPointer pTrack = newTrack(sampleRate); - - double bpm = 60.1; - const int kFrameSize = 2; - pTrack->setBpm(bpm); - double beatLength = (60.0 * sampleRate / bpm) * kFrameSize; - - auto pGrid = std::make_unique(*pTrack, 0); - pGrid->setBpm(bpm); - - // Pretend we're just before the 20th beat. - const double kClosestBeat = 20 * beatLength; - double position = kClosestBeat - beatLength * 0.005; - - // The spec dictates that a value of 0 is always invalid and returns -1 - EXPECT_EQ(-1, pGrid->findNthBeat(position, 0)); - - // findNthBeat should return exactly the current beat if we ask for 1 or - // -1. For all other values, it should return n times the beat length. - for (int i = 1; i < 20; ++i) { - EXPECT_NEAR(kClosestBeat + beatLength * (i - 1), pGrid->findNthBeat(position, i), kMaxBeatError); - EXPECT_NEAR(kClosestBeat + beatLength * (-i + 1), pGrid->findNthBeat(position, -i), kMaxBeatError); - } - - // Also test prev/next beat calculation. - double prevBeat, nextBeat; - pGrid->findPrevNextBeats(position, &prevBeat, &nextBeat); - EXPECT_NEAR(kClosestBeat, prevBeat, kMaxBeatError); - EXPECT_NEAR(kClosestBeat + beatLength, nextBeat, kMaxBeatError); - - // Both previous and next beat should return the closest beat. - EXPECT_NEAR(kClosestBeat, pGrid->findNextBeat(position), kMaxBeatError); - EXPECT_NEAR(kClosestBeat, pGrid->findPrevBeat(position), kMaxBeatError); -} - -TEST(BeatGridTest, TestNthBeatWhenOnBeat_AfterEpsilon) { - int sampleRate = 44100; - TrackPointer pTrack = newTrack(sampleRate); - - double bpm = 60.1; - const int kFrameSize = 2; - pTrack->setBpm(bpm); - double beatLength = (60.0 * sampleRate / bpm) * kFrameSize; - - auto pGrid = std::make_unique(*pTrack, 0); - pGrid->setBpm(bpm); - - // Pretend we're just before the 20th beat. - const double kClosestBeat = 20 * beatLength; - double position = kClosestBeat + beatLength * 0.005; - - // The spec dictates that a value of 0 is always invalid and returns -1 - EXPECT_EQ(-1, pGrid->findNthBeat(position, 0)); - - // findNthBeat should return exactly the current beat if we ask for 1 or - // -1. For all other values, it should return n times the beat length. - for (int i = 1; i < 20; ++i) { - EXPECT_NEAR(kClosestBeat + beatLength * (i - 1), pGrid->findNthBeat(position, i), kMaxBeatError); - EXPECT_NEAR(kClosestBeat + beatLength * (-i + 1), pGrid->findNthBeat(position, -i), kMaxBeatError); - } - - // Also test prev/next beat calculation. - double prevBeat, nextBeat; - pGrid->findPrevNextBeats(position, &prevBeat, &nextBeat); - EXPECT_NEAR(kClosestBeat, prevBeat, kMaxBeatError); - EXPECT_NEAR(kClosestBeat + beatLength, nextBeat, kMaxBeatError); - - // Both previous and next beat should return the closest beat. - EXPECT_NEAR(kClosestBeat, pGrid->findNextBeat(position), kMaxBeatError); - EXPECT_NEAR(kClosestBeat, pGrid->findPrevBeat(position), kMaxBeatError); -} - -TEST(BeatGridTest, TestNthBeatWhenNotOnBeat) { - int sampleRate = 44100; - TrackPointer pTrack = newTrack(sampleRate); - - double bpm = 60.1; - const int kFrameSize = 2; - pTrack->setBpm(bpm); - double beatLength = (60.0 * sampleRate / bpm) * kFrameSize; - - auto pGrid = std::make_unique(*pTrack, 0); - pGrid->setBpm(bpm); - - // Pretend we're half way between the 20th and 21st beat - double previousBeat = beatLength * 20.0; - double nextBeat = beatLength * 21.0; - double position = (nextBeat + previousBeat) / 2.0; - - // The spec dictates that a value of 0 is always invalid and returns -1 - EXPECT_EQ(-1, pGrid->findNthBeat(position, 0)); - - // findNthBeat should return multiples of beats starting from the next or - // previous beat, depending on whether N is positive or negative. - for (int i = 1; i < 20; ++i) { - EXPECT_NEAR(nextBeat + beatLength*(i-1), pGrid->findNthBeat(position, i), kMaxBeatError); - EXPECT_NEAR(previousBeat + beatLength*(-i+1), pGrid->findNthBeat(position, -i), kMaxBeatError); - } - - // Also test prev/next beat calculation - double foundPrevBeat, foundNextBeat; - pGrid->findPrevNextBeats(position, &foundPrevBeat, &foundNextBeat); - EXPECT_NEAR(previousBeat, foundPrevBeat, kMaxBeatError); - EXPECT_NEAR(nextBeat, foundNextBeat, kMaxBeatError); -} - -} // namespace diff --git a/src/test/beatmaptest.cpp b/src/test/beatmaptest.cpp deleted file mode 100644 index 4692fc191de..00000000000 --- a/src/test/beatmaptest.cpp +++ /dev/null @@ -1,296 +0,0 @@ -#include -#include - -#include "track/beatmap.h" -#include "util/memory.h" - -using namespace mixxx; - -namespace { - -class BeatMapTest : public testing::Test { - protected: - - BeatMapTest() - : m_pTrack(Track::newTemporary()), - m_iSampleRate(100), - m_iFrameSize(2) { - m_pTrack->setAudioProperties( - mixxx::audio::ChannelCount(2), - mixxx::audio::SampleRate(m_iSampleRate), - mixxx::audio::Bitrate(), - mixxx::Duration::fromSeconds(180)); - } - - double getBeatLengthFrames(double bpm) { - return (60.0 * m_iSampleRate / bpm); - } - - double getBeatLengthSamples(double bpm) { - return getBeatLengthFrames(bpm) * m_iFrameSize; - } - - QVector createBeatVector(double first_beat, - unsigned int num_beats, - double beat_length) { - QVector beats; - for (unsigned int i = 0; i < num_beats; ++i) { - beats.append(first_beat + i * beat_length); - } - return beats; - } - - TrackPointer m_pTrack; - int m_iSampleRate; - int m_iFrameSize; -}; - -TEST_F(BeatMapTest, Scale) { - const double bpm = 60.0; - m_pTrack->setBpm(bpm); - double beatLengthFrames = getBeatLengthFrames(bpm); - double startOffsetFrames = 7; - const int numBeats = 100; - // Note beats must be in frames, not samples. - QVector beats = createBeatVector(startOffsetFrames, numBeats, beatLengthFrames); - auto pMap = std::make_unique(*m_pTrack, 0, beats); - - EXPECT_DOUBLE_EQ(bpm, pMap->getBpm()); - pMap->scale(Beats::DOUBLE); - EXPECT_DOUBLE_EQ(2 * bpm, pMap->getBpm()); - - pMap->scale(Beats::HALVE); - EXPECT_DOUBLE_EQ(bpm, pMap->getBpm()); - - pMap->scale(Beats::TWOTHIRDS); - EXPECT_DOUBLE_EQ(bpm * 2 / 3, pMap->getBpm()); - - pMap->scale(Beats::THREEHALVES); - EXPECT_DOUBLE_EQ(bpm, pMap->getBpm()); - - pMap->scale(Beats::THREEFOURTHS); - EXPECT_DOUBLE_EQ(bpm * 3 / 4, pMap->getBpm()); - - pMap->scale(Beats::FOURTHIRDS); - EXPECT_DOUBLE_EQ(bpm, pMap->getBpm()); -} - -TEST_F(BeatMapTest, TestNthBeat) { - const double bpm = 60.0; - m_pTrack->setBpm(bpm); - double beatLengthFrames = getBeatLengthFrames(bpm); - double startOffsetFrames = 7; - double beatLengthSamples = getBeatLengthSamples(bpm); - double startOffsetSamples = startOffsetFrames * 2; - const int numBeats = 100; - // Note beats must be in frames, not samples. - QVector beats = createBeatVector(startOffsetFrames, numBeats, beatLengthFrames); - auto pMap = std::make_unique(*m_pTrack, 0, beats); - - // Check edge cases - double firstBeat = startOffsetSamples + beatLengthSamples * 0; - double lastBeat = startOffsetSamples + beatLengthSamples * (numBeats - 1); - EXPECT_EQ(lastBeat, pMap->findNthBeat(lastBeat, 1)); - EXPECT_EQ(lastBeat, pMap->findNextBeat(lastBeat)); - EXPECT_EQ(-1, pMap->findNthBeat(lastBeat, 2)); - EXPECT_EQ(firstBeat, pMap->findNthBeat(firstBeat, -1)); - EXPECT_EQ(firstBeat, pMap->findPrevBeat(firstBeat)); - EXPECT_EQ(-1, pMap->findNthBeat(firstBeat, -2)); - - double prevBeat, nextBeat; - pMap->findPrevNextBeats(lastBeat, &prevBeat, &nextBeat); - EXPECT_EQ(lastBeat, prevBeat); - EXPECT_EQ(-1, nextBeat); - - pMap->findPrevNextBeats(firstBeat, &prevBeat, &nextBeat); - EXPECT_EQ(firstBeat, prevBeat); - EXPECT_EQ(firstBeat + beatLengthSamples, nextBeat); -} - -TEST_F(BeatMapTest, TestNthBeatWhenOnBeat) { - const double bpm = 60.0; - m_pTrack->setBpm(bpm); - double beatLengthFrames = getBeatLengthFrames(bpm); - double startOffsetFrames = 7; - double beatLengthSamples = getBeatLengthSamples(bpm); - double startOffsetSamples = startOffsetFrames * 2; - const int numBeats = 100; - // Note beats must be in frames, not samples. - QVector beats = createBeatVector(startOffsetFrames, numBeats, beatLengthFrames); - auto pMap = std::make_unique(*m_pTrack, 0, beats); - - // Pretend we're on the 20th beat; - const int curBeat = 20; - double position = startOffsetSamples + beatLengthSamples * curBeat; - - // The spec dictates that a value of 0 is always invalid and returns -1 - EXPECT_EQ(-1, pMap->findNthBeat(position, 0)); - - // findNthBeat should return exactly the current beat if we ask for 1 or - // -1. For all other values, it should return n times the beat length. - for (int i = 1; i < curBeat; ++i) { - EXPECT_DOUBLE_EQ(position + beatLengthSamples*(i-1), pMap->findNthBeat(position, i)); - EXPECT_DOUBLE_EQ(position + beatLengthSamples*(-i+1), pMap->findNthBeat(position, -i)); - } - - // Also test prev/next beat calculation. - double prevBeat, nextBeat; - pMap->findPrevNextBeats(position, &prevBeat, &nextBeat); - EXPECT_EQ(position, prevBeat); - EXPECT_EQ(position + beatLengthSamples, nextBeat); - - // Both previous and next beat should return the current position. - EXPECT_EQ(position, pMap->findNextBeat(position)); - EXPECT_EQ(position, pMap->findPrevBeat(position)); -} - -TEST_F(BeatMapTest, TestNthBeatWhenOnBeat_BeforeEpsilon) { - const double bpm = 60.0; - m_pTrack->setBpm(bpm); - double beatLengthFrames = getBeatLengthFrames(bpm); - double startOffsetFrames = 7; - double beatLengthSamples = getBeatLengthSamples(bpm); - double startOffsetSamples = startOffsetFrames * 2; - const int numBeats = 100; - // Note beats must be in frames, not samples. - QVector beats = createBeatVector(startOffsetFrames, numBeats, beatLengthFrames); - auto pMap = std::make_unique(*m_pTrack, 0, beats); - - // Pretend we're just before the 20th beat; - const int curBeat = 20; - const double kClosestBeat = startOffsetSamples + curBeat * beatLengthSamples; - double position = kClosestBeat - beatLengthSamples * 0.005; - - // The spec dictates that a value of 0 is always invalid and returns -1 - EXPECT_EQ(-1, pMap->findNthBeat(position, 0)); - - // findNthBeat should return exactly the current beat if we ask for 1 or - // -1. For all other values, it should return n times the beat length. - for (int i = 1; i < curBeat; ++i) { - EXPECT_DOUBLE_EQ(kClosestBeat + beatLengthSamples*(i-1), pMap->findNthBeat(position, i)); - EXPECT_DOUBLE_EQ(kClosestBeat + beatLengthSamples*(-i+1), pMap->findNthBeat(position, -i)); - } - - // Also test prev/next beat calculation - double prevBeat, nextBeat; - pMap->findPrevNextBeats(position, &prevBeat, &nextBeat); - EXPECT_EQ(kClosestBeat, prevBeat); - EXPECT_EQ(kClosestBeat + beatLengthSamples, nextBeat); - - // Both previous and next beat should return the closest beat. - EXPECT_EQ(kClosestBeat, pMap->findNextBeat(position)); - EXPECT_EQ(kClosestBeat, pMap->findPrevBeat(position)); - -} - -TEST_F(BeatMapTest, TestNthBeatWhenOnBeat_AfterEpsilon) { - const double bpm = 60.0; - m_pTrack->setBpm(bpm); - double beatLengthFrames = getBeatLengthFrames(bpm); - double startOffsetFrames = 7; - double beatLengthSamples = getBeatLengthSamples(bpm); - double startOffsetSamples = startOffsetFrames * 2; - const int numBeats = 100; - // Note beats must be in frames, not samples. - QVector beats = createBeatVector(startOffsetFrames, numBeats, beatLengthFrames); - auto pMap = std::make_unique(*m_pTrack, 0, beats); - - // Pretend we're just after the 20th beat; - const int curBeat = 20; - const double kClosestBeat = startOffsetSamples + curBeat * beatLengthSamples; - double position = kClosestBeat + beatLengthSamples * 0.005; - - // The spec dictates that a value of 0 is always invalid and returns -1 - EXPECT_EQ(-1, pMap->findNthBeat(position, 0)); - - EXPECT_EQ(kClosestBeat, pMap->findClosestBeat(position)); - - // findNthBeat should return exactly the current beat if we ask for 1 or - // -1. For all other values, it should return n times the beat length. - for (int i = 1; i < curBeat; ++i) { - EXPECT_DOUBLE_EQ(kClosestBeat + beatLengthSamples*(i-1), pMap->findNthBeat(position, i)); - EXPECT_DOUBLE_EQ(kClosestBeat + beatLengthSamples*(-i+1), pMap->findNthBeat(position, -i)); - } - - // Also test prev/next beat calculation. - double prevBeat, nextBeat; - pMap->findPrevNextBeats(position, &prevBeat, &nextBeat); - EXPECT_EQ(kClosestBeat, prevBeat); - EXPECT_EQ(kClosestBeat + beatLengthSamples, nextBeat); - - // Both previous and next beat should return the closest beat. - EXPECT_EQ(kClosestBeat, pMap->findNextBeat(position)); - EXPECT_EQ(kClosestBeat, pMap->findPrevBeat(position)); -} - -TEST_F(BeatMapTest, TestNthBeatWhenNotOnBeat) { - const double bpm = 60.0; - m_pTrack->setBpm(bpm); - double beatLengthFrames = getBeatLengthFrames(bpm); - double startOffsetFrames = 7; - double beatLengthSamples = getBeatLengthSamples(bpm); - double startOffsetSamples = startOffsetFrames * 2; - const int numBeats = 100; - // Note beats must be in frames, not samples. - QVector beats = createBeatVector(startOffsetFrames, numBeats, beatLengthFrames); - auto pMap = std::make_unique(*m_pTrack, 0, beats); - - // Pretend we're half way between the 20th and 21st beat - double previousBeat = startOffsetSamples + beatLengthSamples * 20.0; - double nextBeat = startOffsetSamples + beatLengthSamples * 21.0; - double position = (nextBeat + previousBeat) / 2.0; - - // The spec dictates that a value of 0 is always invalid and returns -1 - EXPECT_EQ(-1, pMap->findNthBeat(position, 0)); - - // findNthBeat should return multiples of beats starting from the next or - // previous beat, depending on whether N is positive or negative. - for (int i = 1; i < 20; ++i) { - EXPECT_DOUBLE_EQ(nextBeat + beatLengthSamples*(i-1), - pMap->findNthBeat(position, i)); - EXPECT_DOUBLE_EQ(previousBeat - beatLengthSamples*(i-1), - pMap->findNthBeat(position, -i)); - } - - // Also test prev/next beat calculation - double foundPrevBeat, foundNextBeat; - pMap->findPrevNextBeats(position, &foundPrevBeat, &foundNextBeat); - EXPECT_EQ(previousBeat, foundPrevBeat); - EXPECT_EQ(nextBeat, foundNextBeat); -} - -TEST_F(BeatMapTest, TestBpmAround) { - const double filebpm = 60.0; - double approx_beat_length = getBeatLengthSamples(filebpm); - m_pTrack->setBpm(filebpm); - const int numBeats = 64; - - QVector beats; - double beat_pos = 0; - for (unsigned int i = 0, bpm=60; i < numBeats; ++i, ++bpm) { - double beat_length = getBeatLengthFrames(bpm); - beats.append(beat_pos); - beat_pos += beat_length; - } - - auto pMap = std::make_unique(*m_pTrack, 0, beats); - - // The average of the first 8 beats should be different than the average - // of the last 8 beats. - EXPECT_DOUBLE_EQ(64.024390243902445, - pMap->getBpmAroundPosition(4 * approx_beat_length, 4)); - EXPECT_DOUBLE_EQ(118.98016997167139, - pMap->getBpmAroundPosition(60 * approx_beat_length, 4)); - // Also test at the beginning and end of the track - EXPECT_DOUBLE_EQ(62.968515742128936, - pMap->getBpmAroundPosition(0, 4)); - EXPECT_DOUBLE_EQ(118.98016997167139, - pMap->getBpmAroundPosition(65 * approx_beat_length, 4)); - - // Try a really, really short track - beats = createBeatVector(10, 3, getBeatLengthFrames(filebpm)); - pMap = std::make_unique(*m_pTrack, 0, beats); - EXPECT_DOUBLE_EQ(filebpm, pMap->getBpmAroundPosition(1 * approx_beat_length, 4)); -} - -} // namespace diff --git a/src/test/beatstest.cpp b/src/test/beatstest.cpp new file mode 100644 index 00000000000..e9b67d84ea7 --- /dev/null +++ b/src/test/beatstest.cpp @@ -0,0 +1,409 @@ +#include + +#include + +#include "track/beats.h" +#include "track/track.h" +#include "util/memory.h" + +using namespace mixxx; + +namespace { + +class BeatsTest : public testing::Test { + protected: + BeatsTest() + : m_pTrack(Track::newTemporary()), + m_iChannelCount(2), + m_iSampleRate(100), + m_pBeats1(new Beats(m_pTrack.get())), + m_pBeats2(new Beats(m_pTrack.get())), + m_bpm(60), + m_startOffsetFrames(7) { + m_pTrack->setAudioProperties( + mixxx::audio::ChannelCount(m_iChannelCount), + mixxx::audio::SampleRate(m_iSampleRate), + mixxx::audio::Bitrate(), + mixxx::Duration::fromSeconds(180)); + m_pBeats1->setGrid(m_bpm, m_startOffsetFrames); + m_pBeats2->setGrid(m_bpm, m_startOffsetFrames); + } + + ~BeatsTest() { + } + + FrameDiff_t getBeatLengthFrames(Bpm bpm) const { + if (bpm == Bpm()) { + DEBUG_ASSERT(false); + return 0; + } + return 60.0 * m_iSampleRate / bpm.getValue(); + } + + QVector createBeatVector(FramePos first_beat, + unsigned int num_beats, + FrameDiff_t beat_length) { + QVector beats; + for (unsigned int i = 0; i < num_beats; ++i) { + beats.append(first_beat + beat_length * i); + } + return beats; + } + + TrackPointer m_pTrack; + const int m_iChannelCount; + // Sample Rate is a standard unit + const SINT m_iSampleRate; + // We internally use frames per second since a frame position + // actually matters when calculating beats. + BeatsPointer m_pBeats1; + BeatsPointer m_pBeats2; + const Bpm m_bpm; + const FramePos m_startOffsetFrames; +}; + +TEST_F(BeatsTest, Scale) { + // Initially must be the base value + EXPECT_EQ(m_bpm, m_pBeats1->getBpm()); + + m_pBeats1->scale(BeatsInternal::DOUBLE); + EXPECT_EQ(m_bpm * 2, m_pBeats1->getBpm()); + + m_pBeats1->scale(BeatsInternal::HALVE); + EXPECT_EQ(m_bpm, m_pBeats1->getBpm()); + + m_pBeats1->scale(BeatsInternal::TWOTHIRDS); + EXPECT_EQ(m_bpm * 2 / 3, m_pBeats1->getBpm()); + + m_pBeats1->scale(BeatsInternal::THREEHALVES); + EXPECT_EQ(m_bpm, m_pBeats1->getBpm()); + + m_pBeats1->scale(BeatsInternal::THREEFOURTHS); + EXPECT_EQ(m_bpm * 3 / 4, m_pBeats1->getBpm()); + + m_pBeats1->scale(BeatsInternal::FOURTHIRDS); + EXPECT_EQ(m_bpm, m_pBeats1->getBpm()); +} + +TEST_F(BeatsTest, NthBeat) { + // Check edge cases + EXPECT_EQ(m_pBeats1->getLastBeatPosition(), + m_pBeats1->findNthBeat(m_pBeats1->getLastBeatPosition(), 1)); + EXPECT_EQ(m_pBeats1->getLastBeatPosition(), + m_pBeats1->findNextBeat(m_pBeats1->getLastBeatPosition())); + EXPECT_EQ(kInvalidFramePos, + m_pBeats1->findNthBeat(m_pBeats1->getLastBeatPosition(), 2)); + EXPECT_EQ(m_pBeats1->getFirstBeatPosition(), + m_pBeats1->findNthBeat(m_pBeats1->getFirstBeatPosition(), -1)); + EXPECT_EQ(m_pBeats1->getFirstBeatPosition(), + m_pBeats1->findPrevBeat(m_pBeats1->getFirstBeatPosition())); + EXPECT_EQ(kInvalidFramePos, + m_pBeats1->findNthBeat(m_pBeats1->getFirstBeatPosition(), -2)); + + // TODO(JVC) Add some tests in the middle +} + +TEST_F(BeatsTest, PrevNextBeats) { + FramePos prevBeat, nextBeat; + + m_pBeats1->findPrevNextBeats( + m_pBeats1->getLastBeatPosition(), &prevBeat, &nextBeat); + EXPECT_DOUBLE_EQ( + m_pBeats1->getLastBeatPosition().getValue(), prevBeat.getValue()); + EXPECT_EQ(kInvalidFramePos, nextBeat); + + m_pBeats1->findPrevNextBeats( + m_pBeats1->getFirstBeatPosition(), &prevBeat, &nextBeat); + EXPECT_DOUBLE_EQ( + m_pBeats1->getFirstBeatPosition().getValue(), prevBeat.getValue()); + EXPECT_DOUBLE_EQ( + (m_pBeats1->getFirstBeatPosition() + getBeatLengthFrames(m_bpm)) + .getValue(), + nextBeat.getValue()); + + // TODO(JVC) Add some tests in the middle +} + +TEST_F(BeatsTest, NthBeatWhenOnBeat) { + // Pretend we're on the 20th beat; + const int curBeat = 20; + FramePos position = m_startOffsetFrames + getBeatLengthFrames(m_bpm) * curBeat; + + // The spec dictates that a value of 0 is always invalid + EXPECT_EQ(kInvalidFramePos, m_pBeats1->findNthBeat(position, 0)); + + // findNthBeat should return exactly the current beat if we ask for 1 or + // -1. For all other values, it should return n times the beat length. + for (int i = 1; i < curBeat; ++i) { + EXPECT_DOUBLE_EQ( + (position + getBeatLengthFrames(m_bpm) * (i - 1)).getValue(), + m_pBeats1->findNthBeat(position, i).getValue()); + EXPECT_DOUBLE_EQ( + (position + getBeatLengthFrames(m_bpm) * (-i + 1)).getValue(), + m_pBeats1->findNthBeat(position, -i).getValue()); + } + + // Also test prev/next beat calculation. + FramePos prevBeat, nextBeat; + m_pBeats1->findPrevNextBeats(position, &prevBeat, &nextBeat); + EXPECT_EQ(position, prevBeat); + EXPECT_EQ(position + getBeatLengthFrames(m_bpm), nextBeat); + + // Both previous and next beat should return the current position. + EXPECT_EQ(position, m_pBeats1->findNextBeat(position)); + EXPECT_EQ(position, m_pBeats1->findPrevBeat(position)); +} + +TEST_F(BeatsTest, NthBeatWhenOnBeat_BeforeEpsilon) { + // Pretend we're just before the 20th beat; + const int curBeat = 20; + const FramePos kClosestBeat = m_startOffsetFrames + curBeat * getBeatLengthFrames(m_bpm); + FramePos position = kClosestBeat - getBeatLengthFrames(m_bpm) * 0.005; + + // The spec dictates that a value of 0 is always invalid and returns -1 + EXPECT_EQ(kInvalidFramePos, m_pBeats1->findNthBeat(position, 0)); + + // findNthBeat should return exactly the current beat if we ask for 1 or + // -1. For all other values, it should return n times the beat length. + for (int i = 1; i < curBeat; ++i) { + EXPECT_DOUBLE_EQ((kClosestBeat + getBeatLengthFrames(m_bpm) * (i - 1)).getValue(), + m_pBeats1->findNthBeat(position, i).getValue()); + EXPECT_DOUBLE_EQ((kClosestBeat + getBeatLengthFrames(m_bpm) * (-i + 1)).getValue(), + m_pBeats1->findNthBeat(position, -i).getValue()); + } + + // Also test prev/next beat calculation + FramePos prevBeat, nextBeat; + m_pBeats1->findPrevNextBeats(position, &prevBeat, &nextBeat); + EXPECT_EQ(kClosestBeat, prevBeat); + EXPECT_EQ(kClosestBeat + getBeatLengthFrames(m_bpm), nextBeat); + + // Both previous and next beat should return the closest beat. + EXPECT_EQ(kClosestBeat, m_pBeats1->findNextBeat(position)); + EXPECT_EQ(kClosestBeat, m_pBeats1->findPrevBeat(position)); +} + +TEST_F(BeatsTest, NthBeatWhenOnBeat_AfterEpsilon) { + // Pretend we're just after the 20th beat; + const int curBeat = 20; + const FramePos kClosestBeat = m_startOffsetFrames + curBeat * getBeatLengthFrames(m_bpm); + FramePos position = + kClosestBeat + getBeatLengthFrames(m_bpm) * 0.005; + + // The spec dictates that a value of 0 is always invalid + EXPECT_EQ(kInvalidFramePos, m_pBeats1->findNthBeat(position, 0)); + + EXPECT_EQ(kClosestBeat, m_pBeats1->findClosestBeat(position)); + + // findNthBeat should return exactly the current beat if we ask for 1 or + // -1. For all other values, it should return n times the beat length. + for (int i = 1; i < curBeat; ++i) { + EXPECT_DOUBLE_EQ( + (kClosestBeat + getBeatLengthFrames(m_bpm) * (i - 1)).getValue(), + m_pBeats1->findNthBeat(position, i).getValue()); + EXPECT_DOUBLE_EQ( + (kClosestBeat + getBeatLengthFrames(m_bpm) * (-i + 1)).getValue(), + m_pBeats1->findNthBeat(position, -i).getValue()); + } + + // Also test prev/next beat calculation. + FramePos prevBeat, nextBeat; + m_pBeats1->findPrevNextBeats(position, &prevBeat, &nextBeat); + EXPECT_EQ(kClosestBeat, prevBeat); + EXPECT_EQ(kClosestBeat + getBeatLengthFrames(m_bpm), nextBeat); + + // Both previous and next beat should return the closest beat. + EXPECT_EQ(kClosestBeat, m_pBeats1->findNextBeat(position)); + EXPECT_EQ(kClosestBeat, m_pBeats1->findPrevBeat(position)); +} + +TEST_F(BeatsTest, NthBeatWhenNotOnBeat) { + // Pretend we're half way between the 20th and 21st beat + FramePos previousBeat = + m_startOffsetFrames + getBeatLengthFrames(m_bpm) * 20.0; + FramePos nextBeat = + m_startOffsetFrames + getBeatLengthFrames(m_bpm) * 21.0; + FramePos position = FramePos((previousBeat.getValue() + nextBeat.getValue()) / 2); + + // The spec dictates that a value of 0 is always invalid + EXPECT_EQ(kInvalidFramePos, m_pBeats1->findNthBeat(position, 0)); + + // findNthBeat should return multiples of beats starting from the next or + // previous beat, depending on whether N is positive or negative. + for (int i = 1; i < 20; ++i) { + EXPECT_DOUBLE_EQ( + (nextBeat + getBeatLengthFrames(m_bpm) * (i - 1)) + .getValue(), + m_pBeats1->findNthBeat(position, i).getValue()); + EXPECT_DOUBLE_EQ( + (previousBeat - getBeatLengthFrames(m_bpm) * (i - 1)) + .getValue(), + m_pBeats1->findNthBeat(position, -i).getValue()); + } + + // Also test prev/next beat calculation + FramePos foundPrevBeat, foundNextBeat; + m_pBeats1->findPrevNextBeats(position, &foundPrevBeat, &foundNextBeat); + EXPECT_EQ(previousBeat, foundPrevBeat); + EXPECT_EQ(nextBeat, foundNextBeat); +} + +TEST_F(BeatsTest, BpmAround) { + const FrameDiff_t approxBeatLengthFrames = getBeatLengthFrames(m_bpm); + const int numBeats = 64; + + // Constant BPM, constructed in BeatsTest + for (unsigned int i = 0; i < 100; i++) { + EXPECT_EQ(Bpm(60), m_pBeats1->getBpmAroundPosition(FramePos(i), 5)); + } + + // Prepare a new Beats to test the behavior for variable BPM + QVector beats; + FramePos beat_pos; + Bpm bpm(60); + for (unsigned int i = 0; i < numBeats; ++i, bpm = bpm + 1) { + FrameDiff_t beat_length = getBeatLengthFrames(bpm); + beats.append(beat_pos); + beat_pos += beat_length; + } + BeatsPointer pMap = + std::make_unique(m_pTrack.get(), beats); + + // The average of the first 8 beats should be different than the average + // of the last 8 beats. + EXPECT_DOUBLE_EQ(63.937454161267674, + pMap->getBpmAroundPosition(FramePos(0) + approxBeatLengthFrames * 4, 4).getValue()); + EXPECT_DOUBLE_EQ(118.96637943082918, + pMap->getBpmAroundPosition(FramePos(0) + approxBeatLengthFrames * 60, 4).getValue()); + // Also test at the beginning and end of the track + EXPECT_DOUBLE_EQ(62.936459878052396, + pMap->getBpmAroundPosition(FramePos(0), 4).getValue()); + EXPECT_DOUBLE_EQ(118.96637943082918, + pMap->getBpmAroundPosition(FramePos(0) + approxBeatLengthFrames * 65, 4).getValue()); + + // Try a really, really short track + beats = createBeatVector(FramePos(10), 3, getBeatLengthFrames(m_bpm)); + BeatsPointer pBeats = + std::make_unique(m_pTrack.get(), beats); + EXPECT_DOUBLE_EQ(m_bpm.getValue(), + pBeats->getBpmAroundPosition(FramePos(0) + approxBeatLengthFrames * 1, 4).getValue()); +} + +TEST_F(BeatsTest, Signature) { + // Undefined time signature must be default + EXPECT_EQ(m_pBeats1->getSignature(0), + TimeSignature()) + << "If no Time Signature defined, it must be default(4/4)"; + + const auto timeSignatureInitial = TimeSignature(3, 4); + const auto timeSignatureIntermediate = TimeSignature(4, 4); + const auto timeSignatureLater = TimeSignature(5, 4); + + // Add time signature to the beginning + m_pBeats1->setSignature(timeSignatureInitial, 0); + int firstSwitchBeatIndex = 3 * 4; + int secondSwitchBeatIndex = firstSwitchBeatIndex + 4 * 4; + + // Add time signature in beats not at the beginning + m_pBeats1->setSignature(timeSignatureIntermediate, firstSwitchBeatIndex); + m_pBeats1->setSignature(timeSignatureLater, secondSwitchBeatIndex); + + EXPECT_EQ(m_pBeats1->getSignature(0), + timeSignatureInitial) + << "Starting Time Signature must be" + << "3/4"; + EXPECT_EQ(m_pBeats1->getSignature(firstSwitchBeatIndex / 2), + TimeSignature(3, 4)) + << "Time Signature at " << firstSwitchBeatIndex / 2 << " must be" + << "3/4"; + EXPECT_EQ(m_pBeats1->getSignature(firstSwitchBeatIndex), + timeSignatureIntermediate) + << "Time Signature at " << firstSwitchBeatIndex << " must be" + << "4/4"; + EXPECT_EQ(m_pBeats1->getSignature(secondSwitchBeatIndex), + timeSignatureLater) + << "Time Signature at " << secondSwitchBeatIndex << " must be" + << "5/4"; + + // Add a signature past the end of the track, must have no effect, and check + // TODO(hacksdump): Implement this check in main source. +} + +TEST_F(BeatsTest, Iterator) { + FramePos pos; + + // Full Beatsbeat + auto iter1 = m_pBeats1->findBeats(m_pBeats1->getFirstBeatPosition(), + m_pBeats1->getLastBeatPosition()); + EXPECT_DOUBLE_EQ(iter1->next().frame_position(), m_pBeats1->getFirstBeatPosition().getValue()); + while (iter1->hasNext()) { + auto beat = iter1->next(); + pos = FramePos(beat.frame_position()); + EXPECT_TRUE(pos.getValue()); + } + EXPECT_DOUBLE_EQ(pos.getValue(), m_pBeats1->getLastBeatPosition().getValue()); + + // Past end + auto iter2 = m_pBeats1->findBeats(m_pBeats1->getFirstBeatPosition(), + FramePos(m_pBeats1->getLastBeatPosition().getValue() + 10000000000)); + while (iter2->hasNext()) { + auto beat = iter2->next(); + pos = FramePos(beat.frame_position()); + EXPECT_TRUE(pos.getValue()); + } + EXPECT_DOUBLE_EQ(pos.getValue(), m_pBeats1->getLastBeatPosition().getValue()); + + // Before begining + auto iter3 = m_pBeats1->findBeats( + FramePos(m_pBeats1->getFirstBeatPosition().getValue() - 1000000), + m_pBeats1->getLastBeatPosition()); + EXPECT_DOUBLE_EQ(iter3->next().frame_position(), m_pBeats1->getFirstBeatPosition().getValue()); + while (iter3->hasNext()) { + auto beat = iter3->next(); + pos = FramePos(beat.frame_position()); + EXPECT_TRUE(pos.getValue()); + } + EXPECT_DOUBLE_EQ(pos.getValue(), m_pBeats1->getLastBeatPosition().getValue()); +} + +TEST_F(BeatsTest, Translate) { + FrameDiff_t delta = 500; + + // Move the grid delta frames + m_pBeats1->translate(delta); + + // All beats must have been displaced by delta frames + auto iter1 = m_pBeats1->findBeats(m_pBeats1->getFirstBeatPosition(), + m_pBeats1->getLastBeatPosition()); + auto iter2 = m_pBeats2->findBeats(m_pBeats2->getFirstBeatPosition(), + m_pBeats2->getLastBeatPosition()); + while (iter1->hasNext()) { + double pos1 = iter1->next().frame_position(); + double pos2 = iter2->next().frame_position(); + EXPECT_DOUBLE_EQ(pos1, pos2 + delta); + } + EXPECT_EQ(iter1->hasNext(), iter2->hasNext()); +} + +TEST_F(BeatsTest, FindClosest) { + // Test deltas ranging from previous beat to next beat + for (FrameDiff_t delta = -m_iSampleRate; delta <= m_iSampleRate; delta++) { + auto iter1 = m_pBeats1->findBeats(m_pBeats1->getFirstBeatPosition(), + m_pBeats1->getLastBeatPosition()); + while (iter1->hasNext()) { + FramePos pos = FramePos(iter1->next().frame_position()); + FramePos foundPos = m_pBeats1->findClosestBeat(pos + delta); + // Correct change of beat + FramePos expectedPos = pos + + (delta > (m_iSampleRate / 2.0) ? m_iSampleRate : 0) + + (delta < (-m_iSampleRate / 2.0) ? -m_iSampleRate : 0); + // Enforce boundaries + expectedPos = std::min(expectedPos, m_pBeats1->getLastBeatPosition()); + expectedPos = std::max(expectedPos, m_pBeats1->getFirstBeatPosition()); + EXPECT_DOUBLE_EQ(foundPos.getValue(), expectedPos.getValue()); + } + break; + } +} + +} // namespace diff --git a/src/test/beatstranslatetest.cpp b/src/test/beatstranslatetest.cpp index 8a8021e05bf..115c6ebd2de 100644 --- a/src/test/beatstranslatetest.cpp +++ b/src/test/beatstranslatetest.cpp @@ -1,40 +1,47 @@ #include "test/mockedenginebackendtest.h" -#include "track/beatgrid.h" +#include "track/beats.h" #include "util/memory.h" +using namespace mixxx; + class BeatsTranslateTest : public MockedEngineBackendTest { }; TEST_F(BeatsTranslateTest, SimpleTranslateMatch) { - // Set up BeatGrids for decks 1 and 2. - const double bpm = 60.0; - const double firstBeat = 0.0; - auto grid1 = new mixxx::BeatGrid(*m_pTrack1, m_pTrack1->getSampleRate()); - grid1->setGrid(bpm, firstBeat); - m_pTrack1->setBeats(mixxx::BeatsPointer(grid1)); - ASSERT_DOUBLE_EQ(firstBeat, grid1->findClosestBeat(0)); - - auto grid2 = new mixxx::BeatGrid(*m_pTrack2, m_pTrack2->getSampleRate()); - grid2->setGrid(bpm, firstBeat); - m_pTrack2->setBeats(mixxx::BeatsPointer(grid2)); - ASSERT_DOUBLE_EQ(firstBeat, grid2->findClosestBeat(0)); - - // Seek deck 1 forward a bit. - const double delta = 2222.0; - m_pChannel1->getEngineBuffer()->slotControlSeekAbs(delta); + const mixxx::Bpm bpm = Bpm(60.0); + const FramePos firstBeat(0.0); + + // Set up Beats for decks 1 and 2. + auto beats1 = std::make_shared(m_pTrack1.get()); + beats1->setGrid(bpm, firstBeat); + m_pTrack1->setBeats(beats1); + ASSERT_DOUBLE_EQ(firstBeat.getValue(), beats1->findClosestBeat(firstBeat).getValue()); + auto beats2 = std::make_shared(m_pTrack2.get()); + beats2->setGrid(bpm, firstBeat); + m_pTrack2->setBeats(beats2); + ASSERT_DOUBLE_EQ(firstBeat.getValue(), beats2->findClosestBeat(firstBeat).getValue()); + + const FrameDiff_t deltaFrames = 1111.0; + // Seek deck 1 forward a bit + m_pChannel1->getEngineBuffer()->slotControlSeekAbs(deltaFrames * 2); ProcessBuffer(); EXPECT_TRUE(m_pChannel1->getEngineBuffer()->getVisualPlayPos() > 0); + ASSERT_DOUBLE_EQ(m_pChannel1->getEngineBuffer()->getExactPlayPos(), deltaFrames * 2); // Make both decks playing. ControlObject::getControl(m_sGroup1, "play")->set(1.0); ControlObject::getControl(m_sGroup2, "play")->set(1.0); ProcessBuffer(); - // Manually set the "bpm" control... I would like to figure out why this - // doesn't get set naturally, but this will do for now. + ASSERT_DOUBLE_EQ(m_pChannel1->getEngineBuffer()->getExactPlayPos(), + m_pChannel2->getEngineBuffer()->getExactPlayPos() + + deltaFrames * 2); + + // TODO(XXX) Manually set the "bpm" control... I would like to figure out + // why this doesn't get set naturally, but this will do for now. auto pBpm1 = std::make_unique(m_sGroup1, "bpm"); auto pBpm2 = std::make_unique(m_sGroup1, "bpm"); - pBpm1->set(bpm); - pBpm2->set(bpm); + pBpm1->set(bpm.getValue()); + pBpm2->set(bpm.getValue()); ProcessBuffer(); // Push the button on deck 2. @@ -44,7 +51,8 @@ TEST_F(BeatsTranslateTest, SimpleTranslateMatch) { ProcessBuffer(); // Deck 1 is +delta away from its closest beat (which is at 0). - // Deck 2 was left at 0. We translated grid 2 so that it is also +delta - // away from its closest beat, so that beat should be at -delta. - ASSERT_DOUBLE_EQ(-delta, grid2->findClosestBeat(0)); + // Deck 2 was left at 0. + // We translated grid 2 so that it is also +delta away from its closest beat + // So that beat should be at deck 1 position -delta. + ASSERT_DOUBLE_EQ(-deltaFrames, beats2->findClosestBeat(FramePos(0)).getValue()); } diff --git a/src/test/bpmcontrol_test.cpp b/src/test/bpmcontrol_test.cpp index 5ff847df78c..608104c6dd1 100644 --- a/src/test/bpmcontrol_test.cpp +++ b/src/test/bpmcontrol_test.cpp @@ -1,16 +1,15 @@ +#include "engine/controls/bpmcontrol.h" + #include -#include #include +#include -#include "mixxxtest.h" #include "control/controlobject.h" #include "control/controlpushbutton.h" -#include "engine/controls/bpmcontrol.h" -#include "track/beats.h" +#include "mixxxtest.h" #include "track/beatfactory.h" -#include "track/beatgrid.h" -#include "track/beatmap.h" +#include "track/beats.h" #include "track/track.h" class BpmControlTest : public MixxxTest { @@ -34,18 +33,24 @@ TEST_F(BpmControlTest, BeatContext_BeatGrid) { mixxx::audio::Bitrate(), mixxx::Duration::fromSeconds(180)); - const double bpm = 60.0; - const int kFrameSize = 2; - const double expectedBeatLength = (60.0 * sampleRate / bpm) * kFrameSize; + const mixxx::Bpm bpm = mixxx::Bpm(60.0); + const mixxx::FrameDiff_t expectedBeatLengthFrames = (60.0 * sampleRate / bpm.getValue()); - mixxx::BeatsPointer pBeats = BeatFactory::makeBeatGrid(*pTrack, bpm, 0); + auto pBeats = std::make_shared(pTrack.get()); + pBeats->setGrid(bpm); // On a beat. - double prevBeat, nextBeat, beatLength, beatPercentage; - EXPECT_TRUE(BpmControl::getBeatContext(pBeats, 0.0, &prevBeat, &nextBeat, - &beatLength, &beatPercentage)); - EXPECT_DOUBLE_EQ(0.0, prevBeat); - EXPECT_DOUBLE_EQ(beatLength, nextBeat); - EXPECT_DOUBLE_EQ(expectedBeatLength, beatLength); + mixxx::FramePos prevBeat, nextBeat; + mixxx::FrameDiff_t beatLength; + double beatPercentage; + EXPECT_TRUE(BpmControl::getBeatContext(pBeats, + mixxx::FramePos(0), + &prevBeat, + &nextBeat, + &beatLength, + &beatPercentage)); + EXPECT_DOUBLE_EQ(0.0, prevBeat.getValue()); + EXPECT_DOUBLE_EQ(beatLength, nextBeat.getValue()); + EXPECT_DOUBLE_EQ(expectedBeatLengthFrames, beatLength); EXPECT_DOUBLE_EQ(0.0, beatPercentage); } diff --git a/src/test/enginesynctest.cpp b/src/test/enginesynctest.cpp index 58dc1e87560..c569906ae57 100644 --- a/src/test/enginesynctest.cpp +++ b/src/test/enginesynctest.cpp @@ -18,9 +18,11 @@ #include "test/mixxxtest.h" #include "test/mockedenginebackendtest.h" #include "track/beatfactory.h" -#include "track/beatmap.h" +#include "track/beats.h" #include "util/memory.h" +using namespace mixxx; + class EngineSyncTest : public MockedEngineBackendTest { public: QString getMasterGroup() { @@ -189,9 +191,11 @@ TEST_F(EngineSyncTest, SetMasterSuccess) { TEST_F(EngineSyncTest, ExplicitMasterPersists) { // If we set an explicit master, enabling sync or pressing play on other decks // doesn't cause the master to move around. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 120, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(120)); m_pTrack1->setBeats(pBeats1); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 124, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(124)); m_pTrack2->setBeats(pBeats2); auto pButtonMasterSync1 = @@ -221,11 +225,14 @@ TEST_F(EngineSyncTest, ExplicitMasterPersists) { TEST_F(EngineSyncTest, SetMasterWhilePlaying) { // Make sure we don't get two master lights if we change masters while playing. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 120, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(120)); m_pTrack1->setBeats(pBeats1); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 124, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(124)); m_pTrack2->setBeats(pBeats2); - mixxx::BeatsPointer pBeats3 = BeatFactory::makeBeatGrid(*m_pTrack3, 128, 0.0); + auto pBeats3 = BeatsPointer(new Beats(m_pTrack3.get())); + pBeats3->setGrid(mixxx::Bpm(128)); m_pTrack3->setBeats(pBeats3); auto pButtonMasterSync1 = @@ -255,7 +262,8 @@ TEST_F(EngineSyncTest, SetMasterWhilePlaying) { TEST_F(EngineSyncTest, SetEnabledBecomesMaster) { // If we set the first channel with a valid tempo to follower, it should be master. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 80, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(80)); m_pTrack1->setBeats(pBeats1); auto pButtonMasterSync1 = std::make_unique(m_sGroup1, "sync_mode"); @@ -282,10 +290,12 @@ TEST_F(EngineSyncTest, DisableInternalMasterWhilePlaying) { ASSERT_TRUE(isSoftMaster(m_sInternalClockGroup)); // Make sure both decks are playing. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 80, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(80)); m_pTrack1->setBeats(pBeats1); ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 80, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(80)); m_pTrack2->setBeats(pBeats2); ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0); ProcessBuffer(); @@ -301,13 +311,15 @@ TEST_F(EngineSyncTest, DisableInternalMasterWhilePlaying) { TEST_F(EngineSyncTest, DisableSyncOnMaster) { // Channel 1 follower, channel 2 master. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 130, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(130)); m_pTrack1->setBeats(pBeats1); auto pButtonSyncMode1 = std::make_unique(m_sGroup1, "sync_mode"); pButtonSyncMode1->slotSet(SYNC_FOLLOWER); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 130, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(130)); m_pTrack2->setBeats(pBeats2); auto pButtonSyncMaster2 = std::make_unique(m_sGroup2, "sync_master"); @@ -339,7 +351,8 @@ TEST_F(EngineSyncTest, InternalMasterSetFollowerSliderMoves) { pMasterSyncSlider->set(100.0); // Set the file bpm of channel 1 to 80 bpm. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 80, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(80)); m_pTrack1->setBeats(pBeats1); auto pButtonMasterSync1 = @@ -357,7 +370,8 @@ TEST_F(EngineSyncTest, AnySyncDeckSliderStays) { // If there exists a sync deck, even if it's not playing, don't change the // master BPM if a new deck enables sync. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 80, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(80)); m_pTrack1->setBeats(pBeats1); auto pButtonSyncEnabled1 = std::make_unique(m_sGroup1, "sync_enabled"); @@ -368,7 +382,8 @@ TEST_F(EngineSyncTest, AnySyncDeckSliderStays) { ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "bpm")) ->get()); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 100, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(100)); m_pTrack2->setBeats(pBeats2); auto pButtonSyncEnabled2 = std::make_unique(m_sGroup2, "sync_enabled"); @@ -392,11 +407,13 @@ TEST_F(EngineSyncTest, InternalClockFollowsFirstPlayingDeck) { std::make_unique(m_sGroup2, "sync_enabled"); // Set up decks so they can be playing, and start deck 1. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 100, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(100)); m_pTrack1->setBeats(pBeats1); ControlObject::set(ConfigKey(m_sGroup1, "rate"), getRateSliderValue(1.0)); ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 130, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(130)); m_pTrack2->setBeats(pBeats2); ControlObject::set(ConfigKey(m_sGroup2, "rate"), getRateSliderValue(1.0)); ControlObject::set(ConfigKey(m_sGroup2, "play"), 0.0); @@ -462,10 +479,12 @@ TEST_F(EngineSyncTest, SetExplicitMasterByLights) { std::make_unique(m_sGroup2, "sync_master"); // Set the file bpm of channel 1 to 160bpm. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 160, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(160)); m_pTrack1->setBeats(pBeats1); // Set the file bpm of channel 2 to 150bpm. - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack1, 150, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(150)); m_pTrack2->setBeats(pBeats2); // Set channel 1 to be explicit master. @@ -548,7 +567,8 @@ TEST_F(EngineSyncTest, RateChangeTest) { ProcessBuffer(); // Set the file bpm of channel 1 to 160bpm. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 160, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(160)); m_pTrack1->setBeats(pBeats1); EXPECT_FLOAT_EQ( 160.0, ControlObject::get(ConfigKey(m_sGroup1, "file_bpm"))); @@ -567,7 +587,8 @@ TEST_F(EngineSyncTest, RateChangeTest) { 192.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm"))); // Set the file bpm of channel 2 to 120bpm. - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 120, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(120)); m_pTrack2->setBeats(pBeats2); EXPECT_FLOAT_EQ( 120.0, ControlObject::get(ConfigKey(m_sGroup2, "file_bpm"))); @@ -589,13 +610,15 @@ TEST_F(EngineSyncTest, RateChangeTestWeirdOrder) { ProcessBuffer(); // Set the file bpm of channel 1 to 160bpm. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 160, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(160)); m_pTrack1->setBeats(pBeats1); EXPECT_FLOAT_EQ( 160.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm"))); // Set the file bpm of channel 2 to 120bpm. - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 120, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(120)); m_pTrack2->setBeats(pBeats2); // Set the rate slider of channel 1 to 1.2. @@ -613,13 +636,15 @@ TEST_F(EngineSyncTest, RateChangeTestWeirdOrder) { TEST_F(EngineSyncTest, RateChangeTestOrder3) { // Set the file bpm of channel 1 to 160bpm. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 160, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(160)); m_pTrack1->setBeats(pBeats1); EXPECT_FLOAT_EQ( 160.0, ControlObject::get(ConfigKey(m_sGroup1, "file_bpm"))); // Set the file bpm of channel 2 to 120bpm. - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 120, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(120)); m_pTrack2->setBeats(pBeats2); EXPECT_FLOAT_EQ( 120.0, ControlObject::get(ConfigKey(m_sGroup2, "file_bpm"))); @@ -656,11 +681,13 @@ TEST_F(EngineSyncTest, FollowerRateChange) { ProcessBuffer(); // Set the file bpm of channel 1 to 160bpm. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 160, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(160)); m_pTrack1->setBeats(pBeats1); // Set the file bpm of channel 2 to 120bpm. - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 120, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(120)); m_pTrack2->setBeats(pBeats2); // Set the rate slider of channel 1 to 1.2. @@ -702,13 +729,15 @@ TEST_F(EngineSyncTest, InternalRateChangeTest) { ASSERT_TRUE(isFollower(m_sGroup2)); // Set the file bpm of channel 1 to 160bpm. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 160, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(160)); m_pTrack1->setBeats(pBeats1); EXPECT_FLOAT_EQ(160.0, ControlObject::getControl(ConfigKey(m_sGroup1, "file_bpm"))->get()); // Set the file bpm of channel 2 to 120bpm. - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 120, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(120)); m_pTrack2->setBeats(pBeats2); EXPECT_FLOAT_EQ(120.0, ControlObject::getControl(ConfigKey(m_sGroup2, "file_bpm"))->get()); @@ -755,9 +784,11 @@ TEST_F(EngineSyncTest, InternalRateChangeTest) { TEST_F(EngineSyncTest, MasterStopSliderCheck) { // If the master is playing, and stop is pushed, the sliders should stay the same. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 120, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(120)); m_pTrack1->setBeats(pBeats1); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 128, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(128)); m_pTrack2->setBeats(pBeats2); auto pButtonMasterSync1 = @@ -799,7 +830,8 @@ TEST_F(EngineSyncTest, EnableOneDeckInitsMaster) { ProcessBuffer(); // Set up the deck to play. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 130, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(130)); m_pTrack1->setBeats(pBeats1); ControlObject::getControl(ConfigKey(m_sGroup1, "rate")) ->set(getRateSliderValue(1.0)); @@ -826,7 +858,8 @@ TEST_F(EngineSyncTest, EnableOneDeckInitsMaster) { ConfigKey(m_sInternalClockGroup, "beat_distance"))); // Enable second deck, bpm and beat distance should still match original setting. - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 140, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(140)); m_pTrack2->setBeats(pBeats2); ControlObject::getControl(ConfigKey(m_sGroup2, "rate")) ->set(getRateSliderValue(1.0)); @@ -853,7 +886,8 @@ TEST_F(EngineSyncTest, EnableOneDeckInitsMaster) { TEST_F(EngineSyncTest, EnableOneDeckInitializesMaster) { // Enabling sync on a deck causes it to be master, and sets bpm and clock. // Set the deck to play. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 130, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(130)); m_pTrack1->setBeats(pBeats1); ControlObject::getControl(ConfigKey(m_sGroup1, "rate")) ->set(getRateSliderValue(1.0)); @@ -1050,7 +1084,8 @@ TEST_F(EngineSyncTest, EnableOneDeckSliderUpdates) { auto pButtonSyncEnabled1 = std::make_unique(m_sGroup1, "sync_enabled"); - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 130, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(130)); m_pTrack1->setBeats(pBeats1); ControlObject::getControl(ConfigKey(m_sGroup1, "rate")) ->set(getRateSliderValue(1.0)); @@ -1077,11 +1112,13 @@ TEST_F(EngineSyncTest, SyncToNonSyncDeck) { auto pButtonSyncEnabled2 = std::make_unique(m_sGroup2, "sync_enabled"); - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 130, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(130)); m_pTrack1->setBeats(pBeats1); ProcessBuffer(); ControlObject::set(ConfigKey(m_sGroup1, "rate"), getRateSliderValue(1.0)); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 100, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(100)); m_pTrack2->setBeats(pBeats2); ControlObject::getControl(ConfigKey(m_sGroup2, "rate")) ->set(getRateSliderValue(1.0)); @@ -1160,11 +1197,13 @@ TEST_F(EngineSyncTest, MomentarySyncDependsOnPlayingStates) { std::make_unique(m_sGroup2, "sync_enabled"); // Set up decks so they can be playing, and start deck 1. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 100, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(100)); m_pTrack1->setBeats(pBeats1); ControlObject::set(ConfigKey(m_sGroup1, "rate"), getRateSliderValue(1.0)); ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 130, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(130)); m_pTrack2->setBeats(pBeats2); ControlObject::set(ConfigKey(m_sGroup2, "rate"), getRateSliderValue(1.0)); ControlObject::set(ConfigKey(m_sGroup2, "play"), 1.0); @@ -1233,7 +1272,8 @@ TEST_F(EngineSyncTest, EjectTrackSyncRemains) { std::make_unique(m_sGroup2, "sync_enabled"); auto pButtonEject1 = std::make_unique(m_sGroup1, "eject"); - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 120, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(120)); m_pTrack1->setBeats(pBeats1); pButtonSyncEnabled1->set(1.0); ProcessBuffer(); @@ -1258,7 +1298,8 @@ TEST_F(EngineSyncTest, EjectTrackSyncRemains) { m_pMixerDeck1->loadFakeTrack(false, 128.0); EXPECT_FLOAT_EQ(128.0, ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get()); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 135, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(135)); m_pTrack2->setBeats(pBeats2); pButtonSyncEnabled2->set(1.0); ProcessBuffer(); @@ -1278,21 +1319,24 @@ TEST_F(EngineSyncTest, EjectTrackSyncRemains) { TEST_F(EngineSyncTest, FileBpmChangesDontAffectMaster) { // If filebpm changes, don't treat it like a rate change. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 100, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(100)); m_pTrack1->setBeats(pBeats1); auto pButtonSyncEnabled1 = std::make_unique(m_sGroup1, "sync_enabled"); pButtonSyncEnabled1->set(1.0); ProcessBuffer(); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 120, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(120)); m_pTrack2->setBeats(pBeats2); auto pButtonSyncEnabled2 = std::make_unique(m_sGroup2, "sync_enabled"); pButtonSyncEnabled2->set(1.0); ProcessBuffer(); - pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 160, 0.0); + pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(160)); m_pTrack1->setBeats(pBeats1); EXPECT_FLOAT_EQ( 100.0, ControlObject::get(ConfigKey(m_sInternalClockGroup, "bpm"))); @@ -1304,7 +1348,8 @@ TEST_F(EngineSyncTest, ExplicitMasterPostProcessed) { auto pButtonMasterSync1 = std::make_unique(m_sGroup1, "sync_mode"); pButtonMasterSync1->slotSet(SYNC_MASTER_EXPLICIT); - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 160, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(160)); m_pTrack1->setBeats(pBeats1); ProcessBuffer(); ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0); @@ -1317,7 +1362,8 @@ TEST_F(EngineSyncTest, ExplicitMasterPostProcessed) { TEST_F(EngineSyncTest, ZeroBPMRateAdjustIgnored) { // If a track isn't loaded (0 bpm), but the deck has sync enabled, // don't pay attention to rate changes. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 0, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(0)); m_pTrack1->setBeats(pBeats1); auto pButtonSyncEnabled1 = std::make_unique(m_sGroup1, "sync_enabled"); @@ -1326,7 +1372,8 @@ TEST_F(EngineSyncTest, ZeroBPMRateAdjustIgnored) { ->set(getRateSliderValue(1.0)); ProcessBuffer(); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 120, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(120)); m_pTrack2->setBeats(pBeats2); auto pButtonSyncEnabled2 = std::make_unique(m_sGroup2, "sync_enabled"); @@ -1362,9 +1409,11 @@ TEST_F(EngineSyncTest, ZeroBPMRateAdjustIgnored) { TEST_F(EngineSyncTest, ZeroLatencyRateChange) { // Confirm that a rate change in an explicit master is instantly communicated // to followers. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 128, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(128)); m_pTrack1->setBeats(pBeats1); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 128, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(128)); m_pTrack2->setBeats(pBeats2); ControlObject::getControl(ConfigKey(m_sGroup1, "quantize"))->set(1.0); @@ -1401,9 +1450,11 @@ TEST_F(EngineSyncTest, ZeroLatencyRateChange) { } TEST_F(EngineSyncTest, HalfDoubleBpmTest) { - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 70, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(70)); m_pTrack1->setBeats(pBeats1); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 140, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(140)); m_pTrack2->setBeats(pBeats2); ControlObject::getControl(ConfigKey(m_sGroup1, "quantize"))->set(1.0); @@ -1495,9 +1546,11 @@ TEST_F(EngineSyncTest, HalfDoubleBpmTest) { TEST_F(EngineSyncTest, HalfDoubleThenPlay) { // If a deck plays that had its multiplier set, we need to reset the // internal clock. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 80, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(80)); m_pTrack1->setBeats(pBeats1); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 175, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(175)); m_pTrack2->setBeats(pBeats2); ControlObject::getControl(ConfigKey(m_sGroup1, "rate")) ->set(getRateSliderValue(1.0)); @@ -1597,9 +1650,11 @@ TEST_F(EngineSyncTest, HalfDoubleThenPlay) { TEST_F(EngineSyncTest, HalfDoubleInternalClockTest) { // If we set the file_bpm CO's directly, the correct signals aren't fired. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 70, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(70)); m_pTrack1->setBeats(pBeats1); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 140, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(140)); m_pTrack2->setBeats(pBeats2); ControlObject::getControl(ConfigKey(m_sGroup1, "quantize"))->set(1.0); @@ -1618,11 +1673,12 @@ TEST_F(EngineSyncTest, HalfDoubleInternalClockTest) { } namespace { -QVector createBeatVector( - double first_beat, unsigned int num_beats, double beat_length) { - QVector beats; +QVector createBeatVector(FramePos first_beat, + unsigned int num_beats, + FrameDiff_t beat_length) { + QVector beats; for (unsigned int i = 0; i < num_beats; ++i) { - beats.append(first_beat + i * beat_length); + beats.append(first_beat + beat_length * i); } return beats; } @@ -1630,18 +1686,18 @@ QVector createBeatVector( TEST_F(EngineSyncTest, HalfDoubleConsistency) { // half-double matching should be consistent - double beatLengthFrames = 60.0 * 44100 / 90.0; - double startOffsetFrames = 0; + FrameDiff_t beatLengthFrames = 60.0 * 44100 / 90.0; + FramePos startOffsetFrames = FramePos(0); const int numBeats = 100; - QVector beats1 = + QVector beats1 = createBeatVector(startOffsetFrames, numBeats, beatLengthFrames); - auto pBeats1 = new mixxx::BeatMap(*m_pTrack1, 0, beats1); + auto pBeats1 = new mixxx::Beats(m_pTrack1.get(), beats1); m_pTrack1->setBeats(mixxx::BeatsPointer(pBeats1)); beatLengthFrames = 60.0 * 44100 / 145.0; - QVector beats2 = + QVector beats2 = createBeatVector(startOffsetFrames, numBeats, beatLengthFrames); - auto pBeats2 = new mixxx::BeatMap(*m_pTrack2, 0, beats2); + auto pBeats2 = new mixxx::Beats(m_pTrack2.get(), beats2); m_pTrack2->setBeats(mixxx::BeatsPointer(pBeats2)); ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0); @@ -1668,9 +1724,10 @@ TEST_F(EngineSyncTest, HalfDoubleConsistency) { TEST_F(EngineSyncTest, SetFileBpmUpdatesLocalBpm) { ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance"))->set(0.2); - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 130, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(130)); m_pTrack1->setBeats(pBeats1); - ASSERT_EQ( + EXPECT_FLOAT_EQ( 130.0, m_pEngineSync->getSyncableForGroup(m_sGroup1)->getBaseBpm()); } @@ -1680,14 +1737,16 @@ TEST_F(EngineSyncTest, SyncPhaseToPlayingNonSyncDeck) { auto pButtonSyncEnabled1 = std::make_unique(m_sGroup1, "sync_enabled"); - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 130, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(130)); m_pTrack1->setBeats(pBeats1); ControlObject::getControl(ConfigKey(m_sGroup1, "quantize"))->set(1.0); auto pButtonSyncEnabled2 = std::make_unique(m_sGroup2, "sync_enabled"); ControlObject::set(ConfigKey(m_sGroup2, "rate_ratio"), 1.0); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 100, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(100)); m_pTrack2->setBeats(pBeats2); // Set the sync deck playing with nothing else active. @@ -1775,7 +1834,8 @@ TEST_F(EngineSyncTest, SyncPhaseToPlayingNonSyncDeck) { std::make_unique(m_sGroup3, "sync_enabled"); ControlObject::set(ConfigKey(m_sGroup3, "beat_distance"), 0.6); ControlObject::set(ConfigKey(m_sGroup2, "rate_ratio"), 1.0); - mixxx::BeatsPointer pBeats3 = BeatFactory::makeBeatGrid(*m_pTrack3, 140, 0.0); + auto pBeats3 = BeatsPointer(new Beats(m_pTrack3.get())); + pBeats3->setGrid(mixxx::Bpm(140)); m_pTrack3->setBeats(pBeats3); // This will sync to the first deck here and not the second (lp1784185) pButtonSyncEnabled3->set(1.0); @@ -1816,9 +1876,11 @@ TEST_F(EngineSyncTest, UserTweakBeatDistance) { // If a deck has a user tweak, and another deck stops such that the first // is used to reseed the master beat distance, make sure the user offset // is reset. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 128, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(128)); m_pTrack1->setBeats(pBeats1); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 128, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(128)); m_pTrack2->setBeats(pBeats2); ControlObject::getControl(ConfigKey(m_sGroup1, "quantize"))->set(1.0); @@ -1869,9 +1931,11 @@ TEST_F(EngineSyncTest, UserTweakPreservedInSeek) { // This is about 128 bpm, but results in nice round numbers of samples. const double kDivisibleBpm = 44100.0 / 344.0; - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, kDivisibleBpm, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(kDivisibleBpm)); m_pTrack1->setBeats(pBeats1); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 130, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(130)); m_pTrack2->setBeats(pBeats2); ControlObject::getControl(ConfigKey(m_sGroup2, "sync_enabled"))->set(1); @@ -1929,7 +1993,8 @@ TEST_F(EngineSyncTest, UserTweakPreservedInSeek) { } TEST_F(EngineSyncTest, MasterBpmNeverZero) { - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 128, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(128)); m_pTrack1->setBeats(pBeats1); auto pButtonSyncEnabled1 = std::make_unique(m_sGroup1, "sync_enabled"); @@ -1944,7 +2009,8 @@ TEST_F(EngineSyncTest, ZeroBpmNaturalRate) { // If a track has a zero bpm and a bad beatgrid, make sure the rate // doesn't end up something crazy when sync is enabled.. // Maybe the beatgrid ended up at zero also. - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 0.0, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(0)); m_pTrack1->setBeats(pBeats1); auto pButtonSyncEnabled1 = std::make_unique(m_sGroup1, "sync_enabled"); @@ -1963,11 +2029,13 @@ TEST_F(EngineSyncTest, QuantizeImpliesSyncPhase) { auto pButtonBeatsync1 = std::make_unique(m_sGroup1, "beatsync"); auto pButtonBeatsyncPhase1 = std::make_unique(m_sGroup1, "beatsync_phase"); - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 130, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(130)); m_pTrack1->setBeats(pBeats1); ControlObject::set(ConfigKey(m_sGroup2, "rate"), getRateSliderValue(1.0)); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 100, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(100)); m_pTrack2->setBeats(pBeats2); ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0); @@ -2048,15 +2116,17 @@ TEST_F(EngineSyncTest, QuantizeImpliesSyncPhase) { EXPECT_DOUBLE_EQ(100, ControlObject::get(ConfigKey(m_sGroup2, "bpm"))); // we align here to the past beat, because beat_distance < 1.0/8 - EXPECT_DOUBLE_EQ( + EXPECT_NEAR( ControlObject::get(ConfigKey(m_sGroup1, "beat_distance")) / 130 * 100, - ControlObject::get(ConfigKey(m_sGroup2, "beat_distance"))); + ControlObject::get(ConfigKey(m_sGroup2, "beat_distance")), + 1e-15); } TEST_F(EngineSyncTest, SeekStayInPhase) { ControlObject::set(ConfigKey(m_sGroup1, "quantize"), 1.0); - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 130, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(130)); m_pTrack1->setBeats(pBeats1); ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0); @@ -2077,7 +2147,8 @@ TEST_F(EngineSyncTest, SeekStayInPhase) { ControlObject::set(ConfigKey(m_sGroup1, "play"), 0.0); ProcessBuffer(); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack1, 130, 0.0); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(130)); m_pTrack2->setBeats(pBeats2); ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0); @@ -2099,7 +2170,8 @@ TEST_F(EngineSyncTest, SeekStayInPhase) { TEST_F(EngineSyncTest, SyncWithoutBeatgrid) { // this tests bug lp1783020, notresetting rate when other deck has no beatgrid - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 128, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(128)); m_pTrack1->setBeats(pBeats1); m_pTrack2->setBeats(mixxx::BeatsPointer()); @@ -2118,12 +2190,13 @@ TEST_F(EngineSyncTest, SyncWithoutBeatgrid) { } TEST_F(EngineSyncTest, QuantizeHotCueActivate) { - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 130, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(130)); m_pTrack1->setBeats(pBeats1); auto pHotCue2Activate = std::make_unique(m_sGroup2, "hotcue_1_activate"); - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 100, 0.0); - + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(100)); m_pTrack2->setBeats(pBeats2); ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0); @@ -2178,7 +2251,8 @@ TEST_F(EngineSyncTest, ChangeBeatGrid) { auto pButtonSyncEnabled2 = std::make_unique(m_sGroup2, "sync_enabled"); // set beatgrid - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 130, 0.0); + auto pBeats1 = BeatsPointer(new Beats(m_pTrack1.get())); + pBeats1->setGrid(mixxx::Bpm(130)); m_pTrack1->setBeats(pBeats1); pButtonSyncEnabled1->set(1.0); ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0); @@ -2209,8 +2283,9 @@ TEST_F(EngineSyncTest, ChangeBeatGrid) { ASSERT_TRUE(isSoftMaster(m_sGroup1)); ASSERT_TRUE(isFollower(m_sGroup2)); - // Load a new beatgrid during playing, this happens when the analyser is finished - mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(*m_pTrack2, 140, 0.0); + // Load a new beatgrid, this happens when the analyser is finisched + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2->setGrid(mixxx::Bpm(140)); m_pTrack2->setBeats(pBeats2); ProcessBuffer(); @@ -2232,7 +2307,8 @@ TEST_F(EngineSyncTest, ChangeBeatGrid) { ASSERT_TRUE(isFollower(m_sGroup2)); // Load a new beatgrid again, this happens when the user adjusts the beatgrid - mixxx::BeatsPointer pBeats2n = BeatFactory::makeBeatGrid(*m_pTrack2, 75, 0.0); + auto pBeats2n = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats2n->setGrid(mixxx::Bpm(75)); m_pTrack2->setBeats(pBeats2n); ProcessBuffer(); @@ -2246,15 +2322,14 @@ TEST_F(EngineSyncTest, ChangeBeatGrid) { } TEST_F(EngineSyncTest, BeatMapQantizePlay) { - // This test demonstates https://bugs.launchpad.net/mixxx/+bug/1874918 - mixxx::BeatsPointer pBeats1 = BeatFactory::makeBeatGrid(*m_pTrack1, 120, 0.0); + // This test demonstrates https://bugs.launchpad.net/mixxx/+bug/1874918 + auto pBeats1 = BeatsPointer(new Beats(m_pTrack2.get())); + pBeats1->setGrid(mixxx::Bpm(120)); m_pTrack1->setBeats(pBeats1); - mixxx::BeatsPointer pBeats2 = mixxx::BeatsPointer(new mixxx::BeatMap(*m_pTrack2, 44100)); - // Add two beats at 120 Bpm - pBeats2->addBeat(44100 / 2); - pBeats2->addBeat(44100); - m_pTrack2->setBeats(pBeats2); + auto pBeats2 = BeatsPointer(new Beats(m_pTrack2.get())); + // Set second track's BPM to be the same. + m_pTrack2->setBpm(120); ControlObject::set(ConfigKey(m_sGroup1, "quantize"), 1.0); ControlObject::set(ConfigKey(m_sGroup2, "quantize"), 1.0); diff --git a/src/test/looping_control_test.cpp b/src/test/looping_control_test.cpp index 6e8685d1fb0..51e81ee1209 100644 --- a/src/test/looping_control_test.cpp +++ b/src/test/looping_control_test.cpp @@ -426,12 +426,12 @@ TEST_F(LoopingControlTest, LoopDoubleButton_IgnoresPastTrackEnd) { TEST_F(LoopingControlTest, LoopDoubleButton_DoublesBeatloopSize) { m_pTrack1->setBpm(120.0); - m_pBeatLoopSize->set(16.0); + m_pBeatLoopSize->set(4.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()); + EXPECT_EQ(8.0, m_pBeatLoopSize->get()); } TEST_F(LoopingControlTest, LoopDoubleButton_DoesNotResizeManualLoop) { @@ -477,12 +477,12 @@ TEST_F(LoopingControlTest, LoopHalveButton_IgnoresTooSmall) { TEST_F(LoopingControlTest, LoopHalveButton_HalvesBeatloopSize) { m_pTrack1->setBpm(120.0); - m_pBeatLoopSize->set(64.0); + m_pBeatLoopSize->set(16.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()); + EXPECT_EQ(8.0, m_pBeatLoopSize->get()); } TEST_F(LoopingControlTest, LoopHalveButton_DoesNotResizeManualLoop) { diff --git a/src/track/beatfactory.cpp b/src/track/beatfactory.cpp index a5fb85ff9d3..c7eebeb00d6 100644 --- a/src/track/beatfactory.cpp +++ b/src/track/beatfactory.cpp @@ -1,45 +1,71 @@ -#include +#include "track/beatfactory.h" + #include +#include -#include "track/beatgrid.h" -#include "track/beatmap.h" -#include "track/beatfactory.h" +#include "track/beats.h" #include "track/beatutils.h" +#include "track/track.h" -mixxx::BeatsPointer BeatFactory::loadBeatsFromByteArray(const Track& track, +mixxx::BeatsPointer BeatFactory::loadBeatsFromByteArray(const TrackPointer& track, QString beatsVersion, QString beatsSubVersion, const QByteArray& beatsSerialized) { - if (beatsVersion == BEAT_GRID_1_VERSION || - beatsVersion == BEAT_GRID_2_VERSION) { - mixxx::BeatGrid* pGrid = new mixxx::BeatGrid(track, 0, beatsSerialized); + // Now that the serialized representation is the same for BeatGrids and BeatMaps, + // they can be deserialized in a common function. + if (beatsVersion == mixxx::BeatsInternal::BEAT_GRID_1_VERSION || + beatsVersion == mixxx::BeatsInternal::BEAT_GRID_2_VERSION) { + mixxx::track::io::LegacyBeatGrid legacyBeatGridProto; + if (!legacyBeatGridProto.ParseFromArray( + beatsSerialized.constData(), beatsSerialized.size())) { + qDebug() + << "ERROR: Could not parse legacy" << beatsVersion << "from QByteArray of size" + << beatsSerialized.size(); + return mixxx::BeatsPointer(); + } + mixxx::Beats* pGrid = new mixxx::Beats(track.get()); + pGrid->setGrid(mixxx::Bpm(legacyBeatGridProto.bpm().bpm()), + mixxx::FramePos( + legacyBeatGridProto.first_beat().frame_position())); pGrid->setSubVersion(beatsSubVersion); - qDebug() << "Successfully deserialized BeatGrid"; + qDebug() << "Successfully deserialized Beats from legacy data in format" << beatsVersion; return mixxx::BeatsPointer(pGrid, &BeatFactory::deleteBeats); - } else if (beatsVersion == BEAT_MAP_VERSION) { - mixxx::BeatMap* pMap = new mixxx::BeatMap(track, 0, beatsSerialized); + } else if (beatsVersion == mixxx::BeatsInternal::BEAT_MAP_VERSION) { + mixxx::track::io::LegacyBeatMap legacyBeatMapProto; + if (!legacyBeatMapProto.ParseFromArray( + beatsSerialized.constData(), beatsSerialized.size())) { + qDebug() << "ERROR: Could not parse legacy" << beatsVersion << "from QByteArray of size" + << beatsSerialized.size(); + return mixxx::BeatsPointer(); + } + // Generate intermediate data from the old serialized representation. + QVector beatVector; + for (int i = 0; i < legacyBeatMapProto.beat_size(); ++i) { + const mixxx::track::io::LegacyBeat& beat = legacyBeatMapProto.beat(i); + beatVector.append(mixxx::FramePos(beat.frame_position())); + } + mixxx::Beats* pMap = new mixxx::Beats(track.get(), beatVector); pMap->setSubVersion(beatsSubVersion); - qDebug() << "Successfully deserialized BeatMap"; + qDebug() << "Successfully deserialized Beats from legacy data in format" << beatsVersion; return mixxx::BeatsPointer(pMap, &BeatFactory::deleteBeats); + } else if (beatsVersion == mixxx::BeatsInternal::BEATS_VERSION) { + mixxx::Beats* pBeats = new mixxx::Beats(track.get(), beatsSerialized); + pBeats->setSubVersion(beatsSubVersion); + qDebug() << "Successfully deserialized Beats"; + return mixxx::BeatsPointer(pBeats, &BeatFactory::deleteBeats); } qDebug() << "BeatFactory::loadBeatsFromByteArray could not parse serialized beats."; + // TODO(JVC) May be launching a reanalysis to fix the data? return mixxx::BeatsPointer(); } -mixxx::BeatsPointer BeatFactory::makeBeatGrid( - const Track& track, double dBpm, double dFirstBeatSample) { - mixxx::BeatGrid* pGrid = new mixxx::BeatGrid(track, 0); - pGrid->setGrid(dBpm, dFirstBeatSample); - return mixxx::BeatsPointer(pGrid, &BeatFactory::deleteBeats); -} - // static QString BeatFactory::getPreferredVersion( const bool bEnableFixedTempoCorrection) { if (bEnableFixedTempoCorrection) { - return BEAT_GRID_2_VERSION; + return mixxx::BeatsInternal::BEAT_GRID_2_VERSION; } - return BEAT_MAP_VERSION; + return mixxx::BeatsInternal::BEAT_MAP_VERSION; } QString BeatFactory::getPreferredSubVersion( @@ -92,15 +118,15 @@ QString BeatFactory::getPreferredSubVersion( : ""; } -mixxx::BeatsPointer BeatFactory::makePreferredBeats(const Track& track, +mixxx::BeatsPointer BeatFactory::makePreferredBeats(const TrackPointer& track, QVector beats, const QHash extraVersionInfo, const bool bEnableFixedTempoCorrection, const bool bEnableOffsetCorrection, - const int iSampleRate, const int iTotalSamples, const int iMinBpm, const int iMaxBpm) { + const int iSampleRate = track->getSampleRate(); const QString version = getPreferredVersion(bEnableFixedTempoCorrection); const QString subVersion = getPreferredSubVersion(bEnableFixedTempoCorrection, bEnableOffsetCorrection, @@ -108,22 +134,38 @@ mixxx::BeatsPointer BeatFactory::makePreferredBeats(const Track& track, extraVersionInfo); BeatUtils::printBeatStatistics(beats, iSampleRate); - if (version == BEAT_GRID_2_VERSION) { - double globalBpm = BeatUtils::calculateBpm(beats, iSampleRate, iMinBpm, iMaxBpm); - double firstBeat = BeatUtils::calculateFixedTempoFirstBeat( - bEnableOffsetCorrection, - beats, iSampleRate, iTotalSamples, globalBpm); - mixxx::BeatGrid* pGrid = new mixxx::BeatGrid(track, iSampleRate); - // firstBeat is in frames here and setGrid() takes samples. - pGrid->setGrid(globalBpm, firstBeat * 2); - pGrid->setSubVersion(subVersion); - return mixxx::BeatsPointer(pGrid, &BeatFactory::deleteBeats); - } else if (version == BEAT_MAP_VERSION) { - mixxx::BeatMap* pBeatMap = new mixxx::BeatMap(track, iSampleRate, beats); - pBeatMap->setSubVersion(subVersion); - return mixxx::BeatsPointer(pBeatMap, &BeatFactory::deleteBeats); + if (version == mixxx::BeatsInternal::BEAT_GRID_2_VERSION) { + mixxx::Bpm globalBpm = BeatUtils::calculateBpm(beats, iSampleRate, iMinBpm, iMaxBpm); + mixxx::FramePos firstBeat(BeatUtils::calculateFixedTempoFirstBeat( + bEnableOffsetCorrection, + beats, + iSampleRate, + iTotalSamples, + globalBpm.getValue())); + mixxx::FrameDiff_t beatLength = iSampleRate * 60 / globalBpm.getValue(); + double trackLengthSeconds = track->getDuration(); + int numberOfBeats = globalBpm.getValue() / 60.0 * trackLengthSeconds; + QVector generatedBeats; + for (int i = 0; i < numberOfBeats; i++) { + generatedBeats.append(firstBeat + beatLength * i); + } + mixxx::Beats* pBeats = new mixxx::Beats( + track.get(), generatedBeats); + pBeats->setSubVersion(subVersion); + return mixxx::BeatsPointer(pBeats, &BeatFactory::deleteBeats); + } else if (version == mixxx::BeatsInternal::BEAT_MAP_VERSION) { + QVector intermediateBeatFrameVector; + intermediateBeatFrameVector.reserve(beats.size()); + std::transform(beats.begin(), + beats.end(), + std::back_inserter(intermediateBeatFrameVector), + [](double value) { return mixxx::FramePos(value); }); + mixxx::Beats* pBeats = new mixxx::Beats( + track.get(), intermediateBeatFrameVector); + pBeats->setSubVersion(subVersion); + return mixxx::BeatsPointer(pBeats, &BeatFactory::deleteBeats); } else { - qDebug() << "ERROR: Could not determine what type of beatgrid to create."; + DEBUG_ASSERT("Could not determine what type of beatgrid to create."); return mixxx::BeatsPointer(); } } diff --git a/src/track/beatfactory.h b/src/track/beatfactory.h index cb25021f19d..830bdab3d0d 100644 --- a/src/track/beatfactory.h +++ b/src/track/beatfactory.h @@ -8,11 +8,11 @@ class BeatFactory { public: - static mixxx::BeatsPointer loadBeatsFromByteArray(const Track& track, + static mixxx::BeatsPointer loadBeatsFromByteArray(const TrackPointer& track, QString beatsVersion, QString beatsSubVersion, const QByteArray& beatsSerialized); - static mixxx::BeatsPointer makeBeatGrid(const Track& track, + static mixxx::BeatsPointer makeBeatGrid(const TrackPointer& track, double dBpm, double dFirstBeatSample); @@ -24,12 +24,11 @@ class BeatFactory { const int iMinBpm, const int iMaxBpm, const QHash extraVersionInfo); - static mixxx::BeatsPointer makePreferredBeats(const Track& track, + static mixxx::BeatsPointer makePreferredBeats(const TrackPointer& track, QVector beats, const QHash extraVersionInfo, const bool bEnableFixedTempoCorrection, const bool bEnableOffsetCorrection, - const int iSampleRate, const int iTotalSamples, const int iMinBpm, const int iMaxBpm); diff --git a/src/track/beatgrid.cpp b/src/track/beatgrid.cpp deleted file mode 100644 index 815314f736e..00000000000 --- a/src/track/beatgrid.cpp +++ /dev/null @@ -1,368 +0,0 @@ -#include -#include - -#include "track/beatgrid.h" -#include "util/math.h" - -static const int kFrameSize = 2; - -struct BeatGridData { - double bpm; - double firstBeat; -}; - -namespace mixxx { - -class BeatGridIterator : public BeatIterator { - public: - BeatGridIterator(double dBeatLength, double dFirstBeat, double dEndSample) - : m_dBeatLength(dBeatLength), - m_dCurrentSample(dFirstBeat), - m_dEndSample(dEndSample) { - } - - virtual bool hasNext() const { - return m_dBeatLength > 0 && m_dCurrentSample <= m_dEndSample; - } - - virtual double next() { - double beat = m_dCurrentSample; - m_dCurrentSample += m_dBeatLength; - return beat; - } - - private: - double m_dBeatLength; - double m_dCurrentSample; - double m_dEndSample; -}; - -BeatGrid::BeatGrid( - const Track& track, - SINT iSampleRate) - : m_mutex(QMutex::Recursive), - m_iSampleRate(iSampleRate > 0 ? iSampleRate : track.getSampleRate()), - m_dBeatLength(0.0) { - // BeatGrid should live in the same thread as the track it is associated - // with. - moveToThread(track.thread()); -} - -BeatGrid::BeatGrid( - const Track& track, - SINT iSampleRate, - const QByteArray& byteArray) - : BeatGrid(track, iSampleRate) { - readByteArray(byteArray); -} - -BeatGrid::BeatGrid(const BeatGrid& other) - : m_mutex(QMutex::Recursive), - m_subVersion(other.m_subVersion), - m_iSampleRate(other.m_iSampleRate), - m_grid(other.m_grid), - m_dBeatLength(other.m_dBeatLength) { - moveToThread(other.thread()); -} - -void BeatGrid::setGrid(double dBpm, double dFirstBeatSample) { - if (dBpm < 0) { - dBpm = 0.0; - } - - QMutexLocker lock(&m_mutex); - m_grid.mutable_bpm()->set_bpm(dBpm); - m_grid.mutable_first_beat()->set_frame_position(dFirstBeatSample / kFrameSize); - // Calculate beat length as sample offsets - m_dBeatLength = (60.0 * m_iSampleRate / dBpm) * kFrameSize; -} - -QByteArray BeatGrid::toByteArray() const { - QMutexLocker locker(&m_mutex); - std::string output; - m_grid.SerializeToString(&output); - return QByteArray(output.data(), output.length()); -} - -BeatsPointer BeatGrid::clone() const { - QMutexLocker locker(&m_mutex); - BeatsPointer other(new BeatGrid(*this)); - return other; -} - -void BeatGrid::readByteArray(const QByteArray& byteArray) { - mixxx::track::io::BeatGrid grid; - if (grid.ParseFromArray(byteArray.constData(), byteArray.length())) { - m_grid = grid; - m_dBeatLength = (60.0 * m_iSampleRate / bpm()) * kFrameSize; - return; - } - - // Legacy fallback for BeatGrid-1.0 - if (byteArray.size() != sizeof(BeatGridData)) { - return; - } - const BeatGridData* blob = reinterpret_cast(byteArray.constData()); - - // We serialize into frame offsets but use sample offsets at runtime - setGrid(blob->bpm, blob->firstBeat * kFrameSize); -} - -double BeatGrid::firstBeatSample() const { - return m_grid.first_beat().frame_position() * kFrameSize; -} - -double BeatGrid::bpm() const { - return m_grid.bpm().bpm(); -} - -QString BeatGrid::getVersion() const { - QMutexLocker locker(&m_mutex); - return BEAT_GRID_2_VERSION; -} - -QString BeatGrid::getSubVersion() const { - QMutexLocker locker(&m_mutex); - return m_subVersion; -} - -void BeatGrid::setSubVersion(QString subVersion) { - m_subVersion = subVersion; -} - -// internal use only -bool BeatGrid::isValid() const { - return m_iSampleRate > 0 && bpm() > 0; -} - -// This could be implemented in the Beats Class itself. -// If necessary, the child class can redefine it. -double BeatGrid::findNextBeat(double dSamples) const { - return findNthBeat(dSamples, +1); -} - -// This could be implemented in the Beats Class itself. -// If necessary, the child class can redefine it. -double BeatGrid::findPrevBeat(double dSamples) const { - return findNthBeat(dSamples, -1); -} - -// This is an internal call. This could be implemented in the Beats Class itself. -double BeatGrid::findClosestBeat(double dSamples) const { - QMutexLocker locker(&m_mutex); - if (!isValid()) { - return -1; - } - double prevBeat; - double nextBeat; - findPrevNextBeats(dSamples, &prevBeat, &nextBeat); - if (prevBeat == -1) { - // If both values are -1, we correctly return -1. - return nextBeat; - } else if (nextBeat == -1) { - return prevBeat; - } - return (nextBeat - dSamples > dSamples - prevBeat) ? prevBeat : nextBeat; -} - -double BeatGrid::findNthBeat(double dSamples, int n) const { - QMutexLocker locker(&m_mutex); - if (!isValid() || n == 0) { - return -1; - } - - double beatFraction = (dSamples - firstBeatSample()) / m_dBeatLength; - double prevBeat = floor(beatFraction); - double nextBeat = ceil(beatFraction); - - // If the position is within 1/100th of the next or previous beat, treat it - // as if it is that beat. - const double kEpsilon = .01; - - if (fabs(nextBeat - beatFraction) < kEpsilon) { - // If we are going to pretend we were actually on nextBeat then prevBeat - // needs to be re-calculated. Since it is floor(beatFraction), that's - // the same as nextBeat. We only use prevBeat so no need to increment - // nextBeat. - prevBeat = nextBeat; - } else if (fabs(prevBeat - beatFraction) < kEpsilon) { - // If we are going to pretend we were actually on prevBeat then nextBeat - // needs to be re-calculated. Since it is ceil(beatFraction), that's - // the same as prevBeat. We will only use nextBeat so no need to - // decrement prevBeat. - nextBeat = prevBeat; - } - - double dClosestBeat; - if (n > 0) { - // We're going forward, so use ceil to round up to the next multiple of - // m_dBeatLength - dClosestBeat = nextBeat * m_dBeatLength + firstBeatSample(); - n = n - 1; - } else { - // We're going backward, so use floor to round down to the next multiple - // of m_dBeatLength - dClosestBeat = prevBeat * m_dBeatLength + firstBeatSample(); - n = n + 1; - } - - double dResult = dClosestBeat + n * m_dBeatLength; - return dResult; -} - -bool BeatGrid::findPrevNextBeats(double dSamples, - double* dpPrevBeatSamples, - double* dpNextBeatSamples) const { - double dFirstBeatSample; - double dBeatLength; - { - QMutexLocker locker(&m_mutex); - if (!isValid()) { - *dpPrevBeatSamples = -1.0; - *dpNextBeatSamples = -1.0; - return false; - } - dFirstBeatSample = firstBeatSample(); - dBeatLength = m_dBeatLength; - } - - double beatFraction = (dSamples - dFirstBeatSample) / dBeatLength; - double prevBeat = floor(beatFraction); - double nextBeat = ceil(beatFraction); - - // If the position is within 1/100th of the next or previous beat, treat it - // as if it is that beat. - const double kEpsilon = .01; - - if (fabs(nextBeat - beatFraction) < kEpsilon) { - beatFraction = nextBeat; - // If we are going to pretend we were actually on nextBeat then prevBeatFraction - // needs to be re-calculated. Since it is floor(beatFraction), that's - // the same as nextBeat. - prevBeat = nextBeat; - // And nextBeat needs to be incremented. - ++nextBeat; - } - *dpPrevBeatSamples = prevBeat * dBeatLength + dFirstBeatSample; - *dpNextBeatSamples = nextBeat * dBeatLength + dFirstBeatSample; - return true; -} - - -std::unique_ptr BeatGrid::findBeats(double startSample, double stopSample) const { - QMutexLocker locker(&m_mutex); - if (!isValid() || startSample > stopSample) { - return std::unique_ptr(); - } - //qDebug() << "BeatGrid::findBeats startSample" << startSample << "stopSample" - // << stopSample << "beatlength" << m_dBeatLength << "BPM" << bpm(); - double curBeat = findNextBeat(startSample); - if (curBeat == -1.0) { - return std::unique_ptr(); - } - return std::make_unique(m_dBeatLength, curBeat, stopSample); -} - -bool BeatGrid::hasBeatInRange(double startSample, double stopSample) const { - QMutexLocker locker(&m_mutex); - if (!isValid() || startSample > stopSample) { - return false; - } - double curBeat = findNextBeat(startSample); - if (curBeat != -1.0 && curBeat <= stopSample) { - return true; - } - return false; -} - -double BeatGrid::getBpm() const { - QMutexLocker locker(&m_mutex); - if (!isValid()) { - return 0; - } - return bpm(); -} - -double BeatGrid::getBpmRange(double startSample, double stopSample) const { - QMutexLocker locker(&m_mutex); - if (!isValid() || startSample > stopSample) { - return -1; - } - return bpm(); -} - -double BeatGrid::getBpmAroundPosition(double curSample, int n) const { - Q_UNUSED(curSample); - Q_UNUSED(n); - - QMutexLocker locker(&m_mutex); - if (!isValid()) { - return -1; - } - return bpm(); -} - -void BeatGrid::addBeat(double dBeatSample) { - Q_UNUSED(dBeatSample); - //QMutexLocker locker(&m_mutex); - return; -} - -void BeatGrid::removeBeat(double dBeatSample) { - Q_UNUSED(dBeatSample); - //QMutexLocker locker(&m_mutex); - return; -} - -void BeatGrid::translate(double dNumSamples) { - QMutexLocker locker(&m_mutex); - if (!isValid()) { - return; - } - double newFirstBeatFrames = (firstBeatSample() + dNumSamples) / kFrameSize; - m_grid.mutable_first_beat()->set_frame_position(newFirstBeatFrames); - locker.unlock(); - emit updated(); -} - -void BeatGrid::scale(enum BPMScale scale) { - double bpm = getBpm(); - - switch (scale) { - case DOUBLE: - bpm *= 2; - break; - case HALVE: - bpm *= 1.0 / 2; - break; - case TWOTHIRDS: - bpm *= 2.0 / 3; - break; - case THREEFOURTHS: - bpm *= 3.0 / 4; - break; - case FOURTHIRDS: - bpm *= 4.0 / 3; - break; - case THREEHALVES: - bpm *= 3.0 / 2; - break; - default: - DEBUG_ASSERT(!"scale value invalid"); - return; - } - setBpm(bpm); -} - -void BeatGrid::setBpm(double dBpm) { - QMutexLocker locker(&m_mutex); - if (dBpm > getMaxBpm()) { - dBpm = getMaxBpm(); - } - m_grid.mutable_bpm()->set_bpm(dBpm); - m_dBeatLength = (60.0 * m_iSampleRate / dBpm) * kFrameSize; - locker.unlock(); - emit updated(); -} - -} // namespace mixxx diff --git a/src/track/beatgrid.h b/src/track/beatgrid.h deleted file mode 100644 index f1f09faf288..00000000000 --- a/src/track/beatgrid.h +++ /dev/null @@ -1,101 +0,0 @@ -#ifndef BEATGRID_H -#define BEATGRID_H - -#include - -#include "track/track.h" -#include "track/beats.h" -#include "proto/beats.pb.h" - -#define BEAT_GRID_1_VERSION "BeatGrid-1.0" -#define BEAT_GRID_2_VERSION "BeatGrid-2.0" - -namespace mixxx { - -// BeatGrid is an implementation of the Beats interface that implements an -// infinite grid of beats, aligned to a song simply by a starting offset of the -// first beat and the song's average beats-per-minute. -class BeatGrid final : public Beats { - public: - // Construct a BeatGrid. If a more accurate sample rate is known, provide it - // in the iSampleRate parameter -- otherwise pass 0. - BeatGrid(const Track& track, SINT iSampleRate); - // Construct a BeatGrid. If a more accurate sample rate is known, provide it - // in the iSampleRate parameter -- otherwise pass 0. The BeatGrid will be - // deserialized from the byte array. - BeatGrid(const Track& track, SINT iSampleRate, - const QByteArray& byteArray); - ~BeatGrid() override = default; - - // Initializes the BeatGrid to have a BPM of dBpm and the first beat offset - // of dFirstBeatSample. Does not generate an updated() signal, since it is - // meant for initialization. - void setGrid(double dBpm, double dFirstBeatSample); - - // The following are all methods from the Beats interface, see method - // comments in beats.h - - Beats::CapabilitiesFlags getCapabilities() const override { - return BEATSCAP_TRANSLATE | BEATSCAP_SCALE | BEATSCAP_SETBPM; - } - - QByteArray toByteArray() const override; - BeatsPointer clone() const override; - QString getVersion() const override; - QString getSubVersion() const override; - virtual void setSubVersion(QString subVersion); - - //////////////////////////////////////////////////////////////////////////// - // Beat calculations - //////////////////////////////////////////////////////////////////////////// - - double findNextBeat(double dSamples) const override; - double findPrevBeat(double dSamples) const override; - bool findPrevNextBeats(double dSamples, - double* dpPrevBeatSamples, - double* dpNextBeatSamples) const override; - double findClosestBeat(double dSamples) const override; - double findNthBeat(double dSamples, int n) const override; - std::unique_ptr findBeats(double startSample, double stopSample) const override; - bool hasBeatInRange(double startSample, double stopSample) const override; - double getBpm() const override; - double getBpmRange(double startSample, double stopSample) const override; - double getBpmAroundPosition(double curSample, int n) const override; - - //////////////////////////////////////////////////////////////////////////// - // Beat mutations - //////////////////////////////////////////////////////////////////////////// - - void addBeat(double dBeatSample) override; - void removeBeat(double dBeatSample) override; - void translate(double dNumSamples) override; - void scale(enum BPMScale scale) override; - void setBpm(double dBpm) override; - - SINT getSampleRate() const override { - return m_iSampleRate; - } - - private: - BeatGrid(const BeatGrid& other); - double firstBeatSample() const; - double bpm() const; - - void readByteArray(const QByteArray& byteArray); - // For internal use only. - bool isValid() const; - - mutable QMutex m_mutex; - // The sub-version of this beatgrid. - QString m_subVersion; - // The number of samples per second - SINT m_iSampleRate; - // Data storage for BeatGrid - mixxx::track::io::BeatGrid m_grid; - // The length of a beat in samples - double m_dBeatLength; -}; - -} // namespace mixxx - -#endif /* BEATGRID_H */ diff --git a/src/track/beatiterator.h b/src/track/beatiterator.h new file mode 100644 index 00000000000..ad2b1d04242 --- /dev/null +++ b/src/track/beatiterator.h @@ -0,0 +1,64 @@ +#pragma once + +#include "track/beats.h" +#include "track/beatutils.h" + +namespace mixxx { +/// A forward iterator for Beats. +/// BeatIterator do not fulfills the Iterator semantics, use with care. +class BeatIterator final { + public: + BeatIterator(BeatList::const_iterator start, BeatList::const_iterator end) + : m_currentBeat(start), + m_endBeat(end) { + } + + using iterator_category = std::forward_iterator_tag; + + bool hasNext() const { + return m_currentBeat != m_endBeat; + } + + /// Advances the iterator and returns the current beat frame position. + /// If you need the frame position make sure you store it, is not possible + /// to get it again. + track::io::Beat next() { + if (hasNext()) { + return *m_currentBeat++; + } else { + return track::io::Beat(); + } + } + + /* + // TODO(hacksdump): These will be removed from this iterator and + // will be made methods of the Beat class to be introduced in #2844. + + /// Returns true if the current beat is a down beat. + bool isBar() const { + return m_currentBeat->type() == mixxx::track::io::BAR; + } + + /// Returns true if the current beat is a phrase beat. + bool isPhrase() const { + return m_currentBeat->type() == mixxx::track::io::PHRASE; + } + + /// The current beat becomes a regular beat. + void makeBeat() { + // TODO(JVC) Const_cast is needed until we manage to make BeatIterator read/write + const_cast(*m_currentBeat).set_type(mixxx::track::io::BEAT); + } + + /// The current beat becomes a downbeat. + void makeBar() { + // TODO(JVC) Const_cast is needed until we manage to make BeatIterator read/write + const_cast(*m_currentBeat).set_type(mixxx::track::io::BAR); + }*/ + + private: + BeatList::const_iterator m_currentBeat; + BeatList::const_iterator m_endBeat; +}; + +} // Namespace mixxx diff --git a/src/track/beatmap.cpp b/src/track/beatmap.cpp deleted file mode 100644 index 661f79e9f98..00000000000 --- a/src/track/beatmap.cpp +++ /dev/null @@ -1,729 +0,0 @@ -/* - * beatmap.cpp - * - * Created on: 08/dic/2011 - * Author: vittorio - */ - -#include -#include -#include -#include - -#include "track/beatmap.h" -#include "track/beatutils.h" -#include "util/math.h" - -using mixxx::track::io::Beat; - -const int kFrameSize = 2; - -inline double samplesToFrames(const double samples) { - return floor(samples / kFrameSize); -} - -inline double framesToSamples(const int frames) { - return frames * kFrameSize; -} - -bool BeatLessThan(const Beat& beat1, const Beat& beat2) { - return beat1.frame_position() < beat2.frame_position(); -} - -namespace mixxx { - -class BeatMapIterator : public BeatIterator { - public: - BeatMapIterator(BeatList::const_iterator start, BeatList::const_iterator end) - : m_currentBeat(start), - m_endBeat(end) { - // Advance to the first enabled beat. - while (m_currentBeat != m_endBeat && !m_currentBeat->enabled()) { - ++m_currentBeat; - } - } - - virtual bool hasNext() const { - return m_currentBeat != m_endBeat; - } - - virtual double next() { - double beat = framesToSamples(m_currentBeat->frame_position()); - ++m_currentBeat; - while (m_currentBeat != m_endBeat && !m_currentBeat->enabled()) { - ++m_currentBeat; - } - return beat; - } - - private: - BeatList::const_iterator m_currentBeat; - BeatList::const_iterator m_endBeat; -}; - -BeatMap::BeatMap(const Track& track, SINT iSampleRate) - : m_mutex(QMutex::Recursive), - m_iSampleRate(iSampleRate > 0 ? iSampleRate : track.getSampleRate()), - m_dCachedBpm(0), - m_dLastFrame(0) { - // BeatMap should live in the same thread as the track it is associated - // with. - moveToThread(track.thread()); -} - -BeatMap::BeatMap(const Track& track, SINT iSampleRate, - const QByteArray& byteArray) - : BeatMap(track, iSampleRate) { - readByteArray(byteArray); -} - -BeatMap::BeatMap(const Track& track, SINT iSampleRate, - const QVector& beats) - : BeatMap(track, iSampleRate) { - if (beats.size() > 0) { - createFromBeatVector(beats); - } -} - -BeatMap::BeatMap (const BeatMap& other) - : m_mutex(QMutex::Recursive), - m_subVersion(other.m_subVersion), - m_iSampleRate(other.m_iSampleRate), - m_dCachedBpm(other.m_dCachedBpm), - m_dLastFrame(other.m_dLastFrame), - m_beats(other.m_beats) { - moveToThread(other.thread()); -} - -QByteArray BeatMap::toByteArray() const { - QMutexLocker locker(&m_mutex); - // No guarantees BeatLists are made of a data type which located adjacent - // items in adjacent memory locations. - mixxx::track::io::BeatMap map; - - for (int i = 0; i < m_beats.size(); ++i) { - map.add_beat()->CopyFrom(m_beats[i]); - } - - std::string output; - map.SerializeToString(&output); - return QByteArray(output.data(), output.length()); -} - -BeatsPointer BeatMap::clone() const { - QMutexLocker locker(&m_mutex); - BeatsPointer other(new BeatMap(*this)); - return other; -} - -bool BeatMap::readByteArray(const QByteArray& byteArray) { - mixxx::track::io::BeatMap map; - if (!map.ParseFromArray(byteArray.constData(), byteArray.size())) { - qDebug() << "ERROR: Could not parse BeatMap from QByteArray of size" - << byteArray.size(); - return false; - } - for (int i = 0; i < map.beat_size(); ++i) { - const Beat& beat = map.beat(i); - m_beats.append(beat); - } - onBeatlistChanged(); - return true; -} - -void BeatMap::createFromBeatVector(const QVector& beats) { - if (beats.isEmpty()) { - return; - } - double previous_beatpos = -1; - Beat beat; - - foreach (double beatpos, beats) { - // beatpos is in frames. Do not accept fractional frames. - beatpos = floor(beatpos); - if (beatpos <= previous_beatpos || beatpos < 0) { - qDebug() << "BeatMap::createFromVector: beats not in increasing order or negative"; - qDebug() << "discarding beat " << beatpos; - } else { - beat.set_frame_position(beatpos); - m_beats.append(beat); - previous_beatpos = beatpos; - } - } - onBeatlistChanged(); -} - -QString BeatMap::getVersion() const { - QMutexLocker locker(&m_mutex); - return BEAT_MAP_VERSION; -} - -QString BeatMap::getSubVersion() const { - QMutexLocker locker(&m_mutex); - return m_subVersion; -} - -void BeatMap::setSubVersion(QString subVersion) { - m_subVersion = subVersion; -} - -bool BeatMap::isValid() const { - return m_iSampleRate > 0 && m_beats.size() > 0; -} - -double BeatMap::findNextBeat(double dSamples) const { - return findNthBeat(dSamples, 1); -} - -double BeatMap::findPrevBeat(double dSamples) const { - return findNthBeat(dSamples, -1); -} - -double BeatMap::findClosestBeat(double dSamples) const { - QMutexLocker locker(&m_mutex); - if (!isValid()) { - return -1; - } - double prevBeat; - double nextBeat; - findPrevNextBeats(dSamples, &prevBeat, &nextBeat); - if (prevBeat == -1) { - // If both values are -1, we correctly return -1. - return nextBeat; - } else if (nextBeat == -1) { - return prevBeat; - } - return (nextBeat - dSamples > dSamples - prevBeat) ? prevBeat : nextBeat; -} - -double BeatMap::findNthBeat(double dSamples, int n) const { - QMutexLocker locker(&m_mutex); - - if (!isValid() || n == 0) { - return -1; - } - - Beat beat; - // Reduce sample offset to a frame offset. - beat.set_frame_position(samplesToFrames(dSamples)); - - // it points at the first occurrence of beat or the next largest beat - BeatList::const_iterator it = - std::lower_bound(m_beats.constBegin(), m_beats.constEnd(), beat, BeatLessThan); - - // If the position is within 1/10th of a second of the next or previous - // beat, pretend we are on that beat. - const double kFrameEpsilon = 0.1 * m_iSampleRate; - - // Back-up by one. - if (it != m_beats.begin()) { - --it; - } - - // Scan forward to find whether we are on a beat. - BeatList::const_iterator on_beat = m_beats.constEnd(); - BeatList::const_iterator previous_beat = m_beats.constEnd(); - BeatList::const_iterator next_beat = m_beats.constEnd(); - for (; it != m_beats.end(); ++it) { - qint32 delta = it->frame_position() - beat.frame_position(); - - // We are "on" this beat. - if (abs(delta) < kFrameEpsilon) { - on_beat = it; - break; - } - - if (delta < 0) { - // If we are not on the beat and delta < 0 then this beat comes - // before our current position. - previous_beat = it; - } else { - // If we are past the beat and we aren't on it then this beat comes - // after our current position. - next_beat = it; - // Stop because we have everything we need now. - break; - } - } - - // If we are within epsilon samples of a beat then the immediately next and - // previous beats are the beat we are on. - if (on_beat != m_beats.end()) { - next_beat = on_beat; - previous_beat = on_beat; - } - - if (n > 0) { - for (; next_beat != m_beats.end(); ++next_beat) { - if (!next_beat->enabled()) { - continue; - } - if (n == 1) { - // Return a sample offset - return framesToSamples(next_beat->frame_position()); - } - --n; - } - } else if (n < 0 && previous_beat != m_beats.end()) { - for (; true; --previous_beat) { - if (previous_beat->enabled()) { - if (n == -1) { - // Return a sample offset - return framesToSamples(previous_beat->frame_position()); - } - ++n; - } - - // Don't step before the start of the list. - if (previous_beat == m_beats.begin()) { - break; - } - } - } - return -1; -} - -bool BeatMap::findPrevNextBeats(double dSamples, - double* dpPrevBeatSamples, - double* dpNextBeatSamples) const { - QMutexLocker locker(&m_mutex); - - if (!isValid()) { - *dpPrevBeatSamples = -1; - *dpNextBeatSamples = -1; - return false; - } - - Beat beat; - // Reduce sample offset to a frame offset. - beat.set_frame_position(samplesToFrames(dSamples)); - - // it points at the first occurrence of beat or the next largest beat - BeatList::const_iterator it = - std::lower_bound(m_beats.constBegin(), m_beats.constEnd(), beat, BeatLessThan); - - // If the position is within 1/10th of a second of the next or previous - // beat, pretend we are on that beat. - const double kFrameEpsilon = 0.1 * m_iSampleRate; - - // Back-up by one. - if (it != m_beats.begin()) { - --it; - } - - // Scan forward to find whether we are on a beat. - BeatList::const_iterator on_beat = m_beats.constEnd(); - BeatList::const_iterator previous_beat = m_beats.constEnd(); - BeatList::const_iterator next_beat = m_beats.constEnd(); - for (; it != m_beats.end(); ++it) { - qint32 delta = it->frame_position() - beat.frame_position(); - - // We are "on" this beat. - if (abs(delta) < kFrameEpsilon) { - on_beat = it; - break; - } - - if (delta < 0) { - // If we are not on the beat and delta < 0 then this beat comes - // before our current position. - previous_beat = it; - } else { - // If we are past the beat and we aren't on it then this beat comes - // after our current position. - next_beat = it; - // Stop because we have everything we need now. - break; - } - } - - // If we are within epsilon samples of a beat then the immediately next and - // previous beats are the beat we are on. - if (on_beat != m_beats.end()) { - previous_beat = on_beat; - next_beat = on_beat + 1; - } - - *dpPrevBeatSamples = -1; - *dpNextBeatSamples = -1; - - for (; next_beat != m_beats.end(); ++next_beat) { - if (!next_beat->enabled()) { - continue; - } - *dpNextBeatSamples = framesToSamples(next_beat->frame_position()); - break; - } - if (previous_beat != m_beats.end()) { - for (; true; --previous_beat) { - if (previous_beat->enabled()) { - *dpPrevBeatSamples = framesToSamples(previous_beat->frame_position()); - break; - } - - // Don't step before the start of the list. - if (previous_beat == m_beats.begin()) { - break; - } - } - } - return *dpPrevBeatSamples != -1 && *dpNextBeatSamples != -1; -} - -std::unique_ptr BeatMap::findBeats(double startSample, double stopSample) const { - QMutexLocker locker(&m_mutex); - //startSample and stopSample are sample offsets, converting them to - //frames - if (!isValid() || startSample > stopSample) { - return std::unique_ptr(); - } - - Beat startBeat, stopBeat; - startBeat.set_frame_position(samplesToFrames(startSample)); - stopBeat.set_frame_position(samplesToFrames(stopSample)); - - BeatList::const_iterator curBeat = - std::lower_bound(m_beats.constBegin(), m_beats.constEnd(), - startBeat, BeatLessThan); - - BeatList::const_iterator lastBeat = - std::upper_bound(m_beats.constBegin(), m_beats.constEnd(), - stopBeat, BeatLessThan); - - if (curBeat >= lastBeat) { - return std::unique_ptr(); - } - return std::make_unique(curBeat, lastBeat); -} - -bool BeatMap::hasBeatInRange(double startSample, double stopSample) const { - QMutexLocker locker(&m_mutex); - if (!isValid() || startSample > stopSample) { - return false; - } - double curBeat = findNextBeat(startSample); - if (curBeat <= stopSample) { - return true; - } - return false; -} - -double BeatMap::getBpm() const { - QMutexLocker locker(&m_mutex); - if (!isValid()) - return -1; - return m_dCachedBpm; -} - -double BeatMap::getBpmRange(double startSample, double stopSample) const { - QMutexLocker locker(&m_mutex); - if (!isValid()) - return -1; - Beat startBeat, stopBeat; - startBeat.set_frame_position(samplesToFrames(startSample)); - stopBeat.set_frame_position(samplesToFrames(stopSample)); - return calculateBpm(startBeat, stopBeat); -} - -double BeatMap::getBpmAroundPosition(double curSample, int n) const { - QMutexLocker locker(&m_mutex); - if (!isValid()) - return -1; - - // To make sure we are always counting n beats, iterate backward to the - // lower bound, then iterate forward from there to the upper bound. - // a value of -1 indicates we went off the map -- count from the beginning. - double lower_bound = findNthBeat(curSample, -n); - if (lower_bound == -1) { - lower_bound = framesToSamples(m_beats.first().frame_position()); - } - - // If we hit the end of the beat map, recalculate the lower bound. - double upper_bound = findNthBeat(lower_bound, n * 2); - if (upper_bound == -1) { - upper_bound = framesToSamples(m_beats.last().frame_position()); - lower_bound = findNthBeat(upper_bound, n * -2); - // Super edge-case -- the track doesn't have n beats! Do the best - // we can. - if (lower_bound == -1) { - lower_bound = framesToSamples(m_beats.first().frame_position()); - } - } - - Beat startBeat, stopBeat; - startBeat.set_frame_position(samplesToFrames(lower_bound)); - stopBeat.set_frame_position(samplesToFrames(upper_bound)); - return calculateBpm(startBeat, stopBeat); -} - -void BeatMap::addBeat(double dBeatSample) { - QMutexLocker locker(&m_mutex); - Beat beat; - beat.set_frame_position(samplesToFrames(dBeatSample)); - BeatList::iterator it = std::lower_bound( - m_beats.begin(), m_beats.end(), beat, BeatLessThan); - - // Don't insert a duplicate beat. TODO(XXX) determine what epsilon to - // consider a beat identical to another. - if (it != m_beats.end() && it->frame_position() == beat.frame_position()) { - return; - } - - m_beats.insert(it, beat); - onBeatlistChanged(); - locker.unlock(); - emit updated(); -} - -void BeatMap::removeBeat(double dBeatSample) { - QMutexLocker locker(&m_mutex); - Beat beat; - beat.set_frame_position(samplesToFrames(dBeatSample)); - BeatList::iterator it = std::lower_bound( - m_beats.begin(), m_beats.end(), beat, BeatLessThan); - - // In case there are duplicates, remove every instance of dBeatSample - // TODO(XXX) add invariant checks against this - // TODO(XXX) determine what epsilon to consider a beat identical to another - while (it->frame_position() == beat.frame_position()) { - it = m_beats.erase(it); - } - onBeatlistChanged(); - locker.unlock(); - emit updated(); -} - -void BeatMap::translate(double dNumSamples) { - QMutexLocker locker(&m_mutex); - // Converting to frame offset - if (!isValid()) { - return; - } - - double dNumFrames = samplesToFrames(dNumSamples); - for (BeatList::iterator it = m_beats.begin(); - it != m_beats.end(); ) { - double newpos = it->frame_position() + dNumFrames; - if (newpos >= 0) { - it->set_frame_position(newpos); - ++it; - } else { - it = m_beats.erase(it); - } - } - onBeatlistChanged(); - locker.unlock(); - emit updated(); -} - -void BeatMap::scale(enum BPMScale scale) { - - QMutexLocker locker(&m_mutex); - if (!isValid() || m_beats.isEmpty()) { - return; - } - - switch (scale) { - case DOUBLE: - // introduce a new beat into every gap - scaleDouble(); - break; - case HALVE: - // remove every second beat - scaleHalve(); - break; - case TWOTHIRDS: - // introduce a new beat into every gap - scaleDouble(); - // remove every second and third beat - scaleThird(); - break; - case THREEFOURTHS: - // introduce two beats into every gap - scaleTriple(); - // remove every second third and forth beat - scaleFourth(); - break; - case FOURTHIRDS: - // introduce three beats into every gap - scaleQuadruple(); - // remove every second third and forth beat - scaleThird(); - break; - case THREEHALVES: - // introduce two beats into every gap - scaleTriple(); - // remove every second beat - scaleHalve(); - break; - default: - DEBUG_ASSERT(!"scale value invalid"); - return; - } - onBeatlistChanged(); - locker.unlock(); - emit updated(); -} - -void BeatMap::scaleDouble() { - Beat prevBeat = m_beats.first(); - // Skip the first beat to preserve the first beat in a measure - BeatList::iterator it = m_beats.begin() + 1; - for (; it != m_beats.end(); ++it) { - // Need to not accrue fractional frames. - int distance = it->frame_position() - prevBeat.frame_position(); - Beat beat; - beat.set_frame_position(prevBeat.frame_position() + distance / 2); - it = m_beats.insert(it, beat); - prevBeat = (++it)[0]; - } -} - -void BeatMap::scaleTriple() { - Beat prevBeat = m_beats.first(); - // Skip the first beat to preserve the first beat in a measure - BeatList::iterator it = m_beats.begin() + 1; - for (; it != m_beats.end(); ++it) { - // Need to not accrue fractional frames. - int distance = it->frame_position() - prevBeat.frame_position(); - Beat beat; - beat.set_frame_position(prevBeat.frame_position() + distance / 3); - it = m_beats.insert(it, beat); - ++it; - beat.set_frame_position(prevBeat.frame_position() + distance * 2 / 3); - it = m_beats.insert(it, beat); - prevBeat = (++it)[0]; - } -} - -void BeatMap::scaleQuadruple() { - Beat prevBeat = m_beats.first(); - // Skip the first beat to preserve the first beat in a measure - BeatList::iterator it = m_beats.begin() + 1; - for (; it != m_beats.end(); ++it) { - // Need to not accrue fractional frames. - int distance = it->frame_position() - prevBeat.frame_position(); - Beat beat; - for (int i = 1; i <= 3; i++) { - beat.set_frame_position(prevBeat.frame_position() + distance * i / 4); - it = m_beats.insert(it, beat); - ++it; - } - prevBeat = it[0]; - } -} - -void BeatMap::scaleHalve() { - // Skip the first beat to preserve the first beat in a measure - BeatList::iterator it = m_beats.begin() + 1; - for (; it != m_beats.end(); ++it) { - it = m_beats.erase(it); - if (it == m_beats.end()) { - break; - } - } -} - -void BeatMap::scaleThird() { - // Skip the first beat to preserve the first beat in a measure - BeatList::iterator it = m_beats.begin() + 1; - for (; it != m_beats.end(); ++it) { - it = m_beats.erase(it); - if (it == m_beats.end()) { - break; - } - it = m_beats.erase(it); - if (it == m_beats.end()) { - break; - } - } -} - -void BeatMap::scaleFourth() { - // Skip the first beat to preserve the first beat in a measure - BeatList::iterator it = m_beats.begin() + 1; - for (; it != m_beats.end(); ++it) { - it = m_beats.erase(it); - if (it == m_beats.end()) { - break; - } - it = m_beats.erase(it); - if (it == m_beats.end()) { - break; - } - it = m_beats.erase(it); - if (it == m_beats.end()) { - break; - } - } -} - -void BeatMap::setBpm(double dBpm) { - Q_UNUSED(dBpm); - DEBUG_ASSERT(!"BeatMap::setBpm() not implemented"); - return; - - /* - * One of the problems of beattracking algorithms is the so called "octave error" - * that is, calculated bpm is a power-of-two fraction of the bpm of the track. - * But there is more. In an experiment, it had been proved that roughly 30% of the humans - * fail to guess the correct bpm of a track by usually reporting it as the double or one - * half of the correct one. - * We can interpret it in two ways: - * On one hand, a beattracking algorithm which totally avoid the octave error does not yet exists. - * On the other hand, even if the algorithm guesses the correct bpm, - * 30% of the users will perceive a different bpm and likely change it. - * In this case, we assume that calculated beat markers are correctly placed. All - * that we have to do is to delete or add some beat markers, while leaving others - * so that the number of the beat markers per minute matches the new bpm. - * We are jealous of our well-guessed beats since they belong to a time-expensive analysis. - * When requested we simply turn them off instead of deleting them, so that they can be recollected. - * If the new provided bpm is not a power-of-two fraction, we assume that the algorithm failed - * at all to guess the bpm. I have no idea on how to deal with this. - * If we assume that bpm does not change along the track, i.e. if we use - * fixed tempo approximation (see analyzerbeat.*), this should coincide with the - * method in beatgrid.cpp. - * - * - vittorio. - */ -} - -void BeatMap::onBeatlistChanged() { - if (!isValid()) { - m_dLastFrame = 0; - m_dCachedBpm = 0; - return; - } - m_dLastFrame = m_beats.last().frame_position(); - Beat startBeat = m_beats.first(); - Beat stopBeat = m_beats.last(); - m_dCachedBpm = calculateBpm(startBeat, stopBeat); -} - -double BeatMap::calculateBpm(const Beat& startBeat, const Beat& stopBeat) const { - if (startBeat.frame_position() > stopBeat.frame_position()) { - return -1; - } - - BeatList::const_iterator curBeat = - std::lower_bound(m_beats.constBegin(), m_beats.constEnd(), startBeat, BeatLessThan); - - BeatList::const_iterator lastBeat = - std::upper_bound(m_beats.constBegin(), m_beats.constEnd(), stopBeat, BeatLessThan); - - QVector beatvect; - for (; curBeat != lastBeat; ++curBeat) { - const Beat& beat = *curBeat; - if (beat.enabled()) { - beatvect.append(beat.frame_position()); - } - } - - if (beatvect.isEmpty()) { - return -1; - } - - return BeatUtils::calculateBpm(beatvect, m_iSampleRate, 0, 9999); -} - -} // namespace mixxx diff --git a/src/track/beatmap.h b/src/track/beatmap.h deleted file mode 100644 index 5d1ce7df572..00000000000 --- a/src/track/beatmap.h +++ /dev/null @@ -1,115 +0,0 @@ -/* - * beatmap.h - * - * Created on: 08/dic/2011 - * Author: vittorio - */ - -#ifndef BEATMAP_H_ -#define BEATMAP_H_ - -#include - -#include "track/track.h" -#include "track/beats.h" -#include "proto/beats.pb.h" - -#define BEAT_MAP_VERSION "BeatMap-1.0" - -typedef QList BeatList; - -namespace mixxx { - -class BeatMap final : public Beats { - public: - // Construct a BeatMap. iSampleRate may be provided if a more accurate - // sample rate is known than the one associated with the Track. - BeatMap(const Track& track, SINT iSampleRate); - // Construct a BeatMap. iSampleRate may be provided if a more accurate - // sample rate is known than the one associated with the Track. If it is - // zero then the track's sample rate will be used. The BeatMap will be - // deserialized from the byte array. - BeatMap(const Track& track, SINT iSampleRate, - const QByteArray& byteArray); - // Construct a BeatMap. iSampleRate may be provided if a more accurate - // sample rate is known than the one associated with the Track. If it is - // zero then the track's sample rate will be used. A list of beat locations - // in audio frames may be provided. - BeatMap(const Track& track, SINT iSampleRate, - const QVector& beats); - - ~BeatMap() override = default; - - // See method comments in beats.h - - Beats::CapabilitiesFlags getCapabilities() const override { - return BEATSCAP_TRANSLATE | BEATSCAP_SCALE | BEATSCAP_ADDREMOVE | - BEATSCAP_MOVEBEAT; - } - - QByteArray toByteArray() const override; - BeatsPointer clone() const override; - QString getVersion() const override; - QString getSubVersion() const override; - virtual void setSubVersion(QString subVersion); - - //////////////////////////////////////////////////////////////////////////// - // Beat calculations - //////////////////////////////////////////////////////////////////////////// - - double findNextBeat(double dSamples) const override; - double findPrevBeat(double dSamples) const override; - bool findPrevNextBeats(double dSamples, - double* dpPrevBeatSamples, - double* dpNextBeatSamples) const override; - double findClosestBeat(double dSamples) const override; - double findNthBeat(double dSamples, int n) const override; - std::unique_ptr findBeats(double startSample, double stopSample) const override; - bool hasBeatInRange(double startSample, double stopSample) const override; - - double getBpm() const override; - double getBpmRange(double startSample, double stopSample) const override; - double getBpmAroundPosition(double curSample, int n) const override; - - //////////////////////////////////////////////////////////////////////////// - // Beat mutations - //////////////////////////////////////////////////////////////////////////// - - void addBeat(double dBeatSample) override; - void removeBeat(double dBeatSample) override; - void translate(double dNumSamples) override; - void scale(enum BPMScale scale) override; - void setBpm(double dBpm) override; - - SINT getSampleRate() const override { - return m_iSampleRate; - } - - private: - BeatMap(const BeatMap& other); - bool readByteArray(const QByteArray& byteArray); - void createFromBeatVector(const QVector& beats); - void onBeatlistChanged(); - - double calculateBpm(const mixxx::track::io::Beat& startBeat, - const mixxx::track::io::Beat& stopBeat) const; - // For internal use only. - bool isValid() const; - - void scaleDouble(); - void scaleTriple(); - void scaleQuadruple(); - void scaleHalve(); - void scaleThird(); - void scaleFourth(); - - mutable QMutex m_mutex; - QString m_subVersion; - SINT m_iSampleRate; - double m_dCachedBpm; - double m_dLastFrame; - BeatList m_beats; -}; - -} // namespace mixxx -#endif /* BEATMAP_H_ */ diff --git a/src/track/beats.cpp b/src/track/beats.cpp index 307f0a6b9ab..a30ebad0648 100644 --- a/src/track/beats.cpp +++ b/src/track/beats.cpp @@ -1,29 +1,574 @@ - #include "track/beats.h" +#include "track/beatutils.h" +#include "track/track.h" + namespace mixxx { -int Beats::numBeatsInRange(double dStartSample, double dEndSample) { - double dLastCountedBeat = 0.0; +const QString BeatsInternal::BEAT_MAP_VERSION = "BeatMap-1.0"; +const QString BeatsInternal::BEAT_GRID_1_VERSION = "BeatGrid-1.0"; +const QString BeatsInternal::BEAT_GRID_2_VERSION = "BeatGrid-2.0"; +const QString BeatsInternal::BEATS_VERSION = "Beats-1.0"; + +namespace { +inline bool BeatLessThan(const track::io::Beat& beat1, const track::io::Beat& beat2) { + return beat1.frame_position() < beat2.frame_position(); +} +inline bool TimeSignatureMarkerLessThan( + const track::io::TimeSignatureMarker& marker1, + const track::io::TimeSignatureMarker& marker2) { + return marker1.beat_index() < marker2.beat_index(); +} +constexpr int kSecondsPerMinute = 60; +constexpr double kBeatVicinityFactor = 0.1; + +inline FrameDiff_t getBeatLengthFrames(Bpm bpm, double sampleRate) { + return kSecondsPerMinute * sampleRate / bpm.getValue(); +} +} // namespace + +Beats::Beats(const Track* track, + const QVector& beats, + const QVector& timeSignatureMarkers, + const QVector& phraseMarkers, + const QVector& sectionMarkers) + : Beats(track) { + if (beats.size() > 0) { + // This causes BeatsInternal constructor to be called twice. + // But it can't be included in ctor initializer list since + // we already have a delegating constructor. + m_beatsInternal = BeatsInternal( + beats, timeSignatureMarkers, phraseMarkers, sectionMarkers); + } + slotTrackBeatsUpdated(); +} + +Beats::Beats(const Track* track, const QByteArray& byteArray) + : Beats(track) { + m_beatsInternal = BeatsInternal(byteArray); + slotTrackBeatsUpdated(); +} + +Beats::Beats(const Track* track) + : m_mutex(QMutex::Recursive), m_track(track) { + // BeatMap should live in the same thread as the track it is associated + // with. + slotTrackBeatsUpdated(); + connect(m_track, &Track::beatsUpdated, this, &Beats::slotTrackBeatsUpdated); + connect(m_track, &Track::changed, this, &Beats::slotTrackBeatsUpdated); + moveToThread(track->thread()); +} + +Beats::Beats(const Beats& other) + : Beats(other.m_track) { + m_beatsInternal = other.m_beatsInternal; +} + +int Beats::numBeatsInRange(FramePos startFrame, FramePos endFrame) { + return m_beatsInternal.numBeatsInRange(startFrame, endFrame); +}; + +QByteArray Beats::toProtobuf() const { + QMutexLocker locker(&m_mutex); + return m_beatsInternal.toProtobuf(); +} + +BeatsPointer Beats::clone() const { + QMutexLocker locker(&m_mutex); + BeatsPointer other(new Beats(*this)); + return other; +} + +QString Beats::getVersion() const { + return m_beatsInternal.getVersion(); +} + +QString Beats::getSubVersion() const { + return m_beatsInternal.getSubVersion(); +} + +FramePos Beats::findNextBeat(FramePos frame) const { + return m_beatsInternal.findNextBeat(frame); +} + +void Beats::setSubVersion(const QString& subVersion) { + m_beatsInternal.setSubVersion(subVersion); +} + +void Beats::setGrid(Bpm bpm, FramePos firstBeatFrame) { + QMutexLocker lock(&m_mutex); + m_beatsInternal.setGrid(bpm, firstBeatFrame); +} + +FramePos Beats::findNBeatsFromFrame(FramePos fromFrame, double beats) const { + return m_beatsInternal.findNBeatsFromFrame(fromFrame, beats); +}; + +bool Beats::isValid() const { + return m_beatsInternal.isValid(); +} + +Bpm Beats::calculateBpm(const track::io::Beat& startBeat, + const track::io::Beat& stopBeat) const { + return m_beatsInternal.calculateBpm(startBeat, stopBeat); +} + +FramePos Beats::findPrevBeat(FramePos frame) const { + return m_beatsInternal.findPrevBeat(frame); +} + +bool Beats::findPrevNextBeats(FramePos frame, + FramePos* pPrevBeatFrame, + FramePos* pNextBeatFrame) const { + QMutexLocker locker(&m_mutex); + return m_beatsInternal.findPrevNextBeats( + frame, pPrevBeatFrame, pNextBeatFrame); +} + +FramePos Beats::findClosestBeat(FramePos frame) const { + QMutexLocker locker(&m_mutex); + return m_beatsInternal.findClosestBeat(frame); +} + +FramePos Beats::findNthBeat(FramePos frame, int n) const { + QMutexLocker locker(&m_mutex); + return m_beatsInternal.findNthBeat(frame, n); +} + +std::unique_ptr Beats::findBeats( + FramePos startFrame, FramePos stopFrame) const { + QMutexLocker locker(&m_mutex); + return m_beatsInternal.findBeats(startFrame, stopFrame); +} + +bool Beats::hasBeatInRange(FramePos startFrame, FramePos stopFrame) const { + QMutexLocker locker(&m_mutex); + return m_beatsInternal.hasBeatInRange(startFrame, stopFrame); +} + +Bpm Beats::getBpm() const { + QMutexLocker locker(&m_mutex); + return m_beatsInternal.getBpm(); +} + +double Beats::getBpmRange(FramePos startFrame, FramePos stopFrame) const { + QMutexLocker locker(&m_mutex); + return m_beatsInternal.getBpmRange(startFrame, stopFrame); +} + +Bpm Beats::getBpmAroundPosition(FramePos curFrame, int n) const { + QMutexLocker locker(&m_mutex); + return m_beatsInternal.getBpmAroundPosition(curFrame, n); +} + +TimeSignature Beats::getSignature(int beatIndex) const { + QMutexLocker locker(&m_mutex); + return m_beatsInternal.getSignature(beatIndex); +} + +void Beats::setSignature(TimeSignature sig, int beatIndex) { + QMutexLocker locker(&m_mutex); + m_beatsInternal.setSignature(sig, beatIndex); + locker.unlock(); + emit(updated()); +} + +void Beats::translate(FrameDiff_t numFrames) { + QMutexLocker locker(&m_mutex); + m_beatsInternal.translate(numFrames); + locker.unlock(); + emit updated(); +} + +void Beats::scale(enum BeatsInternal::BPMScale scale) { + QMutexLocker locker(&m_mutex); + m_beatsInternal.scale(scale); + locker.unlock(); + emit updated(); +} + +void Beats::setBpm(Bpm bpm) { + m_beatsInternal.setBpm(bpm); +} + +FramePos Beats::getFirstBeatPosition() const { + return m_beatsInternal.getFirstBeatPosition(); +} + +FramePos Beats::getLastBeatPosition() const { + return m_beatsInternal.getLastBeatPosition(); +} + +SINT Beats::getSampleRate() const { + return m_track->getSampleRate(); +} + +QDebug operator<<(QDebug dbg, const BeatsPointer& arg) { + dbg << arg->m_beatsInternal; + return dbg; +} + +QDebug operator<<(QDebug dbg, const BeatsInternal& arg) { + QVector beatFramePositions; + for (const auto& beat : arg.m_beats) { + beatFramePositions.append(FramePos(beat.frame_position())); + } + dbg << "[" + << "Cached BPM:" << arg.m_bpm << "|" + << "Number of beats:" << arg.m_beats.size() << "|" + << "Beats:" << beatFramePositions << "]"; + return dbg; +} +FramePos BeatsInternal::findNthBeat(FramePos frame, int n) const { + if (!isValid() || n == 0) { + return kInvalidFramePos; + } + + track::io::Beat beat; + beat.set_frame_position(frame.getValue()); + + // it points at the first occurrence of beat or the next largest beat + BeatList::const_iterator it = std::lower_bound( + m_beats.cbegin(), m_beats.cend(), beat, BeatLessThan); + + // If the position is within 1/10th of the average beat length, + // pretend we are on that beat. + const double kFrameEpsilon = + kBeatVicinityFactor * getBeatLengthFrames(getBpm(), m_iSampleRate); + + // Back-up by one. + if (it != m_beats.begin()) { + --it; + } + + // Scan forward to find whether we are on a beat. + BeatList::const_iterator on_beat = m_beats.cend(); + BeatList::const_iterator previous_beat = m_beats.cend(); + BeatList::const_iterator next_beat = m_beats.cend(); + for (; it != m_beats.end(); ++it) { + qint32 delta = it->frame_position() - beat.frame_position(); + + // We are "on" this beat. + if (abs(delta) < kFrameEpsilon) { + on_beat = it; + break; + } + + if (delta < 0) { + // If we are not on the beat and delta < 0 then this beat comes + // before our current position. + previous_beat = it; + } else { + // If we are past the beat and we aren't on it then this beat comes + // after our current position. + next_beat = it; + // Stop because we have everything we need now. + break; + } + } + + // If we are within epsilon frames of a beat then the immediately next and + // previous beats are the beat we are on. + if (on_beat != m_beats.end()) { + next_beat = on_beat; + previous_beat = on_beat; + } + + if (n > 0) { + for (; next_beat != m_beats.end(); ++next_beat) { + if (!next_beat->enabled()) { + continue; + } + if (n == 1) { + // Return a sample offset + return FramePos(next_beat->frame_position()); + } + --n; + } + } else if (n < 0 && previous_beat != m_beats.end()) { + for (; true; --previous_beat) { + if (previous_beat->enabled()) { + if (n == -1) { + // Return a sample offset + return FramePos(previous_beat->frame_position()); + } + ++n; + } + + // Don't step before the start of the list. + if (previous_beat == m_beats.begin()) { + break; + } + } + } + return kInvalidFramePos; +} +Bpm BeatsInternal::getBpm() const { + if (!isValid()) { + return Bpm(); + } + return m_bpm; +} +bool BeatsInternal::isValid() const { + return m_iSampleRate > 0 && !m_beats.empty(); +} +void BeatsInternal::setSampleRate(int sampleRate) { + m_iSampleRate = sampleRate; + updateBpm(); +} +BeatsInternal::BeatsInternal() + : m_iSampleRate(0), m_dDurationSeconds(0) { +} + +BeatsInternal::BeatsInternal(const QByteArray& byteArray) { + track::io::Beats beatsProto; + if (!beatsProto.ParseFromArray(byteArray.constData(), byteArray.size())) { + qDebug() << "ERROR: Could not parse Beats from QByteArray of size" + << byteArray.size(); + } + for (int i = 0; i < beatsProto.beat_size(); ++i) { + const track::io::Beat& beat = beatsProto.beat(i); + m_beats.append(beat); + } + + VERIFY_OR_DEBUG_ASSERT(beatsProto.time_signature_markers_size() > 0) { + // This marker will get the default values from the protobuf definitions, + // beatIndex = 0 and timeSignature = 4/4. + track::io::TimeSignatureMarker generatedTimeSignatureMarker; + beatsProto.add_time_signature_markers()->CopyFrom( + generatedTimeSignatureMarker); + } + for (int i = 0; i < beatsProto.time_signature_markers_size(); ++i) { + const track::io::TimeSignatureMarker& timeSignatureMarker = + beatsProto.time_signature_markers(i); + m_timeSignatureMarkers.append(timeSignatureMarker); + } + updateBpm(); +} + +BeatsInternal::BeatsInternal(const QVector& beats, + const QVector& timeSignatureMarkers, + const QVector& phraseMarkers, + const QVector& sectionMarkers) { + Q_UNUSED(phraseMarkers); + Q_UNUSED(sectionMarkers); + FramePos previousBeatPos = kInvalidFramePos; + track::io::Beat protoBeat; + + for (const auto& beat : beats) { + VERIFY_OR_DEBUG_ASSERT(beat > previousBeatPos && beat >= FramePos(0)) { + qDebug() << "Beats not in increasing order or negative, discarding beat" << beat; + } + else { + protoBeat.set_frame_position(beat.getValue()); + m_beats.append(protoBeat); + previousBeatPos = beat; + } + } + + VERIFY_OR_DEBUG_ASSERT(timeSignatureMarkers.size() > 0) { + // If the analyzer does not send time signature information, just assume 4/4 + // for the whole track and the first beat as downbeat. + setSignature(TimeSignature(), 0); + } + else { + m_timeSignatureMarkers = timeSignatureMarkers; + } + + updateBpm(); +} + +void Beats::slotTrackBeatsUpdated() { + m_beatsInternal.setSampleRate(m_track->getSampleRate()); + m_beatsInternal.setDurationSeconds(m_track->getDuration()); +} + +int BeatsInternal::numBeatsInRange( + FramePos startFrame, FramePos endFrame) const { + FramePos lastCountedBeat(0.0); int iBeatsCounter; - for (iBeatsCounter = 1; dLastCountedBeat < dEndSample; iBeatsCounter++) { - dLastCountedBeat = findNthBeat(dStartSample, iBeatsCounter); - if (dLastCountedBeat == -1) { + for (iBeatsCounter = 1; lastCountedBeat < endFrame; iBeatsCounter++) { + lastCountedBeat = findNthBeat(startFrame, iBeatsCounter); + if (lastCountedBeat == kInvalidFramePos) { break; } } return iBeatsCounter - 2; -}; +} + +QByteArray BeatsInternal::toProtobuf() const { + // No guarantees BeatLists are made of a data type which located adjacent + // items in adjacent memory locations. + track::io::Beats beatsProto; + + for (const auto& beat : m_beats) { + beatsProto.add_beat()->CopyFrom(beat); + } + + for (const auto& timeSignatureMarker : m_timeSignatureMarkers) { + beatsProto.add_time_signature_markers()->CopyFrom( + timeSignatureMarker); + } + + std::string output; + beatsProto.SerializeToString(&output); + return QByteArray(output.data(), output.length()); +} + +void BeatsInternal::setSubVersion(const QString& subVersion) { + m_subVersion = subVersion; +} +QString BeatsInternal::getVersion() const { + return BEATS_VERSION; +} +QString BeatsInternal::getSubVersion() const { + return m_subVersion; +} +void BeatsInternal::scale(BeatsInternal::BPMScale scale) { + if (!isValid()) { + return; + } + + switch (scale) { + case DOUBLE: + // introduce a new beat into every gap + scaleDouble(); + break; + case HALVE: + // remove every second beat + scaleHalve(); + break; + case TWOTHIRDS: + // introduce a new beat into every gap + scaleDouble(); + // remove every second and third beat + scaleThird(); + break; + case THREEFOURTHS: + // introduce two beats into every gap + scaleTriple(); + // remove every second third and forth beat + scaleFourth(); + break; + case FOURTHIRDS: + // introduce three beats into every gap + scaleQuadruple(); + // remove every second third and forth beat + scaleThird(); + break; + case THREEHALVES: + // introduce two beats into every gap + scaleTriple(); + // remove every second beat + scaleHalve(); + break; + default: + DEBUG_ASSERT(!"scale value invalid"); + return; + } + updateBpm(); +} + +void BeatsInternal::scaleDouble() { + scaleMultiple(2); +} -double Beats::findNBeatsFromSample(double fromSample, double beats) const { - double nthBeat; - double prevBeat; - double nextBeat; +void BeatsInternal::scaleTriple() { + scaleMultiple(3); +} - if (!findPrevNextBeats(fromSample, &prevBeat, &nextBeat)) { - return fromSample; +void BeatsInternal::scaleQuadruple() { + scaleMultiple(4); +} + +void BeatsInternal::scaleHalve() { + scaleFraction(2); +} + +void BeatsInternal::scaleThird() { + scaleFraction(3); +} + +void BeatsInternal::scaleFourth() { + scaleFraction(4); +} + +void BeatsInternal::scaleMultiple(uint multiple) { + track::io::Beat prevBeat = m_beats.first(); + // Skip the first beat to preserve the first beat in a measure + BeatList::iterator it = m_beats.begin() + 1; + for (; it != m_beats.end(); ++it) { + // Need to not accrue fractional frames. + int distance = it->frame_position() - prevBeat.frame_position(); + track::io::Beat beat; + for (uint i = 1; i <= multiple - 1; i++) { + beat.set_frame_position( + prevBeat.frame_position() + distance * i / multiple); + it = m_beats.insert(it, beat); + ++it; + } + prevBeat = it[0]; + } +} + +void BeatsInternal::scaleFraction(uint fraction) { + // Skip the first beat to preserve the first beat in a measure + BeatList::iterator it = m_beats.begin() + 1; + for (; it != m_beats.end(); ++it) { + for (uint i = 1; i <= fraction - 1; i++) { + it = m_beats.erase(it); + if (it == m_beats.end()) { + return; + } + } + } +} + +void BeatsInternal::updateBpm() { + if (!isValid()) { + m_bpm = Bpm(); + return; + } + track::io::Beat startBeat = m_beats.first(); + track::io::Beat stopBeat = m_beats.last(); + m_bpm = calculateBpm(startBeat, stopBeat); +} +Bpm BeatsInternal::calculateBpm(const track::io::Beat& startBeat, + const track::io::Beat& stopBeat) const { + if (startBeat.frame_position() > stopBeat.frame_position()) { + return Bpm(); + } + + BeatList::const_iterator curBeat = std::lower_bound( + m_beats.cbegin(), m_beats.cend(), startBeat, BeatLessThan); + + BeatList::const_iterator lastBeat = std::upper_bound( + m_beats.cbegin(), m_beats.cend(), stopBeat, BeatLessThan); + + QVector beatvect; + for (; curBeat != lastBeat; ++curBeat) { + const track::io::Beat& beat = *curBeat; + if (beat.enabled()) { + beatvect.append(beat.frame_position()); + } + } + + if (beatvect.isEmpty()) { + return Bpm(); } - double fromFractionBeats = (fromSample - prevBeat) / (nextBeat - prevBeat); + + return BeatUtils::calculateBpm(beatvect, m_iSampleRate, 0, 9999); +} +FramePos BeatsInternal::findNBeatsFromFrame( + FramePos fromFrame, double beats) const { + FramePos nthBeat; + FramePos prevBeat; + FramePos nextBeat; + + if (!findPrevNextBeats(fromFrame, &prevBeat, &nextBeat)) { + return fromFrame; + } + double fromFractionBeats = (fromFrame - prevBeat) / (nextBeat - prevBeat); double beatsFromPrevBeat = fromFractionBeats + beats; int fullBeats = static_cast(beatsFromPrevBeat); @@ -37,20 +582,354 @@ double Beats::findNBeatsFromSample(double fromSample, double beats) const { nthBeat = findNthBeat(prevBeat, fullBeats - 1); } - if (nthBeat == -1) { - return fromSample; + if (nthBeat == kInvalidFramePos) { + return fromFrame; } // Add the fraction of the beat if (fractionBeats != 0) { nextBeat = findNthBeat(nthBeat, 2); - if (nextBeat == -1) { - return fromSample; + if (nextBeat == kInvalidFramePos) { + return fromFrame; } nthBeat += (nextBeat - nthBeat) * fractionBeats; } return nthBeat; -}; +} +bool BeatsInternal::findPrevNextBeats(FramePos frame, + FramePos* pPrevBeatFrame, + FramePos* pNextBeatFrame) const { + if (pPrevBeatFrame == nullptr || pNextBeatFrame == nullptr) { + return false; + } + + if (!isValid()) { + *pPrevBeatFrame = kInvalidFramePos; + *pNextBeatFrame = kInvalidFramePos; + return false; + } + track::io::Beat beat; + beat.set_frame_position(frame.getValue()); + + // it points at the first occurrence of beat or the next largest beat + BeatList::const_iterator it = std::lower_bound( + m_beats.cbegin(), m_beats.cend(), beat, BeatLessThan); + + // If the position is within 1/10th of the average beat length, + // pretend we are on that beat. + const double kFrameEpsilon = + kBeatVicinityFactor * getBeatLengthFrames(getBpm(), m_iSampleRate); + + // Back-up by one. + if (it != m_beats.begin()) { + --it; + } + + // Scan forward to find whether we are on a beat. + BeatList::const_iterator on_beat = m_beats.cend(); + BeatList::const_iterator previous_beat = m_beats.cend(); + BeatList::const_iterator next_beat = m_beats.cend(); + for (; it != m_beats.end(); ++it) { + qint32 delta = it->frame_position() - beat.frame_position(); + + // We are "on" this beat. + if (abs(delta) < kFrameEpsilon) { + on_beat = it; + break; + } + + if (delta < 0) { + // If we are not on the beat and delta < 0 then this beat comes + // before our current position. + previous_beat = it; + } else { + // If we are past the beat and we aren't on it then this beat comes + // after our current position. + next_beat = it; + // Stop because we have everything we need now. + break; + } + } + + // If we are within epsilon samples of a beat then the immediately next and + // previous beats are the beat we are on. + if (on_beat != m_beats.end()) { + previous_beat = on_beat; + next_beat = on_beat + 1; + } + + *pPrevBeatFrame = kInvalidFramePos; + *pNextBeatFrame = kInvalidFramePos; + + for (; next_beat != m_beats.end(); ++next_beat) { + if (!next_beat->enabled()) { + continue; + } + pNextBeatFrame->setValue(next_beat->frame_position()); + break; + } + if (previous_beat != m_beats.end()) { + for (; true; --previous_beat) { + if (previous_beat->enabled()) { + pPrevBeatFrame->setValue(previous_beat->frame_position()); + break; + } + + // Don't step before the start of the list. + if (previous_beat == m_beats.begin()) { + break; + } + } + } + return *pPrevBeatFrame != kInvalidFramePos && + *pNextBeatFrame != kInvalidFramePos; +} +void BeatsInternal::setGrid(Bpm dBpm, FramePos firstBeatFrame) { + FramePos trackLastFrame = FramePos(m_dDurationSeconds * m_iSampleRate); + m_beats.clear(); + track::io::Beat beat; + beat.set_frame_position(firstBeatFrame.getValue()); + for (FramePos frame = firstBeatFrame; frame <= trackLastFrame; + frame += getBeatLengthFrames(dBpm, m_iSampleRate)) { + beat.set_frame_position(frame.getValue()); + m_beats.push_back(beat); + } + + updateBpm(); +} +FramePos BeatsInternal::findClosestBeat(FramePos frame) const { + if (!isValid()) { + return kInvalidFramePos; + } + FramePos prevBeat; + FramePos nextBeat; + findPrevNextBeats(frame, &prevBeat, &nextBeat); + if (prevBeat == kInvalidFramePos) { + // If both values are invalid, we correctly return kInvalidFramePos. + return nextBeat; + } else if (nextBeat == kInvalidFramePos) { + return prevBeat; + } + return (nextBeat - frame > frame - prevBeat) ? prevBeat : nextBeat; +} + +std::unique_ptr BeatsInternal::findBeats( + FramePos startFrame, FramePos stopFrame) const { + if (!isValid() || startFrame > stopFrame) { + return std::unique_ptr(); + } + + track::io::Beat startBeat, stopBeat; + startBeat.set_frame_position(startFrame.getValue()); + stopBeat.set_frame_position(stopFrame.getValue()); + + BeatList::const_iterator firstBeat = std::lower_bound( + m_beats.cbegin(), m_beats.cend(), startBeat, BeatLessThan); + + BeatList::const_iterator lastBeat = std::upper_bound( + m_beats.cbegin(), m_beats.cend(), stopBeat, BeatLessThan); + if (lastBeat >= m_beats.cbegin()) { + lastBeat = m_beats.cend() - 1; + } + + if (firstBeat >= lastBeat) { + return std::unique_ptr(); + } + return std::make_unique(firstBeat, lastBeat + 1); +} +bool BeatsInternal::hasBeatInRange( + FramePos startFrame, FramePos stopFrame) const { + if (!isValid() || startFrame > stopFrame) { + return false; + } + FramePos curBeat = findNextBeat(startFrame); + if (curBeat <= stopFrame) { + return true; + } + return false; +} +FramePos BeatsInternal::findNextBeat(FramePos frame) const { + return findNthBeat(frame, 1); +} +FramePos BeatsInternal::findPrevBeat(FramePos frame) const { + return findNthBeat(frame, -1); +} +double BeatsInternal::getBpmRange( + FramePos startFrame, FramePos stopFrame) const { + if (!isValid()) { + return -1; + } + track::io::Beat startBeat, stopBeat; + startBeat.set_frame_position(startFrame.getValue()); + stopBeat.set_frame_position(stopFrame.getValue()); + return calculateBpm(startBeat, stopBeat).getValue(); +} +Bpm BeatsInternal::getBpmAroundPosition(FramePos curFrame, int n) const { + if (!isValid()) { + return Bpm(); + } + + // To make sure we are always counting n beats, iterate backward to the + // lower bound, then iterate forward from there to the upper bound. + // kInvalidFramePos indicates we went off the map -- count from the beginning. + FramePos lower_bound = findNthBeat(curFrame, -n); + if (lower_bound == kInvalidFramePos) { + lower_bound = FramePos(m_beats.first().frame_position()); + } + + // If we hit the end of the beat map, recalculate the lower bound. + FramePos upper_bound = findNthBeat(lower_bound, n * 2); + if (upper_bound == kInvalidFramePos) { + upper_bound = FramePos(m_beats.last().frame_position()); + lower_bound = findNthBeat(upper_bound, n * -2); + // Super edge-case -- the track doesn't have n beats! Do the best + // we can. + if (lower_bound == kInvalidFramePos) { + lower_bound = FramePos(m_beats.first().frame_position()); + } + } + + // TODO(JVC) We are extracting frame numbers to then construct beats. + // Then in calculateBpm we are using the frame position to find + // the beats to use them to calculate. Seems inefficient + // Will not make more sense to extract the Beats straight? + // We can use getBpmRange and move the logic of calculateBpm there + track::io::Beat startBeat, stopBeat; + startBeat.set_frame_position(lower_bound.getValue()); + stopBeat.set_frame_position(upper_bound.getValue()); + return calculateBpm(startBeat, stopBeat); +} + +TimeSignature BeatsInternal::getSignature(int beatIndex) const { + if (!isValid()) { + return kNullTimeSignature; + } + + if (beatIndex == -1) { + return kNullTimeSignature; + } + + // Find the time signature marker just before (or on) this frame + // TODO: This is linear search. Optimize to binary search. + TimeSignature timeSignature; + for (const auto& timeSignatureMarker : m_timeSignatureMarkers) { + if (timeSignatureMarker.beat_index() <= beatIndex) { + timeSignature = TimeSignature(timeSignatureMarker.time_signature()); + } else { + break; + } + } + return timeSignature; +} + +void BeatsInternal::setSignature( + const TimeSignature& signature, int beatIndex) { + // TODO(hacksdump): Handle note values other than 1/4. + if (!isValid()) { + return; + } + + // Signature can only be set on a downbeat. Check if beatIndex falls on a downbeat. + // Allow normal flow if there are no time signature markers before beatIndex. + if (!isDownbeat(beatIndex) && hasTimeSignatureMarkerBefore(beatIndex)) { + return; + } + track::io::TimeSignatureMarker markerToInsert; + markerToInsert.set_beat_index(beatIndex); + markerToInsert.mutable_time_signature()->set_beats_per_bar( + signature.getBeatsPerBar()); + markerToInsert.mutable_time_signature()->set_note_value( + signature.getNoteValue()); + auto prevTimeSignatureMarker = + std::lower_bound(m_timeSignatureMarkers.begin(), + m_timeSignatureMarkers.end(), + markerToInsert, + TimeSignatureMarkerLessThan); + m_timeSignatureMarkers.insert(prevTimeSignatureMarker, markerToInsert); +} + +bool BeatsInternal::isDownbeat(int beatIndex) { + TimeSignature timeSignatureBefore; + int timeSignatureBeforeMarkerIndex = 0; + for (const auto& timeSignatureMarker : m_timeSignatureMarkers) { + if (timeSignatureMarker.beat_index() < beatIndex) { + timeSignatureBefore = + TimeSignature(timeSignatureMarker.time_signature()); + timeSignatureBeforeMarkerIndex = timeSignatureMarker.beat_index(); + } else { + break; + } + } + int beatCountFromPreviousTimeSignatureMarker = + beatIndex - timeSignatureBeforeMarkerIndex; + return beatCountFromPreviousTimeSignatureMarker % + timeSignatureBefore.getBeatsPerBar() == + 0; +} + +bool BeatsInternal::hasTimeSignatureMarkerBefore(int beatIndex) { + track::io::TimeSignatureMarker searchBeforeMarker; + searchBeforeMarker.set_beat_index(beatIndex); + auto prevTimeSignatureMarker = + std::lower_bound(m_timeSignatureMarkers.cbegin(), + m_timeSignatureMarkers.cend(), + searchBeforeMarker, + TimeSignatureMarkerLessThan); + return prevTimeSignatureMarker != m_timeSignatureMarkers.cend(); +} + +void BeatsInternal::translate(FrameDiff_t numFrames) { + if (!isValid()) { + return; + } + + for (BeatList::iterator it = m_beats.begin(); it != m_beats.end();) { + FramePos newpos = FramePos(it->frame_position()) + numFrames; + it->set_frame_position(newpos.getValue()); + ++it; + } + updateBpm(); +} + +void BeatsInternal::setBpm(Bpm dBpm) { + // Temporarily creating this adapter to generate beats from a given bpm assuming + // uniform bpm. + // TODO(hacksdump): A check for preferences will be added to only allow setting bpm + // when "Assume Constant Tempo" is checked. + setGrid(dBpm, FramePos(m_beats.first().frame_position())); + + /* + * One of the problems of beattracking algorithms is the so called "octave error" + * that is, calculated bpm is a power-of-two fraction of the bpm of the track. + * But there is more. In an experiment, it had been proved that roughly 30% of the humans + * fail to guess the correct bpm of a track by usually reporting it as the double or one + * half of the correct one. + * We can interpret it in two ways: + * On one hand, a beattracking algorithm which totally avoid the octave error does not yet exists. + * On the other hand, even if the algorithm guesses the correct bpm, + * 30% of the users will perceive a different bpm and likely change it. + * In this case, we assume that calculated beat markers are correctly placed. All + * that we have to do is to delete or add some beat markers, while leaving others + * so that the number of the beat markers per minute matches the new bpm. + * We are jealous of our well-guessed beats since they belong to a time-expensive analysis. + * When requested we simply turn them off instead of deleting them, so that they can be recollected. + * If the new provided bpm is not a power-of-two fraction, we assume that the algorithm failed + * at all to guess the bpm. I have no idea on how to deal with this. + * If we assume that bpm does not change along the track, i.e. if we use + * fixed tempo approximation (see analyzerbeat.*), this should coincide with the + * method in beatgrid.cpp. + * + * - vittorio. + */ +} + +FramePos BeatsInternal::getFirstBeatPosition() const { + return m_beats.empty() ? kInvalidFramePos : FramePos(m_beats.front().frame_position()); +} + +FramePos BeatsInternal::getLastBeatPosition() const { + return m_beats.empty() ? kInvalidFramePos : FramePos(m_beats.back().frame_position()); +} } // namespace mixxx diff --git a/src/track/beats.h b/src/track/beats.h index cdd155e97d3..3e9d61a4873 100644 --- a/src/track/beats.h +++ b/src/track/beats.h @@ -1,49 +1,42 @@ -#ifndef BEATS_H -#define BEATS_H +#pragma once +#include +#include +#include #include #include -#include -#include -#include +#include +#include -#include "util/memory.h" +#include "proto/beats.pb.h" +#include "track/bpm.h" +#include "track/frame.h" #include "util/types.h" -namespace { - double kMaxBpm = 500; -} - namespace mixxx { class Beats; -typedef QSharedPointer BeatsPointer; +class BeatIterator; +using BeatsPointer = std::shared_ptr; +using BeatList = QList; +} // namespace mixxx -class BeatIterator { - public: - virtual ~BeatIterator() = default; - virtual bool hasNext() const = 0; - virtual double next() = 0; -}; +#include "track/beatiterator.h" +#include "track/timesignature.h" -// Beats is a pure abstract base class for BPM and beat management classes. It -// provides a specification of all methods a beat-manager class must provide, as -// well as a capability model for representing optional features. -class Beats : public QObject { - Q_OBJECT - public: - Beats() { } - ~Beats() override = default; +class Track; - enum Capabilities { - BEATSCAP_NONE = 0x0000, - BEATSCAP_ADDREMOVE = 0x0001, // Add or remove a single beat - BEATSCAP_TRANSLATE = 0x0002, // Move all beat markers earlier or later - BEATSCAP_SCALE = 0x0004, // Scale beat distance by a fixed ratio - BEATSCAP_MOVEBEAT = 0x0008, // Move a single Beat - BEATSCAP_SETBPM = 0x0010 // Set new bpm, beat grid only - }; - typedef int CapabilitiesFlags; // Allows us to do ORing +namespace mixxx { +/// This is an intermediate class which encapsulates the beats into a +/// plain copyable, movable object. +class BeatsInternal { + public: + BeatsInternal(); + BeatsInternal(const QByteArray& byteArray); + BeatsInternal(const QVector& beats, + const QVector& timeSignatureMarkers, + const QVector& phraseMarkers, + const QVector& sectionMarkers); enum BPMScale { DOUBLE, @@ -54,124 +47,230 @@ class Beats : public QObject { THREEHALVES, }; - virtual Beats::CapabilitiesFlags getCapabilities() const = 0; - - // Serialization - virtual QByteArray toByteArray() const = 0; - virtual BeatsPointer clone() const = 0; + using iterator = BeatIterator; + + static const QString BEAT_MAP_VERSION; + static const QString BEAT_GRID_1_VERSION; + static const QString BEAT_GRID_2_VERSION; + static const QString BEATS_VERSION; + FramePos findNthBeat(FramePos frame, int offset) const; + FramePos findNextBeat(FramePos frame) const; + FramePos findPrevBeat(FramePos frame) const; + Bpm getBpm() const; + bool isValid() const; + void setSampleRate(int sampleRate); + void setDurationSeconds(double duration) { + m_dDurationSeconds = duration; + } + int numBeatsInRange(FramePos startFrame, FramePos endFrame) const; + QByteArray toProtobuf() const; + QString getVersion() const; + QString getSubVersion() const; + void setSubVersion(const QString& subVersion); + Bpm calculateBpm(const track::io::Beat& startBeat, + const track::io::Beat& stopBeat) const; + void scale(enum BPMScale scale); + FramePos findNBeatsFromFrame(FramePos fromFrame, double beats) const; + bool findPrevNextBeats(FramePos frame, + FramePos* pPrevBeatFrame, + FramePos* pNextBeatFrame) const; + void setGrid(Bpm dBpm, FramePos firstBeatFrame = FramePos()); + FramePos findClosestBeat(FramePos frame) const; + std::unique_ptr findBeats( + FramePos startFrame, FramePos stopFrame) const; + bool hasBeatInRange(FramePos startFrame, FramePos stopFrame) const; + double getBpmRange(FramePos startFrame, FramePos stopFrame) const; + Bpm getBpmAroundPosition(FramePos curFrame, int n) const; + TimeSignature getSignature(int beatIndex) const; + void setSignature(const TimeSignature& signature, int beatIndex); + void translate(FrameDiff_t numFrames); + void setBpm(Bpm bpm); + inline int size() { + return m_beats.size(); + } + FramePos getFirstBeatPosition() const; + FramePos getLastBeatPosition() const; + + private: + void updateBpm(); + void scaleDouble(); + void scaleTriple(); + void scaleQuadruple(); + void scaleHalve(); + void scaleThird(); + void scaleFourth(); + void scaleMultiple(uint multiple); + void scaleFraction(uint fraction); + bool isDownbeat(int beatIndex); + bool hasTimeSignatureMarkerBefore(int beatIndex); + + QString m_subVersion; + Bpm m_bpm; + BeatList m_beats; + QVector m_timeSignatureMarkers; + int m_iSampleRate; + double m_dDurationSeconds; + friend QDebug operator<<(QDebug dbg, const BeatsInternal& arg); +}; - // A string representing the version of the beat-processing code that - // produced this Beats instance. Used by BeatsFactory for associating a - // given serialization with the version that produced it. - virtual QString getVersion() const = 0; - // A sub-version can be used to represent the preferences used to generate - // the beats object. - virtual QString getSubVersion() const = 0; +/// Beats is a class for BPM and beat management classes. +/// It stores beats information including beats position, down beats position, +/// phrase beat position and changes in tempo. +class Beats final : public QObject { + Q_OBJECT + public: + /// Initialize beats with only the track pointer. + explicit Beats(const Track* track); + /// The source of this byte array is the serialized representation of beats + /// generated by the protocol buffer and stored in the database. + Beats(const Track* track, const QByteArray& byteArray); + /// A list of beat locations in audio frames may be provided. + /// The source of this data is the analyzer. + Beats(const Track* track, + const QVector& beats, + const QVector& + timeSignatureMarkers = + QVector(), + const QVector& phraseMarkers = + QVector(), + const QVector& sectionMarkers = + QVector()); + ~Beats() override = default; - //////////////////////////////////////////////////////////////////////////// - // Beat calculations - //////////////////////////////////////////////////////////////////////////// + using iterator = BeatIterator; + + // TODO(JVC) Is a copy constructor needed? of we can force a move logic?? + Beats(const mixxx::Beats& other); + + /// Serializes into a protobuf. + QByteArray toProtobuf() const; + BeatsPointer clone() const; + + /// Returns a string representing the version of the beat-processing code that + /// produced this Beats instance. Used by BeatsFactory for associating a + /// given serialization with the version that produced it. + QString getVersion() const; + /// Return a string that represent the preferences used to generate + /// the beats object. + QString getSubVersion() const; + void setSubVersion(const QString& subVersion); + bool isValid() const; + /// Calculates the BPM between two beat positions. + Bpm calculateBpm(const track::io::Beat& startBeat, + const track::io::Beat& stopBeat) const; + + /// Initializes the BeatGrid to have a BPM of dBpm and the first beat offset + /// of firstBeatFrame. Does not generate an updated() signal, since it is + /// meant for initialization. + void setGrid(Bpm dBpm, FramePos firstBeatFrame = FramePos()); - // TODO: We may want all of these find functions to return an integer - // instead of a double. // TODO: We may want to implement these with common code that returns // the triple of closest, next, and prev. - // Starting from sample dSamples, return the sample of the next beat in the - // track, or -1 if none exists. If dSamples refers to the location of a - // beat, dSamples is returned. - virtual double findNextBeat(double dSamples) const = 0; - - // Starting from sample dSamples, return the sample of the previous beat in - // the track, or -1 if none exists. If dSamples refers to the location of - // beat, dSamples is returned. - virtual double findPrevBeat(double dSamples) const = 0; - - // Starting from sample dSamples, fill the samples of the previous beat - // and next beat. Either can be -1 if none exists. If dSamples refers - // to the location of the beat, the first value is dSamples, and the second - // value is the next beat position. Non- -1 values are guaranteed to be - // even. Returns false if *at least one* sample is -1. (Can return false - // with one beat successfully filled) - virtual bool findPrevNextBeats(double dSamples, - double* dpPrevBeatSamples, - double* dpNextBeatSamples) const = 0; - - // Starting from sample dSamples, return the sample of the closest beat in - // the track, or -1 if none exists. Non- -1 values are guaranteed to be - // even. - virtual double findClosestBeat(double dSamples) const = 0; - - // Find the Nth beat from sample dSamples. Works with both positive and - // negative values of n. Calling findNthBeat with n=0 is invalid. Calling - // findNthBeat with n=1 or n=-1 is equivalent to calling findNextBeat and - // findPrevBeat, respectively. If dSamples refers to the location of a beat, - // then dSamples is returned. If no beat can be found, returns -1. - virtual double findNthBeat(double dSamples, int n) const = 0; - - int numBeatsInRange(double dStartSample, double dEndSample); - - // Find the sample N beats away from dSample. The number of beats may be - // negative and does not need to be an integer. - double findNBeatsFromSample(double fromSample, double beats) const; - - - // Adds to pBeatsList the position in samples of every beat occurring between - // startPosition and endPosition. BeatIterator must be iterated while - // holding a strong references to the Beats object to ensure that the Beats - // object is not deleted. Caller takes ownership of the returned BeatIterator; - virtual std::unique_ptr findBeats(double startSample, double stopSample) const = 0; - - // Return whether or not a sample lies between startPosition and endPosition - virtual bool hasBeatInRange(double startSample, double stopSample) const = 0; - - // Return the average BPM over the entire track if the BPM is - // valid, otherwise returns -1 - virtual double getBpm() const = 0; - - // Return the average BPM over the range from startSample to endSample, - // specified in samples if the BPM is valid, otherwise returns -1 - virtual double getBpmRange(double startSample, double stopSample) const = 0; - - // Return the average BPM over the range of n*2 beats centered around - // curSample. (An n of 4 results in an averaging of 8 beats). Invalid - // BPM returns -1. - virtual double getBpmAroundPosition(double curSample, int n) const = 0; - - virtual double getMaxBpm() const { - return kMaxBpm; + /// Starting from frame, return the frame number of the next beat + /// in the track, or -1 if none exists. If frame refers to the location + /// of a beat, frame is returned. + FramePos findNextBeat(FramePos frame) const; + + /// Starting from frame frame, return the frame number of the previous + /// beat in the track, or -1 if none exists. If frame refers to the + /// location of beat, frame is returned. + FramePos findPrevBeat(FramePos frame) const; + + /// Starting from frame, fill the frame numbers of the previous beat + /// and next beat. Either can be -1 if none exists. If frame refers + /// to the location of the beat, the first value is frame, and the second + /// value is the next beat position. Non- -1 values are guaranteed to be + /// even. Returns false if *at least one* frame is -1. (Can return false + /// with one beat successfully filled) + bool findPrevNextBeats(FramePos frame, + FramePos* pPrevBeatFrame, + FramePos* pNextBeatFrame) const; + + /// Starting from frame, return the frame number of the closest beat + /// in the track, or -1 if none exists. Non- -1 values are guaranteed to be + /// even. + FramePos findClosestBeat(FramePos frame) const; + + /// Find the Nth beat from frame. Works with both positive and + /// negative values of n. If frame refers to the location of a beat, + /// then frame is returned. If no beat can be found, returns -1. + FramePos findNthBeat(FramePos frame, int offset) const; + + int numBeatsInRange(FramePos startFrame, FramePos endFrame); + + /// Find the frame N beats away from frame. The number of beats may be + /// negative and does not need to be an integer. + FramePos findNBeatsFromFrame(FramePos fromFrame, double beats) const; + + /// Return an iterator to a container of Beats containing the Beats + /// between startFrameNum and endFrameNum. THe BeatIterator must be iterated + /// while a strong reference to the Beats object to ensure that the Beats + /// object is not deleted. Caller takes ownership of the returned BeatsIterator + std::unique_ptr findBeats(FramePos startFrame, + FramePos stopFrame) const; + + /// Return whether or not a Beat lies between startFrameNum and endFrameNum + bool hasBeatInRange(FramePos startFrame, + FramePos stopFrame) const; + + /// Return the average BPM over the entire track if the BPM is + /// valid, otherwise returns -1 + Bpm getBpm() const; + + /// Return the average BPM over the range from startFrameNum to endFrameNum, + /// specified in frames if the BPM is valid, otherwise returns -1 + double getBpmRange(FramePos startFrame, + FramePos stopFrame) const; + + /// Return the average BPM over the range of n*2 beats centered around + /// curFrameNum. (An n of 4 results in an averaging of 8 beats). Invalid + /// BPM returns -1. + Bpm getBpmAroundPosition(FramePos curFrame, int n) const; + + /// Sets the track signature at the nearest frame + void setSignature(TimeSignature sig, int beatIndex); + + /// Return the track signature at the given frame position + TimeSignature getSignature(int beatIndex) const; + + /// Sets the nearest beat as a downbeat + + /// Translate all beats in the song by numFrames. Beats that lie + /// before the start of the track or after the end of the track are not + /// removed. + void translate(FrameDiff_t numFrames); + + /// Scale the position of every beat in the song by dScalePercentage. + void scale(enum BeatsInternal::BPMScale scale); + + /// Adjust the beats so the global average BPM matches dBpm. + void setBpm(Bpm bpm); + + /// Returns the number of beats + inline int size() { + return m_beatsInternal.size(); } - //////////////////////////////////////////////////////////////////////////// - // Beat mutations - //////////////////////////////////////////////////////////////////////////// - - // Add a beat at location dBeatSample. Beats instance must have the - // capability BEATSCAP_ADDREMOVE. - virtual void addBeat(double dBeatSample) = 0; + /// Returns the frame number for the first beat, -1 is no beats + FramePos getFirstBeatPosition() const; - // Remove a beat at location dBeatSample. Beats instance must have the - // capability BEATSCAP_ADDREMOVE. - virtual void removeBeat(double dBeatSample) = 0; + /// Returns the frame number for the last beat, -1 if no beats + FramePos getLastBeatPosition() const; - // Translate all beats in the song by dNumSamples samples. Beats that lie - // before the start of the track or after the end of the track are not - // removed. Beats instance must have the capability BEATSCAP_TRANSLATE. - virtual void translate(double dNumSamples) = 0; + /// Return the sample rate + SINT getSampleRate() const; - // Scale the position of every beat in the song by dScalePercentage. Beats - // class must have the capability BEATSCAP_SCALE. - virtual void scale(enum BPMScale scale) = 0; - - // Adjust the beats so the global average BPM matches dBpm. Beats class must - // have the capability BEATSCAP_SET. - virtual void setBpm(double dBpm) = 0; - - virtual SINT getSampleRate() const = 0; + /// Prints debugging information in stderr + friend QDebug operator<<(QDebug dbg, const BeatsPointer& arg); + private slots: + void slotTrackBeatsUpdated(); + private: + mutable QMutex m_mutex; + const Track* m_track; + BeatsInternal m_beatsInternal; signals: void updated(); }; - } // namespace mixxx -#endif /* BEATS_H */ diff --git a/src/track/beatutils.cpp b/src/track/beatutils.cpp index 1eb2a0afd6f..044917a2af8 100644 --- a/src/track/beatutils.cpp +++ b/src/track/beatutils.cpp @@ -82,8 +82,11 @@ double BeatUtils::computeSampleMedian(QList sortedItems) { } QList BeatUtils::computeWindowedBpmsAndFrequencyHistogram( - const QVector beats, const int windowSize, const int windowStep, - const int sampleRate, QMap* frequencyHistogram) { + const QVector& beats, + int windowSize, + int windowStep, + int framesPerSecond, + QMap* frequencyHistogram) { QList averageBpmList; for (int i = windowSize; i < beats.size(); i += windowStep) { //get start and end sample of the beats @@ -91,7 +94,7 @@ QList BeatUtils::computeWindowedBpmsAndFrequencyHistogram( double end_sample = beats.at(i); // Time needed to count a bar (4 beats) - double time = (end_sample - start_sample) / sampleRate; + double time = (end_sample - start_sample) / framesPerSecond; if (time == 0) continue; double localBpm = 60.0 * windowSize / time; @@ -107,10 +110,10 @@ QList BeatUtils::computeWindowedBpmsAndFrequencyHistogram( } double BeatUtils::computeFilteredWeightedAverage( - const QMap frequencyTable, - const double filterCenter, - const double filterTolerance, - QMap* filteredFrequencyTable) { + const QMap& frequencyTable, + double filterCenter, + double filterTolerance, + QMap* filteredFrequencyTable) { double filterWeightedAverage = 0.0; int filterSum = 0; QMapIterator i(frequencyTable); @@ -142,8 +145,9 @@ double BeatUtils::computeFilteredWeightedAverage( return filterWeightedAverage / static_cast(filterSum); } -double BeatUtils::calculateBpm(const QVector& beats, int SampleRate, - int min_bpm, int max_bpm) { +// TODO(JVC) Use Bpm class internally instead of only convert the calculated on return +mixxx::Bpm BeatUtils::calculateBpm( + const QVector& beats, int framesPerSecond, int min_bpm, int max_bpm) { /* * Let's compute the average local * BPM for N subsequent beats. @@ -176,20 +180,28 @@ double BeatUtils::calculateBpm(const QVector& beats, int SampleRate, * beats. We then sort the averages and take the middle to find the median * BPM. */ + if (sDebug) { + qDebug() << "Analysis data(Frame numbers):" << beats; + } if (beats.size() < 2) { - return 0; + return mixxx::Bpm(); } // If we don't have enough beats for our regular approach, just divide the # // of beats by the duration in minutes. if (beats.size() <= N) { - return 60.0 * (beats.size()-1) * SampleRate / (beats.last() - beats.first()); + mixxx::Bpm result(60.0 * (beats.size() - 1) * framesPerSecond / + (beats.last() - beats.first())); + if (sDebug) { + qDebug() << "Simplified calculation. BPM:" << result; + } + return result; } QMap frequency_table; QList average_bpm_list = computeWindowedBpmsAndFrequencyHistogram( - beats, N, 1, SampleRate, &frequency_table); + beats, N, 1, framesPerSecond, &frequency_table); // Get the median BPM. std::sort(average_bpm_list.begin(), average_bpm_list.end()); @@ -207,8 +219,6 @@ double BeatUtils::calculateBpm(const QVector& beats, int SampleRate, * non-electronic music isn't too bad. */ - //qDebug() << "BPM range between " << min_bpm << " and " << max_bpm; - // a subset of the 'frequency_table', where the bpm values are +-1 away from // the median average BPM. QMap filtered_bpm_frequency_table; @@ -249,13 +259,16 @@ double BeatUtils::calculateBpm(const QVector& beats, int SampleRate, double beat_end = beats.at(i); // Time needed to count a bar (N beats) - double time = (beat_end - beat_start) / SampleRate; + double time = (beat_end - beat_start) / framesPerSecond; if (time == 0) continue; double local_bpm = 60.0 * N / time; // round BPM to have two decimal places local_bpm = floor(local_bpm * kHistogramDecimalScale + 0.5) / kHistogramDecimalScale; - //qDebug() << "Local BPM beat " << i << ": " << local_bpm; + if (sDebug) { + qDebug() << "Local BPM beat " << i << ": " << local_bpm; + } + if (!foundFirstCorrectBeat && filtered_bpm_frequency_table.contains(local_bpm) && fabs(local_bpm - filterWeightedAverageBpm) < BPM_ERROR) { @@ -273,7 +286,7 @@ double BeatUtils::calculateBpm(const QVector& beats, int SampleRate, } else { counter += 1; } - double time2 = (beat_end - firstCorrectBeatSample) / SampleRate; + double time2 = (beat_end - firstCorrectBeatSample) / framesPerSecond; double correctedBpm = 60 * counter / time2; if (fabs(correctedBpm - filterWeightedAverageBpm) <= BPM_ERROR) { @@ -309,12 +322,11 @@ double BeatUtils::calculateBpm(const QVector& beats, int SampleRate, qDebug() << "Perform rounding=" << perform_rounding; qDebug() << "Constrained to Range [" << min_bpm << "," << max_bpm << "]=" << constrainedBpm; } - return constrainedBpm; + return mixxx::Bpm(constrainedBpm); } double BeatUtils::calculateOffset( - const QVector beats1, const double bpm1, - const QVector beats2, const int SampleRate) { + const QVector& beats1, double bpm1, const QVector& beats2, int SampleRate) { /* * Here we compare to beats vector and try to determine the best offset * based on the occurrences, i.e. by assuming that the almost correct beats @@ -354,8 +366,9 @@ double BeatUtils::calculateOffset( return floor(bestOffset + beatLength1Epsilon); } -double BeatUtils::findFirstCorrectBeat(const QVector rawbeats, - const int SampleRate, const double global_bpm) { +double BeatUtils::findFirstCorrectBeat(const QVector& rawbeats, + int SampleRate, + double global_bpm) { for (int i = N; i < rawbeats.size(); i++) { // get start and end sample of the beats double start_sample = rawbeats.at(i-N); @@ -384,9 +397,11 @@ double BeatUtils::findFirstCorrectBeat(const QVector rawbeats, // static double BeatUtils::calculateFixedTempoFirstBeat( - bool enableOffsetCorrection, - const QVector rawbeats, const int sampleRate, - const int totalSamples, const double globalBpm) { + bool enableOffsetCorrection, + const QVector& rawbeats, + int sampleRate, + int totalSamples, + double globalBpm) { if (rawbeats.size() == 0) { return 0; } diff --git a/src/track/beatutils.h b/src/track/beatutils.h index 906e5f00fd8..bd2f87cd17f 100644 --- a/src/track/beatutils.h +++ b/src/track/beatutils.h @@ -9,12 +9,15 @@ #include +// Included to get mixxx::kEngineChannelCount +#include "engine/engine.h" +#include "track/bpm.h" + class BeatUtils { public: static void printBeatStatistics(const QVector& beats, int SampleRate); - static double constrainBpm(double bpm, const int min_bpm, - const int max_bpm, bool aboveRange) { + static double constrainBpm(double bpm, int min_bpm, int max_bpm, bool aboveRange) { if (bpm <= 0.0 || min_bpm < 0 || max_bpm < 0 || min_bpm >= max_bpm || (bpm >= min_bpm && bpm <= max_bpm)) { @@ -37,7 +40,6 @@ class BeatUtils { return bpm; } - /* * This method detects the BPM given a set of beat positions. * We compute the average local BPM of by considering 8 beats @@ -45,17 +47,21 @@ class BeatUtils { * from which the statistical median is computed. This value provides * a pretty good guess of the global BPM value. */ - static double calculateBpm(const QVector& beats, int SampleRate, - int min_bpm, int max_bpm); - static double findFirstCorrectBeat(const QVector rawBeats, - const int SampleRate, const double global_bpm); + static mixxx::Bpm calculateBpm(const QVector& beats, + int framesPerSecond, + int min_bpm, + int max_bpm); + static double findFirstCorrectBeat(const QVector& rawBeats, + int SampleRate, + double global_bpm); /* This implement a method to find the best offset so that * the grid generated from bpm is close enough to the one we get from vamp. */ - static double calculateOffset( - const QVector beats1, const double bpm1, - const QVector beats2, const int SampleRate); + static double calculateOffset(const QVector& beats1, + double bpm1, + const QVector& beats2, + int SampleRate); // By default Vamp does not assume a 4/4 signature. This is basically a good // property of Vamp, however, it leads to inaccurate beat grids if a 4/4 @@ -64,21 +70,32 @@ class BeatUtils { // positions, this method calculates the position of the first beat assuming // the beats have a fixed tempo given by globalBpm. static double calculateFixedTempoFirstBeat( - bool enableOffsetCorrection, - const QVector rawbeats, const int sampleRate, - const int totalSamples, const double globalBpm); + bool enableOffsetCorrection, + const QVector& rawbeats, + int sampleRate, + int totalSamples, + double globalBpm); + + static double samplesToFrames(double samples) { + return samples / mixxx::kEngineChannelCount; + } + static double framesToSamples(double frames) { + return frames * mixxx::kEngineChannelCount; + } private: static double computeSampleMedian(QList sortedItems); static double computeFilteredWeightedAverage( - const QMap frequencyTable, - const double filterCenter, - const double filterTolerance, - QMap* filteredFrequencyTable); + const QMap& frequencyTable, + double filterCenter, + double filterTolerance, + QMap* filteredFrequencyTable); static QList computeWindowedBpmsAndFrequencyHistogram( - const QVector beats, const int windowSize, const int windowStep, - const int sampleRate, QMap* frequencyHistogram); - + const QVector& beats, + int windowSize, + int windowStep, + int framesPerSecond, + QMap* frequencyHistogram); }; #endif /* BEATUTILS_H_ */ diff --git a/src/track/bpm.h b/src/track/bpm.h index 968d351c783..725e4ef8bbe 100644 --- a/src/track/bpm.h +++ b/src/track/bpm.h @@ -65,7 +65,7 @@ class Bpm final { }; bool compareEq( - const Bpm& bpm, + Bpm bpm, Comparison cmp = Comparison::Default) const { switch (cmp) { case Comparison::Integer: @@ -82,21 +82,33 @@ class Bpm final { double m_value; }; -inline -bool operator==(const Bpm& lhs, const Bpm& rhs) { +inline bool operator==(Bpm lhs, Bpm rhs) { return lhs.compareEq(rhs); } -inline -bool operator!=(const Bpm& lhs, const Bpm& rhs) { +inline bool operator!=(Bpm lhs, Bpm rhs) { return !(lhs == rhs); } -inline -QDebug operator<<(QDebug dbg, const Bpm& arg) { +inline QDebug operator<<(QDebug dbg, Bpm arg) { return dbg << arg.getValue(); } +inline Bpm operator*(Bpm bpm, double val) { + return Bpm(bpm.getValue() * val); +} + +inline Bpm operator/(Bpm bpm, double val) { + return Bpm(bpm.getValue() / val); +} + +inline Bpm operator+(Bpm bpm, double val) { + return Bpm(bpm.getValue() + val); +} + +inline Bpm operator-(Bpm& bpm, double val) { + return Bpm(bpm.getValue() - val); +} } Q_DECLARE_TYPEINFO(mixxx::Bpm, Q_MOVABLE_TYPE); diff --git a/src/track/frame.h b/src/track/frame.h new file mode 100644 index 00000000000..b88cb6214cb --- /dev/null +++ b/src/track/frame.h @@ -0,0 +1,111 @@ +#pragma once + +#include +#include + +namespace mixxx { +/// FrameDiff_t can be used to store the difference in position between +/// two frames and to store the length of a segment of track in terms of frames. +typedef double FrameDiff_t; +typedef double value_t; + +/// FramePos defines the position of a frame in a track +/// with respect to a fixed origin, i.e. start of the track. +class FramePos final { + public: + FramePos() + : m_dFramePos(0) { + } + + explicit FramePos(value_t dFramePos) + : m_dFramePos(dFramePos) { + } + + void setValue(value_t dFramePos) { + m_dFramePos = dFramePos; + } + + value_t getValue() const { + return m_dFramePos; + } + + FramePos& operator+=(FrameDiff_t increment) { + m_dFramePos += increment; + return *this; + } + + FramePos& operator-=(FrameDiff_t decrement) { + m_dFramePos -= decrement; + return *this; + } + + FramePos& operator*=(double multiple) { + m_dFramePos *= multiple; + return *this; + } + + FramePos& operator/=(double divisor) { + m_dFramePos /= divisor; + return *this; + } + + private: + value_t m_dFramePos; +}; + +// FramePos can be added to and subracted from a FrameDiff_t +inline FramePos operator+(FramePos framePos, FrameDiff_t frameDiff) { + return FramePos(framePos.getValue() + frameDiff); +} + +inline FramePos operator-(FramePos framePos, FrameDiff_t frameDiff) { + return FramePos(framePos.getValue() - frameDiff); +} + +inline FrameDiff_t operator-(FramePos framePos1, FramePos framePos2) { + return framePos1.getValue() - framePos2.getValue(); +} + +// Adding two FramePos is not allowed since every FramePos shares a common +// reference or origin i.e. the start of the track. + +// FramePos can be multiplied or divided by a double +inline FramePos operator*(FramePos framePos, double multiple) { + return FramePos(framePos.getValue() * multiple); +} + +inline FramePos operator/(FramePos framePos, double divisor) { + return FramePos(framePos.getValue() / divisor); +} + +inline bool operator<(FramePos frame1, FramePos frame2) { + return frame1.getValue() < frame2.getValue(); +} + +inline bool operator<=(FramePos frame1, FramePos frame2) { + return frame1.getValue() <= frame2.getValue(); +} + +inline bool operator>(FramePos frame1, FramePos frame2) { + return frame1.getValue() > frame2.getValue(); +} + +inline bool operator>=(FramePos frame1, FramePos frame2) { + return frame1.getValue() >= frame2.getValue(); +} + +inline bool operator==(FramePos frame1, FramePos frame2) { + return frame1.getValue() == frame2.getValue(); +} + +inline bool operator!=(FramePos frame1, FramePos frame2) { + return !(frame1.getValue() == frame2.getValue()); +} + +inline QDebug operator<<(QDebug dbg, FramePos arg) { + dbg << arg.getValue(); + return dbg; +} + +const FramePos kInvalidFramePos = FramePos(std::numeric_limits::lowest()); +} // namespace mixxx diff --git a/src/track/timesignature.h b/src/track/timesignature.h new file mode 100644 index 00000000000..f7e0169721d --- /dev/null +++ b/src/track/timesignature.h @@ -0,0 +1,68 @@ +// signature.h +// Created on: 06/11/2019 by Javier Vilarroig + +#pragma once + +namespace mixxx { + +/// Musical Time signature +/// Right now is to be used for bar detection only, so only the beats per bar is useful +class TimeSignature final { + public: + // The default constructor will set time signature to 4/4 + // since it is defined as the default in beats.proto + TimeSignature() = default; + + TimeSignature(int beatsPerBar, int noteValue) { + setTimeSignature(beatsPerBar, noteValue); + } + + explicit TimeSignature(track::io::TimeSignature timeSignatureProto) { + m_timeSignature = timeSignatureProto; + } + + ~TimeSignature() = default; + + void setBeatsPerBar(int beatsPerBar) { + m_timeSignature.set_beats_per_bar(beatsPerBar); + } + + void setNoteValue(int noteValue) { + m_timeSignature.set_note_value(noteValue); + } + + void setTimeSignature(int beatsPerBar, int noteValue) { + setBeatsPerBar(beatsPerBar); + setNoteValue(noteValue); + } + + int getBeatsPerBar() const { + return m_timeSignature.beats_per_bar(); + } + + int getNoteValue() const { + return m_timeSignature.note_value(); + } + + private: + // Using the protocol buffer defined type. + mixxx::track::io::TimeSignature m_timeSignature; +}; + +inline bool operator==(TimeSignature signature1, TimeSignature signature2) { + return signature1.getBeatsPerBar() == signature2.getBeatsPerBar() && + signature1.getNoteValue() == signature2.getNoteValue(); +} + +inline bool operator!=(TimeSignature signature1, TimeSignature signature2) { + return !(signature1 == signature2); +} + +inline QDebug operator<<(QDebug dbg, TimeSignature timeSignature) { + return dbg << timeSignature.getBeatsPerBar() << "/" << timeSignature.getNoteValue(); +} + +// Invalid Signature +const TimeSignature kNullTimeSignature = TimeSignature(0,0); + +} // namespace mixxx diff --git a/src/track/track.cpp b/src/track/track.cpp index 16c14597596..a4b8797aa2b 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -5,10 +5,10 @@ #include #include "engine/engine.h" -#include "track/beatfactory.h" #include "track/trackref.h" #include "util/assert.h" #include "util/color/color.h" +#include "util/frameadapter.h" #include "util/logger.h" namespace { @@ -49,7 +49,7 @@ inline mixxx::Bpm getActualBpm( // Reason: The BPM value in the metadata might be normalized // or rounded, e.g. ID3v2 only supports integer values. if (pBeats) { - return mixxx::Bpm(pBeats->getBpm()); + return mixxx::Bpm(pBeats->getBpm().getValue()); } else { return bpm; } @@ -258,7 +258,7 @@ double Track::getBpm() const { // BPM from beat grid overrides BPM from metadata // Reason: The BPM value in the metadata might be imprecise, // e.g. ID3v2 only supports integer values! - double beatsBpm = m_pBeats->getBpm(); + double beatsBpm = m_pBeats->getBpm().getValue(); if (mixxx::Bpm::isValidValue(beatsBpm)) { bpm = beatsBpm; } @@ -266,6 +266,7 @@ double Track::getBpm() const { return bpm; } +// TODO(JVC) It makes no sense to setBpm on a beatmap. To be removed. double Track::setBpm(double bpmValue) { if (!mixxx::Bpm::isValidValue(bpmValue)) { // If the user sets the BPM to an invalid value, we assume @@ -276,20 +277,23 @@ double Track::setBpm(double bpmValue) { QMutexLocker lock(&m_qMutex); + // TODO(JVC) A track must always have a Beats even if it's empty if (!m_pBeats) { // No beat grid available -> create and initialize - double cue = getCuePoint().getPosition(); - mixxx::BeatsPointer pBeats(BeatFactory::makeBeatGrid(*this, bpmValue, cue)); + mixxx::FramePos cue = samplePosToFramePos(getCuePoint().getPosition()); + mixxx::BeatsPointer pBeats = std::make_shared(this); + // setGrid accepts frames, but cue is in samples. + pBeats->setGrid(mixxx::Bpm(bpmValue), cue); setBeatsAndUnlock(&lock, pBeats); return bpmValue; } // Continue with the regular case - if (m_pBeats->getBpm() != bpmValue) { + if (m_pBeats->getBpm().getValue() != bpmValue) { if (kLogger.debugEnabled()) { kLogger.debug() << "Updating BPM:" << getLocation(); } - m_pBeats->setBpm(bpmValue); + m_pBeats->setBpm(mixxx::Bpm(bpmValue)); markDirtyAndUnlock(&lock); // Tell the GUI to update the bpm label... //qDebug() << "Track signaling BPM update to" << f; @@ -318,15 +322,15 @@ void Track::setBeatsAndUnlock(QMutexLocker* pLock, mixxx::BeatsPointer pBeats) { } if (m_pBeats) { - disconnect(m_pBeats.data(), &mixxx::Beats::updated, this, &Track::slotBeatsUpdated); + disconnect(m_pBeats.get(), &mixxx::Beats::updated, this, &Track::slotBeatsUpdated); } m_pBeats = std::move(pBeats); auto bpmValue = mixxx::Bpm::kValueUndefined; if (m_pBeats) { - bpmValue = m_pBeats->getBpm(); - connect(m_pBeats.data(), &mixxx::Beats::updated, this, &Track::slotBeatsUpdated); + bpmValue = m_pBeats->getBpm().getValue(); + connect(m_pBeats.get(), &mixxx::Beats::updated, this, &Track::slotBeatsUpdated); } m_record.refMetadata().refTrackInfo().setBpm(mixxx::Bpm(bpmValue)); @@ -345,7 +349,7 @@ void Track::slotBeatsUpdated() { auto bpmValue = mixxx::Bpm::kValueUndefined; if (m_pBeats) { - bpmValue = m_pBeats->getBpm(); + bpmValue = m_pBeats->getBpm().getValue(); } m_record.refMetadata().refTrackInfo().setBpm(mixxx::Bpm(bpmValue)); @@ -1047,20 +1051,17 @@ void Track::setDirtyAndUnlock(QMutexLocker* pLock, bool bDirty) { // Unlock before emitting any signals! pLock->unlock(); - - if (trackId.isValid()) { - if (dirtyChanged) { - if (bDirty) { - emit dirty(trackId); - } else { - emit clean(trackId); - } - } + if (dirtyChanged) { if (bDirty) { - // Emit a changed signal regardless if this attempted to set us dirty. - emit changed(trackId); + emit dirty(trackId); + } else { + emit clean(trackId); } } + if (bDirty) { + // Emit a changed signal regardless if this attempted to set us dirty. + emit changed(trackId); + } } bool Track::isDirty() { @@ -1384,3 +1385,21 @@ void Track::updateAudioPropertiesFromStream( importPendingCueInfosMarkDirtyAndUnlock( &lock); } + +QDebug operator<<(QDebug dbg, const TrackPointer& arg) { + dbg << "Track Debug Info"; + dbg << "m_bDirty:" << arg->m_bDirty; + dbg << "m_bMarkedForMetadataExport:" << arg->m_bMarkedForMetadataExport; + dbg << "duration:" << arg->getDuration(); + dbg << "Sample Rate:" << arg->getSampleRate(); + dbg << "m_cuePoints:"; + for (auto cuePoint : arg->m_cuePoints) { + dbg << "\tDirty:" << cuePoint->isDirty(); + //qDebug() << "\tPosition:" << cuePoint->getPosition(); + dbg << "\tType:" << int(cuePoint->getType()); + dbg << "\tLabel:" << cuePoint->getLabel(); + } + dbg << "m_pBeats:"; + dbg << arg->m_pBeats; + return dbg; +} diff --git a/src/track/track.h b/src/track/track.h index 56b920fedbd..99e49ded310 100644 --- a/src/track/track.h +++ b/src/track/track.h @@ -450,8 +450,14 @@ class Track : public QObject { mixxx::CueInfoImporterPointer m_pCueInfoImporterPending; + /// Prints track contents information, for debugging purposes only + friend QDebug operator<<(QDebug dbg, const TrackPointer& arg); + friend class TrackDAO; friend class GlobalTrackCache; friend class GlobalTrackCacheResolver; friend class SoundSourceProxy; }; + +/// Prints track contents information, for debugging purposes only +QDebug operator<<(QDebug dbg, const Track& arg); diff --git a/src/util/frameadapter.h b/src/util/frameadapter.h new file mode 100644 index 00000000000..2f6d0e7a891 --- /dev/null +++ b/src/util/frameadapter.h @@ -0,0 +1,35 @@ +#pragma once + +// WARNING: These functions are for internal use to assist the migration +// from sample positions to frame positions in beat related operations. +// These should only be used to convert between legacy Control Object data +// which is a double value for sample position and frame position which is +// an object of the class mixxx::FramePos +// The legacy use of samples for positions assumed interleaved stereo buffers. + +// This file can be removed when Control Objects are entirely migrated +// to using frames instead of samples. + +// Side note: Sample rate directly corresponds to frames not samples. +// For example, a sample rate of 44100 Hz in a stereo track means there are +// 44100 audio frames and 88200 audio samples (44100 for each channel). + +/// Convert sample position (double) to frame position (mixxx::FramePos) +inline mixxx::FramePos samplePosToFramePos(double samplePos) { + return mixxx::FramePos(samplePos / mixxx::kEngineChannelCount); +} + +/// Convert frame position (mixxx::FramePos) to sample position (double) +inline double framePosToSamplePos(mixxx::FramePos framePos) { + return framePos.getValue() * mixxx::kEngineChannelCount; +} + +/// Convert sample count (double) to frame count (double aka mixxx::FrameDiff_t) +inline mixxx::FrameDiff_t samplesToFrames(double samples) { + return samples / mixxx::kEngineChannelCount; +} + +/// Convert frame count (double aka mixxx::FrameDiff_t) to sample count (double) +inline double framesToSamples(mixxx::FrameDiff_t frames) { + return frames * mixxx::kEngineChannelCount; +} diff --git a/src/vinylcontrol/vinylcontrolprocessor.cpp b/src/vinylcontrol/vinylcontrolprocessor.cpp index 837f62ca1cf..43c29003dd7 100644 --- a/src/vinylcontrol/vinylcontrolprocessor.cpp +++ b/src/vinylcontrol/vinylcontrolprocessor.cpp @@ -94,13 +94,8 @@ void VinylControlProcessor::run() { FIFO* pSamplePipe = m_samplePipes[i]; if (pSamplePipe->readAvailable() > 0) { - int samplesRead = pSamplePipe->read(m_pWorkBuffer, MAX_BUFFER_LEN); - - if (samplesRead % 2 != 0) { - qWarning() << "VinylControlProcessor received non-even number of samples via sample FIFO."; - samplesRead--; - } - int framesRead = samplesRead / 2; + double samplesRead = pSamplePipe->read(m_pWorkBuffer, MAX_BUFFER_LEN); + double framesRead = samplesRead / 2; if (pProcessor) { pProcessor->analyzeSamples(m_pWorkBuffer, framesRead); diff --git a/src/waveform/renderers/waveformrenderbeat.cpp b/src/waveform/renderers/waveformrenderbeat.cpp index 78a25f5990c..1677c09b343 100644 --- a/src/waveform/renderers/waveformrenderbeat.cpp +++ b/src/waveform/renderers/waveformrenderbeat.cpp @@ -51,12 +51,12 @@ void WaveformRenderBeat::draw(QPainter* painter, QPaintEvent* /*event*/) { m_waveformRenderer->getLastDisplayedPosition(); // qDebug() << "trackSamples" << trackSamples - // << "firstDisplayedPosition" << firstDisplayedPosition - // << "lastDisplayedPosition" << lastDisplayedPosition; + // << "firstDisplayedPosition" << firstDisplayedPosition + // << "lastDisplayedPosition" << lastDisplayedPosition; - std::unique_ptr it(trackBeats->findBeats( - firstDisplayedPosition * trackSamples, - lastDisplayedPosition * trackSamples)); + std::unique_ptr it(trackBeats->findBeats( + mixxx::FramePos(firstDisplayedPosition * trackSamples / 2.0), + mixxx::FramePos(lastDisplayedPosition * trackSamples / 2.0))); // if no beat do not waste time saving/restoring painter if (!it || !it->hasNext()) { @@ -78,9 +78,10 @@ void WaveformRenderBeat::draw(QPainter* painter, QPaintEvent* /*event*/) { int beatCount = 0; while (it->hasNext()) { - double beatPosition = it->next(); + // Beats->next returns Frame number and we need Sample number + double beatSamplePosition = it->next().frame_position() * mixxx::kEngineChannelCount; double xBeatPoint = - m_waveformRenderer->transformSamplePositionInRendererWorld(beatPosition); + m_waveformRenderer->transformSamplePositionInRendererWorld(beatSamplePosition); xBeatPoint = qRound(xBeatPoint); diff --git a/src/widget/wtrackmenu.cpp b/src/widget/wtrackmenu.cpp index 54d1c8cc1f4..5fa23852b35 100644 --- a/src/widget/wtrackmenu.cpp +++ b/src/widget/wtrackmenu.cpp @@ -276,22 +276,22 @@ void WTrackMenu::createActions() { m_pBpmThreeHalvesAction = new QAction(tr("3/2 BPM"), m_pBPMMenu); connect(m_pBpmDoubleAction, &QAction::triggered, this, [this] { - slotScaleBpm(mixxx::Beats::DOUBLE); + slotScaleBpm(mixxx::BeatsInternal::DOUBLE); }); connect(m_pBpmHalveAction, &QAction::triggered, this, [this] { - slotScaleBpm(mixxx::Beats::HALVE); + slotScaleBpm(mixxx::BeatsInternal::HALVE); }); connect(m_pBpmTwoThirdsAction, &QAction::triggered, this, [this] { - slotScaleBpm(mixxx::Beats::TWOTHIRDS); + slotScaleBpm(mixxx::BeatsInternal::TWOTHIRDS); }); connect(m_pBpmThreeFourthsAction, &QAction::triggered, this, [this] { - slotScaleBpm(mixxx::Beats::THREEFOURTHS); + slotScaleBpm(mixxx::BeatsInternal::THREEFOURTHS); }); connect(m_pBpmFourThirdsAction, &QAction::triggered, this, [this] { - slotScaleBpm(mixxx::Beats::FOURTHIRDS); + slotScaleBpm(mixxx::BeatsInternal::FOURTHIRDS); }); connect(m_pBpmThreeHalvesAction, &QAction::triggered, this, [this] { - slotScaleBpm(mixxx::Beats::THREEHALVES); + slotScaleBpm(mixxx::BeatsInternal::THREEHALVES); }); m_pBpmResetAction = new QAction(tr("Reset BPM"), m_pBPMMenu); @@ -1132,7 +1132,7 @@ namespace { class ScaleBpmTrackPointerOperation : public mixxx::TrackPointerOperation { public: - explicit ScaleBpmTrackPointerOperation(mixxx::Beats::BPMScale bpmScale) + explicit ScaleBpmTrackPointerOperation(mixxx::BeatsInternal::BPMScale bpmScale) : m_bpmScale(bpmScale) { } @@ -1146,10 +1146,10 @@ class ScaleBpmTrackPointerOperation : public mixxx::TrackPointerOperation { if (!pBeats) { return; } - pBeats->scale(m_bpmScale); + pBeats->scale(static_cast(m_bpmScale)); } - const mixxx::Beats::BPMScale m_bpmScale; + const mixxx::BeatsInternal::BPMScale m_bpmScale; }; } // anonymous namespace @@ -1159,7 +1159,7 @@ void WTrackMenu::slotScaleBpm(int scale) { tr("Scaling BPM of %n track(s)", "", getTrackCount()); const auto trackOperator = ScaleBpmTrackPointerOperation( - static_cast(scale)); + static_cast(scale)); applyTrackPointerOperation( progressLabelText, &trackOperator);