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