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 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)

@ -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})

@ -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) {
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);

@ -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;

@ -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;

@ -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;

@ -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

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

@ -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;

@ -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,10 +111,15 @@ 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);
}
}
auto Running::mountStorage() -> bool {
@ -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.

@ -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&);

@ -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);
}

Loading…
Cancel
Save