Add shuffle and repeat options for the playback queue

custom
jacqueline 1 year ago
parent aaa949f718
commit ca5d7b867c
  1. 8
      lib/millershuffle/CMakeLists.txt
  2. 190
      lib/millershuffle/LICENSE
  3. 65
      lib/millershuffle/MillerShuffle.c
  4. 14
      lib/millershuffle/MillerShuffle.h
  5. 80
      lib/millershuffle/README.md
  6. 2
      src/audio/CMakeLists.txt
  7. 35
      src/audio/include/track_queue.hpp
  8. 204
      src/audio/track_queue.cpp
  9. 22
      src/lua/stubs/queue.lua
  10. 4
      src/ui/include/ui_fsm.hpp
  11. 26
      src/ui/ui_fsm.cpp
  12. 5
      tools/cmake/common.cmake

@ -0,0 +1,8 @@
# Copyright 2023 jacqueline <me@jacqueline.id.au>
#
# SPDX-License-Identifier: GPL-3.0-only
idf_component_register(
SRCS "MillerShuffle.c"
INCLUDE_DIRS "."
)

@ -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.

@ -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<listSize/2) si=(si*p3+r4) % (listSize/2);
if ((si^rx) < listSize) si ^= rx; // flip some bits with Xor
si = ((unsigned long)si*p3 + r3) % listSize; // relatively prime gears turning operation
if ((si^rx2) < listSize) si ^= rx2;
return(si); // return 'Shuffled' index
}

@ -0,0 +1,14 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
// Miller Shuffle Algorithm proto types
// unsigned int MillerShuffleAlgo_a(unsigned int, unsigned int, unsigned int);
// unsigned int MillerShuffleAlgo_b(unsigned int, unsigned int, unsigned int);
unsigned int MillerShuffle(unsigned int, unsigned int, unsigned int);
#ifdef __cplusplus
}
#endif

