Skip to content

Fix for Bug #1023804: Stop AutoDJ shuffle from putting two of the same song together#287

Merged
daschuer merged 5 commits into
mixxxdj:masterfrom
zr3:master
Aug 3, 2014
Merged

Fix for Bug #1023804: Stop AutoDJ shuffle from putting two of the same song together#287
daschuer merged 5 commits into
mixxxdj:masterfrom
zr3:master

Conversation

@zr3
Copy link
Copy Markdown
Contributor

@zr3 zr3 commented Jul 6, 2014

I've modified the shuffling algorithm to try to keep 1/4 of the length of the playlist between tracks. It works in both normal and extreme situations at an acceptable rate, still taking a fraction of a second shuffling a playlist of 1600+ of the same 80 tracks. Details of the algorithm can be found in the comments and the 2nd commit message. I've tried to adhere to the style guidelines but this is my first time contributing, so I'm all ears if I've missed anything!

zr3 added 2 commits July 3, 2014 03:29
…ces of the same track next to each other. It checks the IDs of the tracks before and after the positions of the tracks that will be swapped, and if there's a match, it tries again with a new position.

It tries a maximum of 10 times, which should be far more than enough for normal situations. It uses the PlaylistTracks table in the database to get its track ID numbers. I didn't notice any significant performance decreases, even with hundreds of the same 10-15 tracks in the AutoDJ playlist.
Here's a brief description of how the algorithm now works:
Loop through the set of tracks to be shuffled:
    1) Set Track A as the current point in the shuffle set
    2) Repeat a maximum of 10 times or until a good place (1/4 of the
       playlist away from a conflict) is reached:
        a) Pick a random track within the shuffle set (Track B)
        b) Check 1/4 of the playlist up and down (clamped at the
           beginning and end) from Track B's position for Track A
        c) Check 1/4 of the playlist up and down (clamped at the
           beginning and end) from Track A's position for Track B
        d) If there was a conflict, store the position if it was better
           than the already stored best position. The position is deemed
           "better" if the distance (square of the difference) of
           the closest conflict (Track B near Track A's position and vv)
           is larger than previous iterations.
3) If no good place was found, use the stored best position
4) Swap Track A and Track B

The "tempTrackDistance" variable in the following might be confusing:
...
            for (int count=startPosition; count < endPosition; count++) {
                if (trackPositionIds.value(count) == trackBId && count != trackBPosition) {
                    conflictFound = true;
                    int tempTrackDistance =
                        (trackAPosition - count) * (trackAPosition - count);
                    if (tempTrackDistance < trackDistance || trackDistance == -1)
                        trackDistance = tempTrackDistance;
                }
            }
...
The reason it is there is to prevent a longer distance from being saved in
the case that there are multiple conflicts within the range.

The section of code that checks around Track A's and Track B's position is
repetitive; I chose to leave it that way for clarity for now but perhaps
it would be better to extract it into a private function?

I have also renamed a few variables and fixed some comment typos for clarity.
@daschuer
Copy link
Copy Markdown
Member

daschuer commented Jul 6, 2014

Hi Zak,

Thank you for the nice feature :-)
I have just tested it and it works like described.

I have just some nit picks:

  • Please double check to so round all operators like = and > with spaces.
  • There seams to be really no performance issue, but to be honest I still did not understand why you try 10 times to get a nice place. What will happens you just add a constant offset when a conflict occurs?

Comment thread src/library/dao/playlistdao.cpp Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we remove this renaming?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean changing shuffleSet to something like shuffledPositions or newPositions? Or do you mean avoiding creating the copied variables in the first place? The reason I did it that way was so that the position and ID data could be passed in as constants, and the algorithm needs to be able to keep track of the newly shuffled positions as it goes along. I could remove the "const" from those two variables in the function declaration and just use them directly, but that differed from the rest of the functions in the class so it seemed like the wrong thing to do.

@zr3
Copy link
Copy Markdown
Contributor Author

zr3 commented Jul 8, 2014

Ah, sorry I didn't address the 10-tries concern earlier. The reason I did it that way was to stay truer to the original algorithm's randomness (it doesn't actually try 10 times for every track; it's just an arbitrary limit to avoid an infinite loop in the extreme situation that the algorithm doesn't find a non-conflicting place randomly, which usually happens when there are more than 3 copies of a track in the playlist). I think if you just added a constant or "slid" the track through the playlist until there were no conflicts (which could also end up being impossible if there are enough duplicate tracks), it would turn into more of a sorting algorithm than a shuffling algorithm. I haven't experimented with that technique, but I would be happy to try if you think that would be better :)

@daschuer
Copy link
Copy Markdown
Member

daschuer commented Jul 9, 2014

Thank you for explaining.
This seams to be good reasons, so keep it like it is.

Made a few changes to match the coding guidelines for Mixxx.
@daschuer
Copy link
Copy Markdown
Member

Hi Zak,

I have just tested your branch and it seams not to work reliable for small playlists.

A 3 track playlist with two doublets should have always the same result
1 A
2 B
3 A

I think searchDistance should be clamped at 1.

It does not work reliable for a 5 tracks Playlist es well. If there is one doublet, It should be always on other track between, but that does not happen in any case.

