diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b1ad402d65..83f7d4bb727 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -802,6 +802,8 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/util/workerthread.cpp src/util/workerthreadscheduler.cpp src/util/xml.cpp + src/util/descriptivestatistics.cpp + src/util/windowedstatistics.cpp src/waveform/guitick.cpp src/waveform/renderers/glslwaveformrenderersignal.cpp src/waveform/renderers/glvsynctestrenderer.cpp diff --git a/build/depends.py b/build/depends.py index 979ca00664a..a992d615320 100644 --- a/build/depends.py +++ b/build/depends.py @@ -1343,7 +1343,9 @@ def sources(self, build): "src/util/widgethelper.cpp", "src/util/widgetrendertimer.cpp", "src/util/workerthread.cpp", - "src/util/workerthreadscheduler.cpp" + "src/util/workerthreadscheduler.cpp", + "src/util/descriptivestatistics.cpp", + "src/util/windowedstatistics.cpp" ] proto_args = { 'PROTOCPROTOPATH': ['src'], diff --git a/src/analyzer/analyzerbeats.cpp b/src/analyzer/analyzerbeats.cpp index cdbdbf50128..662bb59ef06 100644 --- a/src/analyzer/analyzerbeats.cpp +++ b/src/analyzer/analyzerbeats.cpp @@ -35,6 +35,8 @@ AnalyzerBeats::AnalyzerBeats(UserSettingsPointer pConfig, bool enforceBpmDetecti m_bPreferencesFixedTempo(true), m_bPreferencesOffsetCorrection(false), m_bPreferencesFastAnalysis(false), + m_bPreferencesEnableIroning(false), + m_bPreferencesEnableArrhythmicRemoval(false), m_iSampleRate(0), m_iTotalSamples(0), m_iMaxSamplesToProcess(0), @@ -68,6 +70,8 @@ bool AnalyzerBeats::initialize(TrackPointer tio, int sampleRate, int totalSample m_bPreferencesOffsetCorrection = m_bpmSettings.getFixedTempoOffsetCorrection(); m_bPreferencesReanalyzeOldBpm = m_bpmSettings.getReanalyzeWhenSettingsChange(); m_bPreferencesFastAnalysis = m_bpmSettings.getFastAnalysis(); + m_bPreferencesEnableArrhythmicRemoval = m_bpmSettings.getEnableArrythmicRemoval(); + m_bPreferencesEnableIroning = m_bpmSettings.getEnableIroning(); if (availablePlugins().size() > 0) { m_pluginId = defaultPlugin().id; @@ -177,6 +181,8 @@ bool AnalyzerBeats::shouldAnalyze(TrackPointer tio) const { QString newSubVersion = BeatFactory::getPreferredSubVersion( m_bPreferencesFixedTempo, m_bPreferencesOffsetCorrection, + m_bPreferencesEnableIroning, + m_bPreferencesEnableArrhythmicRemoval, iMinBpm, iMaxBpm, extraVersionInfo); @@ -234,6 +240,8 @@ void AnalyzerBeats::storeResults(TrackPointer tio) { extraVersionInfo, m_bPreferencesFixedTempo, m_bPreferencesOffsetCorrection, + m_bPreferencesEnableIroning, + m_bPreferencesEnableArrhythmicRemoval, m_iSampleRate, m_iTotalSamples, m_iMinBpm, diff --git a/src/analyzer/analyzerbeats.h b/src/analyzer/analyzerbeats.h index 5dbb9a7f5f4..329f992ccee 100644 --- a/src/analyzer/analyzerbeats.h +++ b/src/analyzer/analyzerbeats.h @@ -45,6 +45,8 @@ class AnalyzerBeats : public Analyzer { bool m_bPreferencesFixedTempo; bool m_bPreferencesOffsetCorrection; bool m_bPreferencesFastAnalysis; + bool m_bPreferencesEnableIroning; + bool m_bPreferencesEnableArrhythmicRemoval; int m_iSampleRate; int m_iTotalSamples; diff --git a/src/library/rekordbox/rekordboxfeature.cpp b/src/library/rekordbox/rekordboxfeature.cpp index 5359d1223be..a4bcf361f08 100644 --- a/src/library/rekordbox/rekordboxfeature.cpp +++ b/src/library/rekordbox/rekordboxfeature.cpp @@ -852,7 +852,7 @@ void readAnalyze(TrackPointer track, double sampleRate, int timingOffset, bool i QHash extraVersionInfo; mixxx::BeatsPointer pBeats = BeatFactory::makePreferredBeats( - *track, beats, extraVersionInfo, false, false, sampleRate, 0, 0, 0); + *track, beats, extraVersionInfo, false, false, false, false, sampleRate, 0, 0, 0); track->setBeats(pBeats); } break; diff --git a/src/preferences/beatdetectionsettings.h b/src/preferences/beatdetectionsettings.h index ea78c08238b..badbeea617d 100644 --- a/src/preferences/beatdetectionsettings.h +++ b/src/preferences/beatdetectionsettings.h @@ -19,6 +19,9 @@ #define BPM_REANALYZE_WHEN_SETTINGS_CHANGE "ReanalyzeWhenSettingsChange" #define BPM_FAST_ANALYSIS_ENABLED "FastAnalysisEnabled" +#define BPM_ENABLE_IRONING "BeatDetectionEnableIroning" +#define BPM_ENABLE_ARRYTHMIC_REGIONS_REMOVAL "BeatDetectionEnableArrythmicRemoval" + #define BPM_RANGE_START "BPMRangeStart" #define BPM_RANGE_END "BPMRangeEnd" @@ -35,7 +38,7 @@ class BeatDetectionSettings { DEFINE_PREFERENCE_HELPERS(FixedTempoAssumption, bool, BPM_CONFIG_KEY, BPM_FIXED_TEMPO_ASSUMPTION, true); - + DEFINE_PREFERENCE_HELPERS(FixedTempoOffsetCorrection, bool, BPM_CONFIG_KEY, BPM_FIXED_TEMPO_OFFSET_CORRECTION, true); @@ -45,6 +48,11 @@ class BeatDetectionSettings { DEFINE_PREFERENCE_HELPERS(FastAnalysis, bool, BPM_CONFIG_KEY, BPM_FAST_ANALYSIS_ENABLED, false); + // TODO(xxx) Remove these preferences before 2.4 release + DEFINE_PREFERENCE_HELPERS(EnableIroning, bool, BPM_CONFIG_KEY, BPM_ENABLE_IRONING, true); + + DEFINE_PREFERENCE_HELPERS(EnableArrythmicRemoval, bool, BPM_CONFIG_KEY, BPM_ENABLE_ARRYTHMIC_REGIONS_REMOVAL, true); + QString getBeatPluginId() const { return m_pConfig->getValue(ConfigKey( VAMP_CONFIG_KEY, VAMP_ANALYZER_BEAT_PLUGIN_ID)); diff --git a/src/preferences/dialog/dlgprefbeats.cpp b/src/preferences/dialog/dlgprefbeats.cpp index 9e16fdc54b2..2ba2ce0d11c 100644 --- a/src/preferences/dialog/dlgprefbeats.cpp +++ b/src/preferences/dialog/dlgprefbeats.cpp @@ -43,6 +43,9 @@ DlgPrefBeats::DlgPrefBeats(QWidget *parent, UserSettingsPointer pConfig) connect(bReanalyse,SIGNAL(stateChanged(int)), this, SLOT(slotReanalyzeChanged(int))); + + connect(bIron, SIGNAL(stateChanged(int)), this, SLOT(ironingEnabled(int))); + connect(bRemoveArrythmic, SIGNAL(stateChanged(int)), this, SLOT(removeArrythmicEnabled(int))); } DlgPrefBeats::~DlgPrefBeats() { @@ -59,11 +62,11 @@ void DlgPrefBeats::loadSettings() { m_boffsetEnabled = m_bpmSettings.getFixedTempoOffsetCorrection(); m_bReanalyze = m_bpmSettings.getReanalyzeWhenSettingsChange(); m_FastAnalysisEnabled = m_bpmSettings.getFastAnalysis(); - // TODO(rryan): Above range enabled is not exposed? m_minBpm = m_bpmSettings.getBpmRangeStart(); m_maxBpm = m_bpmSettings.getBpmRangeEnd(); - + m_bEnableIroning = m_bpmSettings.getEnableIroning(); + m_bEnableArrythmicRemoval = m_bpmSettings.getEnableArrythmicRemoval(); slotUpdate(); } @@ -79,6 +82,8 @@ void DlgPrefBeats::slotResetToDefaults() { // TODO(rryan): Above range enabled is not exposed? m_minBpm = m_bpmSettings.getBpmRangeStartDefault(); m_maxBpm = m_bpmSettings.getBpmRangeEndDefault(); + m_bEnableIroning = m_bpmSettings.getEnableIroningDefault(); + m_bEnableArrythmicRemoval = m_bpmSettings.getEnableArrythmicRemovalDefault(); slotUpdate(); } @@ -115,6 +120,16 @@ void DlgPrefBeats::maxBpmRangeChanged(int value) { slotUpdate(); } +void DlgPrefBeats::removeArrythmicEnabled(int value) { + m_bEnableArrythmicRemoval = static_cast(value); + slotUpdate(); +} + +void DlgPrefBeats::ironingEnabled(int value) { + m_bEnableIroning = static_cast(value); + slotUpdate(); +} + void DlgPrefBeats::slotUpdate() { bfixedtempo->setEnabled(m_banalyzerEnabled); boffset->setEnabled((m_banalyzerEnabled && m_bfixedtempoEnabled)); @@ -125,6 +140,9 @@ void DlgPrefBeats::slotUpdate() { txtMaxBpm->setEnabled(m_banalyzerEnabled && m_bfixedtempoEnabled); txtMinBpm->setEnabled(m_banalyzerEnabled && m_bfixedtempoEnabled); bReanalyse->setEnabled(m_banalyzerEnabled); + // Only apply corrections on non-constant tempo beatgrids + bIron->setEnabled(m_banalyzerEnabled && !m_bfixedtempoEnabled); + bRemoveArrythmic->setEnabled(m_banalyzerEnabled && !m_bfixedtempoEnabled); if (!m_banalyzerEnabled) { return; @@ -158,6 +176,9 @@ void DlgPrefBeats::slotUpdate() { txtMaxBpm->setValue(m_maxBpm); txtMinBpm->setValue(m_minBpm); bReanalyse->setChecked(m_bReanalyze); + + bIron->setChecked(m_bEnableIroning); + bRemoveArrythmic->setChecked(m_bEnableArrythmicRemoval); } void DlgPrefBeats::slotReanalyzeChanged(int value) { @@ -179,4 +200,6 @@ void DlgPrefBeats::slotApply() { m_bpmSettings.setFastAnalysis(m_FastAnalysisEnabled); m_bpmSettings.setBpmRangeStart(m_minBpm); m_bpmSettings.setBpmRangeEnd(m_maxBpm); + m_bpmSettings.setEnableIroning(m_bEnableIroning); + m_bpmSettings.setEnableArrythmicRemoval(m_bEnableArrythmicRemoval); } diff --git a/src/preferences/dialog/dlgprefbeats.h b/src/preferences/dialog/dlgprefbeats.h index 2e09913297a..a70418e5471 100644 --- a/src/preferences/dialog/dlgprefbeats.h +++ b/src/preferences/dialog/dlgprefbeats.h @@ -38,6 +38,8 @@ class DlgPrefBeats : public DlgPreferencePage, public Ui::DlgBeatsDlg { void minBpmRangeChanged(int value); void maxBpmRangeChanged(int value); void slotReanalyzeChanged(int value); + void ironingEnabled(int value); + void removeArrythmicEnabled(int value); private: void loadSettings(); @@ -52,6 +54,8 @@ class DlgPrefBeats : public DlgPreferencePage, public Ui::DlgBeatsDlg { bool m_boffsetEnabled; bool m_FastAnalysisEnabled; bool m_bReanalyze; + bool m_bEnableIroning; + bool m_bEnableArrythmicRemoval; }; #endif // DLGPREFBEATS_H diff --git a/src/preferences/dialog/dlgprefbeatsdlg.ui b/src/preferences/dialog/dlgprefbeatsdlg.ui index 3bbdde29106..704a094ab54 100644 --- a/src/preferences/dialog/dlgprefbeatsdlg.ui +++ b/src/preferences/dialog/dlgprefbeatsdlg.ui @@ -149,6 +149,26 @@ by analyzing the beats to discard outliers. + + + + <html><head/><body><p>The detected beats outputed by the Queen Mary algorithm fluctuates around a center tempo value. Attempts to remove unintended tempo variations by making the longest possible sequence of equidistant beats that fall within a +-25ms phase error of a const tempo, which should sound unnoticeable.</p></body></html> + + + Enable ironing detected beats (Recommended) + + + + + + + <html><head/><body><p>Sometimes - especially on percussion free, such as ambient music, or effects heavy regions, such as builds and breaks of EDM - the detector fails to keep track of the steady pulse and will deviate from the metronome tempo. Attempts to remove the beats on these outliers tempo values by making them conform to the previous and next stable detected tempo if they are the same and regions is small.</p></body></html> + + + Enable removal of arrythmic regions (Recommended) + + + diff --git a/src/track/beatfactory.cpp b/src/track/beatfactory.cpp index a5fb85ff9d3..82d5659cea3 100644 --- a/src/track/beatfactory.cpp +++ b/src/track/beatfactory.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -5,6 +6,63 @@ #include "track/beatmap.h" #include "track/beatfactory.h" #include "track/beatutils.h" +#include "util/cmdlineargs.h" + + +namespace { + +void debugBeats(const Track& track, const QVector& rawBeats, + const QVector& correctedBeats, QString beatsVersion, QString beatsSubVersion) { + if(!CmdlineArgs::Instance().getAnalyzerDebug()) { + return; + } + QString debugFilename = QDir(CmdlineArgs::Instance().getSettingsPath()).filePath("beatAnalyzerOutput.csv"); + QFile debugFile(debugFilename); + if (!debugFile.open(QIODevice::Append | QIODevice::Text)) { + qWarning() << "ERROR: Could not open debug file:" << debugFilename; + return; + } + QString trackHeader; + trackHeader += track.getInfo(); + trackHeader += ", analysed at "; + trackHeader += QDateTime::currentDateTime().toString("yyyy-MM-dd_hh'h'mm'm'ss's'"); + trackHeader += ", with " + beatsVersion + beatsSubVersion; + debugFile.write(trackHeader.toLocal8Bit()); + QString sRawBeats; + QString sRawBeatLenght; + auto previousBeat = rawBeats.begin(); + sRawBeats += QString::number(*previousBeat, 'f') + ","; + for (auto beat = std::begin(rawBeats) + 1, end = std::end(rawBeats); beat != end; beat += 1) { + sRawBeats += QString::number(*beat, 'f') + ","; + sRawBeatLenght += QString::number(*beat - *previousBeat, 'f') + ","; + previousBeat = beat; + } + QString sCorrectedBeats; + QString sCorrectedBeatLenght; + previousBeat = correctedBeats.begin(); + sCorrectedBeats += QString::number(*previousBeat, 'f') + ","; + for (auto beat = std::begin(correctedBeats) + 1, end = std::end(correctedBeats); beat != end; beat += 1) { + sCorrectedBeats += QString::number(*beat, 'f') + ","; + sCorrectedBeatLenght += QString::number(*beat - *previousBeat, 'f') + ","; + previousBeat = beat; + } + QString resultHeader = "\nRaw beats\n"; + debugFile.write(resultHeader.toLocal8Bit()); + debugFile.write(sRawBeats.toLocal8Bit()); + resultHeader = "\nCorrected beats\n"; + debugFile.write(resultHeader.toLocal8Bit()); + debugFile.write(sCorrectedBeats.toLocal8Bit()); + resultHeader = "\nRaw beat length\n"; + debugFile.write(resultHeader.toLocal8Bit()); + debugFile.write(sRawBeatLenght.toLocal8Bit()); + resultHeader = "\nCorrected beat length\n"; + debugFile.write(resultHeader.toLocal8Bit()); + debugFile.write(sCorrectedBeatLenght.toLocal8Bit()); + debugFile.write("\n"); + debugFile.close(); +} + +} mixxx::BeatsPointer BeatFactory::loadBeatsFromByteArray(const Track& track, QString beatsVersion, @@ -45,9 +103,11 @@ QString BeatFactory::getPreferredVersion( QString BeatFactory::getPreferredSubVersion( const bool bEnableFixedTempoCorrection, const bool bEnableOffsetCorrection, + const bool bEnableIroning, + const bool bEnableArrytimicRemoval, const int iMinBpm, const int iMaxBpm, - const QHash extraVersionInfo) { + const QHash& extraVersionInfo) { const char* kSubVersionKeyValueSeparator = "="; const char* kSubVersionFragmentSeparator = "|"; QStringList fragments; @@ -83,6 +143,18 @@ QString BeatFactory::getPreferredSubVersion( QString::number(1)); } + if (bEnableIroning && !bEnableFixedTempoCorrection) { + fragments << QString("enable_ironing%1%2") + .arg(kSubVersionKeyValueSeparator, + QString::number(1)); + } + + if (bEnableArrytimicRemoval && !bEnableFixedTempoCorrection) { + fragments << QString("enable_arrytimic_removal%1%2") + .arg(kSubVersionKeyValueSeparator, + QString::number(1)); + } + fragments << QString("rounding%1%2") .arg(kSubVersionKeyValueSeparator, QString::number(0.05)); @@ -94,18 +166,23 @@ QString BeatFactory::getPreferredSubVersion( mixxx::BeatsPointer BeatFactory::makePreferredBeats(const Track& track, QVector beats, - const QHash extraVersionInfo, + const QHash& extraVersionInfo, const bool bEnableFixedTempoCorrection, const bool bEnableOffsetCorrection, + const bool bEnableIroning, + const bool bEnableArrytimicRemoval, const int iSampleRate, const int iTotalSamples, const int iMinBpm, const int iMaxBpm) { const QString version = getPreferredVersion(bEnableFixedTempoCorrection); const QString subVersion = getPreferredSubVersion(bEnableFixedTempoCorrection, - bEnableOffsetCorrection, - iMinBpm, iMaxBpm, - extraVersionInfo); + bEnableOffsetCorrection, + bEnableIroning, + bEnableArrytimicRemoval, + iMinBpm, + iMaxBpm, + extraVersionInfo); BeatUtils::printBeatStatistics(beats, iSampleRate); if (version == BEAT_GRID_2_VERSION) { @@ -119,9 +196,16 @@ mixxx::BeatsPointer BeatFactory::makePreferredBeats(const Track& track, pGrid->setSubVersion(subVersion); return mixxx::BeatsPointer(pGrid, &BeatFactory::deleteBeats); } else if (version == BEAT_MAP_VERSION) { - mixxx::BeatMap* pBeatMap = new mixxx::BeatMap(track, iSampleRate, beats); - pBeatMap->setSubVersion(subVersion); - return mixxx::BeatsPointer(pBeatMap, &BeatFactory::deleteBeats); + if (bEnableIroning) { + QVector correctedBeats = BeatUtils::correctBeatmap( + beats, mixxx::audio::SampleRate(iSampleRate), bEnableArrytimicRemoval); + debugBeats(track, beats, correctedBeats, version, subVersion); + beats = correctedBeats; + } + auto pMap = new mixxx::BeatMap(track, iSampleRate, beats); + pMap->setSubVersion(subVersion); + return mixxx::BeatsPointer(pMap, &BeatFactory::deleteBeats); + } else { qDebug() << "ERROR: Could not determine what type of beatgrid to create."; return mixxx::BeatsPointer(); diff --git a/src/track/beatfactory.h b/src/track/beatfactory.h index cb25021f19d..9745f135452 100644 --- a/src/track/beatfactory.h +++ b/src/track/beatfactory.h @@ -19,16 +19,21 @@ class BeatFactory { static QString getPreferredVersion(const bool bEnableFixedTempoCorrection); static QString getPreferredSubVersion( - const bool bEnableFixedTempoCorrection, - const bool bEnableOffsetCorrection, - const int iMinBpm, const int iMaxBpm, - const QHash extraVersionInfo); + const bool bEnableFixedTempoCorrection, + const bool bEnableOffsetCorrection, + const bool bEnableIroning, + const bool bEnableArrytimicRemoval, + const int iMinBpm, + const int iMaxBpm, + const QHash& extraVersionInfo); static mixxx::BeatsPointer makePreferredBeats(const Track& track, QVector beats, - const QHash extraVersionInfo, + const QHash& extraVersionInfo, const bool bEnableFixedTempoCorrection, const bool bEnableOffsetCorrection, + const bool bEnableIroning, + const bool bEnableArrytimicRemoval, const int iSampleRate, const int iTotalSamples, const int iMinBpm, diff --git a/src/track/beatutils.cpp b/src/track/beatutils.cpp index 1eb2a0afd6f..86acf82b8ad 100644 --- a/src/track/beatutils.cpp +++ b/src/track/beatutils.cpp @@ -5,22 +5,33 @@ * Author: vittorio */ -#include -#include -#include +#include "track/beatutils.h" + #include #include +#include +#include +#include -#include "track/beatutils.h" +#include "util/assert.h" #include "util/math.h" +#include "util/descriptivestatistics.h" +#include "util/windowedstatistics.h" + +namespace { // we are generous and assume the global_BPM to be at most 0.05 BPM far away // from the correct one -#define BPM_ERROR 0.05 +constexpr double kMaxBpmError = 0.05; +// When ironing the grid for long sequences of const tempo we use +// a 25ms tolerence because this small of a difference is inaudible +constexpr double kMaxSecsPhaseError = 0.025; // the raw beatgrid is divided into blocks of size N from which the local bpm is // computed. Tweaked from 8 to 12 which improves the BPM accuracy for 'problem songs'. -#define N 12 +constexpr int kBeatsToCountTempo = 12; + +constexpr int kMaxBeatsForChange = 32; static bool sDebug = false; @@ -28,6 +39,230 @@ const double kCorrectBeatLocalBpmEpsilon = 0.05; //0.2; const int kHistogramDecimalPlaces = 2; const double kHistogramDecimalScale = pow(10.0, kHistogramDecimalPlaces); const double kBpmFilterTolerance = 1.0; +// maximum difference for BPMs to be considered the same +constexpr double kMaxDiffSameBpm = 0.3; + +QVector makeQVector(QVector::iterator begin, QVector::iterator end) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + return QVector(begin, end); +#else + return QVector::fromStdVector(std::vector(begin, end)); +#endif +} + +} // namespace + + +QMap BeatUtils::findStableTempoRegions(const QVector& beats, + const mixxx::audio::SampleRate& sampleRate) { + + QVector durationOfBeats = computeDurationOfEachBeat(beats); + auto sortedDurationOfBeats = durationOfBeats.toList(); + std::sort(sortedDurationOfBeats.begin(), sortedDurationOfBeats.end()); + // We have to make sure we have odd numbers here because we are going + // to compute the median value and it must belong to frequencyOfTempos. + // The median is only used as rough guess of the tempo so it's ok that + // we accidentally change the median by popping the last value. + bool poped = false; + double reserveLast = 0.0; + if (sortedDurationOfBeats.size() % 2 == 0 && sortedDurationOfBeats.size() > 1) { + reserveLast = sortedDurationOfBeats.last(); + sortedDurationOfBeats.pop_back(); + poped = true; + } + const double medianBeatLenght = computeSampleMedian(sortedDurationOfBeats); + if (poped) { + sortedDurationOfBeats << reserveLast; + } + // Forming a meter perception takes a few seconds, so we assume sections of consistent + // metrical structure to be at least around 10s long. So we use a window of the double + // of that in our filtering since changes in the tempo shorter than that are very likely + // to be noise from the analyzer. The median filter is good to discard the outliers values + // but still may fluctatue because of the jitter so we still look for the most frequent median + int numberOfBeatsInFilteringWindow = (10 / (medianBeatLenght / sampleRate)) * 2; + if (numberOfBeatsInFilteringWindow % 2 == 0) { + numberOfBeatsInFilteringWindow += 1; + } + auto tempoMedianFilter = MovingMedian(numberOfBeatsInFilteringWindow); + auto mostFrequentTempo = MovingMode(numberOfBeatsInFilteringWindow); + int currentBeat = -1; + int lastBeatChange = 0; + QMap stableBeatLenghtByPosition; + // We start at beat 0 with the median bpm as rough guess of the "correct" tempo + stableBeatLenghtByPosition[lastBeatChange] = medianBeatLenght; + // Here we are going to track the significant tempo changes over the track to make + // regions of stable tempos and independently make const tempo or ironed grid for them + for (double beatLenght : durationOfBeats) { + currentBeat += 1; + double filteredBeatLenght = tempoMedianFilter.pushAndEvaluate(beatLenght); + double newStableBeatLenght = mostFrequentTempo.pushAndEvaluate(filteredBeatLenght); + // The analyzer has some jitter that causes a steady beat to fluctuate around the correct + // value so we don't consider changes to a neighboring value in the ordered tempo table + if (newStableBeatLenght == stableBeatLenghtByPosition.last()) { + continue; + // Check if the new tempo is the right neighbor of the previous tempo + } else if (stableBeatLenghtByPosition.last() != sortedDurationOfBeats.last() && + newStableBeatLenght == *(std::upper_bound( + sortedDurationOfBeats.begin(), + sortedDurationOfBeats.end(), stableBeatLenghtByPosition.last()) + 1)) { + continue; + // Check if the new tempo is the left neighbor of the previous tempo + } else if (stableBeatLenghtByPosition.last() != sortedDurationOfBeats.first() && + newStableBeatLenght == *(std::lower_bound( + sortedDurationOfBeats.begin(), + sortedDurationOfBeats.end(), stableBeatLenghtByPosition.last()) - 1)) { + continue; + } else { + // We need to make sure newStableBeatLenght belongs to sortedDurationOfBeats + // because in the loop we call std::lower_bound on it and will dereference + if (std::binary_search(sortedDurationOfBeats.begin(), sortedDurationOfBeats.end(), newStableBeatLenght)) { + lastBeatChange = currentBeat - tempoMedianFilter.lag() - mostFrequentTempo.lag(); + // Since we used the median as guess of the first tempo + // we change position at 0 if we detect a change in very beginning + if (lastBeatChange > kBeatsToCountTempo) { + stableBeatLenghtByPosition[lastBeatChange] = newStableBeatLenght; + } else { + stableBeatLenghtByPosition[0] = newStableBeatLenght; + } + } + } + } + // We also add the median as rough guess as our last tempo + // This way we always have both sentinels for the whole track + // as a constant tempo region if no significant tempo changes + stableBeatLenghtByPosition[durationOfBeats.count()] = medianBeatLenght; + return stableBeatLenghtByPosition; +} + +void BeatUtils::removeSmallArrhythmic( + QVector& beats, const mixxx::audio::SampleRate& sampleRate, const QMap& stableBeatLenghtByPosition) { + // A common problem the analyzer has is to detect arrhythmic regions + // of tacks with a constant tempo as in a different unsteady tempo. + // This happens frequently on builds and breaks with heavy effects on edm music. + // Since these occurs most on beatless regions we do not want them to be + // on a different tempo, because they are still syncable in the true tempo + // We arbitraly remove these arrhythmic regions if they are short than 32 beats + // and return to same tempo they start deviating from. + // TODO(Cristiano) Use a better heuristic for "finding" these regions + // like for example the avarage energy of the beats + auto positionsWithTempoChange = stableBeatLenghtByPosition.keys(); + auto beatLenghts = stableBeatLenghtByPosition.values(); + // We are going to make a deep copy since we are very likely to + // shift our whole beats vector which will results in a copy anyway + QVector anchoredBeats; + anchoredBeats.reserve(beats.size()); + anchoredBeats << makeQVector(beats.begin(), beats.begin() + positionsWithTempoChange[1] + 1); + + for (int i = 2; i < positionsWithTempoChange.size(); i += 1) { + double previousTempoRoughGuess = 60.0 * sampleRate / beatLenghts[i - 2]; + int limitAtLeft = positionsWithTempoChange[i - 1]; + int limitAtRight = positionsWithTempoChange[i]; + int lenghtOfChange = limitAtRight - limitAtLeft; + double nextTempoRoughGuess = 60.0 * sampleRate / beatLenghts[i]; + if (lenghtOfChange <= kMaxBeatsForChange && + fabs(nextTempoRoughGuess - previousTempoRoughGuess) < kMaxDiffSameBpm) { + //qDebug() << "removing" << limitAtRight - limitAtLeft << "beats from" << limitAtLeft << limitAtRight; + auto beatsAtLeft = makeQVector( + beats.begin() + positionsWithTempoChange[i - 2], + beats.begin() + positionsWithTempoChange[i - 1]); + double tempoAtLeft = calculateBpm(beatsAtLeft, sampleRate, 60, 180); + double beatLength = (60.0 * sampleRate) / tempoAtLeft; + double regionLengthInFrames = beats[limitAtRight] - beats[limitAtLeft]; + int beatsToAdd = floor((regionLengthInFrames / beatLength) + 0.5); + // Make sure the last beat we add is extacly at litmitAtRight + double preciseBeatLeght = regionLengthInFrames / beatsToAdd; + double beatOffset = beats[limitAtLeft]; + int beatsAdded = 0; + while (beatsAdded != beatsToAdd) { + anchoredBeats << floor(beatOffset + 0.5); + beatOffset += preciseBeatLeght; + beatsAdded += 1; + } + } else { + anchoredBeats << makeQVector( + beats.begin() + positionsWithTempoChange[i - 1], + beats.begin() + positionsWithTempoChange[i] + 1); + } + } + double previousBeat = 0.0; + beats.clear(); + for (double& beat : anchoredBeats) { + if (beat - previousBeat > 0.01) { + beats << beat; + previousBeat = beat; + } + } +} + +QVector BeatUtils::correctBeatmap( + QVector& rawBeats, const mixxx::audio::SampleRate& sampleRate, bool removeArrythmic) { + QMap stableBeatLenghtByPosition = findStableTempoRegions(rawBeats, sampleRate); + if (removeArrythmic) { + removeSmallArrhythmic(rawBeats, sampleRate, stableBeatLenghtByPosition); + // Since we changed our beats vector we need to recalculate its tempos + // TODO(Cristiano) adjust these on the fly on removeSmallArrhythmic + stableBeatLenghtByPosition = findStableTempoRegions(rawBeats, sampleRate); + } + QVector correctedBeats; + QVector ironedBeats; + correctedBeats.reserve(rawBeats.size()); + auto tempoChanges = stableBeatLenghtByPosition.keys(); + for (int lastTempoChage = 1; + lastTempoChage < tempoChanges.size(); + lastTempoChage++) { + int beatStart = tempoChanges[lastTempoChage - 1]; + int beatEnd = tempoChanges[lastTempoChage]; + + auto splittedAtTempoChange = makeQVector( + rawBeats.begin() + beatStart, rawBeats.begin() + beatEnd + 1); + ironedBeats = calculateIronedGrid(splittedAtTempoChange, sampleRate); + + if (correctedBeats.size() > 0) { + if (correctedBeats.last() == ironedBeats.first()) { + correctedBeats.pop_back(); + } + } + correctedBeats << ironedBeats; + } + return correctedBeats; +} + +QVector BeatUtils::calculateIronedGrid( + const QVector& rawbeats, const mixxx::audio::SampleRate& sampleRate) { + // Daniel's ironing algorithm + // loop backwards through the raw beats. and calculate the average beat length from the first beat. + // add an inner loop and check for outliers using the momentary average as beat length. + // once you have found an average with only single outliers, store the beats using the current average. + // reset and do the loop again, starting with the region from the found beat to the end. + double maxPhaseError = kMaxSecsPhaseError * sampleRate; + int leftIndex = 0; + int rightIndex = rawbeats.size() - 1; + QVector ironedBeats; + while (leftIndex < rawbeats.size() - 1) { + double meanBeatLength = (rawbeats[rightIndex] - rawbeats[leftIndex]) / (rightIndex - leftIndex); + int outliersCount = 0; + double beatOffset = rawbeats[leftIndex]; + for (int i = leftIndex; i < rightIndex; i += 1) { + double phaseError = beatOffset - rawbeats[i]; + if (fabs(phaseError) > maxPhaseError) { + outliersCount += 1; + } + beatOffset += meanBeatLength; + } + if (outliersCount <= 1) { + beatOffset = rawbeats[leftIndex]; + for (int i = leftIndex; i < rightIndex; i += 1) { + ironedBeats << floor(beatOffset + 0.5); + beatOffset += meanBeatLength; + } + leftIndex = rightIndex; + rightIndex = rawbeats.size() - 1; + continue; + } + rightIndex -= 1; + } + return ironedBeats; +} void BeatUtils::printBeatStatistics(const QVector& beats, int SampleRate) { if (!sDebug) { @@ -35,14 +270,14 @@ void BeatUtils::printBeatStatistics(const QVector& beats, int SampleRate } QMap frequency; - for (int i = N; i < beats.size(); i += 1) { - double beat_start = beats.at(i - N); + for (int i = kBeatsToCountTempo; i < beats.size(); i += 1) { + double beat_start = beats.at(i - kBeatsToCountTempo); double beat_end = beats.at(i); // Time needed to count a bar (N beats) const double time = (beat_end - beat_start) / SampleRate; if (time == 0) continue; - double local_bpm = 60.0 * N / time; + double local_bpm = 60.0 * kBeatsToCountTempo / time; qDebug() << "Beat" << i << "local BPM:" << local_bpm; @@ -81,27 +316,47 @@ double BeatUtils::computeSampleMedian(QList sortedItems) { return sortedItems.at(item_position - 1); } +QVector BeatUtils::computeDurationOfEachBeat(const QVector& beats) { + QVector beatDurations; + beatDurations.reserve(beats.size() -1); + auto previousBeat = beats.begin(); + for(auto beat = beats.begin() + 1; beat != beats.end(); beat += 1) { + double duration = *beat - *previousBeat; + beatDurations << duration; + previousBeat = beat; + } + return beatDurations; +} + QList BeatUtils::computeWindowedBpmsAndFrequencyHistogram( - const QVector beats, const int windowSize, const int windowStep, - const int sampleRate, QMap* frequencyHistogram) { + const QVector beats, int windowSize, const int windowStep, + const int sampleRate, QMap* pFrequencyHistogram) { + DEBUG_ASSERT(pFrequencyHistogram); + // avoid out of range access case beats with small + while (beats.size() - 1 < windowSize * 2) { + windowSize /= 2; + } QList averageBpmList; - for (int i = windowSize; i < beats.size(); i += windowStep) { + for (int i = 0; i < beats.size(); i += windowStep) { //get start and end sample of the beats - double start_sample = beats.at(i - windowSize); - double end_sample = beats.at(i); - - // Time needed to count a bar (4 beats) + double start_sample = beats.at(i); + double end_sample; + if (i + windowSize > beats.size() - 1) { + end_sample = beats.at(i); + start_sample = beats.at(i - windowSize); + } else { + end_sample = beats.at(i + windowSize); + } + // Time needed to count kbeats double time = (end_sample - start_sample) / sampleRate; if (time == 0) continue; double localBpm = 60.0 * windowSize / time; - // round BPM to have two decimal places double roundedBpm = floor(localBpm * kHistogramDecimalScale + 0.5) / kHistogramDecimalScale; - // add to local BPM to list and increment frequency count averageBpmList << roundedBpm; - (*frequencyHistogram)[roundedBpm] += 1; + (*pFrequencyHistogram)[roundedBpm] += 1; } return averageBpmList; } @@ -183,13 +438,13 @@ double BeatUtils::calculateBpm(const QVector& beats, int SampleRate, // If we don't have enough beats for our regular approach, just divide the # // of beats by the duration in minutes. - if (beats.size() <= N) { + if (beats.size() <= kBeatsToCountTempo) { return 60.0 * (beats.size()-1) * SampleRate / (beats.last() - beats.first()); } QMap frequency_table; QList average_bpm_list = computeWindowedBpmsAndFrequencyHistogram( - beats, N, 1, SampleRate, &frequency_table); + beats, kBeatsToCountTempo, 1, SampleRate, &frequency_table); // Get the median BPM. std::sort(average_bpm_list.begin(), average_bpm_list.end()); @@ -243,44 +498,44 @@ double BeatUtils::calculateBpm(const QVector& beats, int SampleRate, int counter = 0; int perfectBeats = 0; - for (int i = N; i < beats.size(); i += 1) { + for (int i = kBeatsToCountTempo; i < beats.size(); i += 1) { // get start and end sample of the beats - double beat_start = beats.at(i-N); + double beat_start = beats.at(i-kBeatsToCountTempo); double beat_end = beats.at(i); // Time needed to count a bar (N beats) double time = (beat_end - beat_start) / SampleRate; if (time == 0) continue; - double local_bpm = 60.0 * N / time; + double local_bpm = 60.0 * kBeatsToCountTempo / time; // round BPM to have two decimal places local_bpm = floor(local_bpm * kHistogramDecimalScale + 0.5) / kHistogramDecimalScale; //qDebug() << "Local BPM beat " << i << ": " << local_bpm; if (!foundFirstCorrectBeat && - filtered_bpm_frequency_table.contains(local_bpm) && - fabs(local_bpm - filterWeightedAverageBpm) < BPM_ERROR) { + filtered_bpm_frequency_table.contains(local_bpm) && + fabs(local_bpm - filterWeightedAverageBpm) < kMaxBpmError) { firstCorrectBeatSample = beat_start; foundFirstCorrectBeat = true; if (sDebug) { - qDebug() << "Beat #" << (i - N) + qDebug() << "Beat #" << (i - kBeatsToCountTempo) << "is considered as reference beat with BPM:" << local_bpm; } } if (foundFirstCorrectBeat) { if (counter == 0) { - counter = N; + counter = kBeatsToCountTempo; } else { counter += 1; } double time2 = (beat_end - firstCorrectBeatSample) / SampleRate; double correctedBpm = 60 * counter / time2; - if (fabs(correctedBpm - filterWeightedAverageBpm) <= BPM_ERROR) { + if (fabs(correctedBpm - filterWeightedAverageBpm) <= kMaxBpmError) { perfect_bpm += correctedBpm; ++perfectBeats; if (sDebug) { - qDebug() << "Beat #" << (i-N) + qDebug() << "Beat #" << (i-kBeatsToCountTempo) << "is considered as correct -->BPM improved to:" << correctedBpm; } @@ -294,7 +549,7 @@ double BeatUtils::calculateBpm(const QVector& beats, int SampleRate, // Round values that are within BPM_ERROR of a whole number. const double rounded_bpm = floor(perfectAverageBpm + 0.5); const double bpm_diff = fabs(rounded_bpm - perfectAverageBpm); - bool perform_rounding = (bpm_diff <= BPM_ERROR); + bool perform_rounding = (bpm_diff <= kMaxBpmError); // Finally, restrict the BPM to be within min_bpm and max_bpm. const double maybeRoundedBpm = perform_rounding ? rounded_bpm : perfectAverageBpm; @@ -356,16 +611,16 @@ double BeatUtils::calculateOffset( double BeatUtils::findFirstCorrectBeat(const QVector rawbeats, const int SampleRate, const double global_bpm) { - for (int i = N; i < rawbeats.size(); i++) { + for (int i = kBeatsToCountTempo; i < rawbeats.size(); i++) { // get start and end sample of the beats - double start_sample = rawbeats.at(i-N); + double start_sample = rawbeats.at(i-kBeatsToCountTempo); double end_sample = rawbeats.at(i); // The time in seconds represented by this sample range. double time = (end_sample - start_sample)/SampleRate; // Average BPM within this sample range. - double avg_bpm = 60.0 * N / time; + double avg_bpm = 60.0 * kBeatsToCountTempo / time; //qDebug() << "Local BPM between beat " << (i-N) << " and " << i << " is " << avg_bpm; @@ -399,7 +654,6 @@ double BeatUtils::calculateFixedTempoFirstBeat( // Length of a beat at globalBpm in mono samples. const double beat_length = 60.0 * sampleRate / globalBpm; - double firstCorrectBeat = findFirstCorrectBeat( rawbeats, sampleRate, globalBpm); diff --git a/src/track/beatutils.h b/src/track/beatutils.h index 906e5f00fd8..ea52fafa57a 100644 --- a/src/track/beatutils.h +++ b/src/track/beatutils.h @@ -4,6 +4,7 @@ #ifndef BEATUTILS_H_ #define BEATUTILS_H_ +#include "audio/types.h" // to tell the msvs compiler about `isnan` #include "util/math.h" @@ -47,6 +48,11 @@ class BeatUtils { */ static double calculateBpm(const QVector& beats, int SampleRate, int min_bpm, int max_bpm); + + // Remove jitter and false beats noise from the beats that make a beatmap + static QVector correctBeatmap( + QVector& rawBeats, const mixxx::audio::SampleRate& sampleRate, bool removeArrythmic); + static double findFirstCorrectBeat(const QVector rawBeats, const int SampleRate, const double global_bpm); @@ -75,10 +81,16 @@ class BeatUtils { const double filterCenter, const double filterTolerance, QMap* filteredFrequencyTable); + static QMap findStableTempoRegions(const QVector& beats, + const mixxx::audio::SampleRate& sampleRate); + static void removeSmallArrhythmic(QVector& rawBeats, + const mixxx::audio::SampleRate& sampleRate, const QMap& stableTemposByPosition); + static QVector calculateIronedGrid( + const QVector& rawbeats, const mixxx::audio::SampleRate& sampleRate); static QList computeWindowedBpmsAndFrequencyHistogram( - const QVector beats, const int windowSize, const int windowStep, - const int sampleRate, QMap* frequencyHistogram); - + const QVector beats, int windowSize, + const int windowStep, const int sampleRate, QMap* pFrequencyHistogram); + static QVector computeDurationOfEachBeat(const QVector& beats); }; #endif /* BEATUTILS_H_ */ diff --git a/src/util/cmdlineargs.cpp b/src/util/cmdlineargs.cpp index 8b040b58b19..90740ee5719 100644 --- a/src/util/cmdlineargs.cpp +++ b/src/util/cmdlineargs.cpp @@ -1,5 +1,6 @@ #include +#include #include #include "util/cmdlineargs.h" @@ -15,6 +16,7 @@ CmdlineArgs::CmdlineArgs() m_safeMode(false), m_debugAssertBreak(false), m_settingsPathSet(false), + m_analyzerDebug(false), m_logLevel(mixxx::kLogLevelDefault), m_logFlushLevel(mixxx::kLogFlushLevelDefault), // We are not ready to switch to XDG folders under Linux, so keeping $HOME/.mixxx as preferences folder. see lp:1463273 @@ -103,6 +105,8 @@ when a critical error occurs unless this is set properly.\n", stdout); m_safeMode = true; } else if (QString::fromLocal8Bit(argv[i]).contains("--debugAssertBreak", Qt::CaseInsensitive)) { m_debugAssertBreak = true; + } else if (QString::fromLocal8Bit(argv[i]).contains("--analyzerDebug", Qt::CaseInsensitive)) { + m_analyzerDebug = true; } else { m_musicFiles += QString::fromLocal8Bit(argv[i]); } @@ -114,6 +118,17 @@ when a critical error occurs unless this is set properly.\n", stdout); m_logLevel = mixxx::LogLevel::Debug; } + if (m_analyzerDebug) { + // truncate file + QString debugFilename = QDir(m_settingsPath).filePath("beatAnalyzerOutput.csv"); + QFile debugFile(debugFilename); + if (!debugFile.open(QIODevice::WriteOnly)) { + qWarning() << "ERROR: Could not open debug file:" << debugFilename; + } else { + debugFile.close(); + } + } + return true; } @@ -167,6 +182,8 @@ void CmdlineArgs::printUsage() { --logFlushLevel LEVEL Sets the the logging level at which the log buffer\n\ is flushed to mixxx.log. LEVEL is one of the values\n\ defined at --logLevel above.\n\ +\n\ +--analyzerDebug Enable output the beat analyses results to a csv file.\n\ \n" #ifdef MIXXX_BUILD_DEBUG "\ diff --git a/src/util/cmdlineargs.h b/src/util/cmdlineargs.h index 22ce3099dbd..a4a24d3d376 100644 --- a/src/util/cmdlineargs.h +++ b/src/util/cmdlineargs.h @@ -30,6 +30,7 @@ class CmdlineArgs final { bool getSafeMode() const { return m_safeMode; } bool getDebugAssertBreak() const { return m_debugAssertBreak; } bool getSettingsPathSet() const { return m_settingsPathSet; } + bool getAnalyzerDebug() const { return m_analyzerDebug; } mixxx::LogLevel getLogLevel() const { return m_logLevel; } mixxx::LogLevel getLogFlushLevel() const { return m_logFlushLevel; } bool getTimelineEnabled() const { return !m_timelinePath.isEmpty(); } @@ -50,6 +51,7 @@ class CmdlineArgs final { bool m_safeMode; bool m_debugAssertBreak; bool m_settingsPathSet; // has --settingsPath been set on command line ? + bool m_analyzerDebug; mixxx::LogLevel m_logLevel; // Level of stderr logging message verbosity mixxx::LogLevel m_logFlushLevel; // Level of mixx.log file flushing QString m_locale; diff --git a/src/util/descriptivestatistics.cpp b/src/util/descriptivestatistics.cpp new file mode 100644 index 00000000000..7ea148b4fa4 --- /dev/null +++ b/src/util/descriptivestatistics.cpp @@ -0,0 +1,36 @@ +#include "util/descriptivestatistics.h" + +double DescriptiveStatistics::median(const QList& sortedItems) { + if (sortedItems.empty()) { + return 0.0; + } + // When there are an even number of elements, the sample median is the mean + // of the middle 2 elements. + if (sortedItems.size() % 2 == 0) { + int item_position = sortedItems.size() / 2; + double item_value1 = sortedItems.at(item_position - 1); + double item_value2 = sortedItems.at(item_position); + return (item_value1 + item_value2) / 2.0; + } + // When there are an odd number of elements, find the {(n+1)/2}th item in + // the sorted list. + int item_position = (sortedItems.size() + 1) / 2; + return sortedItems.at(item_position - 1); +} + +// this is an incomplete implementation that do NOT +// handle cases where the mode is not unique. +// but is good enough for our proporses +double DescriptiveStatistics::mode(const QHash& frequencyOfValues) { + QHashIterator valuesAndFrequency(frequencyOfValues); + int max = 0; + int mode = 0; + while (valuesAndFrequency.hasNext()) { + valuesAndFrequency.next(); + if (max < valuesAndFrequency.value()) { + mode = valuesAndFrequency.key(); + max = valuesAndFrequency.value(); + } + } + return mode; +} diff --git a/src/util/descriptivestatistics.h b/src/util/descriptivestatistics.h new file mode 100644 index 00000000000..698824aa73c --- /dev/null +++ b/src/util/descriptivestatistics.h @@ -0,0 +1,11 @@ +#include +#include + +class DescriptiveStatistics { + public: + // The median is the middle value of a sorted sequence + // If sequence is even the mean of both middle values. + static double median(const QList& sortedItems); + // The mode is most repeated value in a sequence + static double mode(const QHash& frequencyOfValues); +}; diff --git a/src/util/windowedstatistics.cpp b/src/util/windowedstatistics.cpp new file mode 100644 index 00000000000..7d2e9c9a023 --- /dev/null +++ b/src/util/windowedstatistics.cpp @@ -0,0 +1,33 @@ +#include "util/windowedstatistics.h" + +#include + +#include "util/descriptivestatistics.h" + +void MovingMode::update(double newValue, double oldValue) { + // Our update window method returns a nan + // case the window is not filled yet + if (!isnan(oldValue)) { + m_frequencyOfValues[oldValue] -= 1; + } + m_frequencyOfValues[newValue] += 1; +} +// this is an incomplete implementation that do NOT +// handle cases where the mode is not unique. +double MovingMode::compute() { + return DescriptiveStatistics::mode(m_frequencyOfValues); +} + +void MovingMedian::update(double newValue, double oldValue) { + // Our update window method returns a nan + // case the window is not filled yet + if (!isnan(oldValue)) { + m_sortedValues.removeAt(m_sortedValues.indexOf(oldValue)); + } + auto insertPosition = std::lower_bound(m_sortedValues.begin(), m_sortedValues.end(), newValue); + m_sortedValues.insert(insertPosition, newValue); +} + +double MovingMedian::compute() { + return DescriptiveStatistics::median(m_sortedValues); +} diff --git a/src/util/windowedstatistics.h b/src/util/windowedstatistics.h new file mode 100644 index 00000000000..d4f0dbb0387 --- /dev/null +++ b/src/util/windowedstatistics.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include +#include + +#include "util/assert.h" +#include "util/circularbuffer.h" +#include "util/fpclassify.h" + +/// These classes are used to compute statistics descriptors +/// of a series of tempo values and are called from beatutils +class WindowedStatistics { + public: + WindowedStatistics(int windowSize) { + DEBUG_ASSERT(windowSize > 0); + m_windowSize = windowSize; + m_window = std::make_unique>(windowSize); + m_currentCount = 0; + } + virtual ~WindowedStatistics() = default; + + double pushAndEvaluate(double newValue) { + if (isnan(newValue)) { + return newValue; + } + double oldValue = updateWindow(newValue); + update(newValue, oldValue); + return compute(); + } + + void clear() { + m_window->clear(); + } + + int lag() { + // expected latency + return (m_currentCount - 1) / 2; + } + + int windowCurrentSize() { + return m_currentCount; + } + + private: + double updateWindow(double newValue) { + // case not full we do not want to remove the + // first element on child class update method + double front = std::nan(""); + if (m_window->isFull()) { + m_window->read(&front, 1); + } else { + m_currentCount += 1; + } + m_window->write(&newValue, 1); + return front; + } + + virtual double compute() = 0; + virtual void update(double newValue, double oldValue) = 0; + + std::unique_ptr> m_window; + int m_windowSize; + int m_currentCount; +}; + +class MovingMedian : public WindowedStatistics { + public: + MovingMedian(int windowSize) + : WindowedStatistics(windowSize) { + m_sortedValues.reserve(windowSize); + }; + ~MovingMedian() = default; + + private: + + void update(double, double) override; + double compute() override; + + QList m_sortedValues; +}; + +class MovingMode : public WindowedStatistics { + public: + MovingMode(int windowSize) + : WindowedStatistics(windowSize) { + m_frequencyOfValues.reserve(windowSize); + }; + ~MovingMode() = default; + + private: + + void update(double, double) override; + double compute() override; + + QHash m_frequencyOfValues; +};