From ca5d7b867c381b7886a660ce744df0b74f38b2e6 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Fri, 8 Dec 2023 11:11:57 +1100 Subject: [PATCH] Add shuffle and repeat options for the playback queue --- lib/millershuffle/CMakeLists.txt | 8 ++ lib/millershuffle/LICENSE | 190 +++++++++++++++++++++++++ lib/millershuffle/MillerShuffle.c | 65 +++++++++ lib/millershuffle/MillerShuffle.h | 14 ++ lib/millershuffle/README.md | 80 +++++++++++ src/audio/CMakeLists.txt | 2 +- src/audio/include/track_queue.hpp | 35 ++++- src/audio/track_queue.cpp | 226 ++++++++++++++++++++++++------ src/lua/stubs/queue.lua | 22 +++ src/ui/include/ui_fsm.hpp | 4 + src/ui/ui_fsm.cpp | 26 +++- tools/cmake/common.cmake | 5 +- 12 files changed, 632 insertions(+), 45 deletions(-) create mode 100644 lib/millershuffle/CMakeLists.txt create mode 100644 lib/millershuffle/LICENSE create mode 100644 lib/millershuffle/MillerShuffle.c create mode 100644 lib/millershuffle/MillerShuffle.h create mode 100644 lib/millershuffle/README.md create mode 100644 src/lua/stubs/queue.lua diff --git a/lib/millershuffle/CMakeLists.txt b/lib/millershuffle/CMakeLists.txt new file mode 100644 index 00000000..4c7ef484 --- /dev/null +++ b/lib/millershuffle/CMakeLists.txt @@ -0,0 +1,8 @@ +# Copyright 2023 jacqueline +# +# SPDX-License-Identifier: GPL-3.0-only + +idf_component_register( + SRCS "MillerShuffle.c" + INCLUDE_DIRS "." +) diff --git a/lib/millershuffle/LICENSE b/lib/millershuffle/LICENSE new file mode 100644 index 00000000..43c94242 --- /dev/null +++ b/lib/millershuffle/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2022 Ronald Ross Miller + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/lib/millershuffle/MillerShuffle.c b/lib/millershuffle/MillerShuffle.c new file mode 100644 index 00000000..f47bca5b --- /dev/null +++ b/lib/millershuffle/MillerShuffle.c @@ -0,0 +1,65 @@ +// ================================================================ +// the Miller Shuffle Algorithm +// +// source: https://github.com/RondeSC/Miller_Shuffle_Algo +// license: Attribution-NonCommercial-ShareAlike +// Copyright 2022 Ronald R. Miller +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Update Aug 2022 added Miller Shuffle Algo-C (also a _lite version) +// the Algo-C is by & large suitable to replace both algo-A and -B +// Update April 2023 added Miller Shuffle Algo-D +// The NEW Algo-D performs superiorly to earlier variants (~Fisher-Yates statistics). +// Optimized so as to generate greater permutations of possible shuffles over time. +// A -Max variant was also added, pushing (obsessively?) random-permutations to the limit. +// Update May 2023 added Miller Shuffle Algo-E and improved MS-lite. +// Removed MSA-A and MSA-C as obsolite, having no use cases not better served by MS-lite or MSA-D +// Update June 2023: greatly increased the potential number by unique shuffle permutations for MSA_d & MSA_e +// Also improved the randomness and permutations of MS_lite. +// Update Aug 2023: updated MillerShuffleAlgo-E to rely on 'Chinese remainder theorem' to ensure unique randomizing factors +// + + +// -------------------------------------------------------------- +// the Miller Shuffle Algorithms +// produces a shuffled Index given a base Index, a shuffle ID "seed" and the length of the list being +// indexed. For each inx: 0 to listSize-1, unique indexes are returned in a pseudo "random" order. +// Utilizes minimum resources. +// As such the Miller Shuffle algorithm is the better choice for a playlist shuffle. +// +// The 'shuffleID' is an unsigned 32bit value and can be selected by utilizing a PRNG. +// Each time you want another pseudo random index from a current shuffle (incrementing 'inx') +// you must be sure to pass in the "shuffleID" for that shuffle. +// Note that you can exceed the listSize with the input 'inx' value and get very good results, +// as the code effectively uses a secondary shuffle by way of using a 'working' modified value of the input shuffle ID. + + +// -------------------------------------------------------------- +// Miller Shuffle Algorithm D variant +// aka: MillerShuffleAlgo_d +unsigned int MillerShuffle(unsigned int inx, unsigned int shuffleID, unsigned int listSize) { + unsigned int si, r1, r2, r3, r4, rx, rx2; + const unsigned int p1=24317, p2=32141, p3=63629; // for shuffling 60,000+ indexes + + shuffleID+=131*(inx/listSize); // have inx overflow effect the mix + si=(inx+shuffleID)%listSize; // cut the deck + + r1=shuffleID%p1+42; // randomizing factors crafted empirically (by automated trial and error) + r2=((shuffleID*0x89)^r1)%p2; + r3=(r1+r2+p3)%listSize; + r4=r1^r2^r3; + rx = (shuffleID/listSize) % listSize + 1; + rx2 = ((shuffleID/listSize/listSize)) % listSize + 1; + + // perform conditional multi-faceted mathematical spin-mixing (on avg 2 1/3 shuffle ops done + 2 simple Xors) + if (si%3==0) si=(((unsigned long)(si/3)*p1+r1) % ((listSize+2)/3)) *3; // spin multiples of 3 + if (si%2==0) si=(((unsigned long)(si/2)*p2+r2) % ((listSize+1)/2)) *2; // spin multiples of 2 + if (si> < + + Figures here are not directly comparable with earilier results, + due to testing and shuffleID generation changes. +``` +The repeated permutation statistics are from shuffling 52 items. As the number of items being shuffled goes above 200 the number of repeated shuffles per million approaches zero, for all current MillerShuffle variations including the MS eXtra lite version. + +If really needed, or for academic pursuit, the potential numbers of unique shuffle permutations could be improved by orders of magnitude by increasing the shuffleID beyond 32 bits and using those bits to increase the algorithm’s randomizing factors. Alternatively a second 32bit input value could be utilized. One easy way to effectively do this is to do back to back shuffles ie: myItem = MShuffle(MShuffle(i,sID,n), sID2,n). With two randomly generated 32bit shuffleIDs. I found the results, doing the latter, indistinguishable from those of Fisher-Yates; and generated 0 repeated shuffles while testing the generation of billions of unique shuffle permutations (a billion-billions may be possible). + +If you don’t mind giving up the deterministic feature, you can alternatively utilize MSA_b instead, for the improvement in permutation generation (of ~ x1000). It is likly that 99.9% of all applications could be best served by either MS_lite, MSA_d or MSA_e shuffle algorithms. + +I have included a function: random() to give 32bit pseudo random values (>4 billion) suitable for use as shuffleIDs. Using the C function rand(), as is, to set ‘shuffleID’s will yield about 32K unique shuffles, and may very well suit your needs. + - - - +Up until recently I have been getting tens of repeats/million from my 32b random function. Previous to that, I had been dealing with 100s/million. +I tried almost everything and got nowhere improving on this, until I did. I am glad to report that I updated my random() function and now it will yield over 500 million unique values in a row. This is as coded, compiled with VS-C 2022, and ran on my PC; and is dependent on the associated rand() implementation. You may get different results with other development stacks. +So now the testing of MSA_d reports only 12 repeats/million, and MSA_e reports 0/million. Further testing of **MSA_e results in Zero repeated shuffles** out of the 500 million generated. No telling how many billions of 52 item shuffle permutations it could potentially provide. +Note: Due to the hash table based process I use for testing, there may actually be many times more unique shuffles being generated, and there is theoretically some chance that there are duplicate shuffles that are not detected. + - - - +Prior to **Aug 2023**, the randomizing factors (r1-rx) were set empirically for best test results. Now setting them to modulo values of different primes, where the primes multiplied together is greater than the SID 32bit value range ensures that the r-factors (e.g. r1,r2,r3) are a unique set per a corollary to the Chinese remainder theorem. +Doing this for the r-factors maximizes the potential shuffle permutations generated. This has little effect on the random nature of a given shuffle. Further I don’t consider that using different r-factors constitutes a different Miller Shuffle variant. The heart of a variant is determined by the algorithm making up its combined shuffle operations. This is also what predominantly determines the random nature of the shuffles generated. +The latest MillerShuffleAlgo -E has been updated accordingly. + diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index 0f90334b..b219ab6e 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -9,6 +9,6 @@ idf_component_register( "audio_source.cpp" INCLUDE_DIRS "include" REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" - "database" "system_fsm" "speexdsp") + "database" "system_fsm" "speexdsp" "millershuffle") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/audio/include/track_queue.hpp b/src/audio/include/track_queue.hpp index 4a1984c9..24b4fe48 100644 --- a/src/audio/include/track_queue.hpp +++ b/src/audio/include/track_queue.hpp @@ -18,6 +18,30 @@ namespace audio { +/* + * Utility that uses a Miller shuffle to yield well-distributed random indexes + * from within a range. + */ +class RandomIterator { + public: + RandomIterator(size_t size); + + auto current() const -> size_t; + + auto next() -> void; + auto prev() -> void; + + // Note resizing has the side-effect of restarting iteration. + auto resize(size_t) -> void; + auto repeat(bool) -> void; + + private: + size_t seed_; + size_t pos_; + size_t size_; + bool repeat_; +}; + /* * Owns and manages a complete view of the playback queue. Includes the * currently playing track, a truncated list of previously played tracks, and @@ -51,7 +75,7 @@ class TrackQueue { auto totalSize() const -> size_t; using Item = std::variant; - auto insert(Item) -> void; + auto insert(Item, size_t index = 0) -> void; auto append(Item i) -> void; /* @@ -68,6 +92,12 @@ class TrackQueue { */ auto clear() -> void; + auto random(bool) -> void; + auto random() const -> bool; + + auto repeat(bool) -> void; + auto repeat() const -> bool; + // Cannot be copied or moved. TrackQueue(const TrackQueue&) = delete; TrackQueue& operator=(const TrackQueue&) = delete; @@ -79,6 +109,9 @@ class TrackQueue { size_t pos_; std::pmr::vector tracks_; + + std::optional shuffle_; + bool repeat_; }; } // namespace audio diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp index c1187107..7e08e3a2 100644 --- a/src/audio/track_queue.cpp +++ b/src/audio/track_queue.cpp @@ -5,14 +5,18 @@ */ #include "track_queue.hpp" -#include #include +#include #include #include #include +#include #include +#include "MillerShuffle.h" +#include "esp_random.h" + #include "audio_events.hpp" #include "audio_fsm.hpp" #include "cppbor.h" @@ -28,6 +32,42 @@ namespace audio { [[maybe_unused]] static constexpr char kTag[] = "tracks"; +RandomIterator::RandomIterator(size_t size) + : seed_(), pos_(0), size_(size), repeat_(false) { + esp_fill_random(&seed_, sizeof(seed_)); +} + +auto RandomIterator::current() const -> size_t { + if (pos_ < size_ || repeat_) { + return MillerShuffle(pos_, seed_, size_); + } + return size_; +} + +auto RandomIterator::next() -> void { + // MillerShuffle behaves well with pos > size, returning different + // permutations each 'cycle'. We therefore don't need to worry about wrapping + // this value. + pos_++; +} + +auto RandomIterator::prev() -> void { + if (pos_ > 0) { + pos_--; + } +} + +auto RandomIterator::resize(size_t s) -> void { + size_ = s; + // Changing size will yield a different current position anyway, so reset pos + // to ensure we yield a full sweep of both new and old indexes. + pos_ = 0; +} + +auto RandomIterator::repeat(bool r) -> void { + repeat_ = r; +} + auto notifyChanged(bool current_changed) -> void { QueueUpdate ev{.current_changed = current_changed}; events::Ui().Dispatch(ev); @@ -38,7 +78,9 @@ TrackQueue::TrackQueue(tasks::Worker& bg_worker) : mutex_(), bg_worker_(bg_worker), pos_(0), - tracks_(&memory::kSpiRamResource) {} + tracks_(&memory::kSpiRamResource), + shuffle_(), + repeat_(false) {} auto TrackQueue::current() const -> std::optional { const std::shared_lock lock(mutex_); @@ -78,87 +120,191 @@ auto TrackQueue::totalSize() const -> size_t { return tracks_.size(); } -auto TrackQueue::insert(Item i) -> void { - bool current_changed = pos_ == tracks_.size(); +auto TrackQueue::insert(Item i, size_t index) -> void { + bool was_queue_empty; + bool current_changed; + { + const std::shared_lock lock(mutex_); + was_queue_empty = pos_ == tracks_.size(); + current_changed = pos_ == was_queue_empty || index == pos_; + } + + auto update_shuffler = [=, this]() { + if (shuffle_) { + shuffle_->resize(tracks_.size()); + // If there wasn't anything already playing, then we should make sure we + // begin playback at a random point, instead of always starting with + // whatever was inserted first and *then* shuffling. + // We don't base this purely off of current_changed because we would like + // 'play this track now' (by inserting at the current pos) to work even + // when shuffling is enabled. + if (was_queue_empty) { + pos_ = shuffle_->current(); + } + } + }; + if (std::holds_alternative(i)) { - const std::unique_lock lock(mutex_); - tracks_.push_back(std::get(i)); + { + const std::unique_lock lock(mutex_); + if (index <= tracks_.size()) { + tracks_.insert(tracks_.begin() + index, std::get(i)); + update_shuffler(); + } + } notifyChanged(current_changed); } else if (std::holds_alternative(i)) { + // Iterators can be very large, and retrieving items from them often + // requires disk i/o. Handle them asynchronously so that inserting them + // doesn't block. bg_worker_.Dispatch([=, this]() { database::TrackIterator it = std::get(i); - size_t working_pos = pos_; + size_t working_pos = index; while (true) { auto next = *it; if (!next) { break; } - const std::unique_lock lock(mutex_); - tracks_.insert(tracks_.begin() + working_pos, *next); + // Keep this critical section small so that we're not blocking methods + // like current(). + { + const std::unique_lock lock(mutex_); + if (working_pos <= tracks_.size()) { + tracks_.insert(tracks_.begin() + working_pos, *next); + } + } working_pos++; it++; } + { + const std::unique_lock lock(mutex_); + update_shuffler(); + } notifyChanged(current_changed); }); } } auto TrackQueue::append(Item i) -> void { - bool current_changed = pos_ == tracks_.size(); - if (std::holds_alternative(i)) { - const std::unique_lock lock(mutex_); - tracks_.push_back(std::get(i)); - notifyChanged(current_changed); - } else if (std::holds_alternative(i)) { - bg_worker_.Dispatch([=, this]() { - database::TrackIterator it = std::get(i); - while (true) { - auto next = *it; - if (!next) { - break; - } - const std::unique_lock lock(mutex_); - tracks_.push_back(*next); - it++; - } - notifyChanged(current_changed); - }); + size_t end; + { + const std::shared_lock lock(mutex_); + end = tracks_.size(); } + insert(i, end); } auto TrackQueue::next() -> void { const std::unique_lock lock(mutex_); - pos_ = std::min(pos_ + 1, tracks_.size()); + if (shuffle_) { + shuffle_->next(); + pos_ = shuffle_->current(); + } else { + pos_++; + if (pos_ >= tracks_.size() && repeat_) { + pos_ = 0; + } + } notifyChanged(true); } auto TrackQueue::previous() -> void { const std::unique_lock lock(mutex_); - if (pos_ > 0) { - pos_--; + if (shuffle_) { + shuffle_->prev(); + pos_ = shuffle_->current(); + } else { + if (pos_ == 0) { + if (repeat_) { + pos_ = tracks_.size() - 1; + } + } else { + pos_--; + } } notifyChanged(true); } auto TrackQueue::skipTo(database::TrackId id) -> void { - const std::unique_lock lock(mutex_); - for (size_t i = pos_; i < tracks_.size(); i++) { - if (tracks_[i] == id) { - pos_ = i; + // Defer this work to the background not because it's particularly + // long-running (although it could be), but because we want to ensure we only + // search for the given id after any previously pending iterator insertions + // have finished. + bg_worker_.Dispatch([=, this]() { + bool found = false; + { + const std::unique_lock lock(mutex_); + for (size_t i = 0; i < tracks_.size(); i++) { + if (tracks_[i] == id) { + pos_ = i; + found = true; + break; + } + } + } + if (found) { + notifyChanged(true); + } + }); +} + +auto TrackQueue::clear() -> void { + { + const std::unique_lock lock(mutex_); + if (tracks_.empty()) { + return; + } + + pos_ = 0; + tracks_.clear(); + + if (shuffle_) { + shuffle_->resize(0); } } notifyChanged(true); } -auto TrackQueue::clear() -> void { - const std::unique_lock lock(mutex_); - pos_ = 0; - tracks_.clear(); +auto TrackQueue::random(bool en) -> void { + { + const std::unique_lock lock(mutex_); + // Don't check for en == true already; this has the side effect that + // repeated calls with en == true will re-shuffle. + if (en) { + shuffle_.emplace(tracks_.size()); + shuffle_->repeat(repeat_); + } else { + shuffle_.reset(); + } + } - notifyChanged(true); + // Current track doesn't get randomised until next(). + notifyChanged(false); +} + +auto TrackQueue::random() const -> bool { + const std::shared_lock lock(mutex_); + return shuffle_.has_value(); +} + +auto TrackQueue::repeat(bool en) -> void { + { + const std::unique_lock lock(mutex_); + repeat_ = en; + if (shuffle_) { + shuffle_->repeat(en); + } + } + + notifyChanged(false); +} + +auto TrackQueue::repeat() const -> bool { + const std::shared_lock lock(mutex_); + return repeat_; } } // namespace audio diff --git a/src/lua/stubs/queue.lua b/src/lua/stubs/queue.lua new file mode 100644 index 00000000..000c35d3 --- /dev/null +++ b/src/lua/stubs/queue.lua @@ -0,0 +1,22 @@ +--- Properties and functions for inspecting and manipulating the track playback queue +-- @module queue + +local queue = {} + +--- queue.position returns the index in the queue of the currently playing track. This may be zero if the queue is empty. +-- @treturn types.Property a positive integer property, which is a 1-based index +function queue.position() end + +--- queue.size returns the total number of tracks in the queue, including tracks which have already been played. +-- @treturn types.Property a positive integer property +function queue.size() end + +--- queue.replay determines whether or not the queue will be restarted after the final track is played. +-- @treturn types.Property a writeable boolean property +function queue.replay() end + +--- queue.random determines whether, when progressing to the next track in the queue, the next track will be chosen randomly. The random selection algorithm used is a Miller Shuffle, which guarantees that no repeat selections will be made until every item in the queue has been played. +-- @treturn types.Property a writeable boolean property +function queue.random() end + +return queue \ No newline at end of file diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index 7ad6be93..ba3f5e3f 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -137,6 +137,8 @@ class Lua : public UiState { auto PushLuaScreen(lua_State*) -> int; auto PopLuaScreen(lua_State*) -> int; auto SetPlaying(const lua::LuaValue&) -> bool; + auto SetRandom(const lua::LuaValue&) -> bool; + auto SetRepeat(const lua::LuaValue&) -> bool; std::shared_ptr battery_pct_; std::shared_ptr battery_mv_; @@ -150,6 +152,8 @@ class Lua : public UiState { std::shared_ptr queue_position_; std::shared_ptr queue_size_; + std::shared_ptr queue_repeat_; + std::shared_ptr queue_random_; }; class Browse : public UiState { diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 783494dd..f4b56a27 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -176,6 +176,8 @@ void Lua::entry() { queue_position_ = std::make_shared(0); queue_size_ = std::make_shared(0); + queue_repeat_ = std::make_shared(false); + queue_random_ = std::make_shared(false); playback_playing_ = std::make_shared( false, [&](const lua::LuaValue& val) { return SetPlaying(val); }); @@ -203,6 +205,8 @@ void Lua::entry() { sLua->bridge().AddPropertyModule("queue", { {"position", queue_position_}, {"size", queue_size_}, + {"replay", queue_repeat_}, + {"random", queue_random_}, }); sLua->bridge().AddPropertyModule( "backstack", @@ -242,7 +246,7 @@ auto Lua::PushLuaScreen(lua_State* s) -> int { return 0; } -auto Lua::PopLuaScreen(lua_State *s) -> int { +auto Lua::PopLuaScreen(lua_State* s) -> int { PopScreen(); luavgl_set_root(s, sCurrentScreen->content()); lv_group_set_default(sCurrentScreen->group()); @@ -261,6 +265,24 @@ auto Lua::SetPlaying(const lua::LuaValue& val) -> bool { return true; } +auto Lua::SetRandom(const lua::LuaValue& val) -> bool { + if (!std::holds_alternative(val)) { + return false; + } + bool b = std::get(val); + sServices->track_queue().random(b); + return true; +} + +auto Lua::SetRepeat(const lua::LuaValue& val) -> bool { + if (!std::holds_alternative(val)) { + return false; + } + bool b = std::get(val); + sServices->track_queue().repeat(b); + return true; +} + void Lua::exit() { lv_group_set_default(NULL); } @@ -288,6 +310,8 @@ void Lua::react(const audio::QueueUpdate&) { current_pos++; } queue_position_->Update(current_pos); + queue_random_->Update(queue.random()); + queue_repeat_->Update(queue.repeat()); } void Lua::react(const audio::PlaybackStarted& ev) { diff --git a/tools/cmake/common.cmake b/tools/cmake/common.cmake index 1ab8fcdd..f6e00cae 100644 --- a/tools/cmake/common.cmake +++ b/tools/cmake/common.cmake @@ -12,13 +12,16 @@ set(COMPONENTS "") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/bindey") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/catch2") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/cbor") +list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/esp-idf-lua") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/fatfs") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/komihash") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/libcppbor") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/libfoxenflac") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/libmad") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/libtags") +list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/luavgl") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/lvgl") +list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/millershuffle") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/ogg") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/opusfile") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/result") @@ -26,8 +29,6 @@ list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/span") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/speexdsp") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/tinyfsm") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/tremor") -list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/esp-idf-lua") -list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/luavgl") include($ENV{IDF_PATH}/tools/cmake/project.cmake)