diff --git a/build/depends.py b/build/depends.py index d57bb701935b..99ec0afd5719 100644 --- a/build/depends.py +++ b/build/depends.py @@ -786,7 +786,8 @@ def sources(self, build): "engine/cachingreaderchunk.cpp", "engine/cachingreaderworker.cpp", - "analyzer/analyzerqueue.cpp", + "analyzer/analyzermanager.cpp", + "analyzer/analyzerworker.cpp", "analyzer/analyzerwaveform.cpp", "analyzer/analyzergain.cpp", "analyzer/analyzerebur128.cpp", diff --git a/src/analyzer/analyzerbeats.cpp b/src/analyzer/analyzerbeats.cpp index e627105bcedf..c548e6c8f1dc 100644 --- a/src/analyzer/analyzerbeats.cpp +++ b/src/analyzer/analyzerbeats.cpp @@ -17,7 +17,7 @@ #include "track/beatutils.h" #include "track/track.h" -AnalyzerBeats::AnalyzerBeats(UserSettingsPointer pConfig) +AnalyzerBeats::AnalyzerBeats(UserSettingsPointer pConfig, bool forceBeatDetection) : m_pConfig(pConfig), m_pVamp(NULL), m_bPreferencesReanalyzeOldBpm(false), @@ -27,7 +27,8 @@ AnalyzerBeats::AnalyzerBeats(UserSettingsPointer pConfig) m_iSampleRate(0), m_iTotalSamples(0), m_iMinBpm(0), - m_iMaxBpm(9999) { + m_iMaxBpm(9999), + m_forceBeatDetection(forceBeatDetection) { } AnalyzerBeats::~AnalyzerBeats() { @@ -38,9 +39,9 @@ bool AnalyzerBeats::initialize(TrackPointer tio, int sampleRate, int totalSample return false; } - bool bPreferencesBeatDetectionEnabled = m_pConfig->getValue( + bool beatDetectionEnabled = m_forceBeatDetection || m_pConfig->getValue( ConfigKey(BPM_CONFIG_KEY, BPM_DETECTION_ENABLED)); - if (!bPreferencesBeatDetectionEnabled) { + if (!beatDetectionEnabled) { qDebug() << "Beat calculation is deactivated"; return false; } diff --git a/src/analyzer/analyzerbeats.h b/src/analyzer/analyzerbeats.h index 0de0d4e4167b..f3d876bd0e0e 100644 --- a/src/analyzer/analyzerbeats.h +++ b/src/analyzer/analyzerbeats.h @@ -16,7 +16,7 @@ class AnalyzerBeats: public Analyzer { public: - AnalyzerBeats(UserSettingsPointer pConfig); + AnalyzerBeats(UserSettingsPointer pConfig, bool forceBeatDetection); virtual ~AnalyzerBeats(); bool initialize(TrackPointer tio, int sampleRate, int totalSamples) override; @@ -40,6 +40,7 @@ class AnalyzerBeats: public Analyzer { int m_iSampleRate, m_iTotalSamples; int m_iMinBpm, m_iMaxBpm; + bool m_forceBeatDetection; }; #endif /* ANALYZER_ANALYZERBEATS_H */ diff --git a/src/analyzer/analyzermanager.cpp b/src/analyzer/analyzermanager.cpp new file mode 100644 index 000000000000..6f954a30bbe4 --- /dev/null +++ b/src/analyzer/analyzermanager.cpp @@ -0,0 +1,311 @@ +#include "analyzer/analyzermanager.h" + +#include +#include + +#include +#include +#include + +#include "library/trackcollection.h" +#include "mixer/playerinfo.h" +#include "track/track.h" +#include "util/compatibility.h" +#include "util/event.h" +#include "util/timer.h" +#include "util/trace.h" + +AnalyzerManager::AnalyzerManager(UserSettingsPointer pConfig, + mixxx::DbConnectionPoolPtr pDbConnectionPool) : + m_pDbConnectionPool(std::move(pDbConnectionPool)), + m_pConfig(pConfig), + m_nextWorkerId(0), + m_defaultTrackQueue(), + m_prioTrackQueue(), + m_defaultWorkers(), + m_priorityWorkers(), + m_pausedWorkers(), + m_endingWorkers() { + + int ideal = QThread::idealThreadCount(); + int maxThreads = m_pConfig->getValue(ConfigKey("[Library]", "MaxAnalysisThreads"), ideal); + if (ideal < 1) { + if (maxThreads > 0 && maxThreads <= 32) { + qDebug() << "Cannot detect idealThreadCount. maxThreads is: " << maxThreads; + } + else { + qWarning() << "Cannot detect idealThreadCount and maxThreads is incorrect: " << maxThreads <<". Using the sane value of 1"; + maxThreads = ideal; + m_pConfig->setValue(ConfigKey("[Library]", "MaxAnalysisThreads"), maxThreads); + } + } + else if (maxThreads <= 0 || maxThreads > ideal) { + qWarning() << "maxThreads value is incorrect: " << maxThreads << ". Changing it to " << ideal; + //Assume the value is incorrect, so fix it. + maxThreads = ideal; + m_pConfig->setValue(ConfigKey("[Library]", "MaxAnalysisThreads"), maxThreads); + } + m_MaxThreads = maxThreads; +} + +AnalyzerManager::~AnalyzerManager() { + stop(true); +} + +bool AnalyzerManager::isActive() { + int total = m_priorityWorkers.size() + + m_defaultWorkers.size() + m_pausedWorkers.size(); + return total > 0; +} +bool AnalyzerManager::isDefaultQueueActive() { + int total = m_defaultWorkers.size() + m_pausedWorkers.size(); + return total > 0; +} + +void AnalyzerManager::stop(bool shutdown) { + m_defaultTrackQueue.clear(); + foreach(AnalyzerWorker* worker, m_defaultWorkers) { + worker->endProcess(); + m_endingWorkers.append(worker); + } + foreach(AnalyzerWorker* worker, m_pausedWorkers) { + worker->endProcess(); + m_endingWorkers.append(worker); + } + if (shutdown) { + m_prioTrackQueue.clear(); + foreach(AnalyzerWorker* worker, m_priorityWorkers) { + worker->endProcess(); + m_endingWorkers.append(worker); + } + //TODO: ensure that they are all forcibly stopped. + } +} +//Add a track to be analyzed with a priority worker. (Like those required by loading a track into a player). +void AnalyzerManager::analyseTrackNow(TrackPointer tio) { + if (m_defaultTrackQueue.contains(tio)) { + m_defaultTrackQueue.removeAll(tio); + } + //TODO: There's one scenario that we still miss: load on a deck a track that is currently + //being analyzed by the background worker. We cannot reuse the background worker, but we should discard its work. + if (!m_prioTrackQueue.contains(tio)) { + m_prioTrackQueue.append(tio); + if (m_priorityWorkers.isEmpty() && m_defaultWorkers.size() > 0) { + //In order to keep the application responsive, and ensure that a priority worker is + //not slowed down by default workers (because they have the same OS thread priority), + //we stop one additional default worker + AnalyzerWorker * backwork = m_defaultWorkers.first(); + backwork->pause(); + //Ideally i would have done this on the slotPaused slot, but then i cannot + //ensure i won't call pause twice for the same worker. + m_pausedWorkers.append(backwork); + m_defaultWorkers.removeAll(backwork); + } + if (m_priorityWorkers.size() < m_MaxThreads-1) { + createNewWorker(WorkerType::priorityWorker); + if (m_priorityWorkers.size() + m_defaultWorkers.size() > m_MaxThreads-1) { + AnalyzerWorker * backwork = m_defaultWorkers.first(); + backwork->pause(); + //Ideally i would have done this on the slotPaused slot, but then i cannot + //ensure i won't call pause twice for the same worker. + m_pausedWorkers.append(backwork); + m_defaultWorkers.removeAll(backwork); + } + } + } +} +// This is called from the GUI for the analysis feature of the library. +void AnalyzerManager::queueAnalyseTrack(TrackPointer tio) { + //See notes on analyseTrackNow of why we reduce the number of threads if there are priority workers. + int maxDefThreads = (m_priorityWorkers.isEmpty()) ? m_MaxThreads : m_MaxThreads-1; + if (!m_defaultTrackQueue.contains(tio)) { + m_defaultTrackQueue.append(tio); + if (m_pausedWorkers.size() + m_defaultWorkers.size() < maxDefThreads) { + createNewWorker(WorkerType::defaultWorker); + } + } +} + +// This slot is called from the decks and samplers when the track is loaded. +void AnalyzerManager::slotAnalyseTrack(TrackPointer tio) { + analyseTrackNow(tio); +} + + +//slot +void AnalyzerManager::slotUpdateProgress(int workerIdx, struct AnalyzerWorker::progress_info* progressInfo) { + //Updates to wave overview and player status text comes from a signal emited from the track by calling setAnalyzerProgress. + progressInfo->current_track->setAnalyzerProgress(progressInfo->track_progress); + //These update the Analysis feature and analysis view. + emit(trackProgress(workerIdx, progressInfo->track_progress / 10)); + if (progressInfo->track_progress == 1000) { + //Right now no one is listening to trackDone, but it's here just in case. + emit(trackDone(progressInfo->current_track)); + //Report that a track analysis has finished, and how many are still remaining. + emit(trackFinished(m_defaultWorkers.size() + m_defaultTrackQueue.size() - 1)); + } + //TODO: Which is the consequence of not calling reset? + progressInfo->current_track.reset(); + progressInfo->sema.release(); +} + +void AnalyzerManager::slotNextTrack(AnalyzerWorker* worker) { + //TODO: The old scan checked in isLoadedTrackWaiting for pTrack->getAnalyzerProgress() + // and either tried to load a previuos scan, or discarded the track if it had already been + // analyzed. I don't fully understand the scenario and I am not doing that right now. + + //This is used when the maxThreads change. Extra workers are paused until active workers end. + //Then, those are terminated and the paused workers are resumed. + TrackPointer track; + AnalyzerWorker* forepaused=nullptr; + foreach(AnalyzerWorker* theworker, m_pausedWorkers) { + if (theworker->isPriorized()) { forepaused=theworker; break; } + } + if (!forepaused) { + if (worker->isPriorized()) { + while (!track && !m_prioTrackQueue.isEmpty()) { + track = m_prioTrackQueue.dequeue(); + } + } + else { + if (m_defaultWorkers.size() + m_pausedWorkers.size() <= m_MaxThreads) { + //The while loop is done in the event that the track which was added to the queue is no + //longer available. + while (!track && !m_defaultTrackQueue.isEmpty()) { + track = m_defaultTrackQueue.dequeue(); + } + } + } + } + if (track) { + worker->nextTrack(track); + } + else { + worker->endProcess(); + //Removing from active lists, so that "isActive" can return the correct value. + m_defaultWorkers.removeAll(worker); + m_priorityWorkers.removeAll(worker); + m_endingWorkers.append(worker); + + if (forepaused) { + forepaused->resume(); + m_pausedWorkers.removeOne(forepaused); + m_priorityWorkers.append(forepaused); + } + else if (!m_pausedWorkers.isEmpty()) { + AnalyzerWorker* otherworker = m_pausedWorkers.first(); + otherworker->resume(); + m_pausedWorkers.removeOne(otherworker); + m_defaultWorkers.append(otherworker); + if (m_priorityWorkers.isEmpty() && !m_pausedWorkers.isEmpty()) { + //Once the priority workers have ended, restart the extra paused default worker. + otherworker = m_pausedWorkers.first(); + otherworker->resume(); + m_pausedWorkers.removeOne(otherworker); + m_defaultWorkers.append(otherworker); + } + } + } +} +void AnalyzerManager::slotWorkerFinished(AnalyzerWorker* worker) { + m_endingWorkers.removeAll(worker); + m_defaultWorkers.removeAll(worker); + m_priorityWorkers.removeAll(worker); + m_pausedWorkers.removeAll(worker); + if (!isDefaultQueueActive()) { + emit(queueEmpty()); + } +} +void AnalyzerManager::slotPaused(AnalyzerWorker* worker) { + //No useful code to execute right now. + Q_UNUSED(worker); +} +void AnalyzerManager::slotErrorString(QString errMsg) { + //TODO: This is currently unused. + qWarning() << "Testing with :" << errMsg; +} + + +void AnalyzerManager::slotMaxThreadsChanged(int threads) { + //See notes on analyseTrackNow of why we reduce the number of threads if there are priority workers. + int maxDefThreads = (m_priorityWorkers.isEmpty()) ? threads : threads-1; + // If it is Active, adapt the amount of workers. If it is not active, it will just update the variable. + if (threads < m_MaxThreads) { + //Pause workers + while (!m_defaultWorkers.isEmpty() + && m_priorityWorkers.size() + m_defaultWorkers.size() > maxDefThreads) { + AnalyzerWorker * backwork = m_defaultWorkers.first(); + backwork->pause(); + //Ideally i would have done this on the slotPaused slot, but then i cannot + //ensure i won't call pause twice for the same worker. + m_pausedWorkers.append(backwork); + m_defaultWorkers.removeAll(backwork); + } + while (m_priorityWorkers.size() > threads) { + AnalyzerWorker * backwork = m_priorityWorkers.first(); + backwork->pause(); + //Ideally i would have done this on the slotPaused slot, but then i cannot + //ensure i won't call pause twice for the same worker. + m_pausedWorkers.append(backwork); + m_priorityWorkers.removeAll(backwork); + } + } + else { + //resume workers + int pendingworkers=threads-m_MaxThreads; + foreach(AnalyzerWorker* worker, m_pausedWorkers) { + if (worker->isPriorized() && pendingworkers > 0) { + worker->resume(); + m_pausedWorkers.removeOne(worker); + m_priorityWorkers.append(worker); + --pendingworkers; + } + } + if (!m_priorityWorkers.isEmpty() && pendingworkers > 0) { + pendingworkers--; + } + foreach(AnalyzerWorker* worker, m_pausedWorkers) { + if (!worker->isPriorized() && pendingworkers > 0) { + worker->resume(); + m_pausedWorkers.removeOne(worker); + m_defaultWorkers.append(worker); + --pendingworkers; + } + } + //Create new workers, if tracks in queue. + pendingworkers = math_min(pendingworkers,m_defaultTrackQueue.size()); + for ( ;pendingworkers > 0; --pendingworkers) { + createNewWorker(WorkerType::defaultWorker); + } + } + m_MaxThreads=threads; +} + +AnalyzerWorker* AnalyzerManager::createNewWorker(WorkerType wtype) { + bool priorized = (wtype == WorkerType::priorityWorker); + QThread* thread = new QThread(); + AnalyzerWorker* worker = new AnalyzerWorker(m_pConfig, m_pDbConnectionPool, ++m_nextWorkerId, priorized); + worker->moveToThread(thread); + //Auto startup and auto cleanup of worker and thread. + connect(thread, SIGNAL(started()), worker, SLOT(slotProcess())); + connect(worker, SIGNAL(finished()), thread, SLOT(quit())); + connect(worker, SIGNAL(finished()), worker, SLOT(deleteLater())); + connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); + //Connect with manager. + connect(worker, SIGNAL(updateProgress(int, struct AnalyzerWorker::progress_info*)), this, SLOT(slotUpdateProgress(int, struct AnalyzerWorker::progress_info*))); + connect(worker, SIGNAL(waitingForNextTrack(AnalyzerWorker*)), this, SLOT(slotNextTrack(AnalyzerWorker*))); + connect(worker, SIGNAL(paused(AnalyzerWorker*)), this, SLOT(slotPaused(AnalyzerWorker*))); + connect(worker, SIGNAL(workerFinished(AnalyzerWorker*)), this, SLOT(slotWorkerFinished(AnalyzerWorker*))); + connect(worker, SIGNAL(error(QString)), this, SLOT(slotErrorString(QString))); + thread->start(QThread::LowPriority); + if (priorized) { + m_priorityWorkers.append(worker); + } + else { + m_defaultWorkers.append(worker); + } + return worker; +} + + + diff --git a/src/analyzer/analyzermanager.h b/src/analyzer/analyzermanager.h new file mode 100644 index 000000000000..f829a938d0bc --- /dev/null +++ b/src/analyzer/analyzermanager.h @@ -0,0 +1,106 @@ +#ifndef ANALYZER_ANALYZERMANAGER_H +#define ANALYZER_ANALYZERMANAGER_H + +#include +#include + +#include + +#include "analyzer/analyzerworker.h" +#include "preferences/usersettings.h" +#include "track/track.h" + +class TrackCollection; + +/* AnalyzerManager class +* Manages the task of analysing multiple tracks. Setups a maximum amount of workers +* and provides the tracks to analyze. It also sends the signals to other parts of Mixxx. +*/ +class AnalyzerManager : public QObject { + Q_OBJECT + +protected: +enum class WorkerType { + defaultWorker, + priorityWorker +}; + +public: + //There should exist only one AnalyzerManager in order to control the amount of threads executing. + AnalyzerManager(UserSettingsPointer pConfig, + mixxx::DbConnectionPoolPtr pDbConnectionPool); + virtual ~AnalyzerManager(); + + //Tell the analyzers of the default queue to stop. If shutdown is true. stop also the priority analyzers. + void stop(bool shutdown); + //Add a track to be analyzed with a priority worker. (Like those required by loading a track into a player). + //This method might need to be protected an called only via the slotAnalyzeTrack slot. + void analyseTrackNow(TrackPointer tio); + //Add a track to be analyzed by the default queue. + void queueAnalyseTrack(TrackPointer tio); + //Check if there is any default worker or priority worker active, paused or a track in queue + bool isActive(); + //Check if there is any default worker active, paused or track in queue + bool isDefaultQueueActive(); + + +public slots: + // This slot is called from the decks and samplers when the track is loaded. + void slotAnalyseTrack(TrackPointer tio); + // This slot is called from the workers to indicate progress. + void slotUpdateProgress(int, struct AnalyzerWorker::progress_info*); + // This slot is called from the workers when they need a new track to process. + void slotNextTrack(AnalyzerWorker*); + // This slot is called from the workers to inform that they reached the paused status. + void slotPaused(AnalyzerWorker*); + // This slot is called from the workers to inform that they are ending themselves. + void slotWorkerFinished(AnalyzerWorker*); + // This slot is intended to receive textual messages. It us unused right now. + void slotErrorString(QString); + // This slot is called from the preferences dialog to update the max value. It will stop extra threads if running. + void slotMaxThreadsChanged(int threads); + +signals: + //This signal is emited to inform other UI elements about the analysis progress. + //Note: the foreground analyzers don't listen to this, but instead they listen to + //a signal emited by the track object itself. + void trackProgress(int worker, int progress); + //This signal is emited to indicate that such track is done. Not really used right now. + void trackDone(TrackPointer track); + //This sitnal is emited to indicate that a track has finished. The size indicates how many + //tracks remain to be scanned. It is currently used by the AnalysisFeature and DlgAnalisys + //to show the track progression. + void trackFinished(int size); + //Indicates that the default analysis job has finished (I.e. all tracks queued on the default queue have been + // analyzed). It is used for the UI to refresh the text and buttons. + void queueEmpty(); +private: + //Method that creates a worker, assigns it to a new thread and the correct list, and starts + //the thread with low priority. + AnalyzerWorker* createNewWorker(WorkerType wtype); + + mixxx::DbConnectionPoolPtr m_pDbConnectionPool; + UserSettingsPointer m_pConfig; + // Autoincremented ID to use as an identifier for each worker. + int m_nextWorkerId; + // Max number of threads to be active analyzing at a time including both, default and priority analysis + int m_MaxThreads; + // TODO: We do a "contains" over these queues before adding a new track to them. + // The more tracks that we add to the queue, the slower this check is. + // No UI response is shown until all tracks are queued. + // The processing queue for the analysis feature of the library. + QQueue m_defaultTrackQueue; + // The processing queue for the analysis of tracks loaded into players. + QQueue m_prioTrackQueue; + + //List of default workers (excluding the paused ones). + QList m_defaultWorkers; + //List of priority workers (excluding the paused ones). + QList m_priorityWorkers; + //List of workers that are currently paused (priority workers are only paused if it was required from reducing the maxThreads) + QList m_pausedWorkers; + //This list is used mostly so that isActive() can return the correct value + QList m_endingWorkers; +}; + +#endif /* ANALYZER_ANALYZERMANAGER_H */ diff --git a/src/analyzer/analyzerqueue.cpp b/src/analyzer/analyzerqueue.cpp index f574f79cd9ea..00105c63ce5f 100644 --- a/src/analyzer/analyzerqueue.cpp +++ b/src/analyzer/analyzerqueue.cpp @@ -44,6 +44,7 @@ QAtomicInt s_instanceCounter(0); } // anonymous namespace +//DEPRECATED. Now we use AnalyzerManager and AnalyzerWorkers. AnalyzerQueue::AnalyzerQueue( mixxx::DbConnectionPoolPtr pDbConnectionPool, const UserSettingsPointer& pConfig, diff --git a/src/analyzer/analyzerqueue.h b/src/analyzer/analyzerqueue.h index 41bd403b85e5..46641d833f4c 100644 --- a/src/analyzer/analyzerqueue.h +++ b/src/analyzer/analyzerqueue.h @@ -18,6 +18,7 @@ class Analyzer; class AnalysisDao; +//DEPRECATED. Now we use AnalyzerManager and AnalyzerWorkers. class AnalyzerQueue : public QThread { Q_OBJECT diff --git a/src/analyzer/analyzerwaveform.cpp b/src/analyzer/analyzerwaveform.cpp index eb90ba162742..804b780314ad 100644 --- a/src/analyzer/analyzerwaveform.cpp +++ b/src/analyzer/analyzerwaveform.cpp @@ -16,8 +16,10 @@ mixxx::Logger kLogger("AnalyzerWaveform"); } // anonymous AnalyzerWaveform::AnalyzerWaveform( - AnalysisDao* pAnalysisDao) : + AnalysisDao* pAnalysisDao, + Mode mode) : m_pAnalysisDao(pAnalysisDao), + m_mode(mode), m_skipProcessing(false), m_waveformData(nullptr), m_waveformSummaryData(nullptr), @@ -92,6 +94,11 @@ bool AnalyzerWaveform::initialize(TrackPointer tio, int sampleRate, int totalSam } bool AnalyzerWaveform::isDisabledOrLoadStoredSuccess(TrackPointer tio) const { + + if (m_mode == Mode::WithoutWaveform) { + return true; + } + ConstWaveformPointer pTrackWaveform = tio->getWaveform(); ConstWaveformPointer pTrackWaveformSummary = tio->getWaveformSummary(); ConstWaveformPointer pLoadedTrackWaveform; @@ -109,8 +116,9 @@ bool AnalyzerWaveform::isDisabledOrLoadStoredSuccess(TrackPointer tio) const { while (it.hasNext()) { const AnalysisDao::AnalysisInfo& analysis = it.next(); WaveformFactory::VersionClass vc; - - if (analysis.type == AnalysisDao::TYPE_WAVEFORM) { + if (analysis.data.size() == 0 ) { + m_pAnalysisDao->deleteAnalysis(analysis.analysisId); + } else if (analysis.type == AnalysisDao::TYPE_WAVEFORM) { vc = WaveformFactory::waveformVersionToVersionClass(analysis.version); if (missingWaveform && vc == WaveformFactory::VC_USE) { pLoadedTrackWaveform = ConstWaveformPointer( @@ -120,7 +128,7 @@ bool AnalyzerWaveform::isDisabledOrLoadStoredSuccess(TrackPointer tio) const { // remove all other Analysis except that one we should keep m_pAnalysisDao->deleteAnalysis(analysis.analysisId); } - } if (analysis.type == AnalysisDao::TYPE_WAVESUMMARY) { + } else if (analysis.type == AnalysisDao::TYPE_WAVESUMMARY) { vc = WaveformFactory::waveformSummaryVersionToVersionClass(analysis.version); if (missingWavesummary && vc == WaveformFactory::VC_USE) { pLoadedTrackWaveformSummary = ConstWaveformPointer( diff --git a/src/analyzer/analyzerwaveform.h b/src/analyzer/analyzerwaveform.h index 085194707da1..fe33e54fe1cb 100644 --- a/src/analyzer/analyzerwaveform.h +++ b/src/analyzer/analyzerwaveform.h @@ -136,8 +136,13 @@ struct WaveformStride { class AnalyzerWaveform : public Analyzer { public: + enum class Mode { + Default, + WithoutWaveform, + }; + explicit AnalyzerWaveform( - AnalysisDao* pAnalysisDao); + AnalysisDao* pAnalysisDao, Mode mode); ~AnalyzerWaveform() override; bool initialize(TrackPointer tio, int sampleRate, int totalSamples) override; @@ -155,6 +160,7 @@ class AnalyzerWaveform : public Analyzer { void storeIfGreater(float* pDest, float source); AnalysisDao* m_pAnalysisDao; + Mode m_mode; bool m_skipProcessing; diff --git a/src/analyzer/analyzerworker.cpp b/src/analyzer/analyzerworker.cpp new file mode 100644 index 000000000000..62891666fed5 --- /dev/null +++ b/src/analyzer/analyzerworker.cpp @@ -0,0 +1,340 @@ +#include "analyzer/analyzerworker.h" + +#include + +#include +#include + +#ifdef __VAMP__ +#include "analyzer/analyzerbeats.h" +#include "analyzer/analyzerkey.h" +#endif +#include "analyzer/analyzergain.h" +#include "analyzer/analyzerebur128.h" +#include "analyzer/analyzerwaveform.h" +#include "library/dao/analysisdao.h" +#include "engine/engine.h" +#include "sources/soundsourceproxy.h" +#include "sources/audiosourcestereoproxy.h" +#include "util/compatibility.h" +#include "util/db/dbconnectionpooler.h" +#include "util/db/dbconnectionpooled.h" +#include "util/event.h" +#include "util/timer.h" +#include "util/trace.h" +#include "util/logger.h" + +// Measured in 0.1%, +// 0 for no progress during finalize +// 1 to display the text "finalizing" +// 100 for 10% step after finalize +// NOTE: If this is changed, change woverview.cpp slotAnalyzerProgress(). +#define FINALIZE_PROMILLE 1.0 + +namespace { + +mixxx::Logger kLogger("AnalyzerWorker"); +// Analysis is done in blocks. +// We need to use a smaller block size, because on Linux the AnalyzerWorker +// can starve the CPU of its resources, resulting in xruns. A block size +// of 4096 frames per block seems to do fine. +const mixxx::AudioSignal::ChannelCount kAnalysisChannels(mixxx::kEngineChannelCount); +const SINT kAnalysisFramesPerBlock = 4096; +const SINT kAnalysisSamplesPerBlock = + kAnalysisFramesPerBlock * kAnalysisChannels; + +inline +AnalyzerWaveform::Mode getAnalyzerWorkerMode( + const UserSettingsPointer& pConfig) { + if (pConfig->getValue(ConfigKey("[Library]", "EnableWaveformGenerationWithAnalysis"), true)) { + return AnalyzerWaveform::Mode::Default; + } else { + return AnalyzerWaveform::Mode::WithoutWaveform; + } +} + +} // anonymous namespace + + + +// --- CONSTRUCTOR --- +AnalyzerWorker::AnalyzerWorker(UserSettingsPointer pConfig, + mixxx::DbConnectionPoolPtr pDbConnectionPool, int workerIdx, bool priorized) : + m_pConfig(pConfig), + m_pAnalyzers(), + m_priorizedJob(priorized), + m_workerIdx(workerIdx), + m_sampleBuffer(kAnalysisSamplesPerBlock), + m_exit(false), + m_pauseRequested(false), + m_qm(), + m_qwait(), + m_pDbConnectionPool(std::move(pDbConnectionPool)) { + + m_pAnalysisDao = std::make_unique(m_pConfig); + createAnalyzers(); + +} + +// --- DESTRUCTOR --- +AnalyzerWorker::~AnalyzerWorker() { + kLogger.debug() << "Ending AnalyzerWorker"; + // free resources + m_progressInfo.sema.release(); +} + +void AnalyzerWorker::nextTrack(TrackPointer newTrack) { + QMutexLocker locker(&m_qm); + m_currentTrack = newTrack; + m_qwait.wakeAll(); +} +void AnalyzerWorker::pause() { + m_pauseRequested = true; +} +void AnalyzerWorker::resume() { + QMutexLocker locker(&m_qm); + m_qwait.wakeAll(); +} + +void AnalyzerWorker::endProcess() { + QMutexLocker locker(&m_qm); + m_exit = true; + m_qwait.wakeAll(); +} + +// This is called from the AnalyzerWorker thread +bool AnalyzerWorker::doAnalysis(TrackPointer pTrack, mixxx::AudioSourcePointer pAudioSource) { + + QTime progressUpdateInhibitTimer; + progressUpdateInhibitTimer.start(); // Inhibit Updates for 60 milliseconds + + mixxx::AudioSourceStereoProxy audioSourceProxy( + pAudioSource, + kAnalysisFramesPerBlock); + DEBUG_ASSERT(audioSourceProxy.channelCount() == kAnalysisChannels); + + mixxx::IndexRange remainingFrames = pAudioSource->frameIndexRange(); + bool dieflag = false; + bool cancelled = false; + + kLogger.debug() << "Analyzing" << pTrack->getTitle() << pTrack->getLocation(); + while (!dieflag && !remainingFrames.empty()) { + ScopedTimer t("AnalyzerWorker::doAnalysis block"); + + const auto inputFrameIndexRange = + remainingFrames.splitAndShrinkFront( + math_min(kAnalysisFramesPerBlock, remainingFrames.length())); + DEBUG_ASSERT(!inputFrameIndexRange.empty()); + const auto readableSampleFrames = + audioSourceProxy.readSampleFrames( + mixxx::WritableSampleFrames( + inputFrameIndexRange, + mixxx::SampleBuffer::WritableSlice(m_sampleBuffer))); + // To compare apples to apples, let's only look at blocks that are + // the full block size. + if (readableSampleFrames.frameLength() == kAnalysisFramesPerBlock) { + // Complete analysis block of audio samples has been read. + for (auto const& pAnalyzer: m_pAnalyzers) { + pAnalyzer->process( + readableSampleFrames.readableData(), + readableSampleFrames.readableLength()); + } + } else { + // Partial analysis block of audio samples has been read. + // This should only happen at the end of an audio stream, + // otherwise a decoding error must have occurred. + if (!remainingFrames.empty()) { + // EOF not reached -> Maybe a corrupt file? + kLogger.warning() + << "Aborting analysis after failed to read sample data from " + << pTrack->getLocation() + << ": expected frames =" << inputFrameIndexRange + << ", actual frames =" << readableSampleFrames.frameIndexRange(); + dieflag = true; // abort + cancelled = false; // completed, no retry + } + } + + // emit progress updates + // During the doAnalysis function it goes only to 100% - FINALIZE_PERCENT + // because the finalize functions will take also some time + //fp div here prevents insane signed overflow + const double frameProgress = + double(pAudioSource->frameLength() - remainingFrames.length()) / + double(pAudioSource->frameLength()); + int progressPromille = frameProgress * (1000.0 - FINALIZE_PROMILLE); + + if (m_progressInfo.track_progress != progressPromille && + progressUpdateInhibitTimer.elapsed() > 60) { + // Inhibit Updates for 60 milliseconds + emitUpdateProgress(progressPromille); + progressUpdateInhibitTimer.start(); + } + + // When a priority analysis comes in, we pause this working thread until one prioritized + // worker finishes. Once it finishes, this worker will get resumed. + if (m_pauseRequested.fetchAndStoreAcquire(false)) { + QMutexLocker locker(&m_qm); + emit(paused(this)); + m_qwait.wait(&m_qm); + } + + if (m_exit) { + dieflag = true; + cancelled = true; + } + + // Ignore blocks in which we decided to bail for stats purposes. + if (dieflag || cancelled) { + t.cancel(); + } + } + + return !cancelled; //don't return !dieflag or we might reanalyze over and over +} + +//Called automatically by the owning thread to start the process (Configured to do so by AnalyzerManager) +void AnalyzerWorker::slotProcess() { + QThread::currentThread()->setObjectName(QString("AnalyzerWorker %1").arg(m_workerIdx)); + + + // The thread-local database connection for waveform analysis must not + // be closed before returning from this function. Therefore the + // DbConnectionPooler is defined at this outer function scope, + // independent of whether a database connection will be opened + // or not. + mixxx::DbConnectionPooler dbConnectionPooler; + // m_pAnalysisDao remains null if no analyzer needs database access. + // Currently only waveform analyses makes use of it. + if (m_pAnalysisDao) { + dbConnectionPooler = mixxx::DbConnectionPooler(m_pDbConnectionPool); // move assignment + if (!dbConnectionPooler.isPooling()) { + kLogger.warning() + << "Failed to obtain database connection for analyzer queue thread"; + return; + } + // Obtain and use the newly created database connection within this thread + QSqlDatabase dbConnection = mixxx::DbConnectionPooled(m_pDbConnectionPool); + DEBUG_ASSERT(dbConnection.isOpen()); + m_pAnalysisDao->initialize(dbConnection); + } + + m_progressInfo.sema.release(); + + while (!m_exit) { + //We emit waitingForNextTrack to inform that we're done and we need a new track. + { + QMutexLocker locker(&m_qm); + emit(waitingForNextTrack(this)); + m_qwait.wait(&m_qm); + } + // We recheck m_exit, since it's also the way that the manager indicates that there are no + // more tracks to process. + if (m_exit) { + break; + } + Event::start(QString("AnalyzerWorker %1 process").arg(m_workerIdx)); + Trace trace("AnalyzerWorker analyzing track"); + + // Get the audio + mixxx::AudioSource::OpenParams openParams; + openParams.setChannelCount(kAnalysisChannels); + auto pAudioSource = SoundSourceProxy(m_currentTrack).openAudioSource(openParams); + if (!pAudioSource) { + kLogger.warning() + << "Failed to open file for analyzing: " + << m_currentTrack->getLocation() + << " " << *pAudioSource; + //TODO: maybe emit error("Failed to bblablalba"); + continue; + } + + bool processTrack = false; + for (auto const& pAnalyzer: m_pAnalyzers) { + // Make sure not to short-circuit initialize(...) + if (pAnalyzer->initialize(m_currentTrack, + pAudioSource->sampleRate(), + pAudioSource->frameLength() * kAnalysisChannels)) { + processTrack = true; + } + } + + if (processTrack) { + emitUpdateProgress(0); + bool completed = doAnalysis(m_currentTrack, pAudioSource); + // We can end doAnalysis because of two causes: The analysis has finished + // or the analysis has been interrupted. + if (!completed) { + // This track was cancelled + for (auto const& pAnalyzer: m_pAnalyzers) { + pAnalyzer->cleanup(m_currentTrack); + } + emitUpdateProgress(0); + } else { + // 100% - FINALIZE_PERCENT finished + emitUpdateProgress(1000 - FINALIZE_PROMILLE); + for (auto const& pAnalyzer: m_pAnalyzers) { + pAnalyzer->finalize(m_currentTrack); + } + emitUpdateProgress(1000); // 100% + } + } else { + emitUpdateProgress(1000); // 100% + kLogger.debug() << "Skipping track analysis because no analyzer initialized."; + } + Event::end(QString("AnalyzerWorker %1 process").arg(m_workerIdx)); + } + + if (m_pAnalysisDao) { + // Invalidate reference to the thread-local database connection + // that will be closed soon. Not necessary, just in case ;) + m_pAnalysisDao->initialize(QSqlDatabase()); + } + + emit(workerFinished(this)); + emit(finished()); +} + +// This is called from the AnalyzerWorker thread +void AnalyzerWorker::emitUpdateProgress(int progress) { + if (!m_exit) { + // TODO: Since we are using a timer of 60 milliseconds in doAnalysis, we probably don't + // need the semaphore. On the other hand, the semaphore would be useful if it was the UI + // the one that requested us about updates. Then, the semaphore would prevent updates + // until the UI has rendered the last update. As it is, it only prevents sending another + // update if the analyzermanager slot hasn't read the update. (not the UI slot). + // --------- + // First tryAcquire will have always success because sema is initialized with on + // The following tries will success if the previous signal was processed in the GUI Thread + // This prevent the AnalysisQueue from filling up the GUI Thread event Queue + // 100 % is emitted in any case + if (progress < 1000 - FINALIZE_PROMILLE && progress > 0) { + // Signals during processing are not required in any case + if (!m_progressInfo.sema.tryAcquire()) { + return; + } + } else { + m_progressInfo.sema.acquire(); + } + m_progressInfo.current_track = m_currentTrack; + m_progressInfo.track_progress = progress; + emit(updateProgress(m_workerIdx, &m_progressInfo)); + } +} + +void AnalyzerWorker::createAnalyzers() { + AnalyzerWaveform::Mode mode; + if (m_priorizedJob) { + mode = AnalyzerWaveform::Mode::Default; + } else { + mode = getAnalyzerWorkerMode(m_pConfig); + } + + m_pAnalyzers.push_back(std::make_unique(m_pAnalysisDao.get(), mode)); + m_pAnalyzers.push_back(std::make_unique(m_pConfig)); + m_pAnalyzers.push_back(std::make_unique(m_pConfig)); +#ifdef __VAMP__ + m_pAnalyzers.push_back(std::make_unique(m_pConfig, !m_priorizedJob)); + m_pAnalyzers.push_back(std::make_unique(m_pConfig)); +#endif +} diff --git a/src/analyzer/analyzerworker.h b/src/analyzer/analyzerworker.h new file mode 100644 index 000000000000..2d134ee19d97 --- /dev/null +++ b/src/analyzer/analyzerworker.h @@ -0,0 +1,111 @@ +#ifndef ANALYZER_ANALYZERWORKER_H +#define ANALYZER_ANALYZERWORKER_H + +#include +#include +#include + +#include + +#include "sources/audiosource.h" +#include "track/track.h" +#include "util/samplebuffer.h" +#include "preferences/usersettings.h" +#include "util/db/dbconnectionpool.h" + +class Analyzer; +class QThread; +class AnalysisDao; + +/* Worker class. +* It represents a job that runs on a thread, analyzing tracks until no more tracks need to be analyzed. +*/ +class AnalyzerWorker : public QObject { + Q_OBJECT + +public: + //Information of the analysis used with the updateProgress signal. + struct progress_info { + //Worker identifier. This is used to differentiate between updateProgress signals, and it is + //what lets the DlgAnalysis class to differentiate and show the different percentages. + int worker; + //Track being analyzed. This is currently used in the AnalyzerManager. + TrackPointer current_track; + //track progress in steps of 0.1 % + int track_progress; + //Semaphore to avoid exccesive signaling. + QSemaphore sema; + }; + + // Constructor. If it is a priorized job, the analyzers are configured differently. + // Call Qthread->start() when you are ready for the worker to start. + AnalyzerWorker(UserSettingsPointer pConfig, + mixxx::DbConnectionPoolPtr pDbConnectionPool, + int workerIdx, bool priorized); + virtual ~AnalyzerWorker(); + + //Called by the manager as a response to the waitingForNextTrack signal. and ONLY then. + void nextTrack(TrackPointer newTrack); + //called to pause this worker (The call is not blocking. The worker will wait on a qwaitcondition) + void pause(); + //resumes from a previous call to pause (The call is not blocking) + void resume(); + //Tells this worker to end. (the call is not blocking. Sets a variable for the worker to end) + // An updateProgress signal with progress 0 will be emited and also the finished signal. + // The AnalyzerManager connects it so that it will delete itself and the Qthread. + void endProcess(); + // Is this a priorized worker? + bool isPriorized(); + +public slots: + //starts the analysis job. + //Called automatically by the owning thread to start the process (Configured to do so by AnalyzerManager) + void slotProcess(); + +signals: + //Signal that informs about the progress of the analysis. + void updateProgress(int workerIdx, struct AnalyzerWorker::progress_info*); + //Signal emited to the manager in order to receive a new track. The manager should call nextTrack(); + void waitingForNextTrack(AnalyzerWorker* worker); + //Signal emited when the worker has effectively paused + void paused(AnalyzerWorker* worker); + //Signal emited when the worker ends and deletes itself. + void workerFinished(AnalyzerWorker* worker); + //Signal emited when this worker ends + void finished(); + //Currently this signal is unused. It might be useful in the future. + void error(QString err); + +private: + //Analyze one track. + bool doAnalysis(TrackPointer tio, mixxx::AudioSourcePointer pAudioSource); + //helper function to emit the updateProgress signal + void emitUpdateProgress(int progress); + //helper function to create the analyzers. + void createAnalyzers(); + + UserSettingsPointer m_pConfig; + mixxx::DbConnectionPoolPtr m_pDbConnectionPool; + + std::unique_ptr m_pAnalysisDao; + + typedef std::unique_ptr AnalyzerPtr; + std::vector m_pAnalyzers; + bool m_priorizedJob; + int m_workerIdx; + mixxx::SampleBuffer m_sampleBuffer; + TrackPointer m_currentTrack; + + QAtomicInt m_exit; + QAtomicInt m_pauseRequested; + QMutex m_qm; + QWaitCondition m_qwait; + struct progress_info m_progressInfo; + +}; + +inline bool AnalyzerWorker::isPriorized() { + return m_priorizedJob; +} + +#endif /* ANALYZER_ANALYZERWORKER_H */ diff --git a/src/engine/sidechain/shoutconnection.cpp b/src/engine/sidechain/shoutconnection.cpp index 3c4d463cd18d..1e80cf6345ce 100644 --- a/src/engine/sidechain/shoutconnection.cpp +++ b/src/engine/sidechain/shoutconnection.cpp @@ -325,7 +325,7 @@ void ShoutConnection::updateFromPreferences() { } if (shout_set_format(m_pShout, format) != SHOUTERR_SUCCESS) { - errorDialog("Error setting streaming format!", shout_get_error(m_pShout)); + errorDialog(tr("Error setting streaming format!"), shout_get_error(m_pShout)); return; } diff --git a/src/library/analysisfeature.cpp b/src/library/analysisfeature.cpp index 8d47b3ad2a05..bbe16f1639fb 100644 --- a/src/library/analysisfeature.cpp +++ b/src/library/analysisfeature.cpp @@ -13,7 +13,7 @@ #include "library/dlganalysis.h" #include "widget/wlibrary.h" #include "controllers/keyboard/keyboardeventfilter.h" -#include "analyzer/analyzerqueue.h" +#include "analyzer/analyzermanager.h" #include "sources/soundsourceproxy.h" #include "util/dnd.h" #include "util/debug.h" @@ -22,13 +22,13 @@ const QString AnalysisFeature::m_sAnalysisViewName = QString("Analysis"); AnalysisFeature::AnalysisFeature(Library* parent, UserSettingsPointer pConfig, - TrackCollection* pTrackCollection) : + TrackCollection* pTrackCollection, + AnalyzerManager* pAnalyzerManager) : LibraryFeature(parent), m_pConfig(pConfig), m_pDbConnectionPool(parent->dbConnectionPool()), m_pTrackCollection(pTrackCollection), - m_pAnalyzerQueue(nullptr), - m_iOldBpmEnabled(0), + m_pAnalyzerManager(pAnalyzerManager), m_analysisTitleName(tr("Analyze")), m_pAnalysisView(nullptr) { setTitleDefault(); @@ -84,10 +84,20 @@ void AnalysisFeature::bindWidget(WLibrary* libraryWidget, connect(this, SIGNAL(trackAnalysisStarted(int)), m_pAnalysisView, SLOT(trackAnalysisStarted(int))); + connect(m_pAnalyzerManager, SIGNAL(trackProgress(int, int)), + m_pAnalysisView, SLOT(trackAnalysisProgress(int, int))); + connect(m_pAnalyzerManager, SIGNAL(trackFinished(int)), + this, SLOT(slotProgressUpdate(int))); + connect(m_pAnalyzerManager, SIGNAL(trackFinished(int)), + m_pAnalysisView, SLOT(trackAnalysisFinished(int))); + + connect(m_pAnalyzerManager, SIGNAL(queueEmpty()), + this, SLOT(cleanupAnalyzer())); + m_pAnalysisView->installEventFilter(keyboard); // Let the DlgAnalysis know whether or not analysis is active. - bool bAnalysisActive = m_pAnalyzerQueue != NULL; + bool bAnalysisActive = m_pAnalyzerManager->isDefaultQueueActive(); emit(analysisActive(bAnalysisActive)); libraryWidget->registerView(m_sAnalysisViewName, m_pAnalysisView); @@ -112,48 +122,16 @@ void AnalysisFeature::activate() { emit(enableCoverArtDisplay(true)); } -namespace { - inline - AnalyzerQueue::Mode getAnalyzerQueueMode( - const UserSettingsPointer& pConfig) { - if (pConfig->getValue(ConfigKey("[Library]", "EnableWaveformGenerationWithAnalysis"), true)) { - return AnalyzerQueue::Mode::Default; - } else { - return AnalyzerQueue::Mode::WithoutWaveform; - } - } -} // anonymous namespace void AnalysisFeature::analyzeTracks(QList trackIds) { - if (m_pAnalyzerQueue == NULL) { - // Save the old BPM detection prefs setting (on or off) - m_iOldBpmEnabled = m_pConfig->getValueString(ConfigKey("[BPM]","BPMDetectionEnabled")).toInt(); - // Force BPM detection to be on. - m_pConfig->set(ConfigKey("[BPM]","BPMDetectionEnabled"), ConfigValue(1)); - // Note: this sucks... we should refactor the prefs/analyzer to fix this hacky bit ^^^^. - - m_pAnalyzerQueue = new AnalyzerQueue( - m_pDbConnectionPool, - m_pConfig, - getAnalyzerQueueMode(m_pConfig)); - - connect(m_pAnalyzerQueue, SIGNAL(trackProgress(int)), - m_pAnalysisView, SLOT(trackAnalysisProgress(int))); - connect(m_pAnalyzerQueue, SIGNAL(trackFinished(int)), - this, SLOT(slotProgressUpdate(int))); - connect(m_pAnalyzerQueue, SIGNAL(trackFinished(int)), - m_pAnalysisView, SLOT(trackAnalysisFinished(int))); - - connect(m_pAnalyzerQueue, SIGNAL(queueEmpty()), - this, SLOT(cleanupAnalyzer())); - emit(analysisActive(true)); - } + + emit(analysisActive(true)); for (const auto& trackId: trackIds) { TrackPointer pTrack = m_pTrackCollection->getTrackDAO().getTrack(trackId); if (pTrack) { //qDebug() << this << "Queueing track for analysis" << pTrack->getLocation(); - m_pAnalyzerQueue->queueAnalyseTrack(pTrack); + m_pAnalyzerManager->queueAnalyseTrack(pTrack); } } if (trackIds.size() > 0) { @@ -165,28 +143,18 @@ void AnalysisFeature::analyzeTracks(QList trackIds) { void AnalysisFeature::slotProgressUpdate(int num_left) { int num_tracks = m_pAnalysisView->getNumTracks(); if (num_left > 0) { - int currentTrack = num_tracks - num_left + 1; + int currentTrack = num_tracks - num_left; setTitleProgress(currentTrack, num_tracks); } } void AnalysisFeature::stopAnalysis() { - //qDebug() << this << "stopAnalysis()"; - if (m_pAnalyzerQueue != NULL) { - m_pAnalyzerQueue->stop(); - } + m_pAnalyzerManager->stop(false); } void AnalysisFeature::cleanupAnalyzer() { setTitleDefault(); emit(analysisActive(false)); - if (m_pAnalyzerQueue != NULL) { - m_pAnalyzerQueue->stop(); - m_pAnalyzerQueue->deleteLater(); - m_pAnalyzerQueue = NULL; - // Restore old BPM detection setting for preferences... - m_pConfig->set(ConfigKey("[BPM]","BPMDetectionEnabled"), ConfigValue(m_iOldBpmEnabled)); - } } bool AnalysisFeature::dropAccept(QList urls, QObject* pSource) { diff --git a/src/library/analysisfeature.h b/src/library/analysisfeature.h index b6b4211f99c3..d4ec4fd84261 100644 --- a/src/library/analysisfeature.h +++ b/src/library/analysisfeature.h @@ -13,21 +13,22 @@ #include #include "library/libraryfeature.h" -#include "library/dlganalysis.h" #include "library/treeitemmodel.h" #include "preferences/usersettings.h" #include "util/db/dbconnectionpool.h" +class DlgAnalysis; class Library; class TrackCollection; -class AnalyzerQueue; +class AnalyzerManager; class AnalysisFeature : public LibraryFeature { Q_OBJECT public: AnalysisFeature(Library* parent, UserSettingsPointer pConfig, - TrackCollection* pTrackCollection); + TrackCollection* pTrackCollection, + AnalyzerManager* pAnalyzerManager); virtual ~AnalysisFeature(); QVariant title(); @@ -67,9 +68,7 @@ class AnalysisFeature : public LibraryFeature { UserSettingsPointer m_pConfig; mixxx::DbConnectionPoolPtr m_pDbConnectionPool; TrackCollection* m_pTrackCollection; - AnalyzerQueue* m_pAnalyzerQueue; - // Used to temporarily enable BPM detection in the prefs before we analyse - int m_iOldBpmEnabled; + AnalyzerManager* m_pAnalyzerManager; // The title returned by title() QVariant m_Title; TreeItemModel m_childModel; diff --git a/src/library/dlganalysis.cpp b/src/library/dlganalysis.cpp index 52e27740a348..fbdb639eb46b 100644 --- a/src/library/dlganalysis.cpp +++ b/src/library/dlganalysis.cpp @@ -15,6 +15,7 @@ DlgAnalysis::DlgAnalysis(QWidget* parent, m_pConfig(pConfig), m_pTrackCollection(pTrackCollection), m_bAnalysisActive(false), + m_percentages(), m_tracksInQueue(0), m_currentTrack(0) { setupUi(this); @@ -150,6 +151,7 @@ void DlgAnalysis::analysisActive(bool bActive) { pushButtonAnalyze->setText(tr("Analyze")); labelProgress->setText(""); labelProgress->setEnabled(false); + m_percentages.clear(); } } @@ -157,18 +159,34 @@ void DlgAnalysis::analysisActive(bool bActive) { void DlgAnalysis::trackAnalysisFinished(int size) { qDebug() << "Analysis finished" << size << "tracks left"; if (size > 0) { - m_currentTrack = m_tracksInQueue - size + 1; + m_currentTrack = m_tracksInQueue - size; } } // slot -void DlgAnalysis::trackAnalysisProgress(int progress) { +void DlgAnalysis::trackAnalysisProgress(int worker, int progress) { if (m_bAnalysisActive) { + m_percentages[worker] = progress; + //This is a bit cumbersome, yes, I just avoided to change the translating text. + QString perc; + bool add = false; + foreach(int percentage, m_percentages) { + if (add) { + perc+= "% "; + } + perc += QString::number(percentage); + add = true; + } QString text = tr("Analyzing %1/%2 %3%").arg( QString::number(m_currentTrack), QString::number(m_tracksInQueue), - QString::number(progress)); + perc); labelProgress->setText(text); + //This isn't strictly necessary, but it is useful to remove priority (player) worker analysis + //which would accumulate otherwise. Another option is to not send this signal for priority workers. + if (progress == 100) { + m_percentages.remove(worker); + } } } diff --git a/src/library/dlganalysis.h b/src/library/dlganalysis.h index 67c4a676649b..1d5c16ad260b 100644 --- a/src/library/dlganalysis.h +++ b/src/library/dlganalysis.h @@ -40,7 +40,7 @@ class DlgAnalysis : public QWidget, public Ui::DlgAnalysis, public virtual Libra void selectAll(); void analyze(); void trackAnalysisFinished(int size); - void trackAnalysisProgress(int progress); + void trackAnalysisProgress(int worker, int progress); void trackAnalysisStarted(int size); void showRecentSongs(); void showAllSongs(); @@ -62,6 +62,9 @@ class DlgAnalysis : public QWidget, public Ui::DlgAnalysis, public virtual Libra QButtonGroup m_songsButtonGroup; WAnalysisLibraryTableView* m_pAnalysisLibraryTableView; AnalysisLibraryTableModel* m_pAnalysisLibraryTableModel; + //Individual thread percentages. Since we iterate it, it is better + // to provide a consistent order, and that's why it is a QMap + QMap m_percentages; int m_tracksInQueue; int m_currentTrack; }; diff --git a/src/library/library.cpp b/src/library/library.cpp index 17fa3e8b73d7..e825d961e851 100644 --- a/src/library/library.cpp +++ b/src/library/library.cpp @@ -8,6 +8,7 @@ #include "database/mixxxdb.h" +#include "analyzer/analyzermanager.h" #include "mixer/playermanager.h" #include "library/library.h" #include "library/library_preferences.h" @@ -78,6 +79,8 @@ Library::Library( QSqlDatabase dbConnection = mixxx::DbConnectionPooled(m_pDbConnectionPool); + m_pAnalyzerManager = new AnalyzerManager(pConfig,m_pDbConnectionPool); + // TODO(XXX): Add a checkbox in the library preferences for checking // and repairing the database on the next restart of the application. if (pConfig->getValue(kConfigKeyRepairDatabaseOnNextRestart, false)) { @@ -124,7 +127,7 @@ Library::Library( addFeature(browseFeature); addFeature(new RecordingFeature(this, pConfig, m_pTrackCollection, pRecordingManager)); addFeature(new SetlogFeature(this, pConfig, m_pTrackCollection)); - m_pAnalysisFeature = new AnalysisFeature(this, pConfig, m_pTrackCollection); + m_pAnalysisFeature = new AnalysisFeature(this, pConfig, m_pTrackCollection, m_pAnalyzerManager); connect(m_pPlaylistFeature, SIGNAL(analyzeTracks(QList)), m_pAnalysisFeature, SLOT(analyzeTracks(QList))); connect(m_pCrateFeature, SIGNAL(analyzeTracks(QList)), @@ -196,6 +199,8 @@ Library::~Library() { // we never see the TrackCollection's destructor being called... - Albert // Has to be deleted at last because the features holds references of it. delete m_pTrackCollection; + + delete m_pAnalyzerManager; } void Library::bindSidebarWidget(WLibrarySidebar* pSidebarWidget) { diff --git a/src/library/library.h b/src/library/library.h index 88265aeb5262..a6cd6cdac025 100644 --- a/src/library/library.h +++ b/src/library/library.h @@ -35,6 +35,7 @@ class CrateFeature; class LibraryControl; class KeyboardEventFilter; class PlayerManagerInterface; +class AnalyzerManager; class Library: public QObject, public virtual /*implements*/ TrackCacheEvictor { @@ -63,6 +64,10 @@ class Library: public QObject, void addFeature(LibraryFeature* feature); QStringList getDirs(); + AnalyzerManager* getAnalyzerManager() { + return m_pAnalyzerManager; + } + inline int getTrackTableRowHeight() const { return m_iTrackTableRowHeight; } @@ -140,6 +145,7 @@ class Library: public QObject, PlaylistFeature* m_pPlaylistFeature; CrateFeature* m_pCrateFeature; AnalysisFeature* m_pAnalysisFeature; + AnalyzerManager* m_pAnalyzerManager; LibraryScanner m_scanner; QFont m_trackTableFont; int m_iTrackTableRowHeight; diff --git a/src/mixer/basetrackplayer.cpp b/src/mixer/basetrackplayer.cpp index 80d171c90a7f..7add1c675b4a 100644 --- a/src/mixer/basetrackplayer.cpp +++ b/src/mixer/basetrackplayer.cpp @@ -13,7 +13,6 @@ #include "engine/enginemaster.h" #include "track/beatgrid.h" #include "waveform/renderers/waveformwidgetrenderer.h" -#include "analyzer/analyzerqueue.h" #include "util/platform.h" #include "util/sandbox.h" #include "effects/effectsmanager.h" diff --git a/src/mixer/playermanager.cpp b/src/mixer/playermanager.cpp index ea35f38711b1..f678386935df 100644 --- a/src/mixer/playermanager.cpp +++ b/src/mixer/playermanager.cpp @@ -4,7 +4,7 @@ #include -#include "analyzer/analyzerqueue.h" +#include "analyzer/analyzermanager.h" #include "control/controlobject.h" #include "control/controlobject.h" #include "effects/effectsmanager.h" @@ -32,9 +32,9 @@ PlayerManager::PlayerManager(UserSettingsPointer pConfig, m_pSoundManager(pSoundManager), m_pEffectsManager(pEffectsManager), m_pEngine(pEngine), + m_pAnalyzerManager(nullptr), // NOTE(XXX) LegacySkinParser relies on these controls being Controls // and not ControlProxies. - m_pAnalyzerQueue(nullptr), m_pCONumDecks(new ControlObject( ConfigKey("[Master]", "num_decks"), true, true)), m_pCONumSamplers(new ControlObject( @@ -98,9 +98,6 @@ PlayerManager::~PlayerManager() { delete m_pCONumPreviewDecks; delete m_pCONumMicrophones; delete m_pCONumAuxiliaries; - if (m_pAnalyzerQueue) { - delete m_pAnalyzerQueue; - } } void PlayerManager::bindToLibrary(Library* pLibrary) { @@ -112,27 +109,27 @@ void PlayerManager::bindToLibrary(Library* pLibrary) { connect(this, SIGNAL(loadLocationToPlayer(QString, QString)), pLibrary, SLOT(slotLoadLocationToPlayer(QString, QString))); - m_pAnalyzerQueue = new AnalyzerQueue(pLibrary->dbConnectionPool(), m_pConfig); + m_pAnalyzerManager = pLibrary->getAnalyzerManager(); // Connect the player to the analyzer queue so that loaded tracks are // analysed. foreach(Deck* pDeck, m_decks) { connect(pDeck, SIGNAL(newTrackLoaded(TrackPointer)), - m_pAnalyzerQueue, SLOT(slotAnalyseTrack(TrackPointer))); + m_pAnalyzerManager, SLOT(slotAnalyseTrack(TrackPointer))); } // Connect the player to the analyzer queue so that loaded tracks are // analysed. foreach(Sampler* pSampler, m_samplers) { connect(pSampler, SIGNAL(newTrackLoaded(TrackPointer)), - m_pAnalyzerQueue, SLOT(slotAnalyseTrack(TrackPointer))); + m_pAnalyzerManager, SLOT(slotAnalyseTrack(TrackPointer))); } // Connect the player to the analyzer queue so that loaded tracks are // analysed. foreach(PreviewDeck* pPreviewDeck, m_preview_decks) { connect(pPreviewDeck, SIGNAL(newTrackLoaded(TrackPointer)), - m_pAnalyzerQueue, SLOT(slotAnalyseTrack(TrackPointer))); + m_pAnalyzerManager, SLOT(slotAnalyseTrack(TrackPointer))); } } @@ -349,9 +346,9 @@ void PlayerManager::addDeckInner() { connect(pDeck, SIGNAL(noVinylControlInputConfigured()), this, SIGNAL(noVinylControlInputConfigured())); - if (m_pAnalyzerQueue) { + if (m_pAnalyzerManager) { connect(pDeck, SIGNAL(newTrackLoaded(TrackPointer)), - m_pAnalyzerQueue, SLOT(slotAnalyseTrack(TrackPointer))); + m_pAnalyzerManager, SLOT(slotAnalyseTrack(TrackPointer))); } m_players[group] = pDeck; @@ -408,9 +405,9 @@ void PlayerManager::addSamplerInner() { Sampler* pSampler = new Sampler(this, m_pConfig, m_pEngine, m_pEffectsManager, orientation, group); - if (m_pAnalyzerQueue) { + if (m_pAnalyzerManager) { connect(pSampler, SIGNAL(newTrackLoaded(TrackPointer)), - m_pAnalyzerQueue, SLOT(slotAnalyseTrack(TrackPointer))); + m_pAnalyzerManager, SLOT(slotAnalyseTrack(TrackPointer))); } m_players[group] = pSampler; @@ -436,9 +433,9 @@ void PlayerManager::addPreviewDeckInner() { PreviewDeck* pPreviewDeck = new PreviewDeck(this, m_pConfig, m_pEngine, m_pEffectsManager, orientation, group); - if (m_pAnalyzerQueue) { + if (m_pAnalyzerManager) { connect(pPreviewDeck, SIGNAL(newTrackLoaded(TrackPointer)), - m_pAnalyzerQueue, SLOT(slotAnalyseTrack(TrackPointer))); + m_pAnalyzerManager, SLOT(slotAnalyseTrack(TrackPointer))); } m_players[group] = pPreviewDeck; diff --git a/src/mixer/playermanager.h b/src/mixer/playermanager.h index 54cc17708f4f..b3777266fe91 100644 --- a/src/mixer/playermanager.h +++ b/src/mixer/playermanager.h @@ -12,7 +12,7 @@ #include "preferences/usersettings.h" #include "track/track.h" -class AnalyzerQueue; +class AnalyzerManager; class Auxiliary; class BaseTrackPlayer; class ControlObject; @@ -236,7 +236,7 @@ class PlayerManager : public QObject, public PlayerManagerInterface { EffectsManager* m_pEffectsManager; EngineMaster* m_pEngine; SamplerBank* m_pSamplerBank; - AnalyzerQueue* m_pAnalyzerQueue; + AnalyzerManager* m_pAnalyzerManager; ControlObject* m_pCONumDecks; ControlObject* m_pCONumSamplers; ControlObject* m_pCOSamplerBankLoad; diff --git a/src/mixxx.cpp b/src/mixxx.cpp index 7c41dfbfb95c..f4e1efd233fc 100644 --- a/src/mixxx.cpp +++ b/src/mixxx.cpp @@ -24,7 +24,6 @@ #include #include -#include "analyzer/analyzerqueue.h" #include "dialog/dlgabout.h" #include "preferences/dialog/dlgpreferences.h" #include "preferences/dialog/dlgprefeq.h" diff --git a/src/preferences/dialog/dlgpreflibrary.cpp b/src/preferences/dialog/dlgpreflibrary.cpp index 62a7eccebf56..bc4b0d53425d 100644 --- a/src/preferences/dialog/dlgpreflibrary.cpp +++ b/src/preferences/dialog/dlgpreflibrary.cpp @@ -11,6 +11,7 @@ #include "preferences/dialog/dlgpreflibrary.h" #include "library/dlgtrackmetadataexport.h" #include "sources/soundsourceproxy.h" +#include "analyzer/analyzermanager.h" #define MIXXX_ADDONS_URL "http://www.mixxx.org/wiki/doku.php/add-ons" @@ -23,7 +24,8 @@ DlgPrefLibrary::DlgPrefLibrary( m_pConfig(pConfig), m_pLibrary(pLibrary), m_bAddedDirectory(false), - m_iOriginalTrackTableRowHeight(Library::kDefaultRowHeightPx) { + m_iOriginalTrackTableRowHeight(Library::kDefaultRowHeightPx), + m_iOriginalMaxThreads(1) { setupUi(this); connect(this, SIGNAL(requestAddDir(QString)), @@ -62,6 +64,10 @@ DlgPrefLibrary::DlgPrefLibrary( connect(this, SIGNAL(setTrackTableRowHeight(int)), m_pLibrary, SLOT(slotSetTrackTableRowHeight(int))); + AnalyzerManager* analyzerManager = m_pLibrary->getAnalyzerManager(); + connect(this, SIGNAL(setMaxThreads(int)), + analyzerManager, SLOT(slotMaxThreadsChanged(int))); + // TODO(XXX) this string should be extracted from the soundsources QString builtInFormatsStr = "Ogg Vorbis, FLAC, WAVe, AIFF"; #if defined(__MAD__) || defined(__APPLE__) @@ -130,6 +136,20 @@ void DlgPrefLibrary::initializeDirList() { } } +void DlgPrefLibrary::initializeThreadsCombo() { + // save which index was selected + const int selected = cmbMaxThreads->currentIndex(); + // clear and fill model + cmbMaxThreads->clear(); + int cpuMax = QThread::idealThreadCount(); + if (cpuMax < 1) { cpuMax = 8; } + for (int i=1; i <= cpuMax; i++) { + QString displayname = QString::number(i); + cmbMaxThreads->addItem(displayname); + } + cmbMaxThreads->setCurrentIndex(selected); +} + void DlgPrefLibrary::slotExtraPlugins() { QDesktopServices::openUrl(QUrl(MIXXX_ADDONS_URL)); } @@ -146,11 +166,17 @@ void DlgPrefLibrary::slotResetToDefaults() { radioButton_dbclick_top->setChecked(false); radioButton_dbclick_deck->setChecked(true); spinBoxRowHeight->setValue(Library::kDefaultRowHeightPx); + int cpuMax = QThread::idealThreadCount(); + if (cpuMax < 1) { cpuMax = 1; } + //setCurrentIndex is zero based. threads is one based. + cmbMaxThreads->setCurrentIndex(cpuMax-1); + setLibraryFont(QApplication::font()); } void DlgPrefLibrary::slotUpdate() { initializeDirList(); + initializeThreadsCombo(); checkBox_library_scan->setChecked(m_pConfig->getValue( ConfigKey("[Library]","RescanOnStartup"), false)); checkBox_SyncTrackMetadataExport->setChecked(m_pConfig->getValue( @@ -183,6 +209,11 @@ void DlgPrefLibrary::slotUpdate() { m_iOriginalTrackTableRowHeight = m_pLibrary->getTrackTableRowHeight(); spinBoxRowHeight->setValue(m_iOriginalTrackTableRowHeight); setLibraryFont(m_originalTrackTableFont); + + m_iOriginalMaxThreads = m_pConfig->getValue(ConfigKey("[Library]", "MaxAnalysisThreads")); + //setCurrentIndex is zero based. threads is one based. + cmbMaxThreads->setCurrentIndex(m_iOriginalMaxThreads-1); + } void DlgPrefLibrary::slotCancel() { @@ -321,6 +352,15 @@ void DlgPrefLibrary::slotApply() { ConfigValue(rowHeight)); } + //setCurrentIndex is zero based. threads is one based. + int threads = cmbMaxThreads->currentIndex()+1; + if (m_iOriginalMaxThreads != threads && threads > 0) { + m_pConfig->setValue(ConfigKey("[Library]", "MaxAnalysisThreads"), + threads); + emit(setMaxThreads(threads)); + } + + // TODO(rryan): Don't save here. m_pConfig->save(); } diff --git a/src/preferences/dialog/dlgpreflibrary.h b/src/preferences/dialog/dlgpreflibrary.h index 58b76631890a..40f95e61e939 100644 --- a/src/preferences/dialog/dlgpreflibrary.h +++ b/src/preferences/dialog/dlgpreflibrary.h @@ -52,6 +52,7 @@ class DlgPrefLibrary : public DlgPreferencePage, public Ui::DlgPrefLibraryDlg { void requestRelocateDir(QString currentDir, QString newDir); void setTrackTableFont(const QFont& font); void setTrackTableRowHeight(int rowHeight); + void setMaxThreads(int threads); private slots: void slotRowHeightValueChanged(int); @@ -60,6 +61,7 @@ class DlgPrefLibrary : public DlgPreferencePage, public Ui::DlgPrefLibraryDlg { private: void initializeDirList(); + void initializeThreadsCombo(); void setLibraryFont(const QFont& font); QStandardItemModel m_dirListModel; @@ -68,6 +70,8 @@ class DlgPrefLibrary : public DlgPreferencePage, public Ui::DlgPrefLibraryDlg { bool m_bAddedDirectory; QFont m_originalTrackTableFont; int m_iOriginalTrackTableRowHeight; + int m_iOriginalMaxThreads; + }; #endif diff --git a/src/preferences/dialog/dlgpreflibrarydlg.ui b/src/preferences/dialog/dlgpreflibrarydlg.ui index cf1644e92507..1f2f16867de4 100644 --- a/src/preferences/dialog/dlgpreflibrarydlg.ui +++ b/src/preferences/dialog/dlgpreflibrarydlg.ui @@ -7,7 +7,7 @@ 0 0 593 - 791 + 821 @@ -249,6 +249,16 @@ + + + + Max number of simultaneous track analysis + + + + + + diff --git a/src/test/analyserwaveformtest.cpp b/src/test/analyserwaveformtest.cpp index 8e40121dbb89..74c6d3632e6d 100644 --- a/src/test/analyserwaveformtest.cpp +++ b/src/test/analyserwaveformtest.cpp @@ -19,7 +19,7 @@ class AnalyzerWaveformTest: public MixxxTest { protected: AnalyzerWaveformTest() : analysisDao(config()), - aw(&analysisDao), + aw(&analysisDao, AnalyzerWaveform::Mode::Default), bigbuf(nullptr), canaryBigBuf(nullptr) { }