summaryrefslogtreecommitdiff
path: root/src/audio
diff options
context:
space:
mode:
authorjacqueline <me@jacqueline.id.au>2024-02-05 11:02:45 +1100
committerjacqueline <me@jacqueline.id.au>2024-02-05 11:03:52 +1100
commit299f3cc48f683d3e6dec1efb4957fdb49b4de2c3 (patch)
tree6eb0445c300492e53cdc048f12cc6780a12c55eb /src/audio
parent811c335c2ac425320a1949ab23378172e86ae60a (diff)
downloadtangara-fw-299f3cc48f683d3e6dec1efb4957fdb49b4de2c3.tar.gz
Preserve the queue when going into standby
Diffstat (limited to 'src/audio')
-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
6 files changed, 143 insertions, 20 deletions
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