summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lua/browser.lua4
-rw-r--r--src/audio/CMakeLists.txt2
-rw-r--r--src/audio/audio_fsm.cpp73
-rw-r--r--src/audio/include/audio_events.hpp2
-rw-r--r--src/audio/include/audio_fsm.hpp12
-rw-r--r--src/audio/include/track_queue.hpp3
-rw-r--r--src/audio/track_queue.cpp71
-rw-r--r--src/system_fsm/include/system_events.hpp1
-rw-r--r--src/system_fsm/include/system_fsm.hpp8
-rw-r--r--src/system_fsm/running.cpp49
-rw-r--r--src/ui/include/ui_fsm.hpp2
-rw-r--r--src/ui/ui_fsm.cpp2
12 files changed, 189 insertions, 40 deletions
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<BluetoothAudioOutput> AudioState::sBtOutput;
std::shared_ptr<IAudioOutput> AudioState::sOutput;
std::optional<database::TrackId> 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<states::Playback>()) {
+ transit<states::Playback>();
+ }
+ } else {
+ if (!is_in_state<states::Standby>()) {
+ transit<states::Standby>();
+ }
+ }
+}
+
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<Playback>();
+ if (readyToPlay()) {
+ transit<Playback>();
+ }
}
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<Playback>();
+static const char kQueueKey[] = "audio:queue";
+
+void Standby::react(const system_fsm::KeyLockChanged& ev) {
+ if (!ev.locking) {
+ return;
}
+ sServices->bg_worker().Dispatch<void>([]() {
+ 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<void>([]() {
+ 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<Standby>();
-}
-
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> 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<AudioState> {
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<AudioState> {
static std::shared_ptr<IAudioOutput> sOutput;
static std::optional<database::TrackId> 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 <stdint.h>
#include <algorithm>
#include <cstdint>
@@ -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<database::TrackId>& tracks)
+ : pos_(pos),
+ tracks_(tracks),
+ in_root_array_(false),
+ in_track_list_(false) {}
+
+ ParseClient* item(std::unique_ptr<cppbor::Item>& 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<size_t>(val);
+ }
+ }
+ return this;
+ }
+
+ ParseClient* itemEnd(std::unique_ptr<cppbor::Item>& 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<database::TrackId>& 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<const uint8_t*>(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<SystemState> {
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<Idle>();
- }
+ checkIdle();
}
-void Running::react(const audio::PlaybackFinished& ev) {
- if (IdleCondition()) {
- transit<Idle>();
- }
+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<Idle>();
}
@@ -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::Database>{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<UiState> {
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<int>(ev.seconds_elapsed));
}
-void UiState::react(const audio::PlaybackFinished&) {
+void UiState::react(const audio::PlaybackStopped&) {
sPlaybackPlaying.Update(false);
}