Preserve the queue when going into standby

custom
jacqueline 1 year ago
parent 811c335c2a
commit 299f3cc48f
  1. 4
      lua/browser.lua
  2. 2
      src/audio/CMakeLists.txt
  3. 71
      src/audio/audio_fsm.cpp
  4. 2
      src/audio/include/audio_events.hpp
  5. 12
      src/audio/include/audio_fsm.hpp
  6. 3
      src/audio/include/track_queue.hpp
  7. 71
      src/audio/track_queue.cpp
  8. 1
      src/system_fsm/include/system_events.hpp
  9. 8
      src/system_fsm/include/system_fsm.hpp
  10. 47
      src/system_fsm/running.cpp
  11. 2
      src/ui/include/ui_fsm.hpp
  12. 2
      src/ui/ui_fsm.cpp

@ -5,6 +5,7 @@ local font = require("font")
local queue = require("queue") local queue = require("queue")
local playing = require("playing") local playing = require("playing")
local theme = require("theme") local theme = require("theme")
local playback = require("playback")
local browser = {} local browser = {}
@ -67,12 +68,14 @@ function browser.create(opts)
local enqueue = widgets.IconBtn(buttons, "//lua/img/enqueue.png", "Enqueue") local enqueue = widgets.IconBtn(buttons, "//lua/img/enqueue.png", "Enqueue")
enqueue:onClicked(function() enqueue:onClicked(function()
queue.add(original_iterator) queue.add(original_iterator)
playback.playing:set(true)
end) end)
-- enqueue:add_flag(lvgl.FLAG.HIDDEN) -- enqueue:add_flag(lvgl.FLAG.HIDDEN)
local play = widgets.IconBtn(buttons, "//lua/img/play_small.png", "Play") local play = widgets.IconBtn(buttons, "//lua/img/play_small.png", "Play")
play:onClicked(function() play:onClicked(function()
queue.clear() queue.clear()
queue.add(original_iterator) queue.add(original_iterator)
playback.playing:set(true)
backstack.push(playing) backstack.push(playing)
end end
) )
@ -109,6 +112,7 @@ function browser.create(opts)
else else
queue.clear() queue.clear()
queue.add(contents) queue.add(contents)
playback.playing:set(true)
backstack.push(playing) backstack.push(playing)
end end
end) end)

