[vlc-devel] [PATCH 04/12] core: playlist: add random playback helper
Romain Vimont
rom1v at videolabs.io
Thu Oct 11 23:14:42 CEST 2018
Add a "randomizer" to provide the random playback rules, to be used by
the playlist.
---
src/Makefile.am | 7 +-
src/playlist/randomizer.c | 1100 +++++++++++++++++++++++++++++++++++++
src/playlist/randomizer.h | 148 +++++
3 files changed, 1254 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 2a98636901..ac1ca1ef65 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -225,6 +225,8 @@ libvlccore_la_SOURCES = \
playlist/tree.c \
playlist/item.c \
playlist/playlist.c \
+ playlist/randomizer.c \
+ playlist/randomizer.h \
playlist/search.c \
playlist/services_discovery.c \
playlist/renderer.c \
@@ -539,7 +541,8 @@ check_PROGRAMS = \
test_arrays \
test_vector \
test_shared_data_ptr \
- test_playlist
+ test_playlist \
+ test_randomizer
TESTS = $(check_PROGRAMS) check_symbols
@@ -566,6 +569,8 @@ test_vector_SOURCES = test/vector.c
test_shared_data_ptr_SOURCES = test/shared_data_ptr.cpp
test_playlist_SOURCES = playlist/playlist.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 0000000000..bea5ae647f
--- /dev/null
+++ b/src/playlist/randomizer.c
@@ -0,0 +1,1100 @@
+/*****************************************************************************
+ * 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;
+
+ /* 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
+}
+
+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();
+}
+
+#endif
diff --git a/src/playlist/randomizer.h b/src/playlist/randomizer.h
new file mode 100644
index 0000000000..9b392ec11f
--- /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
--
2.19.1
More information about the vlc-devel
mailing list