Skip to content

Commit

Permalink
Make use of new rounding functions in calculateBpm()
Browse files Browse the repository at this point in the history
  • Loading branch information
daschuer committed Feb 11, 2021
1 parent 6f5edd6 commit c7118d4
Show file tree
Hide file tree
Showing 3 changed files with 8 additions and 315 deletions.
2 changes: 1 addition & 1 deletion src/track/beatmap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,7 @@ double BeatMap::calculateBpm(const Beat& startBeat, const Beat& stopBeat) const
return -1;
}

return BeatUtils::calculateBpm(beatvect, mixxx::audio::SampleRate(m_iSampleRate), 0, 9999);
return BeatUtils::calculateBpm(beatvect, mixxx::audio::SampleRate(m_iSampleRate));
}

} // namespace mixxx
273 changes: 6 additions & 267 deletions src/track/beatutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,6 @@

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

// 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

static bool sDebug = false;

const int kHistogramDecimalPlaces = 2;
const double kHistogramDecimalScale = pow(10.0, kHistogramDecimalPlaces);
const double kBpmFilterTolerance = 1.0;

// When ironing the grid for long sequences of const tempo we use
// a 25 ms tolerance because this small of a difference is inaudible
// This is > 2 * 12 ms, the step width of the QM beat detector
Expand All @@ -42,268 +28,21 @@ constexpr int kMinRegionBeatCount = 16;

} // namespace


// Given a sorted set of numbers, find the sample median.
// http://en.wikipedia.org/wiki/Median#The_sample_median
double BeatUtils::computeSampleMedian(const QList<double>& 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);
}

QList<double> BeatUtils::computeWindowedBpmsAndFrequencyHistogram(
const QVector<double>& beats,
const int windowSize,
const int windowStep,
const int sampleRate,
QMap<double, int>* frequencyHistogram) {
QList<double> averageBpmList;
for (int i = windowSize; 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 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;
}
return averageBpmList;
}

double BeatUtils::computeFilteredWeightedAverage(
const QMap<double, int>& frequencyTable,
const double filterCenter,
const double filterTolerance,
QMap<double, int>* filteredFrequencyTable) {
double filterWeightedAverage = 0.0;
int filterSum = 0;
QMapIterator<double, int> i(frequencyTable);

while (i.hasNext()) {
i.next();
const double value = i.key();
const int frequency = i.value();

if (fabs(value - filterCenter) <= filterTolerance) {
// TODO(raffitea): Why > 1 ?
if (i.value() > 1) {
filterSum += frequency;
filterWeightedAverage += value * frequency;
filteredFrequencyTable->insert(i.key(), frequency);
if (sDebug) {
qDebug() << "Filtered Table:" << value
<< "Frequency:" << frequency;
}
}
}
}
if (sDebug) {
qDebug() << "Sum of filtered frequencies: " << filterSum;
}
if (filterSum == 0) {
return filterCenter;
}
return filterWeightedAverage / static_cast<double>(filterSum);
}

double BeatUtils::calculateBpm(const QVector<double>& beats,
const mixxx::audio::SampleRate& sampleRate,
int min_bpm,
int max_bpm) {
int SampleRate = sampleRate;
/*
* Let's compute the average local
* BPM for N subsequent beats.
* The average BPMs are
* added to a list from which the statistical
* median is computed
*
* N=12 seems to work great; We coincide with Traktor's
* BPM value in many case but not worse than +-0.2 BPM
*/
/*
* Just to demonstrate how you would count the beats manually
*
* Beat numbers: 1 2 3 4 5 6 7 8 9
* Beat positions: ? ? ? ? |? ? ? ? | ?
*
* Usually one measures the time of N beats. One stops the timer just before
* the (N+1)th beat begins. The BPM is then computed by 60*N/<time needed
* to count N beats (in seconds)>
*
* Although beat tracking through QM is promising, the local average BPM of
* 4 beats varies frequently by +-2 BPM. Sometimes there N subsequent beats
* in the grid that are computed wrongly by QM.
*
* Their local BPMs can be considered as outliers which would influence the
* BPM computation negatively. To exclude outliers, we select the median BPM
* over a window of N subsequent beats.
* To do this, we take the average local BPM for every N subsequent
* beats. We then sort the averages and take the middle to find the median
* BPM.
*/

const mixxx::audio::SampleRate& sampleRate) {
if (beats.size() < 2) {
return 0;
}

// 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) {
return 60.0 * (beats.size()-1) * SampleRate / (beats.last() - beats.first());
}

QMap<double, int> frequency_table;
QList<double> average_bpm_list = computeWindowedBpmsAndFrequencyHistogram(
beats, N, 1, SampleRate, &frequency_table);

// Get the median BPM.
std::sort(average_bpm_list.begin(), average_bpm_list.end());
const double median = computeSampleMedian(average_bpm_list);

/*
* Okay, let's consider the median an estimation of the BPM To not solely
* rely on the median, we build the average weighted value of all bpm values
* being at most +-1 BPM from the median away. Please note, this has
* improved the BPM: While relying on median only we may have a deviation of
* about +-0.2 BPM, taking into account BPM values around the median leads
* to deviation of +- 0.05 Please also note that this value refers to
* electronic music, but to be honest, the BPM detection of Traktor and Co
* work best with electronic music, too. But BPM detection for
* non-electronic music isn't too bad.
*/

//qDebug() << "BPM range between " << min_bpm << " and " << max_bpm;

// a subset of the 'frequency_table', where the bpm values are +-1 away from
// the median average BPM.
QMap<double, int> filtered_bpm_frequency_table;
const double filterWeightedAverageBpm = computeFilteredWeightedAverage(
frequency_table, median, kBpmFilterTolerance, &filtered_bpm_frequency_table);

