#include "analyzer/analyzerbeats.h" #include #include #include #include #include "analyzer/constants.h" #include "analyzer/plugins/analyzerqueenmarybeats.h" #include "analyzer/plugins/analyzersoundtouchbeats.h" #include "library/rekordbox/rekordboxconstants.h" #include "track/beatfactory.h" #include "track/beatmap.h" #include "track/beatutils.h" #include "track/track.h" // static QList AnalyzerBeats::availablePlugins() { QList plugins; // First one below is the default plugins.append(mixxx::AnalyzerQueenMaryBeats::pluginInfo()); plugins.append(mixxx::AnalyzerSoundTouchBeats::pluginInfo()); return plugins; } // static mixxx::AnalyzerPluginInfo AnalyzerBeats::defaultPlugin() { const auto plugins = availablePlugins(); DEBUG_ASSERT(!plugins.isEmpty()); return plugins.at(0); } AnalyzerBeats::AnalyzerBeats(UserSettingsPointer pConfig, bool enforceBpmDetection) : m_bpmSettings(pConfig), m_enforceBpmDetection(enforceBpmDetection), m_bPreferencesReanalyzeOldBpm(false), m_bPreferencesReanalyzeImported(false), m_bPreferencesFixedTempo(true), m_bPreferencesFastAnalysis(false), m_totalSamples(0), m_iMaxSamplesToProcess(0), m_iCurrentSample(0) { } bool AnalyzerBeats::initialize(TrackPointer pTrack, int sampleRate, int totalSamples) { if (totalSamples == 0) { return false; } bool bPreferencesBeatDetectionEnabled = m_enforceBpmDetection || m_bpmSettings.getBpmDetectionEnabled(); if (!bPreferencesBeatDetectionEnabled) { qDebug() << "Beat calculation is deactivated"; return false; } bool bpmLock = pTrack->isBpmLocked(); if (bpmLock) { qDebug() << "Track is BpmLocked: Beat calculation will not start"; return false; } m_bPreferencesFixedTempo = m_bpmSettings.getFixedTempoAssumption(); m_bPreferencesReanalyzeOldBpm = m_bpmSettings.getReanalyzeWhenSettingsChange(); m_bPreferencesReanalyzeImported = m_bpmSettings.getReanalyzeImported(); m_bPreferencesFastAnalysis = m_bpmSettings.getFastAnalysis(); const auto plugins = availablePlugins(); if (!plugins.isEmpty()) { m_pluginId = defaultPlugin().id; QString pluginId = m_bpmSettings.getBeatPluginId(); for (const auto& info : plugins) { if (info.id == pluginId) { m_pluginId = pluginId; // configured Plug-In available break; } } } qDebug() << "AnalyzerBeats preference settings:" << "\nPlugin:" << m_pluginId << "\nFixed tempo assumption:" << m_bPreferencesFixedTempo << "\nRe-analyze when settings change:" << m_bPreferencesReanalyzeOldBpm << "\nFast analysis:" << m_bPreferencesFastAnalysis; m_sampleRate = sampleRate; m_totalSamples = totalSamples; // In fast analysis mode, skip processing after // kFastAnalysisSecondsToAnalyze seconds are analyzed. if (m_bPreferencesFastAnalysis) { m_iMaxSamplesToProcess = mixxx::kFastAnalysisSecondsToAnalyze * m_sampleRate * mixxx::kAnalysisChannels; } else { m_iMaxSamplesToProcess = m_totalSamples; } m_iCurrentSample = 0; // if we can load a stored track don't reanalyze it bool bShouldAnalyze = shouldAnalyze(pTrack); DEBUG_ASSERT(!m_pPlugin); if (bShouldAnalyze) { if (m_pluginId == mixxx::AnalyzerQueenMaryBeats::pluginInfo().id) { m_pPlugin = std::make_unique(); } else if (m_pluginId == mixxx::AnalyzerSoundTouchBeats::pluginInfo().id) { m_pPlugin = std::make_unique(); } else { // This must not happen, because we have already verified above // that the PlugInId is valid DEBUG_ASSERT(false); } if (m_pPlugin) { if (m_pPlugin->initialize(sampleRate)) { qDebug() << "Beat calculation started with plugin" << m_pluginId; } else { qDebug() << "Beat calculation will not start."; m_pPlugin.reset(); bShouldAnalyze = false; } } else { bShouldAnalyze = false; } } return bShouldAnalyze; } bool AnalyzerBeats::shouldAnalyze(TrackPointer pTrack) const { bool bpmLock = pTrack->isBpmLocked(); if (bpmLock) { qDebug() << "Track is BpmLocked: Beat calculation will not start"; return false; } QString pluginID = m_bpmSettings.getBeatPluginId(); if (pluginID.isEmpty()) { pluginID = defaultPlugin().id; } // If the track already has a Beats object then we need to decide whether to // analyze this track or not. const mixxx::BeatsPointer pBeats = pTrack->getBeats(); if (!pBeats) { return true; } if (!mixxx::Bpm::isValidValue(pBeats->getBpm())) { // 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 // to keep their zero bpm tracks could lock them to prevent // this re-analysis (see the check above). qDebug() << "Re-analyzing track with invalid BPM despite preference settings."; return true; } QString subVersion = pBeats->getSubVersion(); if (subVersion == mixxx::rekordboxconstants::beatsSubversion) { return m_bPreferencesReanalyzeImported; } if (subVersion.isEmpty() && pBeats->findNextBeat(0) <= 0.0 && m_pluginId != mixxx::AnalyzerSoundTouchBeats::pluginInfo().id) { // This happens if the beat grid was created from the metadata BPM value. qDebug() << "First beat is 0 for grid so analyzing track to find first beat."; return true; } QString version = pBeats->getVersion(); QHash extraVersionInfo = getExtraVersionInfo( pluginID, m_bPreferencesFastAnalysis); QString newVersion = BeatFactory::getPreferredVersion( m_bPreferencesFixedTempo); QString newSubVersion = BeatFactory::getPreferredSubVersion( extraVersionInfo); if (version == newVersion && subVersion == newSubVersion) { // If the version and settings have not changed then if the world is // sane, re-analyzing will do nothing. return false; } // 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."; return false; } return true; } bool AnalyzerBeats::processSamples(const CSAMPLE *pIn, const int iLen) { VERIFY_OR_DEBUG_ASSERT(m_pPlugin) { return false; } m_iCurrentSample += iLen; if (m_iCurrentSample > m_iMaxSamplesToProcess) { return true; // silently ignore all remaining samples } return m_pPlugin->processSamples(pIn, iLen); } void AnalyzerBeats::cleanup() { m_pPlugin.reset(); } void AnalyzerBeats::storeResults(TrackPointer pTrack) { VERIFY_OR_DEBUG_ASSERT(m_pPlugin) { return; } if (!m_pPlugin->finalize()) { qWarning() << "Beat/BPM analysis failed"; return; } mixxx::BeatsPointer pBeats; if (m_pPlugin->supportsBeatTracking()) { QVector beats = m_pPlugin->getBeats(); QHash extraVersionInfo = getExtraVersionInfo( m_pluginId, m_bPreferencesFastAnalysis); pBeats = BeatFactory::makePreferredBeats( beats, extraVersionInfo, m_bPreferencesFixedTempo, m_sampleRate); qDebug() << "AnalyzerBeats plugin detected" << beats.size() << "beats. Average BPM:" << (pBeats ? pBeats->getBpm() : 0.0); } else { float bpm = m_pPlugin->getBpm(); qDebug() << "AnalyzerBeats plugin detected constant BPM: " << bpm; pBeats = BeatFactory::makeBeatGrid(m_sampleRate, bpm, 0.0f); } pTrack->trySetBeats(pBeats); } // static QHash AnalyzerBeats::getExtraVersionInfo( const QString& pluginId, bool bPreferencesFastAnalysis) { QHash extraVersionInfo; extraVersionInfo["vamp_plugin_id"] = pluginId; if (bPreferencesFastAnalysis) { extraVersionInfo["fast_analysis"] = "1"; } return extraVersionInfo; }