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 00000000..80a3387e Binary files /dev/null and b/lua/img/repeat_queue.png differ 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;