diff --git a/lua/browser.lua b/lua/browser.lua index 49088389..a7f0c336 100644 --- a/lua/browser.lua +++ b/lua/browser.lua @@ -5,6 +5,7 @@ local font = require("font") local queue = require("queue") local playing = require("playing") local theme = require("theme") +local playback = require("playback") local browser = {} @@ -67,12 +68,14 @@ function browser.create(opts) local enqueue = widgets.IconBtn(buttons, "//lua/img/enqueue.png", "Enqueue") enqueue:onClicked(function() queue.add(original_iterator) + playback.playing:set(true) end) -- enqueue:add_flag(lvgl.FLAG.HIDDEN) local play = widgets.IconBtn(buttons, "//lua/img/play_small.png", "Play") play:onClicked(function() queue.clear() queue.add(original_iterator) + playback.playing:set(true) backstack.push(playing) end ) @@ -109,6 +112,7 @@ function browser.create(opts) else queue.clear() queue.add(contents) + playback.playing:set(true) backstack.push(playing) end end) diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index b219ab6e..8ed5efbb 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -9,6 +9,6 @@ idf_component_register( "audio_source.cpp" INCLUDE_DIRS "include" REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" - "database" "system_fsm" "speexdsp" "millershuffle") + "database" "system_fsm" "speexdsp" "millershuffle" "libcppbor") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 560e655a..e37c887b 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -49,6 +49,7 @@ std::shared_ptr AudioState::sBtOutput; std::shared_ptr AudioState::sOutput; std::optional AudioState::sCurrentTrack; +bool AudioState::sIsPlaybackAllowed; void AudioState::react(const system_fsm::KeyLockChanged& ev) { if (ev.locking && sServices) { @@ -138,6 +139,23 @@ auto AudioState::playTrack(database::TrackId id) -> void { }); } +auto AudioState::readyToPlay() -> bool { + return sCurrentTrack.has_value() && sIsPlaybackAllowed; +} + +void AudioState::react(const TogglePlayPause& ev) { + sIsPlaybackAllowed = !sIsPlaybackAllowed; + if (readyToPlay()) { + if (!is_in_state()) { + transit(); + } + } else { + if (!is_in_state()) { + transit(); + } + } +} + namespace states { void Uninitialised::react(const system_fsm::BootComplete& ev) { @@ -199,21 +217,55 @@ void Playback::react(const PlayFile& ev) { } void Standby::react(const internal::InputFileOpened& ev) { - transit(); + if (readyToPlay()) { + transit(); + } } void Standby::react(const QueueUpdate& ev) { auto current_track = sServices->track_queue().current(); - if (!current_track || (sCurrentTrack && *sCurrentTrack == *current_track)) { + if (!current_track || (sCurrentTrack && (*sCurrentTrack == *current_track))) { return; } playTrack(*current_track); } -void Standby::react(const TogglePlayPause& ev) { - if (sCurrentTrack) { - transit(); +static const char kQueueKey[] = "audio:queue"; + +void Standby::react(const system_fsm::KeyLockChanged& ev) { + if (!ev.locking) { + return; } + sServices->bg_worker().Dispatch([]() { + auto db = sServices->database().lock(); + if (!db) { + return; + } + auto& queue = sServices->track_queue(); + if (queue.totalSize() <= queue.currentPosition()) { + // Nothing is playing, so don't bother saving the queue. + db->put(kQueueKey, ""); + return; + } + db->put(kQueueKey, queue.serialise()); + }); +} + +void Standby::react(const system_fsm::StorageMounted& ev) { + sServices->bg_worker().Dispatch([]() { + auto db = sServices->database().lock(); + if (!db) { + return; + } + auto res = db->get(kQueueKey); + if (res) { + // Don't restore the same queue again. This ideally should do nothing, + // but guards against bad edge cases where restoring the queue ends up + // causing a crash. + db->put(kQueueKey, ""); + sServices->track_queue().deserialise(*res); + } + }); } void Playback::entry() { @@ -226,17 +278,14 @@ void Playback::entry() { void Playback::exit() { ESP_LOGI(kTag, "finishing playback"); - // TODO(jacqueline): Second case where it's useful to wait for the i2s buffer - // to drain. - vTaskDelay(pdMS_TO_TICKS(10)); sOutput->SetMode(IAudioOutput::Modes::kOnPaused); // Stash the current volume now, in case it changed during playback, since we // might be powering off soon. sServices->nvs().AmpCurrentVolume(sOutput->GetVolume()); - events::System().Dispatch(PlaybackFinished{}); - events::Ui().Dispatch(PlaybackFinished{}); + events::System().Dispatch(PlaybackStopped{}); + events::Ui().Dispatch(PlaybackStopped{}); } void Playback::react(const system_fsm::HasPhonesChanged& ev) { @@ -259,10 +308,6 @@ void Playback::react(const QueueUpdate& ev) { playTrack(*current_track); } -void Playback::react(const TogglePlayPause& ev) { - transit(); -} - void Playback::react(const PlaybackUpdate& ev) { ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed, ev.track->duration); diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp index b76d8c89..03584062 100644 --- a/src/audio/include/audio_events.hpp +++ b/src/audio/include/audio_events.hpp @@ -35,7 +35,7 @@ struct PlaybackUpdate : tinyfsm::Event { std::shared_ptr track; }; -struct PlaybackFinished : tinyfsm::Event {}; +struct PlaybackStopped : tinyfsm::Event {}; struct QueueUpdate : tinyfsm::Event { bool current_changed; diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp index b8c505b0..884af8a8 100644 --- a/src/audio/include/audio_fsm.hpp +++ b/src/audio/include/audio_fsm.hpp @@ -52,12 +52,13 @@ class AudioState : public tinyfsm::Fsm { void react(const OutputModeChanged&); virtual void react(const system_fsm::BootComplete&) {} - void react(const system_fsm::KeyLockChanged&); + virtual void react(const system_fsm::KeyLockChanged&); + virtual void react(const system_fsm::StorageMounted&) {} virtual void react(const PlayFile&) {} virtual void react(const QueueUpdate&) {} virtual void react(const PlaybackUpdate&) {} - virtual void react(const TogglePlayPause&) {} + void react(const TogglePlayPause&); virtual void react(const internal::InputFileOpened&) {} virtual void react(const internal::InputFileClosed&) {} @@ -77,6 +78,9 @@ class AudioState : public tinyfsm::Fsm { static std::shared_ptr sOutput; static std::optional sCurrentTrack; + + auto readyToPlay() -> bool; + static bool sIsPlaybackAllowed; }; namespace states { @@ -92,7 +96,8 @@ class Standby : public AudioState { void react(const PlayFile&) override; void react(const internal::InputFileOpened&) override; void react(const QueueUpdate&) override; - void react(const TogglePlayPause&) override; + void react(const system_fsm::KeyLockChanged&) override; + void react(const system_fsm::StorageMounted&) override; using AudioState::react; }; @@ -107,7 +112,6 @@ class Playback : public AudioState { void react(const PlayFile&) override; void react(const QueueUpdate&) override; void react(const PlaybackUpdate&) override; - void react(const TogglePlayPause&) override; void react(const internal::InputFileOpened&) override; void react(const internal::InputFileClosed&) override; diff --git a/src/audio/include/track_queue.hpp b/src/audio/include/track_queue.hpp index 0ff72021..5b14fd4a 100644 --- a/src/audio/include/track_queue.hpp +++ b/src/audio/include/track_queue.hpp @@ -98,6 +98,9 @@ class TrackQueue { auto repeat(bool) -> void; auto repeat() const -> bool; + auto serialise() -> std::string; + auto deserialise(const std::string&) -> void; + // Cannot be copied or moved. TrackQueue(const TrackQueue&) = delete; TrackQueue& operator=(const TrackQueue&) = delete; diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp index 33858e0a..d68f2821 100644 --- a/src/audio/track_queue.cpp +++ b/src/audio/track_queue.cpp @@ -5,6 +5,7 @@ */ #include "track_queue.hpp" +#include #include #include @@ -310,4 +311,74 @@ auto TrackQueue::repeat() const -> bool { return repeat_; } +auto TrackQueue::serialise() -> std::string { + cppbor::Array tracks{}; + for (database::TrackId track : tracks_) { + tracks.add(cppbor::Uint(track)); + } + // FIXME: this should include the RandomIterator's seed as well. + cppbor::Array encoded{ + cppbor::Uint{pos_}, + std::move(tracks), + }; + return encoded.toString(); +} + +class QueueParseClient : public cppbor::ParseClient { + public: + QueueParseClient(size_t& pos, std::pmr::vector& tracks) + : pos_(pos), + tracks_(tracks), + in_root_array_(false), + in_track_list_(false) {} + + ParseClient* item(std::unique_ptr& item, + const uint8_t* hdrBegin, + const uint8_t* valueBegin, + const uint8_t* end) override { + if (item->type() == cppbor::ARRAY) { + if (!in_root_array_) { + in_root_array_ = true; + } else { + in_track_list_ = true; + } + } else if (item->type() == cppbor::UINT) { + auto val = item->asUint()->unsignedValue(); + if (in_track_list_) { + tracks_.push_back(val); + } else { + pos_ = static_cast(val); + } + } + return this; + } + + ParseClient* itemEnd(std::unique_ptr& item, + const uint8_t* hdrBegin, + const uint8_t* valueBegin, + const uint8_t* end) override { + return this; + } + + void error(const uint8_t* position, + const std::string& errorMessage) override {} + + private: + size_t& pos_; + std::pmr::vector& tracks_; + + bool in_root_array_; + bool in_track_list_; +}; + +auto TrackQueue::deserialise(const std::string& s) -> void { + if (s.empty()) { + return; + } + QueueParseClient client{pos_, tracks_}; + const uint8_t* data = reinterpret_cast(s.data()); + cppbor::parse(data, data + s.size(), &client); + notifyChanged(true); +} + } // namespace audio diff --git a/src/system_fsm/include/system_events.hpp b/src/system_fsm/include/system_events.hpp index a363887e..32394958 100644 --- a/src/system_fsm/include/system_events.hpp +++ b/src/system_fsm/include/system_events.hpp @@ -76,6 +76,7 @@ struct GpioInterrupt : tinyfsm::Event {}; struct SamdInterrupt : tinyfsm::Event {}; struct IdleTimeout : tinyfsm::Event {}; +struct UnmountTimeout : tinyfsm::Event {}; } // namespace internal diff --git a/src/system_fsm/include/system_fsm.hpp b/src/system_fsm/include/system_fsm.hpp index 5a0ea599..cc60e43b 100644 --- a/src/system_fsm/include/system_fsm.hpp +++ b/src/system_fsm/include/system_fsm.hpp @@ -63,8 +63,9 @@ class SystemState : public tinyfsm::Fsm { virtual void react(const SdDetectChanged&) {} virtual void react(const SamdUsbMscChanged&) {} virtual void react(const database::event::UpdateFinished&) {} - virtual void react(const audio::PlaybackFinished&) {} + virtual void react(const audio::PlaybackStopped&) {} virtual void react(const internal::IdleTimeout&) {} + virtual void react(const internal::UnmountTimeout&) {} protected: auto IdleCondition() -> bool; @@ -100,13 +101,16 @@ class Running : public SystemState { void react(const KeyLockChanged&) override; void react(const SdDetectChanged&) override; - void react(const audio::PlaybackFinished&) override; + void react(const audio::PlaybackStopped&) override; void react(const database::event::UpdateFinished&) override; void react(const SamdUsbMscChanged&) override; + void react(const internal::UnmountTimeout&) override; using SystemState::react; private: + auto checkIdle() -> void; + auto mountStorage() -> bool; auto unmountStorage() -> void; diff --git a/src/system_fsm/running.cpp b/src/system_fsm/running.cpp index ec146029..d1d02fab 100644 --- a/src/system_fsm/running.cpp +++ b/src/system_fsm/running.cpp @@ -9,6 +9,7 @@ #include "database.hpp" #include "db_events.hpp" #include "file_gatherer.hpp" +#include "freertos/portmacro.h" #include "freertos/projdefs.h" #include "gpios.hpp" #include "result.hpp" @@ -25,12 +26,22 @@ namespace states { [[maybe_unused]] static const char kTag[] = "RUN"; +static const TickType_t kTicksBeforeUnmount = pdMS_TO_TICKS(10000); + +static TimerHandle_t sUnmountTimer = nullptr; + +static void timer_callback(TimerHandle_t timer) { + events::System().Dispatch(internal::UnmountTimeout{}); +} + static database::IFileGatherer* sFileGatherer; void Running::entry() { - if (mountStorage()) { - events::Ui().Dispatch(StorageMounted{}); + if (!sUnmountTimer) { + sUnmountTimer = xTimerCreate("unmount_timeout", kTicksBeforeUnmount, false, + NULL, timer_callback); } + mountStorage(); } void Running::exit() { @@ -38,18 +49,18 @@ void Running::exit() { } void Running::react(const KeyLockChanged& ev) { - if (IdleCondition()) { - transit(); - } + checkIdle(); } -void Running::react(const audio::PlaybackFinished& ev) { - if (IdleCondition()) { - transit(); - } +void Running::react(const audio::PlaybackStopped& ev) { + checkIdle(); } void Running::react(const database::event::UpdateFinished&) { + checkIdle(); +} + +void Running::react(const internal::UnmountTimeout&) { if (IdleCondition()) { transit(); } @@ -61,10 +72,8 @@ void Running::react(const SdDetectChanged& ev) { return; } - if (ev.has_sd_card) { - if (!sStorage && mountStorage()) { - events::Ui().Dispatch(StorageMounted{}); - } + if (ev.has_sd_card && !sStorage) { + mountStorage(); } // Don't automatically unmount, since this event seems to occasionally happen // supriously. FIXME: Why? @@ -102,9 +111,14 @@ void Running::react(const SamdUsbMscChanged& ev) { gpios.WriteSync(drivers::IGpios::Pin::kSdPowerEnable, 0); // Now it's ready for us. - if (mountStorage()) { - events::Ui().Dispatch(StorageMounted{}); - } + mountStorage(); + } +} + +auto Running::checkIdle() -> void { + xTimerStop(sUnmountTimer, portMAX_DELAY); + if (IdleCondition()) { + xTimerStart(sUnmountTimer, portMAX_DELAY); } } @@ -142,6 +156,9 @@ auto Running::mountStorage() -> bool { std::unique_ptr{database_res.value()}); ESP_LOGI(kTag, "storage loaded okay"); + events::Ui().Dispatch(StorageMounted{}); + events::Audio().Dispatch(StorageMounted{}); + events::System().Dispatch(StorageMounted{}); // Tell the database to refresh so that we pick up any changes from the newly // mounted card. diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index 93a19b02..42110bdb 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -58,7 +58,7 @@ class UiState : public tinyfsm::Fsm { void react(const system_fsm::BatteryStateChanged&); void react(const audio::PlaybackStarted&); - void react(const audio::PlaybackFinished&); + void react(const audio::PlaybackStopped&); void react(const audio::PlaybackUpdate&); void react(const audio::QueueUpdate&); diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 228e61b6..abe88460 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -301,7 +301,7 @@ void UiState::react(const audio::PlaybackUpdate& ev) { sPlaybackPosition.Update(static_cast(ev.seconds_elapsed)); } -void UiState::react(const audio::PlaybackFinished&) { +void UiState::react(const audio::PlaybackStopped&) { sPlaybackPlaying.Update(false); }