Add album shuffle

This commit is contained in:
Christopher Eby 2011-09-23 22:17:07 -05:00
parent f194262c99
commit 370514a316
6 changed files with 215 additions and 52 deletions

View File

@ -31,8 +31,10 @@ THE SOFTWARE.
<string name="display_mode">Display Mode</string>
<string name="shuffle_enable">Enable Shuffle</string>
<string name="shuffle_disable">Disable Shuffle</string>
<string name="shuffle_enabling">Shuffle enabled</string>
<string name="shuffle_disabling">Shuffle disabled</string>
<string name="shuffle_albums">Shuffle Albums</string>
<string name="shuffle_songs_enabled">Song shuffle enabled</string>
<string name="shuffle_albums_enabled">Album shuffle enabled</string>
<string name="shuffle_disabled">Shuffle disabled</string>
<string name="repeat_enable">Enable Repeat</string>
<string name="repeat_current">Repeat Song</string>
<string name="repeat_disable">Disable Repeat</string>

View File

@ -22,14 +22,16 @@
package org.kreed.vanilla;
import java.io.File;
import java.util.Random;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
public class MediaUtils {
/**
@ -294,6 +296,87 @@ public class MediaUtils {
}
}
/**
* Shuffle a Song list using Fisher-Yates algorithm.
*
* @param albumShuffle If true, preserve the order of tracks inside albums.
*/
public static void shuffle(List<Song> list, boolean albumShuffle)
{
int size = list.size();
if (size < 2)
return;
Random random = getRandom();
if (albumShuffle) {
Song[] songs = list.toArray(new Song[size]);
Song[] temp = new Song[size];
// Make sure the albums are in order
Arrays.sort(songs);
// This is Fisher-Yates algorithm, but it swaps albums instead of
// single elements.
for (int i = size; --i != -1; ) {
Song songI = songs[i];
if (i > 0 && songs[i - 1].albumId == songI.albumId)
// This index is not the start of an album. Skip it.
continue;
int j = random.nextInt(i + 1);
while (j > 0 && songs[j - 1].albumId == songs[j].albumId)
// This index is not the start of an album. Find the start.
j -= 1;
int lowerStart = Math.min(i, j);
int upperStart = Math.max(i, j);
if (lowerStart == upperStart)
// Swap with ourself. That was easy!
continue;
long lowerAlbum = songs[lowerStart].albumId;
int lowerEnd = lowerStart;
while (lowerEnd + 1 < size && songs[lowerEnd + 1].albumId == lowerAlbum)
lowerEnd += 1;
long upperAlbum = songs[upperStart].albumId;
int upperEnd = upperStart;
while (upperEnd + 1 < size && songs[upperEnd + 1].albumId == upperAlbum)
upperEnd += 1;
int lowerSize = lowerEnd - lowerStart + 1;
int upperSize = upperEnd - upperStart + 1;
if (lowerSize == 1 && upperSize == 1) {
// Easy, single element swap
Song tempSong = songs[lowerStart];
songs[lowerStart] = songs[upperStart];
songs[upperStart] = tempSong;
} else {
// Slow multi-element swap. Copy to a new array in the
// swapped order.
System.arraycopy(songs, 0, temp, 0, lowerStart); // copy elements before lower
System.arraycopy(songs, upperStart, temp, lowerStart, upperSize); // copy upper elements to lower spot
System.arraycopy(songs, lowerEnd + 1, temp, lowerStart + upperSize, upperStart - lowerEnd - 1); // copy elements between upper and lower
System.arraycopy(songs, lowerStart, temp, lowerStart + upperEnd - lowerEnd, lowerSize); // copy lower elements to upper spot
System.arraycopy(songs, upperEnd + 1, temp, upperEnd + 1, size - upperEnd - 1); // copy elements remaining elements after upper
// New array is finished. Use the old array as temp for the
// next iteration.
Song[] tempTemp = songs;
songs = temp;
temp = tempTemp;
}
}
list.clear();
list.addAll(Arrays.asList(songs));
} else {
Collections.shuffle(list, random);
}
}
/**
* Determine if any songs are available from the library.
*

View File

@ -325,8 +325,22 @@ public class PlaybackActivity extends Activity
public boolean onPrepareOptionsMenu(Menu menu)
{
int state = mState;
boolean isShuffling = (state & PlaybackService.FLAG_SHUFFLE) != 0;
menu.findItem(MENU_SHUFFLE).setTitle(isShuffling ? R.string.shuffle_disable : R.string.shuffle_enable);
int shuffleRes;
switch (PlaybackService.shuffleMode(state)) {
default:
case SongTimeline.SHUFFLE_NONE:
shuffleRes = R.string.shuffle_enable;
break;
case SongTimeline.SHUFFLE_SONGS:
shuffleRes = R.string.shuffle_albums;
break;
case SongTimeline.SHUFFLE_ALBUMS:
shuffleRes = R.string.shuffle_disable;
break;
}
menu.findItem(MENU_SHUFFLE).setTitle(shuffleRes);
int repeatRes;
if ((state & PlaybackService.FLAG_REPEAT) != 0)
repeatRes = R.string.repeat_current;
@ -335,9 +349,11 @@ public class PlaybackActivity extends Activity
else
repeatRes = R.string.repeat_enable;
menu.findItem(MENU_REPEAT).setTitle(repeatRes);
boolean isRandom = (state & PlaybackService.FLAG_RANDOM) != 0;
// TODO: find icon (dice? arrow pointing in many directions?)
menu.findItem(MENU_RANDOM).setTitle(isRandom ? R.string.random_disable : R.string.random_enable);
return true;
}
@ -346,7 +362,7 @@ public class PlaybackActivity extends Activity
{
switch (item.getItemId()) {
case MENU_SHUFFLE:
toggleShuffle();
cycleShuffle();
return true;
case MENU_REPEAT:
cycleRepeat();
@ -371,11 +387,23 @@ public class PlaybackActivity extends Activity
/**
* Toggle shuffle mode on/off
*/
public void toggleShuffle()
public void cycleShuffle()
{
int state = PlaybackService.get(this).toggleShuffle();
int res = (state & PlaybackService.FLAG_SHUFFLE) == 0 ? R.string.shuffle_disabling : R.string.shuffle_enabling;
Toast.makeText(this, res, Toast.LENGTH_SHORT).show();
int state = PlaybackService.get(this).cycleShuffle();
int shuffleRes;
switch (PlaybackService.shuffleMode(state)) {
default:
case SongTimeline.SHUFFLE_NONE:
shuffleRes = R.string.shuffle_disabled;
break;
case SongTimeline.SHUFFLE_SONGS:
shuffleRes = R.string.shuffle_songs_enabled;
break;
case SongTimeline.SHUFFLE_ALBUMS:
shuffleRes = R.string.shuffle_albums_enabled;
break;
}
Toast.makeText(this, shuffleRes, Toast.LENGTH_SHORT).show();
setState(state);
}
@ -441,7 +469,7 @@ public class PlaybackActivity extends Activity
cycleRepeat();
break;
case ACTION_SHUFFLE:
toggleShuffle();
cycleShuffle();
break;
case ACTION_RANDOM:
toggleRandom();

View File

@ -65,7 +65,7 @@ public final class PlaybackService extends Service implements Handler.Callback,
/**
* State file version that indicates data order.
*/
private static final int STATE_VERSION = 2;
private static final int STATE_VERSION = 3;
private static final int NOTIFICATION_ID = 2;
@ -114,19 +114,14 @@ public final class PlaybackService extends Service implements Handler.Callback,
*/
public static final String ACTION_PREVIOUS_SONG_AUTOPLAY = "org.kreed.vanilla.action.PREVIOUS_SONG_AUTOPLAY";
/**
* Set when there is no media available on the device.
*/
public static final int FLAG_NO_MEDIA = 0x2;
/**
* If set, music will play.
*/
public static final int FLAG_PLAYING = 0x1;
/**
* If set, songs selected from the library and repeated songs will be in
* random order.
* Set when there is no media available on the device.
*/
public static final int FLAG_SHUFFLE = 0x4;
public static final int FLAG_NO_MEDIA = 0x2;
/**
* If set, will loop back to the beginning of the timeline when its end is
* reached.
@ -150,6 +145,10 @@ public final class PlaybackService extends Service implements Handler.Callback,
* instead of advancing to the next song.
*/
public static final int FLAG_REPEAT_CURRENT = 0x80;
/**
* These two bits will be one of SongTimeline.SHUFFLE_*.
*/
public static final int MASK_SHUFFLE = 0x100 | 0x200;
public static final int NEVER = 0;
public static final int WHEN_PLAYING = 1;
@ -463,8 +462,8 @@ public final class PlaybackService extends Service implements Handler.Callback,
}
}
if ((toggled & FLAG_SHUFFLE) != 0)
mTimeline.setShuffle((state & FLAG_SHUFFLE) != 0);
if ((toggled & MASK_SHUFFLE) != 0)
mTimeline.setShuffleMode(shuffleMode(state));
if ((toggled & (FLAG_REPEAT | FLAG_RANDOM)) != 0) {
int action;
if ((state & FLAG_RANDOM) != 0)
@ -593,14 +592,15 @@ public final class PlaybackService extends Service implements Handler.Callback,
}
/**
* Toggle shuffle mode.
* Cycle shuffle mode.
*
* @return The new state after this is called.
*/
public int toggleShuffle()
public int cycleShuffle()
{
synchronized (mStateLock) {
return updateState(mState ^ FLAG_SHUFFLE);
int step = (mState & MASK_SHUFFLE) == 0x200 ? 0x200 : 0x100;
return updateState(mState + step);
}
}
@ -1178,8 +1178,7 @@ public final class PlaybackService extends Service implements Handler.Callback,
state |= FLAG_RANDOM;
else if (finishAction == SongTimeline.FINISH_REPEAT)
state |= FLAG_REPEAT;
if (mTimeline.isShuffling())
state |= FLAG_SHUFFLE;
state |= mTimeline.getShuffleMode() << 8;
state |= savedState & FLAG_REPEAT_CURRENT;
}
@ -1213,4 +1212,14 @@ public final class PlaybackService extends Service implements Handler.Callback,
Log.w("VanillaMusic", "Failed to save state", e);
}
}
/**
* Returns the shuffle mode for the given state.
*
* @return The shuffle mode. One of SongTimeline.SHUFFLE_*.
*/
public static int shuffleMode(int state)
{
return (state & MASK_SHUFFLE) >> 8;
}
}

