[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