-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changing tempos support #2930
base: main
Are you sure you want to change the base?
Changing tempos support #2930
Conversation
My impressions from some brief testing. First, wow! I can set useful loops on jazz tracks! This is going to be awesome with #2194! In general, this does quite well with tracks that have a constant tempo. However, it often gets thrown off in sections where there is not a prominent repeating rhythm even though I know the tempo there is really the same as the section before because the whole track was clearly made in a DAW set at one specific tempo. I have no ideas how the analyzer could do better in these cases other than the user manually telling it that there is a constant tempo, so maybe #2804 would be useful after all. This does well enough that I think we can use it by default and leave it to the user to manually force a constant tempo when desired. The analyzer frequently says the tempo is double what it really is. I think this happens more often than in master, but I haven't tried to quantify that. |
I suspect the stability of the momentary BPM shown to the user could improve considerably when we have the downbeats and can report the tempo of the current bar. |
Perhaps if the confidence in the beat positions is low, disregard the detected beats for that phrase or section and extrapolate from the surroundings. This could obviate the need for much manual intervention compared to the algorithm in this PR. Of course, it would help to have the sections and phrases identified for this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a good progress, Thank you.
I have left some comments.
Is this ready for master merge finally?
What is the input? The original QM beats with the ironed tempo changes?
We need to consider the right stack of changes ...
What are your ideas?
src/track/beatfactory.cpp
Outdated
@@ -119,7 +119,8 @@ 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); | |||
auto fixedBeats = BeatUtils::FixBeatmap(beats, iSampleRate, iMinBpm, iMaxBpm); | |||
mixxx::BeatMap* pBeatMap = new mixxx::BeatMap(track, iSampleRate, fixedBeats); | |||
pBeatMap->setSubVersion(subVersion); | |||
return mixxx::BeatsPointer(pBeatMap, &BeatFactory::deleteBeats); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Legacy issue:
I think we should immediately hand over the raw pointer after construction to clarify the ownership.
subVersion can also be set during the constructor.
return mixxx::BeatsPointer(
new mixxx::BeatMap(track, iSampleRate, fixedBeats, subVersion),
&BeatFactory::deleteBeats);
The same above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No constructor of beatmap receives subversion as parameter, since we are getting rid of that anyways I think it's not worth the trouble, right?
src/track/beatutils.cpp
Outdated
#include "util/math.h" | ||
|
||
// 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 kBpmError = 0.05; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kMaxBpmError
src/track/beatutils.cpp
Outdated
@@ -29,20 +31,178 @@ const int kHistogramDecimalPlaces = 2; | |||
const double kHistogramDecimalScale = pow(10.0, kHistogramDecimalPlaces); | |||
const double kBpmFilterTolerance = 1.0; | |||
|
|||
QMap<int, double> BeatUtils::findTempoChanges( | |||
QMap<double, int> tempoFrequency, QList<double> tempoList) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This whole PR contains some formatting issues. Did you try to install our pre-commit hook?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only clang-format.
I will double check that
src/track/beatutils.cpp
Outdated
// Here we are going to track the tempo changes over the track | ||
for (double tempo : tempoList) { | ||
currentBeat += 1; | ||
double newStableTempo = filterTempo(tempo); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Due to the median we introduce a delay of 1/2 median window. Is this considered?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is considered when assigning currrentBeat value to lastBeatChanhge
src/track/beatutils.cpp
Outdated
if (newStableTempo == stableTemposAndPositions.last()) { | ||
continue; | ||
} else if (stableTemposAndPositions.last() != tempoFrequency.lastKey() and | ||
newStableTempo == (tempoFrequency.find(stableTemposAndPositions.last()) + 1).key()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is hard to understand. I guess we need some more comments.
src/track/beatutils.cpp
Outdated
MovingMedian filterTempo(5); // 5 is the lenght our window | ||
int currentBeat = -1; | ||
int lastBeatChange = 0; | ||
QMap<int, double> stableTemposAndPositions; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this "stableTemposByPosition"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, key is position of a new stable tempo and value the tempo
src/track/beatutils.cpp
Outdated
// because find will return an iterator pointing to end that we will * | ||
if (tempoFrequency.contains(newStableTempo)) { | ||
lastBeatChange = currentBeat - filterTempo.lag(); | ||
stableTemposAndPositions[lastBeatChange] = newStableTempo; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder why the kMaxSecsPhaseError is not relevant here?
Lets say we have a continuously increasing tempo. We can keep the grid constant until the kMaxSecsPhaseError is exceeded, right?
Once we are behind by 25 ms we can try to catch up with a faster tempo, but will fall behind soon because of the increasing tempo.
So we are always behind ... :-/ difficult.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we do not care about the beat positions just trying to understand tempo changes.
src/track/beatutils.cpp
Outdated
auto splittedAtTempoChange = QVector<double>::fromStdVector(std::vector<double>( | ||
rawBeats.begin() + beatStart, rawBeats.begin() + beatEnd)); | ||
double bpm = calculateBpm(splittedAtTempoChange, sampleRate, minBpm, maxBpm); | ||
fixedBeats << calculateFixedTempoBeatMap(splittedAtTempoChange, sampleRate, bpm); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, Can we force a minimum fixed region of one measure? Can we align the change to downbeats?
That makes musically pretty much sense. But do we have the info?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's for #2877, we dont have that info here
After addressing the issues you mention I believe this is is ready to merge. |
I am concerned that splitting the work between these two branches is creating more work for no good reason. |
This is, almost, ready to merge and is self contained. |
Since, it's a hard requirement for us to have code merged into master by the end of summer, in a worst case scenario this can be that code. |
Also, #2877 can be a experimental, work only, branch for implementing the new features, but then each one can be merged in a individual PR like this one? |
@ywwg did you get a chance to try this? |
This should be ready to merge now? |
src/track/beatutils.cpp
Outdated
void BeatUtils::RemoveSmallArrhythmic( | ||
QVector<double>& rawBeats, int sampleRate, QMap<int, double> &stableTemposByPosition) { | ||
// A common problem the analyzer has is to detect arrhythmic regions | ||
// of tacks with a constant tempo as in a different unsteady tempo. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// of tacks with a constant tempo as in a different unsteady tempo. | |
// of tracks with a constant tempo as a different, unsteady tempo. |
src/track/beatutils.cpp
Outdated
// We arbitraly remove these arrhythmic regions if they are short than 16s | ||
auto positionsWithTempoChange = stableTemposByPosition.keys(); | ||
auto tempoValues = stableTemposByPosition.values(); | ||
QVector<double> fixedBeats; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unclear which meaning of "fixed" this is using. Are the beats "fixed" as in corrected, or "fixed" as in unmoved?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed to anchored
The new commit is a big improvement for handling sections without loud rhythms. It still sometimes detects significant speed ups in these sections, but I don't see the huge fluctuations like there were before. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some more comments
src/track/beatstats.cpp
Outdated
max = tempo.value(); | ||
} | ||
} | ||
return mode; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This produces a compiler warning:
/home/daniel/workspace/advanced_autodj_2/src/track/beatstats.cpp:29:12: warning: ‘mode’ may be used uninitialized in this function [-Wmaybe-uninitialized]
return mode;
src/track/beatutils.cpp
Outdated
int beatsToFilterMeterChanges = (10 / (60 / median)) * 2; | ||
if (!(beatsToFilterMeterChanges % 2)) {beatsToFilterMeterChanges += 1;} | ||
MovingMedian filterTempo(beatsToFilterMeterChanges); | ||
MovingMode stabilzeTempo(beatsToFilterMeterChanges); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we write the lines above as
auto medianFilterTempo = MovingMedian(beatsToFilterMeterChanges);
auto modeFilterTempo = MovingMode(beatsToFilterMeterChanges);
And with improved names?
This is more pleasant to read.
src/track/beatutils.cpp
Outdated
} | ||
// Since we use the median as a guess first and last tempo | ||
// And it can not have values outside from tempoFrequency | ||
const double median = computeSampleMedian(sortedTempoList); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this a BPM value?
Can we call it "medianBpm" to clarify this?
src/track/beatutils.cpp
Outdated
// 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. | ||
int beatsToFilterMeterChanges = (10 / (60 / median)) * 2; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
beats... sounds as it is a list of beats.
This is the window size for the moving filters, right?
Can we find a name that reflects this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FilteringWindowInBeats? Do you have suggestions?
src/track/beatutils.cpp
Outdated
rawBeats.begin(), rawBeats.begin() + positionsWithTempoChange[1])); | ||
|
||
for (int i = 2; i < positionsWithTempoChange.size(); i+=1) { | ||
int smallInBeats = 16 / (60 / tempoValues[i-1]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is this value bpm?
src/track/beatutils.cpp
Outdated
if (lenghtOfChange <= smallInBeats) { | ||
stableTemposByPosition.remove(limitAtLeft); | ||
double beatOffset = rawBeats[limitAtLeft]; | ||
double beatLength = floor(((60.0 * sampleRate) / previousTempo) + 0.5); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the rounding for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that needs to be removed.
If we add beats here, the later ironing will treat them as strong reference points and will apply the 0,25 ms rule to it. But that is not necessary. We can just advance to the next beat and put them later exactly on the calculated average.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm, just removing the beats was my first idea but that does not work without an additional structure to keep track that we need to add the beats in this region later, and the big jump will result in very large beat length that will completely mess the average so removing them adds a lot of bookkeeping.
Maybe we can set the beats to -1 or something like that and use that idea of beats not increasing as a way to tell not to consider these beats on iron code and just add the avarage length. I will try that, should work...
I'm getting a bunch of warnings with Qt 5.15:
|
From the compare results, I think this is not yet in a merge-able state. Instead of a smoothly floating bpm, we have corrective spikes with a different bpm. I think to something like this.
Than, pick a beat in the middle and keep it const until 0,25 ms offset is exceeded. at both ends. Mask single exceptional beats 25 ms ... 100 ms difference to the const grid build up by two neighbors at each side,
|
Yes, I agree that this is strategy is not very good. I will work on improve this. |
These are because of the new range constructors that Qt introduced on 5.14 that deprecates the fromStd construct. But since we need to keep it compatible with Qt 5.12 I used the old version. Is there another solution for this? |
Here is the document: I have tried to implement my idea in a spreadsheet: blue: oiginal This is a version with constant regions not more than 25 ms off. |
Comments and small styles changes Co-authored-by: Be <be.0@gmx.com> Co-authored-by: Jan Holthuis <holthuis.jan@googlemail.com>
Do you have sample tracks? |
I had a look into "The Learning Process" https://www.youtube.com/watch?v=7pMtdXx-mME The other issue is that in the second part the Tshack beats are marked and not the IMHO more correct Boom beats. |
src/track/beatfactory.cpp
Outdated
resultHeader = "\nRaw beat lenght\n"; | ||
debugFile.write(resultHeader.toLocal8Bit()); | ||
debugFile.write(sRawBeatLenght.toLocal8Bit()); | ||
resultHeader = "\nCorrected beat lenght\n"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resultHeader = "\nCorrected beat lenght\n"; | |
resultHeader = "\nCorrected beat length\n"; |
src/track/beatfactory.cpp
Outdated
resultHeader = "\nCorrected beats\n"; | ||
debugFile.write(resultHeader.toLocal8Bit()); | ||
debugFile.write(sCorrectedBeats.toLocal8Bit()); | ||
resultHeader = "\nRaw beat lenght\n"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resultHeader = "\nRaw beat lenght\n"; | |
resultHeader = "\nRaw beat length\n"; |
…ate type and some more comments, names and styles improvements
@crisclacerda: Will you find time to finish the PR? I would like to take this into account when doing the final evaluations. |
There are still a handful of unresolved review comments. Please mark them resolved if you have fixed them or they're no longer relevant. |
I have added crisclacerda#1 do make the mode calculation more universal, please have a look. |
I have some ideas to improve the Arrythmic removal code. But this part will probably become obsolete once we have the down-beat/phase detection in place. |
connect(bIron, SIGNAL(stateChanged(int)), this, SLOT(ironingEnabled(int))); | ||
connect(bRemoveArrythmic, SIGNAL(stateChanged(int)), this, SLOT(removeArrythmicEnabled(int))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use new-style signal/slot syntax.
// 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<double>& sortedItems); | ||
// The mode is most repeated value in a sequence | ||
static double mode(const QHash<int, int>& frequencyOfValues); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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<double>& sortedItems); | |
// The mode is most repeated value in a sequence | |
static double mode(const QHash<int, int>& frequencyOfValues); | |
/// 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<double>& sortedItems); | |
/// The mode is most repeated value in a sequence | |
static double mode(const QHash<int, int>& frequencyOfValues); |
I gave this branch a test and tried to analyze 4k songs today.
From the 4k tracks, 99.9% are fixed bpm tracks. Some tracks ara analyzed as 96.67, after correcting with 2/3 they are somethinge like 148.00189362, so they get desynced at the end of the track. |
Thank you for you test results, they are very helpful The crash happens in TrackDAO::onPurgingTracks, so probably unrelated to this PR. Are you able to reproduce the crash? It sounds if we basically have the same jitter issue like with constant beat grid. This is good news, because we don't do rounding here. But If we add a rounding that is below the sample rate, we can likely fix the issue. Can you provide two or more tracks that demonstrate the issue the best? Can you also explain where on a beat you like to put the beat marker? A screenshot would be nice, showing the analyzer result and the manual tweaked beats side by side. |
This PR is marked as stale because it has been open 90 days with no activity. |
This is my working product submission for GSoC 2020.
While the core of my project was adding a new rhythm analyzer #2877 that is still a working a progress.
The new analyzer however relies heavy on the current beat detector which works quite well, but suffers from two main issues that his pull request tries to solve and are necessary for detecting downbeat and phrases.
Previously Mixxx solved this issue by not allowing any tempo changes and making a perfect fixed tempo grid using the beat detector output only to compute the average tempo and the most likely beat phase, while that work very well for tracks that have a unique constant tempo it failed heavy on tracks that have a abrupt tempo change or an unsteady tempo.
This pull request on the other hand attempts to find regions of stable tempo and make a independently grid for each of them. To allow unsteady tempos, instead of making a fixed tempo grid it tries to attempts to a grid for all the beats that fall within a 25ms threshold which is close to the human hearing perception threshold of distinguishable sounds.
It works very well for unsteady tempo and is able to follow smooth or abrupt tempo changes well while keep a constant grid, but still fails on some case when the beat detector output is "very" problematic and does not completely eliminate the need for current fixed tempo grid approach.
This can still be improved by combining the fixed grid approach with the stable regions and make one fixed grid instead of the ironing approach of 25ms, the challenge is then to determine which algorithm to use.
Some comparisons with the 2.3 results, showing where it does well and where it still fails: (blue is 2.3, red this PR)
Tracks with unsteady tempos:

Tracks that have more than one constant tempo:

Track with one constant tempo:
