From 2f16d230025c3173cfbecc58b38d6a52b6b0f5f2 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 5 Jul 2023 20:09:03 +1000 Subject: [PATCH 1/2] Start on wiring up playback screen to real data --- src/database/include/track.hpp | 1 + src/ui/include/screen_playing.hpp | 7 +++++++ src/ui/include/ui_fsm.hpp | 13 ++++++++++++- src/ui/screen_playing.cpp | 28 +++++++++++++++++++++++----- src/ui/ui_fsm.cpp | 17 +++++++++++------ 5 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/database/include/track.hpp b/src/database/include/track.hpp index e3f94db4..87fae9b9 100644 --- a/src/database/include/track.hpp +++ b/src/database/include/track.hpp @@ -50,6 +50,7 @@ enum class Tag { kAlbum = 2, kAlbumTrack = 3, kGenre = 4, + kDuration = 5, }; /* diff --git a/src/ui/include/screen_playing.hpp b/src/ui/include/screen_playing.hpp index 3eae32a7..5ccfe391 100644 --- a/src/ui/include/screen_playing.hpp +++ b/src/ui/include/screen_playing.hpp @@ -6,12 +6,15 @@ #pragma once +#include #include +#include #include "lvgl.h" #include "database.hpp" #include "screen.hpp" +#include "track.hpp" namespace ui { namespace screens { @@ -23,6 +26,9 @@ class Playing : public Screen { auto BindTrack(database::Track t) -> void; + auto UpdateTime(uint32_t) -> void; + auto UpdateNextUp(std::vector tracks) -> void; + private: database::Track track_; @@ -34,6 +40,7 @@ class Playing : public Screen { lv_obj_t* play_pause_control_; lv_obj_t* next_up_container_; + std::vector next_tracks_; }; } // namespace screens diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index 32275fab..cd1ec492 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -9,7 +9,9 @@ #include #include +#include "audio_events.hpp" #include "relative_wheel.hpp" +#include "screen_playing.hpp" #include "tinyfsm.hpp" #include "display.hpp" @@ -37,6 +39,8 @@ class UiState : public tinyfsm::Fsm { /* Fallback event handler. Does nothing. */ void react(const tinyfsm::Event& ev) {} + virtual void react(const audio::PlaybackUpdate){}; + virtual void react(const system_fsm::KeyLockChanged&){}; virtual void react(const internal::RecordSelected&){}; @@ -57,6 +61,7 @@ class UiState : public tinyfsm::Fsm { static std::stack> sScreens; static std::shared_ptr sCurrentScreen; + static std::unique_ptr sPlayingScreen; }; namespace states { @@ -68,7 +73,7 @@ class Splash : public UiState { using UiState::react; }; -class Interactive : public UiState { +class Browse : public UiState { void entry() override; void react(const internal::RecordSelected&) override; @@ -78,6 +83,12 @@ class Interactive : public UiState { void react(const system_fsm::StorageMounted&) override; }; +class Playing : public UiState { + void entry() override; + + void react(const audio::PlaybackUpdate) override; +}; + class FatalError : public UiState {}; } // namespace states diff --git a/src/ui/screen_playing.cpp b/src/ui/screen_playing.cpp index 1ac8ad5a..39f6b04d 100644 --- a/src/ui/screen_playing.cpp +++ b/src/ui/screen_playing.cpp @@ -7,6 +7,7 @@ #include "screen_playing.hpp" #include "core/lv_obj.h" +#include "core/lv_obj_tree.h" #include "esp_log.h" #include "extra/layouts/flex/lv_flex.h" #include "extra/layouts/grid/lv_grid.h" @@ -130,11 +131,6 @@ Playing::Playing(database::Track track) : track_(track) { lv_obj_set_flex_align(next_up_container_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_END); - lv_group_add_obj(group_, next_up_label(root_, "Song 2")); - lv_group_add_obj(group_, next_up_label(root_, "Song 3")); - lv_group_add_obj( - group_, next_up_label(root_, "Another song that has a very long name")); - BindTrack(track); } @@ -148,6 +144,28 @@ auto Playing::BindTrack(database::Track t) -> void { lv_label_set_text(album_label_, t.tags().at(database::Tag::kAlbum).value_or("").c_str()); lv_label_set_text(title_label_, t.TitleOrFilename().c_str()); + + // TODO. + lv_bar_set_range(scrubber_, 0, 0); + lv_bar_set_value(scrubber_, 0, LV_ANIM_OFF); +} + +auto Playing::UpdateTime(uint32_t time) -> void { + lv_bar_set_value(scrubber_, time, LV_ANIM_OFF); +} + +auto Playing::UpdateNextUp(std::vector tracks) -> void { + // TODO(jacqueline): Do a proper diff to maintain selection. + int children = lv_obj_get_child_cnt(next_up_container_); + while (children > 0) { + lv_obj_del(lv_obj_get_child(next_up_container_, 0)); + children--; + } + + next_tracks_ = tracks; + for (const auto &track : next_tracks_) { + lv_group_add_obj(group_, next_up_label(next_up_container_, track.TitleOrFilename())); + } } } // namespace screens diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 58b1f641..13658c37 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -34,6 +34,7 @@ std::weak_ptr UiState::sDb; std::stack> UiState::sScreens; std::shared_ptr UiState::sCurrentScreen; +std::unique_ptr UiState::sPlayingScreen; auto UiState::Init(drivers::IGpios* gpio_expander) -> bool { sIGpios = gpio_expander; @@ -78,16 +79,16 @@ void Splash::exit() { } void Splash::react(const system_fsm::BootComplete& ev) { - transit(); + transit(); } -void Interactive::entry() {} +void Browse::entry() {} -void Interactive::react(const system_fsm::KeyLockChanged& ev) { +void Browse::react(const system_fsm::KeyLockChanged& ev) { sDisplay->SetDisplayOn(ev.falling); } -void Interactive::react(const system_fsm::StorageMounted& ev) { +void Browse::react(const system_fsm::StorageMounted& ev) { sDb = ev.db; auto db = ev.db.lock(); if (!db) { @@ -97,7 +98,7 @@ void Interactive::react(const system_fsm::StorageMounted& ev) { PushScreen(std::make_shared(db->GetIndexes())); } -void Interactive::react(const internal::RecordSelected& ev) { +void Browse::react(const internal::RecordSelected& ev) { auto db = sDb.lock(); if (!db) { return; @@ -125,7 +126,7 @@ void Interactive::react(const internal::RecordSelected& ev) { } } -void Interactive::react(const internal::IndexSelected& ev) { +void Browse::react(const internal::IndexSelected& ev) { auto db = sDb.lock(); if (!db) { return; @@ -137,6 +138,10 @@ void Interactive::react(const internal::IndexSelected& ev) { std::move(query))); } +void Playing::react(const audio::PlaybackUpdate ev) { + sPlayingScreen->UpdateTime(ev.seconds_elapsed); +} + } // namespace states } // namespace ui From 39f7545cd5ef7a30bbd482f3579df7744c6b688d Mon Sep 17 00:00:00 2001 From: jacqueline Date: Fri, 7 Jul 2023 15:29:47 +1000 Subject: [PATCH 2/2] wire up the playing screen with some real data Includes implementing song duration calculation for CBR MP3 files --- src/app_console/app_console.cpp | 7 +- src/app_console/include/app_console.hpp | 2 + src/audio/CMakeLists.txt | 2 +- src/audio/audio_decoder.cpp | 14 ++- src/audio/audio_fsm.cpp | 82 ++++++++------- src/audio/audio_task.cpp | 14 ++- src/audio/fatfs_audio_input.cpp | 20 +++- src/audio/include/audio_decoder.hpp | 3 +- src/audio/include/audio_events.hpp | 21 ++-- src/audio/include/audio_fsm.hpp | 33 +++--- src/audio/include/fatfs_audio_input.hpp | 2 + src/audio/include/stream_info.hpp | 9 +- src/audio/include/track_queue.hpp | 85 ++++++++++++++++ src/audio/stream_info.cpp | 4 + src/audio/track_queue.cpp | 128 ++++++++++++++++++++++++ src/codecs/foxenflac.cpp | 1 + src/codecs/include/codec.hpp | 4 + src/codecs/mad.cpp | 20 ++-- src/database/database.cpp | 56 +++++++++++ src/database/include/database.hpp | 9 ++ src/database/include/future_fetcher.hpp | 62 ++++++++++++ src/database/include/track.hpp | 2 + src/database/tag_parser.cpp | 3 + src/system_fsm/booting.cpp | 7 +- src/system_fsm/include/system_fsm.hpp | 3 + src/system_fsm/system_fsm.cpp | 3 + src/ui/include/screen.hpp | 10 ++ src/ui/include/screen_playing.hpp | 35 +++++-- src/ui/include/ui_fsm.hpp | 24 +++-- src/ui/screen_playing.cpp | 95 +++++++++++++++--- src/ui/ui_fsm.cpp | 49 +++++++-- 31 files changed, 690 insertions(+), 119 deletions(-) create mode 100644 src/audio/include/track_queue.hpp create mode 100644 src/audio/track_queue.cpp create mode 100644 src/database/include/future_fetcher.hpp diff --git a/src/app_console/app_console.cpp b/src/app_console/app_console.cpp index b0a90155..2b5b84f7 100644 --- a/src/app_console/app_console.cpp +++ b/src/app_console/app_console.cpp @@ -30,6 +30,7 @@ namespace console { std::weak_ptr AppConsole::sDatabase; +audio::TrackQueue* AppConsole::sTrackQueue; int CmdListDir(int argc, char** argv) { auto lock = AppConsole::sDatabase.lock(); @@ -108,9 +109,10 @@ int CmdPlayFile(int argc, char** argv) { if (is_id) { database::TrackId id = std::atoi(argv[1]); - events::Dispatch( - audio::PlayTrack{.id = id}); + AppConsole::sTrackQueue->AddLast(id); } else { + // TODO. + /* std::ostringstream path; path << '/' << argv[1]; for (int i = 2; i < argc; i++) { @@ -119,6 +121,7 @@ int CmdPlayFile(int argc, char** argv) { events::Dispatch( audio::PlayFile{.filename = path.str()}); + */ } return 0; diff --git a/src/app_console/include/app_console.hpp b/src/app_console/include/app_console.hpp index 48ce0d38..3cb62b21 100644 --- a/src/app_console/include/app_console.hpp +++ b/src/app_console/include/app_console.hpp @@ -10,12 +10,14 @@ #include "console.hpp" #include "database.hpp" +#include "track_queue.hpp" namespace console { class AppConsole : public Console { public: static std::weak_ptr sDatabase; + static audio::TrackQueue* sTrackQueue; protected: virtual auto RegisterExtraComponents() -> void; diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index 2e085306..38e367aa 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -4,7 +4,7 @@ idf_component_register( SRCS "audio_decoder.cpp" "audio_task.cpp" "chunk.cpp" "fatfs_audio_input.cpp" - "stream_message.cpp" "i2s_audio_output.cpp" "stream_buffer.cpp" + "stream_message.cpp" "i2s_audio_output.cpp" "stream_buffer.cpp" "track_queue.cpp" "stream_event.cpp" "pipeline.cpp" "stream_info.cpp" "audio_fsm.cpp" INCLUDE_DIRS "include" REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" "database" "system_fsm") diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp index 966a8c37..b8054574 100644 --- a/src/audio/audio_decoder.cpp +++ b/src/audio/audio_decoder.cpp @@ -53,7 +53,7 @@ auto AudioDecoder::ProcessStreamInfo(const StreamInfo& info) -> bool { } const auto& new_format = std::get(info.format); - current_input_format_ = info.format; + current_input_format_ = new_format; ESP_LOGI(kTag, "creating new decoder"); auto result = codecs::CreateCodecForType(new_format.type); @@ -112,6 +112,15 @@ auto AudioDecoder::Process(const std::vector& inputs, .sample_rate = format.sample_rate_hz, }; + if (format.duration_seconds) { + duration_seconds_from_decoder_ = format.duration_seconds; + } else if (format.bits_per_second && + current_input_format_->duration_bytes) { + duration_seconds_from_decoder_ = + (current_input_format_->duration_bytes.value() - res.first) * 8 / + format.bits_per_second.value() / format.num_channels; + } + if (info.seek_to_seconds) { seek_to_sample_ = *info.seek_to_seconds * format.sample_rate_hz; } else { @@ -144,6 +153,9 @@ auto AudioDecoder::Process(const std::vector& inputs, if (!has_prepared_output_ && !output->prepare(*current_output_format_)) { return; } + if (duration_seconds_from_decoder_) { + output->set_duration(*duration_seconds_from_decoder_); + } has_prepared_output_ = true; // Parse frames and produce samples. diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 805dffc4..ef33d583 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -19,6 +19,7 @@ #include "pipeline.hpp" #include "system_events.hpp" #include "track.hpp" +#include "track_queue.hpp" namespace audio { @@ -32,11 +33,13 @@ std::unique_ptr AudioState::sFileSource; std::unique_ptr AudioState::sI2SOutput; std::vector> AudioState::sPipeline; -std::deque AudioState::sTrackQueue; +TrackQueue* AudioState::sTrackQueue; auto AudioState::Init(drivers::IGpios* gpio_expander, - std::weak_ptr database) -> bool { + std::weak_ptr database, + TrackQueue* queue) -> bool { sIGpios = gpio_expander; + sTrackQueue = queue; auto dac = drivers::I2SDac::create(gpio_expander); if (!dac) { @@ -94,26 +97,23 @@ void Uninitialised::react(const system_fsm::BootComplete&) { transit(); } -void Standby::react(const InputFileOpened& ev) { +void Standby::react(const internal::InputFileOpened& ev) { transit(); } -void Standby::react(const PlayTrack& ev) { +void Standby::react(const QueueUpdate& ev) { + auto current_track = sTrackQueue->GetCurrent(); + if (!current_track) { + return; + } + auto db = sDatabase.lock(); if (!db) { ESP_LOGW(kTag, "database not open; ignoring play request"); return; } - if (ev.data) { - sFileSource->OpenFile(ev.data->filepath()); - } else { - sFileSource->OpenFile(db->GetTrackPath(ev.id)); - } -} - -void Standby::react(const PlayFile& ev) { - sFileSource->OpenFile(ev.filename); + sFileSource->OpenFile(db->GetTrackPath(*current_track)); } void Playback::entry() { @@ -126,42 +126,50 @@ void Playback::exit() { sI2SOutput->SetInUse(false); } -void Playback::react(const PlayTrack& ev) { - sTrackQueue.push_back(EnqueuedItem(ev.id)); -} +void Playback::react(const QueueUpdate& ev) { + auto current_track = sTrackQueue->GetCurrent(); + if (!current_track) { + // TODO: return to standby? + return; + } -void Playback::react(const PlayFile& ev) { - sTrackQueue.push_back(EnqueuedItem(ev.filename)); + auto db = sDatabase.lock(); + if (!db) { + return; + } + + // TODO: what if we just finished this, and are preemptively loading the next + // one? + sFileSource->OpenFile(db->GetTrackPath(*current_track)); } void Playback::react(const PlaybackUpdate& ev) { - ESP_LOGI(kTag, "elapsed: %lu", ev.seconds_elapsed); + // ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed, + // ev.seconds_total); } -void Playback::react(const InputFileOpened& ev) {} +void Playback::react(const internal::InputFileOpened& ev) {} -void Playback::react(const InputFileFinished& ev) { - ESP_LOGI(kTag, "finished file"); - if (sTrackQueue.empty()) { +void Playback::react(const internal::InputFileClosed& ev) { + ESP_LOGI(kTag, "finished reading file"); + auto upcoming = sTrackQueue->GetUpcoming(1); + if (upcoming.empty()) { return; } - EnqueuedItem next_item = sTrackQueue.front(); - sTrackQueue.pop_front(); - - if (std::holds_alternative(next_item)) { - sFileSource->OpenFile(std::get(next_item)); - } else if (std::holds_alternative(next_item)) { - auto db = sDatabase.lock(); - if (!db) { - ESP_LOGW(kTag, "database not open; ignoring play request"); - return; - } - sFileSource->OpenFile( - db->GetTrackPath(std::get(next_item))); + auto db = sDatabase.lock(); + if (!db) { + return; } + ESP_LOGI(kTag, "preemptively opening next file"); + sFileSource->OpenFile(db->GetTrackPath(upcoming.front())); +} + +void Playback::react(const internal::InputFileFinished& ev) { + ESP_LOGI(kTag, "finished playing file"); + sTrackQueue->Next(); } -void Playback::react(const AudioPipelineIdle& ev) { +void Playback::react(const internal::AudioPipelineIdle& ev) { transit(); } diff --git a/src/audio/audio_task.cpp b/src/audio/audio_task.cpp index 24bc7be7..babe6849 100644 --- a/src/audio/audio_task.cpp +++ b/src/audio/audio_task.cpp @@ -37,6 +37,7 @@ #include "stream_message.hpp" #include "sys/_stdint.h" #include "tasks.hpp" +#include "ui_fsm.hpp" namespace audio { @@ -87,7 +88,7 @@ void AudioTaskMain(std::unique_ptr pipeline, IAudioSink* sink) { } if (previously_had_work && !has_work) { - events::Dispatch({}); + events::Dispatch({}); } previously_had_work = has_work; @@ -136,6 +137,10 @@ void AudioTaskMain(std::unique_ptr pipeline, IAudioSink* sink) { if (sink_stream.is_producer_finished()) { sink_stream.mark_consumer_finished(); + if (current_second > 0 || current_sample_in_second > 0) { + events::Dispatch({}); + } + current_second = 0; previous_second = 0; current_sample_in_second = 0; @@ -185,8 +190,11 @@ void AudioTaskMain(std::unique_ptr pipeline, IAudioSink* sink) { current_sample_in_second -= pcm.sample_rate; } if (previous_second != current_second) { - events::Dispatch( - {.seconds_elapsed = current_second}); + events::Dispatch({ + .seconds_elapsed = current_second, + .seconds_total = + sink_stream.info().duration_seconds.value_or(current_second), + }); } previous_second = current_second; } diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp index 894ac842..da605a40 100644 --- a/src/audio/fatfs_audio_input.cpp +++ b/src/audio/fatfs_audio_input.cpp @@ -56,6 +56,7 @@ auto FatfsAudioInput::OpenFile(std::future>&& path) } auto FatfsAudioInput::OpenFile(const std::string& path) -> bool { + current_path_.reset(); if (is_file_open_) { f_close(¤t_file_); is_file_open_ = false; @@ -68,6 +69,11 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool { ESP_LOGI(kTag, "opening file %s", path.c_str()); + FILINFO info; + if (f_stat(path.c_str(), &info) != FR_OK) { + ESP_LOGE(kTag, "failed to stat file"); + } + database::TagParserImpl tag_parser; database::TrackTags tags; if (!tag_parser.ReadAndParseTags(path, &tags)) { @@ -95,7 +101,10 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool { .sample_rate = static_cast(*tags.sample_rate), }; } else { - current_format_ = StreamInfo::Encoded{*stream_type}; + current_format_ = StreamInfo::Encoded{ + .type = *stream_type, + .duration_bytes = info.fsize, + }; } FRESULT res = f_open(¤t_file_, path.c_str(), FA_READ); @@ -104,7 +113,8 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool { return false; } - events::Dispatch({}); + events::Dispatch({}); + current_path_ = path; is_file_open_ = true; return true; } @@ -124,9 +134,10 @@ auto FatfsAudioInput::Process(const std::vector& inputs, if (pending_path_->wait_for(std::chrono::seconds(0)) == std::future_status::ready) { auto result = pending_path_->get(); - if (result) { + if (result && result != current_path_) { OpenFile(*result); } + pending_path_ = {}; } } } @@ -173,10 +184,11 @@ auto FatfsAudioInput::Process(const std::vector& inputs, f_close(¤t_file_); is_file_open_ = false; + current_path_.reset(); has_prepared_output_ = false; output->mark_producer_finished(); - events::Dispatch({}); + events::Dispatch({}); } } diff --git a/src/audio/include/audio_decoder.hpp b/src/audio/include/audio_decoder.hpp index aa051685..a6b4754a 100644 --- a/src/audio/include/audio_decoder.hpp +++ b/src/audio/include/audio_decoder.hpp @@ -40,8 +40,9 @@ class AudioDecoder : public IAudioElement { private: std::unique_ptr current_codec_; - std::optional current_input_format_; + std::optional current_input_format_; std::optional current_output_format_; + std::optional duration_seconds_from_decoder_; std::optional seek_to_sample_; bool has_prepared_output_; bool has_samples_to_send_; diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp index 019b65a2..8af3703a 100644 --- a/src/audio/include/audio_events.hpp +++ b/src/audio/include/audio_events.hpp @@ -13,26 +13,31 @@ #include "tinyfsm.hpp" #include "track.hpp" +#include "track_queue.hpp" namespace audio { -struct PlayFile : tinyfsm::Event { - std::string filename; -}; - -struct PlayTrack : tinyfsm::Event { - database::TrackId id; - std::optional data; +struct PlaybackStarted : tinyfsm::Event { + database::Track track; }; struct PlaybackUpdate : tinyfsm::Event { uint32_t seconds_elapsed; + uint32_t seconds_total; }; +struct QueueUpdate : tinyfsm::Event {}; + +struct VolumeChanged : tinyfsm::Event {}; + +namespace internal { + struct InputFileOpened : tinyfsm::Event {}; +struct InputFileClosed : tinyfsm::Event {}; struct InputFileFinished : tinyfsm::Event {}; + struct AudioPipelineIdle : tinyfsm::Event {}; -struct VolumeChanged : tinyfsm::Event {}; +} // namespace internal } // namespace audio diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp index dadbd072..7910f4e2 100644 --- a/src/audio/include/audio_fsm.hpp +++ b/src/audio/include/audio_fsm.hpp @@ -22,13 +22,15 @@ #include "track.hpp" #include "system_events.hpp" +#include "track_queue.hpp" namespace audio { class AudioState : public tinyfsm::Fsm { public: static auto Init(drivers::IGpios* gpio_expander, - std::weak_ptr) -> bool; + std::weak_ptr, + TrackQueue* queue) -> bool; virtual ~AudioState() {} @@ -45,14 +47,14 @@ class AudioState : public tinyfsm::Fsm { void react(const system_fsm::HasPhonesChanged&); virtual void react(const system_fsm::BootComplete&) {} - virtual void react(const PlayTrack&) {} - virtual void react(const PlayFile&) {} + virtual void react(const QueueUpdate&) {} virtual void react(const PlaybackUpdate&) {} - virtual void react(const InputFileOpened&) {} - virtual void react(const InputFileFinished&) {} - virtual void react(const AudioPipelineIdle&) {} + virtual void react(const internal::InputFileOpened&) {} + virtual void react(const internal::InputFileClosed&) {} + virtual void react(const internal::InputFileFinished&) {} + virtual void react(const internal::AudioPipelineIdle&) {} protected: static drivers::IGpios* sIGpios; @@ -63,8 +65,7 @@ class AudioState : public tinyfsm::Fsm { static std::unique_ptr sI2SOutput; static std::vector> sPipeline; - typedef std::variant EnqueuedItem; - static std::deque sTrackQueue; + static TrackQueue* sTrackQueue; }; namespace states { @@ -77,9 +78,8 @@ class Uninitialised : public AudioState { class Standby : public AudioState { public: - void react(const InputFileOpened&) override; - void react(const PlayTrack&) override; - void react(const PlayFile&) override; + void react(const internal::InputFileOpened&) override; + void react(const QueueUpdate&) override; using AudioState::react; }; @@ -89,14 +89,13 @@ class Playback : public AudioState { void entry() override; void exit() override; - void react(const PlayTrack&) override; - void react(const PlayFile&) override; - + void react(const QueueUpdate&) override; void react(const PlaybackUpdate&) override; - void react(const InputFileOpened&) override; - void react(const InputFileFinished&) override; - void react(const AudioPipelineIdle&) override; + void react(const internal::InputFileOpened&) override; + void react(const internal::InputFileClosed&) override; + void react(const internal::InputFileFinished&) override; + void react(const internal::AudioPipelineIdle&) override; using AudioState::react; }; diff --git a/src/audio/include/fatfs_audio_input.hpp b/src/audio/include/fatfs_audio_input.hpp index 77d3b96d..56f92fcf 100644 --- a/src/audio/include/fatfs_audio_input.hpp +++ b/src/audio/include/fatfs_audio_input.hpp @@ -34,6 +34,7 @@ class FatfsAudioInput : public IAudioElement { FatfsAudioInput(); ~FatfsAudioInput(); + auto CurrentFile() -> std::optional { return current_path_; } auto OpenFile(std::future>&& path) -> void; auto OpenFile(const std::string& path) -> bool; @@ -50,6 +51,7 @@ class FatfsAudioInput : public IAudioElement { -> std::optional; std::optional>> pending_path_; + std::optional current_path_; FIL current_file_; bool is_file_open_; bool has_prepared_output_; diff --git a/src/audio/include/stream_info.hpp b/src/audio/include/stream_info.hpp index 4db3e5fd..69bf3c4b 100644 --- a/src/audio/include/stream_info.hpp +++ b/src/audio/include/stream_info.hpp @@ -30,13 +30,16 @@ struct StreamInfo { bool is_consumer_finished = true; - // - std::optional seek_to_seconds{}; + std::optional duration_seconds; + + std::optional seek_to_seconds{}; struct Encoded { // The codec that this stream is associated with. codecs::StreamType type; + std::optional duration_bytes; + bool operator==(const Encoded&) const = default; }; @@ -95,6 +98,8 @@ class OutputStream { bool prepare(const StreamInfo::Format& new_format); + void set_duration(std::size_t); + const StreamInfo& info() const; cpp::span data() const; diff --git a/src/audio/include/track_queue.hpp b/src/audio/include/track_queue.hpp new file mode 100644 index 00000000..840d71ee --- /dev/null +++ b/src/audio/include/track_queue.hpp @@ -0,0 +1,85 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include +#include + +#include "track.hpp" + +namespace audio { + +/* + * Owns and manages a complete view of the playback queue. Includes the + * currently playing track, a truncated list of previously played tracks, and + * all future tracks that have been queued. + * + * In order to not use all of our memory, this class deals strictly with track + * ids. Consumers that need more data than this should fetch it from the + * database. + * + * Instances of this class are broadly safe to use from multiple tasks; each + * method represents an atomic operation. No guarantees are made about + * consistency between calls however. For example, there may be data changes + * between consecutive calls to AddNext() and GetUpcoming(); + */ +class TrackQueue { + public: + TrackQueue(); + + /* Returns the currently playing track. */ + auto GetCurrent() const -> std::optional; + /* Returns, in order, tracks that have been queued to be played next. */ + auto GetUpcoming(std::size_t limit) const -> std::vector; + + /* + * Enqueues a track, placing it immediately after the current track and + * before anything already queued. + * + * If there is no current track, the given track will begin playback. + */ + auto AddNext(database::TrackId) -> void; + auto AddNext(const std::vector&) -> void; + + /* + * Enqueues a track, placing it the end of all enqueued tracks. + * + * If there is no current track, the given track will begin playback. + */ + auto AddLast(database::TrackId) -> void; + auto AddLast(const std::vector&) -> void; + + /* + * Advances to the next track in the queue, placing the current track at the + * front of the 'played' queue. + */ + auto Next() -> void; + auto Previous() -> void; + + /* + * Removes all tracks from all queues, and stops any currently playing track. + */ + auto Clear() -> void; + /* + * Removes a specific track from the queue of upcoming tracks. Has no effect + * on the currently playing track. + */ + auto RemoveUpcoming(database::TrackId) -> void; + + TrackQueue(const TrackQueue&) = delete; + TrackQueue& operator=(const TrackQueue&) = delete; + + private: + mutable std::mutex mutex_; + + std::deque played_; + std::deque upcoming_; + std::optional current_; +}; + +} // namespace audio diff --git a/src/audio/stream_info.cpp b/src/audio/stream_info.cpp index 748cb9ef..3927e5f8 100644 --- a/src/audio/stream_info.cpp +++ b/src/audio/stream_info.cpp @@ -64,6 +64,10 @@ bool OutputStream::prepare(const StreamInfo::Format& new_format) { return false; } +void OutputStream::set_duration(std::size_t seconds) { + raw_->info->duration_seconds = seconds; +} + const StreamInfo& OutputStream::info() const { return *raw_->info; } diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp new file mode 100644 index 00000000..1c233f8f --- /dev/null +++ b/src/audio/track_queue.cpp @@ -0,0 +1,128 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "track_queue.hpp" + +#include +#include + +#include "audio_events.hpp" +#include "audio_fsm.hpp" +#include "event_queue.hpp" +#include "track.hpp" +#include "ui_fsm.hpp" + +namespace audio { + +TrackQueue::TrackQueue() {} + +auto TrackQueue::GetCurrent() const -> std::optional { + const std::lock_guard lock(mutex_); + return current_; +} + +auto TrackQueue::GetUpcoming(std::size_t limit) const + -> std::vector { + const std::lock_guard lock(mutex_); + std::vector ret; + limit = std::min(limit, upcoming_.size()); + std::for_each_n(upcoming_.begin(), limit, + [&](const auto i) { ret.push_back(i); }); + return ret; +} + +auto TrackQueue::AddNext(database::TrackId t) -> void { + const std::lock_guard lock(mutex_); + if (!current_) { + current_ = t; + } else { + upcoming_.push_front(t); + } + + events::Dispatch({}); +} + +auto TrackQueue::AddNext(const std::vector& t) -> void { + const std::lock_guard lock(mutex_); + std::for_each(t.rbegin(), t.rend(), + [&](const auto i) { upcoming_.push_front(i); }); + if (!current_) { + current_ = upcoming_.front(); + upcoming_.pop_front(); + } + events::Dispatch({}); +} + +auto TrackQueue::AddLast(database::TrackId t) -> void { + const std::lock_guard lock(mutex_); + if (!current_) { + current_ = t; + } else { + upcoming_.push_back(t); + } + + events::Dispatch({}); +} + +auto TrackQueue::AddLast(const std::vector& t) -> void { + const std::lock_guard lock(mutex_); + std::for_each(t.begin(), t.end(), + [&](const auto i) { upcoming_.push_back(i); }); + if (!current_) { + current_ = upcoming_.front(); + upcoming_.pop_front(); + } + events::Dispatch({}); +} + +auto TrackQueue::Next() -> void { + const std::lock_guard lock(mutex_); + if (current_) { + played_.push_front(*current_); + } + if (!upcoming_.empty()) { + current_ = upcoming_.front(); + upcoming_.pop_front(); + } else { + current_.reset(); + } + events::Dispatch({}); +} + +auto TrackQueue::Previous() -> void { + const std::lock_guard lock(mutex_); + if (current_) { + upcoming_.push_front(*current_); + } + if (!played_.empty()) { + current_ = played_.front(); + played_.pop_front(); + } else { + current_.reset(); + } + events::Dispatch({}); +} + +auto TrackQueue::Clear() -> void { + const std::lock_guard lock(mutex_); + played_.clear(); + upcoming_.clear(); + current_.reset(); + events::Dispatch({}); +} + +auto TrackQueue::RemoveUpcoming(database::TrackId t) -> void { + const std::lock_guard lock(mutex_); + for (auto it = upcoming_.begin(); it != upcoming_.end(); it++) { + if (*it == t) { + upcoming_.erase(it); + return; + } + } + events::Dispatch({}); +} + +} // namespace audio diff --git a/src/codecs/foxenflac.cpp b/src/codecs/foxenflac.cpp index a2d6f000..ee21da65 100644 --- a/src/codecs/foxenflac.cpp +++ b/src/codecs/foxenflac.cpp @@ -43,6 +43,7 @@ auto FoxenFlacDecoder::BeginStream(const cpp::span input) .num_channels = static_cast(channels), .bits_per_sample = 32, // libfoxenflac output is fixed-size. .sample_rate_hz = static_cast(fs), + .duration_seconds = {}, }}; } diff --git a/src/codecs/include/codec.hpp b/src/codecs/include/codec.hpp index 4b5ab47f..299b16e4 100644 --- a/src/codecs/include/codec.hpp +++ b/src/codecs/include/codec.hpp @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include @@ -50,6 +51,9 @@ class ICodec { uint8_t num_channels; uint8_t bits_per_sample; uint32_t sample_rate_hz; + + std::optional duration_seconds; + std::optional bits_per_second; }; /* diff --git a/src/codecs/mad.cpp b/src/codecs/mad.cpp index 23b4ccf6..81daeb9f 100644 --- a/src/codecs/mad.cpp +++ b/src/codecs/mad.cpp @@ -6,6 +6,7 @@ #include "mad.hpp" #include +#include #include #include @@ -79,12 +80,19 @@ auto MadMp3Decoder::BeginStream(const cpp::span input) } uint8_t channels = MAD_NCHANNELS(&header); - return {GetBytesUsed(input.size_bytes()), - OutputFormat{ - .num_channels = channels, - .bits_per_sample = 24, // We always scale to 24 bits - .sample_rate_hz = header.samplerate, - }}; + OutputFormat output{ + .num_channels = channels, + .bits_per_sample = 24, // We always scale to 24 bits + .sample_rate_hz = header.samplerate, + .duration_seconds = {}, + .bits_per_second = {}, + }; + + // TODO(jacqueline): Support VBR. Although maybe libtags is the better place + // to handle this? + output.bits_per_second = header.bitrate; + + return {GetBytesUsed(input.size_bytes()), output}; } auto MadMp3Decoder::ContinueStream(cpp::span input, diff --git a/src/database/database.cpp b/src/database/database.cpp index 1ac5d729..0d1c43e2 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -268,6 +268,62 @@ auto Database::GetTrackPath(TrackId id) }); } +auto Database::GetTrack(TrackId id) -> std::future> { + return worker_task_->Dispatch>( + [=, this]() -> std::optional { + std::optional data = dbGetTrackData(id); + if (!data || data->is_tombstoned()) { + return {}; + } + TrackTags tags; + if (!tag_parser_->ReadAndParseTags(data->filepath(), &tags)) { + return {}; + } + return Track(*data, tags); + }); +} + +auto Database::GetBulkTracks(std::vector ids) + -> std::future>> { + return worker_task_->Dispatch>>( + [=, this]() -> std::vector> { + std::map id_to_track{}; + + // Sort the list of ids so that we can retrieve them all in a single + // iteration through the database, without re-seeking. + std::vector sorted_ids = ids; + std::sort(sorted_ids.begin(), sorted_ids.end()); + + leveldb::Iterator* it = db_->NewIterator(leveldb::ReadOptions{}); + for (const TrackId& id : sorted_ids) { + OwningSlice key = EncodeDataKey(id); + it->Seek(key.slice); + if (!it->Valid() || it->key() != key.slice) { + // This id wasn't found at all. Skip it. + continue; + } + std::optional track = + ParseRecord(it->key(), it->value()); + if (track) { + id_to_track.insert({id, *track}); + } + } + + // We've fetched all of the ids in the request, so now just put them + // back into the order they were asked for in. + std::vector> results; + for (const TrackId& id : ids) { + if (id_to_track.contains(id)) { + results.push_back(id_to_track.at(id)); + } else { + // This lookup failed. + results.push_back({}); + } + } + return results; + }); +} + auto Database::GetIndexes() -> std::vector { // TODO(jacqueline): This probably needs to be async? When we have runtime // configurable indexes, they will need to come from somewhere. diff --git a/src/database/include/database.hpp b/src/database/include/database.hpp index 77a17b75..7ffc15b0 100644 --- a/src/database/include/database.hpp +++ b/src/database/include/database.hpp @@ -100,6 +100,15 @@ class Database { auto GetTrackPath(TrackId id) -> std::future>; + auto GetTrack(TrackId id) -> std::future>; + + /* + * Fetches data for multiple tracks more efficiently than multiple calls to + * GetTrack. + */ + auto GetBulkTracks(std::vector id) + -> std::future>>; + auto GetIndexes() -> std::vector; auto GetTracksByIndex(const IndexInfo& index, std::size_t page_size) -> std::future*>; diff --git a/src/database/include/future_fetcher.hpp b/src/database/include/future_fetcher.hpp new file mode 100644 index 00000000..e8ce9729 --- /dev/null +++ b/src/database/include/future_fetcher.hpp @@ -0,0 +1,62 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include + +#include "database.hpp" + +namespace database { + +/* + * Utility to simplify waiting for a std::future to complete without blocking. + * Each instance is good for a single future, and does not directly own anything + * other than the future itself. + */ +template +class FutureFetcher { + public: + explicit FutureFetcher(std::future&& fut) + : is_consumed_(false), fut_(std::move(fut)) {} + + /* + * Returns whether or not the underlying future is still awaiting async work. + */ + auto Finished() -> bool { + if (!fut_.valid()) { + return true; + } + if (fut_.wait_for(std::chrono::seconds(0)) != std::future_status::ready) { + return false; + } + return true; + } + + /* + * Returns the result of the future, and releases ownership of the underling + * resource. Will return an absent value if the future became invalid (e.g. + * the promise associated with it was destroyed.) + */ + auto Result() -> std::optional { + assert(!is_consumed_); + if (is_consumed_) { + return {}; + } + is_consumed_ = true; + if (!fut_.valid()) { + return {}; + } + return fut_.get(); + } + + private: + bool is_consumed_; + std::future fut_; +}; + +} // namespace database diff --git a/src/database/include/track.hpp b/src/database/include/track.hpp index 87fae9b9..620fc59e 100644 --- a/src/database/include/track.hpp +++ b/src/database/include/track.hpp @@ -68,6 +68,8 @@ class TrackTags { std::optional sample_rate; std::optional bits_per_sample; + std::optional duration; + auto set(const Tag& key, const std::string& val) -> void; auto at(const Tag& key) const -> std::optional; auto operator[](const Tag& key) const -> std::optional; diff --git a/src/database/tag_parser.cpp b/src/database/tag_parser.cpp index b2d206d2..2b784ea5 100644 --- a/src/database/tag_parser.cpp +++ b/src/database/tag_parser.cpp @@ -156,6 +156,9 @@ auto TagParserImpl::ReadAndParseTags(const std::string& path, TrackTags* out) if (ctx.bitrate > 0) { out->bits_per_sample = ctx.bitrate; } + if (ctx.duration > 0) { + out->duration = ctx.duration; + } return true; } diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp index dcfefbab..48b027d2 100644 --- a/src/system_fsm/booting.cpp +++ b/src/system_fsm/booting.cpp @@ -17,6 +17,7 @@ #include "spi.hpp" #include "system_events.hpp" #include "system_fsm.hpp" +#include "track_queue.hpp" #include "ui_fsm.hpp" #include "i2c.hpp" @@ -48,8 +49,9 @@ auto Booting::entry() -> void { sGpios->set_listener(&sGpiosCallback); // Start bringing up LVGL now, since we have all of its prerequisites. + sTrackQueue.reset(new audio::TrackQueue()); ESP_LOGI(kTag, "starting ui"); - if (!ui::UiState::Init(sGpios.get())) { + if (!ui::UiState::Init(sGpios.get(), sTrackQueue.get())) { events::Dispatch( FatalError()); return; @@ -70,7 +72,7 @@ auto Booting::entry() -> void { // state machines and inform them that the system is ready. ESP_LOGI(kTag, "starting audio"); - if (!audio::AudioState::Init(sGpios.get(), sDatabase)) { + if (!audio::AudioState::Init(sGpios.get(), sDatabase, sTrackQueue.get())) { events::Dispatch( FatalError()); return; @@ -83,6 +85,7 @@ auto Booting::entry() -> void { auto Booting::exit() -> void { // TODO(jacqueline): Gate this on something. Debug flag? Flashing mode? sAppConsole = new console::AppConsole(); + sAppConsole->sTrackQueue = sTrackQueue.get(); sAppConsole->Launch(); } diff --git a/src/system_fsm/include/system_fsm.hpp b/src/system_fsm/include/system_fsm.hpp index f6a52019..3c3169d1 100644 --- a/src/system_fsm/include/system_fsm.hpp +++ b/src/system_fsm/include/system_fsm.hpp @@ -20,6 +20,7 @@ #include "touchwheel.hpp" #include "system_events.hpp" +#include "track_queue.hpp" namespace system_fsm { @@ -58,6 +59,8 @@ class SystemState : public tinyfsm::Fsm { static std::shared_ptr sDisplay; static std::shared_ptr sDatabase; + static std::shared_ptr sTrackQueue; + static console::AppConsole* sAppConsole; }; diff --git a/src/system_fsm/system_fsm.cpp b/src/system_fsm/system_fsm.cpp index 1b3aab51..769d5e4a 100644 --- a/src/system_fsm/system_fsm.cpp +++ b/src/system_fsm/system_fsm.cpp @@ -9,6 +9,7 @@ #include "event_queue.hpp" #include "relative_wheel.hpp" #include "system_events.hpp" +#include "track_queue.hpp" namespace system_fsm { @@ -22,6 +23,8 @@ std::shared_ptr SystemState::sStorage; std::shared_ptr SystemState::sDisplay; std::shared_ptr SystemState::sDatabase; +std::shared_ptr SystemState::sTrackQueue; + console::AppConsole* SystemState::sAppConsole; void SystemState::react(const FatalError& err) { diff --git a/src/ui/include/screen.hpp b/src/ui/include/screen.hpp index 7ff06fbd..13b92a09 100644 --- a/src/ui/include/screen.hpp +++ b/src/ui/include/screen.hpp @@ -15,14 +15,24 @@ namespace ui { +/* + * Base class for ever discrete screen in the app. Provides a consistent + * interface that can be used for transitioning between screens, adding them to + * back stacks, etc. + */ class Screen { public: Screen() : root_(lv_obj_create(NULL)), group_(lv_group_create()) {} + virtual ~Screen() { lv_obj_del(root_); lv_group_del(group_); } + /* + * Called periodically to allow the screen to update itself, e.g. to handle + * std::futures that are still loading in. + */ virtual auto Tick() -> void {} auto root() -> lv_obj_t* { return root_; } diff --git a/src/ui/include/screen_playing.hpp b/src/ui/include/screen_playing.hpp index 5ccfe391..148f2774 100644 --- a/src/ui/include/screen_playing.hpp +++ b/src/ui/include/screen_playing.hpp @@ -7,30 +7,54 @@ #pragma once #include +#include #include #include #include "lvgl.h" #include "database.hpp" +#include "future_fetcher.hpp" #include "screen.hpp" #include "track.hpp" +#include "track_queue.hpp" namespace ui { namespace screens { +/* + * The 'Now Playing' / 'Currently Playing' screen that contains information + * about the current track, as well as playback controls. + */ class Playing : public Screen { public: - explicit Playing(database::Track t); + explicit Playing(std::weak_ptr db, + audio::TrackQueue* queue); ~Playing(); - auto BindTrack(database::Track t) -> void; + auto Tick() -> void override; - auto UpdateTime(uint32_t) -> void; - auto UpdateNextUp(std::vector tracks) -> void; + // Callbacks invoked by the UI state machine in response to audio events. + + auto OnTrackUpdate() -> void; + auto OnPlaybackUpdate(uint32_t, uint32_t) -> void; + auto OnQueueUpdate() -> void; private: - database::Track track_; + auto BindTrack(const database::Track& track) -> void; + auto ApplyNextUp(const std::vector& tracks) -> void; + + std::weak_ptr db_; + audio::TrackQueue* queue_; + + std::optional track_; + std::vector next_tracks_; + + std::unique_ptr>> + new_track_; + std::unique_ptr< + database::FutureFetcher>>> + new_next_tracks_; lv_obj_t* artist_label_; lv_obj_t* album_label_; @@ -40,7 +64,6 @@ class Playing : public Screen { lv_obj_t* play_pause_control_; lv_obj_t* next_up_container_; - std::vector next_tracks_; }; } // namespace screens diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index cd1ec492..2fc6db4e 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -19,13 +19,14 @@ #include "storage.hpp" #include "system_events.hpp" #include "touchwheel.hpp" +#include "track_queue.hpp" #include "ui_events.hpp" namespace ui { class UiState : public tinyfsm::Fsm { public: - static auto Init(drivers::IGpios* gpio_expander) -> bool; + static auto Init(drivers::IGpios*, audio::TrackQueue*) -> bool; virtual ~UiState() {} @@ -39,12 +40,14 @@ class UiState : public tinyfsm::Fsm { /* Fallback event handler. Does nothing. */ void react(const tinyfsm::Event& ev) {} - virtual void react(const audio::PlaybackUpdate){}; + virtual void react(const audio::PlaybackStarted&) {} + virtual void react(const audio::PlaybackUpdate&) {} + virtual void react(const audio::QueueUpdate&) {} - virtual void react(const system_fsm::KeyLockChanged&){}; + virtual void react(const system_fsm::KeyLockChanged&) {} - virtual void react(const internal::RecordSelected&){}; - virtual void react(const internal::IndexSelected&){}; + virtual void react(const internal::RecordSelected&) {} + virtual void react(const internal::IndexSelected&) {} virtual void react(const system_fsm::DisplayReady&) {} virtual void react(const system_fsm::BootComplete&) {} @@ -52,8 +55,11 @@ class UiState : public tinyfsm::Fsm { protected: void PushScreen(std::shared_ptr); + void PopScreen(); static drivers::IGpios* sIGpios; + static audio::TrackQueue* sQueue; + static std::shared_ptr sTouchWheel; static std::shared_ptr sRelativeWheel; static std::shared_ptr sDisplay; @@ -61,7 +67,6 @@ class UiState : public tinyfsm::Fsm { static std::stack> sScreens; static std::shared_ptr sCurrentScreen; - static std::unique_ptr sPlayingScreen; }; namespace states { @@ -81,12 +86,17 @@ class Browse : public UiState { void react(const system_fsm::KeyLockChanged&) override; void react(const system_fsm::StorageMounted&) override; + using UiState::react; }; class Playing : public UiState { void entry() override; + void exit() override; - void react(const audio::PlaybackUpdate) override; + void react(const audio::PlaybackStarted&) override; + void react(const audio::PlaybackUpdate&) override; + void react(const audio::QueueUpdate&) override; + using UiState::react; }; class FatalError : public UiState {}; diff --git a/src/ui/screen_playing.cpp b/src/ui/screen_playing.cpp index 39f6b04d..50b1d33a 100644 --- a/src/ui/screen_playing.cpp +++ b/src/ui/screen_playing.cpp @@ -5,13 +5,17 @@ */ #include "screen_playing.hpp" +#include +#include #include "core/lv_obj.h" #include "core/lv_obj_tree.h" +#include "database.hpp" #include "esp_log.h" #include "extra/layouts/flex/lv_flex.h" #include "extra/layouts/grid/lv_grid.h" #include "font/lv_symbol_def.h" +#include "future_fetcher.hpp" #include "lvgl.h" #include "core/lv_group.h" @@ -20,8 +24,10 @@ #include "extra/widgets/list/lv_list.h" #include "extra/widgets/menu/lv_menu.h" #include "extra/widgets/spinner/lv_spinner.h" +#include "future_fetcher.hpp" #include "hal/lv_hal_disp.h" #include "index.hpp" +#include "misc/lv_anim.h" #include "misc/lv_area.h" #include "misc/lv_color.h" #include "track.hpp" @@ -35,6 +41,8 @@ namespace ui { namespace screens { +static constexpr std::size_t kMaxUpcoming = 10; + static lv_style_t scrubber_style; auto info_label(lv_obj_t* parent) -> lv_obj_t* { @@ -60,7 +68,13 @@ auto next_up_label(lv_obj_t* parent, const std::string& text) -> lv_obj_t* { return label; } -Playing::Playing(database::Track track) : track_(track) { +Playing::Playing(std::weak_ptr db, audio::TrackQueue* queue) + : db_(db), + queue_(queue), + track_(), + next_tracks_(), + new_track_(), + new_next_tracks_() { lv_obj_set_layout(root_, LV_LAYOUT_FLEX); lv_obj_set_size(root_, lv_pct(100), lv_pct(200)); lv_obj_set_flex_flow(root_, LV_FLEX_FLOW_COLUMN); @@ -131,12 +145,72 @@ Playing::Playing(database::Track track) : track_(track) { lv_obj_set_flex_align(next_up_container_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_END); - BindTrack(track); + OnTrackUpdate(); + OnQueueUpdate(); } Playing::~Playing() {} -auto Playing::BindTrack(database::Track t) -> void { +auto Playing::OnTrackUpdate() -> void { + auto current = queue_->GetCurrent(); + if (!current) { + return; + } + if (track_ && track_->data().id() == *current) { + return; + } + auto db = db_.lock(); + if (!db) { + return; + } + new_track_.reset(new database::FutureFetcher>( + db->GetTrack(*current))); +} + +auto Playing::OnPlaybackUpdate(uint32_t pos_seconds, uint32_t new_duration) + -> void { + if (!track_) { + return; + } + lv_bar_set_range(scrubber_, 0, new_duration); + lv_bar_set_value(scrubber_, pos_seconds, LV_ANIM_ON); +} + +auto Playing::OnQueueUpdate() -> void { + auto current = queue_->GetUpcoming(kMaxUpcoming); + auto db = db_.lock(); + if (!db) { + return; + } + new_next_tracks_.reset( + new database::FutureFetcher>>( + db->GetBulkTracks(current))); +} + +auto Playing::Tick() -> void { + if (new_track_ && new_track_->Finished()) { + auto res = new_track_->Result(); + new_track_.reset(); + if (res && *res) { + BindTrack(**res); + } + } + if (new_next_tracks_ && new_next_tracks_->Finished()) { + auto res = new_next_tracks_->Result(); + new_next_tracks_.reset(); + if (res) { + std::vector filtered; + for (const auto& t : *res) { + if (t) { + filtered.push_back(*t); + } + } + ApplyNextUp(filtered); + } + } +} + +auto Playing::BindTrack(const database::Track& t) -> void { track_ = t; lv_label_set_text(artist_label_, @@ -145,16 +219,12 @@ auto Playing::BindTrack(database::Track t) -> void { t.tags().at(database::Tag::kAlbum).value_or("").c_str()); lv_label_set_text(title_label_, t.TitleOrFilename().c_str()); - // TODO. - lv_bar_set_range(scrubber_, 0, 0); + std::optional duration = t.tags().duration; + lv_bar_set_range(scrubber_, 0, duration.value_or(1)); lv_bar_set_value(scrubber_, 0, LV_ANIM_OFF); } -auto Playing::UpdateTime(uint32_t time) -> void { - lv_bar_set_value(scrubber_, time, LV_ANIM_OFF); -} - -auto Playing::UpdateNextUp(std::vector tracks) -> void { +auto Playing::ApplyNextUp(const std::vector& tracks) -> void { // TODO(jacqueline): Do a proper diff to maintain selection. int children = lv_obj_get_child_cnt(next_up_container_); while (children > 0) { @@ -163,8 +233,9 @@ auto Playing::UpdateNextUp(std::vector tracks) -> void { } next_tracks_ = tracks; - for (const auto &track : next_tracks_) { - lv_group_add_obj(group_, next_up_label(next_up_container_, track.TitleOrFilename())); + for (const auto& track : next_tracks_) { + lv_group_add_obj( + group_, next_up_label(next_up_container_, track.TitleOrFilename())); } } diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 13658c37..5394311c 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -19,6 +19,7 @@ #include "screen_track_browser.hpp" #include "system_events.hpp" #include "touchwheel.hpp" +#include "track_queue.hpp" namespace ui { @@ -27,6 +28,8 @@ static constexpr char kTag[] = "ui_fsm"; static const std::size_t kRecordsPerPage = 10; drivers::IGpios* UiState::sIGpios; +audio::TrackQueue* UiState::sQueue; + std::shared_ptr UiState::sTouchWheel; std::shared_ptr UiState::sRelativeWheel; std::shared_ptr UiState::sDisplay; @@ -34,10 +37,11 @@ std::weak_ptr UiState::sDb; std::stack> UiState::sScreens; std::shared_ptr UiState::sCurrentScreen; -std::unique_ptr UiState::sPlayingScreen; -auto UiState::Init(drivers::IGpios* gpio_expander) -> bool { +auto UiState::Init(drivers::IGpios* gpio_expander, audio::TrackQueue* queue) + -> bool { sIGpios = gpio_expander; + sQueue = queue; lv_init(); sDisplay.reset( @@ -70,6 +74,14 @@ void UiState::PushScreen(std::shared_ptr screen) { sCurrentScreen = screen; } +void UiState::PopScreen() { + if (sScreens.empty()) { + return; + } + sCurrentScreen = sScreens.top(); + sScreens.pop(); +} + namespace states { void Splash::exit() { @@ -107,12 +119,9 @@ void Browse::react(const internal::RecordSelected& ev) { if (ev.record.track()) { ESP_LOGI(kTag, "selected track '%s'", ev.record.text()->c_str()); // TODO(jacqueline): We should also send some kind of playlist info here. - auto track = ev.record.track().value(); - events::Dispatch(audio::PlayTrack{ - .id = track.data().id(), - .data = track.data(), - }); - PushScreen(std::make_shared(track)); + sQueue->Clear(); + sQueue->AddLast(ev.record.track()->data().id()); + transit(); } else { ESP_LOGI(kTag, "selected record '%s'", ev.record.text()->c_str()); auto cont = ev.record.Expand(kRecordsPerPage); @@ -138,8 +147,28 @@ void Browse::react(const internal::IndexSelected& ev) { std::move(query))); } -void Playing::react(const audio::PlaybackUpdate ev) { - sPlayingScreen->UpdateTime(ev.seconds_elapsed); +static std::shared_ptr sPlayingScreen; + +void Playing::entry() { + sPlayingScreen.reset(new screens::Playing(sDb, sQueue)); + PushScreen(sPlayingScreen); +} + +void Playing::exit() { + sPlayingScreen.reset(); + PopScreen(); +} + +void Playing::react(const audio::PlaybackStarted& ev) { + sPlayingScreen->OnTrackUpdate(); +} + +void Playing::react(const audio::PlaybackUpdate& ev) { + sPlayingScreen->OnPlaybackUpdate(ev.seconds_elapsed, ev.seconds_total); +} + +void Playing::react(const audio::QueueUpdate& ev) { + sPlayingScreen->OnQueueUpdate(); } } // namespace states