From 93164d919642af73912d049f355b91abfa25ac11 Mon Sep 17 00:00:00 2001 From: Romain Vimont <rom1v@videolabs.io> Date: Wed, 18 Jul 2018 19:41:24 +0200 Subject: [PATCH] core: playlist: add random playback helper Add a "randomizer" to provide the random playback rules, to be used by the playlist. Signed-off-by: Thomas Guillem <thomas@gllm.fr> --- src/Makefile.am | 7 +- src/playlist/randomizer.c | 1124 +++++++++++++++++++++++++++++++++++++ src/playlist/randomizer.h | 148 +++++ 3 files changed, 1278 insertions(+), 1 deletion(-) create mode 100644 src/playlist/randomizer.c create mode 100644 src/playlist/randomizer.h diff --git a/src/Makefile.am b/src/Makefile.am index 1b2d04a420e4..eefeab25cdb7 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -241,6 +241,8 @@ libvlccore_la_SOURCES = \ playlist/playlist.h \ playlist/preparse.c \ playlist/preparse.h \ + playlist/randomizer.c \ + playlist/randomizer.h \ playlist/request.c \ preparser/art.c \ preparser/art.h \ @@ -556,7 +558,8 @@ check_PROGRAMS = \ test_arrays \ test_vector \ test_shared_data_ptr \ - test_playlist + test_playlist \ + test_randomizer TESTS = $(check_PROGRAMS) check_symbols @@ -591,6 +594,8 @@ test_playlist_SOURCES = playlist/test.c \ playlist/preparse.c \ playlist/request.c test_playlist_CFLAGS = -DTEST_PLAYLIST +test_randomizer_SOURCES = playlist/randomizer.c +test_randomizer_CFLAGS = -DTEST_RANDOMIZER AM_LDFLAGS = -no-install LDADD = libvlccore.la \ diff --git a/src/playlist/randomizer.c b/src/playlist/randomizer.c new file mode 100644 index 000000000000..7aa5da272fc9 --- /dev/null +++ b/src/playlist/randomizer.c @@ -0,0 +1,1124 @@ +/***************************************************************************** + * randomizer.c + ***************************************************************************** + * Copyright (C) 2018 VLC authors and VideoLAN + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. + *****************************************************************************/ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#ifdef TEST_RANDOMIZER +# undef NDEBUG +#endif + +#include <assert.h> + +#include <vlc_common.h> +#include <vlc_rand.h> +#include "randomizer.h" + +/* Playlist helper to manage random playback. + * + * The purpose is to guarantee the following rules: + * - an item must never be selected twice + * - "prev" navigates back in the history of previously selected items + * - insertions and removals can occur at any time; all these rules must still + * apply + * - the user can request to select a specific item (typically by + * double-clicking on an item in the playlist) + * + * If loop (repeat) is enabled: + * - the random order must be "reshuffled" on every cycle + * - an item must never be selected twice in each cycle + * - the same item must never be selected twice in a row; in particular, it + * must not be the first item of a new cycle if it was the last item of the + * previous one (except if the playlist contains only one item) + * - crossing a new "cycle" is invisible to the user (e.g. it is still + * possible to navigate to previous selected items) + * + * To achieve these goals, a "randomizer" stores a single vector containing all + * the items of the playlist, along with 3 indexes. + * + * The whole vector is not shuffled at once: instead, steps of the Fish-Yates + * algorithm are executed one-by-one on demand. This has several advantages: + * - on insertions and removals, there is no need to reshuffle or shift the + * whole array; + * - if loop is enabled, the history of the last cycle can be kept in place. + * + * 'head' indicates the end of the items already determinated for the current + * cycle (if loop is disabled, there is only one cycle). + * (0 <= head <= size) + * + * 'next' points to the item after the current one (we use 'next' instead of + * 'current' so that all indexes are unsigned, while 'current' could be -1). + * The current item is the one returned by the previous call to _Prev() or + * _Next(). Each call to _Next() makes 'next' (and possibly 'head') move + * forward, each call to _Prev() makes it move back (modulo size). + * 'next' is always in the determinated range (0 <= next <= head) or in the + * "history" range (history < next < size). + * + * 'history' is only used in loop mode, and references the first item of the + * ordered history from the last cycle. + * + * 0 next head history size + * |---------------|-----|.............|-------------| + * <-------------------> <-----------> + * determinated range history range + * + * Here is a sample scenario to understand how it works. + * + * The playlist initially adds 5 items (A, B, C, D and E). + * + * history + * next | + * head | + * | | + * A B C D E + * + * The playlist calls _Next() to retrieve the next random item. The randomizer + * picks one item (say, D), and swaps it with the current head (A). _Next() + * returns D. + * + * history + * next | + * head | + * | | + * D B C A E + * <---> + * determinated range + * + * The playlist calls _Next() one more time. The randomizer selects one item + * outside the determinated range (say, E). _Next() returns E. + * + * history + * next | + * head | + * | | + * D E C A B + * <--------> + * determinated range + * + * The playlist calls _Next() one more time. The randomizer selects C (already + * in place). _Next() returns E. + * + * history + * next | + * head | + * | | + * D E C A B + * <-------------> + * determinated range + * + * The playlist then calls _Prev(). Since the "current" item is C, the previous + * one is E, so _Prev() returns E, and 'next' moves back. + * + * history + * next | + * | head | + * | | | + * D E C A B + * <-------------> + * determinated range + * + * The playlist calls _Next(), which returns C, as expected. + * + * history + * next | + * head | + * | | + * D E C A B + * <-------------> + * determinated range + * + * The playlist calls _Next(), the randomizer selects B, and returns it. + * + * history + * next | + * head | + * | | + * D E C B A + * <------------------> + * determinated range + * + * The playlist calls _Next(), the randomizer selects the last item (it has no + * choice). 'next' and 'head' now points one item past the end (their value is + * the vector size). + * + * history + * next + * head + * | + * D E C B A + * <-----------------------> + * determinated range + * + * At this point, if loop is disabled, it is not possible to call _Next() + * anymore (_HasNext() returns false). So let's enable it by _SetLoop(), and + * call _Next() again. + * + * This will start a new loop cycle. Firstly, 'next' and 'head' are reset, and + * the whole vector belongs to the last cycle history. + * + * history + * next + * head + * | + * D E C B A + * <------------------------> + * history range + * + * Secondly, to avoid to select A twice in a row (as the last item of the + * previous cycle and the first item of the new one), the randomizer will + * immediately determine another item in the vector (say C) to be the first of + * the new cycle. The items that belong to the history are kept in order. + * 'head' and 'history' move forward. + * + * history + * next | + * | head + * | | + * C D E B A + * <---><------------------> + * determinated history range + * range + * + * Finally, it will actually select and return the first item (C). + * + * history + * next + * head + * | + * C D E B A + * <---><------------------> + * determinated history range + * range + * + * Then, the user adds an item to the playlist (F). This item is added in front + * of history. + * + * history + * next | + * head | + * | | + * C F D E B A + * <---> <------------------> + * determinated history range + * range + * + * The playlist calls _Next(), the randomizer randomly selects E. E + * "disappears" from the history of the last cycle. This is a general property: + * each item may not appear more than one in the "history" (both from the last + * and the new cycle). The history order is preserved. + * + * history + * next | + * head | + * | | + * C E F D B A + * <--------> <--------------> + * determinated history range + * range + * + * The playlist then calls _Prev() 3 times, that yield C, then A, then B. + * 'next' is decremented (modulo size) on each call. + * + * history + * | next + * head | | + * | | | + * C E F D B A + * <--------> <--------------> + * determinated history range + * range + */ + +/* On auto-reshuffle, avoid to select the same item before at least + * NOT_SAME_BEFORE other items have been selected (between the end of the + * previous shuffle and the start of the new shuffle). */ +#define NOT_SAME_BEFORE 1 + +void +randomizer_Init(struct randomizer *r) +{ + vlc_vector_init(&r->items); + + /* initialize separately instead of using vlc_lrand48() to avoid locking + * the mutex for every random number generation */ + vlc_rand_bytes(r->xsubi, sizeof(r->xsubi)); + + r->loop = false; + r->head = 0; + r->next = 0; + r->history = 0; +} + +void +randomizer_Destroy(struct randomizer *r) +{ + vlc_vector_destroy(&r->items); +} + +void +randomizer_SetLoop(struct randomizer *r, bool loop) +{ + r->loop = loop; +} + +static inline ssize_t +randomizer_IndexOf(struct randomizer *r, const vlc_playlist_item_t *item) +{ + ssize_t index; + vlc_vector_index_of(&r->items, item, &index); + return index; +} + +bool +randomizer_Count(struct randomizer *r) +{ + return r->items.size; +} + +void +randomizer_Reshuffle(struct randomizer *r) +{ + /* yeah, it's that simple */ + r->head = 0; + r->next = 0; + r->history = r->items.size; +} + +static inline void +swap_items(struct randomizer *r, int i, int j) +{ + vlc_playlist_item_t *item = r->items.data[i]; + r->items.data[i] = r->items.data[j]; + r->items.data[j] = item; +} + +static inline void +randomizer_DetermineOne_(struct randomizer *r, size_t avoid_last_n) +{ + assert(r->head < r->items.size); + assert(r->items.size - r->head > avoid_last_n); + size_t range_len = r->items.size - r->head - avoid_last_n; + size_t selected = r->head + (nrand48(r->xsubi) % range_len); + swap_items(r, r->head, selected); + + if (r->head == r->history) + r->history++; + r->head++; +} + +static inline void +randomizer_DetermineOne(struct randomizer *r) +{ + randomizer_DetermineOne_(r, 0); +} + +/* An autoreshuffle occurs if loop is enabled, once all item have been played. + * In that case, we reshuffle and determine first items so that the same item + * may not be selected before NOT_SAME_BEFORE selections. */ +static void +randomizer_AutoReshuffle(struct randomizer *r) +{ + assert(r->items.size > 0); + r->head = 0; + r->next = 0; + r->history = 0; /* the whole content is history */ + size_t avoid_last_n = NOT_SAME_BEFORE; + if (avoid_last_n > r->items.size - 1) + /* cannot ignore all */ + avoid_last_n = r->items.size - 1; + while (avoid_last_n) + randomizer_DetermineOne_(r, avoid_last_n--); +} + +bool +randomizer_HasPrev(struct randomizer *r) +{ + if (!r->loop) + /* a previous exists if the current is > 0, i.e. next > 1 */ + return r->next > 1; + + if (!r->items.size) + /* avoid modulo 0 */ + return false; + + /* there is no previous only if (current - history) == 0 (modulo size), + * i.e. (next - history) == 1 (modulo size) */ + return (r->next + r->items.size - r->history) % r->items.size != 1; +} + +bool +randomizer_HasNext(struct randomizer *r) +{ + return r->loop || r->next < r->items.size; +} + +vlc_playlist_item_t * +randomizer_PeekPrev(struct randomizer *r) +{ + assert(randomizer_HasPrev(r)); + size_t index = (r->next + r->items.size - 2) % r->items.size; + return r->items.data[index]; +} + +vlc_playlist_item_t * +randomizer_PeekNext(struct randomizer *r) +{ + assert(randomizer_HasNext(r)); + + if (r->next == r->items.size && r->next == r->history) + { + assert(r->loop); + randomizer_AutoReshuffle(r); + } + + if (r->next == r->head) + /* execute 1 step of the Fisher-Yates shuffle */ + randomizer_DetermineOne(r); + + return r->items.data[r->next]; +} + +vlc_playlist_item_t * +randomizer_Prev(struct randomizer *r) +{ + assert(randomizer_HasPrev(r)); + vlc_playlist_item_t *item = randomizer_PeekPrev(r); + r->next = r->next ? r->next - 1 : r->items.size - 1; + return item; +} + +vlc_playlist_item_t * +randomizer_Next(struct randomizer *r) +{ + assert(randomizer_HasNext(r)); + vlc_playlist_item_t *item = randomizer_PeekNext(r); + r->next++; + if (r->next == r->items.size && r->next != r->head) + r->next = 0; + return item; +} + +bool +randomizer_Add(struct randomizer *r, vlc_playlist_item_t *items[], size_t count) +{ + if (r->history) + { + if (!vlc_vector_insert_all(&r->items, r->history, items, count)) + return false; + /* the insertion shifted history (and possibly next) */ + if (r->next > r->history) + r->next += count; + r->history += count; + return true; + } + + return vlc_vector_push_all(&r->items, items, count); +} + +static void +randomizer_SelectIndex(struct randomizer *r, size_t index) +{ + vlc_playlist_item_t *selected = r->items.data[index]; + if (r->history && index >= r->history) + { + if (index > r->history) + { + memmove(&r->items.data[r->history + 1], + &r->items.data[r->history], + (index - r->history) * sizeof(selected)); + index = r->history; + } + r->history = (r->history + 1) % r->items.size; + } + + if (index >= r->head) + { + r->items.data[index] = r->items.data[r->head]; + r->items.data[r->head] = selected; + r->head++; + } + else if (index < r->items.size - 1) + { + memmove(&r->items.data[index], + &r->items.data[index + 1], + (r->head - index - 1) * sizeof(selected)); + r->items.data[r->head - 1] = selected; + } + + r->next = r->head; +} + +void +randomizer_Select(struct randomizer *r, const vlc_playlist_item_t *item) +{ + ssize_t index = randomizer_IndexOf(r, item); + assert(index != -1); /* item must exist */ + randomizer_SelectIndex(r, (size_t) index); +} + +static void +randomizer_RemoveAt(struct randomizer *r, size_t index) +{ + /* + * 0 head history next size + * |-----------|...................................|---------|-----| + * |<--------->| |<------------->| + * ordered order irrelevant ordered + */ + + /* update next before may be updated */ + if (index < r->next) + r->next--; + + if (index < r->head) + { + /* item was selected, keep the selected part ordered */ + memmove(&r->items.data[index], + &r->items.data[index + 1], + (r->head - index - 1) * sizeof(*r->items.data)); + r->head--; + index = r->head; /* the new index to remove */ + } + + if (!r->history || index < r->history) + { + size_t swap = (r->history + r->items.size - 1) % r->items.size; + r->items.data[index] = r->items.data[swap]; + index = swap; + } + + if (r->history) + { + memmove(&r->items.data[index], + &r->items.data[index + 1], + (r->items.size - index - 1) * sizeof(*r->items.data)); + + if (index < r->history) + r->history--; + else if (r->history == r->items.size) + r->history = 0; + + if (r->next == r->items.size) + r->next = 0; + } + + r->items.size--; +} + +static void +randomizer_RemoveOne(struct randomizer *r, const vlc_playlist_item_t *item) +{ + ssize_t index = randomizer_IndexOf(r, item); + assert(index >= 0); /* item must exist */ + randomizer_RemoveAt(r, index); +} + +void +randomizer_Remove(struct randomizer *r, vlc_playlist_item_t *const items[], + size_t count) +{ + for (size_t i = 0; i < count; ++i) + randomizer_RemoveOne(r, items[i]); + + vlc_vector_autoshrink(&r->items); +} + +void +randomizer_Clear(struct randomizer *r) +{ + vlc_vector_clear(&r->items); + r->head = 0; + r->next = 0; + r->history = 0; +} + +#ifdef TEST_RANDOMIZER + +/* fake structure to simplify tests */ +struct vlc_playlist_item { + size_t index; +}; + +static void +ArrayInit(vlc_playlist_item_t *array[], size_t len) +{ + for (size_t i = 0; i < len; ++i) + { + array[i] = malloc(sizeof(*array[i])); + assert(array[i]); + array[i]->index = i; + } +} + +static void +ArrayDestroy(vlc_playlist_item_t *array[], size_t len) +{ + for (size_t i = 0; i < len; ++i) + free(array[i]); +} + +static void +test_all_items_selected_exactly_once(void) +{ + struct randomizer randomizer; + randomizer_Init(&randomizer); + + #define SIZE 100 + vlc_playlist_item_t *items[SIZE]; + ArrayInit(items, SIZE); + + bool ok = randomizer_Add(&randomizer, items, SIZE); + assert(ok); + + bool selected[SIZE] = {0}; + for (int i = 0; i < SIZE; ++i) + { + assert(randomizer_HasNext(&randomizer)); + vlc_playlist_item_t *item = randomizer_Next(&randomizer); + assert(item); + assert(!selected[item->index]); /* never selected twice */ + selected[item->index] = true; + } + + assert(!randomizer_HasNext(&randomizer)); /* no more items */ + + for (int i = 0; i < SIZE; ++i) + assert(selected[i]); /* all selected */ + + ArrayDestroy(items, SIZE); + randomizer_Destroy(&randomizer); + #undef SIZE +} + +static void +test_all_items_selected_exactly_once_per_cycle(void) +{ + struct randomizer randomizer; + randomizer_Init(&randomizer); + randomizer_SetLoop(&randomizer, true); + + #define SIZE 100 + vlc_playlist_item_t *items[SIZE]; + ArrayInit(items, SIZE); + + bool ok = randomizer_Add(&randomizer, items, SIZE); + assert(ok); + + for (int cycle = 0; cycle < 4; ++cycle) + { + bool selected[SIZE] = {0}; + for (int i = 0; i < SIZE; ++i) + { + assert(randomizer_HasNext(&randomizer)); + vlc_playlist_item_t *item = randomizer_Next(&randomizer); + assert(item); + assert(!selected[item->index]); /* never selected twice */ + selected[item->index] = true; + } + + assert(randomizer_HasNext(&randomizer)); /* still has items in loop */ + + for (int i = 0; i < SIZE; ++i) + assert(selected[i]); /* all selected */ + } + + ArrayDestroy(items, SIZE); + randomizer_Destroy(&randomizer); + #undef SIZE +} + +static void +test_all_items_selected_exactly_once_with_additions(void) +{ + struct randomizer randomizer; + randomizer_Init(&randomizer); + + #define SIZE 100 + vlc_playlist_item_t *items[SIZE]; + ArrayInit(items, SIZE); + + bool ok = randomizer_Add(&randomizer, items, 75); + assert(ok); + + bool selected[SIZE] = {0}; + for (int i = 0; i < 50; ++i) + { + assert(randomizer_HasNext(&randomizer)); + vlc_playlist_item_t *item = randomizer_Next(&randomizer); + assert(item); + assert(!selected[item->index]); /* never selected twice */ + selected[item->index] = true; + } + + ok = randomizer_Add(&randomizer, &items[75], 25); + assert(ok); + for (int i = 50; i < SIZE; ++i) + { + assert(randomizer_HasNext(&randomizer)); + vlc_playlist_item_t *item = randomizer_Next(&randomizer); + assert(item); + assert(!selected[item->index]); /* never selected twice */ + selected[item->index] = true; + } + + assert(!randomizer_HasNext(&randomizer)); /* no more items */ + + for (int i = 0; i < SIZE; ++i) + assert(selected[i]); /* all selected */ + + ArrayDestroy(items, SIZE); + randomizer_Destroy(&randomizer); + #undef SIZE +} + +static void +test_all_items_selected_exactly_once_with_removals(void) +{ + struct randomizer randomizer; + randomizer_Init(&randomizer); + + #define SIZE 100 + vlc_playlist_item_t *items[SIZE]; + ArrayInit(items, SIZE); + + bool ok = randomizer_Add(&randomizer, items, 100); + assert(ok); + + bool selected[SIZE] = {0}; + for (int i = 0; i < 50; ++i) + { + assert(randomizer_HasNext(&randomizer)); + vlc_playlist_item_t *item = randomizer_Next(&randomizer); + assert(item); + assert(!selected[item->index]); /* never selected twice */ + selected[item->index] = true; + } + + vlc_playlist_item_t *to_remove[20]; + /* copy 10 items already selected */ + memcpy(to_remove, &randomizer.items.data[20], 10 * sizeof(*to_remove)); + /* copy 10 items not already selected */ + memcpy(&to_remove[10], &randomizer.items.data[70], 10 * sizeof(*to_remove)); + + randomizer_Remove(&randomizer, to_remove, 20); + + for (int i = 50; i < SIZE - 10; ++i) + { + assert(randomizer_HasNext(&randomizer)); + vlc_playlist_item_t *item = randomizer_Next(&randomizer); + assert(item); + assert(!selected[item->index]); /* never selected twice */ + selected[item->index] = true; + } + + assert(!randomizer_HasNext(&randomizer)); /* no more items */ + + int count = 0; + for (int i = 0; i < SIZE; ++i) + if (selected[i]) + count++; + + assert(count == SIZE - 10); + + ArrayDestroy(items, SIZE); + randomizer_Destroy(&randomizer); + #undef SIZE +} + +static void +test_force_select_new_item(void) +{ + struct randomizer randomizer; + randomizer_Init(&randomizer); + + #define SIZE 100 + vlc_playlist_item_t *items[SIZE]; + ArrayInit(items, SIZE); + + bool ok = randomizer_Add(&randomizer, items, SIZE); + assert(ok); + + bool selected[SIZE] = {0}; + for (int i = 0; i < SIZE; ++i) + { + vlc_playlist_item_t *item; + if (i != 50) + { + assert(randomizer_HasNext(&randomizer)); + item = randomizer_Next(&randomizer); + } + else + { + /* force the selection of a new item not already selected */ + item = randomizer.items.data[62]; + randomizer_Select(&randomizer, item); + /* the item should now be the last selected one */ + assert(randomizer.items.data[randomizer.next - 1] == item); + } + assert(item); + assert(!selected[item->index]); /* never selected twice */ + selected[item->index] = true; + } + + assert(!randomizer_HasNext(&randomizer)); /* no more items */ + + for (int i = 0; i < SIZE; ++i) + assert(selected[i]); /* all selected */ + + ArrayDestroy(items, SIZE); + randomizer_Destroy(&randomizer); +} + +static void +test_force_select_item_already_selected(void) +{ + struct randomizer randomizer; + randomizer_Init(&randomizer); + + #define SIZE 100 + vlc_playlist_item_t *items[SIZE]; + ArrayInit(items, SIZE); + + bool ok = randomizer_Add(&randomizer, items, SIZE); + assert(ok); + + bool selected[SIZE] = {0}; + /* we need an additional loop cycle, since we select the same item twice */ + for (int i = 0; i < SIZE + 1; ++i) + { + vlc_playlist_item_t *item; + if (i != 50) + { + assert(randomizer_HasNext(&randomizer)); + item = randomizer_Next(&randomizer); + } + else + { + /* force the selection of an item already selected */ + item = randomizer.items.data[42]; + randomizer_Select(&randomizer, item); + /* the item should now be the last selected one */ + assert(randomizer.items.data[randomizer.next - 1] == item); + } + assert(item); + /* never selected twice, except for item 50 */ + assert((i != 50) ^ selected[item->index]); + selected[item->index] = true; + } + + assert(!randomizer_HasNext(&randomizer)); /* no more items */ + + for (int i = 0; i < SIZE; ++i) + assert(selected[i]); /* all selected */ + + ArrayDestroy(items, SIZE); + randomizer_Destroy(&randomizer); + #undef SIZE +} + +static void +test_prev(void) +{ + struct randomizer randomizer; + randomizer_Init(&randomizer); + + #define SIZE 10 + vlc_playlist_item_t *items[SIZE]; + ArrayInit(items, SIZE); + + bool ok = randomizer_Add(&randomizer, items, SIZE); + assert(ok); + + assert(!randomizer_HasPrev(&randomizer)); + + vlc_playlist_item_t *actual[SIZE]; + for (int i = 0; i < SIZE; ++i) + { + assert(randomizer_HasNext(&randomizer)); + actual[i] = randomizer_Next(&randomizer); + assert(actual[i]); + } + + assert(!randomizer_HasNext(&randomizer)); + + for (int i = SIZE - 2; i >= 0; --i) + { + assert(randomizer_HasPrev(&randomizer)); + vlc_playlist_item_t *item = randomizer_Prev(&randomizer); + assert(item == actual[i]); + } + + assert(!randomizer_HasPrev(&randomizer)); + + for (int i = 1; i < SIZE; ++i) + { + assert(randomizer_HasNext(&randomizer)); + vlc_playlist_item_t *item = randomizer_Next(&randomizer); + assert(item == actual[i]); + } + + ArrayDestroy(items, SIZE); + randomizer_Destroy(&randomizer); + #undef SIZE +} + +static void +test_prev_with_select(void) +{ + struct randomizer randomizer; + randomizer_Init(&randomizer); + + #define SIZE 10 + vlc_playlist_item_t *items[SIZE]; + ArrayInit(items, SIZE); + + bool ok = randomizer_Add(&randomizer, items, SIZE); + assert(ok); + + assert(!randomizer_HasPrev(&randomizer)); + + vlc_playlist_item_t *actual[SIZE]; + for (int i = 0; i < 5; ++i) + { + assert(randomizer_HasNext(&randomizer)); + actual[i] = randomizer_Next(&randomizer); + assert(actual[i]); + } + + randomizer_Select(&randomizer, actual[2]); + + vlc_playlist_item_t *item; + + assert(randomizer_HasPrev(&randomizer)); + item = randomizer_Prev(&randomizer); + assert(item == actual[4]); + + assert(randomizer_HasPrev(&randomizer)); + item = randomizer_Prev(&randomizer); + assert(item == actual[3]); + + assert(randomizer_HasPrev(&randomizer)); + item = randomizer_Prev(&randomizer); + assert(item == actual[1]); + + assert(randomizer_HasPrev(&randomizer)); + item = randomizer_Prev(&randomizer); + assert(item == actual[0]); + + assert(!randomizer_HasPrev(&randomizer)); + + ArrayDestroy(items, SIZE); + randomizer_Destroy(&randomizer); + #undef SIZE +} + +static void +test_prev_across_reshuffle_loops(void) +{ + struct randomizer randomizer; + randomizer_Init(&randomizer); + + #define SIZE 10 + vlc_playlist_item_t *items[SIZE]; + ArrayInit(items, SIZE); + + bool ok = randomizer_Add(&randomizer, items, SIZE); + assert(ok); + + assert(!randomizer_HasPrev(&randomizer)); + + vlc_playlist_item_t *actual[SIZE]; + for (int i = 0; i < SIZE; ++i) + { + assert(randomizer_HasNext(&randomizer)); + actual[i] = randomizer_Next(&randomizer); + assert(actual[i]); + } + + assert(!randomizer_HasNext(&randomizer)); + randomizer_SetLoop(&randomizer, true); + assert(randomizer_HasNext(&randomizer)); + + vlc_playlist_item_t *actualnew[4]; + /* determine the 4 first items */ + for (int i = 0; i < 4; ++i) + { + assert(randomizer_HasNext(&randomizer)); + actualnew[i] = randomizer_Next(&randomizer); + assert(actualnew[i]); + } + + /* go back to the first */ + for (int i = 2; i >= 0; --i) + { + assert(randomizer_HasPrev(&randomizer)); + actualnew[i] = randomizer_Prev(&randomizer); + assert(actualnew[i]); + } + + assert(actualnew[0] == randomizer.items.data[0]); + + /* from now, any "prev" goes back to the history */ + + int index_in_actual = 9; + for (int i = 0; i < 6; ++i) + { + assert(randomizer_HasPrev(&randomizer)); + vlc_playlist_item_t *item = randomizer_Prev(&randomizer); + + int j; + for (j = 3; j >= 0; --j) + if (item == actualnew[j]) + break; + bool in_actualnew = j != 0; + + if (in_actualnew) + /* the item has been selected for the new order, it is not in the + * history anymore */ + index_in_actual--; + else + /* the remaining previous items are retrieved in reverse order in + * the history */ + assert(item == actual[index_in_actual]); + } + + /* no more history: 4 in the current shuffle, 6 in the history */ + assert(!randomizer_HasPrev(&randomizer)); + + ArrayDestroy(items, SIZE); + randomizer_Destroy(&randomizer); + #undef SIZE +} + +/* when loop is enabled, we must take care that the last items of the + * previous order are not the same as the first items of the new order */ +static void +test_loop_respect_not_same_before(void) +{ + struct randomizer randomizer; + randomizer_Init(&randomizer); + randomizer_SetLoop(&randomizer, true); + + #define SIZE (NOT_SAME_BEFORE + 2) + vlc_playlist_item_t *items[SIZE]; + ArrayInit(items, SIZE); + + bool ok = randomizer_Add(&randomizer, items, SIZE); + assert(ok); + + vlc_playlist_item_t *actual[SIZE]; + for (int i = 0; i < SIZE; ++i) + { + assert(randomizer_HasNext(&randomizer)); + actual[i] = randomizer_Next(&randomizer); + } + + for (int cycle = 0; cycle < 20; cycle++) + { + /* check that the first items are not the same as the last ones of the + * previous order */ + for (int i = 0; i < NOT_SAME_BEFORE; ++i) + { + assert(randomizer_HasNext(&randomizer)); + actual[i] = randomizer_Next(&randomizer); + for (int j = (i + SIZE - NOT_SAME_BEFORE) % SIZE; + j != i; + j = (j + 1) % SIZE) + { + assert(actual[i] != actual[j]); + } + } + for (int i = NOT_SAME_BEFORE; i < SIZE; ++i) + { + assert(randomizer_HasNext(&randomizer)); + actual[i] = randomizer_Next(&randomizer); + } + } + + ArrayDestroy(items, SIZE); + randomizer_Destroy(&randomizer); + #undef SIZE +} + +/* if there are less items than NOT_SAME_BEFORE, obviously we can't avoid + * repeating last items in the new order, but it must still work */ +static void +test_loop_respect_not_same_before_impossible(void) +{ + struct randomizer randomizer; + randomizer_Init(&randomizer); + randomizer_SetLoop(&randomizer, true); + + #define SIZE NOT_SAME_BEFORE + vlc_playlist_item_t *items[SIZE]; + ArrayInit(items, SIZE); + + bool ok = randomizer_Add(&randomizer, items, SIZE); + assert(ok); + + for (int i = 0; i < 10 * SIZE; ++i) + { + assert(randomizer_HasNext(&randomizer)); + vlc_playlist_item_t *item = randomizer_Next(&randomizer); + assert(item); + } + + ArrayDestroy(items, SIZE); + randomizer_Destroy(&randomizer); + #undef SIZE +} + +static void +test_has_prev_next_empty(void) +{ + struct randomizer randomizer; + randomizer_Init(&randomizer); + + assert(!randomizer_HasPrev(&randomizer)); + assert(!randomizer_HasNext(&randomizer)); + + randomizer_SetLoop(&randomizer, true); + + assert(!randomizer_HasPrev(&randomizer)); + + /* there are always next items in loop mode */ + assert(randomizer_HasNext(&randomizer)); + + randomizer_Destroy(&randomizer); +} + +int main(void) +{ + test_all_items_selected_exactly_once(); + test_all_items_selected_exactly_once_per_cycle(); + test_all_items_selected_exactly_once_with_additions(); + test_all_items_selected_exactly_once_with_removals(); + test_force_select_new_item(); + test_force_select_item_already_selected(); + test_prev(); + test_prev_with_select(); + test_prev_across_reshuffle_loops(); + test_loop_respect_not_same_before(); + test_loop_respect_not_same_before_impossible(); + test_has_prev_next_empty(); +} + +#endif diff --git a/src/playlist/randomizer.h b/src/playlist/randomizer.h new file mode 100644 index 000000000000..9b392ec11fa0 --- /dev/null +++ b/src/playlist/randomizer.h @@ -0,0 +1,148 @@ +/***************************************************************************** + * randomizer.h + ***************************************************************************** + * Copyright (C) 2018 VLC authors and VideoLAN + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. + *****************************************************************************/ + +#ifndef RANDOMIZER_H +#define RANDOMIZER_H + +#include <vlc_common.h> +#include <vlc_vector.h> + +typedef struct vlc_playlist_item vlc_playlist_item_t; + +/** + * Playlist helper to manage random playback. + * + * See randomizer.c for implementation details. + */ +struct randomizer { + struct VLC_VECTOR(vlc_playlist_item_t *) items; + unsigned short xsubi[3]; /* random state */ + bool loop; + size_t head; + size_t next; + size_t history; +}; + +/** + * Initialize an empty randomizer. + */ +void +randomizer_Init(struct randomizer *randomizer); + +/** + * Destroy a randomizer. + */ +void +randomizer_Destroy(struct randomizer *randomizer); + +/** + * Enable or disable "loop" mode. + * + * This affects the behavior of prev/next. + */ +void +randomizer_SetLoop(struct randomizer *randomizer, bool loop); + +/** + * Return the number of items in the randomizer. + */ +bool +randomizer_Count(struct randomizer *randomizer); + +/** + * Start a new random cycle. + * + * The "history" is lost, and "next" can be called _n_ times if the randomizer + * contains _n_ items (when loop is disabled). + */ +void +randomizer_Reshuffle(struct randomizer *randomizer); + +/** + * Indicate whether there is a previous item. + */ +bool +randomizer_HasPrev(struct randomizer *randomizer); + +/** + * Indicate whether there is a next item. + */ +bool +randomizer_HasNext(struct randomizer *randomizer); + +/** + * Peek the previous item (without changing the current one). + */ +vlc_playlist_item_t * +randomizer_PeekPrev(struct randomizer *randomizer); + +/** + * Peek the next item (without changing the current one). + */ +vlc_playlist_item_t * +randomizer_PeekNext(struct randomizer *randomizer); + +/** + * Go back to the previous item. + */ +vlc_playlist_item_t * +randomizer_Prev(struct randomizer *randomizer); + +/** + * Go back to the next item. + */ +vlc_playlist_item_t * +randomizer_Next(struct randomizer *randomizer); + +/** + * Force the selection of a specific item. + * + * This function should be called when the user requested to play a specific + * item in the playlist. + */ +void +randomizer_Select(struct randomizer *randomizer, + const vlc_playlist_item_t *item); + +/** + * Add items to the randomizer. + * + * This function should be called when items are added to the playlist. + */ +bool +randomizer_Add(struct randomizer *randomizer, vlc_playlist_item_t *items[], + size_t count); + +/** + * Remove items from the randomizer. + * + * This function should be called when items are removed from the playlist. + */ +void +randomizer_Remove(struct randomizer *randomizer, + vlc_playlist_item_t *const items[], size_t count); + +/** + * Clear the randomizer. + */ +void +randomizer_Clear(struct randomizer *randomizer); + +#endif -- GitLab