1 #include "analyzer/analyzerbeats.h"
2 
3 #include <QHash>
4 #include <QString>
5 #include <QVector>
6 #include <QtDebug>
7 
8 #include "analyzer/constants.h"
9 #include "analyzer/plugins/analyzerqueenmarybeats.h"
10 #include "analyzer/plugins/analyzersoundtouchbeats.h"
11 #include "library/rekordbox/rekordboxconstants.h"
12 #include "track/beatfactory.h"
13 #include "track/beatmap.h"
14 #include "track/beatutils.h"
15 #include "track/track.h"
16 
17 // static
availablePlugins()18 QList<mixxx::AnalyzerPluginInfo> AnalyzerBeats::availablePlugins() {
19     QList<mixxx::AnalyzerPluginInfo> plugins;
20     // First one below is the default
21     plugins.append(mixxx::AnalyzerQueenMaryBeats::pluginInfo());
22     plugins.append(mixxx::AnalyzerSoundTouchBeats::pluginInfo());
23     return plugins;
24 }
25 
26 // static
defaultPlugin()27 mixxx::AnalyzerPluginInfo AnalyzerBeats::defaultPlugin() {
28     const auto plugins = availablePlugins();
29     DEBUG_ASSERT(!plugins.isEmpty());
30     return plugins.at(0);
31 }
32 
AnalyzerBeats(UserSettingsPointer pConfig,bool enforceBpmDetection)33 AnalyzerBeats::AnalyzerBeats(UserSettingsPointer pConfig, bool enforceBpmDetection)
34         : m_bpmSettings(pConfig),
35           m_enforceBpmDetection(enforceBpmDetection),
36           m_bPreferencesReanalyzeOldBpm(false),
37           m_bPreferencesReanalyzeImported(false),
38           m_bPreferencesFixedTempo(true),
39           m_bPreferencesFastAnalysis(false),
40           m_totalSamples(0),
41           m_iMaxSamplesToProcess(0),
42           m_iCurrentSample(0) {
43 }
44 
initialize(TrackPointer pTrack,int sampleRate,int totalSamples)45 bool AnalyzerBeats::initialize(TrackPointer pTrack, int sampleRate, int totalSamples) {
46     if (totalSamples == 0) {
47         return false;
48     }
49 
50     bool bPreferencesBeatDetectionEnabled =
51             m_enforceBpmDetection || m_bpmSettings.getBpmDetectionEnabled();
52     if (!bPreferencesBeatDetectionEnabled) {
53         qDebug() << "Beat calculation is deactivated";
54         return false;
55     }
56 
57     bool bpmLock = pTrack->isBpmLocked();
58     if (bpmLock) {
59         qDebug() << "Track is BpmLocked: Beat calculation will not start";
60         return false;
61     }
62 
63     m_bPreferencesFixedTempo = m_bpmSettings.getFixedTempoAssumption();
64     m_bPreferencesReanalyzeOldBpm = m_bpmSettings.getReanalyzeWhenSettingsChange();
65     m_bPreferencesReanalyzeImported = m_bpmSettings.getReanalyzeImported();
66     m_bPreferencesFastAnalysis = m_bpmSettings.getFastAnalysis();
67 
68     const auto plugins = availablePlugins();
69     if (!plugins.isEmpty()) {
70         m_pluginId = defaultPlugin().id;
71         QString pluginId = m_bpmSettings.getBeatPluginId();
72         for (const auto& info : plugins) {
73             if (info.id == pluginId) {
74                 m_pluginId = pluginId; // configured Plug-In available
75                 break;
76             }
77         }
78     }
79 
80     qDebug() << "AnalyzerBeats preference settings:"
81              << "\nPlugin:" << m_pluginId
82              << "\nFixed tempo assumption:" << m_bPreferencesFixedTempo
83              << "\nRe-analyze when settings change:" << m_bPreferencesReanalyzeOldBpm
84              << "\nFast analysis:" << m_bPreferencesFastAnalysis;
85 
86     m_sampleRate = sampleRate;
87     m_totalSamples = totalSamples;
88     // In fast analysis mode, skip processing after
89     // kFastAnalysisSecondsToAnalyze seconds are analyzed.
90     if (m_bPreferencesFastAnalysis) {
91         m_iMaxSamplesToProcess =
92                 mixxx::kFastAnalysisSecondsToAnalyze * m_sampleRate * mixxx::kAnalysisChannels;
93     } else {
94         m_iMaxSamplesToProcess = m_totalSamples;
95     }
96     m_iCurrentSample = 0;
97 
98     // if we can load a stored track don't reanalyze it
99     bool bShouldAnalyze = shouldAnalyze(pTrack);
100 
101     DEBUG_ASSERT(!m_pPlugin);
102     if (bShouldAnalyze) {
103         if (m_pluginId == mixxx::AnalyzerQueenMaryBeats::pluginInfo().id) {
104             m_pPlugin = std::make_unique<mixxx::AnalyzerQueenMaryBeats>();
105         } else if (m_pluginId == mixxx::AnalyzerSoundTouchBeats::pluginInfo().id) {
106             m_pPlugin = std::make_unique<mixxx::AnalyzerSoundTouchBeats>();
107         } else {
108             // This must not happen, because we have already verified above
109             // that the PlugInId is valid
110             DEBUG_ASSERT(false);
111         }
112 
113         if (m_pPlugin) {
114             if (m_pPlugin->initialize(sampleRate)) {
115                 qDebug() << "Beat calculation started with plugin" << m_pluginId;
116             } else {
117                 qDebug() << "Beat calculation will not start.";
118                 m_pPlugin.reset();
119                 bShouldAnalyze = false;
120             }
121         } else {
122             bShouldAnalyze = false;
123         }
124     }
125     return bShouldAnalyze;
126 }
127 
shouldAnalyze(TrackPointer pTrack) const128 bool AnalyzerBeats::shouldAnalyze(TrackPointer pTrack) const {
129     bool bpmLock = pTrack->isBpmLocked();
130     if (bpmLock) {
131         qDebug() << "Track is BpmLocked: Beat calculation will not start";
132         return false;
133     }
134 
135     QString pluginID = m_bpmSettings.getBeatPluginId();
136     if (pluginID.isEmpty()) {
137         pluginID = defaultPlugin().id;
138     }
139 
140     // If the track already has a Beats object then we need to decide whether to
141     // analyze this track or not.
142     const mixxx::BeatsPointer pBeats = pTrack->getBeats();
143     if (!pBeats) {
144         return true;
145     }
146     if (!mixxx::Bpm::isValidValue(pBeats->getBpm())) {
147         // Tracks with an invalid bpm <= 0 should be re-analyzed,
148         // independent of the preference settings. We expect that
149         // all tracks have a bpm > 0 when analyzed. Users that want
150         // to keep their zero bpm tracks could lock them to prevent
151         // this re-analysis (see the check above).
152         qDebug() << "Re-analyzing track with invalid BPM despite preference settings.";
153         return true;
154     }
155 
156     QString subVersion = pBeats->getSubVersion();
157     if (subVersion == mixxx::rekordboxconstants::beatsSubversion) {
158         return m_bPreferencesReanalyzeImported;
159     }
160 
161     if (subVersion.isEmpty() && pBeats->findNextBeat(0) <= 0.0 &&
162             m_pluginId != mixxx::AnalyzerSoundTouchBeats::pluginInfo().id) {
163         // This happens if the beat grid was created from the metadata BPM value.
164         qDebug() << "First beat is 0 for grid so analyzing track to find first beat.";
165         return true;
166     }
167 
168     QString version = pBeats->getVersion();
169     QHash<QString, QString> extraVersionInfo = getExtraVersionInfo(
170             pluginID,
171             m_bPreferencesFastAnalysis);
172     QString newVersion = BeatFactory::getPreferredVersion(
173             m_bPreferencesFixedTempo);
174     QString newSubVersion = BeatFactory::getPreferredSubVersion(
175             extraVersionInfo);
176 
177     if (version == newVersion && subVersion == newSubVersion) {
178         // If the version and settings have not changed then if the world is
179         // sane, re-analyzing will do nothing.
180         return false;
181     }
182     // Beat grid exists but version and settings differ
183     if (!m_bPreferencesReanalyzeOldBpm) {
184         qDebug() << "Beat calculation skips analyzing because the track has"
185                 << "a BPM computed by a previous Mixxx version and user"
186                 << "preferences indicate we should not change it.";
187         return false;
188     }
189 
190     return true;
191 }
192 
processSamples(const CSAMPLE * pIn,const int iLen)193 bool AnalyzerBeats::processSamples(const CSAMPLE *pIn, const int iLen) {
194     VERIFY_OR_DEBUG_ASSERT(m_pPlugin) {
195         return false;
196     }
197 
198     m_iCurrentSample += iLen;
199     if (m_iCurrentSample > m_iMaxSamplesToProcess) {
200         return true; // silently ignore all remaining samples
201     }
202 
203     return m_pPlugin->processSamples(pIn, iLen);
204 }
205 
cleanup()206 void AnalyzerBeats::cleanup() {
207     m_pPlugin.reset();
208 }
209 
storeResults(TrackPointer pTrack)210 void AnalyzerBeats::storeResults(TrackPointer pTrack) {
211     VERIFY_OR_DEBUG_ASSERT(m_pPlugin) {
212         return;
213     }
214 
215     if (!m_pPlugin->finalize()) {
216         qWarning() << "Beat/BPM analysis failed";
217         return;
218     }
219 
220     mixxx::BeatsPointer pBeats;
221     if (m_pPlugin->supportsBeatTracking()) {
222         QVector<double> beats = m_pPlugin->getBeats();
223         QHash<QString, QString> extraVersionInfo = getExtraVersionInfo(
224                 m_pluginId, m_bPreferencesFastAnalysis);
225         pBeats = BeatFactory::makePreferredBeats(
226                 beats,
227                 extraVersionInfo,
228                 m_bPreferencesFixedTempo,
229                 m_sampleRate);
230         qDebug() << "AnalyzerBeats plugin detected" << beats.size()
231                  << "beats. Average BPM:" << (pBeats ? pBeats->getBpm() : 0.0);
232     } else {
233         float bpm = m_pPlugin->getBpm();
234         qDebug() << "AnalyzerBeats plugin detected constant BPM: " << bpm;
235         pBeats = BeatFactory::makeBeatGrid(m_sampleRate, bpm, 0.0f);
236     }
237 
238     pTrack->trySetBeats(pBeats);
239 }
240 
241 // static
getExtraVersionInfo(const QString & pluginId,bool bPreferencesFastAnalysis)242 QHash<QString, QString> AnalyzerBeats::getExtraVersionInfo(
243         const QString& pluginId, bool bPreferencesFastAnalysis) {
244     QHash<QString, QString> extraVersionInfo;
245     extraVersionInfo["vamp_plugin_id"] = pluginId;
246     if (bPreferencesFastAnalysis) {
247         extraVersionInfo["fast_analysis"] = "1";
248     }
249     return extraVersionInfo;
250 }
251