From 5cdc1141ee5f5c7b19809940457b4c4c21db9ae6 Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 19 Dec 2024 04:29:23 +0000 Subject: [PATCH] Queue repeat modes (#126) This replaces the previous system of a separate track and queue repeat, with a RepeatMode type with the following options and behaviours: - OFF: No repeat, queue or track. When the current queue finishes, shuffled or otherwise, playback will stop. - REPEAT_TRACK: The current track will loop indefinitely, unless next is explicitly called through some user action (ie using the next button in the now playing screen) - REPEAT_QUEUE: The entire queue will repeat indefinitely. When shuffled is enabled this will repeat the queue with new combinations each cycle. The repeat mode is persisted in non-volatile storage, so the behaviour will be consistent throughout restarts and queue replacements, and so the "queue repeat by default" use case can be met in this way. In addition, I've made it work a little nicer when the queue runs out in the now playing screen, keeping the previously played track shown and playback can be continued by using the play button or by going to a previous song in the queue. Reviewed-on: https://codeberg.org/cool-tech-zone/tangara-fw/pulls/126 Co-authored-by: ailurux Co-committed-by: ailurux --- lua/images.lua | 3 +- lua/img/repeat_queue.png | Bin 0 -> 10055 bytes lua/playing.lua | 37 ++++++----- luals-stubs/queue.lua | 3 +- src/drivers/include/drivers/nvs.hpp | 5 ++ src/drivers/nvs.cpp | 14 ++++ src/tangara/audio/track_queue.cpp | 99 ++++++++++------------------ src/tangara/audio/track_queue.hpp | 21 +++--- src/tangara/lua/lua_queue.cpp | 18 +++++ src/tangara/system_fsm/booting.cpp | 2 +- src/tangara/ui/ui_fsm.cpp | 37 ++++------- src/tangara/ui/ui_fsm.hpp | 6 +- 12 files changed, 120 insertions(+), 125 deletions(-) create mode 100644 lua/img/repeat_queue.png diff --git a/lua/images.lua b/lua/images.lua index 3b19f694..d7939305 100644 --- a/lua/images.lua +++ b/lua/images.lua @@ -13,8 +13,9 @@ local img = { prev = lvgl.ImgData("//lua/img/prev.png"), shuffle = lvgl.ImgData("//lua/img/shuffle.png"), shuffle_off = lvgl.ImgData("//lua/img/shuffle_off.png"), - repeat_src = lvgl.ImgData("//lua/img/repeat.png"), -- repeat is a reserved word + repeat_track = lvgl.ImgData("//lua/img/repeat.png"), repeat_off = lvgl.ImgData("//lua/img/repeat_off.png"), + repeat_queue = lvgl.ImgData("//lua/img/repeat_queue.png"), queue = lvgl.ImgData("//lua/img/queue.png"), files = lvgl.ImgData("//lua/img/files.png"), settings = lvgl.ImgData("//lua/img/settings.png"), diff --git a/lua/img/repeat_queue.png b/lua/img/repeat_queue.png new file mode 100644 index 0000000000000000000000000000000000000000..80a3387e9573dd878bda77a64d95fb884b28a18d GIT binary patch literal 10055 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 literal 0 HcmV?d00001 diff --git a/lua/playing.lua b/lua/playing.lua index 4ca79ed6..35e9c8eb 100644 --- a/lua/playing.lua +++ b/lua/playing.lua @@ -176,9 +176,9 @@ return screen:new { local repeat_btn = controls:Button {} repeat_btn:onClicked(function() - queue.repeat_track:set(not queue.repeat_track:get()) + queue.repeat_mode:set((queue.repeat_mode:get() + 1) % 3) end) - local repeat_img = repeat_btn:Image { src = img.repeat_src } + local repeat_img = repeat_btn:Image { src = img.repeat_off } theme.set_subject(repeat_btn, icon_enabled_class) local repeat_desc = widgets.Description(repeat_btn) @@ -197,6 +197,10 @@ return screen:new { local play_pause_btn = controls:Button {} play_pause_btn:onClicked(function() + if (not playback.track:get()) then + -- Restart the last played track + queue.position:set(queue.position:get()) + end playback.playing:set(not playback.playing:get()) end) play_pause_btn:focus() @@ -238,7 +242,10 @@ return screen:new { text = format_time(pos) } local track = playback.track:get() - if not track then return end + if not track then + scrubber:set{value = 0} + return + end if not track.duration then return end scrubber:set { value = pos / track.duration * 100 } end @@ -247,13 +254,7 @@ return screen:new { if not track then if queue.loading:get() then title:set { text = "Loading..." } - else - title:set { text = "" } end - artist:set { text = "" } - cur_time:set { text = format_time(0) } - end_time:set { text = format_time(0) } - scrubber:set { value = 0 } return end if track.duration then @@ -268,7 +269,7 @@ return screen:new { if not pos then return end playlist_pos:set { text = tostring(pos) } - local can_next = pos < queue.size:get() or queue.random:get() + local can_next = pos < queue.size:get() or queue.random:get() or queue.repeat_mode:get() == queue.RepeatMode.REPEAT_QUEUE theme.set_subject( next_btn, can_next and icon_enabled_class or icon_disabled_class ) @@ -283,14 +284,16 @@ return screen:new { shuffle_desc:set { text = "Enable shuffle" } end end), - queue.repeat_track:bind(function(en) - theme.set_subject(repeat_btn, en and icon_enabled_class or icon_disabled_class) - if en then - repeat_img:set_src(img.repeat_src) - repeat_desc:set { text = "Disable track repeat" } - else + queue.repeat_mode:bind(function(mode) + if mode == queue.RepeatMode.OFF then repeat_img:set_src(img.repeat_off) - repeat_desc:set { text = "Enable track repeat" } + repeat_desc:set { text = "Repeat off" } + elseif mode == queue.RepeatMode.REPEAT_TRACK then + repeat_img:set_src(img.repeat_track) + repeat_desc:set { text = "Repeat track" } + elseif mode == queue.RepeatMode.REPEAT_QUEUE then + repeat_img:set_src(img.repeat_queue) + repeat_desc:set { text = "Repeat queue" } end end), queue.size:bind(function(num) diff --git a/luals-stubs/queue.lua b/luals-stubs/queue.lua index a0473407..0fcb6258 100644 --- a/luals-stubs/queue.lua +++ b/luals-stubs/queue.lua @@ -8,8 +8,7 @@ --- @class queue --- @field position Property The index in the queue of the currently playing track. This may be zero if the queue is empty. Writeable. --- @field size Property The total number of tracks in the queue, including tracks which have already been played. ---- @field replay Property Whether or not the queue will be restarted after the final track is played. Writeable. ---- @field repeat_track Property Whether or not the current track will repeat indefinitely. Writeable. +--- @field repeat_mode Property The current repeat mode for the queue. Writeable. --- @field random Property 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. Writeable. local queue = {} diff --git a/src/drivers/include/drivers/nvs.hpp b/src/drivers/include/drivers/nvs.hpp index e147c8c7..9725bb0f 100644 --- a/src/drivers/include/drivers/nvs.hpp +++ b/src/drivers/include/drivers/nvs.hpp @@ -138,6 +138,9 @@ class NvsStorage { auto PrimaryInput() -> InputModes; auto PrimaryInput(InputModes) -> void; + auto QueueRepeatMode() -> uint8_t; + auto QueueRepeatMode(uint8_t) -> void; + auto DbAutoIndex() -> bool; auto DbAutoIndex(bool) -> void; @@ -173,6 +176,8 @@ class NvsStorage { Setting db_auto_index_; + Setting queue_repeat_mode_; + util::LruCache<10, bluetooth::mac_addr_t, uint8_t> bt_volumes_; bool bt_volumes_dirty_; diff --git a/src/drivers/nvs.cpp b/src/drivers/nvs.cpp index d004201b..6c916e60 100644 --- a/src/drivers/nvs.cpp +++ b/src/drivers/nvs.cpp @@ -41,6 +41,7 @@ static constexpr char kKeyDisplayRows[] = "disprows"; static constexpr char kKeyHapticMotorType[] = "hapticmtype"; static constexpr char kKeyLraCalibration[] = "lra_cali"; static constexpr char kKeyDbAutoIndex[] = "dbautoindex"; +static constexpr char kKeyQueueRepeatMode[] = "queue_rpt"; static constexpr char kKeyFastCharge[] = "fastchg"; static auto nvs_get_string(nvs_handle_t nvs, const char* key) @@ -278,6 +279,7 @@ NvsStorage::NvsStorage(nvs_handle_t handle) bt_preferred_(kKeyBluetoothPreferred), bt_names_(kKeyBluetoothNames), db_auto_index_(kKeyDbAutoIndex), + queue_repeat_mode_(kKeyQueueRepeatMode), bt_volumes_(), bt_volumes_dirty_(false) {} @@ -304,6 +306,7 @@ auto NvsStorage::Read() -> void { bt_preferred_.read(handle_); bt_names_.read(handle_); db_auto_index_.read(handle_); + queue_repeat_mode_.read(handle_); readBtVolumes(); } @@ -325,6 +328,7 @@ auto NvsStorage::Write() -> bool { bt_preferred_.write(handle_); bt_names_.write(handle_); db_auto_index_.write(handle_); + queue_repeat_mode_.write(handle_); writeBtVolumes(); return nvs_commit(handle_) == ESP_OK; } @@ -566,6 +570,16 @@ auto NvsStorage::PrimaryInput(InputModes mode) -> void { input_mode_.set(static_cast(mode)); } +auto NvsStorage::QueueRepeatMode() -> uint8_t { + std::lock_guard lock{mutex_}; + return queue_repeat_mode_.get().value_or(0); +} + +auto NvsStorage::QueueRepeatMode(uint8_t mode) -> void { + std::lock_guard lock{mutex_}; + queue_repeat_mode_.set(mode); +} + auto NvsStorage::DbAutoIndex() -> bool { std::lock_guard lock{mutex_}; return db_auto_index_.get().value_or(true); diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index 0af8bee0..5689ecf0 100644 --- a/src/tangara/audio/track_queue.cpp +++ b/src/tangara/audio/track_queue.cpp @@ -38,25 +38,26 @@ namespace audio { using Reason = QueueUpdate::Reason; RandomIterator::RandomIterator() - : seed_(0), pos_(0), size_(0), replay_(false) {} + : seed_(0), pos_(0), size_(0) {} RandomIterator::RandomIterator(size_t size) - : seed_(), pos_(0), size_(size), replay_(false) { + : seed_(), pos_(0), size_(size) { esp_fill_random(&seed_, sizeof(seed_)); } auto RandomIterator::current() const -> size_t { - if (pos_ < size_ || replay_) { - return MillerShuffle(pos_, seed_, size_); - } - return size_; + return MillerShuffle(pos_, seed_, size_); } -auto RandomIterator::next() -> void { +auto RandomIterator::next(bool repeat) -> bool { // MillerShuffle behaves well with pos > size, returning different // permutations each 'cycle'. We therefore don't need to worry about wrapping // this value. - pos_++; + if (pos_ < size_ - 1 || repeat) { + pos_++; + return true; + } + return false; } auto RandomIterator::prev() -> void { @@ -72,10 +73,6 @@ auto RandomIterator::resize(size_t s) -> void { pos_ = 0; } -auto RandomIterator::replay(bool r) -> void { - replay_ = r; -} - auto notifyChanged(bool current_changed, Reason reason) -> void { QueueUpdate ev{ .current_changed = current_changed, @@ -95,15 +92,15 @@ auto notifyPlayFrom(uint32_t start_from_position) -> void { events::Audio().Dispatch(ev); } -TrackQueue::TrackQueue(tasks::WorkerPool& bg_worker, database::Handle db) +TrackQueue::TrackQueue(tasks::WorkerPool& bg_worker, database::Handle db, drivers::NvsStorage& nvs) : mutex_(), bg_worker_(bg_worker), db_(db), + nvs_(nvs), playlist_(".queue.playlist"), position_(0), shuffle_(), - repeat_(false), - replay_(false) {} + repeatMode_(static_cast(nvs.QueueRepeatMode())) {} auto TrackQueue::current() const -> TrackItem { const std::shared_lock lock(mutex_); @@ -293,26 +290,25 @@ auto TrackQueue::goTo(size_t position) -> void { } auto TrackQueue::next(Reason r) -> void { - bool changed = true; + bool changed = false; { const std::unique_lock lock(mutex_); - auto pos = position_; if (shuffle_) { - shuffle_->next(); + changed = shuffle_->next(repeatMode_ == RepeatMode::REPEAT_QUEUE); position_ = shuffle_->current(); } else { if (position_ + 1 < totalSize()) { position_++; // Next track - } - else { + changed = true; + } else if (repeatMode_ == RepeatMode::REPEAT_QUEUE) { position_ = 0; // Go to beginning + changed = true; } } goTo(position_); - changed = pos != position_; } notifyChanged(changed, r); @@ -329,9 +325,8 @@ auto TrackQueue::previous() -> void { } else { if (position_ > 0) { position_--; - } - else { - position_ = totalSize(); + } else if (repeatMode_ == RepeatMode::REPEAT_QUEUE) { + position_ = totalSize()-1; // Go to the end of the queue } } goTo(position_); @@ -341,7 +336,7 @@ auto TrackQueue::previous() -> void { } auto TrackQueue::finish() -> void { - if (repeat_) { + if (repeatMode_ == RepeatMode::REPEAT_TRACK) { notifyChanged(true, Reason::kRepeatingLastTrack); } else { next(Reason::kTrackFinished); @@ -367,7 +362,6 @@ auto TrackQueue::random(bool en) -> void { const std::unique_lock lock(mutex_); if (en) { shuffle_.emplace(totalSize()); - shuffle_->replay(replay_); } else { shuffle_.reset(); } @@ -382,34 +376,14 @@ auto TrackQueue::random() const -> bool { return shuffle_.has_value(); } -auto TrackQueue::repeat(bool en) -> void { - { - const std::unique_lock lock(mutex_); - repeat_ = en; - } - +auto TrackQueue::repeatMode(RepeatMode mode) -> void { + repeatMode_ = mode; + nvs_.QueueRepeatMode(repeatMode_); notifyChanged(false, Reason::kExplicitUpdate); } -auto TrackQueue::repeat() const -> bool { - const std::shared_lock lock(mutex_); - return repeat_; -} - -auto TrackQueue::replay(bool en) -> void { - { - const std::unique_lock lock(mutex_); - replay_ = en; - if (shuffle_) { - shuffle_->replay(en); - } - } - notifyChanged(false, Reason::kExplicitUpdate); -} - -auto TrackQueue::replay() const -> bool { - const std::shared_lock lock(mutex_); - return replay_; +auto TrackQueue::repeatMode() const -> RepeatMode { + return repeatMode_; } auto TrackQueue::serialise() -> std::string { @@ -417,9 +391,8 @@ auto TrackQueue::serialise() -> std::string { cppbor::Map encoded; cppbor::Array metadata{ - cppbor::Bool{repeat_}, - cppbor::Bool{replay_}, cppbor::Uint{position_}, + cppbor::Uint{repeatMode_}, }; if (opened_playlist_) { @@ -471,26 +444,24 @@ cppbor::ParseClient* TrackQueue::QueueParseClient::item( i_ = 0; } else if (item->type() == cppbor::UINT) { auto val = item->asUint()->unsignedValue(); - // Save the position so we can apply it later when we have finished - // serialising - position_to_set_ = val; - } else if (item->type() == cppbor::TSTR) { - auto val = item->asTstr(); - queue_.openPlaylist(val->value(), false); - } else if (item->type() == cppbor::SIMPLE) { - bool val = item->asBool()->value(); if (i_ == 0) { - queue_.repeat_ = val; + // First value == position + // Save the position so we can apply it later when we have finished + // serialising + position_to_set_ = val; } else if (i_ == 1) { - queue_.replay_ = val; + // Second value == repeat mode + queue_.repeatMode_ = static_cast(val); } i_++; + } else if (item->type() == cppbor::TSTR) { + auto val = item->asTstr(); + queue_.openPlaylist(val->value(), false); } } else if (state_ == State::kShuffle) { if (item->type() == cppbor::ARRAY) { i_ = 0; queue_.shuffle_.emplace(); - queue_.shuffle_->replay(queue_.replay_); } else if (item->type() == cppbor::UINT) { auto val = item->asUint()->unsignedValue(); switch (i_) { diff --git a/src/tangara/audio/track_queue.hpp b/src/tangara/audio/track_queue.hpp index 383c204e..bc9c3ed2 100644 --- a/src/tangara/audio/track_queue.hpp +++ b/src/tangara/audio/track_queue.hpp @@ -32,12 +32,11 @@ class RandomIterator { auto current() const -> size_t; - auto next() -> void; + auto next(bool repeat) -> bool; auto prev() -> void; // Note resizing has the side-effect of restarting iteration. auto resize(size_t) -> void; - auto replay(bool) -> void; auto seed() -> size_t& { return seed_; } auto pos() -> size_t& { return pos_; } @@ -47,7 +46,6 @@ class RandomIterator { size_t seed_; size_t pos_; size_t size_; - bool replay_; }; /* @@ -65,7 +63,7 @@ class RandomIterator { */ class TrackQueue { public: - TrackQueue(tasks::WorkerPool& bg_worker, database::Handle db); + TrackQueue(tasks::WorkerPool& bg_worker, database::Handle db, drivers::NvsStorage& nvs); /* Returns the currently playing track. */ using TrackItem = @@ -107,11 +105,14 @@ class TrackQueue { auto random(bool) -> void; auto random() const -> bool; - auto repeat(bool) -> void; - auto repeat() const -> bool; + enum RepeatMode { + OFF = 0, + REPEAT_TRACK = 1, + REPEAT_QUEUE = 2, + }; - auto replay(bool) -> void; - auto replay() const -> bool; + auto repeatMode(RepeatMode mode) -> void; + auto repeatMode() const -> RepeatMode; auto serialise() -> std::string; auto deserialise(const std::string&) -> void; @@ -129,6 +130,7 @@ class TrackQueue { tasks::WorkerPool& bg_worker_; database::Handle db_; + drivers::NvsStorage& nvs_; MutablePlaylist playlist_; std::optional opened_playlist_; @@ -136,8 +138,7 @@ class TrackQueue { size_t position_; std::optional shuffle_; - bool repeat_; - bool replay_; + RepeatMode repeatMode_; class QueueParseClient : public cppbor::ParseClient { public: diff --git a/src/tangara/lua/lua_queue.cpp b/src/tangara/lua/lua_queue.cpp index 07093390..0c57aea5 100644 --- a/src/tangara/lua/lua_queue.cpp +++ b/src/tangara/lua/lua_queue.cpp @@ -101,6 +101,24 @@ static const struct luaL_Reg kQueueFuncs[] = { static auto lua_queue(lua_State* state) -> int { luaL_newlib(state, kQueueFuncs); + + // Repeat Mode Enum + lua_pushliteral(state, "RepeatMode"); + lua_newtable(state); + + lua_pushliteral(state, "OFF"); + lua_pushinteger(state, (int)audio::TrackQueue::RepeatMode::OFF); + lua_rawset(state, -3); + + lua_pushliteral(state, "REPEAT_TRACK"); + lua_pushinteger(state, (int)audio::TrackQueue::RepeatMode::REPEAT_TRACK); + lua_rawset(state, -3); + + lua_pushliteral(state, "REPEAT_QUEUE"); + lua_pushinteger(state, (int)audio::TrackQueue::RepeatMode::REPEAT_QUEUE); + lua_rawset(state, -3); + + lua_rawset(state, -3); return 1; } diff --git a/src/tangara/system_fsm/booting.cpp b/src/tangara/system_fsm/booting.cpp index 1f99e3ab..5253e4dd 100644 --- a/src/tangara/system_fsm/booting.cpp +++ b/src/tangara/system_fsm/booting.cpp @@ -97,7 +97,7 @@ auto Booting::entry() -> void { sServices->samd(), std::unique_ptr(adc))); sServices->track_queue(std::make_unique( - sServices->bg_worker(), sServices->database())); + sServices->bg_worker(), sServices->database(), sServices->nvs())); sServices->tag_parser(std::make_unique()); sServices->collator(locale::CreateCollator()); sServices->tts(std::make_unique()); diff --git a/src/tangara/ui/ui_fsm.cpp b/src/tangara/ui/ui_fsm.cpp index 3f59d4ad..5ea4617e 100644 --- a/src/tangara/ui/ui_fsm.cpp +++ b/src/tangara/ui/ui_fsm.cpp @@ -210,20 +210,15 @@ lua::Property UiState::sQueuePosition{0, [](const lua::LuaValue& val){ return sServices->track_queue().currentPosition(new_val-1); }}; lua::Property UiState::sQueueSize{0}; -lua::Property UiState::sQueueRepeat{false, [](const lua::LuaValue& val) { - if (!std::holds_alternative(val)) { +lua::Property UiState::sQueueRepeatMode{0, [](const lua::LuaValue& val) { + if (!std::holds_alternative(val)) { return false; } - bool new_val = std::get(val); - sServices->track_queue().repeat(new_val); - return true; - }}; -lua::Property UiState::sQueueReplay{false, [](const lua::LuaValue& val) { - if (!std::holds_alternative(val)) { + int new_val = std::get(val); + if (new_val < 0 || new_val >= 3) { return false; } - bool new_val = std::get(val); - sServices->track_queue().replay(new_val); + sServices->track_queue().repeatMode(static_cast(new_val)); return true; }}; lua::Property UiState::sQueueRandom{false, [](const lua::LuaValue& val) { @@ -450,8 +445,7 @@ void UiState::react(const audio::QueueUpdate& update) { } sQueuePosition.setDirect(current_pos); sQueueRandom.setDirect(queue.random()); - sQueueRepeat.setDirect(queue.repeat()); - sQueueReplay.setDirect(queue.replay()); + sQueueRepeatMode.setDirect(queue.repeatMode()); if (update.reason == audio::QueueUpdate::Reason::kBulkLoadingUpdate) { sQueueLoading.setDirect(true); @@ -654,8 +648,7 @@ void Lua::entry() { {"previous", [&](lua_State* s) { return QueuePrevious(s); }}, {"position", &sQueuePosition}, {"size", &sQueueSize}, - {"replay", &sQueueReplay}, - {"repeat_track", &sQueueRepeat}, + {"repeat_mode", &sQueueRepeatMode}, {"random", &sQueueRandom}, {"loading", &sQueueLoading}, }); @@ -850,23 +843,15 @@ auto Lua::SetRandom(const lua::LuaValue& val) -> bool { return true; } -auto Lua::SetRepeat(const lua::LuaValue& val) -> bool { - if (!std::holds_alternative(val)) { +auto Lua::SetRepeatMode(const lua::LuaValue& val) -> bool { + if (!std::holds_alternative(val)) { return false; } - bool b = std::get(val); - sServices->track_queue().repeat(b); + int mode = std::get(val); + sServices->track_queue().repeatMode(static_cast(mode)); return true; } -auto Lua::SetReplay(const lua::LuaValue& val) -> bool { - if (!std::holds_alternative(val)) { - return false; - } - bool b = std::get(val); - sServices->track_queue().replay(b); - return true; -} void Lua::exit() { lv_group_set_default(NULL); diff --git a/src/tangara/ui/ui_fsm.hpp b/src/tangara/ui/ui_fsm.hpp index 32966657..c73c77f9 100644 --- a/src/tangara/ui/ui_fsm.hpp +++ b/src/tangara/ui/ui_fsm.hpp @@ -119,8 +119,7 @@ class UiState : public tinyfsm::Fsm { static lua::Property sQueuePosition; static lua::Property sQueueSize; - static lua::Property sQueueReplay; - static lua::Property sQueueRepeat; + static lua::Property sQueueRepeatMode; static lua::Property sQueueRandom; static lua::Property sQueueLoading; @@ -177,8 +176,7 @@ class Lua : public UiState { auto SetPlaying(const lua::LuaValue&) -> bool; auto SetRandom(const lua::LuaValue&) -> bool; - auto SetRepeat(const lua::LuaValue&) -> bool; - auto SetReplay(const lua::LuaValue&) -> bool; + auto SetRepeatMode(const lua::LuaValue&) -> bool; auto QueueNext(lua_State*) -> int; auto QueuePrevious(lua_State*) -> int;