View File

@ -38,7 +38,7 @@ import java.io.FileNotFoundException;
* Represents a Song backed by the MediaStore. Includes basic metadata and
* utilities to retrieve songs from the MediaStore.
*/
public class Song {
public class Song implements Comparable<Song> {
/**
* Indicates that this song was randomly selected from all songs.
*/
@ -61,6 +61,7 @@ public class Song {
MediaStore.Audio.Media.ALBUM_ID,
MediaStore.Audio.Media.ARTIST_ID,
MediaStore.Audio.Media.DURATION,
MediaStore.Audio.Media.TRACK,
};
public static final String[] EMPTY_PLAYLIST_PROJECTION = {
@ -76,6 +77,7 @@ public class Song {
MediaStore.Audio.Playlists.Members.ALBUM_ID,
MediaStore.Audio.Playlists.Members.ARTIST_ID,
MediaStore.Audio.Playlists.Members.DURATION,
MediaStore.Audio.Media.TRACK,
};
/**
@ -123,6 +125,10 @@ public class Song {
* Length of the song in milliseconds.
*/
public long duration;
/**
* The position of the song in its album.
*/
public int trackNumber;
/**
* Song flags. Currently FLAG_RANDOM or 0.
@ -171,6 +177,7 @@ public class Song {
albumId = cursor.getLong(5);
artistId = cursor.getLong(6);
duration = cursor.getLong(7);
trackNumber = cursor.getInt(8);
}
/**
@ -245,6 +252,19 @@ public class Song {
@Override
public String toString()
{
return String.format("%d %s", id, path);
return String.format("%d %d", id, albumId);
}
/**
* Compares the album ids of the two songs; if equal, compares track order.
*/
@Override
public int compareTo(Song other)
{
if (albumId == other.albumId)
return trackNumber - other.trackNumber;
if (albumId > other.albumId)
return 1;
return -1;
}
}

View File

@ -81,6 +81,26 @@ public final class SongTimeline {
*/
public static final int MODE_ENQUEUE = 2;
/**
* Disable shuffle.
*
* @see SongTimeline#setShuffleMode(int)
*/
public static final int SHUFFLE_NONE = 0;
/**
* Randomize order of songs.
*
* @see SongTimeline#setShuffleMode(int)
*/
public static final int SHUFFLE_SONGS = 1;
/**
* Randomize order of albums, preserving the order of tracks inside the
* albums.
*
* @see SongTimeline#setShuffleMode(int)
*/
public static final int SHUFFLE_ALBUMS = 2;
private Context mContext;
/**
* All the songs currently contained in the timeline. Each Song object
@ -92,10 +112,9 @@ public final class SongTimeline {
*/
private int mCurrentPos;
/**
* Whether shuffling is enabled. Shuffling will shuffle sets of songs
* that are added with chooseSongs and shuffle sets of repeated songs.
* How to shuffle/whether to shuffle. One of SongTimeline.SHUFFLE_*.
*/
private boolean mShuffle;
private int mShuffleMode;
/**
* What to do when the end of the playlist is reached.
* Must be one of SongTimeline.FINISH_*.
@ -229,7 +248,7 @@ public final class SongTimeline {
mCurrentPos = Math.min(mSongs == null ? 0 : mSongs.size(), in.readInt());
mFinishAction = in.readInt();
mShuffle = in.readBoolean();
mShuffleMode = in.readInt();
}
}
@ -261,7 +280,7 @@ public final class SongTimeline {
out.writeInt(mCurrentPos);
out.writeInt(mFinishAction);
out.writeBoolean(mShuffle);
out.writeInt(mShuffleMode);
}
}
@ -274,11 +293,13 @@ public final class SongTimeline {
}
/**
* Return whether shuffling is enabled.
* Return the current shuffle mode.
*
* @return The shuffle mode. One of SongTimeline.SHUFFLE_*.
*/
public boolean isShuffling()
public int getShuffleMode()
{
return mShuffle;
return mShuffleMode;
}
/**
@ -292,25 +313,25 @@ public final class SongTimeline {
}
/**
* Set whether shuffling is enabled. Will shuffle the current set of songs
* when enabling shuffling if random mode is not enabled.
* Set how to shuffle. Will shuffle the current set of songs when enabling
* shuffling if random mode is not enabled.
*
* @param mode One of SongTimeline.MODE_*
*/
public void setShuffle(boolean shuffle)
public void setShuffleMode(int mode)
{
if (shuffle == mShuffle)
if (mode == mShuffleMode)
return;
synchronized (this) {
saveActiveSongs();
mShuffle = shuffle;
mShuffledSongs = null;
if (shuffle && mFinishAction != FINISH_RANDOM) {
mShuffleMode = mode;
if (mode != SHUFFLE_NONE && mFinishAction != FINISH_RANDOM && mSongs.size() != 0) {
shuffleAll();
ArrayList<Song> songs = mShuffledSongs;
mShuffledSongs = null;
int i = songs.indexOf(mSavedCurrent);
songs.set(i, songs.get(mCurrentPos));
songs.set(mCurrentPos, mSavedCurrent);
mCurrentPos = songs.indexOf(mSavedCurrent);
mSongs = songs;
}
broadcastChangedSongs();
@ -343,7 +364,7 @@ public final class SongTimeline {
return mShuffledSongs.get(0);
ArrayList<Song> songs = new ArrayList<Song>(mSongs);
Collections.shuffle(songs, MediaUtils.getRandom());
MediaUtils.shuffle(songs, mShuffleMode == SHUFFLE_ALBUMS);
mShuffledSongs = songs;
return songs.get(0);
}
@ -378,7 +399,7 @@ public final class SongTimeline {
if (size == 0)
// empty queue
return null;
else if (mShuffle)
else if (mShuffleMode != SHUFFLE_NONE)
song = shuffleAll();
else
song = timeline.get(0);
@ -416,7 +437,7 @@ public final class SongTimeline {
int pos = mCurrentPos + delta;
if (mFinishAction != FINISH_RANDOM && pos == mSongs.size()) {
if (mShuffle && mSongs.size() > 0) {
if (mShuffleMode != SHUFFLE_NONE && mSongs.size() > 0) {
if (mShuffledSongs == null)
shuffleAll();
mSongs = mShuffledSongs;
@ -488,8 +509,8 @@ public final class SongTimeline {
timeline.add(song);
}
if (mShuffle)
Collections.shuffle(timeline.subList(start, timeline.size()), MediaUtils.getRandom());
if (mShuffleMode != SHUFFLE_NONE)
MediaUtils.shuffle(timeline.subList(start, timeline.size()), mShuffleMode == SHUFFLE_ALBUMS);
broadcastChangedSongs();
}