if (sDebug) {
qDebug() << "Statistical median BPM: " << median;
qDebug() << "Weighted Avg of BPM values +- 1BPM from the media"
<< filterWeightedAverageBpm;
if (beats.size() < kMinRegionBeatCount) {
return 60.0 * (beats.size() - 1) * sampleRate / (beats.last() - beats.first());
}

/*
* Although we have a minimal deviation of about +- 0.05 BPM units compared
* to Traktor, this deviation may cause the beat grid to look unaligned,
* especially at the end of a track. Let's try to get the BPM 'perfect' :-)
*
* Idea: Iterate over the original beat set where some detected beats may be
* wrong. The beat is considered 'correct' if the beat position is within
* epsilon of a beat grid obtained by the global BPM.
*
* If the beat turns out correct, we can compute the error in BPM units.
* E.g., we can check the original beat position after 60 seconds. Ideally,
* the approached beat is just a couple of samples away, i.e., not worse
* than 0.05 BPM units. The distance between these two samples can be used
* for BPM error correction.
*/

double perfect_bpm = 0;
double firstCorrectBeatSample = beats.first();
bool foundFirstCorrectBeat = false;

int counter = 0;
int perfectBeats = 0;
for (int i = N; i < beats.size(); i += 1) {
// get start and end sample of the beats
double beat_start = beats.at(i-N);
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;
// 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) {
firstCorrectBeatSample = beat_start;
foundFirstCorrectBeat = true;
if (sDebug) {
qDebug() << "Beat #" << (i - N)
<< "is considered as reference beat with BPM:"
<< local_bpm;
}
}
if (foundFirstCorrectBeat) {
if (counter == 0) {
counter = N;
} else {
counter += 1;
}
double time2 = (beat_end - firstCorrectBeatSample) / SampleRate;
double correctedBpm = 60 * counter / time2;

if (fabs(correctedBpm - filterWeightedAverageBpm) <= BPM_ERROR) {
perfect_bpm += correctedBpm;
++perfectBeats;
if (sDebug) {
qDebug() << "Beat #" << (i-N)
<< "is considered as correct -->BPM improved to:"
<< correctedBpm;
}
}
}
}

const double perfectAverageBpm = perfectBeats > 0 ?
perfect_bpm / perfectBeats : filterWeightedAverageBpm;

// 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);

// Finally, restrict the BPM to be within min_bpm and max_bpm.
const double maybeRoundedBpm = perform_rounding ? rounded_bpm : perfectAverageBpm;
const double constrainedBpm = constrainBpm(maybeRoundedBpm, min_bpm, max_bpm, false);

if (sDebug) {
qDebug() << "SampleMedianBpm=" << median;
qDebug() << "FilterWeightedAverageBpm=" << filterWeightedAverageBpm;
qDebug() << "Perfect BPM=" << perfectAverageBpm;
qDebug() << "Rounded Perfect BPM=" << rounded_bpm;
qDebug() << "Rounded difference=" << bpm_diff;
qDebug() << "Perform rounding=" << perform_rounding;
qDebug() << "Constrained to Range [" << min_bpm << "," << max_bpm << "]=" << constrainedBpm;
}
return constrainedBpm;
QVector<BeatUtils::ConstRegion> constantRegions =
retrieveConstRegions(beats, sampleRate);
return makeConstBpm(constantRegions, sampleRate, nullptr);
}

QVector<BeatUtils::ConstRegion> BeatUtils::retrieveConstRegions(
Expand Down
48 changes: 1 addition & 47 deletions src/track/beatutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,42 +13,8 @@ class BeatUtils {
double beatLength;
};

static double constrainBpm(double bpm, const int min_bpm,
const int max_bpm, bool aboveRange) {
if (bpm <= 0.0 || min_bpm < 0 || max_bpm < 0 ||
min_bpm >= max_bpm ||
(bpm >= min_bpm && bpm <= max_bpm)) {
return bpm;
}

if (isnan(bpm) || isinf(bpm)) {
return 0.0;
}

if (!aboveRange) {
while (bpm > max_bpm) {
bpm /= 2.0;
}
}
while (bpm < min_bpm) {
bpm *= 2.0;
}

return bpm;
}


/*
* This method detects the BPM given a set of beat positions.
* We compute the average local BPM of by considering 8 beats
* at a time. Internally, a sorted list of average BPM values is constructed
* from which the statistical median is computed. This value provides
* a pretty good guess of the global BPM value.
*/
static double calculateBpm(const QVector<double>& beats,
const mixxx::audio::SampleRate& sampleRate,
int min_bpm,
int max_bpm);
const mixxx::audio::SampleRate& sampleRate);

static QVector<ConstRegion> retrieveConstRegions(
const QVector<double>& coarseBeats,
Expand All @@ -68,17 +34,5 @@ class BeatUtils {
static QVector<double> getBeats(const QVector<ConstRegion>& constantRegions);

private:
static double computeSampleMedian(const QList<double>& sortedItems);
static double computeFilteredWeightedAverage(
const QMap<double, int>& frequencyTable,
const double filterCenter,
const double filterTolerance,
QMap<double, int>* filteredFrequencyTable);
static QList<double> computeWindowedBpmsAndFrequencyHistogram(
const QVector<double>& beats,
const int windowSize,
const int windowStep,
const int sampleRate,
QMap<double, int>* frequencyHistogram);
static double roundBpmWithinRange(double minBpm, double centerBpm, double maxBpm);
};

0 comments on commit c7118d4

Please sign in to comment.