Something went wrong. Try again.
Mixxx DJ software with MCP, Subsonic/Navidrome streaming library support — browse and load tracks from your self-hosted music server mixxx.org
subsonic navidrome streaming audio dj mac linux midi
Something went wrong. Try again.
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297#include "analyzer/analyzerbeats.h"
#include <QHash>#include <QString>#include <QVector>#include <QtDebug>
#include "analyzer/analyzertrack.h"#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/track.h"
// staticQList<mixxx::AnalyzerPluginInfo> AnalyzerBeats::availablePlugins() { QList<mixxx::AnalyzerPluginInfo> plugins; // First one below is the default plugins.append(mixxx::AnalyzerQueenMaryBeats::pluginInfo()); plugins.append(mixxx::AnalyzerSoundTouchBeats::pluginInfo()); return plugins;}
// staticmixxx::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_maxFramesToProcess(0), m_currentFrame(0) {}
bool AnalyzerBeats::initialize(const AnalyzerTrack& track, mixxx::audio::SampleRate sampleRate, mixxx::audio::ChannelCount channelCount, SINT frameLength) { if (frameLength <= 0) { return false; }
bool bPreferencesBeatDetectionEnabled = m_enforceBpmDetection || m_bpmSettings.getBpmDetectionEnabled(); if (!bPreferencesBeatDetectionEnabled) { qDebug() << "Beat calculation is deactivated"; return false; }
bool bpmLock = track.getTrack()->isBpmLocked(); if (bpmLock) { qDebug() << "Track is BpmLocked: Beat calculation will not start"; return false; }
m_bPreferencesFixedTempo = track.getOptions().useFixedTempo.value_or( 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 << "\nRe-analyze imported from other software:" << m_bPreferencesReanalyzeImported << "\nFast analysis:" << m_bPreferencesFastAnalysis;
m_sampleRate = sampleRate; m_channelCount = channelCount; // In fast analysis mode, skip processing after // kFastAnalysisSecondsToAnalyze seconds are analyzed. if (m_bPreferencesFastAnalysis) { m_maxFramesToProcess = mixxx::kFastAnalysisSecondsToAnalyze * m_sampleRate; } else { m_maxFramesToProcess = frameLength; } m_currentFrame = 0;
// if we can load a stored track don't reanalyze it bool bShouldAnalyze = shouldAnalyze(track.getTrack());
DEBUG_ASSERT(!m_pPlugin); if (bShouldAnalyze) { if (m_pluginId == mixxx::AnalyzerQueenMaryBeats::pluginInfo().id()) { m_pPlugin = std::make_unique<mixxx::AnalyzerQueenMaryBeats>(); } else if (m_pluginId == mixxx::AnalyzerSoundTouchBeats::pluginInfo().id()) { m_pPlugin = std::make_unique<mixxx::AnalyzerSoundTouchBeats>(); } 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(m_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 (!pBeats->getBpmInRange(mixxx::audio::kStartFramePos, mixxx::audio::FramePos{ pTrack->getDuration() * pBeats->getSampleRate()}) .isValid()) { // 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->firstBeat() <= mixxx::audio::kStartFramePos && 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<QString, QString> 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, SINT count) { VERIFY_OR_DEBUG_ASSERT(m_pPlugin) { return false; }
SINT numFrames = count / m_channelCount; const CSAMPLE* pBeatInput = pIn; CSAMPLE* pDrumChannel = nullptr;
if (m_channelCount == mixxx::audio::ChannelCount::stem()) { // We have an 8 channel soundsource. The only implemented soundsource with // 8ch is the NI STEM file format. // TODO: If we add other soundsources with 8ch, we need to rework this condition. // // For NI STEM we mix all the stems together except the first one, // which contains drums or beats by convention. count = numFrames * mixxx::audio::ChannelCount::stereo(); pDrumChannel = SampleUtil::alloc(count);
VERIFY_OR_DEBUG_ASSERT(pDrumChannel) { return false; }
if (m_bpmSettings.getStemStrategy() == BeatDetectionSettings::StemStrategy::Enforced) { SampleUtil::copyOneStereoFromMulti(pDrumChannel, pIn, numFrames, m_channelCount, 0); } else { SampleUtil::mixMultichannelToStereo(pDrumChannel, pIn, numFrames, m_channelCount); }
pBeatInput = pDrumChannel; } else if (m_channelCount > mixxx::audio::ChannelCount::stereo()) { DEBUG_ASSERT(!"Unsupported channel count"); return false; }
m_currentFrame += numFrames; if (m_currentFrame > m_maxFramesToProcess) { return true; // silently ignore all remaining samples }
bool ret = m_pPlugin->processSamples(pBeatInput, count); if (pDrumChannel) { SampleUtil::free(pDrumChannel); } return ret;}
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<mixxx::audio::FramePos> beats = m_pPlugin->getBeats(); QHash<QString, QString> extraVersionInfo = getExtraVersionInfo( m_pluginId, m_bPreferencesFastAnalysis); pBeats = BeatFactory::makePreferredBeats( beats, extraVersionInfo, m_bPreferencesFixedTempo, m_sampleRate); qDebug() << "AnalyzerBeats plugin detected" << beats.size() << "beats. Predominant BPM:" << (pBeats ? pBeats->getBpmInRange( mixxx::audio::kStartFramePos, mixxx::audio::FramePos{ pTrack->getDuration() * pBeats->getSampleRate()}) : mixxx::Bpm()); } else { mixxx::Bpm bpm = m_pPlugin->getBpm(); qDebug() << "AnalyzerBeats plugin detected constant BPM: " << bpm; pBeats = mixxx::Beats::fromConstTempo(m_sampleRate, mixxx::audio::kStartFramePos, bpm); }
pTrack->trySetBeats(pBeats);}
// staticQHash<QString, QString> AnalyzerBeats::getExtraVersionInfo( const QString& pluginId, bool bPreferencesFastAnalysis) { QHash<QString, QString> extraVersionInfo; extraVersionInfo["vamp_plugin_id"] = pluginId; if (bPreferencesFastAnalysis) { extraVersionInfo["fast_analysis"] = "1"; } return extraVersionInfo;}