diff --git a/src/tangara/audio/playlist.cpp b/src/tangara/audio/playlist.cpp index 1436e2d2..f57f078d 100644 --- a/src/tangara/audio/playlist.cpp +++ b/src/tangara/audio/playlist.cpp @@ -4,9 +4,12 @@ * SPDX-License-Identifier: GPL-3.0-only */ #include "playlist.hpp" +#include #include +#include "cppbor.h" +#include "cppbor_parse.h" #include "esp_log.h" #include "ff.h" @@ -29,6 +32,7 @@ Playlist::Playlist(const std::string& playlistFilepath) auto Playlist::open() -> bool { std::unique_lock lock(mutex_); + if (file_open_) { return true; } @@ -42,10 +46,12 @@ auto Playlist::open() -> bool { file_open_ = true; file_error_ = false; - // Count the playlist size and build our offset cache. - countItems(); - // Advance to the first item. - skipToWithoutCache(0); + if (!deserialiseCache()) { + // Count the playlist size and build our offset cache. + countItems(); + // Advance to the first item. + skipToWithoutCache(0); + } return !file_error_; } @@ -100,6 +106,106 @@ auto Playlist::skipTo(size_t position) -> void { skipToLocked(position); } +// Serialise the cache to a file to avoid having to rescan +// the entire queue when resuming +auto Playlist::serialiseCache() -> bool { + std::unique_lock lock(mutex_); + if (!file_open_) { + return false; + } + + FIL file; + + // Open the cache file + std::string cache_file = filepath_ + ".cache"; + FRESULT res = + f_open(&file, cache_file.c_str(), FA_READ | FA_WRITE | FA_CREATE_ALWAYS); + if (res != FR_OK) { + ESP_LOGE(kTag, "failed to open cache file! res: %i", res); + return false; + } + + cppbor::Array data; + // First item = file size of queue file (for checking this file matches) + data.add(f_size(&file_)); + // Next item = number of tracks in this queue + data.add(total_size_); + + // Next, write out every cached offset + for (uint64_t offset : offset_cache_) { + data.add(offset); + } + + auto encoded = data.encode(); + + UINT bytes_written = 0; + f_write(&file, encoded.data(), encoded.size(), &bytes_written); + if (bytes_written != encoded.size()) { + return false; + } + + f_close(&file); + return true; +} + +auto Playlist::deserialiseCache() -> bool { + if (!file_open_) { + return false; + } + + FIL file; + + // Open the cache file + std::string cache_file = filepath_ + ".cache"; + FRESULT res = + f_open(&file, cache_file.c_str(), FA_READ | FA_WRITE | FA_OPEN_ALWAYS); + if (res != FR_OK) { + ESP_LOGE(kTag, "failed to open cache file! res: %i", res); + return false; + } + + std::vector encoded; + encoded.resize(f_size(&file)); + + UINT bytes_read; + f_read(&file, encoded.data(), encoded.size(), &bytes_read); + if (bytes_read != encoded.size()) { + return false; + } + + auto [data, unused, err] = cppbor::parse(encoded); + if (!data || data->type() != cppbor::ARRAY) { + return false; + } + auto entries = data->asArray(); + + // Double check the expected file size matches. + if (entries->get(0)->asUint()->unsignedValue() != f_size(&file_)) { + return false; + } + + total_size_ = entries->get(1)->asUint()->unsignedValue(); + + // In case we have existing entries + offset_cache_.clear(); + + // Read in the cache + for (size_t i = 2; i < entries->size(); i++) { + offset_cache_.push_back(entries->get(i)->asUint()->unsignedValue()); + } + + f_close(&file); + return true; +} + +auto Playlist::close() -> void { + if (file_open_) { + f_close(&file_); + file_open_ = false; + file_error_ = false; + } +} + auto Playlist::skipToLocked(size_t position) -> void { if (!file_open_ || file_error_) { return; @@ -210,9 +316,46 @@ auto Playlist::nextItem(std::span buf) MutablePlaylist::MutablePlaylist(const std::string& playlistFilepath) : Playlist(playlistFilepath) {} +auto MutablePlaylist::open() -> bool { + std::unique_lock lock(mutex_); + + if (file_open_) { + return true; + } + + FRESULT res = + f_open(&file_, filepath_.c_str(), FA_READ | FA_WRITE | FA_OPEN_ALWAYS); + if (res != FR_OK) { + ESP_LOGE(kTag, "failed to open file! res: %i", res); + return false; + } + file_open_ = true; + file_error_ = false; + + auto queue_filesize = f_size(&file_); + + if (!deserialiseCache()) { + // If there's no cache (or deserialising failed) and the queue is + // sufficiently large, abort and clear the queue + if (queue_filesize > 50000) { + clearLocked(); + } else { + // Otherwise, read in the existing entries + countItems(); + // Advance to the first item. + skipToWithoutCache(0); + } + } + + return !file_error_; +} + auto MutablePlaylist::clear() -> bool { std::unique_lock lock(mutex_); + return clearLocked(); +} +auto MutablePlaylist::clearLocked() -> bool { // Try to recover from any IO errors. if (file_error_ && file_open_) { file_error_ = false; diff --git a/src/tangara/audio/playlist.hpp b/src/tangara/audio/playlist.hpp index ac62c82e..1e05e9c4 100644 --- a/src/tangara/audio/playlist.hpp +++ b/src/tangara/audio/playlist.hpp @@ -33,7 +33,7 @@ class Playlist { virtual ~Playlist(); using Item = std::variant; - auto open() -> bool; + virtual auto open() -> bool; auto filepath() const -> std::string; auto currentPosition() const -> size_t; @@ -45,6 +45,10 @@ class Playlist { auto prev() -> void; auto skipTo(size_t position) -> void; + auto serialiseCache() -> bool; + auto deserialiseCache() -> bool; + auto close() -> void; + protected: const std::string filepath_; @@ -68,7 +72,7 @@ class Playlist { */ const uint32_t sample_size_; - private: + protected: auto skipToLocked(size_t position) -> void; auto countItems() -> void; auto advanceBy(ssize_t amt) -> bool; @@ -79,9 +83,13 @@ class Playlist { class MutablePlaylist : public Playlist { public: MutablePlaylist(const std::string& playlistFilepath); + auto open() -> bool override; auto clear() -> bool; auto append(Item i) -> void; + + private: + auto clearLocked() -> bool; }; } // namespace audio diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index ff24637b..05ac0b95 100644 --- a/src/tangara/audio/track_queue.cpp +++ b/src/tangara/audio/track_queue.cpp @@ -159,6 +159,13 @@ auto TrackQueue::open() -> bool { return playlist_.open(); } +auto TrackQueue::close() -> void { + playlist_.close(); + if (opened_playlist_) { + opened_playlist_->close(); + } +} + auto TrackQueue::openPlaylist(const std::string& playlist_file, bool notify) -> bool { opened_playlist_.emplace(playlist_file); @@ -422,6 +429,9 @@ auto TrackQueue::serialise() -> std::string { cppbor::Uint{shuffle_->pos()}, }); } + + playlist_.serialiseCache(); + return encoded.toString(); } diff --git a/src/tangara/audio/track_queue.hpp b/src/tangara/audio/track_queue.hpp index 727b4be0..383c204e 100644 --- a/src/tangara/audio/track_queue.hpp +++ b/src/tangara/audio/track_queue.hpp @@ -76,8 +76,9 @@ class TrackQueue { auto currentPosition(size_t position) -> bool; auto totalSize() const -> size_t; auto open() -> bool; - auto openPlaylist(const std::string& playlist_file, bool notify = true) - -> bool; + auto close() -> void; + auto openPlaylist(const std::string& playlist_file, + bool notify = true) -> bool; auto playFromPosition(const std::string& filepath, uint32_t position) -> void; using Item = diff --git a/src/tangara/system_fsm/running.cpp b/src/tangara/system_fsm/running.cpp index f065737b..227eac2c 100644 --- a/src/tangara/system_fsm/running.cpp +++ b/src/tangara/system_fsm/running.cpp @@ -214,6 +214,7 @@ void Running::react(const internal::Mount& ev) { auto Running::unmountStorage() -> void { ESP_LOGW(kTag, "unmounting storage"); + sServices->track_queue().close(); sServices->database({}); sStorage.reset(); updateSdState(drivers::SdState::kNotMounted);