Should it be ?
for (int count=startPosition; count <= endPosition; count++) {
This does not entirely fix it :-(

(By the way "count" should be renamed to "pos")

May be there is an issue, when swapping the doublets?

Yo may put a lot of qDebug statements int the code to trace it.


An other issue is the "Re-queue tracks after playback" option, often used for radio DJs. At least if this is enabled, you may also test the wrapping case.

Kind regards,

Daniel

@zr3
Copy link
Copy Markdown
Contributor Author

zr3 commented Jul 15, 2014

Thanks for taking the time to look through this code, and I'm sorry there have been problems with it! Good call on clamping searchDistance to 1; I was too focused on large playlists and neglected small ones. I have a quick question: in order to keep the shuffling function cleaner, would I be allowed to import math.h into this class?

I've found the problem with the algorithm when dealing with short playlists; it can get "stuck" in an unsolvable orientation. For example, with 3 tracks containing a doublet (call them ABA), the algorithm could end up randomly ordering the first two as BA, leaving the entire order of the playlist BAA, and when it gets to the final position, it is unable to create a gap between the A's (no matter which position it swaps the final A with, it leaves either AAB or BAA).

I have a couple ideas for dealing with this:

  1. Create a special case for dealing with smaller playlists and playlists with a lot of duplicate tracks in which this is a problem, and sorting them instead of shuffling them
  2. After doing some research, I found that this is a common problem in playlist shuffling algorithms. Spotify came up with a solution by taking inspiration from dithering algorithms (you can find a write-up about their new algorithm here if you're interested). It basically organizes tracks into groups by artist (in our case, we'd do it by ID), then gives each group a random starting point, then spreads each group evenly (with a small amount of randomness) across the length of the playlist.

I'm leaning towards option 2 even though it's totally different than the algorithm I've been working on. It's more elegant, and it's not truly random but I think people are more concerned with the appearance of it being random rather than true randomness :)

@daschuer
Copy link
Copy Markdown
Member

in order to keep the shuffling function cleaner, would I be allowed to import math.h into this class?

Yes of cause. Do not reinvent the wheel ;-)

The ABA might be too special for creating a special case for it. What does the user expect when randomize it, especially if we decide to consider the re-queue option.

  1. ....
    Uhh, thats an other cool topic, Which deserves a own project ....
    The cluster Idea is very nice. We may put only songs from one genre into a cluster or with matching BPMs
    This will IMHO produce a very good experience if we are going to broadcast this in a half automated way.
    Keep going ....

But IMHO we should first merge this. What is the smallest sensible play-list that should work with the current algorithm? Can we define one, just to sort out bugs?
What about "ABCDEE" is it possible to "fix" the algorithm in away, that there is always a gap between E and E and "EABCDE" also not happend?

I think the current algorithm works already much better than the original one. So if you can explain the issues (they are no bugs) we can merge it without issue special cases.

The algorithm now "wraps" around the playlist, so that if the tracks are re-queued they will still be 1/4 of the playlist apart.
The most extreme case that this algorithm can now handle is a 4-track playlist with two copies of one of the tracks.
@zr3
Copy link
Copy Markdown
Contributor Author

zr3 commented Jul 31, 2014

Sorry for the delay in finishing this! I've finished implementing your suggestions. I solved the re-queue issue by "wrapping" the search around the playlist (e.g. if part of the searchDistance range is beyond the end of the playlist, it will search the remainder of the distance at the beginning). I also found an off-by-one bug that was causing issues.

It appears that the limit to the algorithm (even with two doublets) is 4 tracks. Due to the random nature of the algorithm, it is still possible for two tracks to be placed next to each other in this case, but it's unlikely (it seems to be around a 5% chance), in which case the playlist can just be shuffled again (although realistically, I don't think playlists are going to be this small in practice). However, it doesn't always look like the tracks are shuffling when there are 4 tracks with two doublets (since the duplicates will swap with each other), so I'd suggest a practical limit of no more extreme of a case than a 4-track playlist with one doublet :)

@daschuer
Copy link
Copy Markdown
Member

Sorry for the delay in finishing this!

Hey, we re all spare time workers :-). Thank you for you time.

I have just tested you branch and it seams that all issues are fixed.

I have just a minor code style issue.
We use 8 space indentation for line wraps in function calls and if clauses.
Can you please check you code and fix this. Thank you.

@ALL: Any objection to merge this to master?

Comment thread src/library/dao/playlistdao.cpp Outdated
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just want to check to make sure, does placing this utility function here (and not making it a private member of the class) fit within the style?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have no style guide about this, but since it is only used here, it is more natural to be a private member.

@daschuer
Copy link
Copy Markdown
Member

daschuer commented Aug 3, 2014

@zak-reynolds: Thank you for polishing. IMHO we can merge this now. Are you finished with your work?

@zr3
Copy link
Copy Markdown
Contributor Author

zr3 commented Aug 3, 2014

Yup! As long as you're happy with the style, I consider it done. I think the spotify-like algorithm is a cool idea for the future, but there are other things that are more important to work on now.

daschuer added a commit that referenced this pull request Aug 3, 2014
Fix for Bug #1023804: Stop AutoDJ shuffle from putting two of the same song together
@daschuer daschuer merged commit a75da7e into mixxxdj:master Aug 3, 2014
m0dB pushed a commit to m0dB/mixxx that referenced this pull request Jan 21, 2024
add links to mastodon including verification
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants