From b34959917446ac5d47ddec7bb6d98a6397045558 Mon Sep 17 00:00:00 2001 From: ailurux Date: Tue, 30 Jul 2024 04:36:48 +0000 Subject: [PATCH] daniel/playlist-queue (#84) Support for playlist files being opened along side the queue's own playlist. Playlists can be opened from the file browser, if the file ends in ".playlist" (will add support for .m3u as well eventually) Reviewed-on: https://codeberg.org/cool-tech-zone/tangara-fw/pulls/84 Co-authored-by: ailurux Co-committed-by: ailurux --- lua/file_browser.lua | 8 +- src/tangara/audio/playlist.cpp | 8 +- src/tangara/audio/playlist.hpp | 15 ++-- src/tangara/audio/track_queue.cpp | 115 ++++++++++++++++++----------- src/tangara/audio/track_queue.hpp | 9 ++- src/tangara/lua/file_iterator.cpp | 41 ++++++---- src/tangara/lua/file_iterator.hpp | 4 +- src/tangara/lua/lua_filesystem.cpp | 68 ++++++++--------- src/tangara/lua/lua_queue.cpp | 14 ++++ 9 files changed, 181 insertions(+), 101 deletions(-) diff --git a/lua/file_browser.lua b/lua/file_browser.lua index 91b84c84..aed4aef8 100644 --- a/lua/file_browser.lua +++ b/lua/file_browser.lua @@ -62,10 +62,14 @@ return screen:new{ if is_dir then backstack.push(require("file_browser"):new{ title = self.title, - iterator = filesystem.iterator(tostring(item)), - breadcrumb = tostring(item) + iterator = filesystem.iterator(item:filepath()), + breadcrumb = item:filepath() }) end + if item:filepath():match("%.playlist$") then + queue.open_playlist(item:filepath()) + backstack.push(playing:new()) + end end end }) diff --git a/src/tangara/audio/playlist.cpp b/src/tangara/audio/playlist.cpp index 506e473f..944ad143 100644 --- a/src/tangara/audio/playlist.cpp +++ b/src/tangara/audio/playlist.cpp @@ -15,7 +15,7 @@ namespace audio { [[maybe_unused]] static constexpr char kTag[] = "playlist"; -Playlist::Playlist(std::string playlistFilepath) +Playlist::Playlist(const std::string& playlistFilepath) : filepath_(playlistFilepath), mutex_(), total_size_(0), @@ -49,7 +49,7 @@ auto Playlist::size() const -> size_t { return total_size_; } -auto Playlist::append(Item i) -> void { +auto MutablePlaylist::append(Item i) -> void { std::unique_lock lock(mutex_); auto offset = f_tell(&file_); bool first_entry = current_value_.empty(); @@ -126,7 +126,9 @@ auto Playlist::value() const -> std::string { return current_value_; } -auto Playlist::clear() -> bool { +MutablePlaylist::MutablePlaylist(const std::string& playlistFilepath) : Playlist(playlistFilepath) {} + +auto MutablePlaylist::clear() -> bool { std::unique_lock lock(mutex_); auto res = f_close(&file_); if (res != FR_OK) { diff --git a/src/tangara/audio/playlist.hpp b/src/tangara/audio/playlist.hpp index 3d914663..b248ac77 100644 --- a/src/tangara/audio/playlist.hpp +++ b/src/tangara/audio/playlist.hpp @@ -26,23 +26,21 @@ namespace audio { */ class Playlist { public: - Playlist(std::string playlistFilepath); - ~Playlist(); + Playlist(const std::string& playlistFilepath); + virtual ~Playlist(); using Item = std::variant; auto open() -> bool; auto currentPosition() const -> size_t; auto size() const -> size_t; - auto append(Item i) -> void; auto skipTo(size_t position) -> void; auto next() -> void; auto prev() -> void; auto value() const -> std::string; - auto clear() -> bool; auto atEnd() -> bool; auto filepath() -> std::string; - private: + protected: std::string filepath_; std::mutex mutex_; size_t total_size_; @@ -62,4 +60,11 @@ class Playlist { auto advanceBy(ssize_t amt) -> bool; }; +class MutablePlaylist : public Playlist { +public: + MutablePlaylist(const std::string& playlistFilepath); + auto clear() -> bool; + auto append(Item i) -> void; +}; + } // namespace audio \ No newline at end of file diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index 1aeecf8a..399d6717 100644 --- a/src/tangara/audio/track_queue.cpp +++ b/src/tangara/audio/track_queue.cpp @@ -84,18 +84,25 @@ auto notifyChanged(bool current_changed, Reason reason) -> void { events::Audio().Dispatch(ev); } + TrackQueue::TrackQueue(tasks::WorkerPool& bg_worker, database::Handle db) : mutex_(), bg_worker_(bg_worker), db_(db), - playlist_("queue.playlist"), // TODO + playlist_(".queue.playlist"), + position_(0), shuffle_(), repeat_(false), replay_(false) {} auto TrackQueue::current() const -> TrackItem { const std::shared_lock lock(mutex_); - std::string val = playlist_.value(); + std::string val; + if (opened_playlist_ && position_ < opened_playlist_->size()) { + val = opened_playlist_->value(); + } else { + val = playlist_.value(); + } if (val.empty()) { return {}; } @@ -104,12 +111,21 @@ auto TrackQueue::current() const -> TrackItem { auto TrackQueue::currentPosition() const -> size_t { const std::shared_lock lock(mutex_); - return playlist_.currentPosition(); + return position_; } auto TrackQueue::totalSize() const -> size_t { - const std::shared_lock lock(mutex_); - return playlist_.size(); + size_t sum = playlist_.size(); + if (opened_playlist_) { + sum += opened_playlist_->size(); + } + return sum; +} + +auto TrackQueue::updateShuffler() -> void { + if (shuffle_) { + shuffle_->resize(totalSize()); + } } auto TrackQueue::open() -> bool { @@ -118,6 +134,17 @@ auto TrackQueue::open() -> bool { return playlist_.open(); } +auto TrackQueue::openPlaylist(const std::string& playlist_file) -> bool { + opened_playlist_.emplace(playlist_file); + auto res = opened_playlist_->open(); + if (!res) { + return false; + } + updateShuffler(); + notifyChanged(true, Reason::kExplicitUpdate); + return true; +} + auto TrackQueue::getFilepath(database::TrackId id) -> std::optional { auto db = db_.lock(); if (!db) { @@ -142,20 +169,15 @@ auto TrackQueue::append(Item i) -> void { current_changed = was_queue_empty; // Dont support inserts yet } - auto update_shuffler = [=, this]() { - if (shuffle_) { - shuffle_->resize(playlist_.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) { - playlist_.skipTo(shuffle_->current()); - } - } - }; + // 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 && shuffle_) { + playlist_.skipTo(shuffle_->current()); + } if (std::holds_alternative(i)) { { @@ -164,7 +186,7 @@ auto TrackQueue::append(Item i) -> void { if (!filename.empty()) { playlist_.append(filename); } - update_shuffler(); + updateShuffler(); } notifyChanged(current_changed, Reason::kExplicitUpdate); } else if (std::holds_alternative(i)) { @@ -191,7 +213,7 @@ auto TrackQueue::append(Item i) -> void { } { const std::unique_lock lock(mutex_); - update_shuffler(); + updateShuffler(); } notifyChanged(current_changed, Reason::kExplicitUpdate); }); @@ -202,6 +224,20 @@ auto TrackQueue::next() -> void { next(Reason::kExplicitUpdate); } +auto TrackQueue::goTo(size_t position) { + position_ = position; + if (opened_playlist_) { + if (position_ < opened_playlist_->size()) { + opened_playlist_->skipTo(position_); + } else { + playlist_.skipTo(position_ - opened_playlist_->size()); + } + } else { + playlist_.skipTo(position_); + } +} + + auto TrackQueue::next(Reason r) -> void { bool changed = true; @@ -209,18 +245,13 @@ auto TrackQueue::next(Reason r) -> void { const std::unique_lock lock(mutex_); if (shuffle_) { shuffle_->next(); - playlist_.skipTo(shuffle_->current()); + position_ = shuffle_->current(); } else { - if (playlist_.atEnd()) { - if (replay_) { - playlist_.skipTo(0); - } else { - changed = false; - } - } else { - playlist_.next(); + if (position_ + 1 < totalSize()) { + position_++; } } + goTo(position_); } notifyChanged(changed, r); @@ -233,18 +264,13 @@ auto TrackQueue::previous() -> void { const std::unique_lock lock(mutex_); if (shuffle_) { shuffle_->prev(); - playlist_.skipTo(shuffle_->current()); + position_ = shuffle_->current(); } else { - if (playlist_.currentPosition() == 0) { - if (repeat_) { - playlist_.skipTo(playlist_.size()-1); - } else { - changed = false; - } - } else { - playlist_.prev(); + if (position_ > 0) { + position_--; } } + goTo(position_); } notifyChanged(changed, Reason::kExplicitUpdate); @@ -262,6 +288,7 @@ auto TrackQueue::clear() -> void { { const std::unique_lock lock(mutex_); playlist_.clear(); + opened_playlist_.reset(); if (shuffle_) { shuffle_->resize(0); } @@ -274,7 +301,7 @@ auto TrackQueue::random(bool en) -> void { { const std::unique_lock lock(mutex_); if (en) { - shuffle_.emplace(playlist_.size()); + shuffle_.emplace(totalSize()); shuffle_->replay(replay_); } else { shuffle_.reset(); @@ -326,7 +353,8 @@ auto TrackQueue::serialise() -> std::string { encoded.add(cppbor::Uint{0}, cppbor::Array{ cppbor::Bool{repeat_}, cppbor::Bool{replay_}, - cppbor::Uint{playlist_.currentPosition()}, + cppbor::Uint{position_}, + cppbor::Tstr{opened_playlist_->filepath()} }); if (shuffle_) { encoded.add(cppbor::Uint{1}, cppbor::Array{ @@ -368,7 +396,10 @@ cppbor::ParseClient* TrackQueue::QueueParseClient::item( i_ = 0; } else if (item->type() == cppbor::UINT) { auto val = item->asUint()->unsignedValue(); - queue_.playlist_.skipTo(val); + queue_.goTo(val); + } else if (item->type() == cppbor::TSTR) { + auto val = item->asTstr(); + queue_.openPlaylist(val->value()); } else if (item->type() == cppbor::SIMPLE) { bool val = item->asBool()->value(); if (i_ == 0) { diff --git a/src/tangara/audio/track_queue.hpp b/src/tangara/audio/track_queue.hpp index 6f50f162..72713242 100644 --- a/src/tangara/audio/track_queue.hpp +++ b/src/tangara/audio/track_queue.hpp @@ -74,11 +74,14 @@ class TrackQueue { auto currentPosition() const -> size_t; auto totalSize() const -> size_t; auto open() -> bool; + auto openPlaylist(const std::string& playlist_file) -> bool; using Item = std::variant; auto insert(Item, size_t index = 0) -> void; auto append(Item i) -> void; + auto updateShuffler() -> void; + /* * Advances to the next track in the queue, placing the current track at the * front of the 'played' queue. @@ -114,6 +117,7 @@ class TrackQueue { private: auto next(QueueUpdate::Reason r) -> void; + auto goTo(size_t position); auto getFilepath(database::TrackId id) -> std::optional; mutable std::shared_mutex mutex_; @@ -121,7 +125,10 @@ class TrackQueue { tasks::WorkerPool& bg_worker_; database::Handle db_; - Playlist playlist_; + MutablePlaylist playlist_; + std::optional opened_playlist_; + + size_t position_; std::optional shuffle_; bool repeat_; diff --git a/src/tangara/lua/file_iterator.cpp b/src/tangara/lua/file_iterator.cpp index c3d63a16..823775e8 100644 --- a/src/tangara/lua/file_iterator.cpp +++ b/src/tangara/lua/file_iterator.cpp @@ -15,8 +15,8 @@ namespace lua { [[maybe_unused]] static const char* kTag = "FileIterator"; -FileIterator::FileIterator(std::string filepath) - : original_path_(filepath), current_(), offset_(-1) { +FileIterator::FileIterator(std::string filepath, bool showHidden) + : original_path_(filepath), show_hidden_(showHidden), current_(), offset_(-1) { const TCHAR* path = static_cast(filepath.c_str()); FRESULT res = f_opendir(&dir_, path); if (res != FR_OK) { @@ -33,7 +33,16 @@ auto FileIterator::value() const -> const std::optional& { } auto FileIterator::next() -> void { - iterate(false); + size_t prev_index = -1; + if (current_) { + prev_index = current_->index; + } + do { + bool res = iterate(show_hidden_); + if (!res) { + break; + } + } while (!current_ || current_->index == prev_index); } auto FileIterator::prev() -> void { @@ -45,11 +54,11 @@ auto FileIterator::prev() -> void { auto new_offset = offset_ - 1; offset_ = -1; for (int i = 0; i <= new_offset; i++) { - iterate(false); + iterate(show_hidden_); } } -auto FileIterator::iterate(bool reverse) -> bool { +auto FileIterator::iterate(bool show_hidden) -> bool { FILINFO info; auto res = f_readdir(&dir_, &info); if (res != FR_OK) { @@ -60,18 +69,22 @@ auto FileIterator::iterate(bool reverse) -> bool { // End of directory // Set value to nil current_.reset(); + return false; } else { // Update current value offset_++; - current_ = FileEntry{ - .index = offset_, - .isHidden = (info.fattrib & AM_HID) > 0, - .isDirectory = (info.fattrib & AM_DIR) > 0, - .isTrack = false, // TODO - .filepath = original_path_ + (original_path_.size() > 0 ? "/" : "") + - info.fname, - - }; + bool hidden = (info.fattrib & AM_HID) > 0 || info.fname[0] == '.'; + if (!hidden || show_hidden) { + current_ = FileEntry{ + .index = offset_, + .isHidden = hidden, + .isDirectory = (info.fattrib & AM_DIR) > 0, + .isTrack = false, // TODO + .filepath = original_path_ + (original_path_.size() > 0 ? "/" : "") + + info.fname, + .name = info.fname, + }; + } } return true; } diff --git a/src/tangara/lua/file_iterator.hpp b/src/tangara/lua/file_iterator.hpp index b803062c..c4071445 100644 --- a/src/tangara/lua/file_iterator.hpp +++ b/src/tangara/lua/file_iterator.hpp @@ -21,11 +21,12 @@ struct FileEntry { bool isDirectory; bool isTrack; std::string filepath; + std::string name; }; class FileIterator { public: - FileIterator(std::string filepath); + FileIterator(std::string filepath, bool showHidden); ~FileIterator(); auto value() const -> const std::optional&; @@ -35,6 +36,7 @@ class FileIterator { private: FF_DIR dir_; std::string original_path_; + bool show_hidden_; std::optional current_; int offset_; diff --git a/src/tangara/lua/lua_filesystem.cpp b/src/tangara/lua/lua_filesystem.cpp index de51f555..e3a3018d 100644 --- a/src/tangara/lua/lua_filesystem.cpp +++ b/src/tangara/lua/lua_filesystem.cpp @@ -21,29 +21,21 @@ struct LuaFileEntry { bool isHidden; bool isDirectory; bool isTrack; - size_t path_size; - char path[]; + std::string path; + std::string name; }; -static_assert(std::is_trivially_destructible()); -static_assert(std::is_trivially_copy_assignable()); - static auto push_lua_file_entry(lua_State* L, const lua::FileEntry& r) -> void { - // Create and init the userdata. - LuaFileEntry* file_entry = reinterpret_cast( - lua_newuserdata(L, sizeof(LuaFileEntry) + r.filepath.size())); + lua::FileEntry** entry = reinterpret_cast( + lua_newuserdata(L, sizeof(uintptr_t))); + *entry = new lua::FileEntry(r); luaL_setmetatable(L, kFileEntryMetatable); +} - // Init all the fields - *file_entry = { - .isHidden = r.isHidden, - .isDirectory = r.isDirectory, - .isTrack = r.isTrack, - .path_size = r.filepath.size(), - }; - - // Copy the string data across. - std::memcpy(file_entry->path, r.filepath.data(), r.filepath.size()); +auto check_file_entry(lua_State* L, int stack_pos) -> lua::FileEntry* { + lua::FileEntry* entry = *reinterpret_cast( + luaL_checkudata(L, stack_pos, kFileEntryMetatable)); + return entry; } auto check_file_iterator(lua_State* L, int stack_pos) -> lua::FileIterator* { @@ -56,7 +48,7 @@ static auto push_iterator(lua_State* state, const lua::FileIterator& it) -> void { lua::FileIterator** data = reinterpret_cast( lua_newuserdata(state, sizeof(uintptr_t))); - *data = new lua::FileIterator(it); // TODO... + *data = new lua::FileIterator(it); luaL_setmetatable(state, kFileIteratorMetatable); } @@ -108,45 +100,55 @@ static const struct luaL_Reg kFileIteratorFuncs[] = {{"next", fs_iterate}, {NULL, NULL}}; static auto file_entry_path(lua_State* state) -> int { - LuaFileEntry* data = reinterpret_cast( - luaL_checkudata(state, 1, kFileEntryMetatable)); - lua_pushlstring(state, data->path, data->path_size); + lua::FileEntry* entry = check_file_entry(state, 1); + lua_pushlstring(state, entry->filepath.c_str(), entry->filepath.size()); return 1; } static auto file_entry_is_dir(lua_State* state) -> int { - LuaFileEntry* data = reinterpret_cast( - luaL_checkudata(state, 1, kFileEntryMetatable)); - lua_pushboolean(state, data->isDirectory); + lua::FileEntry* entry = check_file_entry(state, 1); + lua_pushboolean(state, entry->isDirectory); return 1; } static auto file_entry_is_hidden(lua_State* state) -> int { - LuaFileEntry* data = reinterpret_cast( - luaL_checkudata(state, 1, kFileEntryMetatable)); - lua_pushboolean(state, data->isHidden); + lua::FileEntry* entry = check_file_entry(state, 1); + lua_pushboolean(state, entry->isHidden); return 1; } static auto file_entry_is_track(lua_State* state) -> int { - LuaFileEntry* data = reinterpret_cast( - luaL_checkudata(state, 1, kFileEntryMetatable)); - lua_pushboolean(state, data->isTrack); + lua::FileEntry* entry = check_file_entry(state, 1); + lua_pushboolean(state, entry->isTrack); + return 1; +} + +static auto file_entry_name(lua_State* state) -> int { + lua::FileEntry* entry = check_file_entry(state, 1); + lua_pushlstring(state, entry->name.c_str(), entry->name.size()); + return 1; +} + +static auto file_entry_gc(lua_State* state) -> int { + lua::FileEntry* entry = check_file_entry(state, 1); + delete entry; return 1; } static const struct luaL_Reg kFileEntryFuncs[] = {{"filepath", file_entry_path}, + {"name", file_entry_name}, {"is_directory", file_entry_is_dir}, {"is_hidden", file_entry_is_hidden}, {"is_track", file_entry_is_track}, - {"__tostring", file_entry_path}, + {"__tostring", file_entry_name}, + {"__gc", file_entry_gc}, {NULL, NULL}}; static auto fs_new_iterator(lua_State* state) -> int { // Takes a filepath as a string and returns a new FileIterator // on that directory std::string filepath = luaL_checkstring(state, -1); - lua::FileIterator iter(filepath); + lua::FileIterator iter(filepath, false); push_iterator(state, iter); return 1; } diff --git a/src/tangara/lua/lua_queue.cpp b/src/tangara/lua/lua_queue.cpp index bc393aa5..9e2002e6 100644 --- a/src/tangara/lua/lua_queue.cpp +++ b/src/tangara/lua/lua_queue.cpp @@ -57,8 +57,22 @@ static auto queue_clear(lua_State* state) -> int { return 0; } +static auto queue_open_playlist(lua_State* state) -> int { + Bridge* instance = Bridge::Get(state); + audio::TrackQueue& queue = instance->services().track_queue(); + size_t len = 0; + const char* str = luaL_checklstring(state, 1, &len); + if (!str) { + return 0; + } + queue.clear(); + queue.openPlaylist(str); + return 0; +} + static const struct luaL_Reg kQueueFuncs[] = {{"add", queue_add}, {"clear", queue_clear}, + {"open_playlist", queue_open_playlist}, {NULL, NULL}}; static auto lua_queue(lua_State* state) -> int {