From 53798f4a6191b9606ebf8a1dec1b447455081a66 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 10 Jul 2024 15:17:19 +1000 Subject: Move audio decoder priorities below bluetooth The previous priority was leading to a nasty consistent stutter, as reading samples from the drain suffer would lead to the decoder immediately unblocking and preempting the SBC encoding. --- src/tasks/tasks.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/tasks/tasks.cpp b/src/tasks/tasks.cpp index d3937c68..b713d70b 100644 --- a/src/tasks/tasks.cpp +++ b/src/tasks/tasks.cpp @@ -83,11 +83,11 @@ auto Priority() -> UBaseType_t; // highest priority. template <> auto Priority() -> UBaseType_t { - return configMAX_PRIORITIES - 1; + return 15; } template <> auto Priority() -> UBaseType_t { - return configMAX_PRIORITIES - 1; + return 15; } // After audio issues, UI jank is the most noticeable kind of scheduling-induced // slowness that the user is likely to notice or care about. Therefore we place -- cgit v1.2.3 From 11bddb1b1da603a7d6c0e9f239eb89fb724be08e Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 10 Jul 2024 15:18:33 +1000 Subject: add a console command for dumping intr allocations --- src/tangara/dev_console/console.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) (limited to 'src') diff --git a/src/tangara/dev_console/console.cpp b/src/tangara/dev_console/console.cpp index a7f7a721..fcb987bb 100644 --- a/src/tangara/dev_console/console.cpp +++ b/src/tangara/dev_console/console.cpp @@ -13,6 +13,7 @@ #include #include "esp_console.h" +#include "esp_intr_alloc.h" #include "esp_log.h" #include "esp_system.h" @@ -66,12 +67,34 @@ void RegisterLogLevel() { esp_console_cmd_register(&cmd); } +int CmdInterrupts(int argc, char** argv) { + static const std::pmr::string usage = "usage: intr"; + if (argc != 1) { + std::cout << usage << std::endl; + return 1; + } + esp_intr_dump(NULL); + return 0; +} + +void RegisterInterrupts() { + esp_console_cmd_t cmd{.command = "intr", + .help = "Dumps a table of all allocated interrupts", + .hint = NULL, + .func = &CmdInterrupts, + .argtable = NULL}; + esp_console_cmd_register(&cmd); + cmd.command = "interrupts"; + esp_console_cmd_register(&cmd); +} + Console::Console() {} Console::~Console() {} auto Console::RegisterCommonComponents() -> void { esp_console_register_help_command(); RegisterLogLevel(); + RegisterInterrupts(); } static Console* sInstance; -- cgit v1.2.3 From b63e897268bdbaa679cc68b6c9586ba4d5520b45 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 10 Jul 2024 15:18:55 +1000 Subject: Move the SPI interrupt alloc to the second core We're a bit close to the line on core0 allocs, so this helps balance things out a bit. --- src/drivers/spi.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/drivers/spi.cpp b/src/drivers/spi.cpp index 632fe89f..40487197 100644 --- a/src/drivers/spi.cpp +++ b/src/drivers/spi.cpp @@ -41,7 +41,7 @@ esp_err_t init_spi(void) { // manages its own use of DMA-capable memory. .max_transfer_sz = 4096, .flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_IOMUX_PINS, - .isr_cpu_id = ESP_INTR_CPU_AFFINITY_0, + .isr_cpu_id = ESP_INTR_CPU_AFFINITY_1, .intr_flags = ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_IRAM, }; -- cgit v1.2.3 From a9d2335e1d86b3012789a440e7f0e71033393056 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 9 Jul 2024 14:41:02 +1000 Subject: Break FatfsStreamFactory's dep on ServiceLocator --- src/tangara/audio/audio_fsm.cpp | 13 +++++++------ src/tangara/audio/fatfs_stream_factory.cpp | 12 ++++++------ src/tangara/audio/fatfs_stream_factory.hpp | 11 +++++------ src/tangara/database/database.cpp | 6 ++++++ src/tangara/database/database.hpp | 10 ++++++++++ src/tangara/system_fsm/service_locator.hpp | 2 +- 6 files changed, 35 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/tangara/audio/audio_fsm.cpp b/src/tangara/audio/audio_fsm.cpp index 80611082..24f287ac 100644 --- a/src/tangara/audio/audio_fsm.cpp +++ b/src/tangara/audio/audio_fsm.cpp @@ -237,11 +237,11 @@ void AudioState::react(const system_fsm::BluetoothEvent& ev) { break; } } - if (std::holds_alternative(ev.event)) { - auto volume_chg = std::get(ev.event).new_vol; - events::Ui().Dispatch(RemoteVolumeChanged{ - .value = volume_chg - }); + if (std::holds_alternative( + ev.event)) { + auto volume_chg = + std::get(ev.event).new_vol; + events::Ui().Dispatch(RemoteVolumeChanged{.value = volume_chg}); } } @@ -356,7 +356,8 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { sDrainBuffer = std::make_unique(kDrainLatencySamples); - sStreamFactory.reset(new FatfsStreamFactory(*sServices)); + sStreamFactory.reset( + new FatfsStreamFactory(sServices->database(), sServices->tag_parser())); sI2SOutput.reset(new I2SAudioOutput(sServices->gpios(), *sDrainBuffer)); sBtOutput.reset(new BluetoothAudioOutput( sServices->bluetooth(), *sDrainBuffer, sServices->bg_worker())); diff --git a/src/tangara/audio/fatfs_stream_factory.cpp b/src/tangara/audio/fatfs_stream_factory.cpp index 80677b2d..735ec134 100644 --- a/src/tangara/audio/fatfs_stream_factory.cpp +++ b/src/tangara/audio/fatfs_stream_factory.cpp @@ -10,7 +10,6 @@ #include #include -#include "database/database.hpp" #include "esp_log.h" #include "ff.h" #include "freertos/portmacro.h" @@ -19,10 +18,10 @@ #include "audio/audio_source.hpp" #include "audio/fatfs_source.hpp" #include "codec.hpp" +#include "database/database.hpp" #include "database/tag_parser.hpp" #include "database/track.hpp" #include "drivers/spi.hpp" -#include "system_fsm/service_locator.hpp" #include "tasks.hpp" #include "types.hpp" @@ -30,12 +29,13 @@ namespace audio { -FatfsStreamFactory::FatfsStreamFactory(system_fsm::ServiceLocator& services) - : services_(services) {} +FatfsStreamFactory::FatfsStreamFactory(database::Handle&& handle, + database::ITagParser& parser) + : db_(handle), tag_parser_(parser) {} auto FatfsStreamFactory::create(database::TrackId id, uint32_t offset) -> std::shared_ptr { - auto db = services_.database().lock(); + auto db = db_.lock(); if (!db) { return {}; } @@ -48,7 +48,7 @@ auto FatfsStreamFactory::create(database::TrackId id, uint32_t offset) auto FatfsStreamFactory::create(std::string path, uint32_t offset) -> std::shared_ptr { - auto tags = services_.tag_parser().ReadAndParseTags(path); + auto tags = tag_parser_.ReadAndParseTags(path); if (!tags) { ESP_LOGE(kTag, "failed to read tags"); return {}; diff --git a/src/tangara/audio/fatfs_stream_factory.hpp b/src/tangara/audio/fatfs_stream_factory.hpp index 858d2131..84073d2d 100644 --- a/src/tangara/audio/fatfs_stream_factory.hpp +++ b/src/tangara/audio/fatfs_stream_factory.hpp @@ -6,23 +6,21 @@ #pragma once -#include #include #include #include #include #include -#include "database/database.hpp" -#include "database/track.hpp" #include "ff.h" #include "freertos/portmacro.h" #include "audio/audio_source.hpp" #include "codec.hpp" +#include "database/database.hpp" #include "database/future_fetcher.hpp" #include "database/tag_parser.hpp" -#include "system_fsm/service_locator.hpp" +#include "database/track.hpp" #include "tasks.hpp" #include "types.hpp" @@ -33,7 +31,7 @@ namespace audio { */ class FatfsStreamFactory { public: - explicit FatfsStreamFactory(system_fsm::ServiceLocator&); + explicit FatfsStreamFactory(database::Handle&&, database::ITagParser&); auto create(database::TrackId, uint32_t offset = 0) -> std::shared_ptr; @@ -47,7 +45,8 @@ class FatfsStreamFactory { auto ContainerToStreamType(database::Container) -> std::optional; - system_fsm::ServiceLocator& services_; + database::Handle db_; + database::ITagParser& tag_parser_; }; } // namespace audio diff --git a/src/tangara/database/database.cpp b/src/tangara/database/database.cpp index cf1430b3..85700431 100644 --- a/src/tangara/database/database.cpp +++ b/src/tangara/database/database.cpp @@ -684,6 +684,12 @@ auto Database::countRecords(const SearchKey& c) -> size_t { return count; } +Handle::Handle(std::shared_ptr& db) : db_(db) {} + +auto Handle::lock() -> std::shared_ptr { + return db_; +} + auto SearchKey::startKey() const -> std::string_view { if (key) { return *key; diff --git a/src/tangara/database/database.hpp b/src/tangara/database/database.hpp index d2de7c72..c2e72568 100644 --- a/src/tangara/database/database.hpp +++ b/src/tangara/database/database.hpp @@ -128,6 +128,16 @@ class Database { auto countRecords(const SearchKey& c) -> size_t; }; +class Handle { + public: + Handle(std::shared_ptr& db); + + auto lock() -> std::shared_ptr; + + private: + std::shared_ptr& db_; +}; + /* * 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 diff --git a/src/tangara/system_fsm/service_locator.hpp b/src/tangara/system_fsm/service_locator.hpp index 3d136f3a..d441fa70 100644 --- a/src/tangara/system_fsm/service_locator.hpp +++ b/src/tangara/system_fsm/service_locator.hpp @@ -92,7 +92,7 @@ class ServiceLocator { auto haptics(std::unique_ptr i) { haptics_ = std::move(i); } - auto database() -> std::weak_ptr { return database_; } + auto database() -> database::Handle { return database_; } auto database(std::unique_ptr i) { database_ = std::move(i); -- cgit v1.2.3 From a3eb2dd9dc2399ce9c22cd3b07f482f080976440 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 11 Jul 2024 15:11:28 +1000 Subject: WIP improve bluetooth api and settings screen --- src/drivers/bluetooth.cpp | 338 ++++++++++++++---------- src/drivers/include/drivers/bluetooth.hpp | 116 +++++--- src/drivers/include/drivers/bluetooth_types.hpp | 7 +- src/drivers/include/drivers/nvs.hpp | 7 + src/drivers/nvs.cpp | 83 ++++++ src/tangara/app_console/app_console.cpp | 17 +- src/tangara/audio/audio_fsm.cpp | 11 +- src/tangara/audio/bt_audio_output.cpp | 13 +- src/tangara/lua/property.cpp | 30 +-- src/tangara/lua/property.hpp | 4 +- src/tangara/system_fsm/booting.cpp | 3 +- src/tangara/ui/ui_fsm.cpp | 114 +++++--- src/tangara/ui/ui_fsm.hpp | 5 +- 13 files changed, 486 insertions(+), 262 deletions(-) (limited to 'src') diff --git a/src/drivers/bluetooth.cpp b/src/drivers/bluetooth.cpp index 412cba1f..2edf5ad9 100644 --- a/src/drivers/bluetooth.cpp +++ b/src/drivers/bluetooth.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -113,92 +114,111 @@ IRAM_ATTR auto a2dp_data_cb(uint8_t* buf, int32_t buf_size) -> int32_t { return buf_size; } -Bluetooth::Bluetooth(NvsStorage& storage, tasks::WorkerPool& bg_worker) { +Bluetooth::Bluetooth(NvsStorage& storage, + tasks::WorkerPool& bg_worker, + EventHandler cb) + : nvs_(storage) { sBgWorker = &bg_worker; - bluetooth::BluetoothState::Init(storage); + bluetooth::BluetoothState::Init(storage, cb); } -auto Bluetooth::Enable() -> bool { - auto lock = bluetooth::BluetoothState::lock(); - tinyfsm::FsmList::dispatch( - bluetooth::events::Enable{}); +auto Bluetooth::enable(bool en) -> void { + if (en) { + auto lock = bluetooth::BluetoothState::lock(); + tinyfsm::FsmList::dispatch( + bluetooth::events::Enable{}); + } else { + // FIXME: the BT tasks unfortunately call back into us while holding an + // internal lock, which then deadlocks with our fsm lock. + // auto lock = bluetooth::BluetoothState::lock(); + tinyfsm::FsmList::dispatch( + bluetooth::events::Disable{}); + } +} +auto Bluetooth::enabled() -> bool { + auto lock = bluetooth::BluetoothState::lock(); return !bluetooth::BluetoothState::is_in_state(); } -auto Bluetooth::Disable() -> void { - // FIXME: the BT tasks unfortunately call back into us while holding an - // internal lock, which then deadlocks with our fsm lock. - // auto lock = bluetooth::BluetoothState::lock(); +auto Bluetooth::source(PcmBuffer* src) -> void { + if (src == sStream) { + return; + } + auto lock = bluetooth::BluetoothState::lock(); + sStream = src; tinyfsm::FsmList::dispatch( - bluetooth::events::Disable{}); + bluetooth::events::SourceChanged{}); } -auto Bluetooth::IsEnabled() -> bool { - auto lock = bluetooth::BluetoothState::lock(); - return !bluetooth::BluetoothState::is_in_state(); +auto Bluetooth::softVolume(float f) -> void { + sVolumeFactor = f; } -auto Bluetooth::IsConnected() -> bool { +auto Bluetooth::connectionState() -> ConnectionState { auto lock = bluetooth::BluetoothState::lock(); - return bluetooth::BluetoothState::is_in_state(); + if (bluetooth::BluetoothState::is_in_state()) { + return ConnectionState::kConnected; + } else if (bluetooth::BluetoothState::is_in_state()) { + return ConnectionState::kConnecting; + } + return ConnectionState::kDisconnected; } -auto Bluetooth::ConnectedDevice() -> std::optional { +auto Bluetooth::pairedDevice() -> std::optional { auto lock = bluetooth::BluetoothState::lock(); - if (!bluetooth::BluetoothState::is_in_state()) { - return {}; - } - return bluetooth::BluetoothState::preferred_device(); + return bluetooth::BluetoothState::pairedDevice(); } -auto Bluetooth::KnownDevices() -> std::vector { +auto Bluetooth::pairedDevice(std::optional dev) -> void { auto lock = bluetooth::BluetoothState::lock(); - std::vector out = bluetooth::BluetoothState::devices(); - std::sort(out.begin(), out.end(), [](const auto& a, const auto& b) -> bool { - return a.signal_strength < b.signal_strength; - }); - return out; + bluetooth::BluetoothState::pairedDevice(dev); } -auto Bluetooth::SetPreferredDevice(std::optional dev) - -> void { - auto lock = bluetooth::BluetoothState::lock(); - auto cur = bluetooth::BluetoothState::preferred_device(); - if (dev && cur && dev->mac == cur->mac) { - return; - } - ESP_LOGI(kTag, "preferred is '%s' (%u%u%u%u%u%u)", dev->name.c_str(), - dev->mac[0], dev->mac[1], dev->mac[2], dev->mac[3], dev->mac[4], - dev->mac[5]); - bluetooth::BluetoothState::preferred_device(dev); - tinyfsm::FsmList::dispatch( - bluetooth::events::PreferredDeviceChanged{}); +auto Bluetooth::knownDevices() -> std::vector { + return nvs_.BluetoothNames(); } -auto Bluetooth::PreferredDevice() -> std::optional { - auto lock = bluetooth::BluetoothState::lock(); - return bluetooth::BluetoothState::preferred_device(); +auto Bluetooth::forgetKnownDevice(const bluetooth::mac_addr_t& mac) -> void { + nvs_.BluetoothName(mac, {}); } -auto Bluetooth::SetSource(PcmBuffer* src) -> void { +auto Bluetooth::discoveryEnabled(bool en) -> void { auto lock = bluetooth::BluetoothState::lock(); - if (src == bluetooth::BluetoothState::source()) { - return; - } - bluetooth::BluetoothState::source(src); - tinyfsm::FsmList::dispatch( - bluetooth::events::SourceChanged{}); + bluetooth::BluetoothState::discovery(en); } -auto Bluetooth::SetVolumeFactor(float f) -> void { - sVolumeFactor = f; +auto Bluetooth::discoveryEnabled() -> bool { + auto lock = bluetooth::BluetoothState::lock(); + return bluetooth::BluetoothState::discovery(); } -auto Bluetooth::SetEventHandler(std::function cb) - -> void { - auto lock = bluetooth::BluetoothState::lock(); - bluetooth::BluetoothState::event_handler(cb); +auto Bluetooth::discoveredDevices() -> std::vector { + std::vector discovered; + { + auto lock = bluetooth::BluetoothState::lock(); + discovered = bluetooth::BluetoothState::discoveredDevices(); + } + + // Show devices with stronger signals first, since they're more likely to be + // physically close (and therefore more likely to be what the user wants). + std::sort(discovered.begin(), discovered.end(), + [](const auto& a, const auto& b) -> bool { + return a.signal_strength < b.signal_strength; + }); + + // Convert to the right format. + std::vector out; + out.reserve(discovered.size()); + std::transform(discovered.begin(), discovered.end(), std::back_inserter(out), + [&](const bluetooth::Device& dev) { + return bluetooth::MacAndName{ + .mac = dev.address, + .name = {dev.name.data(), dev.name.size()}, + }; + }); + + return out; } static auto DeviceName() -> std::pmr::string { @@ -251,6 +271,10 @@ auto Scanner::StopScanningNow() -> void { } } +auto Scanner::enabled() -> bool { + return enabled_; +} + auto Scanner::HandleGapEvent(const events::internal::Gap& ev) -> void { switch (ev.type) { case ESP_BT_GAP_DISC_RES_EVT: @@ -343,17 +367,18 @@ NvsStorage* BluetoothState::sStorage_; Scanner* BluetoothState::sScanner_; std::mutex BluetoothState::sFsmMutex{}; -std::map BluetoothState::sDevices_{}; -std::optional BluetoothState::sPreferredDevice_{}; -std::optional BluetoothState::sConnectingDevice_{}; +std::map BluetoothState::sDiscoveredDevices_{}; +std::optional BluetoothState::sPairedWith_{}; +std::optional BluetoothState::sConnectingTo_{}; int BluetoothState::sConnectAttemptsRemaining_{0}; -std::atomic BluetoothState::sSource_; std::function BluetoothState::sEventHandler_; -auto BluetoothState::Init(NvsStorage& storage) -> void { +auto BluetoothState::Init(NvsStorage& storage, Bluetooth::EventHandler cb) + -> void { sStorage_ = &storage; - sPreferredDevice_ = storage.PreferredBluetoothDevice(); + sEventHandler_ = cb; + sPairedWith_ = storage.PreferredBluetoothDevice(); tinyfsm::FsmList::start(); } @@ -361,68 +386,85 @@ auto BluetoothState::lock() -> std::lock_guard { return std::lock_guard{sFsmMutex}; } -auto BluetoothState::devices() -> std::vector { - std::vector out; - for (const auto& device : sDevices_) { - out.push_back(device.second); - } - return out; +auto BluetoothState::pairedDevice() -> std::optional { + return sPairedWith_; } -auto BluetoothState::preferred_device() -> std::optional { - return sPreferredDevice_; -} +auto BluetoothState::pairedDevice(std::optional dev) -> void { + auto cur = sPairedWith_; + if (dev && cur && dev->mac == cur->mac) { + return; + } + if (dev) { + ESP_LOGI(kTag, "pairing with '%s' (%u%u%u%u%u%u)", dev->name.c_str(), + dev->mac[0], dev->mac[1], dev->mac[2], dev->mac[3], dev->mac[4], + dev->mac[5]); + } + sPairedWith_ = dev; + std::invoke(sEventHandler_, SimpleEvent::kDeviceDiscovered); -auto BluetoothState::preferred_device(std::optional addr) -> void { - sPreferredDevice_ = addr; + tinyfsm::FsmList::dispatch( + bluetooth::events::PairedDeviceChanged{}); } -auto BluetoothState::source() -> PcmBuffer* { - return sSource_.load(); +auto BluetoothState::discovery() -> bool { + return sScanner_->enabled(); } -auto BluetoothState::source(PcmBuffer* src) -> void { - sSource_.store(src); +auto BluetoothState::discovery(bool en) -> void { + if (en) { + if (!sScanner_->enabled()) { + sDiscoveredDevices_.clear(); + } + sScanner_->ScanContinuously(); + } else { + sScanner_->StopScanning(); + } } -auto BluetoothState::event_handler(std::function cb) -> void { - sEventHandler_ = cb; +auto BluetoothState::discoveredDevices() -> std::vector { + std::vector out; + for (const auto& device : sDiscoveredDevices_) { + out.push_back(device.second); + } + return out; } auto BluetoothState::react(const events::DeviceDiscovered& ev) -> void { - bool is_preferred = false; - bool already_known = sDevices_.contains(ev.device.address); - sDevices_[ev.device.address] = ev.device; + bool is_paired = false; + bool already_known = sDiscoveredDevices_.contains(ev.device.address); + sDiscoveredDevices_[ev.device.address] = ev.device; - if (sPreferredDevice_ && ev.device.address == sPreferredDevice_->mac) { - is_preferred = true; + if (sPairedWith_ && ev.device.address == sPairedWith_->mac) { + is_paired = true; } - if (sEventHandler_ && !already_known) { - std::invoke(sEventHandler_, SimpleEvent::kKnownDevicesChanged); + if (!already_known) { + std::invoke(sEventHandler_, SimpleEvent::kDeviceDiscovered); } - if (is_preferred && sPreferredDevice_) { - connect(*sPreferredDevice_); + if (is_paired && sPairedWith_) { + connect(*sPairedWith_); } } auto BluetoothState::connect(const MacAndName& dev) -> bool { - if (sConnectingDevice_ && sConnectingDevice_->mac == dev.mac) { + if (sConnectingTo_ && sConnectingTo_->mac == dev.mac) { sConnectAttemptsRemaining_--; } else { sConnectAttemptsRemaining_ = 3; } if (sConnectAttemptsRemaining_ == 0) { + sConnectingTo_ = {}; return false; } - sConnectingDevice_ = dev; + sConnectingTo_ = dev; ESP_LOGI(kTag, "connecting to '%s' (%u%u%u%u%u%u)", dev.name.c_str(), dev.mac[0], dev.mac[1], dev.mac[2], dev.mac[3], dev.mac[4], dev.mac[5]); - if (esp_a2d_source_connect(sConnectingDevice_->mac.data()) != ESP_OK) { + if (esp_a2d_source_connect(sConnectingTo_->mac.data()) != ESP_OK) { ESP_LOGI(kTag, "Connecting failed..."); if (sConnectAttemptsRemaining_ > 1) { ESP_LOGI(kTag, "Will retry."); @@ -508,11 +550,13 @@ void Disabled::react(const events::Enable&) { // AVRCP Target err = esp_avrc_tg_init(); if (err != ESP_OK) { - ESP_LOGE(kTag, "Error during target init: %s %d", esp_err_to_name(err), err); + ESP_LOGE(kTag, "Error during target init: %s %d", esp_err_to_name(err), + err); } err = esp_avrc_tg_register_callback(avrcp_tg_cb); if (err != ESP_OK) { - ESP_LOGE(kTag, "Error registering AVRC tg callback: %s %d", esp_err_to_name(err), err); + ESP_LOGE(kTag, "Error registering AVRC tg callback: %s %d", + esp_err_to_name(err), err); } // Set the supported passthrough commands on the tg @@ -522,19 +566,20 @@ void Disabled::react(const events::Enable&) { do { // Sleep for a bit vTaskDelay(pdMS_TO_TICKS(10)); - err = esp_avrc_tg_get_psth_cmd_filter(ESP_AVRC_PSTH_FILTER_ALLOWED_CMD, &psth); + err = esp_avrc_tg_get_psth_cmd_filter(ESP_AVRC_PSTH_FILTER_ALLOWED_CMD, + &psth); } while (err != ESP_OK); - err = esp_avrc_tg_set_psth_cmd_filter(ESP_AVRC_PSTH_FILTER_SUPPORTED_CMD, &psth); + err = esp_avrc_tg_set_psth_cmd_filter(ESP_AVRC_PSTH_FILTER_SUPPORTED_CMD, + &psth); if (err != ESP_OK) { ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err); } esp_avrc_rn_evt_cap_mask_t evt_set = {0}; esp_avrc_rn_evt_bit_mask_operation(ESP_AVRC_BIT_MASK_OP_SET, &evt_set, - ESP_AVRC_RN_VOLUME_CHANGE); + ESP_AVRC_RN_VOLUME_CHANGE); assert(esp_avrc_tg_set_rn_evt_cap(&evt_set) == ESP_OK); - // Initialise A2DP. This handles streaming audio. Currently ESP-IDF's SBC // encoder only supports 2 channels of interleaved 16 bit samples, at // 44.1kHz, so there is no additional configuration to be done for the @@ -547,37 +592,27 @@ void Disabled::react(const events::Enable&) { esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE, ESP_BT_NON_DISCOVERABLE); ESP_LOGI(kTag, "bt enabled"); - if (sPreferredDevice_) { - ESP_LOGI(kTag, "connecting to preferred device '%s'", - sPreferredDevice_->name.c_str()); - connect(*sPreferredDevice_); + if (sPairedWith_) { + ESP_LOGI(kTag, "connecting to paired device '%s'", + sPairedWith_->name.c_str()); + connect(*sPairedWith_); } else { - ESP_LOGI(kTag, "scanning for devices"); transit(); } } void Idle::entry() { ESP_LOGI(kTag, "bt is idle"); - sScanner_->ScanContinuously(); } -void Idle::exit() { - sScanner_->StopScanning(); -} +void Idle::exit() {} void Idle::react(const events::Disable& ev) { transit(); } -void Idle::react(const events::PreferredDeviceChanged& ev) { - bool is_discovered = false; - if (sPreferredDevice_ && sDevices_.contains(sPreferredDevice_->mac)) { - is_discovered = true; - } - if (is_discovered) { - connect(*sPreferredDevice_); - } +void Idle::react(const events::PairedDeviceChanged& ev) { + connect(*sPairedWith_); } void Idle::react(events::internal::Gap ev) { @@ -599,7 +634,6 @@ void Connecting::entry() { timeoutCallback); xTimerStart(sTimeoutTimer, portMAX_DELAY); - sScanner_->StopScanning(); if (sEventHandler_) { std::invoke(sEventHandler_, SimpleEvent::kConnectionStateChanged); } @@ -615,19 +649,24 @@ void Connecting::exit() { void Connecting::react(const events::ConnectTimedOut& ev) { ESP_LOGI(kTag, "timed out awaiting connection"); - esp_a2d_source_disconnect(sConnectingDevice_->mac.data()); - if (!connect(*sConnectingDevice_)) { + esp_a2d_source_disconnect(sConnectingTo_->mac.data()); + if (!connect(*sConnectingTo_)) { transit(); } } void Connecting::react(const events::Disable& ev) { - // TODO: disconnect gracefully + esp_a2d_source_disconnect(sConnectingTo_->mac.data()); transit(); } -void Connecting::react(const events::PreferredDeviceChanged& ev) { - // TODO. Cancel out and start again. +void Connecting::react(const events::PairedDeviceChanged& ev) { + esp_a2d_source_disconnect(sConnectingTo_->mac.data()); + if (sPairedWith_) { + connect(*sPairedWith_); + } else { + transit(); + } } void Connecting::react(events::internal::Gap ev) { @@ -698,15 +737,20 @@ void Connected::entry() { ESP_LOGI(kTag, "entering connected state"); transaction_num_ = 0; - connected_to_ = sConnectingDevice_->mac; - sPreferredDevice_ = sConnectingDevice_; - sConnectingDevice_ = {}; + connected_to_ = sConnectingTo_->mac; + sPairedWith_ = sConnectingTo_; + + sStorage_->BluetoothName(sConnectingTo_->mac, sConnectingTo_->name); + std::invoke(sEventHandler_, SimpleEvent::kKnownDevicesChanged); + + sConnectingTo_ = {}; auto stored_pref = sStorage_->PreferredBluetoothDevice(); - if (!stored_pref || (sPreferredDevice_->name != stored_pref->name || - sPreferredDevice_->mac != stored_pref->mac)) { - sStorage_->PreferredBluetoothDevice(sPreferredDevice_); + if (!stored_pref || (sPairedWith_->name != stored_pref->name || + sPairedWith_->mac != stored_pref->mac)) { + sStorage_->PreferredBluetoothDevice(sPairedWith_); } + // TODO: if we already have a source, immediately start playing } @@ -719,13 +763,11 @@ void Connected::react(const events::Disable& ev) { transit(); } -void Connected::react(const events::PreferredDeviceChanged& ev) { - sConnectingDevice_ = sPreferredDevice_; - transit(); +void Connected::react(const events::PairedDeviceChanged& ev) { + transit(); } void Connected::react(const events::SourceChanged& ev) { - sStream = sSource_; if (sStream != nullptr) { ESP_LOGI(kTag, "checking source is ready"); esp_a2d_media_ctrl(ESP_A2D_MEDIA_CTRL_CHECK_SRC_RDY); @@ -775,7 +817,8 @@ void Connected::react(events::internal::Avrc ev) { switch (ev.type) { case ESP_AVRC_CT_CONNECTION_STATE_EVT: if (ev.param.conn_stat.connected) { - auto err = esp_avrc_ct_send_register_notification_cmd(4, ESP_AVRC_RN_VOLUME_CHANGE, 0); + auto err = esp_avrc_ct_send_register_notification_cmd( + 4, ESP_AVRC_RN_VOLUME_CHANGE, 0); if (err != ESP_OK) { ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err); } @@ -787,15 +830,20 @@ void Connected::react(events::internal::Avrc ev) { case ESP_AVRC_CT_REMOTE_FEATURES_EVT: // The remote device is telling us about its capabilities! We don't // currently care about any of them. - ESP_LOGI(kTag, "Recieved capabilitites: %lu", ev.param.rmt_feats.feat_mask); + ESP_LOGI(kTag, "Recieved capabilitites: %lu", + ev.param.rmt_feats.feat_mask); break; case ESP_AVRC_CT_CHANGE_NOTIFY_EVT: if (ev.param.change_ntf.event_id == ESP_AVRC_RN_VOLUME_CHANGE) { if (sEventHandler_) { - std::invoke(sEventHandler_, bluetooth::RemoteVolumeChanged{.new_vol = ev.param.change_ntf.event_parameter.volume}); + std::invoke( + sEventHandler_, + bluetooth::RemoteVolumeChanged{ + .new_vol = ev.param.change_ntf.event_parameter.volume}); } // Resubscribe to volume facts - auto err = esp_avrc_ct_send_register_notification_cmd(4, ESP_AVRC_RN_VOLUME_CHANGE, 0); + auto err = esp_avrc_ct_send_register_notification_cmd( + 4, ESP_AVRC_RN_VOLUME_CHANGE, 0); if (err != ESP_OK) { ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err); } @@ -809,16 +857,20 @@ void Connected::react(events::internal::Avrc ev) { void Connected::react(const events::internal::Avrctg ev) { switch (ev.type) { case ESP_AVRC_TG_CONNECTION_STATE_EVT: - ESP_LOGI(kTag, "Got connection event. Connected: %s", ev.param.conn_stat.connected ? "true" : "false"); + ESP_LOGI(kTag, "Got connection event. Connected: %s", + ev.param.conn_stat.connected ? "true" : "false"); if (ev.param.conn_stat.connected) { } break; case ESP_AVRC_TG_REMOTE_FEATURES_EVT: - ESP_LOGI(kTag, "Got remote features feat flag %d", ev.param.rmt_feats.ct_feat_flag); - ESP_LOGI(kTag, "Got remote features feat mask %lu", ev.param.rmt_feats.feat_mask); + ESP_LOGI(kTag, "Got remote features feat flag %d", + ev.param.rmt_feats.ct_feat_flag); + ESP_LOGI(kTag, "Got remote features feat mask %lu", + ev.param.rmt_feats.feat_mask); break; case ESP_AVRC_TG_PASSTHROUGH_CMD_EVT: - ESP_LOGI(kTag, "Got passthrough event keycode: %x, %d", ev.param.psth_cmd.key_code, ev.param.psth_cmd.key_state); + ESP_LOGI(kTag, "Got passthrough event keycode: %x, %d", + ev.param.psth_cmd.key_code, ev.param.psth_cmd.key_state); if (ev.param.psth_cmd.key_state == 1 && sEventHandler_) { switch (ev.param.psth_cmd.key_code) { case ESP_AVRC_PT_CMD_PLAY: @@ -840,7 +892,8 @@ void Connected::react(const events::internal::Avrctg ev) { std::invoke(sEventHandler_, bluetooth::SimpleEvent::kBackward); break; default: - ESP_LOGI(kTag, "Unhandled passthrough cmd. Key code: %d", ev.param.psth_cmd.key_code); + ESP_LOGI(kTag, "Unhandled passthrough cmd. Key code: %d", + ev.param.psth_cmd.key_code); } } break; @@ -848,14 +901,15 @@ void Connected::react(const events::internal::Avrctg ev) { if (ev.param.reg_ntf.event_id == ESP_AVRC_RN_VOLUME_CHANGE) { // TODO: actually do this lol esp_avrc_rn_param_t rn_param; - rn_param.volume = 64; + rn_param.volume = 64; auto err = esp_avrc_tg_send_rn_rsp(ESP_AVRC_RN_VOLUME_CHANGE, - ESP_AVRC_RN_RSP_INTERIM, &rn_param); + ESP_AVRC_RN_RSP_INTERIM, &rn_param); if (err != ESP_OK) { ESP_LOGE(kTag, "Error: %s %d", esp_err_to_name(err), err); } } else { - ESP_LOGW(kTag, "unhandled AVRC TG Register Notification event: %u", ev.param.reg_ntf.event_id); + ESP_LOGW(kTag, "unhandled AVRC TG Register Notification event: %u", + ev.param.reg_ntf.event_id); } break; } diff --git a/src/drivers/include/drivers/bluetooth.hpp b/src/drivers/include/drivers/bluetooth.hpp index 94a85263..449812d6 100644 --- a/src/drivers/include/drivers/bluetooth.hpp +++ b/src/drivers/include/drivers/bluetooth.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -25,28 +26,68 @@ namespace drivers { /* - * A handle used to interact with the bluetooth state machine. + * A handle used to interact with the bluetooth state machine. This is the main + * API that the rest of the system should use to interact with Bluetooth. */ class Bluetooth { public: - Bluetooth(NvsStorage& storage, tasks::WorkerPool&); + /* + * Callback invoked when an event is generated by the Bluetooth stack. This + * callback is invoked synchronously from a Bluetooth task context, so + * implementations should immediately hop to a different task to process the + * event. + */ + using EventHandler = std::function; + + Bluetooth(NvsStorage&, tasks::WorkerPool&, EventHandler); + + /* Enables or disables the entire Bluetooth stack. */ + auto enable(bool en) -> void; + auto enabled() -> bool; + + auto source(PcmBuffer*) -> void; + auto softVolume(float) -> void; + + enum class ConnectionState { + kConnected, + kConnecting, + kDisconnected, + }; + + auto connectionState() -> ConnectionState; + + /* + * The 'paired' device is a device that will be preferred for connections. + * When Bluetooth is first enabled, we immediately try to connect to the + * paired device. If the paired device is seen during a scan, then we will + * also automatically connect to it. + */ + auto pairedDevice() -> std::optional; + + /* + * Sets the preferred device. If a device is provided, a connection will be + * attempted immediately, even if the device has not been detected in a + * previous scan. + */ + auto pairedDevice(std::optional dev) -> void; + + /* A list of devices that have previously been the paired device. */ + auto knownDevices() -> std::vector; + auto forgetKnownDevice(const bluetooth::mac_addr_t&) -> void; + + /* Enables or disables scanning for nearby Bluetooth devices. */ + auto discoveryEnabled(bool) -> void; + auto discoveryEnabled() -> bool; + + /* + * A list of nearby devices that have been discovered since discovery was + * last enabled. This list may include the paired device, as well as devices + * that are also present in the known devices list. + */ + auto discoveredDevices() -> std::vector; - auto Enable() -> bool; - auto Disable() -> void; - auto IsEnabled() -> bool; - - auto IsConnected() -> bool; - auto ConnectedDevice() -> std::optional; - - auto KnownDevices() -> std::vector; - - auto SetPreferredDevice(std::optional dev) -> void; - auto PreferredDevice() -> std::optional; - - auto SetSource(PcmBuffer*) -> void; - auto SetVolumeFactor(float) -> void; - - auto SetEventHandler(std::function cb) -> void; + private: + NvsStorage& nvs_; }; namespace bluetooth { @@ -56,7 +97,7 @@ struct Enable : public tinyfsm::Event {}; struct Disable : public tinyfsm::Event {}; struct ConnectTimedOut : public tinyfsm::Event {}; -struct PreferredDeviceChanged : public tinyfsm::Event {}; +struct PairedDeviceChanged : public tinyfsm::Event {}; struct SourceChanged : public tinyfsm::Event {}; struct DeviceDiscovered : public tinyfsm::Event { const Device& device; @@ -94,6 +135,8 @@ class Scanner { auto StopScanning() -> void; auto StopScanningNow() -> void; + auto enabled() -> bool; + auto HandleGapEvent(const events::internal::Gap&) -> void; private: @@ -103,25 +146,22 @@ class Scanner { auto HandleDeviceDiscovery(const esp_bt_gap_cb_param_t& param) -> void; }; +/* + * The main state machine for managing the state of the Bluetooth stack, and + * the current (if any) Bluetooth connection. + */ class BluetoothState : public tinyfsm::Fsm { public: - static auto Init(NvsStorage& storage) -> void; + static auto Init(NvsStorage& storage, Bluetooth::EventHandler) -> void; static auto lock() -> std::lock_guard; - static auto devices() -> std::vector; - - static auto preferred_device() -> std::optional; - static auto preferred_device(std::optional) -> void; + static auto pairedDevice() -> std::optional; + static auto pairedDevice(std::optional) -> void; - static auto scanning() -> bool; static auto discovery() -> bool; static auto discovery(bool) -> void; - - static auto source() -> PcmBuffer*; - static auto source(PcmBuffer*) -> void; - - static auto event_handler(std::function) -> void; + static auto discoveredDevices() -> std::vector; virtual ~BluetoothState(){}; @@ -131,7 +171,7 @@ class BluetoothState : public tinyfsm::Fsm { virtual void react(const events::Enable& ev){}; virtual void react(const events::Disable& ev) = 0; virtual void react(const events::ConnectTimedOut& ev){}; - virtual void react(const events::PreferredDeviceChanged& ev){}; + virtual void react(const events::PairedDeviceChanged& ev){}; virtual void react(const events::SourceChanged& ev){}; virtual void react(const events::DeviceDiscovered&); @@ -146,13 +186,11 @@ class BluetoothState : public tinyfsm::Fsm { static Scanner* sScanner_; static std::mutex sFsmMutex; - static std::map sDevices_; - static std::optional sPreferredDevice_; - - static std::optional sConnectingDevice_; + static std::map sDiscoveredDevices_; + static std::optional sPairedWith_; + static std::optional sConnectingTo_; static int sConnectAttemptsRemaining_; - static std::atomic sSource_; static std::function sEventHandler_; auto connect(const bluetooth::MacAndName&) -> bool; @@ -177,7 +215,7 @@ class Idle : public BluetoothState { void exit() override; void react(const events::Disable& ev) override; - void react(const events::PreferredDeviceChanged& ev) override; + void react(const events::PairedDeviceChanged& ev) override; void react(events::internal::Gap ev) override; @@ -189,7 +227,7 @@ class Connecting : public BluetoothState { void entry() override; void exit() override; - void react(const events::PreferredDeviceChanged& ev) override; + void react(const events::PairedDeviceChanged& ev) override; void react(const events::ConnectTimedOut& ev) override; void react(const events::Disable& ev) override; @@ -204,7 +242,7 @@ class Connected : public BluetoothState { void entry() override; void exit() override; - void react(const events::PreferredDeviceChanged& ev) override; + void react(const events::PairedDeviceChanged& ev) override; void react(const events::SourceChanged& ev) override; void react(const events::Disable& ev) override; diff --git a/src/drivers/include/drivers/bluetooth_types.hpp b/src/drivers/include/drivers/bluetooth_types.hpp index d2e55ee5..05caee47 100644 --- a/src/drivers/include/drivers/bluetooth_types.hpp +++ b/src/drivers/include/drivers/bluetooth_types.hpp @@ -27,9 +27,12 @@ struct Device { }; enum class SimpleEvent { - kKnownDevicesChanged, kConnectionStateChanged, - kPreferredDeviceChanged, + kPairedDeviceChanged, + kKnownDevicesChanged, + kDiscoveryChanged, + kDeviceDiscovered, + // Passthrough events kPlayPause, kStop, diff --git a/src/drivers/include/drivers/nvs.hpp b/src/drivers/include/drivers/nvs.hpp index 88dd5ae0..2bc77a31 100644 --- a/src/drivers/include/drivers/nvs.hpp +++ b/src/drivers/include/drivers/nvs.hpp @@ -96,6 +96,10 @@ class NvsStorage { auto BluetoothVolume(const bluetooth::mac_addr_t&) -> uint8_t; auto BluetoothVolume(const bluetooth::mac_addr_t&, uint8_t) -> void; + auto BluetoothNames() -> std::vector; + auto BluetoothName(const bluetooth::mac_addr_t&, std::optional) + -> void; + enum class Output : uint8_t { kHeadphones = 0, kBluetooth = 1, @@ -154,7 +158,10 @@ class NvsStorage { Setting amp_left_bias_; Setting input_mode_; Setting output_mode_; + Setting bt_preferred_; + Setting> bt_names_; + Setting db_auto_index_; util::LruCache<10, bluetooth::mac_addr_t, uint8_t> bt_volumes_; diff --git a/src/drivers/nvs.cpp b/src/drivers/nvs.cpp index 5c7d2218..c4d8dedc 100644 --- a/src/drivers/nvs.cpp +++ b/src/drivers/nvs.cpp @@ -26,6 +26,7 @@ static constexpr uint8_t kSchemaVersion = 1; static constexpr char kKeyVersion[] = "ver"; static constexpr char kKeyBluetoothPreferred[] = "bt_dev"; static constexpr char kKeyBluetoothVolumes[] = "bt_vols"; +static constexpr char kKeyBluetoothNames[] = "bt_names"; static constexpr char kKeyOutput[] = "out"; static constexpr char kKeyBrightness[] = "bright"; static constexpr char kKeyAmpMaxVolume[] = "hp_vol_max"; @@ -129,6 +130,44 @@ auto Setting::store(nvs_handle_t nvs, nvs_set_blob(nvs, name_, encoded.data(), encoded.size()); } +template <> +auto Setting>::load(nvs_handle_t nvs) + -> std::optional> { + auto raw = nvs_get_string(nvs, name_); + if (!raw) { + return {}; + } + auto [parsed, unused, err] = cppbor::parseWithViews( + reinterpret_cast(raw->data()), raw->size()); + if (parsed->type() != cppbor::MAP) { + return {}; + } + std::vector res; + for (const auto& i : *parsed->asMap()) { + auto mac = i.first->asViewBstr()->view(); + auto name = i.second->asViewTstr()->view(); + bluetooth::MacAndName entry{ + .mac = {}, + .name = {name.begin(), name.end()}, + }; + std::copy(mac.begin(), mac.end(), entry.mac.begin()); + res.push_back(entry); + } + return res; +} + +template <> +auto Setting>::store( + nvs_handle_t nvs, + std::vector v) -> void { + cppbor::Map cbor{}; + for (const auto& i : v) { + cbor.add(cppbor::Bstr{{i.mac.data(), i.mac.size()}}, cppbor::Tstr{i.name}); + } + auto encoded = cbor.encode(); + nvs_set_blob(nvs, name_, encoded.data(), encoded.size()); +} + template <> auto Setting::load(nvs_handle_t nvs) -> std::optional { @@ -208,6 +247,7 @@ NvsStorage::NvsStorage(nvs_handle_t handle) input_mode_(kKeyPrimaryInput), output_mode_(kKeyOutput), bt_preferred_(kKeyBluetoothPreferred), + bt_names_(kKeyBluetoothNames), db_auto_index_(kKeyDbAutoIndex), bt_volumes_(), bt_volumes_dirty_(false) {} @@ -232,6 +272,7 @@ auto NvsStorage::Read() -> void { input_mode_.read(handle_); output_mode_.read(handle_); bt_preferred_.read(handle_); + bt_names_.read(handle_); db_auto_index_.read(handle_); readBtVolumes(); } @@ -251,6 +292,7 @@ auto NvsStorage::Write() -> bool { input_mode_.write(handle_); output_mode_.write(handle_); bt_preferred_.write(handle_); + bt_names_.write(handle_); db_auto_index_.write(handle_); writeBtVolumes(); return nvs_commit(handle_) == ESP_OK; @@ -341,6 +383,47 @@ auto NvsStorage::BluetoothVolume(const bluetooth::mac_addr_t& mac, uint8_t vol) bt_volumes_.Put(mac, vol); } +auto NvsStorage::BluetoothNames() -> std::vector { + std::lock_guard lock{mutex_}; + return bt_names_.get().value_or(std::vector{}); +} + +auto NvsStorage::BluetoothName(const bluetooth::mac_addr_t& mac, + std::optional name) -> void { + std::lock_guard lock{mutex_}; + auto& val = bt_names_.get(); + if (!val) { + val.emplace(); + } + + bool mut = false; + bool found = false; + for (auto it = val->begin(); it != val->end(); it++) { + if (it->mac == mac) { + if (name) { + it->name = *name; + } else { + val->erase(it); + } + found = true; + mut = true; + break; + } + } + + if (!found && name) { + val->push_back(bluetooth::MacAndName{ + .mac = mac, + .name = *name, + }); + mut = true; + } + + if (mut) { + bt_names_.set(*val); + } +} + auto NvsStorage::OutputMode() -> Output { std::lock_guard lock{mutex_}; switch (output_mode_.get().value_or(0xFF)) { diff --git a/src/tangara/app_console/app_console.cpp b/src/tangara/app_console/app_console.cpp index f3593e1b..af9061fe 100644 --- a/src/tangara/app_console/app_console.cpp +++ b/src/tangara/app_console/app_console.cpp @@ -418,28 +418,21 @@ int CmdBtList(int argc, char** argv) { return 1; } - auto devices = AppConsole::sServices->bluetooth().KnownDevices(); + auto devices = AppConsole::sServices->bluetooth().knownDevices(); if (argc == 2) { int index = std::atoi(argv[1]); if (index < 0 || index >= devices.size()) { std::cout << "index out of range" << std::endl; return -1; } - drivers::bluetooth::MacAndName dev{ - .mac = devices[index].address, - .name = {devices[index].name.data(), devices[index].name.size()}, - }; - AppConsole::sServices->bluetooth().SetPreferredDevice(dev); + AppConsole::sServices->bluetooth().pairedDevice(devices[index]); } else { - std::cout << "mac\t\trssi\tname" << std::endl; + std::cout << "mac\t\tname" << std::endl; for (const auto& device : devices) { - for (size_t i = 0; i < device.address.size(); i++) { + for (size_t i = 0; i < device.mac.size(); i++) { std::cout << std::hex << std::setfill('0') << std::setw(2) - << static_cast(device.address[i]); + << static_cast(device.mac[i]); } - float perc = - (static_cast(device.signal_strength) + 127.0) / 256.0 * 100; - std::cout << "\t" << std::fixed << std::setprecision(0) << perc << "%"; std::cout << "\t" << device.name << std::endl; } } diff --git a/src/tangara/audio/audio_fsm.cpp b/src/tangara/audio/audio_fsm.cpp index 24f287ac..fbc38f97 100644 --- a/src/tangara/audio/audio_fsm.cpp +++ b/src/tangara/audio/audio_fsm.cpp @@ -222,7 +222,12 @@ void AudioState::react(const system_fsm::BluetoothEvent& ev) { auto simpleEvent = std::get(ev.event); switch (simpleEvent) { case SimpleEvent::kConnectionStateChanged: { - auto dev = sServices->bluetooth().ConnectedDevice(); + auto bt = sServices->bluetooth(); + if (bt.connectionState() != + drivers::Bluetooth::ConnectionState::kConnected) { + return; + } + auto dev = sServices->bluetooth().pairedDevice(); if (!dev) { return; } @@ -341,7 +346,7 @@ auto AudioState::commitVolume() -> void { if (mode == drivers::NvsStorage::Output::kHeadphones) { sServices->nvs().AmpCurrentVolume(vol); } else if (mode == drivers::NvsStorage::Output::kBluetooth) { - auto dev = sServices->bluetooth().ConnectedDevice(); + auto dev = sServices->bluetooth().pairedDevice(); if (!dev) { return; } @@ -372,7 +377,7 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { sOutput = sI2SOutput; } else { // Ensure Bluetooth gets enabled if it's the default sink. - sServices->bluetooth().Enable(); + sServices->bluetooth().enable(true); sOutput = sBtOutput; } sOutput->mode(IAudioOutput::Modes::kOnPaused); diff --git a/src/tangara/audio/bt_audio_output.cpp b/src/tangara/audio/bt_audio_output.cpp index 616a385f..336fc758 100644 --- a/src/tangara/audio/bt_audio_output.cpp +++ b/src/tangara/audio/bt_audio_output.cpp @@ -13,6 +13,7 @@ #include #include +#include "drivers/bluetooth.hpp" #include "esp_err.h" #include "esp_heap_caps.h" #include "freertos/portmacro.h" @@ -32,6 +33,8 @@ namespace audio { static constexpr uint16_t kVolumeRange = 60; +using ConnectionState = drivers::Bluetooth::ConnectionState; + BluetoothAudioOutput::BluetoothAudioOutput(drivers::Bluetooth& bt, drivers::PcmBuffer& buffer, tasks::WorkerPool& p) @@ -45,9 +48,9 @@ BluetoothAudioOutput::~BluetoothAudioOutput() {} auto BluetoothAudioOutput::changeMode(Modes mode) -> void { if (mode == Modes::kOnPlaying) { - bluetooth_.SetSource(&buffer_); + bluetooth_.source(&buffer_); } else { - bluetooth_.SetSource(nullptr); + bluetooth_.source(nullptr); } } @@ -60,7 +63,7 @@ auto BluetoothAudioOutput::SetVolume(uint16_t v) -> void { bg_worker_.Dispatch([&]() { float factor = pow(10, static_cast(kVolumeRange) * (volume_ - 100) / 100 / 20); - bluetooth_.SetVolumeFactor(factor); + bluetooth_.softVolume(factor); }); } @@ -95,7 +98,7 @@ auto BluetoothAudioOutput::SetVolumeDb(int_fast16_t val) -> bool { } auto BluetoothAudioOutput::AdjustVolumeUp() -> bool { - if (volume_ == 100 || !bluetooth_.IsConnected()) { + if (volume_ == 100) { return false; } volume_++; @@ -104,7 +107,7 @@ auto BluetoothAudioOutput::AdjustVolumeUp() -> bool { } auto BluetoothAudioOutput::AdjustVolumeDown() -> bool { - if (volume_ == 0 || !bluetooth_.IsConnected()) { + if (volume_ == 0) { return false; } volume_--; diff --git a/src/tangara/lua/property.cpp b/src/tangara/lua/property.cpp index 2b93809d..7b4f0b97 100644 --- a/src/tangara/lua/property.cpp +++ b/src/tangara/lua/property.cpp @@ -289,13 +289,14 @@ static void pushTrack(lua_State* L, const audio::TrackInfo& track) { lua_settable(L, -3); } -static void pushDevice(lua_State* L, const drivers::bluetooth::Device& dev) { +static void pushDevice(lua_State* L, + const drivers::bluetooth::MacAndName& dev) { lua_createtable(L, 0, 4); lua_pushliteral(L, "address"); auto* mac = reinterpret_cast( lua_newuserdata(L, sizeof(drivers::bluetooth::mac_addr_t))); - *mac = dev.address; + *mac = dev.mac; lua_rawset(L, -3); // What I just did there was perfectly safe. Look, I can prove it: @@ -308,14 +309,8 @@ static void pushDevice(lua_State* L, const drivers::bluetooth::Device& dev) { lua_pushlstring(L, dev.name.data(), dev.name.size()); lua_rawset(L, -3); - // FIXME: This field deserves a little more structure. - lua_pushliteral(L, "class"); - lua_pushinteger(L, dev.class_of_device); - lua_rawset(L, -3); - - lua_pushliteral(L, "signal_strength"); - lua_pushinteger(L, dev.signal_strength); - lua_rawset(L, -3); + // FIXME: Plumbing through device classes to here could be useful if we ever + // want to show cute little icons. } auto Property::pushValue(lua_State& s) -> int { @@ -332,10 +327,12 @@ auto Property::pushValue(lua_State& s) -> int { lua_pushstring(&s, arg.c_str()); } else if constexpr (std::is_same_v) { pushTrack(&s, arg); - } else if constexpr (std::is_same_v) { + } else if constexpr (std::is_same_v) { pushDevice(&s, arg); } else if constexpr (std::is_same_v< - T, std::vector>) { + T, + std::vector>) { lua_createtable(&s, arg.size(), 0); size_t i = 1; for (const auto& dev : arg) { @@ -364,15 +361,10 @@ auto popRichType(lua_State* L) -> LuaValue { lua_pushliteral(L, "name"); lua_gettable(L, -2); - std::pmr::string name = lua_tostring(L, -1); + std::string name = lua_tostring(L, -1); lua_pop(L, 1); - return drivers::bluetooth::Device{ - .address = mac, - .name = name, - .class_of_device = 0, - .signal_strength = 0, - }; + return drivers::bluetooth::MacAndName{.mac = mac, .name = name}; } return std::monostate{}; diff --git a/src/tangara/lua/property.hpp b/src/tangara/lua/property.hpp index 9f925766..d45821bd 100644 --- a/src/tangara/lua/property.hpp +++ b/src/tangara/lua/property.hpp @@ -24,8 +24,8 @@ using LuaValue = std::variant>; + drivers::bluetooth::MacAndName, + std::vector>; using LuaFunction = std::function; diff --git a/src/tangara/system_fsm/booting.cpp b/src/tangara/system_fsm/booting.cpp index 9d505f81..86993767 100644 --- a/src/tangara/system_fsm/booting.cpp +++ b/src/tangara/system_fsm/booting.cpp @@ -104,8 +104,7 @@ auto Booting::entry() -> void { ESP_LOGI(kTag, "init bluetooth"); sServices->bluetooth(std::make_unique( - sServices->nvs(), sServices->bg_worker())); - sServices->bluetooth().SetEventHandler(bt_event_cb); + sServices->nvs(), sServices->bg_worker(), bt_event_cb)); BootComplete ev{.services = sServices}; events::Audio().Dispatch(ev); diff --git a/src/tangara/ui/ui_fsm.cpp b/src/tangara/ui/ui_fsm.cpp index 7c4147a3..c5ede83c 100644 --- a/src/tangara/ui/ui_fsm.cpp +++ b/src/tangara/ui/ui_fsm.cpp @@ -7,11 +7,13 @@ #include "ui/ui_fsm.hpp" #include +#include #include #include #include #include "FreeRTOSConfig.h" +#include "drivers/bluetooth.hpp" #include "lvgl.h" #include "core/lv_group.h" @@ -100,32 +102,57 @@ lua::Property UiState::sBluetoothEnabled{ if (!std::holds_alternative(val)) { return false; } + // Note we always write the OutputMode NVS change before actually + // modifying the peripheral. We do this because ESP-IDF's Bluetooth stack + // breaks in surprising ways when repeatedly initialised/uninitialised. if (std::get(val)) { sServices->nvs().OutputMode(drivers::NvsStorage::Output::kBluetooth); - sServices->bluetooth().Enable(); + sServices->bluetooth().enable(true); } else { sServices->nvs().OutputMode(drivers::NvsStorage::Output::kHeadphones); - sServices->bluetooth().Disable(); + sServices->bluetooth().enable(false); } events::Audio().Dispatch(audio::OutputModeChanged{}); return true; }}; +lua::Property UiState::sBluetoothConnecting{false}; lua::Property UiState::sBluetoothConnected{false}; + +lua::Property UiState::sBluetoothDiscovering{ + false, [](const lua::LuaValue& val) { + if (!std::holds_alternative(val)) { + return false; + } + // Note we always write the OutputMode NVS change before actually + // modifying the peripheral. We do this because ESP-IDF's Bluetooth stack + // breaks in surprising ways when repeatedly initialised/uninitialised. + if (std::get(val)) { + sServices->bluetooth().discoveryEnabled(true); + } else { + sServices->bluetooth().discoveryEnabled(false); + } + return true; + }}; + lua::Property UiState::sBluetoothPairedDevice{ std::monostate{}, [](const lua::LuaValue& val) { - if (std::holds_alternative(val)) { - auto dev = std::get(val); - sServices->bluetooth().SetPreferredDevice( - drivers::bluetooth::MacAndName{ - .mac = dev.address, - .name = {dev.name.data(), dev.name.size()}, - }); + if (std::holds_alternative(val)) { + auto dev = std::get(val); + sServices->bluetooth().pairedDevice(dev); + } else if (std::holds_alternative(val)) { + sServices->bluetooth().pairedDevice({}); + } else { + // Don't accept any other types. + return false; } - return false; + return true; }}; -lua::Property UiState::sBluetoothDevices{ - std::vector{}}; + +lua::Property UiState::sBluetoothKnownDevices{ + std::vector{}}; +lua::Property UiState::sBluetoothDiscoveredDevices{ + std::vector{}}; lua::Property UiState::sPlaybackPlaying{ false, [](const lua::LuaValue& val) { @@ -412,8 +439,13 @@ void UiState::react(const audio::VolumeLimitChanged& ev) { void UiState::react(const system_fsm::BluetoothEvent& ev) { using drivers::bluetooth::SimpleEvent; + using ConnectionState = drivers::Bluetooth::ConnectionState; + ConnectionState state; auto bt = sServices->bluetooth(); - auto dev = bt.ConnectedDevice(); + + std::optional dev; + std::vector devs; + if (std::holds_alternative(ev.event)) { switch (std::get(ev.event)) { case SimpleEvent::kPlayPause: @@ -438,30 +470,36 @@ void UiState::react(const system_fsm::BluetoothEvent& ev) { break; case SimpleEvent::kFastForward: break; - case SimpleEvent::kKnownDevicesChanged: - sBluetoothDevices.setDirect(bt.KnownDevices()); - break; case SimpleEvent::kConnectionStateChanged: - sBluetoothConnected.setDirect(bt.IsConnected()); + state = bt.connectionState(); + sBluetoothConnected.setDirect(state == ConnectionState::kConnected); + sBluetoothConnecting.setDirect(state == ConnectionState::kConnecting); + break; + case SimpleEvent::kPairedDeviceChanged: + dev = bt.pairedDevice(); if (dev) { - sBluetoothPairedDevice.setDirect(drivers::bluetooth::Device{ - .address = dev->mac, - .name = {dev->name.data(), dev->name.size()}, - .class_of_device = 0, - .signal_strength = 0, - }); + sBluetoothPairedDevice.setDirect(*dev); } else { sBluetoothPairedDevice.setDirect(std::monostate{}); } break; - case SimpleEvent::kPreferredDeviceChanged: + case SimpleEvent::kKnownDevicesChanged: + sBluetoothKnownDevices.setDirect(bt.knownDevices()); + break; + case SimpleEvent::kDiscoveryChanged: + sBluetoothDiscovering.setDirect(bt.discoveryEnabled()); + // Dump the old list of discovered devices when discovery is toggled. + sBluetoothDiscoveredDevices.setDirect(bt.discoveredDevices()); + break; + case SimpleEvent::kDeviceDiscovered: + sBluetoothDiscoveredDevices.setDirect(bt.discoveredDevices()); break; default: break; } } else if (std::holds_alternative( ev.event)) { - // Todo: Do something with this (ie, bt volume alert) + // TODO: Do something with this (ie, bt volume alert) ESP_LOGI( kTag, "Recieved volume changed event with new volume: %d", std::get(ev.event).new_vol); @@ -517,13 +555,16 @@ void Lua::entry() { {"battery_millivolts", &sBatteryMv}, {"plugged_in", &sBatteryCharging}, }); - registry.AddPropertyModule("bluetooth", - { - {"enabled", &sBluetoothEnabled}, - {"connected", &sBluetoothConnected}, - {"paired_device", &sBluetoothPairedDevice}, - {"devices", &sBluetoothDevices}, - }); + registry.AddPropertyModule( + "bluetooth", { + {"enabled", &sBluetoothEnabled}, + {"connected", &sBluetoothConnected}, + {"connecting", &sBluetoothConnecting}, + {"discovering", &sBluetoothDiscovering}, + {"paired_device", &sBluetoothPairedDevice}, + {"discovered_devices", &sBluetoothDiscoveredDevices}, + {"known_devices", &sBluetoothKnownDevices}, + }); registry.AddPropertyModule("playback", { {"playing", &sPlaybackPlaying}, {"track", &sPlaybackTrack}, @@ -601,9 +642,12 @@ void Lua::entry() { sDatabaseAutoUpdate.setDirect(sServices->nvs().DbAutoIndex()); auto bt = sServices->bluetooth(); - sBluetoothEnabled.setDirect(bt.IsEnabled()); - sBluetoothConnected.setDirect(bt.IsConnected()); - sBluetoothDevices.setDirect(bt.KnownDevices()); + sBluetoothEnabled.setDirect(bt.enabled()); + auto paired = bt.pairedDevice(); + if (paired) { + sBluetoothPairedDevice.setDirect(*paired); + } + sBluetoothKnownDevices.setDirect(bt.knownDevices()); if (sServices->sd() == drivers::SdState::kMounted) { sLua->RunScript("/sdcard/config.lua"); diff --git a/src/tangara/ui/ui_fsm.hpp b/src/tangara/ui/ui_fsm.hpp index 7e34db34..72688fa0 100644 --- a/src/tangara/ui/ui_fsm.hpp +++ b/src/tangara/ui/ui_fsm.hpp @@ -102,9 +102,12 @@ class UiState : public tinyfsm::Fsm { static lua::Property sBatteryCharging; static lua::Property sBluetoothEnabled; + static lua::Property sBluetoothConnecting; static lua::Property sBluetoothConnected; + static lua::Property sBluetoothDiscovering; static lua::Property sBluetoothPairedDevice; - static lua::Property sBluetoothDevices; + static lua::Property sBluetoothKnownDevices; + static lua::Property sBluetoothDiscoveredDevices; static lua::Property sPlaybackPlaying; -- cgit v1.2.3 From f78de39a750d58bfe883a789aa6cc4b0a5d9b9e7 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Fri, 12 Jul 2024 14:40:54 +1000 Subject: Give Bluetooth settings a bit of a refresh It's now a bit more responsive to stuff happening, gives you more information, and remembers your previously paired devices for faster switching between them. --- src/drivers/bluetooth.cpp | 24 +++++++++-------- src/drivers/include/drivers/nvs.hpp | 2 +- src/drivers/nvs.cpp | 2 +- src/tangara/lua/property.cpp | 51 +++++++++++++++++++------------------ 4 files changed, 42 insertions(+), 37 deletions(-) (limited to 'src') diff --git a/src/drivers/bluetooth.cpp b/src/drivers/bluetooth.cpp index 2edf5ad9..a0a318e9 100644 --- a/src/drivers/bluetooth.cpp +++ b/src/drivers/bluetooth.cpp @@ -603,16 +603,21 @@ void Disabled::react(const events::Enable&) { void Idle::entry() { ESP_LOGI(kTag, "bt is idle"); + std::invoke(sEventHandler_, SimpleEvent::kConnectionStateChanged); } -void Idle::exit() {} +void Idle::exit() { + std::invoke(sEventHandler_, SimpleEvent::kConnectionStateChanged); +} void Idle::react(const events::Disable& ev) { transit(); } void Idle::react(const events::PairedDeviceChanged& ev) { - connect(*sPairedWith_); + if (sPairedWith_) { + connect(*sPairedWith_); + } } void Idle::react(events::internal::Gap ev) { @@ -633,18 +638,10 @@ void Connecting::entry() { sTimeoutTimer = xTimerCreate("bt_timeout", pdMS_TO_TICKS(15000), false, NULL, timeoutCallback); xTimerStart(sTimeoutTimer, portMAX_DELAY); - - if (sEventHandler_) { - std::invoke(sEventHandler_, SimpleEvent::kConnectionStateChanged); - } } void Connecting::exit() { xTimerDelete(sTimeoutTimer, portMAX_DELAY); - - if (sEventHandler_) { - std::invoke(sEventHandler_, SimpleEvent::kConnectionStateChanged); - } } void Connecting::react(const events::ConnectTimedOut& ev) { @@ -751,12 +748,16 @@ void Connected::entry() { sStorage_->PreferredBluetoothDevice(sPairedWith_); } + std::invoke(sEventHandler_, SimpleEvent::kConnectionStateChanged); + // TODO: if we already have a source, immediately start playing } void Connected::exit() { ESP_LOGI(kTag, "exiting connected state"); esp_a2d_source_disconnect(connected_to_.data()); + + std::invoke(sEventHandler_, SimpleEvent::kConnectionStateChanged); } void Connected::react(const events::Disable& ev) { @@ -765,6 +766,9 @@ void Connected::react(const events::Disable& ev) { void Connected::react(const events::PairedDeviceChanged& ev) { transit(); + if (sPairedWith_) { + connect(*sPairedWith_); + } } void Connected::react(const events::SourceChanged& ev) { diff --git a/src/drivers/include/drivers/nvs.hpp b/src/drivers/include/drivers/nvs.hpp index 2bc77a31..8eb28cc9 100644 --- a/src/drivers/include/drivers/nvs.hpp +++ b/src/drivers/include/drivers/nvs.hpp @@ -34,7 +34,7 @@ class Setting { dirty_ = true; } } - auto get() -> std::optional& { return val_; } + auto get() -> std::optional { return val_; } /* Reads the stored value from NVS and parses it into the correct type. */ auto load(nvs_handle_t) -> std::optional; diff --git a/src/drivers/nvs.cpp b/src/drivers/nvs.cpp index c4d8dedc..e3c4aa06 100644 --- a/src/drivers/nvs.cpp +++ b/src/drivers/nvs.cpp @@ -391,7 +391,7 @@ auto NvsStorage::BluetoothNames() -> std::vector { auto NvsStorage::BluetoothName(const bluetooth::mac_addr_t& mac, std::optional name) -> void { std::lock_guard lock{mutex_}; - auto& val = bt_names_.get(); + auto val = bt_names_.get(); if (!val) { val.emplace(); } diff --git a/src/tangara/lua/property.cpp b/src/tangara/lua/property.cpp index 7b4f0b97..1be1fd2d 100644 --- a/src/tangara/lua/property.cpp +++ b/src/tangara/lua/property.cpp @@ -371,33 +371,34 @@ auto popRichType(lua_State* L) -> LuaValue { } auto Property::popValue(lua_State& s) -> bool { - LuaValue new_val; - switch (lua_type(&s, 2)) { - case LUA_TNIL: - new_val = std::monostate{}; - break; - case LUA_TNUMBER: - if (lua_isinteger(&s, 2)) { - new_val = lua_tointeger(&s, 2); - } else { - new_val = static_cast(std::round(lua_tonumber(&s, 2))); - } - break; - case LUA_TBOOLEAN: - new_val = static_cast(lua_toboolean(&s, 2)); - break; - case LUA_TSTRING: - new_val = lua_tostring(&s, 2); - break; - default: - if (lua_istable(&s, 2)) { - new_val = popRichType(&s); - if (std::holds_alternative(new_val)) { + LuaValue new_val{std::monostate{}}; + if (lua_gettop(&s) >= 2) { + switch (lua_type(&s, 2)) { + case LUA_TNIL: + break; + case LUA_TNUMBER: + if (lua_isinteger(&s, 2)) { + new_val = lua_tointeger(&s, 2); + } else { + new_val = static_cast(std::round(lua_tonumber(&s, 2))); + } + break; + case LUA_TBOOLEAN: + new_val = static_cast(lua_toboolean(&s, 2)); + break; + case LUA_TSTRING: + new_val = lua_tostring(&s, 2); + break; + default: + if (lua_istable(&s, 2)) { + new_val = popRichType(&s); + if (std::holds_alternative(new_val)) { + return false; + } + } else { return false; } - } else { - return false; - } + } } return set(new_val); -- cgit v1.2.3 From 0a271d786be4cc1a1691fa38f184a091721a5251 Mon Sep 17 00:00:00 2001 From: ailurux Date: Tue, 16 Jul 2024 01:23:43 +0000 Subject: daniel/playlist-queue (#83) Reviewed-on: https://codeberg.org/cool-tech-zone/tangara-fw/pulls/83 Reviewed-by: cooljqln Co-authored-by: ailurux Co-committed-by: ailurux --- src/tangara/audio/audio_fsm.cpp | 20 ++-- src/tangara/audio/playlist.cpp | 154 ++++++++++++++++++++++++++++++ src/tangara/audio/playlist.hpp | 56 +++++++++++ src/tangara/audio/track_queue.cpp | 158 +++++++++++-------------------- src/tangara/audio/track_queue.hpp | 24 ++--- src/tangara/system_fsm/booting.cpp | 2 +- src/tangara/test/CMakeLists.txt | 2 +- src/tangara/test/audio/test_playlist.cpp | 86 +++++++++++++++++ src/tangara/ui/ui_fsm.cpp | 6 +- 9 files changed, 375 insertions(+), 133 deletions(-) create mode 100644 src/tangara/audio/playlist.cpp create mode 100644 src/tangara/audio/playlist.hpp create mode 100644 src/tangara/test/audio/test_playlist.cpp (limited to 'src') diff --git a/src/tangara/audio/audio_fsm.cpp b/src/tangara/audio/audio_fsm.cpp index fbc38f97..65261d75 100644 --- a/src/tangara/audio/audio_fsm.cpp +++ b/src/tangara/audio/audio_fsm.cpp @@ -96,9 +96,7 @@ void AudioState::react(const QueueUpdate& ev) { }; auto current = sServices->track_queue().current(); - if (current) { - cmd.new_track = *current; - } + cmd.new_track = current; switch (ev.reason) { case QueueUpdate::kExplicitUpdate: @@ -176,18 +174,21 @@ void AudioState::react(const internal::DecodingFinished& ev) { sServices->bg_worker().Dispatch([=]() { auto& queue = sServices->track_queue(); auto current = queue.current(); - if (!current) { + if (std::holds_alternative(current)) { return; } auto db = sServices->database().lock(); if (!db) { return; } - auto path = db->getTrackPath(*current); - if (!path) { - return; + std::string path; + if (std::holds_alternative(current)) { + path = std::get(current); + } else if (std::holds_alternative(current)) { + auto tid = std::get(current); + path = db->getTrackPath(tid).value_or(""); } - if (*path == ev.track->uri) { + if (path == ev.track->uri) { queue.finish(); } }); @@ -449,6 +450,9 @@ void Standby::react(const system_fsm::SdStateChanged& ev) { return; } + // Open the queue file + sServices->track_queue().open(); + // Restore the currently playing file before restoring the queue. This way, // we can fall back to restarting the queue's current track if there's any // issue restoring the current file. diff --git a/src/tangara/audio/playlist.cpp b/src/tangara/audio/playlist.cpp new file mode 100644 index 00000000..850b7335 --- /dev/null +++ b/src/tangara/audio/playlist.cpp @@ -0,0 +1,154 @@ +/* + * Copyright 2024 ailurux + * + * SPDX-License-Identifier: GPL-3.0-only + */ +#include "playlist.hpp" + +#include + +#include "audio/playlist.hpp" +#include "database/database.hpp" +#include "esp_log.h" +#include "ff.h" + +namespace audio { +[[maybe_unused]] static constexpr char kTag[] = "playlist"; + +Playlist::Playlist(std::string playlistFilepath) + : filepath_(playlistFilepath), mutex_(), total_size_(0), pos_(0) {} + +auto Playlist::open() -> bool { + 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; + } + // Count all entries + consumeAndCount(-1); + // Grab the first one + skipTo(0); + return true; +} + +Playlist::~Playlist() { + f_close(&file_); +} + +auto Playlist::currentPosition() const -> size_t { + return pos_; +} + +auto Playlist::size() const -> size_t { + return total_size_; +} + +auto Playlist::append(Item i) -> void { + std::unique_lock lock(mutex_); + auto offset = f_tell(&file_); + // Seek to end and append + auto res = f_lseek(&file_, f_size(&file_)); + if (res != FR_OK) { + ESP_LOGE(kTag, "Seek to end of file failed? Error %d", res); + return; + } + // TODO: Resolve paths for track id, etc + std::string path; + if (std::holds_alternative(i)) { + path = std::get(i); + f_printf(&file_, "%s\n", path.c_str()); + total_size_++; + if (current_value_.empty()) { + current_value_ = path; + } + } + // Restore position + res = f_lseek(&file_, offset); + if (res != FR_OK) { + ESP_LOGE(kTag, "Failed to restore file position after append?"); + return; + } + res = f_sync(&file_); + if (res != FR_OK) { + ESP_LOGE(kTag, "Failed to sync playlist file after append"); + return; + } +} + +auto Playlist::skipTo(size_t position) -> void { + pos_ = position; + consumeAndCount(position); +} + +auto Playlist::next() -> void { + if (!atEnd()) { + pos_++; + skipTo(pos_); + } +} + +auto Playlist::prev() -> void { + // Naive approach to see how that goes for now + pos_--; + skipTo(pos_); +} + +auto Playlist::value() const -> std::string { + return current_value_; +} + +auto Playlist::clear() -> bool { + auto res = f_close(&file_); + if (res != FR_OK) { + return false; + } + res = + f_open(&file_, filepath_.c_str(), FA_READ | FA_WRITE | FA_CREATE_ALWAYS); + if (res != FR_OK) { + return false; + } + total_size_ = 0; + current_value_.clear(); + pos_ = 0; + return true; +} + +auto Playlist::atEnd() -> bool { + return pos_ + 1 >= total_size_; +} + +auto Playlist::filepath() -> std::string { + return filepath_; +} + +auto Playlist::consumeAndCount(ssize_t upto) -> bool { + std::unique_lock lock(mutex_); + TCHAR buff[512]; + size_t count = 0; + f_rewind(&file_); + while (!f_eof(&file_)) { + // TODO: Correctly handle lines longer than this + // TODO: Also correctly handle the case where the last entry doesn't end in + // \n + auto res = f_gets(buff, 512, &file_); + if (res == NULL) { + ESP_LOGW(kTag, "Error consuming playlist file at line %d", count); + return false; + } + count++; + + if (upto >= 0 && count > upto) { + size_t len = strlen(buff); + current_value_.assign(buff, len - 1); + break; + } + } + if (upto < 0) { + total_size_ = count; + f_rewind(&file_); + } + return true; +} + +} // namespace audio \ No newline at end of file diff --git a/src/tangara/audio/playlist.hpp b/src/tangara/audio/playlist.hpp new file mode 100644 index 00000000..b278d8a6 --- /dev/null +++ b/src/tangara/audio/playlist.hpp @@ -0,0 +1,56 @@ + +/* + * Copyright 2024 ailurux + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once +#include +#include +#include "database/database.hpp" +#include "database/track.hpp" +#include "ff.h" + +namespace audio { + +/* + * Owns and manages a playlist file. + * Each line in the playlist file is the absolute filepath of the track to play. + * In order to avoid mapping to byte offsets, each line must contain only a + * filepath (ie, no comments are supported). This limitation may be removed + * later if benchmarks show that the file can be quickly scanned from 'bookmark' + * offsets. This is a subset of the m3u format and ideally will be + * import/exportable to and from this format, to better support playlists from + * beets import and other music management software. + */ +class Playlist { + public: + Playlist(std::string playlistFilepath); + ~Playlist(); + using Item = + std::variant; + auto open() -> bool; + auto currentPosition() const -> size_t; + auto size() const -> size_t; + auto append(Item i) -> void; + auto skipTo(size_t position) -> void; + auto next() -> void; + auto prev() -> void; + auto value() const -> std::string; + auto clear() -> bool; + auto atEnd() -> bool; + auto filepath() -> std::string; + + private: + std::string filepath_; + std::mutex mutex_; + size_t total_size_; + size_t pos_; + FIL file_; + std::string current_value_; + + auto consumeAndCount(ssize_t upto) -> bool; +}; + +} // namespace audio \ No newline at end of file diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index 603b0de1..1689f06a 100644 --- a/src/tangara/audio/track_queue.cpp +++ b/src/tangara/audio/track_queue.cpp @@ -28,6 +28,7 @@ #include "memory_resource.hpp" #include "tasks.hpp" #include "ui/ui_fsm.hpp" +#include "track_queue.hpp" namespace audio { @@ -83,65 +84,67 @@ auto notifyChanged(bool current_changed, Reason reason) -> void { events::Audio().Dispatch(ev); } -TrackQueue::TrackQueue(tasks::WorkerPool& bg_worker) +TrackQueue::TrackQueue(tasks::WorkerPool& bg_worker, database::Handle db) : mutex_(), bg_worker_(bg_worker), - pos_(0), - tracks_(&memory::kSpiRamResource), + db_(db), + playlist_("queue.playlist"), // TODO shuffle_(), repeat_(false), replay_(false) {} -auto TrackQueue::current() const -> std::optional { +auto TrackQueue::current() const -> TrackItem { const std::shared_lock lock(mutex_); - if (pos_ >= tracks_.size()) { + std::string val = playlist_.value(); + if (val.empty()) { return {}; } - return tracks_[pos_]; + return val; } -auto TrackQueue::peekNext(std::size_t limit) const - -> std::vector { +auto TrackQueue::currentPosition() const -> size_t { 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 out; + return playlist_.currentPosition(); } -auto TrackQueue::peekPrevious(std::size_t limit) const - -> std::vector { +auto TrackQueue::totalSize() const -> size_t { 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 out; + return playlist_.size(); } -auto TrackQueue::currentPosition() const -> size_t { - const std::shared_lock lock(mutex_); - return pos_; +auto TrackQueue::open() -> bool { + // FIX ME: If playlist opening fails, should probably fall back to a vector of tracks or something + // so that we're not necessarily always needing mounted storage + return playlist_.open(); } -auto TrackQueue::totalSize() const -> size_t { - const std::shared_lock lock(mutex_); - return tracks_.size(); +auto TrackQueue::getFilepath(database::TrackId id) -> std::optional { + auto db = db_.lock(); + if (!db) { + return {}; + } + return db->getTrackPath(id); } + +// TODO WIP: Atm only appends are allowed, this will only ever append regardless of what index +// is given. But it is kept like this for compatability for now. auto TrackQueue::insert(Item i, size_t index) -> void { + append(i); +} + +auto TrackQueue::append(Item i) -> void { bool was_queue_empty; bool current_changed; { const std::shared_lock lock(mutex_); - was_queue_empty = pos_ == tracks_.size(); - current_changed = was_queue_empty || index == pos_; + was_queue_empty = playlist_.currentPosition() >= playlist_.size(); + current_changed = was_queue_empty; // Dont support inserts yet } auto update_shuffler = [=, this]() { if (shuffle_) { - shuffle_->resize(tracks_.size()); + shuffle_->resize(playlist_.size()); // If there wasn't anything already playing, then we should make sure we // begin playback at a random point, instead of always starting with // whatever was inserted first and *then* shuffling. @@ -149,7 +152,7 @@ auto TrackQueue::insert(Item i, size_t index) -> void { // 'play this track now' (by inserting at the current pos) to work even // when shuffling is enabled. if (was_queue_empty) { - pos_ = shuffle_->current(); + playlist_.skipTo(shuffle_->current()); } } }; @@ -157,10 +160,11 @@ auto TrackQueue::insert(Item i, size_t index) -> void { if (std::holds_alternative(i)) { { const std::unique_lock lock(mutex_); - if (index <= tracks_.size()) { - tracks_.insert(tracks_.begin() + index, std::get(i)); - update_shuffler(); + auto filename = getFilepath(std::get(i)); + if (filename) { + playlist_.append(*filename); } + update_shuffler(); } notifyChanged(current_changed, Reason::kExplicitUpdate); } else if (std::holds_alternative(i)) { @@ -169,7 +173,6 @@ auto TrackQueue::insert(Item i, size_t index) -> void { // doesn't block. bg_worker_.Dispatch([=, this]() { database::TrackIterator it = std::get(i); - size_t working_pos = index; while (true) { auto next = *it; if (!next) { @@ -179,11 +182,11 @@ auto TrackQueue::insert(Item i, size_t index) -> void { // like current(). { const std::unique_lock lock(mutex_); - if (working_pos <= tracks_.size()) { - tracks_.insert(tracks_.begin() + working_pos, *next); + auto filename = *getFilepath(*next); + if (!filename.empty()) { + playlist_.append(filename); } } - working_pos++; it++; } { @@ -195,15 +198,6 @@ auto TrackQueue::insert(Item i, size_t index) -> void { } } -auto TrackQueue::append(Item i) -> void { - size_t end; - { - const std::shared_lock lock(mutex_); - end = tracks_.size(); - } - insert(i, end); -} - auto TrackQueue::next() -> void { next(Reason::kExplicitUpdate); } @@ -215,17 +209,16 @@ auto TrackQueue::next(Reason r) -> void { const std::unique_lock lock(mutex_); if (shuffle_) { shuffle_->next(); - pos_ = shuffle_->current(); + playlist_.skipTo(shuffle_->current()); } else { - if (pos_ + 1 >= tracks_.size()) { + if (playlist_.atEnd()) { if (replay_) { - pos_ = 0; + playlist_.skipTo(0); } else { - pos_ = tracks_.size(); changed = false; } } else { - pos_++; + playlist_.next(); } } } @@ -240,16 +233,16 @@ auto TrackQueue::previous() -> void { const std::unique_lock lock(mutex_); if (shuffle_) { shuffle_->prev(); - pos_ = shuffle_->current(); + playlist_.skipTo(shuffle_->current()); } else { - if (pos_ == 0) { + if (playlist_.currentPosition() == 0) { if (repeat_) { - pos_ = tracks_.size() - 1; + playlist_.skipTo(playlist_.size()-1); } else { changed = false; } } else { - pos_--; + playlist_.prev(); } } } @@ -265,39 +258,10 @@ auto TrackQueue::finish() -> void { } } -auto TrackQueue::skipTo(database::TrackId id) -> void { - // Defer this work to the background not because it's particularly - // long-running (although it could be), but because we want to ensure we - // only search for the given id after any previously pending iterator - // insertions have finished. - bg_worker_.Dispatch([=, this]() { - bool found = false; - { - const std::unique_lock lock(mutex_); - for (size_t i = 0; i < tracks_.size(); i++) { - if (tracks_[i] == id) { - pos_ = i; - found = true; - break; - } - } - } - if (found) { - notifyChanged(true, Reason::kExplicitUpdate); - } - }); -} - auto TrackQueue::clear() -> void { { const std::unique_lock lock(mutex_); - if (tracks_.empty()) { - return; - } - - pos_ = 0; - tracks_.clear(); - + playlist_.clear(); if (shuffle_) { shuffle_->resize(0); } @@ -309,10 +273,8 @@ auto TrackQueue::clear() -> void { auto TrackQueue::random(bool en) -> void { { const std::unique_lock lock(mutex_); - // Don't check for en == true already; this has the side effect that - // repeated calls with en == true will re-shuffle. if (en) { - shuffle_.emplace(tracks_.size()); + shuffle_.emplace(playlist_.size()); shuffle_->replay(replay_); } else { shuffle_.reset(); @@ -360,14 +322,11 @@ auto TrackQueue::replay() const -> bool { auto TrackQueue::serialise() -> std::string { cppbor::Array tracks{}; - for (database::TrackId track : tracks_) { - tracks.add(cppbor::Uint(track)); - } cppbor::Map encoded; encoded.add(cppbor::Uint{0}, cppbor::Array{ - cppbor::Uint{pos_}, cppbor::Bool{repeat_}, cppbor::Bool{replay_}, + cppbor::Uint{playlist_.currentPosition()}, }); if (shuffle_) { encoded.add(cppbor::Uint{1}, cppbor::Array{ @@ -376,7 +335,6 @@ auto TrackQueue::serialise() -> std::string { cppbor::Uint{shuffle_->pos()}, }); } - encoded.add(cppbor::Uint{2}, std::move(tracks)); return encoded.toString(); } @@ -401,9 +359,6 @@ cppbor::ParseClient* TrackQueue::QueueParseClient::item( case 1: state_ = State::kShuffle; break; - case 2: - state_ = State::kTracks; - break; default: state_ = State::kFinished; } @@ -412,7 +367,8 @@ cppbor::ParseClient* TrackQueue::QueueParseClient::item( if (item->type() == cppbor::ARRAY) { i_ = 0; } else if (item->type() == cppbor::UINT) { - queue_.pos_ = item->asUint()->unsignedValue(); + auto val = item->asUint()->unsignedValue(); + queue_.playlist_.skipTo(val); } else if (item->type() == cppbor::SIMPLE) { bool val = item->asBool()->value(); if (i_ == 0) { @@ -444,10 +400,6 @@ cppbor::ParseClient* TrackQueue::QueueParseClient::item( } i_++; } - } else if (state_ == State::kTracks) { - if (item->type() == cppbor::UINT) { - queue_.tracks_.push_back(item->asUint()->unsignedValue()); - } } else if (state_ == State::kFinished) { } return this; @@ -470,10 +422,6 @@ cppbor::ParseClient* TrackQueue::QueueParseClient::itemEnd( if (item->type() == cppbor::ARRAY) { state_ = State::kRoot; } - } else if (state_ == State::kTracks) { - if (item->type() == cppbor::ARRAY) { - state_ = State::kRoot; - } } else if (state_ == State::kFinished) { } return this; diff --git a/src/tangara/audio/track_queue.hpp b/src/tangara/audio/track_queue.hpp index 427d5f75..6f50f162 100644 --- a/src/tangara/audio/track_queue.hpp +++ b/src/tangara/audio/track_queue.hpp @@ -17,6 +17,7 @@ #include "database/database.hpp" #include "database/track.hpp" #include "tasks.hpp" +#include "playlist.hpp" namespace audio { @@ -64,22 +65,15 @@ class RandomIterator { */ class TrackQueue { public: - TrackQueue(tasks::WorkerPool& bg_worker); + TrackQueue(tasks::WorkerPool& bg_worker, database::Handle db); /* Returns the currently playing track. */ - 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; - - /* - * 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; + using TrackItem = std::variant; + auto current() const -> TrackItem; auto currentPosition() const -> size_t; auto totalSize() const -> size_t; + auto open() -> bool; using Item = std::variant; auto insert(Item, size_t index = 0) -> void; @@ -97,8 +91,6 @@ class TrackQueue { */ auto finish() -> void; - auto skipTo(database::TrackId) -> void; - /* * Removes all tracks from all queues, and stops any currently playing track. */ @@ -122,13 +114,14 @@ class TrackQueue { private: auto next(QueueUpdate::Reason r) -> void; + auto getFilepath(database::TrackId id) -> std::optional; mutable std::shared_mutex mutex_; tasks::WorkerPool& bg_worker_; + database::Handle db_; - size_t pos_; - std::pmr::vector tracks_; + Playlist playlist_; std::optional shuffle_; bool repeat_; @@ -159,7 +152,6 @@ class TrackQueue { kRoot, kMetadata, kShuffle, - kTracks, kFinished, }; State state_; diff --git a/src/tangara/system_fsm/booting.cpp b/src/tangara/system_fsm/booting.cpp index 86993767..a3fed9fa 100644 --- a/src/tangara/system_fsm/booting.cpp +++ b/src/tangara/system_fsm/booting.cpp @@ -97,7 +97,7 @@ auto Booting::entry() -> void { sServices->samd(), std::unique_ptr(adc))); sServices->track_queue( - std::make_unique(sServices->bg_worker())); + std::make_unique(sServices->bg_worker(), sServices->database())); sServices->tag_parser(std::make_unique()); sServices->collator(locale::CreateCollator()); sServices->tts(std::make_unique()); diff --git a/src/tangara/test/CMakeLists.txt b/src/tangara/test/CMakeLists.txt index 728c06b0..58882f9f 100644 --- a/src/tangara/test/CMakeLists.txt +++ b/src/tangara/test/CMakeLists.txt @@ -3,5 +3,5 @@ # SPDX-License-Identifier: GPL-3.0-only idf_component_register( - SRC_DIRS "battery" + SRC_DIRS "battery" "audio" INCLUDE_DIRS "." REQUIRES catch2 cmock tangara fixtures) diff --git a/src/tangara/test/audio/test_playlist.cpp b/src/tangara/test/audio/test_playlist.cpp new file mode 100644 index 00000000..147b3ac0 --- /dev/null +++ b/src/tangara/test/audio/test_playlist.cpp @@ -0,0 +1,86 @@ +/* + * Copyright 2023 ailurux + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "audio/playlist.hpp" + +#include + +#include +#include +#include + +#include "catch2/catch.hpp" + +#include "drivers/gpios.hpp" +#include "drivers/i2c.hpp" +#include "drivers/storage.hpp" +#include "drivers/spi.hpp" +#include "i2c_fixture.hpp" +#include "spi_fixture.hpp" +#include "ff.h" + +namespace audio { + +static const std::string kTestFilename = "test_playlist2.m3u"; +static const std::string kTestFilePath = kTestFilename; + +TEST_CASE("playlist file", "[integration]") { + I2CFixture i2c; + SpiFixture spi; + std::unique_ptr gpios{drivers::Gpios::Create(false)}; + + if (gpios->Get(drivers::IGpios::Pin::kSdCardDetect)) { + // Skip if nothing is inserted. + SKIP("no sd card detected; skipping storage tests"); + return; + } + + { + std::unique_ptr result(drivers::SdStorage::Create(*gpios).value()); + Playlist plist(kTestFilePath); + REQUIRE(plist.clear()); + + SECTION("write to the playlist file") { + plist.append("test1.mp3"); + plist.append("test2.mp3"); + plist.append("test3.mp3"); + plist.append("test4.wav"); + plist.append("directory/test1.mp3"); + plist.append("directory/test2.mp3"); + plist.append("a/really/long/directory/test1.mp3"); + plist.append("directory/and/another/test2.mp3"); + REQUIRE(plist.size() == 8); + + SECTION("read from the playlist file") { + Playlist plist2(kTestFilePath); + REQUIRE(plist2.size() == 8); + REQUIRE(plist2.value() == "test1.mp3"); + plist2.next(); + REQUIRE(plist2.value() == "test2.mp3"); + plist2.prev(); + REQUIRE(plist2.value() == "test1.mp3"); + } + } + + BENCHMARK("appending item") { + plist.append("A/New/Item.wav"); + }; + + BENCHMARK("opening playlist file") { + Playlist plist2(kTestFilePath); + REQUIRE(plist2.size() > 100); + return plist2.size(); + }; + + BENCHMARK("opening playlist file and appending entry") { + Playlist plist2(kTestFilePath); + REQUIRE(plist2.size() > 100); + plist2.append("A/Nother/New/Item.opus"); + return plist2.size(); + }; + } +} +} // namespace audio diff --git a/src/tangara/ui/ui_fsm.cpp b/src/tangara/ui/ui_fsm.cpp index c5ede83c..476732db 100644 --- a/src/tangara/ui/ui_fsm.cpp +++ b/src/tangara/ui/ui_fsm.cpp @@ -398,10 +398,12 @@ void UiState::react(const system_fsm::BatteryStateChanged& ev) { void UiState::react(const audio::QueueUpdate&) { auto& queue = sServices->track_queue(); - sQueueSize.setDirect(static_cast(queue.totalSize())); + auto queue_size = queue.totalSize(); + sQueueSize.setDirect(static_cast(queue_size)); int current_pos = queue.currentPosition(); - if (queue.current()) { + // If there is nothing in the queue, the position should be 0, otherwise, add one because lua + if (queue_size > 0) { current_pos++; } sQueuePosition.setDirect(current_pos); -- cgit v1.2.3 From bc2527135a2ae4b905015bd6d0fa105cda200b8e Mon Sep 17 00:00:00 2001 From: ailurux Date: Tue, 16 Jul 2024 14:39:01 +1000 Subject: Fix std::optional access --- src/tangara/audio/track_queue.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index 1689f06a..1aeecf8a 100644 --- a/src/tangara/audio/track_queue.cpp +++ b/src/tangara/audio/track_queue.cpp @@ -160,9 +160,9 @@ auto TrackQueue::append(Item i) -> void { if (std::holds_alternative(i)) { { const std::unique_lock lock(mutex_); - auto filename = getFilepath(std::get(i)); - if (filename) { - playlist_.append(*filename); + auto filename = getFilepath(std::get(i)).value_or(""); + if (!filename.empty()) { + playlist_.append(filename); } update_shuffler(); } @@ -182,7 +182,7 @@ auto TrackQueue::append(Item i) -> void { // like current(). { const std::unique_lock lock(mutex_); - auto filename = *getFilepath(*next); + auto filename = getFilepath(*next).value_or(""); if (!filename.empty()) { playlist_.append(filename); } -- cgit v1.2.3 From a3f48074fb2870535184e90f8aeda2e98c19d24e Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 17 Jul 2024 13:39:14 +1000 Subject: Add a console command to dump a snapshot --- src/tangara/app_console/app_console.cpp | 21 +++++++++++++++++++++ src/tangara/ui/ui_events.hpp | 4 ++++ src/tangara/ui/ui_fsm.cpp | 26 ++++++++++++++++++++++++++ src/tangara/ui/ui_fsm.hpp | 1 + 4 files changed, 52 insertions(+) (limited to 'src') diff --git a/src/tangara/app_console/app_console.cpp b/src/tangara/app_console/app_console.cpp index af9061fe..11862143 100644 --- a/src/tangara/app_console/app_console.cpp +++ b/src/tangara/app_console/app_console.cpp @@ -683,6 +683,26 @@ void RegisterLua() { esp_console_cmd_register(&cmd_luarun); } +int CmdSnapshot(int argc, char** argv) { + if (argc != 2) { + std::cout << "snapshot expects 1 argument" << std::endl; + return 1; + } + + events::Ui().Dispatch(ui::Screenshot{.filename = argv[1]}); + return 0; +} + +void RegisterSnapshot() { + esp_console_cmd_t cmd_snapshot{ + .command = "snapshot", + .help = "Saves a screenshot of the display to a file", + .hint = "filename", + .func = &CmdSnapshot, + .argtable = NULL}; + esp_console_cmd_register(&cmd_snapshot); +} + auto AppConsole::PrerunCallback() -> void { Console::PrerunCallback(); esp_log_level_set("*", ESP_LOG_NONE); @@ -713,6 +733,7 @@ auto AppConsole::RegisterExtraComponents() -> void { RegisterHapticEffect(); RegisterLua(); + RegisterSnapshot(); } } // namespace console diff --git a/src/tangara/ui/ui_events.hpp b/src/tangara/ui/ui_events.hpp index 05fd4483..0f371769 100644 --- a/src/tangara/ui/ui_events.hpp +++ b/src/tangara/ui/ui_events.hpp @@ -30,6 +30,10 @@ struct OnLuaError : tinyfsm::Event { struct DumpLuaStack : tinyfsm::Event {}; +struct Screenshot : tinyfsm::Event { + std::string filename; +}; + namespace internal { struct InitDisplay : tinyfsm::Event { diff --git a/src/tangara/ui/ui_fsm.cpp b/src/tangara/ui/ui_fsm.cpp index 476732db..4f93fe61 100644 --- a/src/tangara/ui/ui_fsm.cpp +++ b/src/tangara/ui/ui_fsm.cpp @@ -13,6 +13,7 @@ #include #include "FreeRTOSConfig.h" +#include "draw/lv_draw_buf.h" #include "drivers/bluetooth.hpp" #include "lvgl.h" @@ -26,6 +27,9 @@ #include "freertos/projdefs.h" #include "lua.hpp" #include "luavgl.h" +#include "misc/lv_color.h" +#include "misc/lv_utils.h" +#include "others/snapshot/lv_snapshot.h" #include "tick/lv_tick.h" #include "tinyfsm.hpp" @@ -363,6 +367,28 @@ int UiState::PopScreen() { return sScreens.size(); } +void UiState::react(const Screenshot& ev) { + if (!sCurrentScreen) { + return; + } + ESP_LOGI(kTag, "taking snapshot"); + lv_draw_buf_t* buf = + lv_snapshot_take(sCurrentScreen->root(), LV_COLOR_FORMAT_RGB888); + if (!buf) { + ESP_LOGW(kTag, "snapshot failed"); + return; + } + ESP_LOGI(kTag, "writing to file"); + std::string fullpath = "//sdcard/" + ev.filename; + auto res = lv_draw_buf_save_to_file(buf, fullpath.c_str()); + lv_draw_buf_destroy(buf); + if (res == LV_RESULT_OK) { + ESP_LOGI(kTag, "write okay!"); + } else { + ESP_LOGE(kTag, "write failed!"); + } +} + void UiState::react(const system_fsm::KeyLockChanged& ev) { sDisplay->SetDisplayOn(!ev.locking); sInput->lock(ev.locking); diff --git a/src/tangara/ui/ui_fsm.hpp b/src/tangara/ui/ui_fsm.hpp index 72688fa0..cef9a13a 100644 --- a/src/tangara/ui/ui_fsm.hpp +++ b/src/tangara/ui/ui_fsm.hpp @@ -53,6 +53,7 @@ class UiState : public tinyfsm::Fsm { /* Fallback event handler. Does nothing. */ void react(const tinyfsm::Event& ev) {} + void react(const Screenshot&); virtual void react(const OnLuaError&) {} virtual void react(const DumpLuaStack&) {} virtual void react(const internal::BackPressed&) {} -- cgit v1.2.3 From 2ab459598c60b7df0d4b0dddf4a1e7c71ae985a1 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 17 Jul 2024 17:22:59 +1000 Subject: Encode snapshots as PNGs when saving to disk --- src/tangara/ui/screenshot.cpp | 48 +++++++++++++++++++++++++++++++++++++++++++ src/tangara/ui/screenshot.hpp | 17 +++++++++++++++ src/tangara/ui/ui_fsm.cpp | 18 ++-------------- 3 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 src/tangara/ui/screenshot.cpp create mode 100644 src/tangara/ui/screenshot.hpp (limited to 'src') diff --git a/src/tangara/ui/screenshot.cpp b/src/tangara/ui/screenshot.cpp new file mode 100644 index 00000000..e4f9cc3f --- /dev/null +++ b/src/tangara/ui/screenshot.cpp @@ -0,0 +1,48 @@ +/* + * Copyright 2024 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "screenshot.hpp" +#include + +#include + +#define LODEPNG_NO_COMPILE_CPP +#include "libs/lodepng/lodepng.h" + +#include "esp_log.h" +#include "lvgl.h" + +namespace ui { + +[[maybe_unused]] static constexpr char kTag[] = "screenshot"; + +auto SaveScreenshot(lv_obj_t* obj, const std::string& path) -> void { + lv_draw_buf_t* buf = lv_snapshot_take(obj, LV_COLOR_FORMAT_RGB888); + if (!buf) { + return; + } + + // LVGL appears to output BGR data instead. Not quite sure why, but swapping + // each pair is quite easy. + for (size_t i = 0; i < buf->data_size; i += 3) { + uint8_t temp = buf->data[i]; + buf->data[i] = buf->data[i + 2]; + buf->data[i + 2] = temp; + } + + // The LVGL lodepng fork uses LVGL's file API, so an extra '/' is needed. + std::string fullpath = "//sdcard/" + path; + + auto res = lodepng_encode_file(fullpath.c_str(), buf->data, buf->header.w, + buf->header.h, LCT_RGB, 8); + + lv_draw_buf_destroy(buf); + if (res != 0) { + ESP_LOGE(kTag, "lodepng error: '%s'", lodepng_error_text(res)); + } +} + +} // namespace ui diff --git a/src/tangara/ui/screenshot.hpp b/src/tangara/ui/screenshot.hpp new file mode 100644 index 00000000..d19be0f0 --- /dev/null +++ b/src/tangara/ui/screenshot.hpp @@ -0,0 +1,17 @@ +/* + * Copyright 2024 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include + +#include "lvgl.h" + +namespace ui { + +auto SaveScreenshot(lv_obj_t* obj, const std::string& path) -> void; + +} diff --git a/src/tangara/ui/ui_fsm.cpp b/src/tangara/ui/ui_fsm.cpp index 4f93fe61..cd39dc9c 100644 --- a/src/tangara/ui/ui_fsm.cpp +++ b/src/tangara/ui/ui_fsm.cpp @@ -65,6 +65,7 @@ #include "ui/screen.hpp" #include "ui/screen_lua.hpp" #include "ui/screen_splash.hpp" +#include "ui/screenshot.hpp" #include "ui/ui_events.hpp" namespace ui { @@ -371,22 +372,7 @@ void UiState::react(const Screenshot& ev) { if (!sCurrentScreen) { return; } - ESP_LOGI(kTag, "taking snapshot"); - lv_draw_buf_t* buf = - lv_snapshot_take(sCurrentScreen->root(), LV_COLOR_FORMAT_RGB888); - if (!buf) { - ESP_LOGW(kTag, "snapshot failed"); - return; - } - ESP_LOGI(kTag, "writing to file"); - std::string fullpath = "//sdcard/" + ev.filename; - auto res = lv_draw_buf_save_to_file(buf, fullpath.c_str()); - lv_draw_buf_destroy(buf); - if (res == LV_RESULT_OK) { - ESP_LOGI(kTag, "write okay!"); - } else { - ESP_LOGE(kTag, "write failed!"); - } + SaveScreenshot(sCurrentScreen->root(), ev.filename); } void UiState::react(const system_fsm::KeyLockChanged& ev) { -- cgit v1.2.3 From f00e1d74931f74d66f172e78f669309a5b60e7ba Mon Sep 17 00:00:00 2001 From: jacqueline Date: Fri, 19 Jul 2024 16:11:22 +1000 Subject: Fix track ids containing '\n' not decoding properly This has been the cause of the elusive "selecting a track opens it like an index" bug :) --- src/tangara/database/records.cpp | 10 ++++++---- src/util/include/debug.hpp | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/tangara/database/records.cpp b/src/tangara/database/records.cpp index 88ddbd91..3e76ecad 100644 --- a/src/tangara/database/records.cpp +++ b/src/tangara/database/records.cpp @@ -19,6 +19,7 @@ #include "cppbor.h" #include "cppbor_parse.h" +#include "debug.hpp" #include "esp_log.h" #include "database/index.hpp" @@ -229,16 +230,16 @@ auto ParseIndexKey(const leveldb::Slice& slice) -> std::optional { std::istringstream in(key_data.substr(header_length + 1)); std::stringbuf buffer{}; + // FIXME: what if the item contains a '\0'? Probably we make a big mess. in.get(buffer, kFieldSeparator); if (buffer.str().size() > 0) { result.item = buffer.str(); } - buffer = {}; - in.get(buffer); - std::string id_str = buffer.str(); + std::string id_str = + key_data.substr(header_length + 1 + buffer.str().size() + 1); if (id_str.size() > 1) { - result.track = BytesToTrackId(id_str.substr(1)); + result.track = BytesToTrackId(id_str); } return result; @@ -252,6 +253,7 @@ auto BytesToTrackId(std::span bytes) -> std::optional { auto [res, unused, err] = cppbor::parse( reinterpret_cast(bytes.data()), bytes.size()); if (!res || res->type() != cppbor::UINT) { + ESP_LOGE(kTag, "Track ID parsing failed!!"); return {}; } return res->asUint()->unsignedValue(); diff --git a/src/util/include/debug.hpp b/src/util/include/debug.hpp index 27fb2999..06e3833e 100644 --- a/src/util/include/debug.hpp +++ b/src/util/include/debug.hpp @@ -31,8 +31,8 @@ inline std::string format_hex_string(std::span data) { oss << " "; } int byte_val = (int)byte; - oss << "[0x" << std::uppercase << std::setfill('0') << std::setw(2) - << std::hex << byte_val << ']'; + oss << std::uppercase << std::setfill('0') << std::setw(2) << std::hex + << byte_val << ' '; if (byte_val >= 32 && byte_val < 127) { ascii_values << (char)byte; } else { -- cgit v1.2.3 From 514540d89cb99725aa38455c52340f2cd4115896 Mon Sep 17 00:00:00 2001 From: ailurux Date: Mon, 22 Jul 2024 14:37:05 +1000 Subject: Queue now keeps file offsets in memory to speed up search --- src/tangara/audio/playlist.cpp | 68 +++++++++++++++++++++++++++++++++++++----- src/tangara/audio/playlist.hpp | 9 ++++++ 2 files changed, 69 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/tangara/audio/playlist.cpp b/src/tangara/audio/playlist.cpp index 850b7335..f34be9c9 100644 --- a/src/tangara/audio/playlist.cpp +++ b/src/tangara/audio/playlist.cpp @@ -16,7 +16,12 @@ namespace audio { [[maybe_unused]] static constexpr char kTag[] = "playlist"; Playlist::Playlist(std::string playlistFilepath) - : filepath_(playlistFilepath), mutex_(), total_size_(0), pos_(0) {} + : filepath_(playlistFilepath), + mutex_(), + total_size_(0), + pos_(-1), + offset_cache_(&memory::kSpiRamResource), + sample_size_(50) {} auto Playlist::open() -> bool { FRESULT res = @@ -47,8 +52,10 @@ auto Playlist::size() const -> size_t { auto Playlist::append(Item i) -> void { std::unique_lock lock(mutex_); auto offset = f_tell(&file_); + bool first_entry = current_value_.empty(); // Seek to end and append - auto res = f_lseek(&file_, f_size(&file_)); + auto end = f_size(&file_); + auto res = f_lseek(&file_, end); if (res != FR_OK) { ESP_LOGE(kTag, "Seek to end of file failed? Error %d", res); return; @@ -58,16 +65,19 @@ auto Playlist::append(Item i) -> void { if (std::holds_alternative(i)) { path = std::get(i); f_printf(&file_, "%s\n", path.c_str()); - total_size_++; - if (current_value_.empty()) { + if (total_size_ % sample_size_ == 0) { + offset_cache_.push_back(end); + } + if (first_entry) { current_value_ = path; } + total_size_++; } // Restore position res = f_lseek(&file_, offset); - if (res != FR_OK) { - ESP_LOGE(kTag, "Failed to restore file position after append?"); - return; + if (res != FR_OK) { + ESP_LOGE(kTag, "Failed to restore file position after append?"); + return; } res = f_sync(&file_); if (res != FR_OK) { @@ -77,8 +87,25 @@ auto Playlist::append(Item i) -> void { } auto Playlist::skipTo(size_t position) -> void { + std::unique_lock lock(mutex_); + // Check our cache and go to nearest entry pos_ = position; - consumeAndCount(position); + auto remainder = position % sample_size_; + auto quotient = (position - remainder) / sample_size_; + if (offset_cache_.size() < quotient) { + // Fall back case + ESP_LOGW(kTag, "File offset cache failed, falling back..."); + f_rewind(&file_); + advanceBy(pos_); + } + auto entry = offset_cache_.at(quotient); + // Go to byte offset + auto res = f_lseek(&file_, entry); + if (res != FR_OK) { + ESP_LOGW(kTag, "Error going to byte offset %llu for playlist entry index %d", entry, pos_); + } + // Count ahead entries + advanceBy(remainder+1); } auto Playlist::next() -> void { @@ -99,6 +126,7 @@ auto Playlist::value() const -> std::string { } auto Playlist::clear() -> bool { + std::unique_lock lock(mutex_); auto res = f_close(&file_); if (res != FR_OK) { return false; @@ -110,6 +138,7 @@ auto Playlist::clear() -> bool { } total_size_ = 0; current_value_.clear(); + offset_cache_.clear(); pos_ = 0; return true; } @@ -128,6 +157,7 @@ auto Playlist::consumeAndCount(ssize_t upto) -> bool { size_t count = 0; f_rewind(&file_); while (!f_eof(&file_)) { + auto offset = f_tell(&file_); // TODO: Correctly handle lines longer than this // TODO: Also correctly handle the case where the last entry doesn't end in // \n @@ -136,6 +166,9 @@ auto Playlist::consumeAndCount(ssize_t upto) -> bool { ESP_LOGW(kTag, "Error consuming playlist file at line %d", count); return false; } + if (count % sample_size_ == 0) { + offset_cache_.push_back(offset); + } count++; if (upto >= 0 && count > upto) { @@ -151,4 +184,23 @@ auto Playlist::consumeAndCount(ssize_t upto) -> bool { return true; } +auto Playlist::advanceBy(ssize_t amt) -> bool { + TCHAR buff[512]; + size_t count = 0; + while (!f_eof(&file_)) { + auto res = f_gets(buff, 512, &file_); + if (res == NULL) { + ESP_LOGW(kTag, "Error consuming playlist file at line %d", count); + return false; + } + count++; + if (count >= amt) { + size_t len = strlen(buff); + current_value_.assign(buff, len - 1); + break; + } + } + return true; +} + } // namespace audio \ No newline at end of file diff --git a/src/tangara/audio/playlist.hpp b/src/tangara/audio/playlist.hpp index b278d8a6..3d914663 100644 --- a/src/tangara/audio/playlist.hpp +++ b/src/tangara/audio/playlist.hpp @@ -50,7 +50,16 @@ class Playlist { FIL file_; std::string current_value_; + + std::pmr::vector offset_cache_; // List of offsets determined by sample size; + /* + * How many tracks per offset saved (ie, a value of 100 means every 100 tracks the file offset is saved) + * This speeds up searches, especially in the case of shuffling a lot of tracks. + */ + const uint32_t sample_size_; + auto consumeAndCount(ssize_t upto) -> bool; + auto advanceBy(ssize_t amt) -> bool; }; } // namespace audio \ No newline at end of file -- cgit v1.2.3 From 496baefe663795a030f528853c377c158af357a7 Mon Sep 17 00:00:00 2001 From: ailurux Date: Mon, 22 Jul 2024 15:42:20 +1000 Subject: Oops, forgot a return statement --- src/tangara/audio/playlist.cpp | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/tangara/audio/playlist.cpp b/src/tangara/audio/playlist.cpp index f34be9c9..dcb9bfb7 100644 --- a/src/tangara/audio/playlist.cpp +++ b/src/tangara/audio/playlist.cpp @@ -97,6 +97,7 @@ auto Playlist::skipTo(size_t position) -> void { ESP_LOGW(kTag, "File offset cache failed, falling back..."); f_rewind(&file_); advanceBy(pos_); + return; } auto entry = offset_cache_.at(quotient); // Go to byte offset -- cgit v1.2.3 From 6f98eaf85e9ab9d6251c0cf12807b04a6674767d Mon Sep 17 00:00:00 2001 From: ailurux Date: Mon, 22 Jul 2024 15:54:44 +1000 Subject: Fix off by one error --- src/tangara/audio/playlist.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/tangara/audio/playlist.cpp b/src/tangara/audio/playlist.cpp index dcb9bfb7..506e473f 100644 --- a/src/tangara/audio/playlist.cpp +++ b/src/tangara/audio/playlist.cpp @@ -92,7 +92,7 @@ auto Playlist::skipTo(size_t position) -> void { pos_ = position; auto remainder = position % sample_size_; auto quotient = (position - remainder) / sample_size_; - if (offset_cache_.size() < quotient) { + if (offset_cache_.size() <= quotient) { // Fall back case ESP_LOGW(kTag, "File offset cache failed, falling back..."); f_rewind(&file_); -- cgit v1.2.3 From a440d71bef42a2c9cc10d9f3f49fa097257d25f9 Mon Sep 17 00:00:00 2001 From: ailurux Date: Mon, 22 Jul 2024 16:04:20 +1000 Subject: Continue decoding even if OV_HOLE is returned --- src/codecs/vorbis.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/codecs/vorbis.cpp b/src/codecs/vorbis.cpp index 0b2af691..ea33a2af 100644 --- a/src/codecs/vorbis.cpp +++ b/src/codecs/vorbis.cpp @@ -137,10 +137,15 @@ auto TremorVorbisDecoder::DecodeTo(std::span output) ((output.size() - 1) * sizeof(sample::Sample)), &unused); if (bytes_written == OV_HOLE) { ESP_LOGE(kTag, "got OV_HOLE"); - return cpp::fail(Error::kMalformedData); + return OutputInfo{ + .samples_written = 0, + .is_stream_finished = false, + }; } else if (bytes_written == OV_EBADLINK) { ESP_LOGE(kTag, "got OV_EBADLINK"); return cpp::fail(Error::kMalformedData); + } else if (bytes_written == OV_EINVAL) { + return cpp::fail(Error::kMalformedData); } return OutputInfo{ -- cgit v1.2.3 From 0cc75366848e9205ac88884afcc128925024ccec Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 24 Jul 2024 15:29:45 +1000 Subject: Add a settings screen with power+battery info Mostly for debugging, but also u can toggle fast charging off and on now --- src/drivers/include/drivers/nvs.hpp | 4 ++ src/drivers/include/drivers/samd.hpp | 13 ++++-- src/drivers/nvs.cpp | 12 ++++++ src/drivers/samd.cpp | 74 ++++++++++++++++++++++++++++----- src/tangara/app_console/app_console.cpp | 21 +--------- src/tangara/battery/battery.cpp | 2 + src/tangara/battery/battery.hpp | 1 + src/tangara/system_fsm/booting.cpp | 6 +-- src/tangara/ui/ui_fsm.cpp | 34 ++++++++++++--- src/tangara/ui/ui_fsm.hpp | 2 + 10 files changed, 126 insertions(+), 43 deletions(-) (limited to 'src') diff --git a/src/drivers/include/drivers/nvs.hpp b/src/drivers/include/drivers/nvs.hpp index 8eb28cc9..e298ffc3 100644 --- a/src/drivers/include/drivers/nvs.hpp +++ b/src/drivers/include/drivers/nvs.hpp @@ -90,6 +90,9 @@ class NvsStorage { auto LraCalibration() -> std::optional; auto LraCalibration(const LraData&) -> void; + auto FastCharge() -> bool; + auto FastCharge(bool) -> void; + auto PreferredBluetoothDevice() -> std::optional; auto PreferredBluetoothDevice(std::optional) -> void; @@ -150,6 +153,7 @@ class NvsStorage { Setting display_rows_; Setting haptic_motor_type_; Setting lra_calibration_; + Setting fast_charge_; Setting brightness_; Setting sensitivity_; diff --git a/src/drivers/include/drivers/samd.hpp b/src/drivers/include/drivers/samd.hpp index 897e78d6..ff479225 100644 --- a/src/drivers/include/drivers/samd.hpp +++ b/src/drivers/include/drivers/samd.hpp @@ -10,6 +10,7 @@ #include #include +#include "drivers/nvs.hpp" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" @@ -17,9 +18,7 @@ namespace drivers { class Samd { public: - static auto Create() -> Samd* { return new Samd(); } - - Samd(); + Samd(NvsStorage& nvs); ~Samd(); auto Version() -> std::string; @@ -37,8 +36,14 @@ class Samd { kChargingFast, // The battery is full charged, and we are still plugged in. kFullCharge, + // Charging failed. + kFault, + // The battery status returned isn't a known enum value. + kUnknown, }; + static auto chargeStatusToString(ChargeStatus) -> std::string; + auto GetChargeStatus() -> std::optional; auto UpdateChargeStatus() -> void; @@ -68,6 +73,8 @@ class Samd { Samd& operator=(const Samd&) = delete; private: + NvsStorage& nvs_; + uint8_t version_; std::optional charge_status_; UsbStatus usb_status_; diff --git a/src/drivers/nvs.cpp b/src/drivers/nvs.cpp index e3c4aa06..6fac8c61 100644 --- a/src/drivers/nvs.cpp +++ b/src/drivers/nvs.cpp @@ -40,6 +40,7 @@ static constexpr char kKeyDisplayRows[] = "disprows"; static constexpr char kKeyHapticMotorType[] = "hapticmtype"; static constexpr char kKeyLraCalibration[] = "lra_cali"; static constexpr char kKeyDbAutoIndex[] = "dbautoindex"; +static constexpr char kKeyFastCharge[] = "fastchg"; static auto nvs_get_string(nvs_handle_t nvs, const char* key) -> std::optional { @@ -239,6 +240,7 @@ NvsStorage::NvsStorage(nvs_handle_t handle) display_rows_(kKeyDisplayRows), haptic_motor_type_(kKeyHapticMotorType), lra_calibration_(kKeyLraCalibration), + fast_charge_(kKeyFastCharge), brightness_(kKeyBrightness), sensitivity_(kKeyScrollSensitivity), amp_max_vol_(kKeyAmpMaxVolume), @@ -444,6 +446,16 @@ auto NvsStorage::OutputMode(Output out) -> void { nvs_commit(handle_); } +auto NvsStorage::FastCharge() -> bool { + std::lock_guard lock{mutex_}; + return fast_charge_.get().value_or(true); +} + +auto NvsStorage::FastCharge(bool en) -> void { + std::lock_guard lock{mutex_}; + fast_charge_.set(en); +} + auto NvsStorage::ScreenBrightness() -> uint_fast8_t { std::lock_guard lock{mutex_}; return std::clamp(brightness_.get().value_or(50), 0, 100); diff --git a/src/drivers/samd.cpp b/src/drivers/samd.cpp index e4aa73ad..c2308760 100644 --- a/src/drivers/samd.cpp +++ b/src/drivers/samd.cpp @@ -5,11 +5,13 @@ */ #include "drivers/samd.hpp" +#include #include #include #include +#include "drivers/nvs.hpp" #include "esp_err.h" #include "esp_log.h" #include "hal/gpio_types.h" @@ -32,7 +34,29 @@ namespace drivers { static constexpr gpio_num_t kIntPin = GPIO_NUM_35; -Samd::Samd() { +auto Samd::chargeStatusToString(ChargeStatus status) -> std::string { + switch (status) { + case ChargeStatus::kNoBattery: + return "no_battery"; + case ChargeStatus::kBatteryCritical: + return "critical"; + case ChargeStatus::kDischarging: + return "discharging"; + case ChargeStatus::kChargingRegular: + return "charge_regular"; + case ChargeStatus::kChargingFast: + return "charge_fast"; + case ChargeStatus::kFullCharge: + return "full_charge"; + case ChargeStatus::kFault: + return "fault"; + case ChargeStatus::kUnknown: + default: + return "unknown"; + } +} + +Samd::Samd(NvsStorage& nvs) : nvs_(nvs) { gpio_set_direction(kIntPin, GPIO_MODE_INPUT); // Being able to interface with the SAMD properly is critical. To ensure we @@ -51,7 +75,7 @@ Samd::Samd() { UpdateChargeStatus(); UpdateUsbStatus(); - SetFastChargeEnabled(true); + SetFastChargeEnabled(nvs.FastCharge()); } Samd::~Samd() {} @@ -78,16 +102,38 @@ auto Samd::UpdateChargeStatus() -> void { return; } - // FIXME: Ideally we should be using the three 'charge status' bits to work - // out whether we're actually charging, or if we've got a full charge, - // critically low charge, etc. + // Lower two bits are the usb power status, next three are the BMS status. + // See 'gpio.c' in the SAMD21 firmware for how these bits get packed. + uint8_t charge_state = (raw_res & 0b11100) >> 2; uint8_t usb_state = raw_res & 0b11; - if (usb_state == 0) { - charge_status_ = ChargeStatus::kDischarging; - } else if (usb_state == 1) { - charge_status_ = ChargeStatus::kChargingRegular; - } else { - charge_status_ = ChargeStatus::kChargingFast; + switch (charge_state) { + case 0b000: + charge_status_ = ChargeStatus::kNoBattery; + break; + case 0b001: + // BMS says we're charging; work out how fast we're charging. + if (usb_state >= 0b10 && nvs_.FastCharge()) { + charge_status_ = ChargeStatus::kChargingFast; + } else { + charge_status_ = ChargeStatus::kChargingRegular; + } + break; + case 0b010: + charge_status_ = ChargeStatus::kFullCharge; + break; + case 0b011: + charge_status_ = ChargeStatus::kFault; + break; + case 0b100: + charge_status_ = ChargeStatus::kBatteryCritical; + break; + case 0b101: + charge_status_ = ChargeStatus::kDischarging; + break; + case 0b110: + case 0b111: + charge_status_ = ChargeStatus::kUnknown; + break; } } @@ -127,9 +173,15 @@ auto Samd::ResetToFlashSamd() -> void { } auto Samd::SetFastChargeEnabled(bool en) -> void { + // Always update NVS, so that the setting is right after the SAMD firmware is + // updated. + nvs_.FastCharge(en); + if (version_ < 4) { return; } + ESP_LOGI(kTag, "set fast charge %u", en); + I2CTransaction transaction; transaction.start() .write_addr(kAddress, I2C_MASTER_WRITE) diff --git a/src/tangara/app_console/app_console.cpp b/src/tangara/app_console/app_console.cpp index 11862143..21dec56a 100644 --- a/src/tangara/app_console/app_console.cpp +++ b/src/tangara/app_console/app_console.cpp @@ -465,26 +465,7 @@ int CmdSamd(int argc, char** argv) { } else if (cmd == "charge") { auto res = samd.GetChargeStatus(); if (res) { - switch (res.value()) { - case drivers::Samd::ChargeStatus::kNoBattery: - std::cout << "kNoBattery" << std::endl; - break; - case drivers::Samd::ChargeStatus::kBatteryCritical: - std::cout << "kBatteryCritical" << std::endl; - break; - case drivers::Samd::ChargeStatus::kDischarging: - std::cout << "kDischarging" << std::endl; - break; - case drivers::Samd::ChargeStatus::kChargingRegular: - std::cout << "kChargingRegular" << std::endl; - break; - case drivers::Samd::ChargeStatus::kChargingFast: - std::cout << "kChargingFast" << std::endl; - break; - case drivers::Samd::ChargeStatus::kFullCharge: - std::cout << "kFullCharge" << std::endl; - break; - } + std::cout << drivers::Samd::chargeStatusToString(*res) << std::endl; } else { std::cout << "unknown" << std::endl; } diff --git a/src/tangara/battery/battery.cpp b/src/tangara/battery/battery.cpp index 3cfdb20c..9bde86a8 100644 --- a/src/tangara/battery/battery.cpp +++ b/src/tangara/battery/battery.cpp @@ -93,6 +93,8 @@ auto Battery::Update() -> void { .percent = percent, .millivolts = mV, .is_charging = is_charging, + .raw_status = + charge_state.value_or(drivers::Samd::ChargeStatus::kUnknown), }; EmitEvent(); } diff --git a/src/tangara/battery/battery.hpp b/src/tangara/battery/battery.hpp index 80b0f2d2..c4f631e0 100644 --- a/src/tangara/battery/battery.hpp +++ b/src/tangara/battery/battery.hpp @@ -27,6 +27,7 @@ class Battery { uint_fast8_t percent; uint32_t millivolts; bool is_charging; + drivers::Samd::ChargeStatus raw_status; bool operator==(const BatteryState& other) const { return percent == other.percent && is_charging == other.is_charging; diff --git a/src/tangara/system_fsm/booting.cpp b/src/tangara/system_fsm/booting.cpp index a3fed9fa..1f99e3ab 100644 --- a/src/tangara/system_fsm/booting.cpp +++ b/src/tangara/system_fsm/booting.cpp @@ -87,7 +87,7 @@ auto Booting::entry() -> void { ESP_LOGI(kTag, "installing remaining drivers"); drivers::spiffs_mount(); - sServices->samd(std::unique_ptr(drivers::Samd::Create())); + sServices->samd(std::make_unique(sServices->nvs())); sServices->touchwheel( std::unique_ptr{drivers::TouchWheel::Create()}); sServices->haptics(std::make_unique(sServices->nvs())); @@ -96,8 +96,8 @@ auto Booting::entry() -> void { sServices->battery(std::make_unique( sServices->samd(), std::unique_ptr(adc))); - sServices->track_queue( - std::make_unique(sServices->bg_worker(), sServices->database())); + sServices->track_queue(std::make_unique( + sServices->bg_worker(), sServices->database())); sServices->tag_parser(std::make_unique()); sServices->collator(locale::CreateCollator()); sServices->tts(std::make_unique()); diff --git a/src/tangara/ui/ui_fsm.cpp b/src/tangara/ui/ui_fsm.cpp index cd39dc9c..38e9b8e1 100644 --- a/src/tangara/ui/ui_fsm.cpp +++ b/src/tangara/ui/ui_fsm.cpp @@ -101,6 +101,15 @@ static auto lvgl_delay_cb(uint32_t ms) -> void { lua::Property UiState::sBatteryPct{0}; lua::Property UiState::sBatteryMv{0}; lua::Property UiState::sBatteryCharging{false}; +lua::Property UiState::sPowerChargeState{"unknown"}; +lua::Property UiState::sPowerFastChargeEnabled{ + false, [](const lua::LuaValue& val) { + if (!std::holds_alternative(val)) { + return false; + } + sServices->samd().SetFastChargeEnabled(std::get(val)); + return true; + }}; lua::Property UiState::sBluetoothEnabled{ false, [](const lua::LuaValue& val) { @@ -406,6 +415,13 @@ void UiState::react(const system_fsm::BatteryStateChanged& ev) { sBatteryPct.setDirect(static_cast(ev.new_state.percent)); sBatteryMv.setDirect(static_cast(ev.new_state.millivolts)); sBatteryCharging.setDirect(ev.new_state.is_charging); + sPowerChargeState.setDirect( + drivers::Samd::chargeStatusToString(ev.new_state.raw_status)); + + // FIXME: Avoid calling these event handlers before boot. + if (sServices) { + sPowerFastChargeEnabled.setDirect(sServices->nvs().FastCharge()); + } } void UiState::react(const audio::QueueUpdate&) { @@ -414,7 +430,8 @@ void UiState::react(const audio::QueueUpdate&) { sQueueSize.setDirect(static_cast(queue_size)); int current_pos = queue.currentPosition(); - // If there is nothing in the queue, the position should be 0, otherwise, add one because lua + // If there is nothing in the queue, the position should be 0, otherwise, add + // one because lua if (queue_size > 0) { current_pos++; } @@ -564,11 +581,14 @@ void Lua::entry() { auto& registry = lua::Registry::instance(*sServices); sLua = registry.uiThread(); - registry.AddPropertyModule("power", { - {"battery_pct", &sBatteryPct}, - {"battery_millivolts", &sBatteryMv}, - {"plugged_in", &sBatteryCharging}, - }); + registry.AddPropertyModule("power", + { + {"battery_pct", &sBatteryPct}, + {"battery_millivolts", &sBatteryMv}, + {"plugged_in", &sBatteryCharging}, + {"charge_state", &sPowerChargeState}, + {"fast_charge", &sPowerFastChargeEnabled}, + }); registry.AddPropertyModule( "bluetooth", { {"enabled", &sBluetoothEnabled}, @@ -663,6 +683,8 @@ void Lua::entry() { } sBluetoothKnownDevices.setDirect(bt.knownDevices()); + sPowerFastChargeEnabled.setDirect(sServices->nvs().FastCharge()); + if (sServices->sd() == drivers::SdState::kMounted) { sLua->RunScript("/sdcard/config.lua"); } diff --git a/src/tangara/ui/ui_fsm.hpp b/src/tangara/ui/ui_fsm.hpp index cef9a13a..41f0db3a 100644 --- a/src/tangara/ui/ui_fsm.hpp +++ b/src/tangara/ui/ui_fsm.hpp @@ -101,6 +101,8 @@ class UiState : public tinyfsm::Fsm { static lua::Property sBatteryPct; static lua::Property sBatteryMv; static lua::Property sBatteryCharging; + static lua::Property sPowerChargeState; + static lua::Property sPowerFastChargeEnabled; static lua::Property sBluetoothEnabled; static lua::Property sBluetoothConnecting; -- cgit v1.2.3 From be9564d1c7ef2fed3330964472b5ebda557da3d6 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 25 Jul 2024 09:50:31 +1000 Subject: Parse single-byte track ids properly --- src/tangara/database/records.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/tangara/database/records.cpp b/src/tangara/database/records.cpp index 3e76ecad..6406f080 100644 --- a/src/tangara/database/records.cpp +++ b/src/tangara/database/records.cpp @@ -238,7 +238,7 @@ auto ParseIndexKey(const leveldb::Slice& slice) -> std::optional { std::string id_str = key_data.substr(header_length + 1 + buffer.str().size() + 1); - if (id_str.size() > 1) { + if (id_str.size() > 0) { result.track = BytesToTrackId(id_str); } -- cgit v1.2.3 From 4210a8ac548ce526e3023729859d3a3931d31c13 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 25 Jul 2024 09:50:50 +1000 Subject: add some helpful hex dump overloads --- src/util/include/debug.hpp | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'src') diff --git a/src/util/include/debug.hpp b/src/util/include/debug.hpp index 06e3833e..37c26f6a 100644 --- a/src/util/include/debug.hpp +++ b/src/util/include/debug.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -43,4 +44,12 @@ inline std::string format_hex_string(std::span data) { return oss.str(); } +inline std::string format_hex_string(std::span data) { + return format_hex_string(std::as_bytes(data)); +} + +inline std::string format_hex_string(std::span data) { + return format_hex_string(std::as_bytes(data)); +} + } // namespace util -- cgit v1.2.3 From 64c8496a91a166e52b7d77a3189e2696720294b7 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Fri, 26 Jul 2024 16:09:40 +1000 Subject: Use a piecewise linear formula to calculate battery % --- src/tangara/battery/battery.cpp | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/tangara/battery/battery.cpp b/src/tangara/battery/battery.cpp index 9bde86a8..f68746ae 100644 --- a/src/tangara/battery/battery.cpp +++ b/src/tangara/battery/battery.cpp @@ -26,6 +26,8 @@ static const TickType_t kBatteryCheckPeriod = pdMS_TO_TICKS(60 * 1000); */ static const uint32_t kFullChargeMilliVolts = 4200; +static const uint32_t kCriticalChargeMilliVolts = 3500; + /* * Battery voltage, in millivolts, at which *we* will consider the battery to * be completely discharged. This is intentionally higher than the charger IC @@ -65,12 +67,35 @@ auto Battery::Update() -> void { // Ideally the way you're 'supposed' to measure battery charge percent is to // keep continuous track of the amps going in and out of it at any point. I'm // skeptical of this approach, and we're not set up with the hardware needed - // to do it anyway. Instead, we use a curve-fitting formula by StackOverflow - // user 'Roho' to estimate the remaining capacity based on the battery's - // voltage. This seems to work pretty good! - double v = mV / 1000.0; - uint_fast8_t percent = static_cast(std::clamp( - 123 - (123 / std::pow(1 + std::pow(v / 3.7, 80.0), 0.165)), 0.0, 100.0)); + // to do it anyway. Instead, we use a piecewise linear formula derived from + // voltage measurements of our actual cells. + uint_fast8_t percent; + if (mV > kCriticalChargeMilliVolts) { + // Above the 'critical' point, the relationship between battery voltage and + // charge percentage is close enough to linear. + percent = ((mV - kCriticalChargeMilliVolts) * 100 / + (kFullChargeMilliVolts - kCriticalChargeMilliVolts)) + + 5; + } else { + // Below the 'critical' point, battery voltage drops very very quickly. + // Give this part of the curve the lowest 5% to work with. + percent = (mV - kEmptyChargeMilliVolts) * 5 / + (kCriticalChargeMilliVolts - kEmptyChargeMilliVolts); + } + + // A full charge is always 100%. + if (charge_state == ChargeStatus::kFullCharge) { + percent = 100; + } + // Critical charge is always <= 5% + if (charge_state == ChargeStatus::kBatteryCritical) { + percent = std::min(percent, 5); + } + // When very close to full, the BMS transitions to a constant-voltage charge + // algorithm. Hold off on reporting 100% charge until this stage is finished. + if (percent >= 95 && charge_state != ChargeStatus::kFullCharge) { + percent = std::min(percent, 95); + } bool is_charging; if (!charge_state) { -- cgit v1.2.3 From b34959917446ac5d47ddec7bb6d98a6397045558 Mon Sep 17 00:00:00 2001 From: ailurux Date: Tue, 30 Jul 2024 04:36:48 +0000 Subject: daniel/playlist-queue (#84) Support for playlist files being opened along side the queue's own playlist. Playlists can be opened from the file browser, if the file ends in ".playlist" (will add support for .m3u as well eventually) Reviewed-on: https://codeberg.org/cool-tech-zone/tangara-fw/pulls/84 Co-authored-by: ailurux Co-committed-by: ailurux --- src/tangara/audio/playlist.cpp | 8 ++- src/tangara/audio/playlist.hpp | 15 +++-- src/tangara/audio/track_queue.cpp | 115 +++++++++++++++++++++++-------------- src/tangara/audio/track_queue.hpp | 9 ++- src/tangara/lua/file_iterator.cpp | 41 ++++++++----- src/tangara/lua/file_iterator.hpp | 4 +- src/tangara/lua/lua_filesystem.cpp | 68 +++++++++++----------- src/tangara/lua/lua_queue.cpp | 14 +++++ 8 files changed, 175 insertions(+), 99 deletions(-) (limited to 'src') diff --git a/src/tangara/audio/playlist.cpp b/src/tangara/audio/playlist.cpp index 506e473f..944ad143 100644 --- a/src/tangara/audio/playlist.cpp +++ b/src/tangara/audio/playlist.cpp @@ -15,7 +15,7 @@ namespace audio { [[maybe_unused]] static constexpr char kTag[] = "playlist"; -Playlist::Playlist(std::string playlistFilepath) +Playlist::Playlist(const std::string& playlistFilepath) : filepath_(playlistFilepath), mutex_(), total_size_(0), @@ -49,7 +49,7 @@ auto Playlist::size() const -> size_t { return total_size_; } -auto Playlist::append(Item i) -> void { +auto MutablePlaylist::append(Item i) -> void { std::unique_lock lock(mutex_); auto offset = f_tell(&file_); bool first_entry = current_value_.empty(); @@ -126,7 +126,9 @@ auto Playlist::value() const -> std::string { return current_value_; } -auto Playlist::clear() -> bool { +MutablePlaylist::MutablePlaylist(const std::string& playlistFilepath) : Playlist(playlistFilepath) {} + +auto MutablePlaylist::clear() -> bool { std::unique_lock lock(mutex_); auto res = f_close(&file_); if (res != FR_OK) { diff --git a/src/tangara/audio/playlist.hpp b/src/tangara/audio/playlist.hpp index 3d914663..b248ac77 100644 --- a/src/tangara/audio/playlist.hpp +++ b/src/tangara/audio/playlist.hpp @@ -26,23 +26,21 @@ namespace audio { */ class Playlist { public: - Playlist(std::string playlistFilepath); - ~Playlist(); + Playlist(const std::string& playlistFilepath); + virtual ~Playlist(); using Item = std::variant; auto open() -> bool; auto currentPosition() const -> size_t; auto size() const -> size_t; - auto append(Item i) -> void; auto skipTo(size_t position) -> void; auto next() -> void; auto prev() -> void; auto value() const -> std::string; - auto clear() -> bool; auto atEnd() -> bool; auto filepath() -> std::string; - private: + protected: std::string filepath_; std::mutex mutex_; size_t total_size_; @@ -62,4 +60,11 @@ class Playlist { auto advanceBy(ssize_t amt) -> bool; }; +class MutablePlaylist : public Playlist { +public: + MutablePlaylist(const std::string& playlistFilepath); + auto clear() -> bool; + auto append(Item i) -> void; +}; + } // namespace audio \ No newline at end of file diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index 1aeecf8a..399d6717 100644 --- a/src/tangara/audio/track_queue.cpp +++ b/src/tangara/audio/track_queue.cpp @@ -84,18 +84,25 @@ auto notifyChanged(bool current_changed, Reason reason) -> void { events::Audio().Dispatch(ev); } + TrackQueue::TrackQueue(tasks::WorkerPool& bg_worker, database::Handle db) : mutex_(), bg_worker_(bg_worker), db_(db), - playlist_("queue.playlist"), // TODO + playlist_(".queue.playlist"), + position_(0), shuffle_(), repeat_(false), replay_(false) {} auto TrackQueue::current() const -> TrackItem { const std::shared_lock lock(mutex_); - std::string val = playlist_.value(); + std::string val; + if (opened_playlist_ && position_ < opened_playlist_->size()) { + val = opened_playlist_->value(); + } else { + val = playlist_.value(); + } if (val.empty()) { return {}; } @@ -104,12 +111,21 @@ auto TrackQueue::current() const -> TrackItem { auto TrackQueue::currentPosition() const -> size_t { const std::shared_lock lock(mutex_); - return playlist_.currentPosition(); + return position_; } auto TrackQueue::totalSize() const -> size_t { - const std::shared_lock lock(mutex_); - return playlist_.size(); + size_t sum = playlist_.size(); + if (opened_playlist_) { + sum += opened_playlist_->size(); + } + return sum; +} + +auto TrackQueue::updateShuffler() -> void { + if (shuffle_) { + shuffle_->resize(totalSize()); + } } auto TrackQueue::open() -> bool { @@ -118,6 +134,17 @@ auto TrackQueue::open() -> bool { return playlist_.open(); } +auto TrackQueue::openPlaylist(const std::string& playlist_file) -> bool { + opened_playlist_.emplace(playlist_file); + auto res = opened_playlist_->open(); + if (!res) { + return false; + } + updateShuffler(); + notifyChanged(true, Reason::kExplicitUpdate); + return true; +} + auto TrackQueue::getFilepath(database::TrackId id) -> std::optional { auto db = db_.lock(); if (!db) { @@ -142,20 +169,15 @@ auto TrackQueue::append(Item i) -> void { current_changed = was_queue_empty; // Dont support inserts yet } - auto update_shuffler = [=, this]() { - if (shuffle_) { - shuffle_->resize(playlist_.size()); - // If there wasn't anything already playing, then we should make sure we - // begin playback at a random point, instead of always starting with - // whatever was inserted first and *then* shuffling. - // We don't base this purely off of current_changed because we would like - // 'play this track now' (by inserting at the current pos) to work even - // when shuffling is enabled. - if (was_queue_empty) { - playlist_.skipTo(shuffle_->current()); - } - } - }; + // If there wasn't anything already playing, then we should make sure we + // begin playback at a random point, instead of always starting with + // whatever was inserted first and *then* shuffling. + // We don't base this purely off of current_changed because we would like + // 'play this track now' (by inserting at the current pos) to work even + // when shuffling is enabled. + if (was_queue_empty && shuffle_) { + playlist_.skipTo(shuffle_->current()); + } if (std::holds_alternative(i)) { { @@ -164,7 +186,7 @@ auto TrackQueue::append(Item i) -> void { if (!filename.empty()) { playlist_.append(filename); } - update_shuffler(); + updateShuffler(); } notifyChanged(current_changed, Reason::kExplicitUpdate); } else if (std::holds_alternative(i)) { @@ -191,7 +213,7 @@ auto TrackQueue::append(Item i) -> void { } { const std::unique_lock lock(mutex_); - update_shuffler(); + updateShuffler(); } notifyChanged(current_changed, Reason::kExplicitUpdate); }); @@ -202,6 +224,20 @@ auto TrackQueue::next() -> void { next(Reason::kExplicitUpdate); } +auto TrackQueue::goTo(size_t position) { + position_ = position; + if (opened_playlist_) { + if (position_ < opened_playlist_->size()) { + opened_playlist_->skipTo(position_); + } else { + playlist_.skipTo(position_ - opened_playlist_->size()); + } + } else { + playlist_.skipTo(position_); + } +} + + auto TrackQueue::next(Reason r) -> void { bool changed = true; @@ -209,18 +245,13 @@ auto TrackQueue::next(Reason r) -> void { const std::unique_lock lock(mutex_); if (shuffle_) { shuffle_->next(); - playlist_.skipTo(shuffle_->current()); + position_ = shuffle_->current(); } else { - if (playlist_.atEnd()) { - if (replay_) { - playlist_.skipTo(0); - } else { - changed = false; - } - } else { - playlist_.next(); + if (position_ + 1 < totalSize()) { + position_++; } } + goTo(position_); } notifyChanged(changed, r); @@ -233,18 +264,13 @@ auto TrackQueue::previous() -> void { const std::unique_lock lock(mutex_); if (shuffle_) { shuffle_->prev(); - playlist_.skipTo(shuffle_->current()); + position_ = shuffle_->current(); } else { - if (playlist_.currentPosition() == 0) { - if (repeat_) { - playlist_.skipTo(playlist_.size()-1); - } else { - changed = false; - } - } else { - playlist_.prev(); + if (position_ > 0) { + position_--; } } + goTo(position_); } notifyChanged(changed, Reason::kExplicitUpdate); @@ -262,6 +288,7 @@ auto TrackQueue::clear() -> void { { const std::unique_lock lock(mutex_); playlist_.clear(); + opened_playlist_.reset(); if (shuffle_) { shuffle_->resize(0); } @@ -274,7 +301,7 @@ auto TrackQueue::random(bool en) -> void { { const std::unique_lock lock(mutex_); if (en) { - shuffle_.emplace(playlist_.size()); + shuffle_.emplace(totalSize()); shuffle_->replay(replay_); } else { shuffle_.reset(); @@ -326,7 +353,8 @@ auto TrackQueue::serialise() -> std::string { encoded.add(cppbor::Uint{0}, cppbor::Array{ cppbor::Bool{repeat_}, cppbor::Bool{replay_}, - cppbor::Uint{playlist_.currentPosition()}, + cppbor::Uint{position_}, + cppbor::Tstr{opened_playlist_->filepath()} }); if (shuffle_) { encoded.add(cppbor::Uint{1}, cppbor::Array{ @@ -368,7 +396,10 @@ cppbor::ParseClient* TrackQueue::QueueParseClient::item( i_ = 0; } else if (item->type() == cppbor::UINT) { auto val = item->asUint()->unsignedValue(); - queue_.playlist_.skipTo(val); + queue_.goTo(val); + } else if (item->type() == cppbor::TSTR) { + auto val = item->asTstr(); + queue_.openPlaylist(val->value()); } else if (item->type() == cppbor::SIMPLE) { bool val = item->asBool()->value(); if (i_ == 0) { diff --git a/src/tangara/audio/track_queue.hpp b/src/tangara/audio/track_queue.hpp index 6f50f162..72713242 100644 --- a/src/tangara/audio/track_queue.hpp +++ b/src/tangara/audio/track_queue.hpp @@ -74,11 +74,14 @@ class TrackQueue { auto currentPosition() const -> size_t; auto totalSize() const -> size_t; auto open() -> bool; + auto openPlaylist(const std::string& playlist_file) -> bool; using Item = std::variant; auto insert(Item, size_t index = 0) -> void; auto append(Item i) -> void; + auto updateShuffler() -> void; + /* * Advances to the next track in the queue, placing the current track at the * front of the 'played' queue. @@ -114,6 +117,7 @@ class TrackQueue { private: auto next(QueueUpdate::Reason r) -> void; + auto goTo(size_t position); auto getFilepath(database::TrackId id) -> std::optional; mutable std::shared_mutex mutex_; @@ -121,7 +125,10 @@ class TrackQueue { tasks::WorkerPool& bg_worker_; database::Handle db_; - Playlist playlist_; + MutablePlaylist playlist_; + std::optional opened_playlist_; + + size_t position_; std::optional shuffle_; bool repeat_; diff --git a/src/tangara/lua/file_iterator.cpp b/src/tangara/lua/file_iterator.cpp index c3d63a16..823775e8 100644 --- a/src/tangara/lua/file_iterator.cpp +++ b/src/tangara/lua/file_iterator.cpp @@ -15,8 +15,8 @@ namespace lua { [[maybe_unused]] static const char* kTag = "FileIterator"; -FileIterator::FileIterator(std::string filepath) - : original_path_(filepath), current_(), offset_(-1) { +FileIterator::FileIterator(std::string filepath, bool showHidden) + : original_path_(filepath), show_hidden_(showHidden), current_(), offset_(-1) { const TCHAR* path = static_cast(filepath.c_str()); FRESULT res = f_opendir(&dir_, path); if (res != FR_OK) { @@ -33,7 +33,16 @@ auto FileIterator::value() const -> const std::optional& { } auto FileIterator::next() -> void { - iterate(false); + size_t prev_index = -1; + if (current_) { + prev_index = current_->index; + } + do { + bool res = iterate(show_hidden_); + if (!res) { + break; + } + } while (!current_ || current_->index == prev_index); } auto FileIterator::prev() -> void { @@ -45,11 +54,11 @@ auto FileIterator::prev() -> void { auto new_offset = offset_ - 1; offset_ = -1; for (int i = 0; i <= new_offset; i++) { - iterate(false); + iterate(show_hidden_); } } -auto FileIterator::iterate(bool reverse) -> bool { +auto FileIterator::iterate(bool show_hidden) -> bool { FILINFO info; auto res = f_readdir(&dir_, &info); if (res != FR_OK) { @@ -60,18 +69,22 @@ auto FileIterator::iterate(bool reverse) -> bool { // End of directory // Set value to nil current_.reset(); + return false; } else { // Update current value offset_++; - current_ = FileEntry{ - .index = offset_, - .isHidden = (info.fattrib & AM_HID) > 0, - .isDirectory = (info.fattrib & AM_DIR) > 0, - .isTrack = false, // TODO - .filepath = original_path_ + (original_path_.size() > 0 ? "/" : "") + - info.fname, - - }; + bool hidden = (info.fattrib & AM_HID) > 0 || info.fname[0] == '.'; + if (!hidden || show_hidden) { + current_ = FileEntry{ + .index = offset_, + .isHidden = hidden, + .isDirectory = (info.fattrib & AM_DIR) > 0, + .isTrack = false, // TODO + .filepath = original_path_ + (original_path_.size() > 0 ? "/" : "") + + info.fname, + .name = info.fname, + }; + } } return true; } diff --git a/src/tangara/lua/file_iterator.hpp b/src/tangara/lua/file_iterator.hpp index b803062c..c4071445 100644 --- a/src/tangara/lua/file_iterator.hpp +++ b/src/tangara/lua/file_iterator.hpp @@ -21,11 +21,12 @@ struct FileEntry { bool isDirectory; bool isTrack; std::string filepath; + std::string name; }; class FileIterator { public: - FileIterator(std::string filepath); + FileIterator(std::string filepath, bool showHidden); ~FileIterator(); auto value() const -> const std::optional&; @@ -35,6 +36,7 @@ class FileIterator { private: FF_DIR dir_; std::string original_path_; + bool show_hidden_; std::optional current_; int offset_; diff --git a/src/tangara/lua/lua_filesystem.cpp b/src/tangara/lua/lua_filesystem.cpp index de51f555..e3a3018d 100644 --- a/src/tangara/lua/lua_filesystem.cpp +++ b/src/tangara/lua/lua_filesystem.cpp @@ -21,29 +21,21 @@ struct LuaFileEntry { bool isHidden; bool isDirectory; bool isTrack; - size_t path_size; - char path[]; + std::string path; + std::string name; }; -static_assert(std::is_trivially_destructible()); -static_assert(std::is_trivially_copy_assignable()); - static auto push_lua_file_entry(lua_State* L, const lua::FileEntry& r) -> void { - // Create and init the userdata. - LuaFileEntry* file_entry = reinterpret_cast( - lua_newuserdata(L, sizeof(LuaFileEntry) + r.filepath.size())); + lua::FileEntry** entry = reinterpret_cast( + lua_newuserdata(L, sizeof(uintptr_t))); + *entry = new lua::FileEntry(r); luaL_setmetatable(L, kFileEntryMetatable); +} - // Init all the fields - *file_entry = { - .isHidden = r.isHidden, - .isDirectory = r.isDirectory, - .isTrack = r.isTrack, - .path_size = r.filepath.size(), - }; - - // Copy the string data across. - std::memcpy(file_entry->path, r.filepath.data(), r.filepath.size()); +auto check_file_entry(lua_State* L, int stack_pos) -> lua::FileEntry* { + lua::FileEntry* entry = *reinterpret_cast( + luaL_checkudata(L, stack_pos, kFileEntryMetatable)); + return entry; } auto check_file_iterator(lua_State* L, int stack_pos) -> lua::FileIterator* { @@ -56,7 +48,7 @@ static auto push_iterator(lua_State* state, const lua::FileIterator& it) -> void { lua::FileIterator** data = reinterpret_cast( lua_newuserdata(state, sizeof(uintptr_t))); - *data = new lua::FileIterator(it); // TODO... + *data = new lua::FileIterator(it); luaL_setmetatable(state, kFileIteratorMetatable); } @@ -108,45 +100,55 @@ static const struct luaL_Reg kFileIteratorFuncs[] = {{"next", fs_iterate}, {NULL, NULL}}; static auto file_entry_path(lua_State* state) -> int { - LuaFileEntry* data = reinterpret_cast( - luaL_checkudata(state, 1, kFileEntryMetatable)); - lua_pushlstring(state, data->path, data->path_size); + lua::FileEntry* entry = check_file_entry(state, 1); + lua_pushlstring(state, entry->filepath.c_str(), entry->filepath.size()); return 1; } static auto file_entry_is_dir(lua_State* state) -> int { - LuaFileEntry* data = reinterpret_cast( - luaL_checkudata(state, 1, kFileEntryMetatable)); - lua_pushboolean(state, data->isDirectory); + lua::FileEntry* entry = check_file_entry(state, 1); + lua_pushboolean(state, entry->isDirectory); return 1; } static auto file_entry_is_hidden(lua_State* state) -> int { - LuaFileEntry* data = reinterpret_cast( - luaL_checkudata(state, 1, kFileEntryMetatable)); - lua_pushboolean(state, data->isHidden); + lua::FileEntry* entry = check_file_entry(state, 1); + lua_pushboolean(state, entry->isHidden); return 1; } static auto file_entry_is_track(lua_State* state) -> int { - LuaFileEntry* data = reinterpret_cast( - luaL_checkudata(state, 1, kFileEntryMetatable)); - lua_pushboolean(state, data->isTrack); + lua::FileEntry* entry = check_file_entry(state, 1); + lua_pushboolean(state, entry->isTrack); + return 1; +} + +static auto file_entry_name(lua_State* state) -> int { + lua::FileEntry* entry = check_file_entry(state, 1); + lua_pushlstring(state, entry->name.c_str(), entry->name.size()); + return 1; +} + +static auto file_entry_gc(lua_State* state) -> int { + lua::FileEntry* entry = check_file_entry(state, 1); + delete entry; return 1; } static const struct luaL_Reg kFileEntryFuncs[] = {{"filepath", file_entry_path}, + {"name", file_entry_name}, {"is_directory", file_entry_is_dir}, {"is_hidden", file_entry_is_hidden}, {"is_track", file_entry_is_track}, - {"__tostring", file_entry_path}, + {"__tostring", file_entry_name}, + {"__gc", file_entry_gc}, {NULL, NULL}}; static auto fs_new_iterator(lua_State* state) -> int { // Takes a filepath as a string and returns a new FileIterator // on that directory std::string filepath = luaL_checkstring(state, -1); - lua::FileIterator iter(filepath); + lua::FileIterator iter(filepath, false); push_iterator(state, iter); return 1; } diff --git a/src/tangara/lua/lua_queue.cpp b/src/tangara/lua/lua_queue.cpp index bc393aa5..9e2002e6 100644 --- a/src/tangara/lua/lua_queue.cpp +++ b/src/tangara/lua/lua_queue.cpp @@ -57,8 +57,22 @@ static auto queue_clear(lua_State* state) -> int { return 0; } +static auto queue_open_playlist(lua_State* state) -> int { + Bridge* instance = Bridge::Get(state); + audio::TrackQueue& queue = instance->services().track_queue(); + size_t len = 0; + const char* str = luaL_checklstring(state, 1, &len); + if (!str) { + return 0; + } + queue.clear(); + queue.openPlaylist(str); + return 0; +} + static const struct luaL_Reg kQueueFuncs[] = {{"add", queue_add}, {"clear", queue_clear}, + {"open_playlist", queue_open_playlist}, {NULL, NULL}}; static auto lua_queue(lua_State* state) -> int { -- cgit v1.2.3 From 649cb74f036c392264264d35f98bef1fa4a5a8aa Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 30 Jul 2024 14:56:58 +1000 Subject: Advance the queue when the current track fails to start --- src/tangara/audio/audio_fsm.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'src') diff --git a/src/tangara/audio/audio_fsm.cpp b/src/tangara/audio/audio_fsm.cpp index 65261d75..a43cd932 100644 --- a/src/tangara/audio/audio_fsm.cpp +++ b/src/tangara/audio/audio_fsm.cpp @@ -142,7 +142,20 @@ void AudioState::react(const SetTrack& ev) { sStreamFactory->create(std::get(new_track), seek_to); } + // Always give the stream to the decoder, even if it turns out to be empty. + // This has the effect of stopping the current playback, which is generally + // what the user expects to happen when they say "Play this track!", even + // if the new track has an issue. sDecoder->open(stream); + + // ...but if the stream that failed is the front of the queue, then we + // should advance to the next track in order to keep the tunes flowing. + if (!stream) { + auto& queue = sServices->track_queue(); + if (new_track == queue.current()) { + queue.finish(); + } + } }); } -- cgit v1.2.3 From 1ff28233bd6a64fab97c56861477e122e4c3eac6 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 7 Aug 2024 12:09:23 +1000 Subject: Recalibrate the touchwheel after unlocking Also power it down whilst we're locked. This saves about half a milliamp. --- src/drivers/include/drivers/touchwheel.hpp | 3 ++- src/drivers/touchwheel.cpp | 8 ++++++-- src/tangara/input/input_device.hpp | 5 +++++ src/tangara/input/input_touch_wheel.cpp | 9 +++++++++ src/tangara/input/input_touch_wheel.hpp | 7 +++++-- src/tangara/input/lvgl_input_driver.cpp | 11 +++++++++++ src/tangara/input/lvgl_input_driver.hpp | 3 +-- src/tangara/system_fsm/idle.cpp | 2 +- 8 files changed, 40 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/drivers/include/drivers/touchwheel.hpp b/src/drivers/include/drivers/touchwheel.hpp index 60902087..9cd925a6 100644 --- a/src/drivers/include/drivers/touchwheel.hpp +++ b/src/drivers/include/drivers/touchwheel.hpp @@ -39,7 +39,8 @@ class TouchWheel { auto Update() -> void; auto GetTouchWheelData() const -> TouchWheelData; - auto PowerDown() -> void; + auto Recalibrate() -> void; + auto LowPowerMode(bool en) -> void; private: TouchWheelData data_; diff --git a/src/drivers/touchwheel.cpp b/src/drivers/touchwheel.cpp index 5d55c6f2..402b839d 100644 --- a/src/drivers/touchwheel.cpp +++ b/src/drivers/touchwheel.cpp @@ -137,8 +137,12 @@ TouchWheelData TouchWheel::GetTouchWheelData() const { return data_; } -auto TouchWheel::PowerDown() -> void { - WriteRegister(LOW_POWER, 0); +auto TouchWheel::Recalibrate() -> void { + WriteRegister(CALIBRATE, 1); +} + +auto TouchWheel::LowPowerMode(bool en) -> void { + WriteRegister(LOW_POWER, en ? 0 : 1); } } // namespace drivers diff --git a/src/tangara/input/input_device.hpp b/src/tangara/input/input_device.hpp index da2b31cd..7edded3e 100644 --- a/src/tangara/input/input_device.hpp +++ b/src/tangara/input/input_device.hpp @@ -32,6 +32,11 @@ class IInputDevice { virtual auto triggers() -> std::vector> { return {}; } + + /* Called by the LVGL driver when controls are being locked. */ + virtual auto onLock() -> void {} + /* Called by the LVGL driver when controls are being unlocked. */ + virtual auto onUnlock() -> void {} }; } // namespace input diff --git a/src/tangara/input/input_touch_wheel.cpp b/src/tangara/input/input_touch_wheel.cpp index b961bb02..a5069ae4 100644 --- a/src/tangara/input/input_touch_wheel.cpp +++ b/src/tangara/input/input_touch_wheel.cpp @@ -108,6 +108,15 @@ auto TouchWheel::triggers() return {centre_, up_, right_, down_, left_}; } +auto TouchWheel::onLock() -> void { + wheel_.LowPowerMode(true); +} + +auto TouchWheel::onUnlock() -> void { + wheel_.LowPowerMode(false); + wheel_.Recalibrate(); +} + auto TouchWheel::sensitivity() -> lua::Property& { return sensitivity_; } diff --git a/src/tangara/input/input_touch_wheel.hpp b/src/tangara/input/input_touch_wheel.hpp index cf86eced..d5cdbbfc 100644 --- a/src/tangara/input/input_touch_wheel.hpp +++ b/src/tangara/input/input_touch_wheel.hpp @@ -12,12 +12,12 @@ #include "indev/lv_indev.h" #include "drivers/haptics.hpp" +#include "drivers/nvs.hpp" +#include "drivers/touchwheel.hpp" #include "input/input_device.hpp" #include "input/input_hook.hpp" #include "input/input_trigger.hpp" #include "lua/property.hpp" -#include "drivers/nvs.hpp" -#include "drivers/touchwheel.hpp" namespace input { @@ -30,6 +30,9 @@ class TouchWheel : public IInputDevice { auto name() -> std::string override; auto triggers() -> std::vector> override; + auto onLock() -> void override; + auto onUnlock() -> void override; + auto sensitivity() -> lua::Property&; private: diff --git a/src/tangara/input/lvgl_input_driver.cpp b/src/tangara/input/lvgl_input_driver.cpp index 86f9b279..f6beabda 100644 --- a/src/tangara/input/lvgl_input_driver.cpp +++ b/src/tangara/input/lvgl_input_driver.cpp @@ -132,6 +132,17 @@ auto LvglInputDriver::feedback(uint8_t event) -> void { } } +auto LvglInputDriver::lock(bool l) -> void { + is_locked_ = l; + for (auto&& device : inputs_) { + if (l) { + device->onLock(); + } else { + device->onUnlock(); + } + } +} + LvglInputDriver::LuaTrigger::LuaTrigger(LvglInputDriver& driver, IInputDevice& dev, TriggerHooks& trigger) diff --git a/src/tangara/input/lvgl_input_driver.hpp b/src/tangara/input/lvgl_input_driver.hpp index ddbdee55..9b62c24d 100644 --- a/src/tangara/input/lvgl_input_driver.hpp +++ b/src/tangara/input/lvgl_input_driver.hpp @@ -40,8 +40,7 @@ class LvglInputDriver { auto setGroup(lv_group_t*) -> void; auto read(lv_indev_data_t* data) -> void; auto feedback(uint8_t) -> void; - - auto lock(bool l) -> void { is_locked_ = l; } + auto lock(bool l) -> void; auto pushHooks(lua_State* L) -> int; diff --git a/src/tangara/system_fsm/idle.cpp b/src/tangara/system_fsm/idle.cpp index e499693d..d233f603 100644 --- a/src/tangara/system_fsm/idle.cpp +++ b/src/tangara/system_fsm/idle.cpp @@ -76,7 +76,7 @@ void Idle::react(const internal::IdleTimeout& ev) { // other state machines, etc. auto touchwheel = sServices->touchwheel(); if (touchwheel) { - touchwheel.value()->PowerDown(); + touchwheel.value()->LowPowerMode(true); } auto& gpios = sServices->gpios(); -- cgit v1.2.3 From 2811a3c899fdb5e4e3ba68a242055b1d43c29446 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 7 Aug 2024 14:21:28 +1000 Subject: Don't try to serialise a missing playlist name --- src/tangara/audio/track_queue.cpp | 42 ++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index 399d6717..91bdda39 100644 --- a/src/tangara/audio/track_queue.cpp +++ b/src/tangara/audio/track_queue.cpp @@ -27,8 +27,8 @@ #include "events/event_queue.hpp" #include "memory_resource.hpp" #include "tasks.hpp" -#include "ui/ui_fsm.hpp" #include "track_queue.hpp" +#include "ui/ui_fsm.hpp" namespace audio { @@ -84,7 +84,6 @@ auto notifyChanged(bool current_changed, Reason reason) -> void { events::Audio().Dispatch(ev); } - TrackQueue::TrackQueue(tasks::WorkerPool& bg_worker, database::Handle db) : mutex_(), bg_worker_(bg_worker), @@ -129,8 +128,9 @@ auto TrackQueue::updateShuffler() -> void { } auto TrackQueue::open() -> bool { - // FIX ME: If playlist opening fails, should probably fall back to a vector of tracks or something - // so that we're not necessarily always needing mounted storage + // FIX ME: If playlist opening fails, should probably fall back to a vector of + // tracks or something so that we're not necessarily always needing mounted + // storage return playlist_.open(); } @@ -145,7 +145,8 @@ auto TrackQueue::openPlaylist(const std::string& playlist_file) -> bool { return true; } -auto TrackQueue::getFilepath(database::TrackId id) -> std::optional { +auto TrackQueue::getFilepath(database::TrackId id) + -> std::optional { auto db = db_.lock(); if (!db) { return {}; @@ -153,9 +154,8 @@ auto TrackQueue::getFilepath(database::TrackId id) -> std::optional return db->getTrackPath(id); } - -// TODO WIP: Atm only appends are allowed, this will only ever append regardless of what index -// is given. But it is kept like this for compatability for now. +// TODO WIP: Atm only appends are allowed, this will only ever append regardless +// of what index is given. But it is kept like this for compatability for now. auto TrackQueue::insert(Item i, size_t index) -> void { append(i); } @@ -166,7 +166,7 @@ auto TrackQueue::append(Item i) -> void { { const std::shared_lock lock(mutex_); was_queue_empty = playlist_.currentPosition() >= playlist_.size(); - current_changed = was_queue_empty; // Dont support inserts yet + current_changed = was_queue_empty; // Dont support inserts yet } // If there wasn't anything already playing, then we should make sure we @@ -182,7 +182,7 @@ auto TrackQueue::append(Item i) -> void { if (std::holds_alternative(i)) { { const std::unique_lock lock(mutex_); - auto filename = getFilepath(std::get(i)).value_or(""); + auto filename = getFilepath(std::get(i)).value_or(""); if (!filename.empty()) { playlist_.append(filename); } @@ -204,7 +204,7 @@ auto TrackQueue::append(Item i) -> void { // like current(). { const std::unique_lock lock(mutex_); - auto filename = getFilepath(*next).value_or(""); + auto filename = getFilepath(*next).value_or(""); if (!filename.empty()) { playlist_.append(filename); } @@ -237,7 +237,6 @@ auto TrackQueue::goTo(size_t position) { } } - auto TrackQueue::next(Reason r) -> void { bool changed = true; @@ -350,12 +349,19 @@ auto TrackQueue::replay() const -> bool { auto TrackQueue::serialise() -> std::string { cppbor::Array tracks{}; cppbor::Map encoded; - encoded.add(cppbor::Uint{0}, cppbor::Array{ - cppbor::Bool{repeat_}, - cppbor::Bool{replay_}, - cppbor::Uint{position_}, - cppbor::Tstr{opened_playlist_->filepath()} - }); + + cppbor::Array metadata{ + cppbor::Bool{repeat_}, + cppbor::Bool{replay_}, + cppbor::Uint{position_}, + }; + + if (opened_playlist_) { + metadata.add(cppbor::Tstr{opened_playlist_->filepath()}); + } + + encoded.add(cppbor::Uint{0}, std::move(metadata)); + if (shuffle_) { encoded.add(cppbor::Uint{1}, cppbor::Array{ cppbor::Uint{shuffle_->size()}, -- cgit v1.2.3 From 5d390c821a7fdddec5eb24a7a3a08bf8bf4be1cc Mon Sep 17 00:00:00 2001 From: jacqueline Date: Fri, 9 Aug 2024 13:11:34 +1000 Subject: Claw back some internal ram - 'main' doesn't need 12k of internal ram - lvgl's draw task doesn't need that much either - also lower the bg worker stack sizes whilst we're here, since they've got tons over headroom --- src/tasks/tasks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/tasks/tasks.cpp b/src/tasks/tasks.cpp index b713d70b..9980c97e 100644 --- a/src/tasks/tasks.cpp +++ b/src/tasks/tasks.cpp @@ -64,7 +64,7 @@ auto AllocateStack() -> std::span { // an eye-wateringly large amount of stack. template <> auto AllocateStack() -> std::span { - std::size_t size = 64 * 1024; + std::size_t size = 32 * 1024; return {static_cast(heap_caps_malloc(size, MALLOC_CAP_SPIRAM)), size}; } -- cgit v1.2.3 From d719f9c5017ad8006c21b6d546a5d70e846e9502 Mon Sep 17 00:00:00 2001 From: ailurux Date: Mon, 12 Aug 2024 03:19:03 +0000 Subject: daniel/theme-setting (#87) - Themes can be loaded from disk and built-in - Themes can be selected in a new themes menu of the settings screen - Some touch-ups to existing themes - The saved theme is persisted in nvs Reviewed-on: https://codeberg.org/cool-tech-zone/tangara-fw/pulls/87 Reviewed-by: cooljqln Co-authored-by: ailurux Co-committed-by: ailurux --- src/drivers/include/drivers/nvs.hpp | 5 +++++ src/drivers/nvs.cpp | 39 +++++++++++++++++++++++++++++++++++++ src/tangara/lua/lua_theme.cpp | 33 +++++++++++++++++++++++++++++++ src/tangara/ui/themes.cpp | 5 +++++ src/tangara/ui/themes.hpp | 4 ++++ 5 files changed, 86 insertions(+) (limited to 'src') diff --git a/src/drivers/include/drivers/nvs.hpp b/src/drivers/include/drivers/nvs.hpp index e298ffc3..e147c8c7 100644 --- a/src/drivers/include/drivers/nvs.hpp +++ b/src/drivers/include/drivers/nvs.hpp @@ -113,6 +113,9 @@ class NvsStorage { auto ScreenBrightness() -> uint_fast8_t; auto ScreenBrightness(uint_fast8_t) -> void; + auto InterfaceTheme() -> std::optional; + auto InterfaceTheme(std::string) -> void; + auto ScrollSensitivity() -> uint_fast8_t; auto ScrollSensitivity(uint_fast8_t) -> void; @@ -163,6 +166,8 @@ class NvsStorage { Setting input_mode_; Setting output_mode_; + Setting theme_; + Setting bt_preferred_; Setting> bt_names_; diff --git a/src/drivers/nvs.cpp b/src/drivers/nvs.cpp index 6fac8c61..d004201b 100644 --- a/src/drivers/nvs.cpp +++ b/src/drivers/nvs.cpp @@ -29,6 +29,7 @@ static constexpr char kKeyBluetoothVolumes[] = "bt_vols"; static constexpr char kKeyBluetoothNames[] = "bt_names"; static constexpr char kKeyOutput[] = "out"; static constexpr char kKeyBrightness[] = "bright"; +static constexpr char kKeyInterfaceTheme[] = "ui_theme"; static constexpr char kKeyAmpMaxVolume[] = "hp_vol_max"; static constexpr char kKeyAmpCurrentVolume[] = "hp_vol"; static constexpr char kKeyAmpLeftBias[] = "hp_bias"; @@ -169,6 +170,31 @@ auto Setting>::store( nvs_set_blob(nvs, name_, encoded.data(), encoded.size()); } +template <> +auto Setting::store( + nvs_handle_t nvs, + std::string v) -> void { + cppbor::Tstr cbor{v}; + auto encoded = cbor.encode(); + nvs_set_blob(nvs, name_, encoded.data(), encoded.size()); +} + +template <> +auto Setting::load(nvs_handle_t nvs) + -> std::optional { + auto raw = nvs_get_string(nvs, name_); + if (!raw) { + return {}; + } + auto [parsed, unused, err] = cppbor::parseWithViews( + reinterpret_cast(raw->data()), raw->size()); + if (parsed->type() != cppbor::TSTR) { + return {}; + } + auto v = parsed->asViewTstr()->view(); + return std::string{v.begin(), v.end()}; +} + template <> auto Setting::load(nvs_handle_t nvs) -> std::optional { @@ -248,6 +274,7 @@ NvsStorage::NvsStorage(nvs_handle_t handle) amp_left_bias_(kKeyAmpLeftBias), input_mode_(kKeyPrimaryInput), output_mode_(kKeyOutput), + theme_{kKeyInterfaceTheme}, bt_preferred_(kKeyBluetoothPreferred), bt_names_(kKeyBluetoothNames), db_auto_index_(kKeyDbAutoIndex), @@ -273,6 +300,7 @@ auto NvsStorage::Read() -> void { amp_left_bias_.read(handle_); input_mode_.read(handle_); output_mode_.read(handle_); + theme_.read(handle_); bt_preferred_.read(handle_); bt_names_.read(handle_); db_auto_index_.read(handle_); @@ -293,6 +321,7 @@ auto NvsStorage::Write() -> bool { amp_left_bias_.write(handle_); input_mode_.write(handle_); output_mode_.write(handle_); + theme_.write(handle_); bt_preferred_.write(handle_); bt_names_.write(handle_); db_auto_index_.write(handle_); @@ -466,6 +495,16 @@ auto NvsStorage::ScreenBrightness(uint_fast8_t val) -> void { brightness_.set(val); } +auto NvsStorage::InterfaceTheme() -> std::optional { + std::lock_guard lock{mutex_}; + return theme_.get(); +} + +auto NvsStorage::InterfaceTheme(std::string themeFile) -> void { + std::lock_guard lock{mutex_}; + theme_.set(themeFile); +} + auto NvsStorage::ScrollSensitivity() -> uint_fast8_t { std::lock_guard lock{mutex_}; return std::clamp(sensitivity_.get().value_or(128), 0, 255); diff --git a/src/tangara/lua/lua_theme.cpp b/src/tangara/lua/lua_theme.cpp index 5edde104..03578778 100644 --- a/src/tangara/lua/lua_theme.cpp +++ b/src/tangara/lua/lua_theme.cpp @@ -75,8 +75,41 @@ static auto set_theme(lua_State* L) -> int { return 0; } + +static auto load_theme(lua_State* L) -> int { + std::string filename = luaL_checkstring(L, -1); + // Set the theme filename in non-volatile storage + Bridge* instance = Bridge::Get(L); + // Load the theme using lua + auto status = luaL_loadfile(L, filename.c_str()); + if (status != LUA_OK) { + lua_pushboolean(L, false); + return 1; + } + status = lua::CallProtected(L, 0, 1); + if (status == LUA_OK) { + ui::themes::Theme::instance()->Reset(); + set_theme(L); + instance->services().nvs().InterfaceTheme(filename); + lua_pushboolean(L, true); + } else { + lua_pushboolean(L, false); + } + + return 1; +} + +static auto theme_filename(lua_State* L) -> int { + Bridge* instance = Bridge::Get(L); + auto file = instance->services().nvs().InterfaceTheme().value_or("/lua/theme_light.lua"); + lua_pushstring(L, file.c_str()); + return 1; +} + static const struct luaL_Reg kThemeFuncs[] = {{"set", set_theme}, {"set_style", set_style}, + {"load_theme", load_theme}, + {"theme_filename", theme_filename}, {NULL, NULL}}; static auto lua_theme(lua_State* L) -> int { diff --git a/src/tangara/ui/themes.cpp b/src/tangara/ui/themes.cpp index 726bd5f0..3d532d10 100644 --- a/src/tangara/ui/themes.cpp +++ b/src/tangara/ui/themes.cpp @@ -16,6 +16,7 @@ #include "widgets/bar/lv_bar.h" #include "widgets/button/lv_button.h" #include "widgets/slider/lv_slider.h" +#include "themes.hpp" namespace ui { namespace themes { @@ -81,6 +82,10 @@ void Theme::ApplyStyle(lv_obj_t* obj, std::string style_key) { } } +void Theme::Reset() { + style_map.clear(); +} + auto Theme::instance() -> Theme* { static Theme sTheme{}; return &sTheme; diff --git a/src/tangara/ui/themes.hpp b/src/tangara/ui/themes.hpp index fd576478..4826859e 100644 --- a/src/tangara/ui/themes.hpp +++ b/src/tangara/ui/themes.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include "lvgl.h" @@ -26,12 +27,15 @@ class Theme { void AddStyle(std::string key, int selector, lv_style_t* style); + void Reset(); + static auto instance() -> Theme*; private: Theme(); std::map>> style_map; lv_theme_t theme_; + std::optional filename_; }; } // namespace themes } // namespace ui -- cgit v1.2.3 From f8a3c16aad4e55bd19374c5029b4ac606b07dd7d Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 8 Aug 2024 10:29:46 +1000 Subject: Use one MMU page per leveldb write buffer Also drop some of the other tuning changes, since they don't seem to impact much. --- src/tangara/database/database.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/tangara/database/database.cpp b/src/tangara/database/database.cpp index 85700431..e3f3df67 100644 --- a/src/tangara/database/database.cpp +++ b/src/tangara/database/database.cpp @@ -144,10 +144,10 @@ auto Database::Open(IFileGatherer& gatherer, leveldb::Options options; options.env = sEnv.env(); - options.write_buffer_size = 4 * 1024; - options.max_file_size = 16 * 1024; + // Match the write buffer size to the MMU page size in order to + // make most efficient use of PSRAM mapping. + options.write_buffer_size = CONFIG_MMU_PAGE_SIZE; options.block_cache = cache.get(); - options.block_size = 2048; auto status = leveldb::DB::Open(options, kDbPath, &db); if (!status.ok()) { -- cgit v1.2.3 From b5dc53670a259c3fdf2d3f20f52880f2218221d7 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 8 Aug 2024 12:30:49 +1000 Subject: Derive the next track id from stored track data, instead of tracking it explicitly This saves about 1ms per new track right now, but more importantly means that minting a new track id is now a single atomic operation, rather than being its own database write. This is a useful property that will come in handy in a few commits time. --- src/tangara/database/database.cpp | 56 ++++++++++++++++++++++++++++----------- src/tangara/database/database.hpp | 2 ++ 2 files changed, 42 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/tangara/database/database.cpp b/src/tangara/database/database.cpp index e3f3df67..c543b941 100644 --- a/src/tangara/database/database.cpp +++ b/src/tangara/database/database.cpp @@ -24,6 +24,7 @@ #include "cppbor.h" #include "cppbor_parse.h" #include "database/index.hpp" +#include "debug.hpp" #include "esp_log.h" #include "esp_timer.h" #include "ff.h" @@ -60,7 +61,6 @@ static const char kKeyDbVersion[] = "schema_version"; static const char kKeyCustom[] = "U\0"; static const char kKeyCollator[] = "collator"; -static const char kKeyTrackId[] = "next_track_id"; static std::atomic sIsDbOpen(false); @@ -190,7 +190,10 @@ Database::Database(leveldb::DB* db, file_gatherer_(file_gatherer), tag_parser_(tag_parser), collator_(collator), - is_updating_(false) {} + is_updating_(false) { + dbCalculateNextTrackId(); + ESP_LOGI(kTag, "next track id is %lu", next_track_id_.load()); +} Database::~Database() { // Delete db_ first so that any outstanding background work finishes before @@ -492,24 +495,45 @@ auto Database::isUpdating() -> bool { return is_updating_; } -auto Database::dbMintNewTrackId() -> TrackId { - TrackId next_id = 1; - std::string val; - auto status = db_->Get(leveldb::ReadOptions(), kKeyTrackId, &val); - if (status.ok()) { - next_id = BytesToTrackId(val).value_or(next_id); - } else if (!status.IsNotFound()) { - // TODO(jacqueline): Handle this more. - ESP_LOGE(kTag, "failed to get next track id"); +auto Database::dbCalculateNextTrackId() -> void { + std::unique_ptr it{ + db_->NewIterator(leveldb::ReadOptions())}; + + // Track data entries are of the format 'D/trackid', where track ids are + // encoded as big-endian cbor types. They can therefore be compared through + // byte ordering, which means we can determine what the next id should be by + // looking at the larged track data record in the database. + std::string prefix = EncodeDataPrefix(); + std::string prefixPlusOne = prefix; + prefixPlusOne[prefixPlusOne.size() - 1]++; + + // Seek to just past the track data section. + it->Seek(prefixPlusOne); + if (!it->Valid()) { + next_track_id_ = 1; + return; } - if (!db_->Put(leveldb::WriteOptions(), kKeyTrackId, - TrackIdToBytes(next_id + 1)) - .ok()) { - ESP_LOGE(kTag, "failed to write next track id"); + // Go back to the last track data record. + it->Prev(); + if (!it->Valid() || !it->key().starts_with(prefix)) { + next_track_id_ = 1; + return; } - return next_id; + // Parse the track id back out of the key. + std::span key{it->key().data(), it->key().size()}; + auto id_part = key.subspan(prefix.size()); + if (id_part.empty()) { + next_track_id_ = 1; + return; + } + + next_track_id_ = BytesToTrackId(id_part).value_or(0) + 1; +} + +auto Database::dbMintNewTrackId() -> TrackId { + return next_track_id_++; } auto Database::dbEntomb(TrackId id, uint64_t hash) -> void { diff --git a/src/tangara/database/database.hpp b/src/tangara/database/database.hpp index c2e72568..2b385013 100644 --- a/src/tangara/database/database.hpp +++ b/src/tangara/database/database.hpp @@ -100,6 +100,7 @@ class Database { locale::ICollator& collator_; std::atomic is_updating_; + std::atomic next_track_id_; Database(leveldb::DB* db, leveldb::Cache* cache, @@ -107,6 +108,7 @@ class Database { ITagParser& tag_parser, locale::ICollator& collator); + auto dbCalculateNextTrackId() -> void; auto dbMintNewTrackId() -> TrackId; auto dbEntomb(TrackId track, uint64_t hash) -> void; -- cgit v1.2.3 From 30aaefca64445efa421edb93403036d59382920f Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 8 Aug 2024 14:35:53 +1000 Subject: Batch up the db operations associated with adding new tracks This is ostensibly yet another 'prepare for multithreaded updates' commit, however it does actually save us another 60(!!) odd milliseconds per track. --- src/tangara/database/database.cpp | 183 ++++++++++++++++---------------------- src/tangara/database/database.hpp | 14 +-- src/tangara/database/index.cpp | 57 +++++++----- src/tangara/database/index.hpp | 3 +- src/tangara/database/track.cpp | 11 --- src/tangara/database/track.hpp | 2 - 6 files changed, 122 insertions(+), 148 deletions(-) (limited to 'src') diff --git a/src/tangara/database/database.cpp b/src/tangara/database/database.cpp index c543b941..aec661d9 100644 --- a/src/tangara/database/database.cpp +++ b/src/tangara/database/database.cpp @@ -352,11 +352,19 @@ auto Database::updateIndexes() -> void { // 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); + ESP_LOGI(kTag, "entombing missing #%lx", track->id); + + // Remove the indexes first, so that interrupted operations don't leave + // dangling index records. dbRemoveIndexes(track); + + // Do the rest of the tombstoning as one atomic write. + leveldb::WriteBatch batch; track->is_tombstoned = true; - dbPutTrackData(*track); - db_->Delete(leveldb::WriteOptions{}, EncodePathKey(track->filepath)); + batch.Put(EncodeDataKey(track->id), EncodeDataValue(*track)); + batch.Delete(EncodePathKey(track->filepath)); + + db_->Write(leveldb::WriteOptions(), &batch); continue; } @@ -370,12 +378,20 @@ auto Database::updateIndexes() -> void { // database. ESP_LOGI(kTag, "updating hash (%llx -> %llx)", track->tags_hash, new_hash); + + // Again, we remove the old index records first so has to avoid + // dangling references. dbRemoveIndexes(track); + // Atomically correct the hash + create the new index records. + leveldb::WriteBatch batch; track->tags_hash = new_hash; - dbIngestTagHashes(*tags, track->individual_tag_hashes); - dbPutTrackData(*track); - dbPutHash(new_hash, track->id); + dbIngestTagHashes(*tags, track->individual_tag_hashes, batch); + + dbCreateIndexesForTrack(*track, *tags, batch); + batch.Put(EncodeDataKey(track->id), EncodeDataValue(*track)); + batch.Put(EncodeHashKey(new_hash), EncodeHashValue(track->id)); + db_->Write(leveldb::WriteOptions(), &batch); } } } @@ -404,72 +420,56 @@ auto Database::updateIndexes() -> void { return; } - // Check for any existing record with the same hash. + // Check for any existing track with the same hash. uint64_t hash = tags->Hash(); - std::string key = EncodeHashKey(hash); - std::optional existing_hash; + std::optional existing_id; std::string raw_entry; - if (db_->Get(leveldb::ReadOptions(), key, &raw_entry).ok()) { - existing_hash = ParseHashValue(raw_entry); + if (db_->Get(leveldb::ReadOptions(), EncodeHashKey(hash), &raw_entry) + .ok()) { + existing_id = ParseHashValue(raw_entry); } - std::pair modified{info.fdate, info.ftime}; - 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_LOGD(kTag, "recording new 0x%lx", id); + std::shared_ptr data; + if (existing_id) { + // Do we have any existing data for this track? This could be the case if + // this is a tombstoned entry. In such as case, we want to reuse the + // previous TrackData so that any extra metadata is preserved. + data = dbGetTrackData(*existing_id); + if (!data) { + data = std::make_shared(); + data->id = *existing_id; + } else if (data->filepath != path) { + ESP_LOGW(kTag, "hash collision: %s, %s, %s", + tags->title().value_or("no title").c_str(), + tags->artist().value_or("no artist").c_str(), + tags->album().value_or("no album").c_str()); + // Don't commit anything if there's a hash collision, since we're + // likely to make a big mess. + return; + } + } else { num_new_tracks++; - - 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); - db_->Put(leveldb::WriteOptions{}, EncodePathKey(path), - TrackIdToBytes(id)); - return; + data = std::make_shared(); + data->id = dbMintNewTrackId(); } - 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); - db_->Put(leveldb::WriteOptions{}, EncodePathKey(path), - TrackIdToBytes(new_data->id)); - return; - } + // Make sure the file-based metadata on the TrackData is up to date. + data->filepath = path; + data->tags_hash = hash; + data->modified_at = {info.fdate, info.ftime}; - 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); - db_->Put(leveldb::WriteOptions{}, EncodePathKey(path), - TrackIdToBytes(existing_data->id)); - } else if (existing_data->filepath != - std::pmr::string{path.data(), path.size()}) { - ESP_LOGW(kTag, "hash collision: %s, %s, %s", - tags->title().value_or("no title").c_str(), - tags->artist().value_or("no artist").c_str(), - tags->album().value_or("no album").c_str()); - } + // Apply all the actual database changes as one atomic batch. This makes + // the whole 'new track' operation atomic, and also reduces the amount of + // lock contention when adding many tracks at once. + leveldb::WriteBatch batch; + dbIngestTagHashes(*tags, data->individual_tag_hashes, batch); + + dbCreateIndexesForTrack(*data, *tags, batch); + batch.Put(EncodeDataKey(data->id), EncodeDataValue(*data)); + batch.Put(EncodeHashKey(data->tags_hash), EncodeHashValue(data->id)); + batch.Put(EncodePathKey(path), TrackIdToBytes(data->id)); + + db_->Write(leveldb::WriteOptions(), &batch); }); uint64_t end_time = esp_timer_get_time(); @@ -536,22 +536,6 @@ auto Database::dbMintNewTrackId() -> TrackId { return next_track_id_++; } -auto Database::dbEntomb(TrackId id, uint64_t hash) -> void { - std::string key = EncodeHashKey(hash); - std::string val = EncodeHashValue(id); - if (!db_->Put(leveldb::WriteOptions(), key, val).ok()) { - ESP_LOGE(kTag, "failed to entomb #%llx (id #%lx)", hash, id); - } -} - -auto Database::dbPutTrackData(const TrackData& s) -> void { - std::string key = EncodeDataKey(s.id); - std::string val = EncodeDataValue(s); - if (!db_->Put(leveldb::WriteOptions(), key, val).ok()) { - ESP_LOGE(kTag, "failed to write data for #%lx", s.id); - } -} - auto Database::dbGetTrackData(TrackId id) -> std::shared_ptr { std::string key = EncodeDataKey(id); std::string raw_val; @@ -562,33 +546,19 @@ auto Database::dbGetTrackData(TrackId id) -> std::shared_ptr { return ParseDataValue(raw_val); } -auto Database::dbPutHash(const uint64_t& hash, TrackId i) -> void { - std::string key = EncodeHashKey(hash); - std::string val = EncodeHashValue(i); - if (!db_->Put(leveldb::WriteOptions(), key, val).ok()) { - ESP_LOGE(kTag, "failed to write hash for #%lx", i); - } -} - -auto Database::dbGetHash(const uint64_t& hash) -> std::optional { - std::string key = EncodeHashKey(hash); - std::string raw_val; - if (!db_->Get(leveldb::ReadOptions(), key, &raw_val).ok()) { - ESP_LOGW(kTag, "no key found for hash #%llx", hash); - return {}; - } - return ParseHashValue(raw_val); +auto Database::dbCreateIndexesForTrack(const Track& track, + leveldb::WriteBatch& batch) -> void { + dbCreateIndexesForTrack(track.data(), track.tags(), batch); } -auto Database::dbCreateIndexesForTrack(const Track& track) -> void { +auto Database::dbCreateIndexesForTrack(const TrackData& data, + const TrackTags& tags, + leveldb::WriteBatch& batch) -> void { for (const IndexInfo& index : getIndexes()) { - leveldb::WriteBatch writes; - auto entries = Index(collator_, index, track); + auto entries = Index(collator_, index, data, tags); for (const auto& it : entries) { - writes.Put(EncodeIndexKey(it.first), - {it.second.data(), it.second.size()}); + batch.Put(EncodeIndexKey(it.first), {it.second.data(), it.second.size()}); } - db_->Write(leveldb::WriteOptions(), &writes); } } @@ -597,9 +567,8 @@ auto Database::dbRemoveIndexes(std::shared_ptr data) -> void { if (!tags) { return; } - Track track{data, tags}; for (const IndexInfo& index : getIndexes()) { - auto entries = Index(collator_, index, track); + auto entries = Index(collator_, index, *data, *tags); for (auto it = entries.rbegin(); it != entries.rend(); it++) { auto key = EncodeIndexKey(it->first); auto status = db_->Delete(leveldb::WriteOptions{}, key); @@ -626,16 +595,14 @@ auto Database::dbRemoveIndexes(std::shared_ptr data) -> void { } auto Database::dbIngestTagHashes(const TrackTags& tags, - std::pmr::unordered_map& out) - -> void { - leveldb::WriteBatch batch{}; + std::pmr::unordered_map& out, + leveldb::WriteBatch& batch) -> void { for (const auto& tag : tags.allPresent()) { auto val = tags.get(tag); auto hash = tagHash(val); batch.Put(EncodeTagHashKey(hash), tagToString(val)); out[tag] = hash; } - db_->Write(leveldb::WriteOptions{}, &batch); } auto Database::dbRecoverTagsFromHashes( diff --git a/src/tangara/database/database.hpp b/src/tangara/database/database.hpp index 2b385013..39665dbf 100644 --- a/src/tangara/database/database.hpp +++ b/src/tangara/database/database.hpp @@ -29,6 +29,7 @@ #include "leveldb/iterator.h" #include "leveldb/options.h" #include "leveldb/slice.h" +#include "leveldb/write_batch.h" #include "memory_resource.hpp" #include "result.hpp" #include "tasks.hpp" @@ -111,17 +112,18 @@ class Database { auto dbCalculateNextTrackId() -> void; auto dbMintNewTrackId() -> TrackId; - 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 dbCreateIndexesForTrack(const Track&, leveldb::WriteBatch&) -> void; + auto dbCreateIndexesForTrack(const TrackData&, + const TrackTags&, + leveldb::WriteBatch&) -> void; + auto dbRemoveIndexes(std::shared_ptr) -> void; auto dbIngestTagHashes(const TrackTags&, - std::pmr::unordered_map&) -> void; + std::pmr::unordered_map&, + leveldb::WriteBatch&) -> void; auto dbRecoverTagsFromHashes(const std::pmr::unordered_map&) -> std::shared_ptr; diff --git a/src/tangara/database/index.cpp b/src/tangara/database/index.cpp index 93a2b1c2..dec458f4 100644 --- a/src/tangara/database/index.cpp +++ b/src/tangara/database/index.cpp @@ -52,10 +52,29 @@ const IndexInfo kAllAlbums{ .components = {Tag::kAlbum, Tag::kAlbumOrder}, }; +static auto titleOrFilename(const TrackData& data, const TrackTags& tags) + -> std::pmr::string { + auto title = tags.title(); + if (title) { + return *title; + } + auto start = data.filepath.find_last_of('/'); + if (start == std::pmr::string::npos) { + return data.filepath; + } + return data.filepath.substr(start + 1); +} + class Indexer { public: - Indexer(locale::ICollator& collator, const Track& t, const IndexInfo& idx) - : collator_(collator), track_(t), index_(idx) {} + Indexer(locale::ICollator& collator, + const IndexInfo& idx, + const TrackData& data, + const TrackTags& tags) + : collator_(collator), + index_(idx), + track_data_(data), + track_tags_(tags) {} auto index() -> std::vector>; @@ -70,14 +89,13 @@ class Indexer { auto missing_value(Tag tag) -> TagValue { switch (tag) { case Tag::kTitle: - return track_.TitleOrFilename(); + return titleOrFilename(track_data_, track_tags_); case Tag::kArtist: return "Unknown Artist"; case Tag::kAlbum: return "Unknown Album"; case Tag::kAlbumArtist: - return track_.tags().artist().value_or("Unknown Artist"); - return "Unknown Album"; + return track_tags_.artist().value_or("Unknown Artist"); case Tag::kGenres: return std::pmr::vector{}; case Tag::kDisc: @@ -91,8 +109,9 @@ class Indexer { } locale::ICollator& collator_; - const Track& track_; const IndexInfo index_; + const TrackData& track_data_; + const TrackTags& track_tags_; std::vector> out_; }; @@ -113,7 +132,7 @@ auto Indexer::index() -> std::vector> { auto Indexer::handleLevel(const IndexKey::Header& header, std::span components) -> void { Tag component = components.front(); - TagValue value = track_.tags().get(component); + TagValue value = track_tags_.get(component); if (std::holds_alternative(value)) { value = missing_value(component); } @@ -157,21 +176,17 @@ auto Indexer::handleItem(const IndexKey::Header& header, auto xfrm = collator_.Transform(value); key.item = {xfrm.data(), xfrm.size()}; } else if constexpr (std::is_same_v) { - value = std::to_string(arg); - // FIXME: this sucks lol. we should just write the number directly, - // LSB-first, but then we need to be able to parse it back properly. - std::ostringstream str; - str << std::setw(8) << std::setfill('0') << arg; - std::string encoded = str.str(); - key.item = {encoded.data(), encoded.size()}; + // CBOR's varint encoding actually works great for lexicographical + // sorting. + key.item = cppbor::Uint{arg}.toString(); } }, item); std::optional next_level; if (components.size() == 1) { - value = track_.TitleOrFilename(); - key.track = track_.data().id; + value = titleOrFilename(track_data_, track_tags_); + key.track = track_data_.id; } else { next_level = ExpandHeader(key.header, key.item); } @@ -183,10 +198,12 @@ auto Indexer::handleItem(const IndexKey::Header& header, } } -auto Index(locale::ICollator& c, - const IndexInfo& i, - const Track& t) -> std::vector> { - Indexer indexer{c, t, i}; +auto Index(locale::ICollator& collator, + const IndexInfo& index, + const TrackData& data, + const TrackTags& tags) + -> std::vector> { + Indexer indexer{collator, index, data, tags}; return indexer.index(); } diff --git a/src/tangara/database/index.hpp b/src/tangara/database/index.hpp index 8f78439b..bc01ec2f 100644 --- a/src/tangara/database/index.hpp +++ b/src/tangara/database/index.hpp @@ -63,7 +63,8 @@ struct IndexKey { auto Index(locale::ICollator&, const IndexInfo&, - const Track&) -> std::vector>; + const TrackData&, + const TrackTags&) -> std::vector>; auto ExpandHeader(const IndexKey::Header&, const std::optional&) -> IndexKey::Header; diff --git a/src/tangara/database/track.cpp b/src/tangara/database/track.cpp index 5bf8c3e2..461f4561 100644 --- a/src/tangara/database/track.cpp +++ b/src/tangara/database/track.cpp @@ -293,15 +293,4 @@ auto TrackTags::Hash() const -> uint64_t { return komihash_stream_final(&stream); } -auto Track::TitleOrFilename() const -> std::pmr::string { - auto title = tags().title(); - if (title) { - return *title; - } - auto start = data().filepath.find_last_of('/'); - if (start == std::pmr::string::npos) { - return data().filepath; - } - return data().filepath.substr(start + 1); -} } // namespace database diff --git a/src/tangara/database/track.hpp b/src/tangara/database/track.hpp index b097ab52..6501e31f 100644 --- a/src/tangara/database/track.hpp +++ b/src/tangara/database/track.hpp @@ -195,8 +195,6 @@ class Track { auto data() const -> const TrackData& { return *data_; } auto tags() const -> const TrackTags& { return *tags_; } - auto TitleOrFilename() const -> std::pmr::string; - private: std::shared_ptr data_; std::shared_ptr tags_; -- cgit v1.2.3 From 28cf749951a8f811606bb233efecfd36738c3c89 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 8 Aug 2024 16:08:46 +1000 Subject: Make FileGatherer shaped more like a normal iterator --- src/tangara/audio/audio_fsm.cpp | 1 + src/tangara/database/database.cpp | 34 ++++++------- src/tangara/database/database.hpp | 6 +-- src/tangara/database/file_gatherer.cpp | 72 ---------------------------- src/tangara/database/file_gatherer.hpp | 34 ------------- src/tangara/database/track_finder.cpp | 84 +++++++++++++++++++++++++++++++++ src/tangara/database/track_finder.hpp | 33 +++++++++++++ src/tangara/input/lvgl_input_driver.cpp | 1 + src/tangara/system_fsm/idle.cpp | 4 +- src/tangara/system_fsm/running.cpp | 9 +--- 10 files changed, 141 insertions(+), 137 deletions(-) delete mode 100644 src/tangara/database/file_gatherer.cpp delete mode 100644 src/tangara/database/file_gatherer.hpp create mode 100644 src/tangara/database/track_finder.cpp create mode 100644 src/tangara/database/track_finder.hpp (limited to 'src') diff --git a/src/tangara/audio/audio_fsm.cpp b/src/tangara/audio/audio_fsm.cpp index a43cd932..8da11665 100644 --- a/src/tangara/audio/audio_fsm.cpp +++ b/src/tangara/audio/audio_fsm.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "audio/audio_source.hpp" diff --git a/src/tangara/database/database.cpp b/src/tangara/database/database.cpp index aec661d9..2d72fe95 100644 --- a/src/tangara/database/database.cpp +++ b/src/tangara/database/database.cpp @@ -24,6 +24,7 @@ #include "cppbor.h" #include "cppbor_parse.h" #include "database/index.hpp" +#include "database/track_finder.hpp" #include "debug.hpp" #include "esp_log.h" #include "esp_timer.h" @@ -40,7 +41,6 @@ #include "database/db_events.hpp" #include "database/env_esp.hpp" -#include "database/file_gatherer.hpp" #include "database/records.hpp" #include "database/tag_parser.hpp" #include "database/track.hpp" @@ -122,8 +122,7 @@ static auto CheckDatabase(leveldb::DB& db, locale::ICollator& col) -> bool { return true; } -auto Database::Open(IFileGatherer& gatherer, - ITagParser& parser, +auto Database::Open(ITagParser& parser, locale::ICollator& collator, tasks::WorkerPool& bg_worker) -> cpp::result { @@ -168,8 +167,7 @@ auto Database::Open(IFileGatherer& gatherer, } ESP_LOGI(kTag, "Database opened successfully"); - return new Database(db, cache.release(), gatherer, parser, - collator); + return new Database(db, cache.release(), parser, collator); }) .get(); } @@ -182,12 +180,10 @@ auto Database::Destroy() -> void { Database::Database(leveldb::DB* db, leveldb::Cache* cache, - IFileGatherer& file_gatherer, ITagParser& tag_parser, locale::ICollator& collator) : db_(db), cache_(cache), - file_gatherer_(file_gatherer), tag_parser_(tag_parser), collator_(collator), is_updating_(false) { @@ -401,7 +397,11 @@ auto Database::updateIndexes() -> void { // Stage 2: search for newly added files. ESP_LOGI(kTag, "scanning for new tracks"); uint64_t num_files = 0; - file_gatherer_.FindFiles("", [&](std::string_view path, const FILINFO& info) { + + auto track_finder = std::make_shared(""); + + FILINFO info; + while (auto path = track_finder->next(info)) { num_files++; events::Ui().Dispatch(event::UpdateProgress{ .stage = event::UpdateProgress::Stage::kScanningForNewTracks, @@ -409,15 +409,15 @@ auto Database::updateIndexes() -> void { }); std::string unused; - if (db_->Get(read_options, EncodePathKey(path), &unused).ok()) { + if (db_->Get(read_options, EncodePathKey(*path), &unused).ok()) { // This file is already in the database; skip it. - return; + continue; } - std::shared_ptr tags = tag_parser_.ReadAndParseTags(path); + std::shared_ptr tags = tag_parser_.ReadAndParseTags(*path); if (!tags || tags->encoding() == Container::kUnsupported) { // No parseable tags; skip this fiile. - return; + continue; } // Check for any existing track with the same hash. @@ -438,14 +438,14 @@ auto Database::updateIndexes() -> void { if (!data) { data = std::make_shared(); data->id = *existing_id; - } else if (data->filepath != path) { + } else if (std::string_view{data->filepath} != *path) { ESP_LOGW(kTag, "hash collision: %s, %s, %s", tags->title().value_or("no title").c_str(), tags->artist().value_or("no artist").c_str(), tags->album().value_or("no album").c_str()); // Don't commit anything if there's a hash collision, since we're // likely to make a big mess. - return; + continue; } } else { num_new_tracks++; @@ -454,7 +454,7 @@ auto Database::updateIndexes() -> void { } // Make sure the file-based metadata on the TrackData is up to date. - data->filepath = path; + data->filepath = *path; data->tags_hash = hash; data->modified_at = {info.fdate, info.ftime}; @@ -467,10 +467,10 @@ auto Database::updateIndexes() -> void { dbCreateIndexesForTrack(*data, *tags, batch); batch.Put(EncodeDataKey(data->id), EncodeDataValue(*data)); batch.Put(EncodeHashKey(data->tags_hash), EncodeHashValue(data->id)); - batch.Put(EncodePathKey(path), TrackIdToBytes(data->id)); + batch.Put(EncodePathKey(*path), TrackIdToBytes(data->id)); db_->Write(leveldb::WriteOptions(), &batch); - }); + }; uint64_t end_time = esp_timer_get_time(); diff --git a/src/tangara/database/database.hpp b/src/tangara/database/database.hpp index 39665dbf..6994d0b8 100644 --- a/src/tangara/database/database.hpp +++ b/src/tangara/database/database.hpp @@ -19,7 +19,6 @@ #include "collation.hpp" #include "cppbor.h" -#include "database/file_gatherer.hpp" #include "database/index.hpp" #include "database/records.hpp" #include "database/tag_parser.hpp" @@ -56,8 +55,7 @@ class Database { ALREADY_OPEN, FAILED_TO_OPEN, }; - static auto Open(IFileGatherer& file_gatherer, - ITagParser& tag_parser, + static auto Open(ITagParser& tag_parser, locale::ICollator& collator, tasks::WorkerPool& bg_worker) -> cpp::result; @@ -96,7 +94,6 @@ class Database { leveldb::Cache* cache_; // Not owned. - IFileGatherer& file_gatherer_; ITagParser& tag_parser_; locale::ICollator& collator_; @@ -105,7 +102,6 @@ class Database { Database(leveldb::DB* db, leveldb::Cache* cache, - IFileGatherer& file_gatherer, ITagParser& tag_parser, locale::ICollator& collator); diff --git a/src/tangara/database/file_gatherer.cpp b/src/tangara/database/file_gatherer.cpp deleted file mode 100644 index dd4b1138..00000000 --- a/src/tangara/database/file_gatherer.cpp +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#include "database/file_gatherer.hpp" - -#include -#include -#include -#include - -#include "ff.h" - -#include "drivers/spi.hpp" -#include "memory_resource.hpp" - -namespace database { - -static_assert(sizeof(TCHAR) == sizeof(char), "TCHAR must be CHAR"); - -auto FileGathererImpl::FindFiles( - const std::string& root, - std::function cb) -> void { - std::pmr::deque to_explore{&memory::kSpiRamResource}; - to_explore.push_back({root.data(), root.size()}); - - while (!to_explore.empty()) { - auto next_path_str = to_explore.front(); - to_explore.pop_front(); - - const TCHAR* next_path = static_cast(next_path_str.c_str()); - - FF_DIR dir; - FRESULT res = f_opendir(&dir, next_path); - if (res != FR_OK) { - // TODO: log. - continue; - } - - for (;;) { - FILINFO info; - res = f_readdir(&dir, &info); - if (res != FR_OK || info.fname[0] == 0) { - // No more files in the directory. - break; - } else if (info.fattrib & (AM_HID | AM_SYS) || info.fname[0] == '.') { - // System or hidden file. Ignore it and move on. - continue; - } else { - std::pmr::string full_path{&memory::kSpiRamResource}; - full_path += next_path_str; - full_path += "/"; - full_path += info.fname; - - if (info.fattrib & AM_DIR) { - // This is a directory. Add it to the explore queue. - to_explore.push_back(full_path); - } else { - // This is a file! Let the callback know about it. - // std::invoke(cb, full_path.str(), info); - std::invoke(cb, full_path, info); - } - } - } - - f_closedir(&dir); - } -} - -} // namespace database diff --git a/src/tangara/database/file_gatherer.hpp b/src/tangara/database/file_gatherer.hpp deleted file mode 100644 index 38558b9e..00000000 --- a/src/tangara/database/file_gatherer.hpp +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#pragma once - -#include -#include -#include -#include - -#include "ff.h" - -namespace database { - -class IFileGatherer { - public: - virtual ~IFileGatherer() {}; - - virtual auto FindFiles( - const std::string& root, - std::function cb) -> void = 0; -}; - -class FileGathererImpl : public IFileGatherer { - public: - virtual auto FindFiles(const std::string& root, - std::function - cb) -> void override; -}; - -} // namespace database diff --git a/src/tangara/database/track_finder.cpp b/src/tangara/database/track_finder.cpp new file mode 100644 index 00000000..86948e70 --- /dev/null +++ b/src/tangara/database/track_finder.cpp @@ -0,0 +1,84 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "database/track_finder.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "database/track_finder.hpp" +#include "ff.h" + +#include "drivers/spi.hpp" +#include "memory_resource.hpp" + +namespace database { + +static_assert(sizeof(TCHAR) == sizeof(char), "TCHAR must be CHAR"); + +TrackFinder::TrackFinder(std::string_view root) + : to_explore_(&memory::kSpiRamResource) { + to_explore_.push_back({root.data(), root.size()}); +} + +auto TrackFinder::next(FILINFO& out_info) -> std::optional { + std::scoped_lock lock{mut_}; + while (!to_explore_.empty() || current_) { + if (!current_) { + current_.emplace(); + + // Get the next directory to iterate through. + current_->first = to_explore_.front(); + to_explore_.pop_front(); + const TCHAR* next_path = + static_cast(current_->first.data()); + + // Open it for iterating. + FRESULT res = f_opendir(¤t_->second, next_path); + if (res != FR_OK) { + current_.reset(); + continue; + } + } + + FILINFO info; + FRESULT res = f_readdir(¤t_->second, &info); + if (res != FR_OK || info.fname[0] == 0) { + // No more files in the directory. + f_closedir(¤t_->second); + current_.reset(); + continue; + } else if (info.fattrib & (AM_HID | AM_SYS) || info.fname[0] == '.') { + // System or hidden file. Ignore it and move on. + continue; + } else { + // A valid file or folder. + std::pmr::string full_path{&memory::kSpiRamResource}; + full_path += current_->first; + full_path += "/"; + full_path += info.fname; + + if (info.fattrib & AM_DIR) { + // This is a directory. Add it to the explore queue. + to_explore_.push_back(full_path); + } else { + // This is a file! We can return now. + out_info = info; + return {{full_path.data(), full_path.size()}}; + } + } + } + + // Out of things to explore. + return {}; +} + +} // namespace database diff --git a/src/tangara/database/track_finder.hpp b/src/tangara/database/track_finder.hpp new file mode 100644 index 00000000..aba208e9 --- /dev/null +++ b/src/tangara/database/track_finder.hpp @@ -0,0 +1,33 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "ff.h" + +namespace database { + +class TrackFinder { + public: + TrackFinder(std::string_view root); + + auto next(FILINFO&) -> std::optional; + + private: + std::mutex mut_; + std::pmr::deque to_explore_; + std::optional> current_; +}; + +} // namespace database diff --git a/src/tangara/input/lvgl_input_driver.cpp b/src/tangara/input/lvgl_input_driver.cpp index f6beabda..824e49cc 100644 --- a/src/tangara/input/lvgl_input_driver.cpp +++ b/src/tangara/input/lvgl_input_driver.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include "core/lv_group.h" diff --git a/src/tangara/system_fsm/idle.cpp b/src/tangara/system_fsm/idle.cpp index d233f603..2d66b01d 100644 --- a/src/tangara/system_fsm/idle.cpp +++ b/src/tangara/system_fsm/idle.cpp @@ -4,8 +4,9 @@ * SPDX-License-Identifier: GPL-3.0-only */ +#include "ui/ui_fsm.hpp" + #include "app_console/app_console.hpp" -#include "database/file_gatherer.hpp" #include "drivers/gpios.hpp" #include "freertos/portmacro.h" #include "freertos/projdefs.h" @@ -17,7 +18,6 @@ #include "events/event_queue.hpp" #include "system_fsm/system_events.hpp" #include "system_fsm/system_fsm.hpp" -#include "ui/ui_fsm.hpp" namespace system_fsm { namespace states { diff --git a/src/tangara/system_fsm/running.cpp b/src/tangara/system_fsm/running.cpp index c808e9da..f9bca074 100644 --- a/src/tangara/system_fsm/running.cpp +++ b/src/tangara/system_fsm/running.cpp @@ -8,7 +8,6 @@ #include "audio/audio_events.hpp" #include "database/database.hpp" #include "database/db_events.hpp" -#include "database/file_gatherer.hpp" #include "drivers/gpios.hpp" #include "drivers/spi.hpp" #include "ff.h" @@ -36,8 +35,6 @@ static void timer_callback(TimerHandle_t timer) { events::System().Dispatch(internal::UnmountTimeout{}); } -static database::IFileGatherer* sFileGatherer; - void Running::entry() { if (!sUnmountTimer) { sUnmountTimer = xTimerCreate("unmount_timeout", kTicksBeforeUnmount, false, @@ -174,10 +171,8 @@ auto Running::mountStorage() -> void { sStorage.reset(storage_res.value()); ESP_LOGI(kTag, "opening database"); - sFileGatherer = new database::FileGathererImpl(); - auto database_res = - database::Database::Open(*sFileGatherer, sServices->tag_parser(), - sServices->collator(), sServices->bg_worker()); + auto database_res = database::Database::Open( + sServices->tag_parser(), sServices->collator(), sServices->bg_worker()); if (database_res.has_error()) { unmountStorage(); return; -- cgit v1.2.3 From 2ad83cb2108dc55c9eb0573b0645513a1e8a61f5 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Fri, 9 Aug 2024 11:43:48 +1000 Subject: Shard searching for new tracks across multiple tasks This also has the effect of breaking up the enormous 'updateIndexes' method into one call per file, which means database updates also no longer monopolise a single background task for their entire duration. avg. time per new file is now <140ms for a completely fresh database, which is pretty good i think! --- src/tangara/database/database.cpp | 253 ++++++++++++++++++---------------- src/tangara/database/database.hpp | 30 +++- src/tangara/database/track_finder.cpp | 44 +++++- src/tangara/database/track_finder.hpp | 50 ++++++- 4 files changed, 247 insertions(+), 130 deletions(-) (limited to 'src') diff --git a/src/tangara/database/database.cpp b/src/tangara/database/database.cpp index 2d72fe95..491ad8b7 100644 --- a/src/tangara/database/database.cpp +++ b/src/tangara/database/database.cpp @@ -6,9 +6,6 @@ #include "database/database.hpp" -#include -#include - #include #include #include @@ -20,12 +17,8 @@ #include #include -#include "collation.hpp" #include "cppbor.h" #include "cppbor_parse.h" -#include "database/index.hpp" -#include "database/track_finder.hpp" -#include "debug.hpp" #include "esp_log.h" #include "esp_timer.h" #include "ff.h" @@ -39,12 +32,14 @@ #include "leveldb/status.h" #include "leveldb/write_batch.h" +#include "collation.hpp" #include "database/db_events.hpp" #include "database/env_esp.hpp" +#include "database/index.hpp" #include "database/records.hpp" #include "database/tag_parser.hpp" #include "database/track.hpp" -#include "drivers/spi.hpp" +#include "database/track_finder.hpp" #include "events/event_queue.hpp" #include "memory_resource.hpp" #include "result.hpp" @@ -58,12 +53,16 @@ static SingletonEnv sEnv; static const char kDbPath[] = "/.tangara-db"; static const char kKeyDbVersion[] = "schema_version"; - static const char kKeyCustom[] = "U\0"; static const char kKeyCollator[] = "collator"; +static constexpr size_t kMaxParallelism = 2; + static std::atomic sIsDbOpen(false); +using std::placeholders::_1; +using std::placeholders::_2; + static auto CreateNewDatabase(leveldb::Options& options, locale::ICollator& col) -> leveldb::DB* { Database::Destroy(); @@ -167,7 +166,8 @@ auto Database::Open(ITagParser& parser, } ESP_LOGI(kTag, "Database opened successfully"); - return new Database(db, cache.release(), parser, collator); + return new Database(db, cache.release(), bg_worker, parser, + collator); }) .get(); } @@ -180,15 +180,20 @@ auto Database::Destroy() -> void { Database::Database(leveldb::DB* db, leveldb::Cache* cache, + tasks::WorkerPool& pool, ITagParser& tag_parser, locale::ICollator& collator) : db_(db), cache_(cache), + track_finder_( + pool, + kMaxParallelism, + std::bind(&Database::processCandidateCallback, this, _1, _2), + std::bind(&Database::indexingCompleteCallback, this)), tag_parser_(tag_parser), collator_(collator), is_updating_(false) { dbCalculateNextTrackId(); - ESP_LOGI(kTag, "next track id is %lu", next_track_id_.load()); } Database::~Database() { @@ -243,7 +248,7 @@ auto Database::get(const std::string& key) -> std::optional { } auto Database::getTrackPath(TrackId id) -> std::optional { - auto track_data = dbGetTrackData(id); + auto track_data = dbGetTrackData(leveldb::ReadOptions(), id); if (!track_data) { return {}; } @@ -251,7 +256,7 @@ auto Database::getTrackPath(TrackId id) -> std::optional { } auto Database::getTrack(TrackId id) -> std::shared_ptr { - std::shared_ptr data = dbGetTrackData(id); + std::shared_ptr data = dbGetTrackData(leveldb::ReadOptions(), id); if (!data || data->is_tombstoned) { return {}; } @@ -274,34 +279,61 @@ auto Database::getIndexes() -> std::vector { }; } -class UpdateNotifier { - public: - UpdateNotifier(std::atomic& is_updating) : is_updating_(is_updating) { - events::Ui().Dispatch(event::UpdateStarted{}); - events::System().Dispatch(event::UpdateStarted{}); +Database::UpdateTracker::UpdateTracker() + : num_old_tracks_(0), + num_new_tracks_(0), + start_time_(esp_timer_get_time()) { + events::Ui().Dispatch(event::UpdateStarted{}); + events::System().Dispatch(event::UpdateStarted{}); +} + +Database::UpdateTracker::~UpdateTracker() { + uint64_t end_time = esp_timer_get_time(); + + uint64_t time_per_old = 0; + if (num_old_tracks_) { + time_per_old = (verification_finish_time_ - start_time_) / num_old_tracks_; } - ~UpdateNotifier() { - is_updating_ = false; - events::Ui().Dispatch(event::UpdateFinished{}); - events::System().Dispatch(event::UpdateFinished{}); + uint64_t time_per_new = 0; + if (num_new_tracks_) { + time_per_new = (end_time - verification_finish_time_) / num_new_tracks_; } - private: - std::atomic& is_updating_; -}; + ESP_LOGI( + kTag, + "processed %lu old tracks and %lu new tracks in %llu seconds (%llums " + "per old, %llums per new)", + num_old_tracks_, num_new_tracks_, (end_time - start_time_) / 1000000, + time_per_old / 1000, time_per_new / 1000); + + events::Ui().Dispatch(event::UpdateFinished{}); + events::System().Dispatch(event::UpdateFinished{}); +} + +auto Database::UpdateTracker::onTrackVerified() -> void { + events::Ui().Dispatch(event::UpdateProgress{ + .stage = event::UpdateProgress::Stage::kVerifyingExistingTracks, + .val = ++num_old_tracks_, + }); +} + +auto Database::UpdateTracker::onVerificationFinished() -> void { + verification_finish_time_ = esp_timer_get_time(); +} + +auto Database::UpdateTracker::onTrackAdded() -> void { + num_new_tracks_++; +} auto Database::updateIndexes() -> void { if (is_updating_.exchange(true)) { return; } - UpdateNotifier notifier{is_updating_}; - - uint32_t num_old_tracks = 0; - uint32_t num_new_tracks = 0; - uint64_t start_time = esp_timer_get_time(); + update_tracker_ = std::make_unique(); leveldb::ReadOptions read_options; - read_options.fill_cache = true; + read_options.fill_cache = false; + read_options.verify_checksums = true; // Stage 1: verify all existing tracks are still valid. ESP_LOGI(kTag, "verifying existing tracks"); @@ -310,11 +342,7 @@ auto Database::updateIndexes() -> void { std::string prefix = EncodeDataPrefix(); for (it->Seek(prefix); it->Valid() && it->key().starts_with(prefix); it->Next()) { - num_old_tracks++; - events::Ui().Dispatch(event::UpdateProgress{ - .stage = event::UpdateProgress::Stage::kVerifyingExistingTracks, - .val = num_old_tracks, - }); + update_tracker_->onTrackVerified(); std::shared_ptr track = ParseDataValue(it->value()); if (!track) { @@ -325,7 +353,6 @@ auto Database::updateIndexes() -> void { } if (track->is_tombstoned) { - ESP_LOGW(kTag, "skipping tombstoned %lx", track->id); continue; } @@ -392,103 +419,86 @@ auto Database::updateIndexes() -> void { } } - uint64_t verify_end_time = esp_timer_get_time(); + update_tracker_->onVerificationFinished(); // Stage 2: search for newly added files. ESP_LOGI(kTag, "scanning for new tracks"); - uint64_t num_files = 0; - - auto track_finder = std::make_shared(""); + track_finder_.launch(""); +}; - FILINFO info; - while (auto path = track_finder->next(info)) { - num_files++; - events::Ui().Dispatch(event::UpdateProgress{ - .stage = event::UpdateProgress::Stage::kScanningForNewTracks, - .val = num_files, - }); +auto Database::processCandidateCallback(FILINFO& info, std::string_view path) + -> void { + leveldb::ReadOptions read_options; + read_options.fill_cache = true; + read_options.verify_checksums = false; - std::string unused; - if (db_->Get(read_options, EncodePathKey(*path), &unused).ok()) { - // This file is already in the database; skip it. - continue; - } + std::string unused; + if (db_->Get(read_options, EncodePathKey(path), &unused).ok()) { + // This file is already in the database; skip it. + return; + } - std::shared_ptr tags = tag_parser_.ReadAndParseTags(*path); - if (!tags || tags->encoding() == Container::kUnsupported) { - // No parseable tags; skip this fiile. - continue; - } + std::shared_ptr tags = tag_parser_.ReadAndParseTags(path); + if (!tags || tags->encoding() == Container::kUnsupported) { + // No parseable tags; skip this fiile. + return; + } - // Check for any existing track with the same hash. - uint64_t hash = tags->Hash(); - std::optional existing_id; - std::string raw_entry; - if (db_->Get(leveldb::ReadOptions(), EncodeHashKey(hash), &raw_entry) - .ok()) { - existing_id = ParseHashValue(raw_entry); - } + // Check for any existing track with the same hash. + uint64_t hash = tags->Hash(); + std::optional existing_id; + std::string raw_entry; + if (db_->Get(read_options, EncodeHashKey(hash), &raw_entry).ok()) { + existing_id = ParseHashValue(raw_entry); + } - std::shared_ptr data; - if (existing_id) { - // Do we have any existing data for this track? This could be the case if - // this is a tombstoned entry. In such as case, we want to reuse the - // previous TrackData so that any extra metadata is preserved. - data = dbGetTrackData(*existing_id); - if (!data) { - data = std::make_shared(); - data->id = *existing_id; - } else if (std::string_view{data->filepath} != *path) { - ESP_LOGW(kTag, "hash collision: %s, %s, %s", - tags->title().value_or("no title").c_str(), - tags->artist().value_or("no artist").c_str(), - tags->album().value_or("no album").c_str()); - // Don't commit anything if there's a hash collision, since we're - // likely to make a big mess. - continue; - } - } else { - num_new_tracks++; + std::shared_ptr data; + if (existing_id) { + // Do we have any existing data for this track? This could be the case if + // this is a tombstoned entry. In such as case, we want to reuse the + // previous TrackData so that any extra metadata is preserved. + data = dbGetTrackData(read_options, *existing_id); + if (!data) { data = std::make_shared(); - data->id = dbMintNewTrackId(); + data->id = *existing_id; + } else if (data->filepath != path && !data->is_tombstoned) { + ESP_LOGW(kTag, "hash collision: %s, %s, %s", + tags->title().value_or("no title").c_str(), + tags->artist().value_or("no artist").c_str(), + tags->album().value_or("no album").c_str()); + // Don't commit anything if there's a hash collision, since we're + // likely to make a big mess. + return; } + } else { + update_tracker_->onTrackAdded(); + data = std::make_shared(); + data->id = dbMintNewTrackId(); + } - // Make sure the file-based metadata on the TrackData is up to date. - data->filepath = *path; - data->tags_hash = hash; - data->modified_at = {info.fdate, info.ftime}; - - // Apply all the actual database changes as one atomic batch. This makes - // the whole 'new track' operation atomic, and also reduces the amount of - // lock contention when adding many tracks at once. - leveldb::WriteBatch batch; - dbIngestTagHashes(*tags, data->individual_tag_hashes, batch); - - dbCreateIndexesForTrack(*data, *tags, batch); - batch.Put(EncodeDataKey(data->id), EncodeDataValue(*data)); - batch.Put(EncodeHashKey(data->tags_hash), EncodeHashValue(data->id)); - batch.Put(EncodePathKey(*path), TrackIdToBytes(data->id)); + // Make sure the file-based metadata on the TrackData is up to date. + data->filepath = path; + data->tags_hash = hash; + data->modified_at = {info.fdate, info.ftime}; + data->is_tombstoned = false; - db_->Write(leveldb::WriteOptions(), &batch); - }; + // Apply all the actual database changes as one atomic batch. This makes + // the whole 'new track' operation atomic, and also reduces the amount of + // lock contention when adding many tracks at once. + leveldb::WriteBatch batch; + dbIngestTagHashes(*tags, data->individual_tag_hashes, batch); - uint64_t end_time = esp_timer_get_time(); + dbCreateIndexesForTrack(*data, *tags, batch); + batch.Put(EncodeDataKey(data->id), EncodeDataValue(*data)); + batch.Put(EncodeHashKey(data->tags_hash), EncodeHashValue(data->id)); + batch.Put(EncodePathKey(path), TrackIdToBytes(data->id)); - uint64_t time_per_old = 0; - if (num_old_tracks) { - time_per_old = (verify_end_time - start_time) / num_old_tracks; - } - uint64_t time_per_new = 0; - if (num_new_tracks) { - time_per_new = (end_time - verify_end_time) / num_new_tracks; - } + db_->Write(leveldb::WriteOptions(), &batch); +} - ESP_LOGI( - kTag, - "processed %lu old tracks and %lu new tracks in %llu seconds (%llums " - "per old, %llums per new)", - num_old_tracks, num_new_tracks, (end_time - start_time) / 1000000, - time_per_old / 1000, time_per_new / 1000); +auto Database::indexingCompleteCallback() -> void { + update_tracker_.reset(); + is_updating_ = false; } auto Database::isUpdating() -> bool { @@ -536,10 +546,11 @@ auto Database::dbMintNewTrackId() -> TrackId { return next_track_id_++; } -auto Database::dbGetTrackData(TrackId id) -> std::shared_ptr { +auto Database::dbGetTrackData(leveldb::ReadOptions options, TrackId id) + -> std::shared_ptr { std::string key = EncodeDataKey(id); std::string raw_val; - if (!db_->Get(leveldb::ReadOptions(), key, &raw_val).ok()) { + if (!db_->Get(options, key, &raw_val).ok()) { ESP_LOGW(kTag, "no key found for #%lx", id); return {}; } diff --git a/src/tangara/database/database.hpp b/src/tangara/database/database.hpp index 6994d0b8..6daffd23 100644 --- a/src/tangara/database/database.hpp +++ b/src/tangara/database/database.hpp @@ -23,6 +23,8 @@ #include "database/records.hpp" #include "database/tag_parser.hpp" #include "database/track.hpp" +#include "database/track_finder.hpp" +#include "ff.h" #include "leveldb/cache.h" #include "leveldb/db.h" #include "leveldb/iterator.h" @@ -93,22 +95,48 @@ class Database { leveldb::DB* db_; leveldb::Cache* cache_; + TrackFinder track_finder_; + // Not owned. ITagParser& tag_parser_; locale::ICollator& collator_; + /* Internal utility for tracking a currently in-progress index update. */ + class UpdateTracker { + public: + UpdateTracker(); + ~UpdateTracker(); + + auto onTrackVerified() -> void; + auto onVerificationFinished() -> void; + auto onTrackAdded() -> void; + + private: + uint32_t num_old_tracks_; + uint32_t num_new_tracks_; + uint64_t start_time_; + uint64_t verification_finish_time_; + }; + std::atomic is_updating_; + std::unique_ptr update_tracker_; + std::atomic next_track_id_; Database(leveldb::DB* db, leveldb::Cache* cache, + tasks::WorkerPool& pool, ITagParser& tag_parser, locale::ICollator& collator); + auto processCandidateCallback(FILINFO&, std::string_view) -> void; + auto indexingCompleteCallback() -> void; + auto dbCalculateNextTrackId() -> void; auto dbMintNewTrackId() -> TrackId; - auto dbGetTrackData(TrackId id) -> std::shared_ptr; + auto dbGetTrackData(leveldb::ReadOptions, TrackId id) + -> std::shared_ptr; auto dbCreateIndexesForTrack(const Track&, leveldb::WriteBatch&) -> void; auto dbCreateIndexesForTrack(const TrackData&, diff --git a/src/tangara/database/track_finder.cpp b/src/tangara/database/track_finder.cpp index 86948e70..21a44339 100644 --- a/src/tangara/database/track_finder.cpp +++ b/src/tangara/database/track_finder.cpp @@ -24,12 +24,12 @@ namespace database { static_assert(sizeof(TCHAR) == sizeof(char), "TCHAR must be CHAR"); -TrackFinder::TrackFinder(std::string_view root) +CandidateIterator::CandidateIterator(std::string_view root) : to_explore_(&memory::kSpiRamResource) { to_explore_.push_back({root.data(), root.size()}); } -auto TrackFinder::next(FILINFO& out_info) -> std::optional { +auto CandidateIterator::next(FILINFO& info) -> std::optional { std::scoped_lock lock{mut_}; while (!to_explore_.empty() || current_) { if (!current_) { @@ -49,7 +49,6 @@ auto TrackFinder::next(FILINFO& out_info) -> std::optional { } } - FILINFO info; FRESULT res = f_readdir(¤t_->second, &info); if (res != FR_OK || info.fname[0] == 0) { // No more files in the directory. @@ -71,14 +70,49 @@ auto TrackFinder::next(FILINFO& out_info) -> std::optional { to_explore_.push_back(full_path); } else { // This is a file! We can return now. - out_info = info; return {{full_path.data(), full_path.size()}}; } } } - // Out of things to explore. + // Out of paths to explore. return {}; } +TrackFinder::TrackFinder( + tasks::WorkerPool& pool, + size_t parallelism, + std::function processor, + std::function complete_cb) + : pool_{pool}, + parallelism_(parallelism), + processor_(processor), + complete_cb_(complete_cb) {} + +auto TrackFinder::launch(std::string_view root) -> void { + iterator_ = std::make_unique(root); + num_workers_ = parallelism_; + for (size_t i = 0; i < parallelism_; i++) { + schedule(); + } +} + +auto TrackFinder::schedule() -> void { + pool_.Dispatch([&]() { + FILINFO info; + auto next = iterator_->next(info); + if (next) { + std::invoke(processor_, info, *next); + schedule(); + } else { + std::scoped_lock lock{workers_mutex_}; + num_workers_ -= 1; + if (num_workers_ == 0) { + iterator_.reset(); + std::invoke(complete_cb_); + } + } + }); +} + } // namespace database diff --git a/src/tangara/database/track_finder.hpp b/src/tangara/database/track_finder.hpp index aba208e9..daaaa2f2 100644 --- a/src/tangara/database/track_finder.hpp +++ b/src/tangara/database/track_finder.hpp @@ -16,13 +16,27 @@ #include "ff.h" +#include "tasks.hpp" + namespace database { -class TrackFinder { +/* + * Iterator that recursively stats every file within the given directory root. + */ +class CandidateIterator { public: - TrackFinder(std::string_view root); + CandidateIterator(std::string_view root); + + /* + * Returns the next file. The stat result is placed within `out`. If the + * iterator has finished, returns absent. This method always modifies the + * contents of `out`, even if no file is returned. + */ + auto next(FILINFO& out) -> std::optional; - auto next(FILINFO&) -> std::optional; + // Cannot be copied or moved. + CandidateIterator(const CandidateIterator&) = delete; + CandidateIterator& operator=(const CandidateIterator&) = delete; private: std::mutex mut_; @@ -30,4 +44,34 @@ class TrackFinder { std::optional> current_; }; +/* + * Utility for iterating through each file within a directory root. Iteration + * can be sharded across several tasks. + */ +class TrackFinder { + public: + TrackFinder(tasks::WorkerPool&, + size_t parallelism, + std::function processor, + std::function complete_cb); + + auto launch(std::string_view root) -> void; + + // Cannot be copied or moved. + TrackFinder(const TrackFinder&) = delete; + TrackFinder& operator=(const TrackFinder&) = delete; + + private: + tasks::WorkerPool& pool_; + const size_t parallelism_; + const std::function processor_; + const std::function complete_cb_; + + std::mutex workers_mutex_; + std::unique_ptr iterator_; + size_t num_workers_; + + auto schedule() -> void; +}; + } // namespace database -- cgit v1.2.3 From 4fd15f148a86a748f92ce60fa3c6255700f41057 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 12 Aug 2024 15:09:07 +1000 Subject: Bump up the ui task stack size --- src/tasks/tasks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/tasks/tasks.cpp b/src/tasks/tasks.cpp index 9980c97e..f0b567f2 100644 --- a/src/tasks/tasks.cpp +++ b/src/tasks/tasks.cpp @@ -47,7 +47,7 @@ auto AllocateStack() -> std::span { // separately. template <> auto AllocateStack() -> std::span { - constexpr std::size_t size = 14 * 1024; + constexpr std::size_t size = 20 * 1024; static StackType_t sStack[size]; return {sStack, size}; } -- cgit v1.2.3 From 9e1fc64c8880bf6920b156e9f9d19fd0b098a468 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 13 Aug 2024 14:21:07 +1000 Subject: Accept a specific tag in `loglevel` --- src/tangara/dev_console/console.cpp | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/tangara/dev_console/console.cpp b/src/tangara/dev_console/console.cpp index fcb987bb..bc3a7aca 100644 --- a/src/tangara/dev_console/console.cpp +++ b/src/tangara/dev_console/console.cpp @@ -23,34 +23,47 @@ namespace console { int CmdLogLevel(int argc, char** argv) { static const std::pmr::string usage = - "usage: loglevel [VERBOSE,DEBUG,INFO,WARN,ERROR,NONE]"; - if (argc != 2) { + "usage: loglevel [tag] [VERBOSE,DEBUG,INFO,WARN,ERROR,NONE]"; + if (argc < 2 || argc > 3) { std::cout << usage << std::endl; return 1; } - std::pmr::string level_str = argv[1]; - std::transform(level_str.begin(), level_str.end(), level_str.begin(), + + std::string tag; + if (argc == 2) { + tag = "*"; + } else { + tag = argv[1]; + } + + std::string raw_level; + if (argc == 2) { + raw_level = argv[1]; + } else { + raw_level = argv[2]; + } + std::transform(raw_level.begin(), raw_level.end(), raw_level.begin(), [](unsigned char c) { return std::toupper(c); }); esp_log_level_t level; - if (level_str == "VERBOSE") { + if (raw_level == "VERBOSE") { level = ESP_LOG_VERBOSE; - } else if (level_str == "DEBUG") { + } else if (raw_level == "DEBUG") { level = ESP_LOG_DEBUG; - } else if (level_str == "INFO") { + } else if (raw_level == "INFO") { level = ESP_LOG_INFO; - } else if (level_str == "WARN") { + } else if (raw_level == "WARN") { level = ESP_LOG_WARN; - } else if (level_str == "ERROR") { + } else if (raw_level == "ERROR") { level = ESP_LOG_ERROR; - } else if (level_str == "NONE") { + } else if (raw_level == "NONE") { level = ESP_LOG_NONE; } else { std::cout << usage << std::endl; return 1; } - esp_log_level_set("*", level); + esp_log_level_set(tag.c_str(), level); return 0; } -- cgit v1.2.3 From 822c9dc93e868254059598ddeb58713135f0a4a1 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 13 Aug 2024 13:01:48 +1000 Subject: Fix build errors from stricter visibility requirements --- src/drivers/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt index 33d25894..fea5780f 100644 --- a/src/drivers/CMakeLists.txt +++ b/src/drivers/CMakeLists.txt @@ -8,5 +8,5 @@ idf_component_register( "samd.cpp" "wm8523.cpp" "nvs.cpp" "haptics.cpp" "spiffs.cpp" "pcm_buffer.cpp" INCLUDE_DIRS "include" REQUIRES "esp_adc" "fatfs" "result" "lvgl" "nvs_flash" "spiffs" "bt" - "tasks" "tinyfsm" "util" "libcppbor") + "tasks" "tinyfsm" "util" "libcppbor" "driver") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) -- cgit v1.2.3 From 6a2d259f46ac5ae9ede5733fb06cf93ca01fe162 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 13 Aug 2024 13:02:07 +1000 Subject: Move off of deprecated APIs --- src/drivers/bluetooth.cpp | 2 +- src/drivers/i2s_dac.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/drivers/bluetooth.cpp b/src/drivers/bluetooth.cpp index a0a318e9..8ec30395 100644 --- a/src/drivers/bluetooth.cpp +++ b/src/drivers/bluetooth.cpp @@ -528,7 +528,7 @@ void Disabled::react(const events::Enable&) { // Set a reasonable name for the device. std::pmr::string name = DeviceName(); - esp_bt_dev_set_device_name(name.c_str()); + esp_bt_gap_set_device_name(name.c_str()); // Initialise GAP. This controls advertising our device, and scanning for // other devices. diff --git a/src/drivers/i2s_dac.cpp b/src/drivers/i2s_dac.cpp index b1044896..9c9bb793 100644 --- a/src/drivers/i2s_dac.cpp +++ b/src/drivers/i2s_dac.cpp @@ -46,12 +46,12 @@ extern "C" IRAM_ATTR auto callback(i2s_chan_handle_t handle, if (event == nullptr || user_ctx == nullptr) { return false; } - if (event->data == nullptr || event->size == 0) { + if (event->dma_buf == nullptr || event->size == 0) { return false; } assert(event->size % 4 == 0); - uint8_t* buf = *reinterpret_cast(event->data); + uint8_t* buf = reinterpret_cast(event->dma_buf); auto* src = reinterpret_cast(user_ctx); BaseType_t ret = -- cgit v1.2.3 From 326cc42a63ec1bd8ff4e62da44658e8facd181c2 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 13 Aug 2024 16:25:47 +1000 Subject: Don't spuriously report that the current track has changed Fixes the last track in the queue repeating forever --- src/tangara/audio/track_queue.cpp | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src') diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index 91bdda39..5d730a09 100644 --- a/src/tangara/audio/track_queue.cpp +++ b/src/tangara/audio/track_queue.cpp @@ -242,6 +242,8 @@ auto TrackQueue::next(Reason r) -> void { { const std::unique_lock lock(mutex_); + auto pos = position_; + if (shuffle_) { shuffle_->next(); position_ = shuffle_->current(); @@ -250,7 +252,9 @@ auto TrackQueue::next(Reason r) -> void { position_++; } } + goTo(position_); + changed = pos != position_; } notifyChanged(changed, r); -- cgit v1.2.3 From 022aa38396cecb11b9de4b61665a6416378b4a95 Mon Sep 17 00:00:00 2001 From: ailurux Date: Wed, 14 Aug 2024 10:19:26 +1000 Subject: Fix for position persisting when queue reset --- src/tangara/audio/track_queue.cpp | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index 91bdda39..fb4ff696 100644 --- a/src/tangara/audio/track_queue.cpp +++ b/src/tangara/audio/track_queue.cpp @@ -286,6 +286,7 @@ auto TrackQueue::finish() -> void { auto TrackQueue::clear() -> void { { const std::unique_lock lock(mutex_); + position_ = 0; playlist_.clear(); opened_playlist_.reset(); if (shuffle_) { -- cgit v1.2.3 From 40c754a72a23a849321b60dbd77fa1303c77953b Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 14 Aug 2024 15:18:57 +1000 Subject: Always initialise bytes_cleared when clearing PcmBuffers --- src/drivers/pcm_buffer.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/drivers/pcm_buffer.cpp b/src/drivers/pcm_buffer.cpp index 3f4a0443..142a6376 100644 --- a/src/drivers/pcm_buffer.cpp +++ b/src/drivers/pcm_buffer.cpp @@ -66,10 +66,17 @@ IRAM_ATTR auto PcmBuffer::receive(std::span dest, bool isr) auto PcmBuffer::clear() -> void { while (!isEmpty()) { - size_t bytes_cleared; + size_t bytes_cleared = 0; void* data = xRingbufferReceive(ringbuf_, &bytes_cleared, 0); - vRingbufferReturnItem(ringbuf_, data); - received_ += bytes_cleared / sizeof(int16_t); + if (data) { + vRingbufferReturnItem(ringbuf_, data); + received_ += bytes_cleared / sizeof(int16_t); + } else { + // Defensively guard against looping forever if for some reason the + // buffer isn't draining. + ESP_LOGW(kTag, "PcmBuffer not draining"); + break; + } } } -- cgit v1.2.3 From 9c56261122a98245c8e6e7e9f1c94a8b683f8832 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 15 Aug 2024 10:26:26 +1000 Subject: Delay DB reindexing slightly This helps with boot time by preventing a ton of disk I/O before the UI has had a chance to load. --- src/tangara/system_fsm/running.cpp | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src') diff --git a/src/tangara/system_fsm/running.cpp b/src/tangara/system_fsm/running.cpp index f9bca074..07166e2f 100644 --- a/src/tangara/system_fsm/running.cpp +++ b/src/tangara/system_fsm/running.cpp @@ -188,6 +188,10 @@ auto Running::mountStorage() -> void { // mounted card. if (sServices->nvs().DbAutoIndex()) { sServices->bg_worker().Dispatch([&]() { + // Delay the index update for a bit, since we don't want to cause a lot + // of disk contention immediately after mounting (especially when we've + // just booted), or else we risk slowing down stuff like UI loading. + vTaskDelay(pdMS_TO_TICKS(6000)); auto db = sServices->database().lock(); if (!db) { return; -- cgit v1.2.3 From 493f8e1200f73a921bf06a51fd1e6689396151ea Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 15 Aug 2024 11:44:49 +1000 Subject: Don't break early from clearing PcmBuffer --- src/drivers/pcm_buffer.cpp | 5 ----- 1 file changed, 5 deletions(-) (limited to 'src') diff --git a/src/drivers/pcm_buffer.cpp b/src/drivers/pcm_buffer.cpp index 142a6376..25762c50 100644 --- a/src/drivers/pcm_buffer.cpp +++ b/src/drivers/pcm_buffer.cpp @@ -71,11 +71,6 @@ auto PcmBuffer::clear() -> void { if (data) { vRingbufferReturnItem(ringbuf_, data); received_ += bytes_cleared / sizeof(int16_t); - } else { - // Defensively guard against looping forever if for some reason the - // buffer isn't draining. - ESP_LOGW(kTag, "PcmBuffer not draining"); - break; } } } -- cgit v1.2.3 From 5ab4c2f0d6eda97b73ac789f3d8fd39bd97eeddb Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 15 Aug 2024 12:42:57 +1000 Subject: Update position when updating the shuffler --- src/tangara/audio/track_queue.cpp | 23 ++++++++--------------- src/tangara/audio/track_queue.hpp | 4 ++-- 2 files changed, 10 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index fb4ff696..51f17a8f 100644 --- a/src/tangara/audio/track_queue.cpp +++ b/src/tangara/audio/track_queue.cpp @@ -121,9 +121,12 @@ auto TrackQueue::totalSize() const -> size_t { return sum; } -auto TrackQueue::updateShuffler() -> void { +auto TrackQueue::updateShuffler(bool andUpdatePosition) -> void { if (shuffle_) { shuffle_->resize(totalSize()); + if (andUpdatePosition) { + goTo(shuffle_->current()); + } } } @@ -140,7 +143,7 @@ auto TrackQueue::openPlaylist(const std::string& playlist_file) -> bool { if (!res) { return false; } - updateShuffler(); + updateShuffler(true); notifyChanged(true, Reason::kExplicitUpdate); return true; } @@ -169,16 +172,6 @@ auto TrackQueue::append(Item i) -> void { current_changed = was_queue_empty; // Dont support inserts yet } - // If there wasn't anything already playing, then we should make sure we - // begin playback at a random point, instead of always starting with - // whatever was inserted first and *then* shuffling. - // We don't base this purely off of current_changed because we would like - // 'play this track now' (by inserting at the current pos) to work even - // when shuffling is enabled. - if (was_queue_empty && shuffle_) { - playlist_.skipTo(shuffle_->current()); - } - if (std::holds_alternative(i)) { { const std::unique_lock lock(mutex_); @@ -186,7 +179,7 @@ auto TrackQueue::append(Item i) -> void { if (!filename.empty()) { playlist_.append(filename); } - updateShuffler(); + updateShuffler(was_queue_empty); } notifyChanged(current_changed, Reason::kExplicitUpdate); } else if (std::holds_alternative(i)) { @@ -213,7 +206,7 @@ auto TrackQueue::append(Item i) -> void { } { const std::unique_lock lock(mutex_); - updateShuffler(); + updateShuffler(was_queue_empty); } notifyChanged(current_changed, Reason::kExplicitUpdate); }); @@ -224,7 +217,7 @@ auto TrackQueue::next() -> void { next(Reason::kExplicitUpdate); } -auto TrackQueue::goTo(size_t position) { +auto TrackQueue::goTo(size_t position) -> void { position_ = position; if (opened_playlist_) { if (position_ < opened_playlist_->size()) { diff --git a/src/tangara/audio/track_queue.hpp b/src/tangara/audio/track_queue.hpp index 72713242..b66d18d1 100644 --- a/src/tangara/audio/track_queue.hpp +++ b/src/tangara/audio/track_queue.hpp @@ -80,7 +80,7 @@ class TrackQueue { auto insert(Item, size_t index = 0) -> void; auto append(Item i) -> void; - auto updateShuffler() -> void; + auto updateShuffler(bool andUpdatePosition) -> void; /* * Advances to the next track in the queue, placing the current track at the @@ -117,7 +117,7 @@ class TrackQueue { private: auto next(QueueUpdate::Reason r) -> void; - auto goTo(size_t position); + auto goTo(size_t position) -> void; auto getFilepath(database::TrackId id) -> std::optional; mutable std::shared_mutex mutex_; -- cgit v1.2.3 From 978429109ed421a8e49b7a155f3e043247d905b1 Mon Sep 17 00:00:00 2001 From: ailurux Date: Mon, 26 Aug 2024 11:15:27 +1000 Subject: Fix queue serialisation so that the position is correctly applied --- src/tangara/audio/track_queue.cpp | 14 +++++++++----- src/tangara/audio/track_queue.hpp | 3 ++- 2 files changed, 11 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index ecf33c74..761ea09a 100644 --- a/src/tangara/audio/track_queue.cpp +++ b/src/tangara/audio/track_queue.cpp @@ -137,14 +137,16 @@ auto TrackQueue::open() -> bool { return playlist_.open(); } -auto TrackQueue::openPlaylist(const std::string& playlist_file) -> bool { +auto TrackQueue::openPlaylist(const std::string& playlist_file, bool notify) -> bool { opened_playlist_.emplace(playlist_file); auto res = opened_playlist_->open(); if (!res) { return false; } updateShuffler(true); - notifyChanged(true, Reason::kExplicitUpdate); + if (notify) { + notifyChanged(true, Reason::kExplicitUpdate); + } return true; } @@ -371,7 +373,7 @@ auto TrackQueue::serialise() -> std::string { } TrackQueue::QueueParseClient::QueueParseClient(TrackQueue& queue) - : queue_(queue), state_(State::kInit), i_(0) {} + : queue_(queue), state_(State::kInit), i_(0), position_to_set_(0) {} cppbor::ParseClient* TrackQueue::QueueParseClient::item( std::unique_ptr& item, @@ -400,10 +402,11 @@ cppbor::ParseClient* TrackQueue::QueueParseClient::item( i_ = 0; } else if (item->type() == cppbor::UINT) { auto val = item->asUint()->unsignedValue(); - queue_.goTo(val); + // Save the position so we can apply it later when we have finished serialising + position_to_set_ = val; } else if (item->type() == cppbor::TSTR) { auto val = item->asTstr(); - queue_.openPlaylist(val->value()); + queue_.openPlaylist(val->value(), false); } else if (item->type() == cppbor::SIMPLE) { bool val = item->asBool()->value(); if (i_ == 0) { @@ -448,6 +451,7 @@ cppbor::ParseClient* TrackQueue::QueueParseClient::itemEnd( if (state_ == State::kInit) { state_ = State::kFinished; } else if (state_ == State::kRoot) { + queue_.goTo(position_to_set_); state_ = State::kFinished; } else if (state_ == State::kMetadata) { if (item->type() == cppbor::ARRAY) { diff --git a/src/tangara/audio/track_queue.hpp b/src/tangara/audio/track_queue.hpp index b66d18d1..54898a5d 100644 --- a/src/tangara/audio/track_queue.hpp +++ b/src/tangara/audio/track_queue.hpp @@ -74,7 +74,7 @@ class TrackQueue { auto currentPosition() const -> size_t; auto totalSize() const -> size_t; auto open() -> bool; - auto openPlaylist(const std::string& playlist_file) -> bool; + auto openPlaylist(const std::string& playlist_file, bool notify = true) -> bool; using Item = std::variant; auto insert(Item, size_t index = 0) -> void; @@ -163,6 +163,7 @@ class TrackQueue { }; State state_; size_t i_; + size_t position_to_set_; }; }; -- cgit v1.2.3 From 275ade5d13f8e49e89ed14c7acff6644f5c644a1 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 26 Aug 2024 13:45:24 +1000 Subject: Move some hot driver functions into iram We've got the space for it now! Also turn SW radio coexistence off whilst we're here; the docs recommend this if you only use Bluetooth(R) --- src/drivers/display.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/drivers/display.cpp b/src/drivers/display.cpp index efc9df93..7321f20b 100644 --- a/src/drivers/display.cpp +++ b/src/drivers/display.cpp @@ -242,6 +242,7 @@ void Display::SendInitialisationSequence(const uint8_t* data) { spi_device_release_bus(handle_); } +IRAM_ATTR void Display::SendCommandWithData(uint8_t command, const uint8_t* data, size_t length) { @@ -249,17 +250,20 @@ void Display::SendCommandWithData(uint8_t command, SendData(data, length); } +IRAM_ATTR void Display::SendCmd(const uint8_t* data, size_t length) { SendTransaction(COMMAND, data, length); } +IRAM_ATTR void Display::SendData(const uint8_t* data, size_t length) { SendTransaction(DATA, data, length); } +IRAM_ATTR void Display::SendTransaction(TransactionType type, - const uint8_t* data, - size_t length) { + const uint8_t* data, + size_t length) { // TODO(jacqueline): What's sending this? if (length == 0) { return; @@ -290,6 +294,7 @@ void Display::SendTransaction(TransactionType type, ESP_ERROR_CHECK(spi_device_transmit(handle_, &sTransaction)); } +IRAM_ATTR void Display::OnLvglFlush(const lv_area_t* area, uint8_t* color_map) { // Swap the pixel byte order first, since we don't want to do this whilst // holding the SPI bus lock. -- cgit v1.2.3 From e6c77f17b87a4cbf358bd2ddd3b82724771ee03c Mon Sep 17 00:00:00 2001 From: ailurux Date: Mon, 26 Aug 2024 15:43:00 +1000 Subject: Switch output mode to headphones when plugged in --- src/tangara/audio/audio_events.hpp | 5 ++++- src/tangara/audio/audio_fsm.cpp | 13 +++++++++++++ src/tangara/audio/audio_fsm.hpp | 1 + 3 files changed, 18 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/tangara/audio/audio_events.hpp b/src/tangara/audio/audio_events.hpp index 503664cc..50fd4b00 100644 --- a/src/tangara/audio/audio_events.hpp +++ b/src/tangara/audio/audio_events.hpp @@ -16,6 +16,7 @@ #include "tinyfsm.hpp" #include "database/track.hpp" +#include "drivers/nvs.hpp" #include "types.hpp" namespace audio { @@ -137,7 +138,9 @@ struct SetVolumeLimit : tinyfsm::Event { int limit_db; }; -struct OutputModeChanged : tinyfsm::Event {}; +struct OutputModeChanged : tinyfsm::Event { + std::optional set_to; +}; namespace internal { diff --git a/src/tangara/audio/audio_fsm.cpp b/src/tangara/audio/audio_fsm.cpp index 8da11665..16c16002 100644 --- a/src/tangara/audio/audio_fsm.cpp +++ b/src/tangara/audio/audio_fsm.cpp @@ -231,6 +231,16 @@ void AudioState::react(const internal::StreamEnded& ev) { sStreamCues.addCue({}, ev.cue_at_sample); } +void AudioState::react(const system_fsm::HasPhonesChanged& ev) { + if (ev.has_headphones) { + events::Audio().Dispatch(audio::OutputModeChanged{.set_to = drivers::NvsStorage::Output::kHeadphones}); + } else { + if (sServices->bluetooth().enabled()) { + events::Audio().Dispatch(audio::OutputModeChanged{.set_to = drivers::NvsStorage::Output::kBluetooth}); + } + } +} + void AudioState::react(const system_fsm::BluetoothEvent& ev) { using drivers::bluetooth::SimpleEvent; if (std::holds_alternative(ev.event)) { @@ -334,6 +344,9 @@ void AudioState::react(const SetVolumeBalance& ev) { void AudioState::react(const OutputModeChanged& ev) { ESP_LOGI(kTag, "output mode changed"); auto new_mode = sServices->nvs().OutputMode(); + if (ev.set_to) { + new_mode = *ev.set_to; + } sOutput->mode(IAudioOutput::Modes::kOff); switch (new_mode) { case drivers::NvsStorage::Output::kBluetooth: diff --git a/src/tangara/audio/audio_fsm.hpp b/src/tangara/audio/audio_fsm.hpp index f949ce8a..0644375f 100644 --- a/src/tangara/audio/audio_fsm.hpp +++ b/src/tangara/audio/audio_fsm.hpp @@ -67,6 +67,7 @@ class AudioState : public tinyfsm::Fsm { virtual void react(const system_fsm::KeyLockChanged&){}; virtual void react(const system_fsm::SdStateChanged&) {} virtual void react(const system_fsm::BluetoothEvent&); + virtual void react(const system_fsm::HasPhonesChanged&); protected: auto emitPlaybackUpdate(bool paused) -> void; -- cgit v1.2.3 From b1c90278ae07ee0c108aef9722a7e54015a6011f Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 27 Aug 2024 21:17:16 +1000 Subject: Delete unused half readme --- src/codecs/README.md | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/codecs/README.md (limited to 'src') diff --git a/src/codecs/README.md b/src/codecs/README.md deleted file mode 100644 index d8eaf405..00000000 --- a/src/codecs/README.md +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -# Software Codecs - -This component contains a collection of software decoders for various -- cgit v1.2.3 From ef227f8c518f2b6cfd5e55ca052a12e70515f2ef Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 27 Aug 2024 21:17:36 +1000 Subject: Move the UI task main loop info iram --- src/tangara/ui/lvgl_task.cpp | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/tangara/ui/lvgl_task.cpp b/src/tangara/ui/lvgl_task.cpp index e82aefc4..287f6b7e 100644 --- a/src/tangara/ui/lvgl_task.cpp +++ b/src/tangara/ui/lvgl_task.cpp @@ -38,6 +38,7 @@ UiTask::~UiTask() { assert(false); } +IRAM_ATTR auto UiTask::Main() -> void { ESP_LOGI(kTag, "start ui task"); lv_group_t* current_group = nullptr; -- cgit v1.2.3 From f253d2ee7568b61ce2fab962f7328a50e2da6adf Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 27 Aug 2024 21:17:53 +1000 Subject: Timeout when writing output samples throughout the audio pipeline This allows the audio pipeline to remain responsive even when the drain buffer has completely filled. This in turn means that you now see the track info in the 'now playing' screen change if the current track changes whilst you are paused. Since I was fucking around a lot in the audio processor anyway, I also added mono->stereo expansion so that playing mono tracks on Bluetooth no longer destroys your ears. --- src/drivers/include/drivers/pcm_buffer.hpp | 8 +- src/drivers/pcm_buffer.cpp | 9 +- src/tangara/audio/audio_decoder.cpp | 46 +++- src/tangara/audio/audio_decoder.hpp | 2 + src/tangara/audio/audio_fsm.cpp | 1 + src/tangara/audio/processor.cpp | 409 +++++++++++++++++++---------- src/tangara/audio/processor.hpp | 70 ++++- src/tangara/audio/resample.cpp | 18 +- src/tangara/audio/resample.hpp | 3 + 9 files changed, 407 insertions(+), 159 deletions(-) (limited to 'src') diff --git a/src/drivers/include/drivers/pcm_buffer.hpp b/src/drivers/include/drivers/pcm_buffer.hpp index 6630f720..8f53317e 100644 --- a/src/drivers/include/drivers/pcm_buffer.hpp +++ b/src/drivers/include/drivers/pcm_buffer.hpp @@ -28,8 +28,12 @@ class PcmBuffer { PcmBuffer(size_t size_in_samples); ~PcmBuffer(); - /* Adds samples to the buffer. */ - auto send(std::span) -> void; + /* + * Adds samples to the buffer. Returns the number of samples that were added, + * which may be less than the number of samples given if this PcmBuffer is + * close to full. + */ + auto send(std::span) -> size_t; /* * Fills the given span with samples. If enough samples are available in diff --git a/src/drivers/pcm_buffer.cpp b/src/drivers/pcm_buffer.cpp index 25762c50..071f5cea 100644 --- a/src/drivers/pcm_buffer.cpp +++ b/src/drivers/pcm_buffer.cpp @@ -17,6 +17,7 @@ #include "freertos/FreeRTOS.h" #include "esp_heap_caps.h" +#include "freertos/projdefs.h" #include "freertos/ringbuf.h" #include "portmacro.h" @@ -39,9 +40,13 @@ PcmBuffer::~PcmBuffer() { heap_caps_free(buf_); } -auto PcmBuffer::send(std::span data) -> void { - xRingbufferSend(ringbuf_, data.data(), data.size_bytes(), portMAX_DELAY); +auto PcmBuffer::send(std::span data) -> size_t { + if (!xRingbufferSend(ringbuf_, data.data(), data.size_bytes(), + pdMS_TO_TICKS(100))) { + return 0; + } sent_ += data.size(); + return data.size(); } IRAM_ATTR auto PcmBuffer::receive(std::span dest, bool isr) diff --git a/src/tangara/audio/audio_decoder.cpp b/src/tangara/audio/audio_decoder.cpp index ee06d984..992444f0 100644 --- a/src/tangara/audio/audio_decoder.cpp +++ b/src/tangara/audio/audio_decoder.cpp @@ -48,7 +48,7 @@ static const char* kTag = "decoder"; * increasing its size. */ static constexpr std::size_t kCodecBufferLength = - drivers::kI2SBufferLengthFrames * sizeof(sample::Sample); + drivers::kI2SBufferLengthFrames * 2; auto Decoder::Start(std::shared_ptr sink) -> Decoder* { Decoder* task = new Decoder(sink); @@ -78,11 +78,17 @@ Decoder::Decoder(std::shared_ptr processor) * Main decoding loop. Handles watching for new streams, or continuing to nudge * along the current stream if we have one. */ +IRAM_ATTR void Decoder::Main() { for (;;) { - // Check whether there's a new stream to begin. If we're idle, then we - // simply park and wait forever for a stream to arrive. - TickType_t wait_time = stream_ ? 0 : portMAX_DELAY; + // How long should we spend waiting for a command? By default, assume we're + // idle and wait forever. + TickType_t wait_time = portMAX_DELAY; + if (!leftover_samples_.empty() || stream_) { + // If we have work to do, then don't block waiting for a new stream. + wait_time = 0; + } + NextStream* next; if (xQueueReceive(next_stream_, &next, wait_time)) { // Copy the data out of the queue, then clean up the item. @@ -103,8 +109,15 @@ void Decoder::Main() { // Start decoding the new stream. prepareDecode(new_stream); + + // Keep handling commands until the command queue is empty. + continue; } + // We should always have a stream if we returned from xQueueReceive without + // receiving a new stream. + assert(stream_); + if (!continueDecode()) { finishDecode(false); } @@ -167,16 +180,36 @@ auto Decoder::prepareDecode(std::shared_ptr stream) -> void { } auto Decoder::continueDecode() -> bool { + // First, see if we have any samples from a previous decode that still need + // to be sent. + if (!leftover_samples_.empty()) { + leftover_samples_ = processor_->continueStream(leftover_samples_); + return true; + } + + // We might have already cleaned up the codec if the last decode pass of the + // stream resulted in leftoverr samples. + if (!codec_) { + return false; + } + auto res = codec_->DecodeTo(codec_buffer_); if (res.has_error()) { return false; } if (res->samples_written > 0) { - processor_->continueStream(codec_buffer_.first(res->samples_written)); + leftover_samples_ = + processor_->continueStream(codec_buffer_.first(res->samples_written)); + } + + if (res->is_stream_finished) { + // The codec has finished, so make sure we don't call it again. + codec_.reset(); } - return !res->is_stream_finished; + // We're done iff the codec has finished and we sent everything. + return codec_ || !leftover_samples_.empty(); } auto Decoder::finishDecode(bool cancel) -> void { @@ -191,6 +224,7 @@ auto Decoder::finishDecode(bool cancel) -> void { processor_->endStream(cancel); // Clean up after ourselves. + leftover_samples_ = {}; stream_.reset(); codec_.reset(); track_.reset(); diff --git a/src/tangara/audio/audio_decoder.hpp b/src/tangara/audio/audio_decoder.hpp index 64561d9d..9f20ec59 100644 --- a/src/tangara/audio/audio_decoder.hpp +++ b/src/tangara/audio/audio_decoder.hpp @@ -15,6 +15,7 @@ #include "audio/processor.hpp" #include "codec.hpp" #include "database/track.hpp" +#include "sample.hpp" #include "types.hpp" namespace audio { @@ -55,6 +56,7 @@ class Decoder { std::shared_ptr track_; std::span codec_buffer_; + std::span leftover_samples_; }; } // namespace audio diff --git a/src/tangara/audio/audio_fsm.cpp b/src/tangara/audio/audio_fsm.cpp index 16c16002..54ea5b6c 100644 --- a/src/tangara/audio/audio_fsm.cpp +++ b/src/tangara/audio/audio_fsm.cpp @@ -216,6 +216,7 @@ void AudioState::react(const internal::StreamStarted& ev) { } sStreamCues.addCue(ev.track, ev.cue_at_sample); + sStreamCues.update(sDrainBuffer->totalReceived()); if (!sIsPaused && !is_in_state()) { transit(); diff --git a/src/tangara/audio/processor.cpp b/src/tangara/audio/processor.cpp index 81858110..29124232 100644 --- a/src/tangara/audio/processor.cpp +++ b/src/tangara/audio/processor.cpp @@ -10,60 +10,57 @@ #include #include #include +#include #include #include -#include "audio/audio_events.hpp" -#include "audio/audio_sink.hpp" -#include "drivers/i2s_dac.hpp" -#include "drivers/pcm_buffer.hpp" +#include "assert.h" #include "esp_heap_caps.h" #include "esp_log.h" -#include "events/event_queue.hpp" +#include "esp_timer.h" #include "freertos/portmacro.h" #include "freertos/projdefs.h" +#include "audio/audio_events.hpp" +#include "audio/audio_sink.hpp" +#include "audio/i2s_audio_output.hpp" #include "audio/resample.hpp" +#include "drivers/i2s_dac.hpp" +#include "drivers/pcm_buffer.hpp" +#include "events/event_queue.hpp" #include "sample.hpp" #include "tasks.hpp" [[maybe_unused]] static constexpr char kTag[] = "mixer"; -static constexpr std::size_t kSampleBufferLength = - drivers::kI2SBufferLengthFrames * sizeof(sample::Sample) * 2; -static constexpr std::size_t kSourceBufferLength = kSampleBufferLength * 2; +static const size_t kSampleBufferLength = drivers::kI2SBufferLengthFrames * 2; +static const size_t kSourceBufferLength = kSampleBufferLength * 2; namespace audio { +static const I2SAudioOutput::Format kTargetFormat{ + .sample_rate = 48000, + .num_channels = 2, + .bits_per_sample = 16, +}; + SampleProcessor::SampleProcessor(drivers::PcmBuffer& sink) - : commands_(xQueueCreate(1, sizeof(Args))), - resampler_(nullptr), - source_(xStreamBufferCreateWithCaps(kSourceBufferLength, - sizeof(sample::Sample) * 2, + : commands_(xQueueCreate(2, sizeof(Args))), + source_(xStreamBufferCreateWithCaps(kSourceBufferLength + 1, + sizeof(sample::Sample), MALLOC_CAP_DMA)), sink_(sink), - leftover_bytes_(0) { - input_buffer_ = { - reinterpret_cast(heap_caps_calloc( - kSampleBufferLength, sizeof(sample::Sample), MALLOC_CAP_DMA)), - kSampleBufferLength}; - input_buffer_as_bytes_ = {reinterpret_cast(input_buffer_.data()), - input_buffer_.size_bytes()}; - - resampled_buffer_ = { - reinterpret_cast(heap_caps_calloc( - kSampleBufferLength, sizeof(sample::Sample), MALLOC_CAP_DMA)), - kSampleBufferLength}; - + unprocessed_samples_(0) { tasks::StartPersistent([&]() { Main(); }); } SampleProcessor::~SampleProcessor() { vQueueDelete(commands_); - vStreamBufferDelete(source_); + vStreamBufferDeleteWithCaps(source_); } auto SampleProcessor::SetOutput(std::shared_ptr output) -> void { + output->Configure(kTargetFormat); // FIXME: We should add synchronisation here, but we should be careful // about not impacting performance given that the output will change only // very rarely (if ever). @@ -80,15 +77,30 @@ auto SampleProcessor::beginStream(std::shared_ptr track) -> void { xQueueSend(commands_, &args, portMAX_DELAY); } -auto SampleProcessor::continueStream(std::span input) -> void { +auto SampleProcessor::continueStream(std::span input) + -> std::span { + size_t bytes_sent = xStreamBufferSend(source_, input.data(), + input.size_bytes(), pdMS_TO_TICKS(100)); + if (!bytes_sent) { + // If nothing could be sent, then bail out early. We don't want to send a + // samples_available command with zero samples. + return input; + } + + // We should only ever be placing whole samples into the buffer. If half + // samples start being sent, then this indicates a serious bug somewhere. + size_t samples_sent = bytes_sent / sizeof(sample::Sample); + assert(samples_sent * sizeof(sample::Sample) == bytes_sent); + Args args{ .track = nullptr, - .samples_available = input.size(), + .samples_available = samples_sent, .is_end_of_stream = false, .clear_buffers = false, }; xQueueSend(commands_, &args, portMAX_DELAY); - xStreamBufferSend(source_, input.data(), input.size_bytes(), portMAX_DELAY); + + return input.subspan(samples_sent); } auto SampleProcessor::endStream(bool cancelled) -> void { @@ -101,152 +113,281 @@ auto SampleProcessor::endStream(bool cancelled) -> void { xQueueSend(commands_, &args, portMAX_DELAY); } +IRAM_ATTR auto SampleProcessor::Main() -> void { for (;;) { + // Block indefinitely if the processor is idle. Otherwise check briefly for + // new commands, then continue processing. + TickType_t wait = hasPendingWork() ? 0 : portMAX_DELAY; + Args args; - while (!xQueueReceive(commands_, &args, portMAX_DELAY)) { + if (xQueueReceive(commands_, &args, wait)) { + if (args.is_end_of_stream && args.clear_buffers) { + // The new command is telling us to clear our buffers! This includes + // discarding any commands that have backed up without being processed. + // Discard all the old commands, then immediately handle the end of + // stream. + while (!pending_commands_.empty()) { + Args discard = pending_commands_.front(); + pending_commands_.pop_front(); + discardCommand(discard); + } + handleEndStream(true); + } else { + pending_commands_.push_back(args); + } } - if (args.track) { - handleBeginStream(*args.track); - delete args.track; + // We need to finish flushing all processed samples before we can process + // more samples. + if (!output_buffer_.isEmpty() && flushOutputBuffer()) { + continue; } - if (args.samples_available) { - handleContinueStream(args.samples_available); + + // We need to finish processing all the samples we've been told about + // before we handle backed up commands. + if (unprocessed_samples_ && !processSamples(false)) { + continue; } - if (args.is_end_of_stream) { - handleEndStream(args.clear_buffers); + + while (!pending_commands_.empty()) { + args = pending_commands_.front(); + pending_commands_.pop_front(); + + if (args.track) { + handleBeginStream(*args.track); + delete args.track; + } + if (args.samples_available) { + unprocessed_samples_ += args.samples_available; + } + if (args.is_end_of_stream) { + if (processSamples(true) || args.clear_buffers) { + handleEndStream(args.clear_buffers); + } else { + // The output filled up while we were trying to flush the last + // samples of this stream, and we haven't been told to clear our + // buffers. Retry handling this command later. + pending_commands_.push_front(args); + break; + } + } } } } auto SampleProcessor::handleBeginStream(std::shared_ptr track) -> void { - if (track->format != source_format_) { - source_format_ = track->format; - // The new stream has a different format to the previous stream (or there - // was no previous stream). - // First, clean up our filters. - resampler_.reset(); - leftover_bytes_ = 0; - - // If the output is idle, then we can reconfigure it to the closest format - // to our new source. - // If the output *wasn't* idle, then we can't reconfigure without an - // audible gap in playback. So instead, we simply keep the same target - // format and begin resampling. - if (sink_.isEmpty()) { - target_format_ = output_->PrepareFormat(track->format); - output_->Configure(target_format_); + // If the new stream's sample rate doesn't match our canonical sample rate, + // then prepare to start resampling. + if (track->format.sample_rate != kTargetFormat.sample_rate) { + ESP_LOGI(kTag, "resampling %lu -> %lu", track->format.sample_rate, + kTargetFormat.sample_rate); + if (!resampler_ || resampler_->sourceRate() != track->format.sample_rate) { + // If there's already a resampler instance for this source rate, then + // reuse it to help gapless playback work smoothly. + resampler_.reset(new Resampler(track->format.sample_rate, + kTargetFormat.sample_rate, + track->format.num_channels)); } + } else { + resampler_.reset(); } + // If the new stream has only one channel, then we double it to get stereo + // audio. + // FIXME: If the Bluetooth stack allowed us to configure the number of + // channels, we could remove this. + double_samples_ = track->format.num_channels != kTargetFormat.num_channels; + events::Audio().Dispatch(internal::StreamStarted{ .track = track, - .sink_format = target_format_, + .sink_format = kTargetFormat, .cue_at_sample = sink_.totalSent(), }); } -auto SampleProcessor::handleContinueStream(size_t samples_available) -> void { - // Loop until we finish reading all the bytes indicated. There might be - // leftovers from each iteration, and from this process as a whole, - // depending on the resampling stage. - size_t bytes_read = 0; - size_t bytes_to_read = samples_available * sizeof(sample::Sample); - while (bytes_read < bytes_to_read) { - // First top up the input buffer, taking care not to overwrite anything - // remaining from a previous iteration. - size_t bytes_read_this_it = xStreamBufferReceive( - source_, input_buffer_as_bytes_.subspan(leftover_bytes_).data(), - std::min(input_buffer_as_bytes_.size() - leftover_bytes_, - bytes_to_read - bytes_read), - portMAX_DELAY); - bytes_read += bytes_read_this_it; - - // Calculate the number of whole samples that are now in the input buffer. - size_t bytes_in_buffer = bytes_read_this_it + leftover_bytes_; - size_t samples_in_buffer = bytes_in_buffer / sizeof(sample::Sample); - - size_t samples_used = handleSamples(input_buffer_.first(samples_in_buffer)); - - // Maybe the resampler didn't consume everything. Maybe the last few - // bytes we read were half a frame. Either way, we need to calculate the - // size of the remainder in bytes, then move it to the front of our - // buffer. - size_t bytes_used = samples_used * sizeof(sample::Sample); - assert(bytes_used <= bytes_in_buffer); - - leftover_bytes_ = bytes_in_buffer - bytes_used; - if (leftover_bytes_ > 0) { - std::memmove(input_buffer_as_bytes_.data(), - input_buffer_as_bytes_.data() + bytes_used, leftover_bytes_); - } - } -} +IRAM_ATTR +auto SampleProcessor::processSamples(bool finalise) -> bool { + for (;;) { + bool out_of_work = true; -auto SampleProcessor::handleSamples(std::span input) -> size_t { - if (source_format_ == target_format_) { - // The happiest possible case: the input format matches the output - // format already. - sink_.send(input); - return input.size(); - } + // First, fill up our input buffer with samples. + if (unprocessed_samples_ > 0) { + out_of_work = false; + auto input = input_buffer_.writeAcquire(); - size_t samples_used = 0; - while (samples_used < input.size()) { - std::span output_source; - if (source_format_.sample_rate != target_format_.sample_rate) { - if (resampler_ == nullptr) { - ESP_LOGI(kTag, "creating new resampler for %lu -> %lu", - source_format_.sample_rate, target_format_.sample_rate); - resampler_.reset(new Resampler(source_format_.sample_rate, - target_format_.sample_rate, - source_format_.num_channels)); - } + size_t bytes_received = xStreamBufferReceive( + source_, input.data(), + std::min(input.size_bytes(), + unprocessed_samples_ * sizeof(sample::Sample)), + 0); - size_t read, written; - std::tie(read, written) = resampler_->Process(input.subspan(samples_used), - resampled_buffer_, false); - samples_used += read; + // We should never receive a half sample. Blow up immediately if we do. + size_t samples_received = bytes_received / sizeof(sample::Sample); + assert(samples_received * sizeof(sample::Sample) == bytes_received); - if (read == 0 && written == 0) { - // Zero samples used or written. We need more input. - break; - } - output_source = resampled_buffer_.first(written); - } else { - output_source = input; - samples_used = input.size(); + unprocessed_samples_ -= samples_received; + input_buffer_.writeCommit(samples_received); } - sink_.send(output_source); - } + // Next, push input samples through the resampler. In the best case, this + // is a simple copy operation. + if (!input_buffer_.isEmpty()) { + out_of_work = false; + auto resample_input = input_buffer_.readAcquire(); + auto resample_output = resampled_buffer_.writeAcquire(); + + size_t read, wrote; + if (resampler_) { + std::tie(read, wrote) = + resampler_->Process(resample_input, resample_output, finalise); + } else { + read = wrote = std::min(resample_input.size(), resample_output.size()); + std::copy_n(resample_input.begin(), read, resample_output.begin()); + } - return samples_used; -} + input_buffer_.readCommit(read); + resampled_buffer_.writeCommit(wrote); + } -auto SampleProcessor::handleEndStream(bool clear_bufs) -> void { - if (resampler_ && !clear_bufs) { - size_t read, written; - std::tie(read, written) = resampler_->Process({}, resampled_buffer_, true); + // Next, we need to make sure the output is in stereo. This is also a simple + // copy in the best case. + if (!resampled_buffer_.isEmpty()) { + out_of_work = false; + auto channels_input = resampled_buffer_.readAcquire(); + auto channels_output = output_buffer_.writeAcquire(); + size_t read, wrote; + if (double_samples_) { + wrote = channels_output.size(); + read = wrote / 2; + if (read > channels_input.size()) { + read = channels_input.size(); + wrote = read * 2; + } + for (size_t i = 0; i < read; i++) { + channels_output[i * 2] = channels_input[i]; + channels_output[(i * 2) + 1] = channels_input[i]; + } + } else { + read = wrote = std::min(channels_input.size(), channels_output.size()); + std::copy_n(channels_input.begin(), read, channels_output.begin()); + } + resampled_buffer_.readCommit(read); + output_buffer_.writeCommit(wrote); + } - if (written > 0) { - sink_.send(resampled_buffer_.first(written)); + // Finally, flush whatever ended up in the output buffer. + if (flushOutputBuffer()) { + if (out_of_work) { + return true; + } + } else { + // The output is congested. Back off of processing for a moment. + return false; } } +} +auto SampleProcessor::handleEndStream(bool clear_bufs) -> void { if (clear_bufs) { sink_.clear(); - } - // FIXME: This discards any leftover samples, but there probably shouldn't be - // any leftover samples. Can this be an assert instead? - leftover_bytes_ = 0; + input_buffer_.clear(); + resampled_buffer_.clear(); + output_buffer_.clear(); + + size_t bytes_discarded = 0; + size_t bytes_to_discard = unprocessed_samples_ * sizeof(sample::Sample); + auto scratch_buf = output_buffer_.writeAcquire(); + while (bytes_discarded < bytes_to_discard) { + size_t bytes_read = + xStreamBufferReceive(source_, scratch_buf.data(), + std::min(scratch_buf.size_bytes(), + bytes_to_discard - bytes_discarded), + 0); + bytes_discarded += bytes_read; + } + unprocessed_samples_ = 0; + } events::Audio().Dispatch(internal::StreamEnded{ .cue_at_sample = sink_.totalSent(), }); } +auto SampleProcessor::hasPendingWork() -> bool { + return !pending_commands_.empty() || unprocessed_samples_ > 0 || + !input_buffer_.isEmpty() || !resampled_buffer_.isEmpty() || + !output_buffer_.isEmpty(); +} + +IRAM_ATTR +auto SampleProcessor::flushOutputBuffer() -> bool { + auto samples = output_buffer_.readAcquire(); + size_t sent = sink_.send(samples); + output_buffer_.readCommit(sent); + return output_buffer_.isEmpty(); +} + +auto SampleProcessor::discardCommand(Args& command) -> void { + if (command.track) { + delete command.track; + } + if (command.samples_available) { + unprocessed_samples_ += command.samples_available; + } + // End of stream commands can just be dropped. Without further actions. +} + +SampleProcessor::Buffer::Buffer() + : buffer_(reinterpret_cast( + heap_caps_calloc(kSampleBufferLength, + sizeof(sample::Sample), + MALLOC_CAP_DMA)), + kSampleBufferLength), + samples_in_buffer_() {} + +SampleProcessor::Buffer::~Buffer() { + heap_caps_free(buffer_.data()); +} + +auto SampleProcessor::Buffer::writeAcquire() -> std::span { + return buffer_.subspan(samples_in_buffer_.size()); +} + +auto SampleProcessor::Buffer::writeCommit(size_t samples) -> void { + if (samples == 0) { + return; + } + samples_in_buffer_ = buffer_.first(samples + samples_in_buffer_.size()); +} + +auto SampleProcessor::Buffer::readAcquire() -> std::span { + return samples_in_buffer_; +} + +auto SampleProcessor::Buffer::readCommit(size_t samples) -> void { + if (samples == 0) { + return; + } + samples_in_buffer_ = samples_in_buffer_.subspan(samples); + + // Move the leftover samples to the front of the buffer, so that we're setup + // for a new write. + if (!samples_in_buffer_.empty()) { + std::memmove(buffer_.data(), samples_in_buffer_.data(), + samples_in_buffer_.size_bytes()); + samples_in_buffer_ = buffer_.first(samples_in_buffer_.size()); + } +} + +auto SampleProcessor::Buffer::isEmpty() -> bool { + return samples_in_buffer_.empty(); +} + +auto SampleProcessor::Buffer::clear() -> void { + samples_in_buffer_ = {}; +} + } // namespace audio diff --git a/src/tangara/audio/processor.hpp b/src/tangara/audio/processor.hpp index 5c4ad0fa..f1b1d921 100644 --- a/src/tangara/audio/processor.hpp +++ b/src/tangara/audio/processor.hpp @@ -8,6 +8,8 @@ #include #include +#include +#include #include #include "audio/audio_events.hpp" @@ -33,18 +35,43 @@ class SampleProcessor { auto SetOutput(std::shared_ptr) -> void; + /* + * Signals to the sample processor that a new discrete stream of audio is now + * being sent. This will typically represent a new track being played. + */ auto beginStream(std::shared_ptr) -> void; - auto continueStream(std::span) -> void; + + /* + * Sends a span of PCM samples to the processor. Returns a subspan of the + * given span containing samples that were not able to be sent during this + * call, e.g. because of congestion downstream from the processor. + */ + auto continueStream(std::span) -> std::span; + + /* + * Signals to the sample processor that the current stream is ending. This + * can either be because the stream has naturally finished, or because it is + * being interrupted. + * If `cancelled` is false, the sample processor will ensure all previous + * samples are processed and sent before communicating the end of the stream + * onwards. If `cancelled` is true, any samples from the current stream that + * have not yet been played will be discarded. + */ auto endStream(bool cancelled) -> void; + SampleProcessor(const SampleProcessor&) = delete; + SampleProcessor& operator=(const SampleProcessor&) = delete; + private: auto Main() -> void; auto handleBeginStream(std::shared_ptr) -> void; - auto handleContinueStream(size_t samples_available) -> void; auto handleEndStream(bool cancel) -> void; - auto handleSamples(std::span) -> size_t; + auto processSamples(bool finalise) -> bool; + + auto hasPendingWork() -> bool; + auto flushOutputBuffer() -> bool; struct Args { std::shared_ptr* track; @@ -53,21 +80,44 @@ class SampleProcessor { bool clear_buffers; }; QueueHandle_t commands_; + std::list pending_commands_; - std::unique_ptr resampler_; + auto discardCommand(Args& command) -> void; StreamBufferHandle_t source_; drivers::PcmBuffer& sink_; - std::span input_buffer_; - std::span input_buffer_as_bytes_; + class Buffer { + public: + Buffer(); + ~Buffer(); + + auto writeAcquire() -> std::span; + auto writeCommit(size_t) -> void; + + auto readAcquire() -> std::span; + auto readCommit(size_t) -> void; - std::span resampled_buffer_; + auto isEmpty() -> bool; + auto clear() -> void; + + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + + private: + std::span buffer_; + std::span samples_in_buffer_; + }; + + Buffer input_buffer_; + Buffer resampled_buffer_; + Buffer output_buffer_; + + std::unique_ptr resampler_; + bool double_samples_; std::shared_ptr output_; - IAudioOutput::Format source_format_; - IAudioOutput::Format target_format_; - size_t leftover_bytes_; + size_t unprocessed_samples_; }; } // namespace audio diff --git a/src/tangara/audio/resample.cpp b/src/tangara/audio/resample.cpp index 143ce230..d6369022 100644 --- a/src/tangara/audio/resample.cpp +++ b/src/tangara/audio/resample.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-only */ #include "audio/resample.hpp" +#include #include #include @@ -31,6 +32,7 @@ Resampler::Resampler(uint32_t source_sample_rate, kQuality, &err_)), num_channels_(num_channels) { + speex_resampler_skip_zeros(resampler_); assert(err_ == 0); } @@ -38,18 +40,24 @@ Resampler::~Resampler() { speex_resampler_destroy(resampler_); } +auto Resampler::sourceRate() -> uint32_t { + uint32_t input = 0; + uint32_t output = 0; + speex_resampler_get_rate(resampler_, &input, &output); + return input; +} + auto Resampler::Process(std::span input, std::span output, bool end_of_data) -> std::pair { - uint32_t samples_used = input.size() / num_channels_; - uint32_t samples_produced = output.size() / num_channels_; + uint32_t frames_used = input.size() / num_channels_; + uint32_t frames_produced = output.size() / num_channels_; int err = speex_resampler_process_interleaved_int( - resampler_, input.data(), &samples_used, output.data(), - &samples_produced); + resampler_, input.data(), &frames_used, output.data(), &frames_produced); assert(err == 0); - return {samples_used * num_channels_, samples_produced * num_channels_}; + return {frames_used * num_channels_, frames_produced * num_channels_}; } } // namespace audio diff --git a/src/tangara/audio/resample.hpp b/src/tangara/audio/resample.hpp index 4d48d47f..df285020 100644 --- a/src/tangara/audio/resample.hpp +++ b/src/tangara/audio/resample.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -24,6 +25,8 @@ class Resampler { ~Resampler(); + auto sourceRate() -> uint32_t; + auto Process(std::span input, std::span output, bool end_of_data) -> std::pair; -- cgit v1.2.3 From 8f4e1ece7512c2b911491d87edc475b803c3989c Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 28 Aug 2024 09:43:41 +1000 Subject: Some minor cleanup, docs, assertions --- src/tangara/audio/processor.cpp | 13 ++++++++++--- src/tangara/audio/processor.hpp | 5 +++++ 2 files changed, 15 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/tangara/audio/processor.cpp b/src/tangara/audio/processor.cpp index 29124232..aa2604b5 100644 --- a/src/tangara/audio/processor.cpp +++ b/src/tangara/audio/processor.cpp @@ -1,11 +1,10 @@ /* - * Copyright 2023 jacqueline + * Copyright 2024 jacqueline * * SPDX-License-Identifier: GPL-3.0-only */ #include "audio/processor.hpp" -#include #include #include @@ -38,6 +37,11 @@ static const size_t kSourceBufferLength = kSampleBufferLength * 2; namespace audio { +/* + * The output format to convert all sources to. This is currently fixed because + * the Bluetooth output doesn't support runtime configuration of its input + * format. + */ static const I2SAudioOutput::Format kTargetFormat{ .sample_rate = 48000, .num_channels = 2, @@ -60,7 +64,10 @@ SampleProcessor::~SampleProcessor() { } auto SampleProcessor::SetOutput(std::shared_ptr output) -> void { + // Make sure our fixed output format is valid. + assert(output->PrepareFormat(kTargetFormat) == kTargetFormat); output->Configure(kTargetFormat); + // FIXME: We should add synchronisation here, but we should be careful // about not impacting performance given that the output will change only // very rarely (if ever). @@ -337,7 +344,7 @@ auto SampleProcessor::discardCommand(Args& command) -> void { if (command.samples_available) { unprocessed_samples_ += command.samples_available; } - // End of stream commands can just be dropped. Without further actions. + // End of stream commands can just be dropped without further action. } SampleProcessor::Buffer::Buffer() diff --git a/src/tangara/audio/processor.hpp b/src/tangara/audio/processor.hpp index f1b1d921..45e05291 100644 --- a/src/tangara/audio/processor.hpp +++ b/src/tangara/audio/processor.hpp @@ -87,15 +87,20 @@ class SampleProcessor { StreamBufferHandle_t source_; drivers::PcmBuffer& sink_; + /* Internal utility for managing buffering samples between our filters. */ class Buffer { public: Buffer(); ~Buffer(); + /* Returns a span of the unused space within the buffer. */ auto writeAcquire() -> std::span; + /* Signals how many samples were just added to the writeAcquire span. */ auto writeCommit(size_t) -> void; + /* Returns a span of the samples stored within the buffer. */ auto readAcquire() -> std::span; + /* Signals how many samples from the readAcquire span were consumed. */ auto readCommit(size_t) -> void; auto isEmpty() -> bool; -- cgit v1.2.3 From d3c15bf070ff6214cd48fa04027ee5d105bc38b7 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 28 Aug 2024 12:47:29 +1000 Subject: spello --- src/tangara/audio/audio_decoder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/tangara/audio/audio_decoder.cpp b/src/tangara/audio/audio_decoder.cpp index 992444f0..8c0b264f 100644 --- a/src/tangara/audio/audio_decoder.cpp +++ b/src/tangara/audio/audio_decoder.cpp @@ -188,7 +188,7 @@ auto Decoder::continueDecode() -> bool { } // We might have already cleaned up the codec if the last decode pass of the - // stream resulted in leftoverr samples. + // stream resulted in leftover samples. if (!codec_) { return false; } -- cgit v1.2.3 From 9ec8d6dafcee6c9722672eefad28ee3aeba4feb9 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 28 Aug 2024 12:45:10 +1000 Subject: Handle the loading state whilst appending many tracks better 1) Update the queue length periodically so that the user can see we're working 2) Clear any previous track and display "loading..." instead --- src/tangara/audio/audio_events.hpp | 1 + src/tangara/audio/audio_fsm.cpp | 5 ++++- src/tangara/audio/track_queue.cpp | 12 ++++++++++++ src/tangara/ui/ui_fsm.cpp | 10 +++++++++- src/tangara/ui/ui_fsm.hpp | 1 + 5 files changed, 27 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/tangara/audio/audio_events.hpp b/src/tangara/audio/audio_events.hpp index 50fd4b00..a141832e 100644 --- a/src/tangara/audio/audio_events.hpp +++ b/src/tangara/audio/audio_events.hpp @@ -103,6 +103,7 @@ struct QueueUpdate : tinyfsm::Event { kRepeatingLastTrack, kTrackFinished, kDeserialised, + kBulkLoadingUpdate, }; Reason reason; }; diff --git a/src/tangara/audio/audio_fsm.cpp b/src/tangara/audio/audio_fsm.cpp index 54ea5b6c..163e56ec 100644 --- a/src/tangara/audio/audio_fsm.cpp +++ b/src/tangara/audio/audio_fsm.cpp @@ -112,10 +112,13 @@ void AudioState::react(const QueueUpdate& ev) { cmd.new_track = std::monostate{}; } break; + case QueueUpdate::kBulkLoadingUpdate: + // Bulk loading updates are informational only; a separate QueueUpdate + // event will be sent when loading is done. case QueueUpdate::kDeserialised: - default: // The current track is deserialised separately in order to retain seek // position. + default: return; } diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index 761ea09a..4f90e2ea 100644 --- a/src/tangara/audio/track_queue.cpp +++ b/src/tangara/audio/track_queue.cpp @@ -26,6 +26,7 @@ #include "database/track.hpp" #include "events/event_queue.hpp" #include "memory_resource.hpp" +#include "random.hpp" #include "tasks.hpp" #include "track_queue.hpp" #include "ui/ui_fsm.hpp" @@ -190,6 +191,8 @@ auto TrackQueue::append(Item i) -> void { // doesn't block. bg_worker_.Dispatch([=, this]() { database::TrackIterator it = std::get(i); + + size_t next_update_at = 10; while (true) { auto next = *it; if (!next) { @@ -205,7 +208,16 @@ auto TrackQueue::append(Item i) -> void { } } it++; + + // Appending very large iterators can take a while. Send out periodic + // queue updates during them so that the user has an idea what's going + // on. + if (!--next_update_at) { + next_update_at = util::sRandom->RangeInclusive(10, 20); + notifyChanged(false, Reason::kBulkLoadingUpdate); + } } + { const std::unique_lock lock(mutex_); updateShuffler(was_queue_empty); diff --git a/src/tangara/ui/ui_fsm.cpp b/src/tangara/ui/ui_fsm.cpp index 38e9b8e1..7da07215 100644 --- a/src/tangara/ui/ui_fsm.cpp +++ b/src/tangara/ui/ui_fsm.cpp @@ -225,6 +225,7 @@ lua::Property UiState::sQueueRandom{false, [](const lua::LuaValue& val) { sServices->track_queue().random(new_val); return true; }}; +lua::Property UiState::sQueueLoading{false}; lua::Property UiState::sVolumeCurrentPct{ 0, [](const lua::LuaValue& val) { @@ -424,7 +425,7 @@ void UiState::react(const system_fsm::BatteryStateChanged& ev) { } } -void UiState::react(const audio::QueueUpdate&) { +void UiState::react(const audio::QueueUpdate& update) { auto& queue = sServices->track_queue(); auto queue_size = queue.totalSize(); sQueueSize.setDirect(static_cast(queue_size)); @@ -439,6 +440,12 @@ void UiState::react(const audio::QueueUpdate&) { sQueueRandom.setDirect(queue.random()); sQueueRepeat.setDirect(queue.repeat()); sQueueReplay.setDirect(queue.replay()); + + if (update.reason == audio::QueueUpdate::Reason::kBulkLoadingUpdate) { + sQueueLoading.setDirect(true); + } else { + sQueueLoading.setDirect(false); + } } void UiState::react(const audio::PlaybackUpdate& ev) { @@ -614,6 +621,7 @@ void Lua::entry() { {"replay", &sQueueReplay}, {"repeat_track", &sQueueRepeat}, {"random", &sQueueRandom}, + {"loading", &sQueueLoading}, }); registry.AddPropertyModule("volume", { diff --git a/src/tangara/ui/ui_fsm.hpp b/src/tangara/ui/ui_fsm.hpp index 41f0db3a..32966657 100644 --- a/src/tangara/ui/ui_fsm.hpp +++ b/src/tangara/ui/ui_fsm.hpp @@ -122,6 +122,7 @@ class UiState : public tinyfsm::Fsm { static lua::Property sQueueReplay; static lua::Property sQueueRepeat; static lua::Property sQueueRandom; + static lua::Property sQueueLoading; static lua::Property sVolumeCurrentPct; static lua::Property sVolumeCurrentDb; -- cgit v1.2.3 From 32869129fff6a0fc434ab2d9ae30d244c020636b Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 28 Aug 2024 12:46:17 +1000 Subject: clang-format --- src/tangara/audio/audio_events.hpp | 5 +++-- src/tangara/audio/audio_fsm.cpp | 6 ++++-- src/tangara/audio/track_queue.cpp | 6 ++++-- 3 files changed, 11 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/tangara/audio/audio_events.hpp b/src/tangara/audio/audio_events.hpp index a141832e..91bcf48b 100644 --- a/src/tangara/audio/audio_events.hpp +++ b/src/tangara/audio/audio_events.hpp @@ -119,10 +119,11 @@ struct SetVolumeBalance : tinyfsm::Event { }; /* - Event emitted when the hardware volume for a connected Bluetooth device has changed. + Event emitted when the hardware volume for a connected Bluetooth device has + changed. */ struct RemoteVolumeChanged : tinyfsm::Event { - uint_fast8_t value; // 0..127 + uint_fast8_t value; // 0..127 }; struct VolumeChanged : tinyfsm::Event { uint_fast8_t percent; diff --git a/src/tangara/audio/audio_fsm.cpp b/src/tangara/audio/audio_fsm.cpp index 163e56ec..5a91c6f9 100644 --- a/src/tangara/audio/audio_fsm.cpp +++ b/src/tangara/audio/audio_fsm.cpp @@ -237,10 +237,12 @@ void AudioState::react(const internal::StreamEnded& ev) { void AudioState::react(const system_fsm::HasPhonesChanged& ev) { if (ev.has_headphones) { - events::Audio().Dispatch(audio::OutputModeChanged{.set_to = drivers::NvsStorage::Output::kHeadphones}); + events::Audio().Dispatch(audio::OutputModeChanged{ + .set_to = drivers::NvsStorage::Output::kHeadphones}); } else { if (sServices->bluetooth().enabled()) { - events::Audio().Dispatch(audio::OutputModeChanged{.set_to = drivers::NvsStorage::Output::kBluetooth}); + events::Audio().Dispatch(audio::OutputModeChanged{ + .set_to = drivers::NvsStorage::Output::kBluetooth}); } } } diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index 4f90e2ea..6fa5511c 100644 --- a/src/tangara/audio/track_queue.cpp +++ b/src/tangara/audio/track_queue.cpp @@ -138,7 +138,8 @@ auto TrackQueue::open() -> bool { return playlist_.open(); } -auto TrackQueue::openPlaylist(const std::string& playlist_file, bool notify) -> bool { +auto TrackQueue::openPlaylist(const std::string& playlist_file, bool notify) + -> bool { opened_playlist_.emplace(playlist_file); auto res = opened_playlist_->open(); if (!res) { @@ -414,7 +415,8 @@ cppbor::ParseClient* TrackQueue::QueueParseClient::item( i_ = 0; } else if (item->type() == cppbor::UINT) { auto val = item->asUint()->unsignedValue(); - // Save the position so we can apply it later when we have finished serialising + // Save the position so we can apply it later when we have finished + // serialising position_to_set_ = val; } else if (item->type() == cppbor::TSTR) { auto val = item->asTstr(); -- cgit v1.2.3 From 71aafc171192e2af987e273395d1d8783a3f7475 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 28 Aug 2024 12:46:25 +1000 Subject: Fix random.cpp not being built --- src/util/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index 49554be8..90778c5d 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -2,4 +2,5 @@ # # SPDX-License-Identifier: GPL-3.0-only -idf_component_register(SRCS INCLUDE_DIRS "include" REQUIRES "memory") +idf_component_register( + SRCS "random.cpp" INCLUDE_DIRS "include" REQUIRES "memory" "komihash") -- cgit v1.2.3 From 9145722b08b9c648e41d6b36f83bebbd106efc1e Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 28 Aug 2024 13:01:31 +1000 Subject: Don't show n+1/n when we run out of queue --- src/tangara/ui/ui_fsm.cpp | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/tangara/ui/ui_fsm.cpp b/src/tangara/ui/ui_fsm.cpp index 7da07215..a20eb0ef 100644 --- a/src/tangara/ui/ui_fsm.cpp +++ b/src/tangara/ui/ui_fsm.cpp @@ -436,6 +436,9 @@ void UiState::react(const audio::QueueUpdate& update) { if (queue_size > 0) { current_pos++; } + if (current_pos > queue_size) { + current_pos = queue_size; + } sQueuePosition.setDirect(current_pos); sQueueRandom.setDirect(queue.random()); sQueueRepeat.setDirect(queue.repeat()); -- cgit v1.2.3 From af7a70450e3ceaaf291aa09b9383b50b879759d9 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 28 Aug 2024 15:30:25 +1000 Subject: Support adding filepaths to the track queue --- src/tangara/audio/track_queue.cpp | 16 ++++++++++------ src/tangara/audio/track_queue.hpp | 12 +++++++----- src/tangara/lua/lua_queue.cpp | 18 ++++++++++++++---- 3 files changed, 31 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index 6fa5511c..cc4770ae 100644 --- a/src/tangara/audio/track_queue.cpp +++ b/src/tangara/audio/track_queue.cpp @@ -161,12 +161,6 @@ auto TrackQueue::getFilepath(database::TrackId id) return db->getTrackPath(id); } -// TODO WIP: Atm only appends are allowed, this will only ever append regardless -// of what index is given. But it is kept like this for compatability for now. -auto TrackQueue::insert(Item i, size_t index) -> void { - append(i); -} - auto TrackQueue::append(Item i) -> void { bool was_queue_empty; bool current_changed; @@ -186,6 +180,16 @@ auto TrackQueue::append(Item i) -> void { updateShuffler(was_queue_empty); } notifyChanged(current_changed, Reason::kExplicitUpdate); + } else if (std::holds_alternative(i)) { + auto& path = std::get(i); + if (!path.empty()) { + { + const std::unique_lock lock(mutex_); + playlist_.append(std::get(i)); + updateShuffler(was_queue_empty); + } + notifyChanged(current_changed, Reason::kExplicitUpdate); + } } else if (std::holds_alternative(i)) { // Iterators can be very large, and retrieving items from them often // requires disk i/o. Handle them asynchronously so that inserting them diff --git a/src/tangara/audio/track_queue.hpp b/src/tangara/audio/track_queue.hpp index 54898a5d..1d25568d 100644 --- a/src/tangara/audio/track_queue.hpp +++ b/src/tangara/audio/track_queue.hpp @@ -16,8 +16,8 @@ #include "cppbor_parse.h" #include "database/database.hpp" #include "database/track.hpp" -#include "tasks.hpp" #include "playlist.hpp" +#include "tasks.hpp" namespace audio { @@ -68,16 +68,18 @@ class TrackQueue { TrackQueue(tasks::WorkerPool& bg_worker, database::Handle db); /* Returns the currently playing track. */ - using TrackItem = std::variant; + using TrackItem = + std::variant; auto current() const -> TrackItem; auto currentPosition() const -> size_t; auto totalSize() const -> size_t; auto open() -> bool; - auto openPlaylist(const std::string& playlist_file, bool notify = true) -> bool; + auto openPlaylist(const std::string& playlist_file, bool notify = true) + -> bool; - using Item = std::variant; - auto insert(Item, size_t index = 0) -> void; + using Item = + std::variant; auto append(Item i) -> void; auto updateShuffler(bool andUpdatePosition) -> void; diff --git a/src/tangara/lua/lua_queue.cpp b/src/tangara/lua/lua_queue.cpp index 9e2002e6..7eb32c62 100644 --- a/src/tangara/lua/lua_queue.cpp +++ b/src/tangara/lua/lua_queue.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-only */ +#include "audio/audio_events.hpp" #include "lua/lua_database.hpp" #include @@ -39,6 +40,14 @@ static auto queue_add(lua_State* state) -> int { audio::TrackQueue& queue = instance->services().track_queue(); queue.append(id); }); + } else if (lua_isstring(state, 1)) { + size_t len; + const char* str = luaL_checklstring(state, 1, &len); + std::string path{str, len}; + instance->services().bg_worker().Dispatch([=]() { + audio::TrackQueue& queue = instance->services().track_queue(); + queue.append(path); + }); } else { database::Iterator* it = db_check_iterator(state, 1); instance->services().bg_worker().Dispatch([=]() { @@ -70,10 +79,11 @@ static auto queue_open_playlist(lua_State* state) -> int { return 0; } -static const struct luaL_Reg kQueueFuncs[] = {{"add", queue_add}, - {"clear", queue_clear}, - {"open_playlist", queue_open_playlist}, - {NULL, NULL}}; +static const struct luaL_Reg kQueueFuncs[] = { + {"add", queue_add}, + {"clear", queue_clear}, + {"open_playlist", queue_open_playlist}, + {NULL, NULL}}; static auto lua_queue(lua_State* state) -> int { luaL_newlib(state, kQueueFuncs); -- cgit v1.2.3 From 3421bd652c39b253872e43d3b6e43664bd0b66e2 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 28 Aug 2024 15:30:53 +1000 Subject: When clicking a track in the file browser, play it Includes adding a `playback.is_playable` for working out whether or not a particular file is able to be played --- src/tangara/lua/file_iterator.cpp | 1 - src/tangara/lua/file_iterator.hpp | 3 +-- src/tangara/lua/lua_filesystem.cpp | 7 ------- src/tangara/ui/ui_fsm.cpp | 26 +++++++++++++++++++++----- 4 files changed, 22 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/tangara/lua/file_iterator.cpp b/src/tangara/lua/file_iterator.cpp index 823775e8..71daf2d8 100644 --- a/src/tangara/lua/file_iterator.cpp +++ b/src/tangara/lua/file_iterator.cpp @@ -79,7 +79,6 @@ auto FileIterator::iterate(bool show_hidden) -> bool { .index = offset_, .isHidden = hidden, .isDirectory = (info.fattrib & AM_DIR) > 0, - .isTrack = false, // TODO .filepath = original_path_ + (original_path_.size() > 0 ? "/" : "") + info.fname, .name = info.fname, diff --git a/src/tangara/lua/file_iterator.hpp b/src/tangara/lua/file_iterator.hpp index c4071445..2d5c2d7d 100644 --- a/src/tangara/lua/file_iterator.hpp +++ b/src/tangara/lua/file_iterator.hpp @@ -19,7 +19,6 @@ struct FileEntry { int index; bool isHidden; bool isDirectory; - bool isTrack; std::string filepath; std::string name; }; @@ -44,4 +43,4 @@ class FileIterator { auto iterate(bool reverse = false) -> bool; }; -} // namespace lua \ No newline at end of file +} // namespace lua diff --git a/src/tangara/lua/lua_filesystem.cpp b/src/tangara/lua/lua_filesystem.cpp index e3a3018d..9c2ea880 100644 --- a/src/tangara/lua/lua_filesystem.cpp +++ b/src/tangara/lua/lua_filesystem.cpp @@ -117,12 +117,6 @@ static auto file_entry_is_hidden(lua_State* state) -> int { return 1; } -static auto file_entry_is_track(lua_State* state) -> int { - lua::FileEntry* entry = check_file_entry(state, 1); - lua_pushboolean(state, entry->isTrack); - return 1; -} - static auto file_entry_name(lua_State* state) -> int { lua::FileEntry* entry = check_file_entry(state, 1); lua_pushlstring(state, entry->name.c_str(), entry->name.size()); @@ -139,7 +133,6 @@ static const struct luaL_Reg kFileEntryFuncs[] = {{"filepath", file_entry_path}, {"name", file_entry_name}, {"is_directory", file_entry_is_dir}, {"is_hidden", file_entry_is_hidden}, - {"is_track", file_entry_is_track}, {"__tostring", file_entry_name}, {"__gc", file_entry_gc}, {NULL, NULL}}; diff --git a/src/tangara/ui/ui_fsm.cpp b/src/tangara/ui/ui_fsm.cpp index a20eb0ef..94d1caf8 100644 --- a/src/tangara/ui/ui_fsm.cpp +++ b/src/tangara/ui/ui_fsm.cpp @@ -15,6 +15,8 @@ #include "FreeRTOSConfig.h" #include "draw/lv_draw_buf.h" #include "drivers/bluetooth.hpp" +#include "lauxlib.h" +#include "lua.h" #include "lvgl.h" #include "core/lv_group.h" @@ -609,11 +611,25 @@ void Lua::entry() { {"discovered_devices", &sBluetoothDiscoveredDevices}, {"known_devices", &sBluetoothKnownDevices}, }); - registry.AddPropertyModule("playback", { - {"playing", &sPlaybackPlaying}, - {"track", &sPlaybackTrack}, - {"position", &sPlaybackPosition}, - }); + registry.AddPropertyModule( + "playback", + { + {"playing", &sPlaybackPlaying}, + {"track", &sPlaybackTrack}, + {"position", &sPlaybackPosition}, + {"is_playable", + [&](lua_State* s) { + size_t len; + const char* path = luaL_checklstring(s, 1, &len); + auto res = sServices->tag_parser().ReadAndParseTags({path, len}); + if (res) { + lua_pushboolean(s, true); + } else { + lua_pushboolean(s, false); + } + return 1; + }}, + }); registry.AddPropertyModule( "queue", { -- cgit v1.2.3 From 96a224c0df4f647b3e5dbbcbbedad3a1d38470ba Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 29 Aug 2024 15:20:22 +1000 Subject: Lua API improvements and fixes Co-authored-by: jacqueline --- src/tangara/audio/track_queue.cpp | 15 +++++++++++++++ src/tangara/audio/track_queue.hpp | 1 + src/tangara/ui/ui_fsm.cpp | 19 ++++++++++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/tangara/audio/track_queue.cpp b/src/tangara/audio/track_queue.cpp index cc4770ae..2c1faf96 100644 --- a/src/tangara/audio/track_queue.cpp +++ b/src/tangara/audio/track_queue.cpp @@ -236,6 +236,21 @@ auto TrackQueue::next() -> void { next(Reason::kExplicitUpdate); } +auto TrackQueue::currentPosition(size_t position) -> bool { + { + const std::shared_lock lock(mutex_); + if (position >= totalSize()) { + return false; + } + goTo(position); + } + + // If we're explicitly setting the position, we want to treat it as though + // the current track has changed, even if the position was the same + notifyChanged(true, Reason::kExplicitUpdate); + return true; +} + auto TrackQueue::goTo(size_t position) -> void { position_ = position; if (opened_playlist_) { diff --git a/src/tangara/audio/track_queue.hpp b/src/tangara/audio/track_queue.hpp index 1d25568d..a8d1dc3a 100644 --- a/src/tangara/audio/track_queue.hpp +++ b/src/tangara/audio/track_queue.hpp @@ -73,6 +73,7 @@ class TrackQueue { auto current() const -> TrackItem; auto currentPosition() const -> size_t; + auto currentPosition(size_t position) -> bool; auto totalSize() const -> size_t; auto open() -> bool; auto openPlaylist(const std::string& playlist_file, bool notify = true) diff --git a/src/tangara/ui/ui_fsm.cpp b/src/tangara/ui/ui_fsm.cpp index 94d1caf8..669b3298 100644 --- a/src/tangara/ui/ui_fsm.cpp +++ b/src/tangara/ui/ui_fsm.cpp @@ -201,7 +201,14 @@ lua::Property UiState::sPlaybackPosition{ return true; }}; -lua::Property UiState::sQueuePosition{0}; +lua::Property UiState::sQueuePosition{0, [](const lua::LuaValue& val){ + if (!std::holds_alternative(val)) { + return false; + } + int new_val = std::get(val); + // val-1 because Lua uses 1-based indexing + return sServices->track_queue().currentPosition(new_val-1); + }}; lua::Property UiState::sQueueSize{0}; lua::Property UiState::sQueueRepeat{false, [](const lua::LuaValue& val) { if (!std::holds_alternative(val)) { @@ -610,6 +617,16 @@ void Lua::entry() { {"paired_device", &sBluetoothPairedDevice}, {"discovered_devices", &sBluetoothDiscoveredDevices}, {"known_devices", &sBluetoothKnownDevices}, + {"enable", + [&](lua_State* s) { + sBluetoothEnabled.set(true); + return 0; + }}, + {"disable", + [&](lua_State* s) { + sBluetoothEnabled.set(false); + return 0; + }}, }); registry.AddPropertyModule( "playback", -- cgit v1.2.3 From 91eaed4b37c7cda29103d3478df3e2c6356f8396 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 29 Aug 2024 15:52:34 +1000 Subject: use snake_case consistently in lua function names --- src/tangara/lua/lua_screen.cpp | 6 +++--- src/tangara/ui/screen_lua.cpp | 6 +++--- src/tangara/ui/ui_fsm.cpp | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/tangara/lua/lua_screen.cpp b/src/tangara/lua/lua_screen.cpp index 8d87eebd..6bb26ec1 100644 --- a/src/tangara/lua/lua_screen.cpp +++ b/src/tangara/lua/lua_screen.cpp @@ -56,9 +56,9 @@ static auto screen_true(lua_State* state) -> int { } static const struct luaL_Reg kScreenFuncs[] = { - {"new", screen_new}, {"createUi", screen_noop}, - {"onShown", screen_noop}, {"onHidden", screen_noop}, - {"canPop", screen_true}, {NULL, NULL}}; + {"new", screen_new}, {"create_ui", screen_noop}, + {"on_show", screen_noop}, {"on_hide", screen_noop}, + {"can_pop", screen_true}, {NULL, NULL}}; static auto lua_screen(lua_State* state) -> int { luaL_newlib(state, kScreenFuncs); diff --git a/src/tangara/ui/screen_lua.cpp b/src/tangara/ui/screen_lua.cpp index 301df143..b8d11ed7 100644 --- a/src/tangara/ui/screen_lua.cpp +++ b/src/tangara/ui/screen_lua.cpp @@ -29,7 +29,7 @@ Lua::~Lua() { } auto Lua::onShown() -> void { - callMethod("onShown"); + callMethod("on_show"); forEachBinding([&](lua::Binding* b) { b->active = true; lua::Binding::apply(s_, -1); @@ -37,7 +37,7 @@ auto Lua::onShown() -> void { } auto Lua::onHidden() -> void { - callMethod("onHidden"); + callMethod("on_hide"); forEachBinding([&](lua::Binding* b) { b->active = false; }); } @@ -46,7 +46,7 @@ auto Lua::canPop() -> bool { return true; } lua_rawgeti(s_, LUA_REGISTRYINDEX, *obj_ref_); - lua_pushliteral(s_, "canPop"); + lua_pushliteral(s_, "can_pop"); if (lua_gettable(s_, -2) == LUA_TFUNCTION) { // If we got a callback instead of a value, then invoke it to turn it into diff --git a/src/tangara/ui/ui_fsm.cpp b/src/tangara/ui/ui_fsm.cpp index 669b3298..2009a888 100644 --- a/src/tangara/ui/ui_fsm.cpp +++ b/src/tangara/ui/ui_fsm.cpp @@ -754,7 +754,7 @@ auto Lua::PushLuaScreen(lua_State* s, bool replace) -> int { // Call the constructor for this screen. // lua_settop(s, 1); // Make sure the screen is actually at top of stack - lua_pushliteral(s, "createUi"); + lua_pushliteral(s, "create_ui"); if (lua_gettable(s, 1) == LUA_TFUNCTION) { lua_pushvalue(s, 1); lua::CallProtected(s, 1, 0); -- cgit v1.2.3 From e6921dc0556d567557b5673a475199121898421f Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 3 Sep 2024 11:17:13 +1000 Subject: Use libogg + our own parser for ogg files This is somewhat faster than relying on libtags to parse these, and also better handles cornercases such as tags that cross physical page boundaries. --- src/tangara/database/database.hpp | 2 +- src/tangara/database/tag_parser.cpp | 171 +++++++++++++++++++++++++++++++++++- src/tangara/database/tag_parser.hpp | 24 ++++- src/tangara/database/track.cpp | 2 +- 4 files changed, 189 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/tangara/database/database.hpp b/src/tangara/database/database.hpp index 6daffd23..18070353 100644 --- a/src/tangara/database/database.hpp +++ b/src/tangara/database/database.hpp @@ -37,7 +37,7 @@ namespace database { -const uint8_t kCurrentDbVersion = 6; +const uint8_t kCurrentDbVersion = 7; struct SearchKey; class Record; diff --git a/src/tangara/database/tag_parser.cpp b/src/tangara/database/tag_parser.cpp index d377adb1..a6a25555 100644 --- a/src/tangara/database/tag_parser.cpp +++ b/src/tangara/database/tag_parser.cpp @@ -6,14 +6,20 @@ #include "database/tag_parser.hpp" +#include #include #include +#include #include +#include #include +#include "database/track.hpp" +#include "debug.hpp" #include "drivers/spi.hpp" #include "esp_log.h" #include "ff.h" +#include "ogg/ogg.h" #include "tags.h" #include "memory_resource.hpp" @@ -106,10 +112,18 @@ static void toc(Tagctx* ctx, int ms, int offset) {} static const std::size_t kBufSize = 1024; [[maybe_unused]] static const char* kTag = "TAGS"; -TagParserImpl::TagParserImpl() {} +TagParserImpl::TagParserImpl() { + parsers_.emplace_back(new OggTagParser()); + parsers_.emplace_back(new GenericTagParser()); +} auto TagParserImpl::ReadAndParseTags(std::string_view path) -> std::shared_ptr { + if (path.empty()) { + return {}; + } + + // Check the cache first to see if we can skip parsing this file completely. { std::lock_guard lock{cache_mutex_}; std::optional> cached = @@ -119,7 +133,15 @@ auto TagParserImpl::ReadAndParseTags(std::string_view path) } } - std::shared_ptr tags = parseNew(path); + // Nothing in the cache; try each of our parsers. + std::shared_ptr tags; + for (auto& parser : parsers_) { + tags = parser->ReadAndParseTags(path); + if (tags) { + break; + } + } + if (!tags) { return {}; } @@ -135,6 +157,7 @@ auto TagParserImpl::ReadAndParseTags(std::string_view path) } } + // Store the result in the cache for later. { std::lock_guard lock{cache_mutex_}; cache_.Put({path.data(), path.size(), &memory::kSpiRamResource}, tags); @@ -143,7 +166,148 @@ auto TagParserImpl::ReadAndParseTags(std::string_view path) return tags; } -auto TagParserImpl::parseNew(std::string_view p) -> std::shared_ptr { +OggTagParser::OggTagParser() { + nameToTag_["TITLE"] = Tag::kTitle; + nameToTag_["ALBUM"] = Tag::kAlbum; + nameToTag_["ARTIST"] = Tag::kArtist; + nameToTag_["ALBUMARTIST"] = Tag::kAlbumArtist; + nameToTag_["TRACKNUMBER"] = Tag::kTrack; + nameToTag_["GENRE"] = Tag::kGenres; + nameToTag_["DISC"] = Tag::kDisc; +} + +auto OggTagParser::ReadAndParseTags(std::string_view p) + -> std::shared_ptr { + if (!p.ends_with(".ogg") && !p.ends_with(".opus") && !p.ends_with(".ogx")) { + return {}; + } + ogg_sync_state sync; + ogg_sync_init(&sync); + + ogg_page page; + ogg_stream_state stream; + bool stream_init = false; + + std::string path{p}; + FIL file; + if (f_open(&file, path.c_str(), FA_READ) != FR_OK) { + ESP_LOGW(kTag, "failed to open file '%s'", path.c_str()); + return {}; + } + + std::shared_ptr tags; + + // The comments packet is the second in the stream. This is *usually* the + // second page, sometimes overflowing onto the third page. There is no + // guarantee of this however, so we read the first five pages before giving + // up just in case. We don't try to read more pages than this as it could take + // quite some time, with no likely benefit. + for (int i = 0; i < 5; i++) { + // Load up the sync with data until we have a complete page. + while (ogg_sync_pageout(&sync, &page) != 1) { + char* buffer = ogg_sync_buffer(&sync, 512); + + UINT br; + FRESULT fres = f_read(&file, buffer, 512, &br); + if (fres != FR_OK || br == 0) { + goto finish; + } + + int res = ogg_sync_wrote(&sync, br); + if (res != 0) { + goto finish; + } + } + + // Ensure the stream has the correct serialno. pagein and packetout both + // give no results if the serialno is incorrect. + if (ogg_page_bos(&page)) { + ogg_stream_init(&stream, ogg_page_serialno(&page)); + stream_init = true; + } + + if (ogg_stream_pagein(&stream, &page) < 0) { + goto finish; + } + + // Try to pull out a packet. + ogg_packet packet; + if (ogg_stream_packetout(&stream, &packet) == 1) { + // We're interested in the second packet (packetno == 1) only. + if (packet.packetno < 1) { + continue; + } + if (packet.packetno > 1) { + goto finish; + } + + tags = TrackTags::create(); + if (memcmp(packet.packet, "OpusTags", 8) == 0) { + std::span data{packet.packet, + static_cast(packet.bytes)}; + tags->encoding(Container::kOpus); + parseComments(*tags, data.subspan(8)); + } else if (packet.packet[0] == 3 && + memcmp(packet.packet + 1, "vorbis", 6) == 0) { + std::span data{packet.packet, + static_cast(packet.bytes)}; + tags->encoding(Container::kOgg); + parseComments(*tags, data.subspan(7)); + } + break; + } + } + +finish: + if (stream_init) { + ogg_stream_clear(&stream); + } + ogg_sync_clear(&sync); + f_close(&file); + + return tags; +} + +auto OggTagParser::parseComments(TrackTags& res, std::span data) + -> void { + uint64_t vendor_len = parseLength(data); + uint64_t num_tags = parseLength(data.subspan(4 + vendor_len)); + + data = data.subspan(4 + vendor_len + 4); + for (size_t i = 0; i < num_tags; i++) { + uint64_t size = parseLength(data); + + std::string_view tag = { + reinterpret_cast(data.subspan(4).data()), + static_cast(size)}; + + auto split = tag.find("="); + + if (split != std::string::npos) { + std::string_view key = tag.substr(0, split); + std::string_view val = tag.substr(split + 1); + + std::string key_upper{key}; + std::transform(key.begin(), key.end(), key_upper.begin(), ::toupper); + + if (nameToTag_.contains(key_upper) && !val.empty()) { + res.set(nameToTag_[key_upper], val); + } + } + + data = data.subspan(4 + size); + } +} + +auto OggTagParser::parseLength(std::span data) -> uint64_t { + return static_cast(data[3]) << 24 | + static_cast(data[2]) << 16 | + static_cast(data[1]) << 8 | + static_cast(data[0]) << 0; +} + +auto GenericTagParser::ReadAndParseTags(std::string_view p) + -> std::shared_ptr { std::string path{p}; libtags::Aux aux; auto out = TrackTags::create(); @@ -151,7 +315,6 @@ auto TagParserImpl::parseNew(std::string_view p) -> std::shared_ptr { if (f_stat(path.c_str(), &aux.info) != FR_OK || f_open(&aux.file, path.c_str(), FA_READ) != FR_OK) { - ESP_LOGW(kTag, "failed to open file %s", path.c_str()); return {}; } diff --git a/src/tangara/database/tag_parser.hpp b/src/tangara/database/tag_parser.hpp index ccbc0ea9..642c4876 100644 --- a/src/tangara/database/tag_parser.hpp +++ b/src/tangara/database/tag_parser.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include #include "database/track.hpp" @@ -27,7 +28,7 @@ class TagParserImpl : public ITagParser { -> std::shared_ptr override; private: - auto parseNew(std::string_view path) -> std::shared_ptr; + std::vector> parsers_; /* * Cache of tags that have already been extracted from files. Ideally this @@ -35,10 +36,25 @@ class TagParserImpl : public ITagParser { */ std::mutex cache_mutex_; util::LruCache<8, std::pmr::string, std::shared_ptr> cache_; +}; + +class OggTagParser : public ITagParser { + public: + OggTagParser(); + auto ReadAndParseTags(std::string_view path) + -> std::shared_ptr override; + + private: + auto parseComments(TrackTags&, std::span data) -> void; + auto parseLength(std::span data) -> uint64_t; + + std::unordered_map nameToTag_; +}; - // 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 GenericTagParser : public ITagParser { + public: + auto ReadAndParseTags(std::string_view path) + -> std::shared_ptr override; }; } // namespace database diff --git a/src/tangara/database/track.cpp b/src/tangara/database/track.cpp index 461f4561..cdb7543c 100644 --- a/src/tangara/database/track.cpp +++ b/src/tangara/database/track.cpp @@ -148,7 +148,7 @@ auto TrackTags::set(Tag t, std::string_view v) -> void { track(v); break; case Tag::kAlbumOrder: - // This tag is derices from disc and track, and so it can't be set. + // This tag is derived from disc and track, and so it can't be set. break; case Tag::kGenres: genres(v); -- cgit v1.2.3 From 99a3a904e4d9cc2e4d92edbbf1ebbd0892d3918e Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 3 Sep 2024 14:36:54 +1000 Subject: Handle collation text that includes '\0' This seems to be tickled by the ogg comment handling changes (possibly libtags doesn't actually handle this case?) --- src/locale/collation.cpp | 4 ++-- src/tangara/database/records.cpp | 16 ++++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/locale/collation.cpp b/src/locale/collation.cpp index 8748742d..c3360483 100644 --- a/src/locale/collation.cpp +++ b/src/locale/collation.cpp @@ -89,8 +89,8 @@ GLibCollator::~GLibCollator() { auto GLibCollator::Transform(const std::string& in) -> std::string { size_t size = glib_strxfrm(NULL, in.c_str(), 0, locale_data_.get()); char* dest = new char[size + 1]{0}; - glib_strxfrm(dest, in.c_str(), size, locale_data_.get()); - std::string out{dest, strnlen(dest, size)}; + size = glib_strxfrm(dest, in.c_str(), size, locale_data_.get()); + std::string out{dest, size}; delete[] dest; return out; } diff --git a/src/tangara/database/records.cpp b/src/tangara/database/records.cpp index 6406f080..17009cd8 100644 --- a/src/tangara/database/records.cpp +++ b/src/tangara/database/records.cpp @@ -227,19 +227,15 @@ auto ParseIndexKey(const leveldb::Slice& slice) -> std::optional { return {}; } - std::istringstream in(key_data.substr(header_length + 1)); - std::stringbuf buffer{}; + key_data = key_data.substr(header_length + 1); + size_t last_sep = key_data.find_last_of('\0'); - // FIXME: what if the item contains a '\0'? Probably we make a big mess. - in.get(buffer, kFieldSeparator); - if (buffer.str().size() > 0) { - result.item = buffer.str(); + if (last_sep > 0) { + result.item = key_data.substr(0, last_sep); } - std::string id_str = - key_data.substr(header_length + 1 + buffer.str().size() + 1); - if (id_str.size() > 0) { - result.track = BytesToTrackId(id_str); + if (last_sep + 1 < key_data.size()) { + result.track = BytesToTrackId(key_data.substr(last_sep + 1)); } return result; -- cgit v1.2.3 From 172d31ec6d69d7372d46e1ba712a2a2187e19d2a Mon Sep 17 00:00:00 2001 From: jacqueline Date: Fri, 6 Sep 2024 12:40:11 +1000 Subject: Ignore comments within playlist files Includes a general cleanup+restructure of playlist.cpp, and fixing the tests and benchmarks --- src/drivers/test/test_samd.cpp | 4 +- src/tangara/audio/playlist.cpp | 335 +++++++++++++++++++----------- src/tangara/audio/playlist.hpp | 49 +++-- src/tangara/test/audio/test_playlist.cpp | 71 +++++-- src/tangara/test/battery/test_battery.cpp | 3 +- 5 files changed, 309 insertions(+), 153 deletions(-) (limited to 'src') diff --git a/src/drivers/test/test_samd.cpp b/src/drivers/test/test_samd.cpp index c466d88e..96248377 100644 --- a/src/drivers/test/test_samd.cpp +++ b/src/drivers/test/test_samd.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-only */ +#include "drivers/nvs.hpp" #include "drivers/samd.hpp" #include @@ -16,7 +17,8 @@ namespace drivers { TEST_CASE("samd21 interface", "[integration]") { I2CFixture i2c; - auto samd = std::make_unique(); + std::unique_ptr nvs{drivers::NvsStorage::OpenSync()}; + auto samd = std::make_unique(*nvs); REQUIRE(samd); diff --git a/src/tangara/audio/playlist.cpp b/src/tangara/audio/playlist.cpp index 944ad143..1436e2d2 100644 --- a/src/tangara/audio/playlist.cpp +++ b/src/tangara/audio/playlist.cpp @@ -5,14 +5,16 @@ */ #include "playlist.hpp" -#include +#include -#include "audio/playlist.hpp" -#include "database/database.hpp" #include "esp_log.h" #include "ff.h" +#include "audio/playlist.hpp" +#include "database/database.hpp" + namespace audio { + [[maybe_unused]] static constexpr char kTag[] = "playlist"; Playlist::Playlist(const std::string& playlistFilepath) @@ -20,190 +22,281 @@ Playlist::Playlist(const std::string& playlistFilepath) mutex_(), total_size_(0), pos_(-1), + file_open_(false), + file_error_(false), offset_cache_(&memory::kSpiRamResource), sample_size_(50) {} 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) { ESP_LOGE(kTag, "failed to open file! res: %i", res); return false; } - // Count all entries - consumeAndCount(-1); - // Grab the first one - skipTo(0); - return true; + file_open_ = true; + file_error_ = false; + + // Count the playlist size and build our offset cache. + countItems(); + // Advance to the first item. + skipToWithoutCache(0); + + return !file_error_; } Playlist::~Playlist() { - f_close(&file_); + if (file_open_) { + f_close(&file_); + } +} + +auto Playlist::filepath() const -> std::string { + return filepath_; } auto Playlist::currentPosition() const -> size_t { - return pos_; + std::unique_lock lock(mutex_); + return pos_ < 0 ? 0 : pos_; } auto Playlist::size() const -> size_t { + std::unique_lock lock(mutex_); return total_size_; } -auto MutablePlaylist::append(Item i) -> void { +auto Playlist::value() const -> std::string { std::unique_lock lock(mutex_); - auto offset = f_tell(&file_); - bool first_entry = current_value_.empty(); - // Seek to end and append - auto end = f_size(&file_); - auto res = f_lseek(&file_, end); - if (res != FR_OK) { - ESP_LOGE(kTag, "Seek to end of file failed? Error %d", res); - return; - } - // TODO: Resolve paths for track id, etc - std::string path; - if (std::holds_alternative(i)) { - path = std::get(i); - f_printf(&file_, "%s\n", path.c_str()); - if (total_size_ % sample_size_ == 0) { - offset_cache_.push_back(end); - } - if (first_entry) { - current_value_ = path; - } - total_size_++; - } - // Restore position - res = f_lseek(&file_, offset); - if (res != FR_OK) { - ESP_LOGE(kTag, "Failed to restore file position after append?"); - return; + return current_value_; +} + +auto Playlist::atEnd() const -> bool { + std::unique_lock lock(mutex_); + return pos_ + 1 >= total_size_; +} + +auto Playlist::next() -> void { + std::unique_lock lock(mutex_); + if (pos_ + 1 < total_size_ && !file_error_) { + advanceBy(1); } - res = f_sync(&file_); - if (res != FR_OK) { - ESP_LOGE(kTag, "Failed to sync playlist file after append"); - return; +} + +auto Playlist::prev() -> void { + std::unique_lock lock(mutex_); + if (!file_error_) { + // Naive approach to see how that goes for now + skipToLocked(pos_ - 1); } } auto Playlist::skipTo(size_t position) -> void { std::unique_lock lock(mutex_); + skipToLocked(position); +} + +auto Playlist::skipToLocked(size_t position) -> void { + if (!file_open_ || file_error_) { + return; + } + // Check our cache and go to nearest entry - pos_ = position; auto remainder = position % sample_size_; auto quotient = (position - remainder) / sample_size_; if (offset_cache_.size() <= quotient) { - // Fall back case - ESP_LOGW(kTag, "File offset cache failed, falling back..."); - f_rewind(&file_); - advanceBy(pos_); + skipToWithoutCache(position); return; } - auto entry = offset_cache_.at(quotient); + // Go to byte offset + auto entry = offset_cache_.at(quotient); auto res = f_lseek(&file_, entry); if (res != FR_OK) { - ESP_LOGW(kTag, "Error going to byte offset %llu for playlist entry index %d", entry, pos_); + ESP_LOGW(kTag, "error seeking %u", res); + file_error_ = true; + return; } - // Count ahead entries - advanceBy(remainder+1); + + // Count ahead entries. + advanceBy(remainder + 1); } -auto Playlist::next() -> void { - if (!atEnd()) { - pos_++; - skipTo(pos_); +auto Playlist::skipToWithoutCache(size_t position) -> void { + if (position >= pos_) { + advanceBy(position - pos_); + } else { + pos_ = -1; + FRESULT res = f_rewind(&file_); + if (res != FR_OK) { + ESP_LOGW(kTag, "error rewinding %u", res); + file_error_ = true; + return; + } + advanceBy(position + 1); } } -auto Playlist::prev() -> void { - // Naive approach to see how that goes for now - pos_--; - skipTo(pos_); -} +auto Playlist::countItems() -> void { + TCHAR buff[512]; -auto Playlist::value() const -> std::string { - return current_value_; + for (;;) { + auto offset = f_tell(&file_); + auto next_item = nextItem(buff); + if (!next_item) { + break; + } + if (total_size_ % sample_size_ == 0) { + offset_cache_.push_back(offset); + } + total_size_++; + } + + f_rewind(&file_); } -MutablePlaylist::MutablePlaylist(const std::string& playlistFilepath) : Playlist(playlistFilepath) {} +auto Playlist::advanceBy(ssize_t amt) -> bool { + TCHAR buff[512]; + std::optional item; -auto MutablePlaylist::clear() -> bool { - std::unique_lock lock(mutex_); - auto res = f_close(&file_); - if (res != FR_OK) { - return false; + while (amt > 0) { + item = nextItem(buff); + if (!item) { + break; + } + pos_++; + amt--; } - res = - f_open(&file_, filepath_.c_str(), FA_READ | FA_WRITE | FA_CREATE_ALWAYS); - if (res != FR_OK) { - return false; + + if (item) { + current_value_ = *item; } - total_size_ = 0; - current_value_.clear(); - offset_cache_.clear(); - pos_ = 0; - return true; -} -auto Playlist::atEnd() -> bool { - return pos_ + 1 >= total_size_; + return amt == 0; } -auto Playlist::filepath() -> std::string { - return filepath_; +auto Playlist::nextItem(std::span buf) + -> std::optional { + while (file_open_ && !file_error_ && !f_eof(&file_)) { + // FIXME: f_gets is quite slow (it does several very small reads instead of + // grabbing a whole sector at a time), and it doesn't work well for very + // long lines. We should do something smarter here. + TCHAR* str = f_gets(buf.data(), buf.size(), &file_); + if (str == NULL) { + ESP_LOGW(kTag, "Error consuming playlist file at offset %llu", + f_tell(&file_)); + file_error_ = true; + return {}; + } + + std::string_view line{str}; + if (line.starts_with("#")) { + continue; + } + if (line.ends_with('\n')) { + line = line.substr(0, line.size() - 1); + } + return line; + } + + // Got to EOF without reading a valid line. + return {}; } -auto Playlist::consumeAndCount(ssize_t upto) -> bool { +MutablePlaylist::MutablePlaylist(const std::string& playlistFilepath) + : Playlist(playlistFilepath) {} + +auto MutablePlaylist::clear() -> bool { std::unique_lock lock(mutex_); - TCHAR buff[512]; - size_t count = 0; - f_rewind(&file_); - while (!f_eof(&file_)) { - auto offset = f_tell(&file_); - // TODO: Correctly handle lines longer than this - // TODO: Also correctly handle the case where the last entry doesn't end in - // \n - auto res = f_gets(buff, 512, &file_); - if (res == NULL) { - ESP_LOGW(kTag, "Error consuming playlist file at line %d", count); + + // Try to recover from any IO errors. + if (file_error_ && file_open_) { + file_error_ = false; + file_open_ = false; + f_close(&file_); + } + + FRESULT res; + if (file_open_) { + res = f_rewind(&file_); + if (res != FR_OK) { + ESP_LOGE(kTag, "error rewinding %u", res); + file_error_ = true; return false; } - if (count % sample_size_ == 0) { - offset_cache_.push_back(offset); + res = f_truncate(&file_); + if (res != FR_OK) { + ESP_LOGE(kTag, "error truncating %u", res); + file_error_ = true; + return false; } - count++; - - if (upto >= 0 && count > upto) { - size_t len = strlen(buff); - current_value_.assign(buff, len - 1); - break; + } else { + res = f_open(&file_, filepath_.c_str(), + FA_READ | FA_WRITE | FA_CREATE_ALWAYS); + if (res != FR_OK) { + ESP_LOGE(kTag, "error opening file %u", res); + file_error_ = true; + return false; } + file_open_ = true; } - if (upto < 0) { - total_size_ = count; - f_rewind(&file_); - } + + total_size_ = 0; + current_value_.clear(); + offset_cache_.clear(); + pos_ = -1; return true; } -auto Playlist::advanceBy(ssize_t amt) -> bool { - TCHAR buff[512]; - size_t count = 0; - while (!f_eof(&file_)) { - auto res = f_gets(buff, 512, &file_); - if (res == NULL) { - ESP_LOGW(kTag, "Error consuming playlist file at line %d", count); - return false; +auto MutablePlaylist::append(Item i) -> void { + std::unique_lock lock(mutex_); + if (!file_open_ || file_error_) { + return; + } + + auto offset = f_tell(&file_); + bool first_entry = current_value_.empty(); + + // Seek to end and append + auto end = f_size(&file_); + auto res = f_lseek(&file_, end); + if (res != FR_OK) { + ESP_LOGE(kTag, "Seek to end of file failed? Error %d", res); + file_error_ = true; + return; + } + + // TODO: Resolve paths for track id, etc + std::string path; + if (std::holds_alternative(i)) { + path = std::get(i); + f_printf(&file_, "%s\n", path.c_str()); + if (total_size_ % sample_size_ == 0) { + offset_cache_.push_back(end); } - count++; - if (count >= amt) { - size_t len = strlen(buff); - current_value_.assign(buff, len - 1); - break; + if (first_entry) { + current_value_ = path; } + total_size_++; + } + + // Restore position + res = f_lseek(&file_, offset); + if (res != FR_OK) { + ESP_LOGE(kTag, "Failed to restore file position after append?"); + file_error_ = true; + return; + } + res = f_sync(&file_); + if (res != FR_OK) { + ESP_LOGE(kTag, "Failed to sync playlist file after append"); + file_error_ = true; + return; } - return true; } -} // namespace audio \ No newline at end of file +} // namespace audio diff --git a/src/tangara/audio/playlist.hpp b/src/tangara/audio/playlist.hpp index b248ac77..ac62c82e 100644 --- a/src/tangara/audio/playlist.hpp +++ b/src/tangara/audio/playlist.hpp @@ -6,11 +6,14 @@ */ #pragma once + #include #include + +#include "ff.h" + #include "database/database.hpp" #include "database/track.hpp" -#include "ff.h" namespace audio { @@ -31,40 +34,54 @@ class Playlist { using Item = std::variant; auto open() -> bool; + + auto filepath() const -> std::string; auto currentPosition() const -> size_t; auto size() const -> size_t; - auto skipTo(size_t position) -> void; + auto value() const -> std::string; + auto atEnd() const -> bool; + auto next() -> void; auto prev() -> void; - auto value() const -> std::string; - auto atEnd() -> bool; - auto filepath() -> std::string; + auto skipTo(size_t position) -> void; protected: - std::string filepath_; - std::mutex mutex_; + const std::string filepath_; + + mutable std::mutex mutex_; size_t total_size_; - size_t pos_; + ssize_t pos_; + FIL file_; + bool file_open_; + bool file_error_; + std::string current_value_; + /* List of offsets determined by sample size */ + std::pmr::vector offset_cache_; - std::pmr::vector offset_cache_; // List of offsets determined by sample size; /* - * How many tracks per offset saved (ie, a value of 100 means every 100 tracks the file offset is saved) - * This speeds up searches, especially in the case of shuffling a lot of tracks. - */ - const uint32_t sample_size_; + * How many tracks per offset saved (ie, a value of 100 means every 100 tracks + * the file offset is saved) This speeds up searches, especially in the case + * of shuffling a lot of tracks. + */ + const uint32_t sample_size_; - auto consumeAndCount(ssize_t upto) -> bool; + private: + auto skipToLocked(size_t position) -> void; + auto countItems() -> void; auto advanceBy(ssize_t amt) -> bool; + auto nextItem(std::span) -> std::optional; + auto skipToWithoutCache(size_t position) -> void; }; class MutablePlaylist : public Playlist { -public: + public: MutablePlaylist(const std::string& playlistFilepath); + auto clear() -> bool; auto append(Item i) -> void; }; -} // namespace audio \ No newline at end of file +} // namespace audio diff --git a/src/tangara/test/audio/test_playlist.cpp b/src/tangara/test/audio/test_playlist.cpp index 147b3ac0..34a6bc56 100644 --- a/src/tangara/test/audio/test_playlist.cpp +++ b/src/tangara/test/audio/test_playlist.cpp @@ -9,18 +9,16 @@ #include #include -#include -#include #include "catch2/catch.hpp" #include "drivers/gpios.hpp" #include "drivers/i2c.hpp" -#include "drivers/storage.hpp" #include "drivers/spi.hpp" +#include "drivers/storage.hpp" +#include "ff.h" #include "i2c_fixture.hpp" #include "spi_fixture.hpp" -#include "ff.h" namespace audio { @@ -39,9 +37,17 @@ TEST_CASE("playlist file", "[integration]") { } { - std::unique_ptr result(drivers::SdStorage::Create(*gpios).value()); - Playlist plist(kTestFilePath); - REQUIRE(plist.clear()); + std::unique_ptr result( + drivers::SdStorage::Create(*gpios).value()); + MutablePlaylist plist(kTestFilePath); + + SECTION("empty file appears empty") { + REQUIRE(plist.clear()); + + REQUIRE(plist.size() == 0); + REQUIRE(plist.currentPosition() == 0); + REQUIRE(plist.value().empty()); + } SECTION("write to the playlist file") { plist.append("test1.mp3"); @@ -56,6 +62,7 @@ TEST_CASE("playlist file", "[integration]") { SECTION("read from the playlist file") { Playlist plist2(kTestFilePath); + REQUIRE(plist2.open()); REQUIRE(plist2.size() == 8); REQUIRE(plist2.value() == "test1.mp3"); plist2.next(); @@ -65,22 +72,58 @@ TEST_CASE("playlist file", "[integration]") { } } - BENCHMARK("appending item") { - plist.append("A/New/Item.wav"); + REQUIRE(plist.clear()); + + size_t tracks = 0; + + BENCHMARK("appending items") { + plist.append("track " + std::to_string(plist.size())); + return tracks++; }; - BENCHMARK("opening playlist file") { + BENCHMARK("opening large playlist file") { Playlist plist2(kTestFilePath); - REQUIRE(plist2.size() > 100); + REQUIRE(plist2.open()); + REQUIRE(plist2.size() == tracks); return plist2.size(); }; - BENCHMARK("opening playlist file and appending entry") { + BENCHMARK("seeking after appending a large file") { + REQUIRE(plist.size() == tracks); + + plist.skipTo(50); + REQUIRE(plist.value() == "track 50"); + plist.skipTo(99); + REQUIRE(plist.value() == "track 99"); + plist.skipTo(1); + REQUIRE(plist.value() == "track 1"); + + return plist.size(); + }; + + BENCHMARK("seeking after opening a large file") { Playlist plist2(kTestFilePath); - REQUIRE(plist2.size() > 100); + REQUIRE(plist2.open()); + REQUIRE(plist.size() == tracks); + REQUIRE(tracks >= 100); + + plist.skipTo(50); + REQUIRE(plist.value() == "track 50"); + plist.skipTo(99); + REQUIRE(plist.value() == "track 99"); + plist.skipTo(1); + REQUIRE(plist.value() == "track 1"); + + return plist.size(); + }; + + BENCHMARK("opening a large file and appending") { + MutablePlaylist plist2(kTestFilePath); + REQUIRE(plist2.open()); + REQUIRE(plist2.size() >= 100); plist2.append("A/Nother/New/Item.opus"); return plist2.size(); }; } } -} // namespace audio +} // namespace audio diff --git a/src/tangara/test/battery/test_battery.cpp b/src/tangara/test/battery/test_battery.cpp index 7b55bd59..cf6b19b0 100644 --- a/src/tangara/test/battery/test_battery.cpp +++ b/src/tangara/test/battery/test_battery.cpp @@ -26,9 +26,10 @@ class FakeAdc : public drivers::AdcBattery { TEST_CASE("battery charge state", "[unit]") { I2CFixture i2c; + std::unique_ptr nvs{drivers::NvsStorage::OpenSync()}; // FIXME: mock the SAMD21 as well. - std::unique_ptr samd{drivers::Samd::Create()}; + auto samd = std::make_unique(*nvs); FakeAdc* adc = new FakeAdc{}; // Freed by Battery. Battery battery{*samd, std::unique_ptr{adc}}; -- cgit v1.2.3 From dacf3efc45677343479b4d3ff9502504b211639a Mon Sep 17 00:00:00 2001 From: jacqueline Date: Fri, 6 Sep 2024 14:53:01 +1000 Subject: Look for music in "/Music", with the root dir as a fallback --- src/tangara/database/database.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/tangara/database/database.cpp b/src/tangara/database/database.cpp index 491ad8b7..64451f48 100644 --- a/src/tangara/database/database.cpp +++ b/src/tangara/database/database.cpp @@ -51,6 +51,7 @@ static SingletonEnv sEnv; [[maybe_unused]] static const char* kTag = "DB"; static const char kDbPath[] = "/.tangara-db"; +static const char kMusicPath[] = "Music"; static const char kKeyDbVersion[] = "schema_version"; static const char kKeyCustom[] = "U\0"; @@ -422,8 +423,14 @@ auto Database::updateIndexes() -> void { update_tracker_->onVerificationFinished(); // Stage 2: search for newly added files. - ESP_LOGI(kTag, "scanning for new tracks"); - track_finder_.launch(""); + std::string root; + FF_DIR dir; + if (f_opendir(&dir, kMusicPath) == FR_OK) { + f_closedir(&dir); + root = kMusicPath; + } + ESP_LOGI(kTag, "scanning for new tracks in '%s'", root.c_str()); + track_finder_.launch(root); }; auto Database::processCandidateCallback(FILINFO& info, std::string_view path) -- cgit v1.2.3