From a169e60cac24f760f01d4a056ca04a768ace755f Mon Sep 17 00:00:00 2001 From: ailurux Date: Tue, 1 Oct 2024 17:42:42 +1000 Subject: [PATCH 1/6] Add additional cache file for playlist table of contents --- src/tangara/audio/playlist.cpp | 131 +++++++++++++++++++++++++++++- src/tangara/audio/playlist.hpp | 3 + src/tangara/audio/track_queue.cpp | 3 + 3 files changed, 133 insertions(+), 4 deletions(-) diff --git a/src/tangara/audio/playlist.cpp b/src/tangara/audio/playlist.cpp index 1436e2d2..48f94e6c 100644 --- a/src/tangara/audio/playlist.cpp +++ b/src/tangara/audio/playlist.cpp @@ -42,10 +42,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 +102,127 @@ 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; + } + + uint8_t header[8]; + + // First 4 bytes = file size of queue file (for checking this file matches) + uint32_t q_file_size = f_size(&file_); + header[0] = q_file_size >> 24; + header[1] = q_file_size >> 16; + header[2] = q_file_size >> 8; + header[3] = q_file_size; + + // Next 4 bytes = number of tracks in this queue + header[4] = total_size_ >> 24; + header[5] = total_size_ >> 16; + header[6] = total_size_ >> 8; + header[7] = total_size_; + + UINT bytes_written = 0; + f_write(&file, header, 8, &bytes_written); + if (bytes_written < 8) { + return false; + } + + // Next, write out every cached offset + for (uint64_t offset : offset_cache_) { + uint8_t bytes_out[8]; + bytes_out[0] = offset >> 56; + bytes_out[1] = offset >> 48; + bytes_out[2] = offset >> 40; + bytes_out[3] = offset >> 32; + bytes_out[4] = offset >> 24; + bytes_out[5] = offset >> 16; + bytes_out[6] = offset >> 8; + bytes_out[7] = offset; + f_write(&file, bytes_out, 8, &bytes_written); + if (bytes_written < 8) { + 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; + } + + uint8_t header[8]; + UINT bytes_read; + f_read(&file, header, 8, &bytes_read); + uint32_t file_size_check = header[0] << 24 | + header[1] << 16 | + header[2] << 8 | + header[3]; + + // TODO: Handle this conversion safely + if (file_size_check != (uint32_t)f_size(&file_)) { + ESP_LOGE("DANIEL", "Check didn't match. File size check: %lu, actual size: %llu", file_size_check, f_size(&file_)); + return false; + } + + uint32_t size = header[4] << 24 | + header[5] << 16 | + header[6] << 8 | + header[7]; + total_size_ = size; + + // Read in the cache + uint8_t buf[8]; + size_t idx = 0; + while (true) { + f_read(&file, buf, 8, &bytes_read); + if (bytes_read == 0) { + break; + } + uint64_t offset = buf[0] << 56 | + buf[1] << 48 | + buf[2] << 40 | + buf[3] << 32 | + buf[4] << 24 | + buf[5] << 16 | + buf[6] << 8 | + buf[7]; + offset_cache_.push_back(offset); + } + + f_close(&file); + return true; +} + auto Playlist::skipToLocked(size_t position) -> void { if (!file_open_ || file_error_) { return; diff --git a/src/tangara/audio/playlist.hpp b/src/tangara/audio/playlist.hpp index ac62c82e..421b28fa 100644 --- a/src/tangara/audio/playlist.hpp +++ b/src/tangara/audio/playlist.hpp @@ -45,6 +45,9 @@ class Playlist { auto prev() -> void; auto skipTo(size_t position) -> void; + auto serialiseCache() -> bool; + auto deserialiseCache() -> bool; + protected: const std::string filepath_; diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index ff24637b..35c1403f 100644 --- a/src/tangara/audio/track_queue.cpp +++ b/src/tangara/audio/track_queue.cpp @@ -422,6 +422,9 @@ auto TrackQueue::serialise() -> std::string { cppbor::Uint{shuffle_->pos()}, }); } + + playlist_.serialiseCache(); + return encoded.toString(); } From 1d2153f69d7c50551a7e1351e37cd8598d933baa Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 3 Oct 2024 11:19:42 +1000 Subject: [PATCH 2/6] Clean up + use 8 bytes for uint64_t --- src/tangara/audio/playlist.cpp | 68 ++++++++++++++++------------------ 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/src/tangara/audio/playlist.cpp b/src/tangara/audio/playlist.cpp index 48f94e6c..6b87b618 100644 --- a/src/tangara/audio/playlist.cpp +++ b/src/tangara/audio/playlist.cpp @@ -47,7 +47,7 @@ auto Playlist::open() -> bool { countItems(); // Advance to the first item. skipToWithoutCache(0); - } + } return !file_error_; } @@ -102,7 +102,6 @@ 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 { @@ -121,28 +120,32 @@ auto Playlist::serialiseCache() -> bool { ESP_LOGE(kTag, "failed to open cache file! res: %i", res); return false; } - - uint8_t header[8]; - // First 4 bytes = file size of queue file (for checking this file matches) - uint32_t q_file_size = f_size(&file_); - header[0] = q_file_size >> 24; - header[1] = q_file_size >> 16; - header[2] = q_file_size >> 8; - header[3] = q_file_size; + uint8_t header[12]; - // Next 4 bytes = number of tracks in this queue + // First 8 bytes = file size of queue file (for checking this file matches) + uint64_t q_file_size = f_size(&file_); + header[0] = q_file_size >> 56; + header[1] = q_file_size >> 48; + header[2] = q_file_size >> 40; + header[3] = q_file_size >> 32; + header[4] = q_file_size >> 24; + header[5] = q_file_size >> 16; + header[6] = q_file_size >> 8; + header[7] = q_file_size; + + // Next 4 bytes = number of tracks in this queue header[4] = total_size_ >> 24; header[5] = total_size_ >> 16; header[6] = total_size_ >> 8; header[7] = total_size_; UINT bytes_written = 0; - f_write(&file, header, 8, &bytes_written); - if (bytes_written < 8) { + f_write(&file, header, 12, &bytes_written); + if (bytes_written < 12) { return false; } - + // Next, write out every cached offset for (uint64_t offset : offset_cache_) { uint8_t bytes_out[8]; @@ -179,25 +182,23 @@ auto Playlist::deserialiseCache() -> bool { ESP_LOGE(kTag, "failed to open cache file! res: %i", res); return false; } - + uint8_t header[8]; UINT bytes_read; - f_read(&file, header, 8, &bytes_read); - uint32_t file_size_check = header[0] << 24 | - header[1] << 16 | - header[2] << 8 | - header[3]; - - // TODO: Handle this conversion safely - if (file_size_check != (uint32_t)f_size(&file_)) { - ESP_LOGE("DANIEL", "Check didn't match. File size check: %lu, actual size: %llu", file_size_check, f_size(&file_)); + f_read(&file, header, 12, &bytes_read); + if (bytes_read != 12) { return false; } + uint64_t file_size_check = + header[0] << 56 | header[1] << 48 | header[2] << 40 | header[3] << 32 | + header[4] << 24 | header[5] << 16 | header[6] << 8 | header[7]; - uint32_t size = header[4] << 24 | - header[5] << 16 | - header[6] << 8 | - header[7]; + if (file_size_check != f_size(&file_)) { + return false; + } + + uint32_t size = + header[4] << 24 | header[5] << 16 | header[6] << 8 | header[7]; total_size_ = size; // Read in the cache @@ -208,16 +209,11 @@ auto Playlist::deserialiseCache() -> bool { if (bytes_read == 0) { break; } - uint64_t offset = buf[0] << 56 | - buf[1] << 48 | - buf[2] << 40 | - buf[3] << 32 | - buf[4] << 24 | - buf[5] << 16 | - buf[6] << 8 | + uint64_t offset = buf[0] << 56 | buf[1] << 48 | buf[2] << 40 | + buf[3] << 32 | buf[4] << 24 | buf[5] << 16 | buf[6] << 8 | buf[7]; offset_cache_.push_back(offset); - } + } f_close(&file); return true; From ea71fa5d2b76a4de6e67a46b0853dc1dc5272d39 Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 3 Oct 2024 14:06:50 +1000 Subject: [PATCH 3/6] Clear queue if loading cache fails and file is large, also fix indices oops --- src/tangara/audio/playlist.cpp | 44 ++++++++++++++++++++++++++++++---- src/tangara/audio/playlist.hpp | 3 ++- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/tangara/audio/playlist.cpp b/src/tangara/audio/playlist.cpp index 6b87b618..630b0097 100644 --- a/src/tangara/audio/playlist.cpp +++ b/src/tangara/audio/playlist.cpp @@ -135,10 +135,10 @@ auto Playlist::serialiseCache() -> bool { header[7] = q_file_size; // Next 4 bytes = number of tracks in this queue - header[4] = total_size_ >> 24; - header[5] = total_size_ >> 16; - header[6] = total_size_ >> 8; - header[7] = total_size_; + header[8] = total_size_ >> 24; + header[9] = total_size_ >> 16; + header[10] = total_size_ >> 8; + header[11] = total_size_; UINT bytes_written = 0; f_write(&file, header, 12, &bytes_written); @@ -198,7 +198,7 @@ auto Playlist::deserialiseCache() -> bool { } uint32_t size = - header[4] << 24 | header[5] << 16 | header[6] << 8 | header[7]; + header[8] << 24 | header[9] << 16 | header[10] << 8 | header[11]; total_size_ = size; // Read in the cache @@ -329,6 +329,40 @@ 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) { + clear(); + } else { + // Otherwise, read in the existing entries + countItems(); + // Advance to the first item. + skipToWithoutCache(0); + } + } + + return !file_error_; + return false; +} + auto MutablePlaylist::clear() -> bool { std::unique_lock lock(mutex_); diff --git a/src/tangara/audio/playlist.hpp b/src/tangara/audio/playlist.hpp index 421b28fa..7cf99953 100644 --- a/src/tangara/audio/playlist.hpp +++ b/src/tangara/audio/playlist.hpp @@ -71,7 +71,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; @@ -82,6 +82,7 @@ class Playlist { class MutablePlaylist : public Playlist { public: MutablePlaylist(const std::string& playlistFilepath); + auto open() -> bool; auto clear() -> bool; auto append(Item i) -> void; From 6318b272842f5447c90d55123a60fdde2202993d Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 3 Oct 2024 15:19:55 +1000 Subject: [PATCH 4/6] Use cppbor for encoding playlist toc Also fix a locking issue with opening+clearing Co-authored-by: ailurux --- src/tangara/audio/playlist.cpp | 93 +++++++++++++--------------------- src/tangara/audio/playlist.hpp | 7 ++- 2 files changed, 40 insertions(+), 60 deletions(-) diff --git a/src/tangara/audio/playlist.cpp b/src/tangara/audio/playlist.cpp index 630b0097..805b0953 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" @@ -121,48 +124,25 @@ auto Playlist::serialiseCache() -> bool { return false; } - uint8_t header[12]; + 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_); - // First 8 bytes = file size of queue file (for checking this file matches) - uint64_t q_file_size = f_size(&file_); - header[0] = q_file_size >> 56; - header[1] = q_file_size >> 48; - header[2] = q_file_size >> 40; - header[3] = q_file_size >> 32; - header[4] = q_file_size >> 24; - header[5] = q_file_size >> 16; - header[6] = q_file_size >> 8; - header[7] = q_file_size; + // Next, write out every cached offset + for (uint64_t offset : offset_cache_) { + data.add(offset); + } - // Next 4 bytes = number of tracks in this queue - header[8] = total_size_ >> 24; - header[9] = total_size_ >> 16; - header[10] = total_size_ >> 8; - header[11] = total_size_; + auto encoded = data.encode(); UINT bytes_written = 0; - f_write(&file, header, 12, &bytes_written); - if (bytes_written < 12) { + f_write(&file, encoded.data(), encoded.size(), &bytes_written); + if (bytes_written != encoded.size()) { return false; } - // Next, write out every cached offset - for (uint64_t offset : offset_cache_) { - uint8_t bytes_out[8]; - bytes_out[0] = offset >> 56; - bytes_out[1] = offset >> 48; - bytes_out[2] = offset >> 40; - bytes_out[3] = offset >> 32; - bytes_out[4] = offset >> 24; - bytes_out[5] = offset >> 16; - bytes_out[6] = offset >> 8; - bytes_out[7] = offset; - f_write(&file, bytes_out, 8, &bytes_written); - if (bytes_written < 8) { - return false; - } - } - f_close(&file); return true; } @@ -183,36 +163,31 @@ auto Playlist::deserialiseCache() -> bool { return false; } - uint8_t header[8]; + std::vector encoded; + encoded.resize(f_size(&file)); + UINT bytes_read; - f_read(&file, header, 12, &bytes_read); - if (bytes_read != 12) { + 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; } - uint64_t file_size_check = - header[0] << 56 | header[1] << 48 | header[2] << 40 | header[3] << 32 | - header[4] << 24 | header[5] << 16 | header[6] << 8 | header[7]; + auto entries = data->asArray(); - if (file_size_check != f_size(&file_)) { + // Double check the expected file size matches. + if (entries->get(0)->asUint()->unsignedValue() != f_size(&file_)) { return false; } - uint32_t size = - header[8] << 24 | header[9] << 16 | header[10] << 8 | header[11]; - total_size_ = size; + total_size_ = entries->get(1)->asUint()->unsignedValue(); // Read in the cache - uint8_t buf[8]; - size_t idx = 0; - while (true) { - f_read(&file, buf, 8, &bytes_read); - if (bytes_read == 0) { - break; - } - uint64_t offset = buf[0] << 56 | buf[1] << 48 | buf[2] << 40 | - buf[3] << 32 | buf[4] << 24 | buf[5] << 16 | buf[6] << 8 | - buf[7]; - offset_cache_.push_back(offset); + for (size_t i = 2; i < entries->size(); i++) { + offset_cache_.push_back(entries->get(i)->asUint()->unsignedValue()); } f_close(&file); @@ -350,7 +325,7 @@ auto MutablePlaylist::open() -> bool { // If there's no cache (or deserialising failed) and the queue is // sufficiently large, abort and clear the queue if (queue_filesize > 50000) { - clear(); + clearLocked(); } else { // Otherwise, read in the existing entries countItems(); @@ -360,12 +335,14 @@ auto MutablePlaylist::open() -> bool { } return !file_error_; - return false; } 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 7cf99953..b794e444 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; @@ -82,10 +82,13 @@ class Playlist { class MutablePlaylist : public Playlist { public: MutablePlaylist(const std::string& playlistFilepath); - auto open() -> bool; + auto open() -> bool override; auto clear() -> bool; auto append(Item i) -> void; + + private: + auto clearLocked() -> bool; }; } // namespace audio From 39a5d062fc13ad81e91968e1c30b729ec606d934 Mon Sep 17 00:00:00 2001 From: ailurux Date: Fri, 4 Oct 2024 14:19:13 +1000 Subject: [PATCH 5/6] Fix issues with deserialising queue when queue exists already --- src/tangara/audio/playlist.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/tangara/audio/playlist.cpp b/src/tangara/audio/playlist.cpp index 805b0953..db712252 100644 --- a/src/tangara/audio/playlist.cpp +++ b/src/tangara/audio/playlist.cpp @@ -32,9 +32,6 @@ Playlist::Playlist(const std::string& playlistFilepath) auto Playlist::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); @@ -185,6 +182,9 @@ auto Playlist::deserialiseCache() -> bool { 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()); @@ -306,10 +306,7 @@ MutablePlaylist::MutablePlaylist(const std::string& 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) { From 7cc6f198cf437ae60185b65bfa556341be8ec63f Mon Sep 17 00:00:00 2001 From: ailurux Date: Fri, 4 Oct 2024 14:41:05 +1000 Subject: [PATCH 6/6] Explicitly close the playlist files in the queue on storage unmount --- src/tangara/audio/playlist.cpp | 16 ++++++++++++++++ src/tangara/audio/playlist.hpp | 1 + src/tangara/audio/track_queue.cpp | 7 +++++++ src/tangara/audio/track_queue.hpp | 5 +++-- src/tangara/system_fsm/running.cpp | 1 + 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/tangara/audio/playlist.cpp b/src/tangara/audio/playlist.cpp index db712252..f57f078d 100644 --- a/src/tangara/audio/playlist.cpp +++ b/src/tangara/audio/playlist.cpp @@ -33,6 +33,10 @@ Playlist::Playlist(const std::string& playlistFilepath) auto Playlist::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) { @@ -194,6 +198,14 @@ auto Playlist::deserialiseCache() -> bool { 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; @@ -306,6 +318,10 @@ MutablePlaylist::MutablePlaylist(const std::string& 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); diff --git a/src/tangara/audio/playlist.hpp b/src/tangara/audio/playlist.hpp index b794e444..1e05e9c4 100644 --- a/src/tangara/audio/playlist.hpp +++ b/src/tangara/audio/playlist.hpp @@ -47,6 +47,7 @@ class Playlist { auto serialiseCache() -> bool; auto deserialiseCache() -> bool; + auto close() -> void; protected: const std::string filepath_; diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index 35c1403f..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); 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);