diff --git a/lib/leveldb/include/leveldb/slice.h b/lib/leveldb/include/leveldb/slice.h index 37cb8217..93438705 100644 --- a/lib/leveldb/include/leveldb/slice.h +++ b/lib/leveldb/include/leveldb/slice.h @@ -35,6 +35,9 @@ class LEVELDB_EXPORT Slice { // Create a slice that refers to the contents of "s" Slice(const std::string& s) : data_(s.data()), size_(s.size()) {} + // Create a slice that refers to the contents of "s" + Slice(std::string_view s) : data_(s.data()), size_(s.size()) {} + // Create a slice that refers to s[0,strlen(s)-1] Slice(const char* s) : data_(s), size_(strlen(s)) {} diff --git a/lua/browser.lua b/lua/browser.lua index 415e5dbb..b9a17af5 100644 --- a/lua/browser.lua +++ b/lua/browser.lua @@ -115,13 +115,15 @@ function browser.create(opts) btn:onevent(lvgl.EVENT.FOCUSED, function() screen.focused_item = this_item if screen.last_item - 5 < this_item then - opts.iterator:next(screen.add_item) + screen.add_item(opts.iterator()) end end) end for _ = 1, 8 do - opts.iterator:next(screen.add_item) + local val = opts.iterator() + if not val then break end + screen.add_item(val) end return screen diff --git a/lua/main_menu.lua b/lua/main_menu.lua index c2d052a3..32386266 100644 --- a/lua/main_menu.lua +++ b/lua/main_menu.lua @@ -34,7 +34,7 @@ return function() end) local indexes = database.indexes() - for id, idx in ipairs(indexes) do + for _, idx in ipairs(indexes) do local btn = menu.list:add_btn(nil, tostring(idx)) btn:onClicked(function() backstack.push(function() diff --git a/src/app_console/app_console.cpp b/src/app_console/app_console.cpp index 7f576a01..b5698792 100644 --- a/src/app_console/app_console.cpp +++ b/src/app_console/app_console.cpp @@ -123,8 +123,7 @@ int CmdPlayFile(int argc, char** argv) { if (is_id) { database::TrackId id = std::atoi(argv[1]); - auto editor = AppConsole::sServices->track_queue().Edit(); - AppConsole::sServices->track_queue().Append(editor, id); + AppConsole::sServices->track_queue().append(id); } else { std::pmr::string path{&memory::kSpiRamResource}; path += '/'; @@ -134,7 +133,8 @@ int CmdPlayFile(int argc, char** argv) { path += argv[i]; } - events::Audio().Dispatch(audio::PlayFile{.filename = path}); + events::Audio().Dispatch( + audio::PlayFile{.filename = {path.data(), path.size()}}); } return 0; @@ -161,7 +161,7 @@ int CmdDbInit(int argc, char** argv) { std::cout << "no database open" << std::endl; return 1; } - db->Update(); + db->updateIndexes(); return 0; } @@ -176,87 +176,6 @@ void RegisterDbInit() { esp_console_cmd_register(&cmd); } -int CmdDbTracks(int argc, char** argv) { - static const std::pmr::string usage = "usage: db_tracks"; - if (argc != 1) { - std::cout << usage << std::endl; - return 1; - } - - auto db = AppConsole::sServices->database().lock(); - if (!db) { - std::cout << "no database open" << std::endl; - return 1; - } - std::unique_ptr> res( - db->GetTracks(20).get()); - while (true) { - for (const auto& s : res->values()) { - std::cout << s->tags()[database::Tag::kTitle].value_or("[BLANK]") - << std::endl; - } - if (res->next_page()) { - auto continuation = res->next_page().value(); - res.reset(db->GetPage(&continuation).get()); - } else { - break; - } - } - - return 0; -} - -void RegisterDbTracks() { - esp_console_cmd_t cmd{.command = "db_tracks", - .help = "lists titles of ALL tracks in the database", - .hint = NULL, - .func = &CmdDbTracks, - .argtable = NULL}; - esp_console_cmd_register(&cmd); -} - -int CmdDbDump(int argc, char** argv) { - static const std::pmr::string usage = "usage: db_dump"; - if (argc != 1) { - std::cout << usage << std::endl; - return 1; - } - - auto db = AppConsole::sServices->database().lock(); - if (!db) { - std::cout << "no database open" << std::endl; - return 1; - } - - std::cout << "=== BEGIN DUMP ===" << std::endl; - - std::unique_ptr> res(db->GetDump(5).get()); - while (true) { - for (const auto& s : res->values()) { - std::cout << *s << std::endl; - } - if (res->next_page()) { - auto continuation = res->next_page().value(); - res.reset(db->GetPage(&continuation).get()); - } else { - break; - } - } - - std::cout << "=== END DUMP ===" << std::endl; - - return 0; -} - -void RegisterDbDump() { - esp_console_cmd_t cmd{.command = "db_dump", - .help = "prints every key/value pair in the db", - .hint = NULL, - .func = &CmdDbDump, - .argtable = NULL}; - esp_console_cmd_register(&cmd); -} - int CmdTasks(int argc, char** argv) { #if (configUSE_TRACE_FACILITY == 0) std::cout << "configUSE_TRACE_FACILITY must be enabled" << std::endl; @@ -270,8 +189,8 @@ int CmdTasks(int argc, char** argv) { return 1; } - // Pad the number of tasks so that uxTaskGetSystemState still returns info if - // new tasks are started during measurement. + // Pad the number of tasks so that uxTaskGetSystemState still returns info + // if new tasks are started during measurement. size_t num_tasks = uxTaskGetNumberOfTasks() + 4; TaskStatus_t* start_status = new TaskStatus_t[num_tasks]; TaskStatus_t* end_status = new TaskStatus_t[num_tasks]; @@ -576,9 +495,11 @@ int CmdHaptics(int argc, char** argv) { " haptic_effect from-effect to-effect\n" " haptic_effect from-effect to-effect library\n" "eg.\n" - " haptic_effect (plays from 1 to 123 with the default library)\n" + " haptic_effect (plays from 1 to 123 with the default " + "library)\n" " haptic_effect 3 (plays from 1 to 123 with library 3\n" - " haptic_effect 1 100 (plays from 1 to 100 with the default library)\n" + " haptic_effect 1 100 (plays from 1 to 100 with the default " + "library)\n" " haptic_effect 1 10 4 (plays from 1 to 10 with library 4)"; auto& haptics = AppConsole::sServices->haptics(); @@ -643,8 +564,6 @@ auto AppConsole::RegisterExtraComponents() -> void { RegisterAudioStatus(); */ RegisterDbInit(); - RegisterDbTracks(); - RegisterDbDump(); RegisterTasks(); RegisterHeaps(); diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index 95bab4c2..0f90334b 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" "playlist" "speexdsp") + "database" "system_fsm" "speexdsp") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index ce610abb..3bd4d396 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -93,6 +93,17 @@ void AudioState::react(const OutputModeChanged& ev) { sOutput->SetMode(IAudioOutput::Modes::kOnPaused); } +auto AudioState::playTrack(database::TrackId id) -> void { + sCurrentTrack = id; + sServices->bg_worker().Dispatch([=]() { + auto db = sServices->database().lock(); + if (!db) { + return; + } + sFileSource->SetPath(db->getTrackPath(id)); + }); +} + namespace states { void Uninitialised::react(const system_fsm::BootComplete& ev) { @@ -143,19 +154,11 @@ void Standby::react(const internal::InputFileOpened& ev) { } void Standby::react(const QueueUpdate& ev) { - auto current_track = sServices->track_queue().Current(); + auto current_track = sServices->track_queue().current(); if (!current_track || (sCurrentTrack && *sCurrentTrack == *current_track)) { return; } - - sCurrentTrack = current_track; - - auto db = sServices->database().lock(); - if (!db) { - ESP_LOGW(kTag, "database not open; ignoring play request"); - return; - } - sFileSource->SetPath(db->GetTrackPath(*current_track)); + playTrack(*current_track); } void Standby::react(const TogglePlayPause& ev) { @@ -187,22 +190,14 @@ void Playback::react(const QueueUpdate& ev) { if (!ev.current_changed) { return; } - auto current_track = sServices->track_queue().Current(); + auto current_track = sServices->track_queue().current(); if (!current_track) { sFileSource->SetPath(); sCurrentTrack.reset(); transit(); return; } - - sCurrentTrack = current_track; - - auto db = sServices->database().lock(); - if (!db) { - return; - } - - sFileSource->SetPath(db->GetTrackPath(*current_track)); + playTrack(*current_track); } void Playback::react(const TogglePlayPause& ev) { @@ -220,9 +215,8 @@ void Playback::react(const internal::InputFileClosed& ev) {} void Playback::react(const internal::InputFileFinished& ev) { ESP_LOGI(kTag, "finished playing file"); - auto editor = sServices->track_queue().Edit(); - sServices->track_queue().Next(editor); - if (!sServices->track_queue().Current()) { + sServices->track_queue().next(); + if (!sServices->track_queue().current()) { transit(); } } diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp index 5594718f..58d5852f 100644 --- a/src/audio/fatfs_audio_input.cpp +++ b/src/audio/fatfs_audio_input.cpp @@ -50,23 +50,19 @@ FatfsAudioInput::FatfsAudioInput(database::ITagParser& tag_parser, bg_worker_(bg_worker), new_stream_mutex_(), new_stream_(), - has_new_stream_(false), - pending_path_() {} + has_new_stream_(false) {} FatfsAudioInput::~FatfsAudioInput() {} -auto FatfsAudioInput::SetPath(std::future> fut) - -> void { - std::lock_guard guard{new_stream_mutex_}; - pending_path_.reset( - new database::FutureFetcher>( - std::move(fut))); - - has_new_stream_ = true; - has_new_stream_.notify_one(); +auto FatfsAudioInput::SetPath(std::optional path) -> void { + if (path) { + SetPath(*path); + } else { + SetPath(); + } } -auto FatfsAudioInput::SetPath(const std::pmr::string& path) -> void { +auto FatfsAudioInput::SetPath(const std::string& path) -> void { std::lock_guard guard{new_stream_mutex_}; if (OpenFile(path)) { has_new_stream_ = true; @@ -96,16 +92,6 @@ auto FatfsAudioInput::NextStream() -> std::shared_ptr { continue; } - // If the path is a future, then wait for it to complete. - if (pending_path_) { - auto res = pending_path_->Result(); - pending_path_.reset(); - - if (res && *res) { - OpenFile(**res); - } - } - if (new_stream_ == nullptr) { continue; } @@ -117,7 +103,7 @@ auto FatfsAudioInput::NextStream() -> std::shared_ptr { } } -auto FatfsAudioInput::OpenFile(const std::pmr::string& path) -> bool { +auto FatfsAudioInput::OpenFile(const std::string& path) -> bool { ESP_LOGI(kTag, "opening file %s", path.c_str()); auto tags = tag_parser_.ReadAndParseTags(path); diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp index 9994a9f6..68efcafb 100644 --- a/src/audio/include/audio_events.hpp +++ b/src/audio/include/audio_events.hpp @@ -42,7 +42,7 @@ struct QueueUpdate : tinyfsm::Event { }; struct PlayFile : tinyfsm::Event { - std::pmr::string filename; + std::string filename; }; struct StepUpVolume : tinyfsm::Event {}; diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp index 256e1430..c9fac08b 100644 --- a/src/audio/include/audio_fsm.hpp +++ b/src/audio/include/audio_fsm.hpp @@ -60,6 +60,8 @@ class AudioState : public tinyfsm::Fsm { virtual void react(const internal::AudioPipelineIdle&) {} protected: + auto playTrack(database::TrackId id) -> void; + static std::shared_ptr sServices; static std::shared_ptr sFileSource; diff --git a/src/audio/include/fatfs_audio_input.hpp b/src/audio/include/fatfs_audio_input.hpp index c7d52ca3..f9635b86 100644 --- a/src/audio/include/fatfs_audio_input.hpp +++ b/src/audio/include/fatfs_audio_input.hpp @@ -38,8 +38,8 @@ class FatfsAudioInput : public IAudioSource { * Immediately cease reading any current source, and begin reading from the * given file path. */ - auto SetPath(std::future>) -> void; - auto SetPath(const std::pmr::string&) -> void; + auto SetPath(std::optional) -> void; + auto SetPath(const std::string&) -> void; auto SetPath() -> void; auto HasNewStream() -> bool override; @@ -49,7 +49,7 @@ class FatfsAudioInput : public IAudioSource { FatfsAudioInput& operator=(const FatfsAudioInput&) = delete; private: - auto OpenFile(const std::pmr::string& path) -> bool; + auto OpenFile(const std::string& path) -> bool; auto ContainerToStreamType(database::Container) -> std::optional; @@ -61,9 +61,6 @@ class FatfsAudioInput : public IAudioSource { std::shared_ptr new_stream_; std::atomic has_new_stream_; - - std::unique_ptr>> - pending_path_; }; } // namespace audio diff --git a/src/audio/include/track_queue.hpp b/src/audio/include/track_queue.hpp index bec887ae..4a1984c9 100644 --- a/src/audio/include/track_queue.hpp +++ b/src/audio/include/track_queue.hpp @@ -9,10 +9,11 @@ #include #include #include +#include #include #include "database.hpp" -#include "source.hpp" +#include "tasks.hpp" #include "track.hpp" namespace audio { @@ -32,77 +33,52 @@ namespace audio { */ class TrackQueue { public: - TrackQueue(); - - class Editor { - public: - ~Editor(); - - // Cannot be copied or moved. - Editor(const Editor&) = delete; - Editor& operator=(const Editor&) = delete; - - private: - friend TrackQueue; - - Editor(TrackQueue&); - - std::lock_guard lock_; - bool has_current_changed_; - }; - - auto Edit() -> Editor; + TrackQueue(tasks::Worker& bg_worker); /* Returns the currently playing track. */ - auto Current() const -> std::optional; + auto current() const -> std::optional; /* Returns, in order, tracks that have been queued to be played next. */ - auto PeekNext(std::size_t limit) const -> std::vector; + auto peekNext(std::size_t limit) const -> std::vector; /* * Returns the tracks in the queue that have already been played, ordered * most recently played first. */ - auto PeekPrevious(std::size_t limit) const -> std::vector; + auto peekPrevious(std::size_t limit) const -> std::vector; - auto GetCurrentPosition() const -> size_t; - auto GetTotalSize() const -> size_t; + auto currentPosition() const -> size_t; + auto totalSize() const -> size_t; using Item = std::variant; - auto Insert(Editor&, Item, size_t) -> void; - auto Append(Editor&, Item i) -> void; + auto insert(Item) -> void; + auto append(Item i) -> void; /* * Advances to the next track in the queue, placing the current track at the * front of the 'played' queue. */ - auto Next(Editor&) -> std::optional; - auto Previous(Editor&) -> std::optional; + auto next() -> void; + auto previous() -> void; - auto SkipTo(Editor&, database::TrackId) -> void; + auto skipTo(database::TrackId) -> void; /* * Removes all tracks from all queues, and stops any currently playing track. */ - auto Clear(Editor&) -> void; - - auto Save(std::weak_ptr) -> void; - auto Load(std::weak_ptr) -> void; + auto clear() -> void; // Cannot be copied or moved. TrackQueue(const TrackQueue&) = delete; TrackQueue& operator=(const TrackQueue&) = delete; private: - // FIXME: Make this a shared_mutex so that multithread reads don't block. - mutable std::recursive_mutex mutex_; + mutable std::shared_mutex mutex_; - std::optional current_; + tasks::Worker& bg_worker_; - // Note: stored in reverse order, i.e. most recent played it at the *back* of - // this vector. - std::pmr::vector played_; - std::pmr::vector enqueued_; + size_t pos_; + std::pmr::vector tracks_; }; } // namespace audio diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp index eb761590..c1187107 100644 --- a/src/audio/track_queue.cpp +++ b/src/audio/track_queue.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -19,7 +20,7 @@ #include "database.hpp" #include "event_queue.hpp" #include "memory_resource.hpp" -#include "source.hpp" +#include "tasks.hpp" #include "track.hpp" #include "ui_fsm.hpp" @@ -27,385 +28,137 @@ namespace audio { [[maybe_unused]] static constexpr char kTag[] = "tracks"; -static const std::string kSerialiseKey = "queue"; -static const std::string kCurrentKey = "cur"; -static const std::string kPlayedKey = "prev"; -static const std::string kEnqueuedKey = "next"; - -TrackQueue::Editor::Editor(TrackQueue& queue) - : lock_(queue.mutex_), has_current_changed_(false) {} - -TrackQueue::Editor::~Editor() { - QueueUpdate ev{.current_changed = has_current_changed_}; - events::Audio().Dispatch(ev); +auto notifyChanged(bool current_changed) -> void { + QueueUpdate ev{.current_changed = current_changed}; events::Ui().Dispatch(ev); + events::Audio().Dispatch(ev); } -TrackQueue::TrackQueue() +TrackQueue::TrackQueue(tasks::Worker& bg_worker) : mutex_(), - current_(), - played_(&memory::kSpiRamResource), - enqueued_(&memory::kSpiRamResource) {} + bg_worker_(bg_worker), + pos_(0), + tracks_(&memory::kSpiRamResource) {} -auto TrackQueue::Edit() -> Editor { - return Editor(*this); -} - -auto TrackQueue::Current() const -> std::optional { - const std::lock_guard lock(mutex_); - return current_; -} - -auto TrackQueue::PeekNext(std::size_t limit) const - -> std::vector { - const std::lock_guard lock(mutex_); - std::vector ret; - - for (auto it = enqueued_.begin(); it != enqueued_.end() && limit > 0; it++) { - std::visit( - [&](auto&& arg) { - using T = std::decay_t; - if constexpr (std::is_same_v) { - ret.push_back(arg); - limit--; - } else if constexpr (std::is_same_v) { - auto copy = arg; - while (limit > 0) { - auto next = copy.Next(); - if (!next) { - break; - } - ret.push_back(*next); - limit--; - } - } - }, - *it); +auto TrackQueue::current() const -> std::optional { + const std::shared_lock lock(mutex_); + if (pos_ >= tracks_.size()) { + return {}; } - - return ret; + return tracks_[pos_]; } -auto TrackQueue::PeekPrevious(std::size_t limit) const +auto TrackQueue::peekNext(std::size_t limit) const -> std::vector { - const std::lock_guard lock(mutex_); - std::vector ret; - ret.reserve(limit); - - for (auto it = played_.rbegin(); it != played_.rend(); it++, limit--) { - ret.push_back(*it); - } - - return ret; -} - -auto TrackQueue::GetCurrentPosition() const -> size_t { - const std::lock_guard lock(mutex_); - size_t played = played_.size(); - if (current_) { - played += 1; + const std::shared_lock lock(mutex_); + std::vector out; + for (size_t i = pos_ + 1; i < pos_ + limit + 1 && i < tracks_.size(); i++) { + out.push_back(i); } - return played; + return out; } -auto TrackQueue::GetTotalSize() const -> size_t { - const std::lock_guard lock(mutex_); - size_t total = GetCurrentPosition(); - - for (const auto& item : enqueued_) { - std::visit( - [&](auto&& arg) { - using T = std::decay_t; - if constexpr (std::is_same_v) { - total++; - } else if constexpr (std::is_same_v) { - total += arg.Size(); - } - }, - item); +auto TrackQueue::peekPrevious(std::size_t limit) const + -> std::vector { + const std::shared_lock lock(mutex_); + std::vector out; + for (size_t i = pos_ - 1; i < pos_ - limit - 1 && i >= tracks_.size(); i--) { + out.push_back(i); } - - return total; + return out; } -auto TrackQueue::Insert(Editor& ed, Item i, size_t index) -> void { - if (index == 0) { - enqueued_.insert(enqueued_.begin(), i); - } - - // We can't insert halfway through an iterator, so we need to ensure that the - // first `index` items in the queue are reified into track ids. - size_t current_index = 0; - while (current_index < index && current_index < enqueued_.size()) { - std::visit( - [&](auto&& arg) { - using T = std::decay_t; - if constexpr (std::is_same_v) { - // This item is already a track id; nothing to do. - current_index++; - } else if constexpr (std::is_same_v) { - // This item is an iterator. Push it back one, replacing its old - // index with the next value from it. - auto next = arg.Next(); - auto iterator_index = enqueued_.begin() + current_index; - if (!next) { - // Out of values. Remove the iterator completely. - enqueued_.erase(iterator_index); - // Don't increment current_index, since the next item in the - // queue will have been moved down. - } else { - enqueued_.insert(iterator_index, *next); - current_index++; - } - } - }, - enqueued_[current_index]); - } - - // Double check the previous loop didn't run out of items. - if (index > enqueued_.size()) { - ESP_LOGE(kTag, "insert index was out of bounds"); - return; - } - - // Finally, we can now do the actual insertion. - enqueued_.insert(enqueued_.begin() + index, i); +auto TrackQueue::currentPosition() const -> size_t { + const std::shared_lock lock(mutex_); + return pos_; } -auto TrackQueue::Append(Editor& ed, Item i) -> void { - enqueued_.push_back(i); - if (!current_) { - Next(ed); - } +auto TrackQueue::totalSize() const -> size_t { + const std::shared_lock lock(mutex_); + return tracks_.size(); } -auto TrackQueue::Next(Editor& ed) -> std::optional { - if (current_) { - ed.has_current_changed_ = true; - played_.push_back(*current_); - } - current_.reset(); - - while (!current_ && !enqueued_.empty()) { - ed.has_current_changed_ = true; - std::visit( - [&](auto&& arg) { - using T = std::decay_t; - if constexpr (std::is_same_v) { - current_ = arg; - enqueued_.erase(enqueued_.begin()); - } else if constexpr (std::is_same_v) { - auto next = arg.Next(); - if (!next) { - enqueued_.erase(enqueued_.begin()); - } else { - current_ = *next; - } - } - }, - enqueued_.front()); +auto TrackQueue::insert(Item i) -> void { + bool current_changed = pos_ == tracks_.size(); + if (std::holds_alternative(i)) { + const std::unique_lock lock(mutex_); + tracks_.push_back(std::get(i)); + notifyChanged(current_changed); + } else if (std::holds_alternative(i)) { + bg_worker_.Dispatch([=, this]() { + database::TrackIterator it = std::get(i); + size_t working_pos = pos_; + while (true) { + auto next = *it; + if (!next) { + break; + } + const std::unique_lock lock(mutex_); + tracks_.insert(tracks_.begin() + working_pos, *next); + working_pos++; + it++; + } + notifyChanged(current_changed); + }); } - - return current_; } -auto TrackQueue::Previous(Editor& ed) -> std::optional { - if (played_.empty()) { - return current_; - } - ed.has_current_changed_ = true; - if (current_) { - enqueued_.insert(enqueued_.begin(), *current_); +auto TrackQueue::append(Item i) -> void { + bool current_changed = pos_ == tracks_.size(); + if (std::holds_alternative(i)) { + const std::unique_lock lock(mutex_); + tracks_.push_back(std::get(i)); + notifyChanged(current_changed); + } else if (std::holds_alternative(i)) { + bg_worker_.Dispatch([=, this]() { + database::TrackIterator it = std::get(i); + while (true) { + auto next = *it; + if (!next) { + break; + } + const std::unique_lock lock(mutex_); + tracks_.push_back(*next); + it++; + } + notifyChanged(current_changed); + }); } - current_ = played_.back(); - played_.pop_back(); - return current_; } -auto TrackQueue::SkipTo(Editor& ed, database::TrackId id) -> void { - while ((!current_ || *current_ != id) && !enqueued_.empty()) { - Next(ed); - } -} +auto TrackQueue::next() -> void { + const std::unique_lock lock(mutex_); + pos_ = std::min(pos_ + 1, tracks_.size()); -auto TrackQueue::Clear(Editor& ed) -> void { - ed.has_current_changed_ = current_.has_value(); - current_.reset(); - played_.clear(); - enqueued_.clear(); + notifyChanged(true); } -auto TrackQueue::Save(std::weak_ptr db) -> void { - cppbor::Map root{}; - - if (current_) { - root.add(cppbor::Bstr{kCurrentKey}, cppbor::Uint{*current_}); - } - - cppbor::Array played{}; - for (const auto& id : played_) { - played.add(cppbor::Uint{id}); - } - root.add(cppbor::Bstr{kPlayedKey}, std::move(played)); - - cppbor::Array enqueued{}; - for (const auto& item : enqueued_) { - std::visit( - [&](auto&& arg) { - using T = std::decay_t; - if constexpr (std::is_same_v) { - enqueued.add(cppbor::Uint{arg}); - } else if constexpr (std::is_same_v) { - enqueued.add(arg.cbor()); - } - }, - item); +auto TrackQueue::previous() -> void { + const std::unique_lock lock(mutex_); + if (pos_ > 0) { + pos_--; } - root.add(cppbor::Bstr{kEnqueuedKey}, std::move(enqueued)); - auto db_lock = db.lock(); - if (!db_lock) { - return; - } - db_lock->Put(kSerialiseKey, root.toString()); + notifyChanged(true); } -class Parser : public cppbor::ParseClient { - public: - Parser(std::weak_ptr db, - std::optional& current, - std::pmr::vector& played, - std::pmr::vector& enqueued) - : state_(State::kInit), - db_(db), - current_(current), - played_(played), - enqueued_(enqueued) {} - - virtual ParseClient* item(std::unique_ptr& item, - const uint8_t* hdrBegin, - const uint8_t* valueBegin, - const uint8_t* end) override { - switch (state_) { - case State::kInit: - if (item->type() == cppbor::MAP) { - state_ = State::kRoot; - } - break; - case State::kRoot: - if (item->type() != cppbor::TSTR) { - break; - } - if (item->asTstr()->value() == kCurrentKey) { - state_ = State::kCurrent; - } else if (item->asTstr()->value() == kPlayedKey) { - state_ = State::kPlayed; - } else if (item->asTstr()->value() == kEnqueuedKey) { - state_ = State::kEnqueued; - } - break; - case State::kCurrent: - if (item->type() == cppbor::UINT) { - current_ = item->asUint()->value(); - } - state_ = State::kRoot; - break; - case State::kPlayed: - if (item->type() == cppbor::UINT) { - played_.push_back(item->asUint()->value()); - } - break; - case State::kEnqueued: - if (item->type() == cppbor::UINT) { - played_.push_back(item->asUint()->value()); - } else if (item->type() == cppbor::ARRAY) { - queue_depth_ = 1; - state_ = State::kEnqueuedIterator; - } - break; - case State::kEnqueuedIterator: - if (item->type() == cppbor::MAP || item->type() == cppbor::ARRAY) { - queue_depth_++; - } - break; - case State::kFinished: - break; - } - - return this; - } - - ParseClient* itemEnd(std::unique_ptr& item, - const uint8_t* hdrBegin, - const uint8_t* valueBegin, - const uint8_t* end) override { - std::optional parsed_it; - switch (state_) { - case State::kInit: - case State::kRoot: - case State::kCurrent: - state_ = State::kFinished; - break; - case State::kEnqueued: - case State::kPlayed: - state_ = State::kRoot; - break; - case State::kEnqueuedIterator: - if (item->type() == cppbor::MAP || item->type() == cppbor::ARRAY) { - queue_depth_++; - } - if (queue_depth_ == 0) { - parsed_it = database::TrackIterator::Parse(db_, *item->asArray()); - if (parsed_it) { - enqueued_.push_back(std::move(*parsed_it)); - } - } - state_ = State::kEnqueued; - break; - case State::kFinished: - break; +auto TrackQueue::skipTo(database::TrackId id) -> void { + const std::unique_lock lock(mutex_); + for (size_t i = pos_; i < tracks_.size(); i++) { + if (tracks_[i] == id) { + pos_ = i; } - return this; } - void error(const uint8_t* position, - const std::string& errorMessage) override { - ESP_LOGE(kTag, "restoring saved queue failed: %s", errorMessage.c_str()); - } - - private: - enum class State { - kInit, - kRoot, - kCurrent, - kPlayed, - kEnqueued, - kEnqueuedIterator, - kFinished, - } state_; - - std::weak_ptr db_; - - int queue_depth_; - - std::optional& current_; - std::pmr::vector& played_; - std::pmr::vector& enqueued_; -}; + notifyChanged(true); +} -auto TrackQueue::Load(std::weak_ptr db) -> void { - auto db_lock = db.lock(); - if (!db_lock) { - return; - } - auto raw = db_lock->Get(kSerialiseKey); - if (!raw) { - return; - } +auto TrackQueue::clear() -> void { + const std::unique_lock lock(mutex_); + pos_ = 0; + tracks_.clear(); - Parser p{db, current_, played_, enqueued_}; - const uint8_t* data = reinterpret_cast(raw->data()); - cppbor::parse(data, data + raw->size(), &p); + notifyChanged(true); } } // namespace audio diff --git a/src/database/database.cpp b/src/database/database.cpp index e646154e..1adfec87 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include "collation.hpp" #include "cppbor.h" @@ -200,11 +201,11 @@ Database::~Database() { sIsDbOpen.store(false); } -auto Database::Put(const std::string& key, const std::string& val) -> void { +auto Database::put(const std::string& key, const std::string& val) -> void { db_->Put(leveldb::WriteOptions{}, kKeyCustom + key, val); } -auto Database::Get(const std::string& key) -> std::optional { +auto Database::get(const std::string& key) -> std::optional { std::string val; auto res = db_->Get(leveldb::ReadOptions{}, kKeyCustom + key, &val); if (!res.ok()) { @@ -213,320 +214,212 @@ auto Database::Get(const std::string& key) -> std::optional { return val; } -auto Database::Update() -> std::future { +auto Database::getTrackPath(TrackId id) -> std::optional { + auto track_data = dbGetTrackData(id); + if (!track_data) { + return {}; + } + return std::string{track_data->filepath.data(), track_data->filepath.size()}; +} + +auto Database::getTrack(TrackId id) -> std::shared_ptr { + std::shared_ptr data = dbGetTrackData(id); + if (!data || data->is_tombstoned) { + return {}; + } + std::shared_ptr tags = tag_parser_.ReadAndParseTags( + {data->filepath.data(), data->filepath.size()}); + if (!tags) { + return {}; + } + return std::make_shared(data, tags); +} + +auto Database::getIndexes() -> std::vector { + // TODO(jacqueline): This probably needs to be async? When we have runtime + // configurable indexes, they will need to come from somewhere. + return { + kAllTracks, + kAllAlbums, + kAlbumsByArtist, + kTracksByGenre, + }; +} + +auto Database::updateIndexes() -> void { events::Ui().Dispatch(event::UpdateStarted{}); - return worker_task_->Dispatch([&]() -> void { - leveldb::ReadOptions read_options; - read_options.fill_cache = false; - - std::pair newest_track{0, 0}; - - // Stage 1: verify all existing tracks are still valid. - ESP_LOGI(kTag, "verifying existing tracks"); - { - uint64_t num_processed = 0; - std::unique_ptr it{db_->NewIterator(read_options)}; - std::string prefix = EncodeDataPrefix(); - for (it->Seek(prefix); it->Valid() && it->key().starts_with(prefix); - it->Next()) { - num_processed++; - events::Ui().Dispatch(event::UpdateProgress{ - .stage = event::UpdateProgress::Stage::kVerifyingExistingTracks, - .val = num_processed, - }); - - std::shared_ptr track = ParseDataValue(it->value()); - if (!track) { - // The value was malformed. Drop this record. - ESP_LOGW(kTag, "dropping malformed metadata"); - db_->Delete(leveldb::WriteOptions(), it->key()); - continue; - } - - if (track->is_tombstoned) { - ESP_LOGW(kTag, "skipping tombstoned %lx", track->id); - continue; - } - - FRESULT res; - FILINFO info; - { - auto lock = drivers::acquire_spi(); - res = f_stat(track->filepath.c_str(), &info); - } - - std::pair modified_at{0, 0}; - if (res == FR_OK) { - modified_at = {info.fdate, info.ftime}; - } - if (modified_at == track->modified_at) { - newest_track = std::max(modified_at, newest_track); - continue; - } else { - track->modified_at = modified_at; - } - - std::shared_ptr tags = - tag_parser_.ReadAndParseTags(track->filepath); - if (!tags || tags->encoding() == Container::kUnsupported) { - // We couldn't read the tags for this track. Either they were - // malformed, or perhaps the file is missing. Either way, tombstone - // this record. - ESP_LOGW(kTag, "entombing missing #%lx", track->id); - dbRemoveIndexes(track); - track->is_tombstoned = true; - dbPutTrackData(*track); - continue; - } - - // At this point, we know that the track still exists in its original - // location. All that's left to do is update any metadata about it. - - uint64_t new_hash = tags->Hash(); - if (new_hash != track->tags_hash) { - // This track's tags have changed. Since the filepath is exactly the - // same, we assume this is a legitimate correction. Update the - // database. - ESP_LOGI(kTag, "updating hash (%llx -> %llx)", track->tags_hash, - new_hash); - dbRemoveIndexes(track); - - track->tags_hash = new_hash; - dbIngestTagHashes(*tags, track->individual_tag_hashes); - dbPutTrackData(*track); - dbPutHash(new_hash, track->id); - } - } - } + leveldb::ReadOptions read_options; + read_options.fill_cache = false; - ESP_LOGI(kTag, "newest unmodified was at %u,%u", newest_track.first, - newest_track.second); + std::pair newest_track{0, 0}; - // Stage 2: search for newly added files. - ESP_LOGI(kTag, "scanning for new tracks"); + // Stage 1: verify all existing tracks are still valid. + ESP_LOGI(kTag, "verifying existing tracks"); + { uint64_t num_processed = 0; - file_gatherer_.FindFiles("", [&](const std::pmr::string& path, - const FILINFO& info) { + std::unique_ptr it{db_->NewIterator(read_options)}; + std::string prefix = EncodeDataPrefix(); + for (it->Seek(prefix); it->Valid() && it->key().starts_with(prefix); + it->Next()) { num_processed++; events::Ui().Dispatch(event::UpdateProgress{ - .stage = event::UpdateProgress::Stage::kScanningForNewTracks, + .stage = event::UpdateProgress::Stage::kVerifyingExistingTracks, .val = num_processed, }); - std::pair modified{info.fdate, info.ftime}; - if (modified < newest_track) { - return; + std::shared_ptr track = ParseDataValue(it->value()); + if (!track) { + // The value was malformed. Drop this record. + ESP_LOGW(kTag, "dropping malformed metadata"); + db_->Delete(leveldb::WriteOptions(), it->key()); + continue; } - std::shared_ptr tags = tag_parser_.ReadAndParseTags(path); - if (!tags || tags->encoding() == Container::kUnsupported) { - // No parseable tags; skip this fiile. - return; + if (track->is_tombstoned) { + ESP_LOGW(kTag, "skipping tombstoned %lx", track->id); + continue; } - // Check for any existing record with the same hash. - uint64_t hash = tags->Hash(); - std::string key = EncodeHashKey(hash); - std::optional existing_hash; - std::string raw_entry; - if (db_->Get(leveldb::ReadOptions(), key, &raw_entry).ok()) { - existing_hash = ParseHashValue(raw_entry); + FRESULT res; + FILINFO info; + { + auto lock = drivers::acquire_spi(); + res = f_stat(track->filepath.c_str(), &info); } - if (!existing_hash) { - // We've never met this track before! Or we have, but the entry is - // malformed. Either way, record this as a new track. - TrackId id = dbMintNewTrackId(); - ESP_LOGI(kTag, "recording new 0x%lx", id); - - auto data = std::make_shared(); - data->id = id; - data->filepath = path; - data->tags_hash = hash; - data->modified_at = modified; - dbIngestTagHashes(*tags, data->individual_tag_hashes); - - dbPutTrackData(*data); - dbPutHash(hash, id); - auto t = std::make_shared(data, tags); - dbCreateIndexesForTrack(*t); - return; + std::pair modified_at{0, 0}; + if (res == FR_OK) { + modified_at = {info.fdate, info.ftime}; + } + if (modified_at == track->modified_at) { + newest_track = std::max(modified_at, newest_track); + continue; + } else { + track->modified_at = modified_at; } - std::shared_ptr existing_data = dbGetTrackData(*existing_hash); - if (!existing_data) { - // We found a hash that matches, but there's no data record? Weird. - auto new_data = std::make_shared(); - new_data->id = dbMintNewTrackId(); - new_data->filepath = path; - new_data->tags_hash = hash; - new_data->modified_at = modified; - dbIngestTagHashes(*tags, new_data->individual_tag_hashes); - dbPutTrackData(*new_data); - auto t = std::make_shared(new_data, tags); - dbCreateIndexesForTrack(*t); - return; + std::shared_ptr tags = tag_parser_.ReadAndParseTags( + {track->filepath.data(), track->filepath.size()}); + if (!tags || tags->encoding() == Container::kUnsupported) { + // We couldn't read the tags for this track. Either they were + // malformed, or perhaps the file is missing. Either way, tombstone + // this record. + ESP_LOGW(kTag, "entombing missing #%lx", track->id); + dbRemoveIndexes(track); + track->is_tombstoned = true; + dbPutTrackData(*track); + continue; } - if (existing_data->is_tombstoned) { - ESP_LOGI(kTag, "exhuming track %lu", existing_data->id); - existing_data->is_tombstoned = false; - existing_data->modified_at = modified; - dbPutTrackData(*existing_data); - auto t = std::make_shared(existing_data, tags); - dbCreateIndexesForTrack(*t); - } else if (existing_data->filepath != path) { - ESP_LOGW(kTag, "tag hash collision for %s and %s", - existing_data->filepath.c_str(), path.c_str()); - ESP_LOGI(kTag, "hash components: %s, %s, %s", - tags->at(Tag::kTitle).value_or("no title").c_str(), - tags->at(Tag::kArtist).value_or("no artist").c_str(), - tags->at(Tag::kAlbum).value_or("no album").c_str()); + // At this point, we know that the track still exists in its original + // location. All that's left to do is update any metadata about it. + + uint64_t new_hash = tags->Hash(); + if (new_hash != track->tags_hash) { + // This track's tags have changed. Since the filepath is exactly the + // same, we assume this is a legitimate correction. Update the + // database. + ESP_LOGI(kTag, "updating hash (%llx -> %llx)", track->tags_hash, + new_hash); + dbRemoveIndexes(track); + + track->tags_hash = new_hash; + dbIngestTagHashes(*tags, track->individual_tag_hashes); + dbPutTrackData(*track); + dbPutHash(new_hash, track->id); } - }); - events::Ui().Dispatch(event::UpdateFinished{}); - }); -} + } + } -auto Database::GetTrackPath(TrackId id) - -> std::future> { - return worker_task_->Dispatch>( - [=, this]() -> std::optional { - auto track_data = dbGetTrackData(id); - if (track_data) { - return track_data->filepath; - } - return {}; - }); -} + ESP_LOGI(kTag, "newest unmodified was at %u,%u", newest_track.first, + newest_track.second); -auto Database::GetTrack(TrackId id) -> std::future> { - return worker_task_->Dispatch>( - [=, this]() -> std::shared_ptr { - std::shared_ptr data = dbGetTrackData(id); - if (!data || data->is_tombstoned) { - return {}; - } - std::shared_ptr tags = - tag_parser_.ReadAndParseTags(data->filepath); - if (!tags) { - return {}; - } - return std::make_shared(data, tags); - }); -} + // Stage 2: search for newly added files. + ESP_LOGI(kTag, "scanning for new tracks"); + uint64_t num_processed = 0; + file_gatherer_.FindFiles("", [&](const std::string& path, + const FILINFO& info) { + num_processed++; + events::Ui().Dispatch(event::UpdateProgress{ + .stage = event::UpdateProgress::Stage::kScanningForNewTracks, + .val = num_processed, + }); -auto Database::GetBulkTracks(std::vector ids) - -> std::future>> { - return worker_task_->Dispatch>>( - [=, this]() -> std::vector> { - std::map> id_to_track{}; - - // Sort the list of ids so that we can retrieve them all in a single - // iteration through the database, without re-seeking. - std::vector sorted_ids = ids; - std::sort(sorted_ids.begin(), sorted_ids.end()); - - std::unique_ptr it{ - db_->NewIterator(leveldb::ReadOptions{})}; - for (const TrackId& id : sorted_ids) { - std::string key = EncodeDataKey(id); - it->Seek(key); - if (!it->Valid() || it->key() != key) { - // This id wasn't found at all. Skip it. - continue; - } - std::shared_ptr track = - ParseRecord(it->key(), it->value()); - if (track) { - id_to_track.insert({id, track}); - } - } - - // We've fetched all of the ids in the request, so now just put them - // back into the order they were asked for in. - std::vector> results; - for (const TrackId& id : ids) { - if (id_to_track.contains(id)) { - results.push_back(id_to_track.at(id)); - } else { - // This lookup failed. - results.push_back({}); - } - } - return results; - }); -} + std::pair modified{info.fdate, info.ftime}; + if (modified < newest_track) { + return; + } -auto Database::GetIndexes() -> std::vector { - // TODO(jacqueline): This probably needs to be async? When we have runtime - // configurable indexes, they will need to come from somewhere. - return { - kAllTracks, - kAllAlbums, - kAlbumsByArtist, - kTracksByGenre, - }; -} + std::shared_ptr tags = tag_parser_.ReadAndParseTags(path); + if (!tags || tags->encoding() == Container::kUnsupported) { + // No parseable tags; skip this fiile. + return; + } -auto Database::GetTracksByIndex(IndexId index, std::size_t page_size) - -> std::future*> { - return worker_task_->Dispatch*>( - [=, this]() -> Result* { - IndexKey::Header header{ - .id = index, - .depth = 0, - .components_hash = 0, - }; - std::string prefix = EncodeIndexPrefix(header); - Continuation c{.prefix = {prefix.data(), prefix.size()}, - .start_key = {prefix.data(), prefix.size()}, - .forward = true, - .was_prev_forward = true, - .page_size = page_size}; - return dbGetPage(c); - }); -} + // Check for any existing record with the same hash. + uint64_t hash = tags->Hash(); + std::string key = EncodeHashKey(hash); + std::optional existing_hash; + std::string raw_entry; + if (db_->Get(leveldb::ReadOptions(), key, &raw_entry).ok()) { + existing_hash = ParseHashValue(raw_entry); + } -auto Database::GetTracks(std::size_t page_size) -> std::future*> { - return worker_task_->Dispatch*>([=, this]() -> Result* { - std::string prefix = EncodeDataPrefix(); - Continuation c{.prefix = {prefix.data(), prefix.size()}, - .start_key = {prefix.data(), prefix.size()}, - .forward = true, - .was_prev_forward = true, - .page_size = page_size}; - return dbGetPage(c); - }); -} + if (!existing_hash) { + // We've never met this track before! Or we have, but the entry is + // malformed. Either way, record this as a new track. + TrackId id = dbMintNewTrackId(); + ESP_LOGI(kTag, "recording new 0x%lx", id); + + auto data = std::make_shared(); + data->id = id; + data->filepath = path; + data->tags_hash = hash; + data->modified_at = modified; + dbIngestTagHashes(*tags, data->individual_tag_hashes); + + dbPutTrackData(*data); + dbPutHash(hash, id); + auto t = std::make_shared(data, tags); + dbCreateIndexesForTrack(*t); + return; + } -auto Database::GetDump(std::size_t page_size) - -> std::future*> { - return worker_task_->Dispatch*>( - [=, this]() -> Result* { - Continuation c{.prefix = "", - .start_key = "", - .forward = true, - .was_prev_forward = true, - .page_size = page_size}; - return dbGetPage(c); - }); -} + std::shared_ptr existing_data = dbGetTrackData(*existing_hash); + if (!existing_data) { + // We found a hash that matches, but there's no data record? Weird. + auto new_data = std::make_shared(); + new_data->id = dbMintNewTrackId(); + new_data->filepath = path; + new_data->tags_hash = hash; + new_data->modified_at = modified; + dbIngestTagHashes(*tags, new_data->individual_tag_hashes); + dbPutTrackData(*new_data); + auto t = std::make_shared(new_data, tags); + dbCreateIndexesForTrack(*t); + return; + } -template -auto Database::GetPage(Continuation* c) -> std::future*> { - Continuation copy = *c; - return worker_task_->Dispatch*>( - [=, this]() -> Result* { return dbGetPage(copy); }); + if (existing_data->is_tombstoned) { + ESP_LOGI(kTag, "exhuming track %lu", existing_data->id); + existing_data->is_tombstoned = false; + existing_data->modified_at = modified; + dbPutTrackData(*existing_data); + auto t = std::make_shared(existing_data, tags); + dbCreateIndexesForTrack(*t); + } else if (existing_data->filepath != + std::pmr::string{path.data(), path.size()}) { + ESP_LOGW(kTag, "tag hash collision for %s and %s", + existing_data->filepath.c_str(), path.c_str()); + ESP_LOGI(kTag, "hash components: %s, %s, %s", + tags->at(Tag::kTitle).value_or("no title").c_str(), + tags->at(Tag::kArtist).value_or("no artist").c_str(), + tags->at(Tag::kAlbum).value_or("no album").c_str()); + } + }); + events::Ui().Dispatch(event::UpdateFinished{}); } -template auto Database::GetPage(Continuation* c) - -> std::future*>; -template auto Database::GetPage(Continuation* c) - -> std::future*>; -template auto Database::GetPage(Continuation* c) - -> std::future*>; - auto Database::dbMintNewTrackId() -> TrackId { TrackId next_id = 1; std::string val; @@ -592,7 +485,7 @@ auto Database::dbGetHash(const uint64_t& hash) -> std::optional { } auto Database::dbCreateIndexesForTrack(const Track& track) -> void { - for (const IndexInfo& index : GetIndexes()) { + for (const IndexInfo& index : getIndexes()) { leveldb::WriteBatch writes; auto entries = Index(collator_, index, track); for (const auto& it : entries) { @@ -609,7 +502,7 @@ auto Database::dbRemoveIndexes(std::shared_ptr data) -> void { return; } Track track{data, tags}; - for (const IndexInfo& index : GetIndexes()) { + for (const IndexInfo& index : getIndexes()) { auto entries = Index(collator_, index, track); for (auto it = entries.rbegin(); it != entries.rend(); it++) { auto key = EncodeIndexKey(it->first); @@ -666,512 +559,209 @@ auto Database::dbRecoverTagsFromHashes( return out; } -template -auto Database::dbGetPage(const Continuation& c) -> Result* { - // Work out our starting point. Sometimes this will already done. - std::unique_ptr it{ - db_->NewIterator(leveldb::ReadOptions{})}; - it->Seek({c.start_key.data(), c.start_key.size()}); - - // Fix off-by-one if we just changed direction. - if (c.forward != c.was_prev_forward) { - if (c.forward) { - it->Next(); - } else { +auto seekToOffset(leveldb::Iterator* it, int offset) { + while (it->Valid() && offset != 0) { + if (offset < 0) { it->Prev(); - } - } - - // Grab results. - std::optional first_key; - std::vector> records; - while (records.size() < c.page_size && it->Valid()) { - if (!it->key().starts_with({c.prefix.data(), c.prefix.size()})) { - break; - } - if (!first_key) { - first_key = it->key().ToString(); - } - std::shared_ptr parsed = ParseRecord(it->key(), it->value()); - if (parsed) { - records.push_back(parsed); - } - if (c.forward) { - it->Next(); + offset++; } else { - it->Prev(); + it->Next(); + offset--; } } +} - if (!it->Valid() || - !it->key().starts_with({c.prefix.data(), c.prefix.size()})) { - it.reset(); - } - - // Put results into canonical order if we were iterating backwards. - if (!c.forward) { - std::reverse(records.begin(), records.end()); - } +auto Database::getRecord(const SearchKey& c) + -> std::optional> { + std::unique_ptr it{ + db_->NewIterator(leveldb::ReadOptions{})}; - // Work out the new continuations. - std::optional next_page; - if (c.forward) { - if (it != nullptr) { - // We were going forward, and now we want the next page. - std::pmr::string key{it->key().data(), it->key().size(), - &memory::kSpiRamResource}; - next_page = Continuation{ - .prefix = c.prefix, - .start_key = key, - .forward = true, - .was_prev_forward = true, - .page_size = c.page_size, - }; - } - // No iterator means we ran out of results in this direction. - } else { - // We were going backwards, and now we want the next page. This is a - // reversal, to set the start key to the first record we saw and mark that - // it's off by one. - next_page = Continuation{ - .prefix = c.prefix, - .start_key = *first_key, - .forward = true, - .was_prev_forward = false, - .page_size = c.page_size, - }; + it->Seek(c.startKey()); + seekToOffset(it.get(), c.offset); + if (!it->Valid() || !it->key().starts_with(std::string_view{c.prefix})) { + return {}; } - std::optional prev_page; - if (c.forward) { - // We were going forwards, and now we want the previous page. Set the - // search key to the first result we saw, and mark that it's off by one. - prev_page = Continuation{ - .prefix = c.prefix, - .start_key = *first_key, - .forward = false, - .was_prev_forward = true, - .page_size = c.page_size, - }; - } else { - if (it != nullptr) { - // We were going backwards, and we still want to go backwards. - std::pmr::string key{it->key().data(), it->key().size(), - &memory::kSpiRamResource}; - prev_page = Continuation{ - .prefix = c.prefix, - .start_key = key, - .forward = false, - .was_prev_forward = false, - .page_size = c.page_size, - }; - } - // No iterator means we ran out of results in this direction. + std::optional key = ParseIndexKey(it->key()); + if (!key) { + ESP_LOGW(kTag, "parsing index key failed"); + return {}; } - return new Result(std::move(records), next_page, prev_page); + return std::make_pair(std::pmr::string{it->key().data(), it->key().size(), + &memory::kSpiRamResource}, + Record{*key, it->value()}); } -auto Database::dbCount(const Continuation& c) -> size_t { +auto Database::countRecords(const SearchKey& c) -> size_t { std::unique_ptr it{ db_->NewIterator(leveldb::ReadOptions{})}; - size_t count = 0; - for (it->Seek({c.start_key.data(), c.start_key.size()}); - it->Valid() && it->key().starts_with({c.prefix.data(), c.prefix.size()}); - it->Next()) { - count++; - } - return count; -} -template auto Database::dbGetPage(const Continuation& c) - -> Result*; -template auto Database::dbGetPage(const Continuation& c) - -> Result*; - -template <> -auto Database::ParseRecord(const leveldb::Slice& key, - const leveldb::Slice& val) - -> std::shared_ptr { - std::optional data = ParseIndexKey(key); - if (!data) { + it->Seek(c.startKey()); + seekToOffset(it.get(), c.offset); + if (!it->Valid() || !it->key().starts_with(std::string_view{c.prefix})) { return {}; } - std::optional title; - if (!val.empty()) { - title = val.ToString(); + size_t count = 0; + while (it->Valid() && it->key().starts_with(std::string_view{c.prefix})) { + it->Next(); + count++; } - return std::make_shared(*data, title, data->track); + return count; } -template <> -auto Database::ParseRecord(const leveldb::Slice& key, - const leveldb::Slice& val) - -> std::shared_ptr { - std::shared_ptr data = ParseDataValue(val); - if (!data || data->is_tombstoned) { - return {}; - } - std::shared_ptr tags = - tag_parser_.ReadAndParseTags(data->filepath); - if (!tags) { - return {}; +auto SearchKey::startKey() const -> std::string_view { + if (key) { + return *key; } - return std::make_shared(data, tags); + return prefix; } -template <> -auto Database::ParseRecord(const leveldb::Slice& key, - const leveldb::Slice& val) - -> std::shared_ptr { - std::ostringstream stream; - stream << "key: "; - if (key.size() < 3 || key.data()[1] != '\0') { - stream << key.ToString().c_str(); +Record::Record(const IndexKey& key, const leveldb::Slice& t) + : text_(t.data(), t.size(), &memory::kSpiRamResource) { + if (key.track) { + contents_ = *key.track; } else { - for (size_t i = 0; i < key.size(); i++) { - if (i == 0) { - stream << key.data()[i]; - } else if (i == 1) { - stream << " / 0x"; - } else { - stream << std::hex << std::setfill('0') << std::setw(2) - << static_cast(key.data()[i]); - } - } - } - if (!val.empty()) { - stream << "\tval: 0x"; - for (int i = 0; i < val.size(); i++) { - stream << std::hex << std::setfill('0') << std::setw(2) - << static_cast(val.data()[i]); - } + contents_ = ExpandHeader(key.header, key.item); } - std::pmr::string res{stream.str(), &memory::kSpiRamResource}; - return std::make_shared(res); } -IndexRecord::IndexRecord(const IndexKey& key, - std::optional title, - std::optional track) - : key_(key), override_text_(title), track_(track) {} - -auto IndexRecord::text() const -> std::optional { - if (override_text_) { - return override_text_; - } - return key_.item; +auto Record::text() const -> std::string_view { + return text_; } -auto IndexRecord::track() const -> std::optional { - return track_; +auto Record::contents() const + -> const std::variant& { + return contents_; } -auto IndexRecord::Expand(std::size_t page_size) const - -> std::optional { - if (track_) { - return {}; - } - std::string new_prefix = EncodeIndexPrefix(ExpandHeader()); - return Continuation{ - .prefix = {new_prefix.data(), new_prefix.size()}, - .start_key = {new_prefix.data(), new_prefix.size()}, - .forward = true, - .was_prev_forward = true, - .page_size = page_size, - }; -} +Iterator::Iterator(std::shared_ptr db, IndexId idx) + : Iterator(db, + IndexKey::Header{ + .id = idx, + .depth = 0, + .components_hash = 0, + }) {} -auto IndexRecord::ExpandHeader() const -> IndexKey::Header { - return ::database::ExpandHeader(key_.header, key_.item); +Iterator::Iterator(std::shared_ptr db, const IndexKey::Header& header) + : db_(db), key_{}, current_() { + std::string prefix = EncodeIndexPrefix(header); + key_ = { + .prefix = {prefix.data(), prefix.size(), &memory::kSpiRamResource}, + .key = {}, + .offset = 0, + }; + iterate(key_); } -Iterator::Iterator(std::weak_ptr db, const IndexInfo& idx) - : db_(db), pos_mutex_(), current_pos_(), prev_pos_() { - std::string prefix = EncodeIndexPrefix( - IndexKey::Header{.id = idx.id, .depth = 0, .components_hash = 0}); - current_pos_ = Continuation{.prefix = {prefix.data(), prefix.size()}, - .start_key = {prefix.data(), prefix.size()}, - .forward = true, - .was_prev_forward = true, - .page_size = 1}; +auto Iterator::value() const -> const std::optional& { + return current_; } -auto Iterator::Parse(std::weak_ptr db, const cppbor::Array& encoded) - -> std::optional { - // Ensure the input looks reasonable. - if (encoded.size() != 3) { - return {}; - } - - if (encoded[0]->type() != cppbor::TSTR) { - return {}; - } - const std::string& prefix = encoded[0]->asTstr()->value(); - - std::optional current_pos{}; - if (encoded[1]->type() == cppbor::TSTR) { - const std::string& key = encoded[1]->asTstr()->value(); - current_pos = Continuation{ - .prefix = {prefix.data(), prefix.size()}, - .start_key = {key.data(), key.size()}, - .forward = true, - .was_prev_forward = true, - .page_size = 1, - }; - } - - std::optional prev_pos{}; - if (encoded[2]->type() == cppbor::TSTR) { - const std::string& key = encoded[2]->asTstr()->value(); - current_pos = Continuation{ - .prefix = {prefix.data(), prefix.size()}, - .start_key = {key.data(), key.size()}, - .forward = false, - .was_prev_forward = false, - .page_size = 1, - }; - } - - return Iterator{db, std::move(current_pos), std::move(prev_pos)}; +auto Iterator::next() -> void { + SearchKey new_key = key_; + new_key.offset = 1; + iterate(new_key); } -Iterator::Iterator(std::weak_ptr db, const Continuation& c) - : db_(db), pos_mutex_(), current_pos_(c), prev_pos_() {} - -Iterator::Iterator(const Iterator& other) - : db_(other.db_), - pos_mutex_(), - current_pos_(other.current_pos_), - prev_pos_(other.prev_pos_) {} - -Iterator::Iterator(std::weak_ptr db, - std::optional&& cur, - std::optional&& prev) - : db_(db), current_pos_(cur), prev_pos_(prev) {} - -Iterator& Iterator::operator=(const Iterator& other) { - current_pos_ = other.current_pos_; - prev_pos_ = other.prev_pos_; - return *this; +auto Iterator::prev() -> void { + SearchKey new_key = key_; + new_key.offset = -1; + iterate(new_key); } -auto Iterator::Next(Callback cb) -> void { +auto Iterator::iterate(const SearchKey& key) -> void { auto db = db_.lock(); if (!db) { - InvokeNull(cb); + ESP_LOGW(kTag, "iterate with dead db"); return; } - db->worker_task_->Dispatch([=]() { - std::lock_guard lock{pos_mutex_}; - if (!current_pos_) { - InvokeNull(cb); - return; - } - std::unique_ptr> res{ - db->dbGetPage(*current_pos_)}; - prev_pos_ = current_pos_; - current_pos_ = res->next_page(); - if (!res || res->values().empty() || !res->values()[0]) { - ESP_LOGI(kTag, "dropping empty result"); - InvokeNull(cb); - return; - } - std::invoke(cb, *res->values()[0]); - }); -} - -auto Iterator::NextSync() -> std::optional { - auto db = db_.lock(); - if (!db) { - return {}; - } - std::lock_guard lock{pos_mutex_}; - if (!current_pos_) { - return {}; - } - std::unique_ptr> res{ - db->dbGetPage(*current_pos_)}; - prev_pos_ = current_pos_; - current_pos_ = res->next_page(); - if (!res || res->values().empty() || !res->values()[0]) { - ESP_LOGI(kTag, "dropping empty result"); - return {}; - } - return *res->values()[0]; -} - -auto Iterator::PeekSync() -> std::optional { - auto db = db_.lock(); - if (!db) { - return {}; - } - auto pos = current_pos_; - if (!pos) { - return {}; - } - std::unique_ptr> res{db->dbGetPage(*pos)}; - if (!res || res->values().empty() || !res->values()[0]) { - return {}; - } - return *res->values()[0]; -} - -auto Iterator::Prev(Callback cb) -> void { - auto db = db_.lock(); - if (!db) { - InvokeNull(cb); - return; + auto res = db->getRecord(key); + if (res) { + key_ = { + .prefix = key_.prefix, + .key = res->first, + .offset = 0, + }; + current_ = res->second; + } else { + key_ = key; + current_.reset(); } - db->worker_task_->Dispatch([=]() { - std::lock_guard lock{pos_mutex_}; - if (!prev_pos_) { - InvokeNull(cb); - return; - } - std::unique_ptr> res{ - db->dbGetPage(*current_pos_)}; - current_pos_ = prev_pos_; - prev_pos_ = res->prev_page(); - std::invoke(cb, *res->values()[0]); - }); } -auto Iterator::Size() const -> size_t { +auto Iterator::count() const -> size_t { auto db = db_.lock(); if (!db) { - return {}; - } - std::optional pos = current_pos_; - if (!pos) { + ESP_LOGW(kTag, "count with dead db"); return 0; } - return db->dbCount(*pos); + return db->countRecords(key_); } -auto Iterator::InvokeNull(Callback cb) -> void { - std::invoke(cb, std::optional{}); +TrackIterator::TrackIterator(const Iterator& it) : db_(it.db_), levels_() { + levels_.push_back(it); + next(false); } -auto Iterator::cbor() const -> cppbor::Array&& { - cppbor::Array res; - - std::pmr::string prefix; - if (current_pos_) { - prefix = current_pos_->prefix; - } else if (prev_pos_) { - prefix = prev_pos_->prefix; - } else { - ESP_LOGW(kTag, "iterator has no prefix"); - return std::move(res); - } - - if (current_pos_) { - res.add(cppbor::Tstr(current_pos_->start_key)); - } else { - res.add(cppbor::Null()); - } - - if (prev_pos_) { - res.add(cppbor::Tstr(prev_pos_->start_key)); - } else { - res.add(cppbor::Null()); - } - - return std::move(res); +auto TrackIterator::next() -> void { + next(true); } -auto TrackIterator::Parse(std::weak_ptr db, - const cppbor::Array& encoded) - -> std::optional { - TrackIterator ret{db}; +auto TrackIterator::next(bool advance) -> void { + while (!levels_.empty()) { + if (advance) { + levels_.back().next(); + } - for (const auto& item : encoded) { - if (item->type() == cppbor::ARRAY) { - auto it = Iterator::Parse(db, *item->asArray()); - if (it) { - ret.levels_.push_back(std::move(*it)); - } else { - return {}; + auto& cur = levels_.back().value(); + if (!cur) { + // The current top iterator is out of tracks. Pop it, and move the parent + // to the next item. + levels_.pop_back(); + advance = true; + } else if (std::holds_alternative(cur->contents())) { + // This record is a branch. Push a new iterator. + auto key = std::get(cur->contents()); + auto db = db_.lock(); + if (!db) { + return; } + levels_.emplace_back(db, key); + // Don't skip the first value of the new level. + advance = false; + } else if (std::holds_alternative(cur->contents())) { + // New record is a leaf. + break; } } - - return ret; } -TrackIterator::TrackIterator(const Iterator& it) : db_(it.db_), levels_() { - if (it.current_pos_) { - levels_.push_back(it); +auto TrackIterator::value() const -> std::optional { + if (levels_.empty()) { + return {}; } - NextLeaf(); -} - -TrackIterator::TrackIterator(const TrackIterator& other) - : db_(other.db_), levels_(other.levels_) {} - -TrackIterator::TrackIterator(std::weak_ptr db) : db_(db), levels_() {} - -TrackIterator& TrackIterator::operator=(TrackIterator&& other) { - levels_ = std::move(other.levels_); - return *this; -} - -auto TrackIterator::Next() -> std::optional { - std::optional next{}; - while (!next && !levels_.empty()) { - auto next_record = levels_.back().NextSync(); - if (!next_record) { - levels_.pop_back(); - NextLeaf(); - continue; - } - // May still be nullopt_t; hence the loop. - next = next_record->track(); + auto cur = levels_.back().value(); + if (!cur) { + return {}; } - return next; + if (std::holds_alternative(cur->contents())) { + return std::get(cur->contents()); + } + return {}; } -auto TrackIterator::Size() const -> size_t { +auto TrackIterator::count() const -> size_t { size_t size = 0; TrackIterator copy{*this}; while (!copy.levels_.empty()) { - size += copy.levels_.back().Size(); + size += copy.levels_.back().count(); copy.levels_.pop_back(); - copy.NextLeaf(); + copy.next(); } return size; } -auto TrackIterator::NextLeaf() -> void { - while (!levels_.empty()) { - ESP_LOGI(kTag, "check next candidate"); - Iterator& candidate = levels_.back(); - auto next = candidate.PeekSync(); - if (!next) { - ESP_LOGI(kTag, "candidate is empty"); - levels_.pop_back(); - continue; - } - if (!next->track()) { - ESP_LOGI(kTag, "candidate is a branch"); - candidate.NextSync(); - levels_.push_back(Iterator{db_, next->Expand(1).value()}); - continue; - } - ESP_LOGI(kTag, "candidate is a leaf"); - break; - } -} - -auto TrackIterator::cbor() const -> cppbor::Array&& { - cppbor::Array res; - for (const auto& i : levels_) { - res.add(i.cbor()); - } - return std::move(res); -} - } // namespace database diff --git a/src/database/file_gatherer.cpp b/src/database/file_gatherer.cpp index 0809ee0d..f07a1b4d 100644 --- a/src/database/file_gatherer.cpp +++ b/src/database/file_gatherer.cpp @@ -21,13 +21,13 @@ namespace database { static_assert(sizeof(TCHAR) == sizeof(char), "TCHAR must be CHAR"); auto FileGathererImpl::FindFiles( - const std::pmr::string& root, - std::function cb) -> void { - std::pmr::deque to_explore(&memory::kSpiRamResource); + const std::string& root, + std::function cb) -> void { + std::deque to_explore; to_explore.push_back(root); while (!to_explore.empty()) { - std::pmr::string next_path_str = to_explore.front(); + std::string next_path_str = to_explore.front(); const TCHAR* next_path = static_cast(next_path_str.c_str()); FF_DIR dir; @@ -54,7 +54,7 @@ auto FileGathererImpl::FindFiles( // System or hidden file. Ignore it and move on. continue; } else { - std::pmr::string full_path{&memory::kSpiRamResource}; + std::string full_path; full_path += next_path_str; full_path += "/"; full_path += info.fname; diff --git a/src/database/include/database.hpp b/src/database/include/database.hpp index 327db3cb..c75dbf96 100644 --- a/src/database/include/database.hpp +++ b/src/database/include/database.hpp @@ -35,61 +35,18 @@ namespace database { -struct Continuation { - std::pmr::string prefix; - std::pmr::string start_key; - bool forward; - bool was_prev_forward; - size_t page_size; -}; +struct SearchKey; +class Record; +class Iterator; /* - * Wrapper for a set of results from the database. Owns the list of results, as - * well as a continuation token that can be used to continue fetching more - * results if they were paginated. + * Handle to an open database. This can be used to store large amounts of + * persistent data on the SD card, in a manner that can be retrieved later very + * quickly. + * + * A database includes a number of 'indexes'. Each index is a sorted, + * hierarchical view of all the playable tracks on the device. */ -template -class Result { - public: - auto values() const -> const std::vector>& { - return values_; - } - - auto next_page() -> std::optional& { return next_page_; } - auto prev_page() -> std::optional& { return prev_page_; } - - Result(const std::vector>&& values, - std::optional next, - std::optional prev) - : values_(values), next_page_(next), prev_page_(prev) {} - - Result(const Result&) = delete; - Result& operator=(const Result&) = delete; - - private: - std::vector> values_; - std::optional next_page_; - std::optional prev_page_; -}; - -class IndexRecord { - public: - explicit IndexRecord(const IndexKey&, - std::optional, - std::optional); - - auto text() const -> std::optional; - auto track() const -> std::optional; - - auto Expand(std::size_t) const -> std::optional; - auto ExpandHeader() const -> IndexKey::Header; - - private: - IndexKey key_; - std::optional override_text_; - std::optional track_; -}; - class Database { public: enum DatabaseError { @@ -106,31 +63,19 @@ class Database { ~Database(); - auto Put(const std::string& key, const std::string& val) -> void; - auto Get(const std::string& key) -> std::optional; - - auto Update() -> std::future; + /* Adds an arbitrary record to the database. */ + auto put(const std::string& key, const std::string& val) -> void; - auto GetTrackPath(TrackId id) -> std::future>; + /* Retrives a value previously stored with `put`. */ + auto get(const std::string& key) -> std::optional; - auto GetTrack(TrackId id) -> std::future>; + auto getTrackPath(TrackId id) -> std::optional; + auto getTrack(TrackId id) -> std::shared_ptr; - /* - * Fetches data for multiple tracks more efficiently than multiple calls to - * GetTrack. - */ - auto GetBulkTracks(std::vector id) - -> std::future>>; - - auto GetIndexes() -> std::vector; - auto GetTracksByIndex(IndexId index, std::size_t page_size) - -> std::future*>; - auto GetTracks(std::size_t page_size) -> std::future*>; - auto GetDump(std::size_t page_size) -> std::future*>; - - template - auto GetPage(Continuation* c) -> std::future*>; + auto getIndexes() -> std::vector; + auto updateIndexes() -> void; + // Cannot be copied or moved. Database(const Database&) = delete; Database& operator=(const Database&) = delete; @@ -157,106 +102,134 @@ class Database { std::shared_ptr worker); auto dbMintNewTrackId() -> TrackId; - auto dbEntomb(TrackId track, uint64_t hash) -> void; + auto dbEntomb(TrackId track, uint64_t hash) -> void; auto dbPutTrackData(const TrackData& s) -> void; auto dbGetTrackData(TrackId id) -> std::shared_ptr; auto dbPutHash(const uint64_t& hash, TrackId i) -> void; auto dbGetHash(const uint64_t& hash) -> std::optional; + auto dbCreateIndexesForTrack(const Track& track) -> void; auto dbRemoveIndexes(std::shared_ptr) -> void; + auto dbIngestTagHashes(const TrackTags&, std::pmr::unordered_map&) -> void; auto dbRecoverTagsFromHashes(const std::pmr::unordered_map&) -> std::shared_ptr; - template - auto dbGetPage(const Continuation& c) -> Result*; + auto getRecord(const SearchKey& c) + -> std::optional>; + auto countRecords(const SearchKey& c) -> size_t; +}; - auto dbCount(const Continuation& c) -> size_t; +/* + * Container for the data needed to iterate through database records. This is a + * lower-level type that the higher-level iterators are built from; most users + * outside this namespace shouldn't need to work with continuations. + */ +struct SearchKey { + std::pmr::string prefix; + /* If not given, then iteration starts from `prefix`. */ + std::optional key; + int offset; - template - auto ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val) - -> std::shared_ptr; + auto startKey() const -> std::string_view; }; -template <> -auto Database::ParseRecord(const leveldb::Slice& key, - const leveldb::Slice& val) - -> std::shared_ptr; -template <> -auto Database::ParseRecord(const leveldb::Slice& key, - const leveldb::Slice& val) - -> std::shared_ptr; -template <> -auto Database::ParseRecord(const leveldb::Slice& key, - const leveldb::Slice& val) - -> std::shared_ptr; - /* - * Utility for accessing a large set of database records, one record at a time. + * A record belonging to one of the database's indexes. This may either be a + * leaf record, containing a track id, or a branch record, containing a new + * Header to retrieve results at the next level of the index. */ -class Iterator { +class Record { public: - static auto Parse(std::weak_ptr, const cppbor::Array&) - -> std::optional; + Record(const IndexKey&, const leveldb::Slice&); - Iterator(std::weak_ptr, const IndexInfo&); - Iterator(std::weak_ptr, const Continuation&); - Iterator(const Iterator&); + Record(const Record&) = default; + Record& operator=(const Record& other) = default; - Iterator& operator=(const Iterator& other); + auto text() const -> std::string_view; + auto contents() const -> const std::variant&; - auto database() const { return db_; } + private: + std::pmr::string text_; + std::variant contents_; +}; - using Callback = std::function)>; +/* + * Utility for accessing a large set of database records, one record at a time. + */ +class Iterator { + public: + Iterator(std::shared_ptr, IndexId); + Iterator(std::shared_ptr, const IndexKey::Header&); - auto Next(Callback) -> void; - auto NextSync() -> std::optional; + Iterator(const Iterator&) = default; + Iterator& operator=(const Iterator& other) = default; - auto Prev(Callback) -> void; + auto value() const -> const std::optional&; + std::optional operator*() const { return value(); } - auto PeekSync() -> std::optional; + auto next() -> void; + std::optional operator++() { + next(); + return value(); + } + std::optional operator++(int) { + auto val = value(); + next(); + return val; + } - auto Size() const -> size_t; + auto prev() -> void; + std::optional operator--() { + prev(); + return value(); + } + std::optional operator--(int) { + auto val = value(); + prev(); + return val; + } - auto cbor() const -> cppbor::Array&&; + auto count() const -> size_t; private: - Iterator(std::weak_ptr, - std::optional&&, - std::optional&&); + auto iterate(const SearchKey& key) -> void; friend class TrackIterator; - auto InvokeNull(Callback) -> void; - std::weak_ptr db_; - - std::mutex pos_mutex_; - std::optional current_pos_; - std::optional prev_pos_; + SearchKey key_; + std::optional current_; }; class TrackIterator { public: - static auto Parse(std::weak_ptr, const cppbor::Array&) - -> std::optional; - TrackIterator(const Iterator&); - TrackIterator(const TrackIterator&); - TrackIterator& operator=(TrackIterator&& other); + TrackIterator(const TrackIterator&) = default; + TrackIterator& operator=(TrackIterator&& other) = default; + + auto value() const -> std::optional; + std::optional operator*() const { return value(); } - auto Next() -> std::optional; - auto Size() const -> size_t; + auto next() -> void; + std::optional operator++() { + next(); + return value(); + } + std::optional operator++(int) { + auto val = value(); + next(); + return val; + } - auto cbor() const -> cppbor::Array&&; + auto count() const -> size_t; private: TrackIterator(std::weak_ptr); - - auto NextLeaf() -> void; + auto next(bool advance) -> void; std::weak_ptr db_; std::vector levels_; diff --git a/src/database/include/file_gatherer.hpp b/src/database/include/file_gatherer.hpp index 378727f7..66127bb7 100644 --- a/src/database/include/file_gatherer.hpp +++ b/src/database/include/file_gatherer.hpp @@ -20,16 +20,16 @@ class IFileGatherer { virtual ~IFileGatherer(){}; virtual auto FindFiles( - const std::pmr::string& root, - std::function cb) + const std::string& root, + std::function cb) -> void = 0; }; class FileGathererImpl : public IFileGatherer { public: virtual auto FindFiles( - const std::pmr::string& root, - std::function cb) + const std::string& root, + std::function cb) -> void override; }; diff --git a/src/database/include/tag_parser.hpp b/src/database/include/tag_parser.hpp index 04817c59..977c9afc 100644 --- a/src/database/include/tag_parser.hpp +++ b/src/database/include/tag_parser.hpp @@ -16,24 +16,24 @@ namespace database { class ITagParser { public: virtual ~ITagParser() {} - virtual auto ReadAndParseTags(const std::pmr::string& path) + virtual auto ReadAndParseTags(const std::string& path) -> std::shared_ptr = 0; }; class GenericTagParser : public ITagParser { public: - auto ReadAndParseTags(const std::pmr::string& path) + auto ReadAndParseTags(const std::string& path) -> std::shared_ptr override; }; class TagParserImpl : public ITagParser { public: TagParserImpl(); - auto ReadAndParseTags(const std::pmr::string& path) + auto ReadAndParseTags(const std::string& path) -> std::shared_ptr override; private: - std::map> extension_to_parser_; + std::map> extension_to_parser_; GenericTagParser generic_parser_; /* @@ -43,14 +43,14 @@ class TagParserImpl : public ITagParser { std::mutex cache_mutex_; util::LruCache<16, std::pmr::string, std::shared_ptr> cache_; - // We could also consider keeping caches of artist name -> std::pmr::string - // and similar. This hasn't been done yet, as this isn't a common workload in + // We could also consider keeping caches of artist name -> std::string and + // similar. This hasn't been done yet, as this isn't a common workload in // any of our UI. }; class OpusTagParser : public ITagParser { public: - auto ReadAndParseTags(const std::pmr::string& path) + auto ReadAndParseTags(const std::string& path) -> std::shared_ptr override; }; diff --git a/src/database/include/track.hpp b/src/database/include/track.hpp index 8a24024f..0497c94d 100644 --- a/src/database/include/track.hpp +++ b/src/database/include/track.hpp @@ -123,7 +123,7 @@ struct TrackData { public: TrackData() : id(0), - filepath(&memory::kSpiRamResource), + filepath(), tags_hash(0), individual_tag_hashes(&memory::kSpiRamResource), is_tombstoned(false), diff --git a/src/database/tag_parser.cpp b/src/database/tag_parser.cpp index eb5f3a43..885c71dd 100644 --- a/src/database/tag_parser.cpp +++ b/src/database/tag_parser.cpp @@ -32,7 +32,7 @@ const static std::array, 5> kVorbisIdToTag = {{ static auto convert_track_number(int number) -> std::pmr::string { std::ostringstream oss; oss << std::setw(4) << std::setfill('0') << number; - return std::pmr::string(oss.str()); + return std::pmr::string(oss.str(), &memory::kSpiRamResource); } static auto convert_track_number(const std::pmr::string& raw) @@ -131,11 +131,12 @@ TagParserImpl::TagParserImpl() { extension_to_parser_["opus"] = std::make_unique(); } -auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path) +auto TagParserImpl::ReadAndParseTags(const std::string& path) -> std::shared_ptr { { std::lock_guard lock{cache_mutex_}; - std::optional> cached = cache_.Get(path); + std::optional> cached = + cache_.Get({path.data(), path.size()}); if (cached) { return *cached; } @@ -143,8 +144,8 @@ auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path) ITagParser* parser = &generic_parser_; auto dot_pos = path.find_last_of("."); - if (dot_pos != std::pmr::string::npos && path.size() - dot_pos > 1) { - std::pmr::string extension = path.substr(dot_pos + 1); + if (dot_pos != std::string::npos && path.size() - dot_pos > 1) { + std::string extension = path.substr(dot_pos + 1); std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char c) { return std::tolower(c); }); if (extension_to_parser_.contains(extension)) { @@ -162,8 +163,9 @@ auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path) // start. if (!tags->at(Tag::kAlbumTrack)) { auto slash_pos = path.find_last_of("/"); - if (slash_pos != std::pmr::string::npos && path.size() - slash_pos > 1) { - tags->set(Tag::kAlbumTrack, path.substr(slash_pos + 1)); + if (slash_pos != std::string::npos && path.size() - slash_pos > 1) { + std::string trunc = path.substr(slash_pos + 1); + tags->set(Tag::kAlbumTrack, {trunc.data(), trunc.size()}); } } @@ -174,13 +176,13 @@ auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path) { std::lock_guard lock{cache_mutex_}; - cache_.Put(path, tags); + cache_.Put({path.data(), path.size(), &memory::kSpiRamResource}, tags); } return tags; } -auto GenericTagParser::ReadAndParseTags(const std::pmr::string& path) +auto GenericTagParser::ReadAndParseTags(const std::string& path) -> std::shared_ptr { libtags::Aux aux; auto out = std::make_shared(); @@ -254,10 +256,10 @@ auto GenericTagParser::ReadAndParseTags(const std::pmr::string& path) return out; } -auto OpusTagParser::ReadAndParseTags(const std::pmr::string& path) +auto OpusTagParser::ReadAndParseTags(const std::string& path) -> std::shared_ptr { auto lock = drivers::acquire_spi(); - std::pmr::string vfs_path = "/sdcard" + path; + std::string vfs_path = "/sdcard" + path; int err; OggOpusFile* f = op_test_file(vfs_path.c_str(), &err); if (f == NULL) { diff --git a/src/lua/bridge.cpp b/src/lua/bridge.cpp index 070dee98..8d7b4fd0 100644 --- a/src/lua/bridge.cpp +++ b/src/lua/bridge.cpp @@ -37,23 +37,8 @@ static auto open_settings_fn(lua_State* state) -> int { return 0; } -static auto open_now_playing_fn(lua_State* state) -> int { - events::Ui().Dispatch(ui::internal::ShowNowPlaying{}); - return 0; -} - -static auto open_browse_fn(lua_State* state) -> int { - int index = luaL_checkinteger(state, 1); - events::Ui().Dispatch(ui::internal::IndexSelected{ - .id = static_cast(index), - }); - return 0; -} - static const struct luaL_Reg kLegacyUiFuncs[] = { {"open_settings", open_settings_fn}, - {"open_now_playing", open_now_playing_fn}, - {"open_browse", open_browse_fn}, {NULL, NULL}}; static auto lua_legacy_ui(lua_State* state) -> int { diff --git a/src/lua/lua_database.cpp b/src/lua/lua_database.cpp index d41f4794..82b22343 100644 --- a/src/lua/lua_database.cpp +++ b/src/lua/lua_database.cpp @@ -8,7 +8,10 @@ #include #include +#include +#include +#include "bridge.hpp" #include "lua.hpp" #include "esp_log.h" @@ -34,6 +37,17 @@ static constexpr char kDbIndexMetatable[] = "db_index"; static constexpr char kDbRecordMetatable[] = "db_record"; static constexpr char kDbIteratorMetatable[] = "db_iterator"; +struct LuaIndexInfo { + database::IndexId id; + size_t name_size; + char name_data[]; + + auto name() -> std::string_view { return {name_data, name_size}; } +}; + +static_assert(std::is_trivially_destructible()); +static_assert(std::is_trivially_copy_assignable()); + static auto indexes(lua_State* state) -> int { Bridge* instance = Bridge::Get(state); @@ -44,11 +58,15 @@ static auto indexes(lua_State* state) -> int { return 1; } - for (const auto& i : db->GetIndexes()) { - database::IndexInfo** data = reinterpret_cast( - lua_newuserdata(state, sizeof(uintptr_t))); + for (const auto& i : db->getIndexes()) { + LuaIndexInfo* data = reinterpret_cast( + lua_newuserdata(state, sizeof(LuaIndexInfo) + i.name.size())); luaL_setmetatable(state, kDbIndexMetatable); - *data = new database::IndexInfo{i}; + *data = LuaIndexInfo{ + .id = i.id, + .name_size = i.name.size(), + }; + std::memcpy(data->name_data, i.name.data(), i.name.size()); lua_rawseti(state, -2, i.id); } @@ -65,33 +83,28 @@ static const struct luaL_Reg kDatabaseFuncs[] = {{"indexes", indexes}, * trivially copyable. */ struct LuaRecord { - database::TrackId id_or_zero; - database::IndexKey::Header header_at_next_depth; + std::variant contents; size_t text_size; char text[]; }; -static_assert(std::is_trivially_copyable_v == true); - -static auto push_lua_record(lua_State* L, const database::IndexRecord& r) - -> void { - // Bake out the text into something concrete. - auto text = r.text().value_or(""); +static_assert(std::is_trivially_destructible()); +static_assert(std::is_trivially_copy_assignable()); +static auto push_lua_record(lua_State* L, const database::Record& r) -> void { // Create and init the userdata. LuaRecord* record = reinterpret_cast( - lua_newuserdata(L, sizeof(LuaRecord) + text.size())); + lua_newuserdata(L, sizeof(LuaRecord) + r.text().size())); luaL_setmetatable(L, kDbRecordMetatable); // Init all the fields *record = { - .id_or_zero = r.track().value_or(0), - .header_at_next_depth = r.ExpandHeader(), - .text_size = text.size(), + .contents = r.contents(), + .text_size = r.text().size(), }; // Copy the string data across. - std::memcpy(record->text, text.data(), text.size()); + std::memcpy(record->text, r.text().data(), r.text().size()); } auto db_check_iterator(lua_State* L, int stack_pos) -> database::Iterator* { @@ -100,49 +113,30 @@ auto db_check_iterator(lua_State* L, int stack_pos) -> database::Iterator* { return it; } -static auto push_iterator(lua_State* state, - std::variant val) -> void { - Bridge* instance = Bridge::Get(state); +static auto push_iterator(lua_State* state, const database::Iterator& it) + -> void { database::Iterator** data = reinterpret_cast( lua_newuserdata(state, sizeof(uintptr_t))); - std::visit( - [&](auto&& arg) { - using T = std::decay_t; - if constexpr (std::is_same_v) { - *data = new database::Iterator(*arg); - } else { - *data = new database::Iterator(instance->services().database(), arg); - } - }, - val); + *data = new database::Iterator(it); luaL_setmetatable(state, kDbIteratorMetatable); } static auto db_iterate(lua_State* state) -> int { database::Iterator* it = db_check_iterator(state, 1); - luaL_checktype(state, 2, LUA_TFUNCTION); - int callback_ref = luaL_ref(state, LUA_REGISTRYINDEX); - - it->Next([=](std::optional res) { - events::Ui().RunOnTask([=]() { - lua_rawgeti(state, LUA_REGISTRYINDEX, callback_ref); - if (res) { - push_lua_record(state, *res); - } else { - lua_pushnil(state); - } - CallProtected(state, 1, 0); - luaL_unref(state, LUA_REGISTRYINDEX, callback_ref); - }); - }); - return 0; + std::optional res = (*it)++; + + if (res) { + push_lua_record(state, *res); + } else { + lua_pushnil(state); + } + + return 1; } static auto db_iterator_clone(lua_State* state) -> int { database::Iterator* it = db_check_iterator(state, 1); - push_iterator(state, it); + push_iterator(state, *it); return 1; } @@ -154,6 +148,7 @@ static auto db_iterator_gc(lua_State* state) -> int { static const struct luaL_Reg kDbIteratorFuncs[] = {{"next", db_iterate}, {"clone", db_iterator_clone}, + {"__call", db_iterate}, {"__gc", db_iterator_gc}, {NULL, NULL}}; @@ -168,18 +163,22 @@ static auto record_contents(lua_State* state) -> int { LuaRecord* data = reinterpret_cast( luaL_checkudata(state, 1, kDbRecordMetatable)); - if (data->id_or_zero) { - lua_pushinteger(state, data->id_or_zero); - } else { - std::string p = database::EncodeIndexPrefix(data->header_at_next_depth); - push_iterator(state, database::Continuation{ - .prefix = {p.data(), p.size()}, - .start_key = {p.data(), p.size()}, - .forward = true, - .was_prev_forward = true, - .page_size = 1, - }); - } + std::visit( + [&](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + lua_pushinteger(state, arg); + } else if constexpr (std::is_same_v) { + Bridge* bridge = Bridge::Get(state); + auto db = bridge->services().database().lock(); + if (!db) { + lua_pushnil(state); + } else { + push_iterator(state, database::Iterator{db, arg}); + } + } + }, + data->contents); return 1; } @@ -190,38 +189,33 @@ static const struct luaL_Reg kDbRecordFuncs[] = {{"title", record_text}, {NULL, NULL}}; static auto index_name(lua_State* state) -> int { - database::IndexInfo** data = reinterpret_cast( + LuaIndexInfo* data = reinterpret_cast( luaL_checkudata(state, 1, kDbIndexMetatable)); if (data == NULL) { return 0; } - lua_pushstring(state, (*data)->name.c_str()); + lua_pushlstring(state, data->name_data, data->name_size); return 1; } static auto index_iter(lua_State* state) -> int { - database::IndexInfo** data = reinterpret_cast( + LuaIndexInfo* data = reinterpret_cast( luaL_checkudata(state, 1, kDbIndexMetatable)); if (data == NULL) { return 0; } - push_iterator(state, **data); - return 1; -} - -static auto index_gc(lua_State* state) -> int { - database::IndexInfo** data = reinterpret_cast( - luaL_checkudata(state, 1, kDbIndexMetatable)); - if (data != NULL) { - delete *data; + Bridge* bridge = Bridge::Get(state); + auto db = bridge->services().database().lock(); + if (!db) { + lua_pushnil(state); } - return 0; + push_iterator(state, database::Iterator{db, data->id}); + return 1; } static const struct luaL_Reg kDbIndexFuncs[] = {{"name", index_name}, {"iter", index_iter}, {"__tostring", index_name}, - {"__gc", index_gc}, {NULL, NULL}}; static auto lua_database(lua_State* state) -> int { diff --git a/src/lua/lua_queue.cpp b/src/lua/lua_queue.cpp index fadcb51c..69d3b03d 100644 --- a/src/lua/lua_queue.cpp +++ b/src/lua/lua_queue.cpp @@ -21,7 +21,6 @@ #include "index.hpp" #include "property.hpp" #include "service_locator.hpp" -#include "source.hpp" #include "track.hpp" #include "track_queue.hpp" #include "ui_events.hpp" @@ -37,15 +36,13 @@ static auto queue_add(lua_State* state) -> int { database::TrackId id = luaL_checkinteger(state, 1); instance->services().bg_worker().Dispatch([=]() { audio::TrackQueue& queue = instance->services().track_queue(); - auto editor = queue.Edit(); - queue.Append(editor, id); + queue.append(id); }); } else { - database::Iterator it = *db_check_iterator(state, 1); + database::Iterator* it = db_check_iterator(state, 1); instance->services().bg_worker().Dispatch([=]() { audio::TrackQueue& queue = instance->services().track_queue(); - auto editor = queue.Edit(); - queue.Append(editor, database::TrackIterator{it}); + queue.append(database::TrackIterator{*it}); }); } @@ -55,8 +52,7 @@ static auto queue_add(lua_State* state) -> int { static auto queue_clear(lua_State* state) -> int { Bridge* instance = Bridge::Get(state); audio::TrackQueue& queue = instance->services().track_queue(); - auto editor = queue.Edit(); - queue.Clear(editor); + queue.clear(); return 0; } diff --git a/src/playlist/CMakeLists.txt b/src/playlist/CMakeLists.txt deleted file mode 100644 index 6c08dd5a..00000000 --- a/src/playlist/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright 2023 jacqueline -# -# SPDX-License-Identifier: GPL-3.0-only - -idf_component_register( - SRCS "source.cpp" "shuffler.cpp" - INCLUDE_DIRS "include" - REQUIRES "database" "util") - -target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/playlist/include/shuffler.hpp b/src/playlist/include/shuffler.hpp deleted file mode 100644 index affc6301..00000000 --- a/src/playlist/include/shuffler.hpp +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#pragma once - -#include -#include -#include -#include -#include - -#include "bloom_filter.hpp" -#include "database.hpp" -#include "future_fetcher.hpp" -#include "random.hpp" -#include "source.hpp" -#include "track.hpp" - -namespace playlist { - -/* - * A source composes of other sources and/or specific extra tracks. Supports - * iteration over its contents in a random order. - */ -class Shuffler : public ISource { - public: - static auto Create() -> Shuffler*; - - explicit Shuffler( - util::IRandom* random, - std::unique_ptr> filter); - - auto Current() -> std::optional override; - auto Advance() -> std::optional override; - auto Peek(std::size_t, std::vector*) - -> std::size_t override; - - typedef std::variant> - Item; - auto Add(Item) -> void; - - /* - * Returns the enqueued items, starting from the current item, in their - * original insertion order. - */ - auto Unshuffle() -> std::vector; - - // Not copyable or movable. - - Shuffler(const Shuffler&) = delete; - Shuffler& operator=(const Shuffler&) = delete; - - private: - auto RefillBuffer() -> void; - - util::IRandom* random_; - - std::unique_ptr> already_played_; - bool out_of_items_; - - std::deque ordered_items_; - std::deque shuffled_items_buffer_; -}; - -} // namespace playlist diff --git a/src/playlist/include/source.hpp b/src/playlist/include/source.hpp deleted file mode 100644 index ce12faf3..00000000 --- a/src/playlist/include/source.hpp +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "bloom_filter.hpp" -#include "database.hpp" -#include "future_fetcher.hpp" -#include "random.hpp" -#include "track.hpp" - -namespace playlist { - -/* - * Stateful interface for iterating over a collection of tracks by id. - */ -class ISource { - public: - virtual ~ISource() {} - - virtual auto Current() -> std::optional = 0; - - /* - * Discards the current track id and continues to the next in this source. - * Returns the new current track id. - */ - virtual auto Advance() -> std::optional = 0; - - /* - * Repeatedly advances until a track with the given id is the current track. - * Returns false if this source ran out of tracks before the requested id - * was encounted, true otherwise. - */ - virtual auto AdvanceTo(database::TrackId id) -> bool { - for (auto t = Current(); t.has_value(); t = Advance()) { - if (*t == id) { - return true; - } - } - return false; - } - - /* - * Places the next n tracks into the given vector, in order. Does not change - * the value returned by Current(). - */ - virtual auto Peek(std::size_t n, std::vector*) - -> std::size_t = 0; -}; - -/* - * A Source that supports restarting iteration from its original initial - * value. - */ -class IResetableSource : public ISource { - public: - virtual ~IResetableSource() {} - - virtual auto Previous() -> std::optional = 0; - - /* - * Restarts iteration from this source's initial value. - */ - virtual auto Reset() -> void = 0; -}; - -class IteratorSource : public IResetableSource { - public: - IteratorSource(const database::Iterator&); - - auto Current() -> std::optional override; - auto Advance() -> std::optional override; - auto Peek(std::size_t n, std::vector*) - -> std::size_t override; - - auto Previous() -> std::optional override; - auto Reset() -> void override; - - private: - const database::Iterator& start_; - std::optional current_; - std::stack> next_; -}; - -auto CreateSourceFromResults( - std::weak_ptr, - std::shared_ptr>) - -> std::shared_ptr; - -class IndexRecordSource : public IResetableSource { - public: - IndexRecordSource(std::weak_ptr db, - std::shared_ptr>); - - IndexRecordSource(std::weak_ptr db, - std::shared_ptr>, - std::size_t, - std::shared_ptr>, - std::size_t); - - auto Current() -> std::optional override; - auto Advance() -> std::optional override; - auto Peek(std::size_t n, std::vector*) - -> std::size_t override; - - auto Previous() -> std::optional override; - auto Reset() -> void override; - - private: - std::weak_ptr db_; - - std::shared_ptr> initial_page_; - ssize_t initial_item_; - - std::shared_ptr> current_page_; - ssize_t current_item_; -}; - -class NestedSource : public IResetableSource { - public: - NestedSource(std::weak_ptr db, - std::shared_ptr>); - - auto Current() -> std::optional override; - auto Advance() -> std::optional override; - auto Peek(std::size_t n, std::vector*) - -> std::size_t override; - - auto Previous() -> std::optional override; - auto Reset() -> void override; - - private: - auto CreateChild(std::shared_ptr page) - -> std::shared_ptr; - - std::weak_ptr db_; - - std::shared_ptr> initial_page_; - ssize_t initial_item_; - - std::shared_ptr> current_page_; - ssize_t current_item_; - - std::shared_ptr current_child_; -}; - -} // namespace playlist diff --git a/src/playlist/shuffler.cpp b/src/playlist/shuffler.cpp deleted file mode 100644 index b1c92335..00000000 --- a/src/playlist/shuffler.cpp +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#include "shuffler.hpp" - -#include -#include -#include -#include -#include - -#include "bloom_filter.hpp" -#include "database.hpp" -#include "komihash.h" -#include "random.hpp" -#include "track.hpp" - -static constexpr std::size_t kShufflerBufferSize = 32; - -namespace playlist { - -auto Shuffler::Create() -> Shuffler* { - return new Shuffler(util::sRandom, - std::make_unique>( - [](database::TrackId id) { - return komihash(&id, sizeof(database::TrackId), 0); - })); -} - -Shuffler::Shuffler(util::IRandom* random, - std::unique_ptr> filter) - : random_(random), already_played_(std::move(filter)) {} - -auto Shuffler::Current() -> std::optional { - if (shuffled_items_buffer_.empty()) { - return {}; - } - return shuffled_items_buffer_.front(); -} - -auto Shuffler::Advance() -> std::optional { - if (shuffled_items_buffer_.empty() && !out_of_items_) { - RefillBuffer(); - } - - auto res = Current(); - if (res) { - // Mark tracks off in the bloom filter only *after* they've been advanced - // past. This gives us the most flexibility for reshuffling when adding new - // items. - already_played_->Insert(*res); - shuffled_items_buffer_.pop_front(); - } - return res; -} - -auto Shuffler::Peek(std::size_t num, std::vector* out) - -> std::size_t { - if (shuffled_items_buffer_.size() < num) { - RefillBuffer(); - } - for (int i = 0; i < num; i++) { - if (i >= shuffled_items_buffer_.size()) { - // We must be out of data, since the buffer didn't fill up. - return i; - } - out->push_back(shuffled_items_buffer_.at(i)); - } - return num; -} - -auto Shuffler::Add(Item item) -> void { - ordered_items_.push_back(item); - out_of_items_ = false; - - // Empty out the buffer of already shuffled items, since we will need to - // shuffle again in order to incorporate the newly added item(s). We keep the - // current item however because we wouldn't want Add() to change the value of - // Current() unless we're completely out of items. - if (shuffled_items_buffer_.size() > 1) { - shuffled_items_buffer_.erase(shuffled_items_buffer_.begin() + 1, - shuffled_items_buffer_.end()); - } - RefillBuffer(); -} - -auto Shuffler::Unshuffle() -> std::vector { - std::vector ret; - database::TrackId current = shuffled_items_buffer_.front(); - bool has_found_current = false; - - for (const Item& item : ordered_items_) { - if (!has_found_current) { - // TODO(jacqueline): *Should* this include previous items? What is the - // 'previous' button meant to do after unshuffling? - if (std::holds_alternative(item)) { - has_found_current = current == std::get(item); - } else { - auto source = std::get>(item); - source->Reset(); - has_found_current = - std::get>(item)->AdvanceTo( - current); - } - } else { - ret.push_back(item); - } - } - - return ret; -} - -auto Shuffler::RefillBuffer() -> void { - // Don't waste time iterating if we know there's nothing new. - if (out_of_items_) { - return; - } - - int num_to_sample = kShufflerBufferSize - shuffled_items_buffer_.size(); - int resovoir_offset = shuffled_items_buffer_.size(); - - std::set in_buffer; - for (const database::TrackId& id : shuffled_items_buffer_) { - in_buffer.insert(id); - } - - uint32_t i = 0; - auto consider_item = [&, this](const database::TrackId& item) { - if (already_played_->Contains(item) || in_buffer.contains(item)) { - return; - } - if (i < num_to_sample) { - shuffled_items_buffer_.push_back(item); - } else { - uint32_t index_to_replace = random_->RangeInclusive(0, i); - if (index_to_replace < num_to_sample) { - shuffled_items_buffer_[resovoir_offset + index_to_replace] = item; - } - } - i++; - }; - - for (const Item& item : ordered_items_) { - if (std::holds_alternative(item)) { - std::invoke(consider_item, std::get(item)); - } else { - auto source = std::get>(item); - source->Reset(); - while (source->Advance()) { - std::invoke(consider_item, *source->Current()); - } - } - } - - out_of_items_ = i > num_to_sample; - // We've now got a random *selection*, but the order might be predictable - // (e.g. if there were only `num_to_sample` new items). Do a final in-memory - // shuffle. - std::random_shuffle(shuffled_items_buffer_.begin() + resovoir_offset, - shuffled_items_buffer_.end()); -} - -} // namespace playlist diff --git a/src/playlist/source.cpp b/src/playlist/source.cpp deleted file mode 100644 index 2540c3fb..00000000 --- a/src/playlist/source.cpp +++ /dev/null @@ -1,360 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#include "source.hpp" - -#include -#include -#include -#include -#include - -#include "esp_log.h" - -#include "bloom_filter.hpp" -#include "database.hpp" -#include "komihash.h" -#include "random.hpp" -#include "track.hpp" - -namespace playlist { - -[[maybe_unused]] static constexpr char kTag[] = "queue_src"; - -IteratorSource::IteratorSource(const database::Iterator& it) - : start_(it), current_(), next_() { - Reset(); - Advance(); -} - -auto IteratorSource::Current() -> std::optional { - return current_; -} - -auto IteratorSource::Advance() -> std::optional { - ESP_LOGI(kTag, "advancing"); - while (!next_.empty()) { - auto next = next_.top().NextSync(); - if (!next) { - ESP_LOGI(kTag, "top was empty"); - next_.pop(); - continue; - } - if (next->track()) { - ESP_LOGI(kTag, "top held track %lu", next->track().value_or(0)); - current_ = next->track(); - return current_; - } - ESP_LOGI(kTag, "top held records"); - next_.push(database::Iterator(start_.database(), next->Expand(1).value())); - } - ESP_LOGI(kTag, "exhausted"); - return {}; -} - -auto IteratorSource::Peek(std::size_t n, std::vector*) - -> std::size_t { - return 0; -} - -auto IteratorSource::Previous() -> std::optional { - return {}; -} - -auto IteratorSource::Reset() -> void { - while (!next_.empty()) { - next_.pop(); - } - next_.push(start_); -} - -auto CreateSourceFromResults( - std::weak_ptr db, - std::shared_ptr> results) - -> std::shared_ptr { - if (results->values()[0]->track()) { - return std::make_shared(db, results); - } else { - return std::make_shared(db, results); - } -} - -IndexRecordSource::IndexRecordSource( - std::weak_ptr db, - std::shared_ptr> initial) - : db_(db), - initial_page_(initial), - initial_item_(0), - current_page_(initial_page_), - current_item_(initial_item_) {} - -IndexRecordSource::IndexRecordSource( - std::weak_ptr db, - std::shared_ptr> initial, - std::size_t initial_index, - std::shared_ptr> current, - std::size_t current_index) - : db_(db), - initial_page_(initial), - initial_item_(initial_index), - current_page_(current), - current_item_(current_index) {} - -auto IndexRecordSource::Current() -> std::optional { - if (current_page_->values().size() <= current_item_) { - return {}; - } - if (current_page_ == initial_page_ && current_item_ < initial_item_) { - return {}; - } - - return current_page_->values().at(current_item_)->track(); -} - -auto IndexRecordSource::Advance() -> std::optional { - current_item_++; - if (current_item_ >= current_page_->values().size()) { - auto next_page = current_page_->next_page(); - if (!next_page) { - current_item_--; - return {}; - } - - auto db = db_.lock(); - if (!db) { - return {}; - } - - current_page_.reset(db->GetPage(&*next_page).get()); - current_item_ = 0; - } - - return Current(); -} - -auto IndexRecordSource::Previous() -> std::optional { - if (current_page_ == initial_page_ && current_item_ <= initial_item_) { - return {}; - } - - current_item_--; - if (current_item_ < 0) { - auto prev_page = current_page_->prev_page(); - if (!prev_page) { - return {}; - } - - auto db = db_.lock(); - if (!db) { - return {}; - } - - current_page_.reset(db->GetPage(&*prev_page).get()); - current_item_ = current_page_->values().size() - 1; - } - - return Current(); -} - -auto IndexRecordSource::Peek(std::size_t n, std::vector* out) - -> std::size_t { - if (current_page_->values().size() <= current_item_) { - return {}; - } - - auto db = db_.lock(); - if (!db) { - return 0; - } - - std::size_t items_added = 0; - - std::shared_ptr> working_page = - current_page_; - std::size_t working_item = current_item_ + 1; - - while (n > 0) { - if (working_item >= working_page->values().size()) { - auto next_page = current_page_->next_page(); - if (!next_page) { - break; - } - // TODO(jacqueline): It would probably be a good idea to hold onto these - // peeked pages, to avoid needing to look them up again later. - working_page.reset(db->GetPage(&*next_page).get()); - working_item = 0; - } - - auto record = working_page->values().at(working_item); - if (record->track()) { - out->push_back(record->track().value()); - n--; - items_added++; - } - working_item++; - } - - return items_added; -} - -auto IndexRecordSource::Reset() -> void { - current_page_ = initial_page_; - current_item_ = initial_item_; -} - -NestedSource::NestedSource( - std::weak_ptr db, - std::shared_ptr> initial) - : db_(db), - initial_page_(initial), - initial_item_(0), - current_page_(initial_page_), - current_item_(initial_item_), - current_child_(CreateChild(initial->values()[0])) {} - -auto NestedSource::Current() -> std::optional { - if (current_child_) { - return current_child_->Current(); - } - return {}; -} - -auto NestedSource::Advance() -> std::optional { - if (!current_child_) { - return {}; - } - - auto child_next = current_child_->Advance(); - if (child_next) { - return child_next; - } - // Our current child has run out of tracks. Move on to the next child. - current_item_++; - current_child_.reset(); - - if (current_item_ >= current_page_->values().size()) { - // We're even out of items in this page! - auto next_page = current_page_->next_page(); - if (!next_page) { - current_item_--; - return {}; - } - - auto db = db_.lock(); - if (!db) { - return {}; - } - - current_page_.reset(db->GetPage(&*next_page).get()); - current_item_ = 0; - } - current_child_ = CreateChild(current_page_->values()[current_item_]); - - return Current(); -} - -auto NestedSource::Previous() -> std::optional { - if (current_page_ == initial_page_ && current_item_ <= initial_item_) { - return {}; - } - - current_item_--; - current_child_.reset(); - - if (current_item_ < 0) { - auto prev_page = current_page_->prev_page(); - if (!prev_page) { - return {}; - } - - auto db = db_.lock(); - if (!db) { - return {}; - } - - current_page_.reset(db->GetPage(&*prev_page).get()); - current_item_ = current_page_->values().size() - 1; - } - current_child_ = CreateChild(current_page_->values()[current_item_]); - - return Current(); -} - -auto NestedSource::Peek(std::size_t n, std::vector* out) - -> std::size_t { - if (current_page_->values().size() <= current_item_) { - return {}; - } - - auto db = db_.lock(); - if (!db) { - return 0; - } - - std::size_t items_added = 0; - - std::shared_ptr> working_page = - current_page_; - std::size_t working_item = current_item_; - std::shared_ptr working_child = current_child_; - - while (working_child) { - auto res = working_child->Peek(n, out); - n -= res; - items_added += res; - - if (n == 0) { - break; - } else { - working_item++; - if (working_item < working_page->values().size()) { - working_child = CreateChild(working_page->values()[working_item]); - } else { - auto next_page = current_page_->next_page(); - if (!next_page) { - break; - } - working_page.reset( - db->GetPage(&*next_page).get()); - working_item = 0; - working_child = CreateChild(working_page->values()[0]); - } - } - } - - return items_added; -} - -auto NestedSource::Reset() -> void { - current_page_ = initial_page_; - current_item_ = initial_item_; - current_child_ = CreateChild(initial_page_->values()[initial_item_]); -} - -auto NestedSource::CreateChild(std::shared_ptr record) - -> std::shared_ptr { - auto cont = record->Expand(10); - if (!cont) { - return {}; - } - auto db = db_.lock(); - if (!db) { - return {}; - } - std::shared_ptr> next_level{ - db->GetPage(&*cont).get()}; - if (!next_level) { - return {}; - } - auto next_level_record = next_level->values()[0]; - if (next_level_record->track()) { - return std::make_shared(db_, next_level); - } else { - return std::make_shared(db_, next_level); - } -} - -} // namespace playlist diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp index c9a0a2d2..9ccc107b 100644 --- a/src/system_fsm/booting.cpp +++ b/src/system_fsm/booting.cpp @@ -86,7 +86,8 @@ auto Booting::entry() -> void { sServices->battery(std::make_unique( sServices->samd(), std::unique_ptr(adc))); - sServices->track_queue(std::make_unique()); + sServices->track_queue( + std::make_unique(sServices->bg_worker())); sServices->tag_parser(std::make_unique()); sServices->collator(locale::CreateCollator()); diff --git a/src/ui/include/ui_events.hpp b/src/ui/include/ui_events.hpp index 6a6be304..111f37a8 100644 --- a/src/ui/include/ui_events.hpp +++ b/src/ui/include/ui_events.hpp @@ -30,18 +30,6 @@ struct OnSystemError : tinyfsm::Event {}; namespace internal { -struct RecordSelected : tinyfsm::Event { - bool show_menu; - std::pmr::vector new_crumbs; - std::shared_ptr> initial_page; - std::shared_ptr> page; - std::size_t record; -}; - -struct IndexSelected : tinyfsm::Event { - database::IndexId id; -}; - struct ControlSchemeChanged : tinyfsm::Event {}; struct ReindexDatabase : tinyfsm::Event {}; diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index a8291a46..f5f8c574 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -64,7 +64,6 @@ class UiState : public tinyfsm::Fsm { virtual void react(const system_fsm::KeyLockChanged&); virtual void react(const OnLuaError&) {} - virtual void react(const internal::RecordSelected&) {} virtual void react(const internal::BackPressed&) {} virtual void react(const internal::ShowSettingsPage&){}; virtual void react(const internal::ModalCancelPressed&) { diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 3c57f573..9fc31481 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -41,7 +41,6 @@ #include "screen_lua.hpp" #include "screen_settings.hpp" #include "screen_splash.hpp" -#include "source.hpp" #include "spiffs.hpp" #include "storage.hpp" #include "system_events.hpp" @@ -118,7 +117,7 @@ void UiState::react(const audio::PlaybackUpdate& ev) {} void UiState::react(const audio::QueueUpdate&) { auto& queue = sServices->track_queue(); - sPlaybackModel.current_track.set(queue.Current()); + sPlaybackModel.current_track.set(queue.current()); } void UiState::react(const internal::ControlSchemeChanged&) { @@ -281,15 +280,14 @@ void Lua::react(const system_fsm::BatteryStateChanged& ev) { } void Lua::react(const audio::QueueUpdate&) { - sServices->bg_worker().Dispatch([=]() { - auto& queue = sServices->track_queue(); - size_t total_size = queue.GetTotalSize(); - size_t current_pos = queue.GetCurrentPosition(); - events::Ui().RunOnTask([=]() { - queue_size_->Update(static_cast(total_size)); - queue_position_->Update(static_cast(current_pos)); - }); - }); + auto& queue = sServices->track_queue(); + queue_size_->Update(static_cast(queue.totalSize())); + + int current_pos = queue.currentPosition(); + if (queue.current()) { + current_pos++; + } + queue_position_->Update(current_pos); } void Lua::react(const audio::PlaybackStarted& ev) { @@ -373,7 +371,7 @@ void Indexing::entry() { // TODO: Hmm. return; } - db->Update(); + db->updateIndexes(); } void Indexing::exit() {