@ -9,6 +9,6 @@ idf_component_register(
"audio_source.cpp" "audio_source.cpp"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" 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}) target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})

@ -49,6 +49,7 @@ std::shared_ptr<BluetoothAudioOutput> AudioState::sBtOutput;
std::shared_ptr<IAudioOutput> AudioState::sOutput; std::shared_ptr<IAudioOutput> AudioState::sOutput;
std::optional<database::TrackId> AudioState::sCurrentTrack; std::optional<database::TrackId> AudioState::sCurrentTrack;
bool AudioState::sIsPlaybackAllowed;
void AudioState::react(const system_fsm::KeyLockChanged& ev) { void AudioState::react(const system_fsm::KeyLockChanged& ev) {
if (ev.locking && sServices) { 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 { namespace states {
void Uninitialised::react(const system_fsm::BootComplete& ev) { 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) { void Standby::react(const internal::InputFileOpened& ev) {
if (readyToPlay()) {
transit<Playback>(); transit<Playback>();
} }
}
void Standby::react(const QueueUpdate& ev) { void Standby::react(const QueueUpdate& ev) {
auto current_track = sServices->track_queue().current(); auto current_track = sServices->track_queue().current();
if (!current_track || (sCurrentTrack && *sCurrentTrack == *current_track)) { if (!current_track || (sCurrentTrack && (*sCurrentTrack == *current_track))) {
return; return;
} }
playTrack(*current_track); playTrack(*current_track);
} }
void Standby::react(const TogglePlayPause& ev) { static const char kQueueKey[] = "audio:queue";
if (sCurrentTrack) {
transit<Playback>(); 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() { void Playback::entry() {
@ -226,17 +278,14 @@ void Playback::entry() {
void Playback::exit() { void Playback::exit() {
ESP_LOGI(kTag, "finishing playback"); 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); sOutput->SetMode(IAudioOutput::Modes::kOnPaused);
// Stash the current volume now, in case it changed during playback, since we // Stash the current volume now, in case it changed during playback, since we
// might be powering off soon. // might be powering off soon.
sServices->nvs().AmpCurrentVolume(sOutput->GetVolume()); sServices->nvs().AmpCurrentVolume(sOutput->GetVolume());
events::System().Dispatch(PlaybackFinished{}); events::System().Dispatch(PlaybackStopped{});
events::Ui().Dispatch(PlaybackFinished{}); events::Ui().Dispatch(PlaybackStopped{});
} }
void Playback::react(const system_fsm::HasPhonesChanged& ev) { void Playback::react(const system_fsm::HasPhonesChanged& ev) {
@ -259,10 +308,6 @@ void Playback::react(const QueueUpdate& ev) {
playTrack(*current_track); playTrack(*current_track);
} }
void Playback::react(const TogglePlayPause& ev) {
transit<Standby>();
}
void Playback::react(const PlaybackUpdate& ev) { void Playback::react(const PlaybackUpdate& ev) {
ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed, ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed,
ev.track->duration); ev.track->duration);

@ -35,7 +35,7 @@ struct PlaybackUpdate : tinyfsm::Event {
std::shared_ptr<Track> track; std::shared_ptr<Track> track;
}; };
struct PlaybackFinished : tinyfsm::Event {}; struct PlaybackStopped : tinyfsm::Event {};
struct QueueUpdate : tinyfsm::Event { struct QueueUpdate : tinyfsm::Event {
bool current_changed; bool current_changed;

@ -52,12 +52,13 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
void react(const OutputModeChanged&); void react(const OutputModeChanged&);
virtual void react(const system_fsm::BootComplete&) {} 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 PlayFile&) {}
virtual void react(const QueueUpdate&) {} virtual void react(const QueueUpdate&) {}
virtual void react(const PlaybackUpdate&) {} 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::InputFileOpened&) {}
virtual void react(const internal::InputFileClosed&) {} virtual void react(const internal::InputFileClosed&) {}
@ -77,6 +78,9 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
static std::shared_ptr<IAudioOutput> sOutput; static std::shared_ptr<IAudioOutput> sOutput;
static std::optional<database::TrackId> sCurrentTrack; static std::optional<database::TrackId> sCurrentTrack;
auto readyToPlay() -> bool;
static bool sIsPlaybackAllowed;
}; };
namespace states { namespace states {
@ -92,7 +96,8 @@ class Standby : public AudioState {
void react(const PlayFile&) override; void react(const PlayFile&) override;
void react(const internal::InputFileOpened&) override; void react(const internal::InputFileOpened&) override;
void react(const QueueUpdate&) 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; using AudioState::react;
}; };
@ -107,7 +112,6 @@ class Playback : public AudioState {
void react(const PlayFile&) override; void react(const PlayFile&) override;
void react(const QueueUpdate&) override; void react(const QueueUpdate&) override;
void react(const PlaybackUpdate&) override; void react(const PlaybackUpdate&) override;
void react(const TogglePlayPause&) override;
void react(const internal::InputFileOpened&) override; void react(const internal::InputFileOpened&) override;
void react(const internal::InputFileClosed&) override; void react(const internal::InputFileClosed&) override;

@ -98,6 +98,9 @@ class TrackQueue {
auto repeat(bool) -> void; auto repeat(bool) -> void;
auto repeat() const -> bool; auto repeat() const -> bool;
auto serialise() -> std::string;
auto deserialise(const std::string&) -> void;
// Cannot be copied or moved. // Cannot be copied or moved.
TrackQueue(const TrackQueue&) = delete; TrackQueue(const TrackQueue&) = delete;
TrackQueue& operator=(const TrackQueue&) = delete; TrackQueue& operator=(const TrackQueue&) = delete;

@ -5,6 +5,7 @@
*/ */
#include "track_queue.hpp" #include "track_queue.hpp"
#include <stdint.h>
#include <algorithm> #include <algorithm>
#include <cstdint> #include <cstdint>
@ -310,4 +311,74 @@ auto TrackQueue::repeat() const -> bool {
return repeat_; 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 } // namespace audio

@ -76,6 +76,7 @@ struct GpioInterrupt : tinyfsm::Event {};
struct SamdInterrupt : tinyfsm::Event {}; struct SamdInterrupt : tinyfsm::Event {};
struct IdleTimeout : tinyfsm::Event {}; struct IdleTimeout : tinyfsm::Event {};
struct UnmountTimeout : tinyfsm::Event {};
} // namespace internal } // namespace internal

@ -63,8 +63,9 @@ class SystemState : public tinyfsm::Fsm<SystemState> {
virtual void react(const SdDetectChanged&) {} virtual void react(const SdDetectChanged&) {}
virtual void react(const SamdUsbMscChanged&) {} virtual void react(const SamdUsbMscChanged&) {}
virtual void react(const database::event::UpdateFinished&) {} 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::IdleTimeout&) {}
virtual void react(const internal::UnmountTimeout&) {}
protected: protected:
auto IdleCondition() -> bool; auto IdleCondition() -> bool;
@ -100,13 +101,16 @@ class Running : public SystemState {
void react(const KeyLockChanged&) override; void react(const KeyLockChanged&) override;
void react(const SdDetectChanged&) 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 database::event::UpdateFinished&) override;
void react(const SamdUsbMscChanged&) override; void react(const SamdUsbMscChanged&) override;
void react(const internal::UnmountTimeout&) override;
using SystemState::react; using SystemState::react;
private: private:
auto checkIdle() -> void;
auto mountStorage() -> bool; auto mountStorage() -> bool;
auto unmountStorage() -> void; auto unmountStorage() -> void;

@ -9,6 +9,7 @@
#include "database.hpp" #include "database.hpp"
#include "db_events.hpp" #include "db_events.hpp"
#include "file_gatherer.hpp" #include "file_gatherer.hpp"
#include "freertos/portmacro.h"
#include "freertos/projdefs.h" #include "freertos/projdefs.h"
#include "gpios.hpp" #include "gpios.hpp"
#include "result.hpp" #include "result.hpp"
@ -25,12 +26,22 @@ namespace states {
[[maybe_unused]] static const char kTag[] = "RUN"; [[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; static database::IFileGatherer* sFileGatherer;
void Running::entry() { void Running::entry() {
if (mountStorage()) { if (!sUnmountTimer) {
events::Ui().Dispatch(StorageMounted{}); sUnmountTimer = xTimerCreate("unmount_timeout", kTicksBeforeUnmount, false,
NULL, timer_callback);
} }
mountStorage();
} }
void Running::exit() { void Running::exit() {
@ -38,18 +49,18 @@ void Running::exit() {
} }
void Running::react(const KeyLockChanged& ev) { void Running::react(const KeyLockChanged& ev) {
if (IdleCondition()) { checkIdle();
transit<Idle>();
}
} }
void Running::react(const audio::PlaybackFinished& ev) { void Running::react(const audio::PlaybackStopped& ev) {
if (IdleCondition()) { checkIdle();
transit<Idle>();
}
} }
void Running::react(const database::event::UpdateFinished&) { void Running::react(const database::event::UpdateFinished&) {
checkIdle();
}
void Running::react(const internal::UnmountTimeout&) {
if (IdleCondition()) { if (IdleCondition()) {
transit<Idle>(); transit<Idle>();
} }
@ -61,10 +72,8 @@ void Running::react(const SdDetectChanged& ev) {
return; return;
} }
if (ev.has_sd_card) { if (ev.has_sd_card && !sStorage) {
if (!sStorage && mountStorage()) { mountStorage();
events::Ui().Dispatch(StorageMounted{});
}
} }
// Don't automatically unmount, since this event seems to occasionally happen // Don't automatically unmount, since this event seems to occasionally happen
// supriously. FIXME: Why? // supriously. FIXME: Why?
@ -102,10 +111,15 @@ void Running::react(const SamdUsbMscChanged& ev) {
gpios.WriteSync(drivers::IGpios::Pin::kSdPowerEnable, 0); gpios.WriteSync(drivers::IGpios::Pin::kSdPowerEnable, 0);
// Now it's ready for us. // Now it's ready for us.
if (mountStorage()) { mountStorage();
events::Ui().Dispatch(StorageMounted{});
} }
} }
auto Running::checkIdle() -> void {
xTimerStop(sUnmountTimer, portMAX_DELAY);
if (IdleCondition()) {
xTimerStart(sUnmountTimer, portMAX_DELAY);
}
} }
auto Running::mountStorage() -> bool { auto Running::mountStorage() -> bool {
@ -142,6 +156,9 @@ auto Running::mountStorage() -> bool {
std::unique_ptr<database::Database>{database_res.value()}); std::unique_ptr<database::Database>{database_res.value()});
ESP_LOGI(kTag, "storage loaded okay"); 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 // Tell the database to refresh so that we pick up any changes from the newly
// mounted card. // mounted card.

@ -58,7 +58,7 @@ class UiState : public tinyfsm::Fsm<UiState> {
void react(const system_fsm::BatteryStateChanged&); void react(const system_fsm::BatteryStateChanged&);
void react(const audio::PlaybackStarted&); void react(const audio::PlaybackStarted&);
void react(const audio::PlaybackFinished&); void react(const audio::PlaybackStopped&);
void react(const audio::PlaybackUpdate&); void react(const audio::PlaybackUpdate&);
void react(const audio::QueueUpdate&); void react(const audio::QueueUpdate&);

@ -301,7 +301,7 @@ void UiState::react(const audio::PlaybackUpdate& ev) {
sPlaybackPosition.Update(static_cast<int>(ev.seconds_elapsed)); sPlaybackPosition.Update(static_cast<int>(ev.seconds_elapsed));
} }
void UiState::react(const audio::PlaybackFinished&) { void UiState::react(const audio::PlaybackStopped&) {
sPlaybackPlaying.Update(false); sPlaybackPlaying.Update(false);
} }

Loading…
Cancel
Save