diff options
| author | jacqueline <me@jacqueline.id.au> | 2023-12-08 11:11:57 +1100 |
|---|---|---|
| committer | jacqueline <me@jacqueline.id.au> | 2023-12-08 11:11:57 +1100 |
| commit | ca5d7b867c381b7886a660ce744df0b74f38b2e6 (patch) | |
| tree | 61987a625e63a124c142be3df2c8dde0d9c14613 /src/audio | |
| parent | aaa949f71805e2040c7ee9a4d0a3c260de95a6d0 (diff) | |
| download | tangara-fw-ca5d7b867c381b7886a660ce744df0b74f38b2e6.tar.gz | |
Add shuffle and repeat options for the playback queue
Diffstat (limited to 'src/audio')
| -rw-r--r-- | src/audio/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/audio/include/track_queue.hpp | 35 | ||||
| -rw-r--r-- | src/audio/track_queue.cpp | 226 |
3 files changed, 221 insertions, 42 deletions
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 @@ -19,6 +19,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 * all future tracks that have been queued. @@ -51,7 +75,7 @@ class TrackQueue { auto totalSize() const -> size_t; 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; /* @@ -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<database::TrackId> tracks_; + + std::optional<RandomIterator> 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 <stdint.h> #include <algorithm> +#include <cstdint> #include <memory> #include <mutex> #include <optional> +#include <shared_mutex> #include <variant> +#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<database::TrackId> { const std::shared_lock<std::shared_mutex> 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<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)) { - const std::unique_lock<std::shared_mutex> lock(mutex_); - tracks_.push_back(std::get<database::TrackId>(i)); + { + const std::unique_lock<std::shared_mutex> lock(mutex_); + if (index <= tracks_.size()) { + tracks_.insert(tracks_.begin() + index, std::get<database::TrackId>(i)); + update_shuffler(); + } + } notifyChanged(current_changed); } 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]() { database::TrackIterator it = std::get<database::TrackIterator>(i); - size_t working_pos = pos_; + size_t working_pos = index; while (true) { auto next = *it; if (!next) { break; } - const std::unique_lock<std::shared_mutex> 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<std::shared_mutex> lock(mutex_); + if (working_pos <= tracks_.size()) { + tracks_.insert(tracks_.begin() + working_pos, *next); + } + } working_pos++; it++; } + { + const std::unique_lock<std::shared_mutex> lock(mutex_); + update_shuffler(); + } notifyChanged(current_changed); }); } } auto TrackQueue::append(Item i) -> void { - bool current_changed = pos_ == tracks_.size(); - if (std::holds_alternative<database::TrackId>(i)) { - const std::unique_lock<std::shared_mutex> lock(mutex_); - tracks_.push_back(std::get<database::TrackId>(i)); - 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); - }); + size_t end; + { + const std::shared_lock<std::shared_mutex> lock(mutex_); + end = tracks_.size(); } + insert(i, end); } auto TrackQueue::next() -> void { 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); } auto TrackQueue::previous() -> void { const std::unique_lock<std::shared_mutex> 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<std::shared_mutex> 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<void>([=, this]() { + bool found = false; + { + const std::unique_lock<std::shared_mutex> 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<std::shared_mutex> 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<std::shared_mutex> lock(mutex_); - pos_ = 0; - tracks_.clear(); +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(); + } + } - notifyChanged(true); + // 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 |