@ -0,0 +1,80 @@
# the Miller Shuffle Algorithm
This is a new Shuffle Algorithm.
When implementing play-list shuffle algorithms, apparently some (even on big name electronics and streaming services), simply use an operation like songIndex=random(NumOfSongs). This use of a PRNG will give you a good mathematical randomness to what is played. But you will get many premature repetitions of song selections.
The Fisher-Yates (aka Knuth) algorithm has been a solution that fixes this unwanted repetition. The issue this algorithm does come with is the added burden of an array in RAM memory of 2 times the maximum number of songs (for up to 65,000 items 128KB of RAM is needed) being dedicated to shuffled indexes for the duration that access to additional items from the shuffle are desired. The array is normally maintained by the calling routine, and passed by reference to a function implementing the Fisher-Yates algorithm. This is a significant issue for a resource limited microprocessor application as well as for an online service with millions upon millions of shuffle lists to maintain.
The algorithm I present here (refered to as the Miller Shuffle algorithm) provides basically the same beneficial functionality with a comparable level of randomness, without the need of any array or upfront processing, and does not utilizing a PRNG.
It reduces the algorithm's time complexity to O(1) from O(n) for Fisher-Yates and O(n^2) for naive implementations. It is essentially a [Pseudo Random **Index** Generator](https://docs.google.com/document/d/1UOzZNXHsaTuRHNFvPH_tQwVWfTXUj9xP) (**PRIG**).
As defined herein, a Pseudo Random Index Generator (PRIG) returns each possible value in a range once and only once in a pseudo random order, with the input of a 'shuffleID' and a reference index (0 to N-1, generally used sequentially).
Additionally when utilizing a PRIG, there is no need for an array to serve as a play history record. You can simply decrement the reference index to step back through the play history.
Characteristics of the Miller Shuffle algorithm
* Provides a pseudo random, yet unique, index within a given stated range and a reference value from that range.
* does Not require RAM memory for an array (saves 2 * size of the # of indexes, over F-Y algo)
* No upfront processing. Minimal processing to generate any shuffled index on the fly.
* Deterministic (except B variant). Does not need a record of past plays in order to go back through selections (like using random() would)
* Not dependent on a system PRNG (except B variant).
The way the algorithm works its magic is by utilizing multiple curated computations which are ‘symmetrical’, in that the range of values which go in are the same values which come out albeit in a different order. Conceptually each computation {e.g. si=(i+K) mod N } stirs or scatters about the values within its pot (aka: range 0 to N-1) in a different way such that the combined result is a well randomized shuffle of the values within the range.
This is achieved without the processing of intermediate “candidates” which are redundant or out of range values (unlike with the use of a PRNG or LFSR) which would cause a geometrically increasing inefficiency, due to the overhead of retries.
In applications where an even distribution of expected patterns like a given pair of cards from a 'shuffled' deck is near esseniencial there is room for improvement. To handle this, I devised the Miller Shuffle Algo-b *(no longer necessary given algo-D or later)*, which adds in a little use of a PRNG function. I can only see where this could be considered to be earnestly needed is where money is involved, like in a casino gaming machine.
Note that while you don't get any repeats within a Fisher-Yates shuffle, new session reshuffles (done with FY) result in session to session repeats. Further these unrequested re-shuffles result in loss of play history.
When utilizing a Miller Shuffle algorithm, a logical reference index and a shuffleID are all that needs to be retained in order to continue where one left off.
Examples of when some code might fetch a new Shuffle Index (si) from a shuffle: ...when a blackJack player says “hit me”, or draws a couple of Dominoes or Scrabble tiles, or needs the next song from their shuffled playlist. A perfectly suited case: would be to select a daily Wordle solution word from a curated dictionary, knowing that it couldn’t have been used in the past months or even years.
Miller Shuffle Algos' statistical behavior have been extensively tested, honed and validated over time.
For details, on initial development, randomness statistics and efficacy analysis, of the Miller Shuffle Algo see:
https://www.instructables.com/Miller-Shuffle-Algorithm/
In this repository there is a simple executable program "exampleShuffles" using MillerShuffleAlgo() in a comparison with the use of rand() shuffling a set of items. There is also a Javascript implementation .
To get a visual feel for the randomness of a given shuffle I have done shatter charts spacially mapping consecutive selection pairs. Where bunching occurs it might correlate to going between the same two groups of items (e.g. suits, card face values or albums). [Take a look.](https://docs.google.com/spreadsheets/d/1n-cfXohH4p2NeRkCWs8eUEnNjbuzbWRlPC8en-Ht3qM/edit?usp=sharing) It’s not that these events are happening close in time as the charts have no time axis.
Details on earilier variant changes and history of randomness test results were moved to: Variant_Evolution.md
Comments on supporting very large and small listsizes has been moved to file: Large_small_item_cnts.md
June 2023 Update:
-----------------
MillerShuffleAlgo_e (& MSA_d) have had two simple XOR operations added. While not especially improving the randomness of items found within a shuffle, the ability to produce a multitude (billions) of shuffle permutations is greatly enhanced. The algorithm's likelihood of giving a repeated permutation goes from ~1:1000 to ~1:100000 (maybe even much better than that, I am still having a little hard time separating out repeated ShuffleIDs from repeated shuffle permutations). I have expanded and improved my testing suite, aiding in the algorithms’ improvement. MSA_d has apparently slightly more randomness with in a shuffle while MSA_e is better at producing unique shuffles.
MS_lite also was updated and now is better than MSA_a, MSA_c and the original MSA_b were. The checksum values for the algorithms were accordingly updated.
```
dv Geo. Permu repeated-shuffles r2D
ChiSq ChiSq err devi /million first@ Mark
MSlite
prior: 489 546 4.24% 60.1 903k 493 1.95
updated: 283 365 1.82% 37.6 250 62722 0.88
MSA_d
prior: 257 351 1.64% 58.6 32968 2107 0.84
updated: 254 282 1.50% 29.1 29 127601 0.65
MSA_e
prior: 263 363 1.28% 41.5 290 43742 0.68
updated: 261 303 1.27% 27.4 10 196568 0.68
Nominal: 255 255 << < << >> <
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.

@ -9,6 +9,6 @@ idf_component_register(
"audio_source.cpp" "audio_source.cpp"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" 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}) target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})

@ -18,6 +18,30 @@
namespace audio { 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 * Owns and manages a complete view of the playback queue. Includes the
* currently playing track, a truncated list of previously played tracks, and * currently playing track, a truncated list of previously played tracks, and
@ -51,7 +75,7 @@ class TrackQueue {
auto totalSize() const -> size_t; auto totalSize() const -> size_t;
using Item = std::variant<database::TrackId, database::TrackIterator>; using Item = std::variant<database::TrackId, database::TrackIterator>;
auto insert(Item) -> void; auto insert(Item, size_t index = 0) -> void;
auto append(Item i) -> void; auto append(Item i) -> void;
/* /*
@ -68,6 +92,12 @@ class TrackQueue {
*/ */
auto clear() -> void; auto clear() -> void;
auto random(bool) -> void;
auto random() const -> bool;
auto repeat(bool) -> void;
auto repeat() const -> bool;
// Cannot be copied or moved. // Cannot be copied or moved.
TrackQueue(const TrackQueue&) = delete; TrackQueue(const TrackQueue&) = delete;
TrackQueue& operator=(const TrackQueue&) = delete; TrackQueue& operator=(const TrackQueue&) = delete;
@ -79,6 +109,9 @@ class TrackQueue {
size_t pos_; size_t pos_;
std::pmr::vector<database::TrackId> tracks_; std::pmr::vector<database::TrackId> tracks_;
std::optional<RandomIterator> shuffle_;
bool repeat_;
}; };
} // namespace audio } // namespace audio

@ -5,14 +5,18 @@
*/ */
#include "track_queue.hpp" #include "track_queue.hpp"
#include <stdint.h>
#include <algorithm> #include <algorithm>
#include <cstdint>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <optional> #include <optional>
#include <shared_mutex>
#include <variant> #include <variant>
#include "MillerShuffle.h"
#include "esp_random.h"
#include "audio_events.hpp" #include "audio_events.hpp"
#include "audio_fsm.hpp" #include "audio_fsm.hpp"
#include "cppbor.h" #include "cppbor.h"
@ -28,6 +32,42 @@ namespace audio {
[[maybe_unused]] static constexpr char kTag[] = "tracks"; [[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 { auto notifyChanged(bool current_changed) -> void {
QueueUpdate ev{.current_changed = current_changed}; QueueUpdate ev{.current_changed = current_changed};
events::Ui().Dispatch(ev); events::Ui().Dispatch(ev);
@ -38,7 +78,9 @@ TrackQueue::TrackQueue(tasks::Worker& bg_worker)
: mutex_(), : mutex_(),
bg_worker_(bg_worker), bg_worker_(bg_worker),
pos_(0), pos_(0),
tracks_(&memory::kSpiRamResource) {} tracks_(&memory::kSpiRamResource),
shuffle_(),
repeat_(false) {}
auto TrackQueue::current() const -> std::optional<database::TrackId> { auto TrackQueue::current() const -> std::optional<database::TrackId> {
const std::shared_lock<std::shared_mutex> lock(mutex_); const std::shared_lock<std::shared_mutex> lock(mutex_);
@ -78,87 +120,191 @@ auto TrackQueue::totalSize() const -> size_t {
return tracks_.size(); return tracks_.size();
} }
auto TrackQueue::insert(Item i) -> void { auto TrackQueue::insert(Item i, size_t index) -> void {
bool current_changed = pos_ == tracks_.size(); bool was_queue_empty;
bool current_changed;
{
const std::shared_lock<std::shared_mutex> 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<database::TrackId>(i)) { if (std::holds_alternative<database::TrackId>(i)) {
{
const std::unique_lock<std::shared_mutex> lock(mutex_); const std::unique_lock<std::shared_mutex> lock(mutex_);
tracks_.push_back(std::get<database::TrackId>(i)); if (index <= tracks_.size()) {
tracks_.insert(tracks_.begin() + index, std::get<database::TrackId>(i));
update_shuffler();
}
}
notifyChanged(current_changed); notifyChanged(current_changed);
} else if (std::holds_alternative<database::TrackIterator>(i)) { } else if (std::holds_alternative<database::TrackIterator>(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<void>([=, this]() { bg_worker_.Dispatch<void>([=, this]() {
database::TrackIterator it = std::get<database::TrackIterator>(i); database::TrackIterator it = std::get<database::TrackIterator>(i);
size_t working_pos = pos_; size_t working_pos = index;
while (true) { while (true) {
auto next = *it; auto next = *it;
if (!next) { if (!next) {
break; break;
} }
// Keep this critical section small so that we're not blocking methods
// like current().
{
const std::unique_lock<std::shared_mutex> lock(mutex_); const std::unique_lock<std::shared_mutex> lock(mutex_);
if (working_pos <= tracks_.size()) {
tracks_.insert(tracks_.begin() + working_pos, *next); tracks_.insert(tracks_.begin() + working_pos, *next);
}
}
working_pos++; working_pos++;
it++; it++;
} }
{
const std::unique_lock<std::shared_mutex> lock(mutex_);
update_shuffler();
}
notifyChanged(current_changed); notifyChanged(current_changed);
}); });
} }
} }
auto TrackQueue::append(Item i) -> void { auto TrackQueue::append(Item i) -> void {
bool current_changed = pos_ == tracks_.size(); size_t end;
if (std::holds_alternative<database::TrackId>(i)) { {
const std::unique_lock<std::shared_mutex> lock(mutex_); const std::shared_lock<std::shared_mutex> lock(mutex_);
tracks_.push_back(std::get<database::TrackId>(i)); end = tracks_.size();
notifyChanged(current_changed);
} else if (std::holds_alternative<database::TrackIterator>(i)) {
bg_worker_.Dispatch<void>([=, this]() {
database::TrackIterator it = std::get<database::TrackIterator>(i);
while (true) {
auto next = *it;
if (!next) {
break;
}
const std::unique_lock<std::shared_mutex> lock(mutex_);
tracks_.push_back(*next);
it++;
}
notifyChanged(current_changed);
});
} }
insert(i, end);
} }
auto TrackQueue::next() -> void { auto TrackQueue::next() -> void {
const std::unique_lock<std::shared_mutex> lock(mutex_); const std::unique_lock<std::shared_mutex> lock(mutex_);
pos_ = std::min<size_t>(pos_ + 1, tracks_.size()); if (shuffle_) {
shuffle_->next();
pos_ = shuffle_->current();
} else {
pos_++;
if (pos_ >= tracks_.size() && repeat_) {
pos_ = 0;
}
}
notifyChanged(true); notifyChanged(true);
} }
auto TrackQueue::previous() -> void { auto TrackQueue::previous() -> void {
const std::unique_lock<std::shared_mutex> lock(mutex_); const std::unique_lock<std::shared_mutex> lock(mutex_);
if (pos_ > 0) { if (shuffle_) {
shuffle_->prev();
pos_ = shuffle_->current();
} else {
if (pos_ == 0) {
if (repeat_) {
pos_ = tracks_.size() - 1;
}
} else {
pos_--; pos_--;
} }
}
notifyChanged(true); notifyChanged(true);
} }
auto TrackQueue::skipTo(database::TrackId id) -> void { auto TrackQueue::skipTo(database::TrackId id) -> void {
// 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<void>([=, this]() {
bool found = false;
{
const std::unique_lock<std::shared_mutex> lock(mutex_); const std::unique_lock<std::shared_mutex> lock(mutex_);
for (size_t i = pos_; i < tracks_.size(); i++) { for (size_t i = 0; i < tracks_.size(); i++) {
if (tracks_[i] == id) { if (tracks_[i] == id) {
pos_ = i; pos_ = i;
found = true;
break;
} }
} }
}
if (found) {
notifyChanged(true); notifyChanged(true);
}
});
} }
auto TrackQueue::clear() -> void { auto TrackQueue::clear() -> void {
{
const std::unique_lock<std::shared_mutex> lock(mutex_); const std::unique_lock<std::shared_mutex> lock(mutex_);
if (tracks_.empty()) {
return;
}
pos_ = 0; pos_ = 0;
tracks_.clear(); tracks_.clear();
if (shuffle_) {
shuffle_->resize(0);
}
}
notifyChanged(true); notifyChanged(true);
} }
auto TrackQueue::random(bool en) -> void {
{
const std::unique_lock<std::shared_mutex> 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();
}
}
// Current track doesn't get randomised until next().
notifyChanged(false);
}
auto TrackQueue::random() const -> bool {
const std::shared_lock<std::shared_mutex> lock(mutex_);
return shuffle_.has_value();
}
auto TrackQueue::repeat(bool en) -> void {
{
const std::unique_lock<std::shared_mutex> lock(mutex_);
repeat_ = en;
if (shuffle_) {
shuffle_->repeat(en);
}
}
notifyChanged(false);
}
auto TrackQueue::repeat() const -> bool {
const std::shared_lock<std::shared_mutex> lock(mutex_);
return repeat_;
}
} // namespace audio } // namespace audio

@ -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

@ -137,6 +137,8 @@ class Lua : public UiState {
auto PushLuaScreen(lua_State*) -> int; auto PushLuaScreen(lua_State*) -> int;
auto PopLuaScreen(lua_State*) -> int; auto PopLuaScreen(lua_State*) -> int;
auto SetPlaying(const lua::LuaValue&) -> bool; auto SetPlaying(const lua::LuaValue&) -> bool;
auto SetRandom(const lua::LuaValue&) -> bool;
auto SetRepeat(const lua::LuaValue&) -> bool;
std::shared_ptr<lua::Property> battery_pct_; std::shared_ptr<lua::Property> battery_pct_;
std::shared_ptr<lua::Property> battery_mv_; std::shared_ptr<lua::Property> battery_mv_;
@ -150,6 +152,8 @@ class Lua : public UiState {
std::shared_ptr<lua::Property> queue_position_; std::shared_ptr<lua::Property> queue_position_;
std::shared_ptr<lua::Property> queue_size_; std::shared_ptr<lua::Property> queue_size_;
std::shared_ptr<lua::Property> queue_repeat_;
std::shared_ptr<lua::Property> queue_random_;
}; };
class Browse : public UiState { class Browse : public UiState {

@ -176,6 +176,8 @@ void Lua::entry() {
queue_position_ = std::make_shared<lua::Property>(0); queue_position_ = std::make_shared<lua::Property>(0);
queue_size_ = std::make_shared<lua::Property>(0); queue_size_ = std::make_shared<lua::Property>(0);
queue_repeat_ = std::make_shared<lua::Property>(false);
queue_random_ = std::make_shared<lua::Property>(false);
playback_playing_ = std::make_shared<lua::Property>( playback_playing_ = std::make_shared<lua::Property>(
false, [&](const lua::LuaValue& val) { return SetPlaying(val); }); false, [&](const lua::LuaValue& val) { return SetPlaying(val); });
@ -203,6 +205,8 @@ void Lua::entry() {
sLua->bridge().AddPropertyModule("queue", { sLua->bridge().AddPropertyModule("queue", {
{"position", queue_position_}, {"position", queue_position_},
{"size", queue_size_}, {"size", queue_size_},
{"replay", queue_repeat_},
{"random", queue_random_},
}); });
sLua->bridge().AddPropertyModule( sLua->bridge().AddPropertyModule(
"backstack", "backstack",
@ -242,7 +246,7 @@ auto Lua::PushLuaScreen(lua_State* s) -> int {
return 0; return 0;
} }
auto Lua::PopLuaScreen(lua_State *s) -> int { auto Lua::PopLuaScreen(lua_State* s) -> int {
PopScreen(); PopScreen();
luavgl_set_root(s, sCurrentScreen->content()); luavgl_set_root(s, sCurrentScreen->content());
lv_group_set_default(sCurrentScreen->group()); lv_group_set_default(sCurrentScreen->group());
@ -261,6 +265,24 @@ auto Lua::SetPlaying(const lua::LuaValue& val) -> bool {
return true; return true;
} }
auto Lua::SetRandom(const lua::LuaValue& val) -> bool {
if (!std::holds_alternative<bool>(val)) {
return false;
}
bool b = std::get<bool>(val);
sServices->track_queue().random(b);
return true;
}
auto Lua::SetRepeat(const lua::LuaValue& val) -> bool {
if (!std::holds_alternative<bool>(val)) {
return false;
}
bool b = std::get<bool>(val);
sServices->track_queue().repeat(b);
return true;
}
void Lua::exit() { void Lua::exit() {
lv_group_set_default(NULL); lv_group_set_default(NULL);
} }
@ -288,6 +310,8 @@ void Lua::react(const audio::QueueUpdate&) {
current_pos++; current_pos++;
} }
queue_position_->Update(current_pos); queue_position_->Update(current_pos);
queue_random_->Update(queue.random());
queue_repeat_->Update(queue.repeat());
} }
void Lua::react(const audio::PlaybackStarted& ev) { void Lua::react(const audio::PlaybackStarted& ev) {

@ -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/bindey")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/catch2") 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/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/fatfs")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/komihash") 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/libcppbor")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/libfoxenflac") 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/libmad")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/libtags") 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/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/ogg")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/opusfile") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/opusfile")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/result") 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/speexdsp")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/tinyfsm") 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/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) include($ENV{IDF_PATH}/tools/cmake/project.cmake)

Loading…
Cancel
Save