Merge branch 'functional-playing'

custom
jacqueline 2 years ago
commit b6e0e0dd4a
  1. 7
      src/app_console/app_console.cpp
  2. 2
      src/app_console/include/app_console.hpp
  3. 2
      src/audio/CMakeLists.txt
  4. 14
      src/audio/audio_decoder.cpp
  5. 76
      src/audio/audio_fsm.cpp
  6. 14
      src/audio/audio_task.cpp
  7. 20
      src/audio/fatfs_audio_input.cpp
  8. 3
      src/audio/include/audio_decoder.hpp
  9. 21
      src/audio/include/audio_events.hpp
  10. 33
      src/audio/include/audio_fsm.hpp
  11. 2
      src/audio/include/fatfs_audio_input.hpp
  12. 9
      src/audio/include/stream_info.hpp
  13. 85
      src/audio/include/track_queue.hpp
  14. 4
      src/audio/stream_info.cpp
  15. 128
      src/audio/track_queue.cpp
  16. 1
      src/codecs/foxenflac.cpp
  17. 4
      src/codecs/include/codec.hpp
  18. 14
      src/codecs/mad.cpp
  19. 56
      src/database/database.cpp
  20. 9
      src/database/include/database.hpp
  21. 62
      src/database/include/future_fetcher.hpp
  22. 3
      src/database/include/track.hpp
  23. 3
      src/database/tag_parser.cpp
  24. 7
      src/system_fsm/booting.cpp
  25. 3
      src/system_fsm/include/system_fsm.hpp
  26. 3
      src/system_fsm/system_fsm.cpp
  27. 10
      src/ui/include/screen.hpp
  28. 36
      src/ui/include/screen_playing.hpp
  29. 31
      src/ui/include/ui_fsm.hpp
  30. 105
      src/ui/screen_playing.cpp
  31. 60
      src/ui/ui_fsm.cpp

@ -30,6 +30,7 @@
namespace console { namespace console {
std::weak_ptr<database::Database> AppConsole::sDatabase; std::weak_ptr<database::Database> AppConsole::sDatabase;
audio::TrackQueue* AppConsole::sTrackQueue;
int CmdListDir(int argc, char** argv) { int CmdListDir(int argc, char** argv) {
auto lock = AppConsole::sDatabase.lock(); auto lock = AppConsole::sDatabase.lock();
@ -108,9 +109,10 @@ int CmdPlayFile(int argc, char** argv) {
if (is_id) { if (is_id) {
database::TrackId id = std::atoi(argv[1]); database::TrackId id = std::atoi(argv[1]);
events::Dispatch<audio::PlayTrack, audio::AudioState>( AppConsole::sTrackQueue->AddLast(id);
audio::PlayTrack{.id = id});
} else { } else {
// TODO.
/*
std::ostringstream path; std::ostringstream path;
path << '/' << argv[1]; path << '/' << argv[1];
for (int i = 2; i < argc; i++) { for (int i = 2; i < argc; i++) {
@ -119,6 +121,7 @@ int CmdPlayFile(int argc, char** argv) {
events::Dispatch<audio::PlayFile, audio::AudioState>( events::Dispatch<audio::PlayFile, audio::AudioState>(
audio::PlayFile{.filename = path.str()}); audio::PlayFile{.filename = path.str()});
*/
} }
return 0; return 0;

@ -10,12 +10,14 @@
#include "console.hpp" #include "console.hpp"
#include "database.hpp" #include "database.hpp"
#include "track_queue.hpp"
namespace console { namespace console {
class AppConsole : public Console { class AppConsole : public Console {
public: public:
static std::weak_ptr<database::Database> sDatabase; static std::weak_ptr<database::Database> sDatabase;
static audio::TrackQueue* sTrackQueue;
protected: protected:
virtual auto RegisterExtraComponents() -> void; virtual auto RegisterExtraComponents() -> void;

@ -4,7 +4,7 @@
idf_component_register( idf_component_register(
SRCS "audio_decoder.cpp" "audio_task.cpp" "chunk.cpp" "fatfs_audio_input.cpp" 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" "stream_event.cpp" "pipeline.cpp" "stream_info.cpp" "audio_fsm.cpp"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" "database" "system_fsm") REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" "database" "system_fsm")

@ -53,7 +53,7 @@ auto AudioDecoder::ProcessStreamInfo(const StreamInfo& info) -> bool {
} }
const auto& new_format = std::get<StreamInfo::Encoded>(info.format); const auto& new_format = std::get<StreamInfo::Encoded>(info.format);
current_input_format_ = info.format; current_input_format_ = new_format;
ESP_LOGI(kTag, "creating new decoder"); ESP_LOGI(kTag, "creating new decoder");
auto result = codecs::CreateCodecForType(new_format.type); auto result = codecs::CreateCodecForType(new_format.type);
@ -112,6 +112,15 @@ auto AudioDecoder::Process(const std::vector<InputStream>& inputs,
.sample_rate = format.sample_rate_hz, .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) { if (info.seek_to_seconds) {
seek_to_sample_ = *info.seek_to_seconds * format.sample_rate_hz; seek_to_sample_ = *info.seek_to_seconds * format.sample_rate_hz;
} else { } else {
@ -144,6 +153,9 @@ auto AudioDecoder::Process(const std::vector<InputStream>& inputs,
if (!has_prepared_output_ && !output->prepare(*current_output_format_)) { if (!has_prepared_output_ && !output->prepare(*current_output_format_)) {
return; return;
} }
if (duration_seconds_from_decoder_) {
output->set_duration(*duration_seconds_from_decoder_);
}
has_prepared_output_ = true; has_prepared_output_ = true;
// Parse frames and produce samples. // Parse frames and produce samples.

@ -19,6 +19,7 @@
#include "pipeline.hpp" #include "pipeline.hpp"
#include "system_events.hpp" #include "system_events.hpp"
#include "track.hpp" #include "track.hpp"
#include "track_queue.hpp"
namespace audio { namespace audio {
@ -32,11 +33,13 @@ std::unique_ptr<FatfsAudioInput> AudioState::sFileSource;
std::unique_ptr<I2SAudioOutput> AudioState::sI2SOutput; std::unique_ptr<I2SAudioOutput> AudioState::sI2SOutput;
std::vector<std::unique_ptr<IAudioElement>> AudioState::sPipeline; std::vector<std::unique_ptr<IAudioElement>> AudioState::sPipeline;
std::deque<AudioState::EnqueuedItem> AudioState::sTrackQueue; TrackQueue* AudioState::sTrackQueue;
auto AudioState::Init(drivers::IGpios* gpio_expander, auto AudioState::Init(drivers::IGpios* gpio_expander,
std::weak_ptr<database::Database> database) -> bool { std::weak_ptr<database::Database> database,
TrackQueue* queue) -> bool {
sIGpios = gpio_expander; sIGpios = gpio_expander;
sTrackQueue = queue;
auto dac = drivers::I2SDac::create(gpio_expander); auto dac = drivers::I2SDac::create(gpio_expander);
if (!dac) { if (!dac) {
@ -94,26 +97,23 @@ void Uninitialised::react(const system_fsm::BootComplete&) {
transit<Standby>(); transit<Standby>();
} }
void Standby::react(const InputFileOpened& ev) { void Standby::react(const internal::InputFileOpened& ev) {
transit<Playback>(); transit<Playback>();
} }
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(); auto db = sDatabase.lock();
if (!db) { if (!db) {
ESP_LOGW(kTag, "database not open; ignoring play request"); ESP_LOGW(kTag, "database not open; ignoring play request");
return; return;
} }
if (ev.data) { sFileSource->OpenFile(db->GetTrackPath(*current_track));
sFileSource->OpenFile(ev.data->filepath());
} else {
sFileSource->OpenFile(db->GetTrackPath(ev.id));
}
}
void Standby::react(const PlayFile& ev) {
sFileSource->OpenFile(ev.filename);
} }
void Playback::entry() { void Playback::entry() {
@ -126,42 +126,50 @@ void Playback::exit() {
sI2SOutput->SetInUse(false); sI2SOutput->SetInUse(false);
} }
void Playback::react(const PlayTrack& ev) { void Playback::react(const QueueUpdate& ev) {
sTrackQueue.push_back(EnqueuedItem(ev.id)); auto current_track = sTrackQueue->GetCurrent();
} if (!current_track) {
// TODO: return to standby?
return;
}
void Playback::react(const PlayFile& ev) { auto db = sDatabase.lock();
sTrackQueue.push_back(EnqueuedItem(ev.filename)); 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) { 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) { void Playback::react(const internal::InputFileClosed& ev) {
ESP_LOGI(kTag, "finished file"); ESP_LOGI(kTag, "finished reading file");
if (sTrackQueue.empty()) { auto upcoming = sTrackQueue->GetUpcoming(1);
if (upcoming.empty()) {
return; return;
} }
EnqueuedItem next_item = sTrackQueue.front();
sTrackQueue.pop_front();
if (std::holds_alternative<std::string>(next_item)) {
sFileSource->OpenFile(std::get<std::string>(next_item));
} else if (std::holds_alternative<database::TrackId>(next_item)) {
auto db = sDatabase.lock(); auto db = sDatabase.lock();
if (!db) { if (!db) {
ESP_LOGW(kTag, "database not open; ignoring play request");
return; return;
} }
sFileSource->OpenFile( ESP_LOGI(kTag, "preemptively opening next file");
db->GetTrackPath(std::get<database::TrackId>(next_item))); 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<Standby>(); transit<Standby>();
} }

@ -37,6 +37,7 @@
#include "stream_message.hpp" #include "stream_message.hpp"
#include "sys/_stdint.h" #include "sys/_stdint.h"
#include "tasks.hpp" #include "tasks.hpp"
#include "ui_fsm.hpp"
namespace audio { namespace audio {
@ -87,7 +88,7 @@ void AudioTaskMain(std::unique_ptr<Pipeline> pipeline, IAudioSink* sink) {
} }
if (previously_had_work && !has_work) { if (previously_had_work && !has_work) {
events::Dispatch<AudioPipelineIdle, AudioState>({}); events::Dispatch<internal::AudioPipelineIdle, AudioState>({});
} }
previously_had_work = has_work; previously_had_work = has_work;
@ -136,6 +137,10 @@ void AudioTaskMain(std::unique_ptr<Pipeline> pipeline, IAudioSink* sink) {
if (sink_stream.is_producer_finished()) { if (sink_stream.is_producer_finished()) {
sink_stream.mark_consumer_finished(); sink_stream.mark_consumer_finished();
if (current_second > 0 || current_sample_in_second > 0) {
events::Dispatch<internal::InputFileFinished, AudioState>({});
}
current_second = 0; current_second = 0;
previous_second = 0; previous_second = 0;
current_sample_in_second = 0; current_sample_in_second = 0;
@ -185,8 +190,11 @@ void AudioTaskMain(std::unique_ptr<Pipeline> pipeline, IAudioSink* sink) {
current_sample_in_second -= pcm.sample_rate; current_sample_in_second -= pcm.sample_rate;
} }
if (previous_second != current_second) { if (previous_second != current_second) {
events::Dispatch<PlaybackUpdate, AudioState>( events::Dispatch<PlaybackUpdate, AudioState, ui::UiState>({
{.seconds_elapsed = current_second}); .seconds_elapsed = current_second,
.seconds_total =
sink_stream.info().duration_seconds.value_or(current_second),
});
} }
previous_second = current_second; previous_second = current_second;
} }

@ -56,6 +56,7 @@ auto FatfsAudioInput::OpenFile(std::future<std::optional<std::string>>&& path)
} }
auto FatfsAudioInput::OpenFile(const std::string& path) -> bool { auto FatfsAudioInput::OpenFile(const std::string& path) -> bool {
current_path_.reset();
if (is_file_open_) { if (is_file_open_) {
f_close(&current_file_); f_close(&current_file_);
is_file_open_ = false; 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()); 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::TagParserImpl tag_parser;
database::TrackTags tags; database::TrackTags tags;
if (!tag_parser.ReadAndParseTags(path, &tags)) { if (!tag_parser.ReadAndParseTags(path, &tags)) {
@ -95,7 +101,10 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool {
.sample_rate = static_cast<uint32_t>(*tags.sample_rate), .sample_rate = static_cast<uint32_t>(*tags.sample_rate),
}; };
} else { } else {
current_format_ = StreamInfo::Encoded{*stream_type}; current_format_ = StreamInfo::Encoded{
.type = *stream_type,
.duration_bytes = info.fsize,
};
} }
FRESULT res = f_open(&current_file_, path.c_str(), FA_READ); FRESULT res = f_open(&current_file_, path.c_str(), FA_READ);
@ -104,7 +113,8 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool {
return false; return false;
} }
events::Dispatch<InputFileOpened, AudioState>({}); events::Dispatch<internal::InputFileOpened, AudioState>({});
current_path_ = path;
is_file_open_ = true; is_file_open_ = true;
return true; return true;
} }
@ -124,9 +134,10 @@ auto FatfsAudioInput::Process(const std::vector<InputStream>& inputs,
if (pending_path_->wait_for(std::chrono::seconds(0)) == if (pending_path_->wait_for(std::chrono::seconds(0)) ==
std::future_status::ready) { std::future_status::ready) {
auto result = pending_path_->get(); auto result = pending_path_->get();
if (result) { if (result && result != current_path_) {
OpenFile(*result); OpenFile(*result);
} }
pending_path_ = {};
} }
} }
} }
@ -173,10 +184,11 @@ auto FatfsAudioInput::Process(const std::vector<InputStream>& inputs,
f_close(&current_file_); f_close(&current_file_);
is_file_open_ = false; is_file_open_ = false;
current_path_.reset();
has_prepared_output_ = false; has_prepared_output_ = false;
output->mark_producer_finished(); output->mark_producer_finished();
events::Dispatch<InputFileFinished, AudioState>({}); events::Dispatch<internal::InputFileClosed, AudioState>({});
} }
} }

@ -40,8 +40,9 @@ class AudioDecoder : public IAudioElement {
private: private:
std::unique_ptr<codecs::ICodec> current_codec_; std::unique_ptr<codecs::ICodec> current_codec_;
std::optional<StreamInfo::Format> current_input_format_; std::optional<StreamInfo::Encoded> current_input_format_;
std::optional<StreamInfo::Format> current_output_format_; std::optional<StreamInfo::Format> current_output_format_;
std::optional<std::size_t> duration_seconds_from_decoder_;
std::optional<std::size_t> seek_to_sample_; std::optional<std::size_t> seek_to_sample_;
bool has_prepared_output_; bool has_prepared_output_;
bool has_samples_to_send_; bool has_samples_to_send_;

@ -13,26 +13,31 @@
#include "tinyfsm.hpp" #include "tinyfsm.hpp"
#include "track.hpp" #include "track.hpp"
#include "track_queue.hpp"
namespace audio { namespace audio {
struct PlayFile : tinyfsm::Event { struct PlaybackStarted : tinyfsm::Event {
std::string filename; database::Track track;
};
struct PlayTrack : tinyfsm::Event {
database::TrackId id;
std::optional<database::TrackData> data;
}; };
struct PlaybackUpdate : tinyfsm::Event { struct PlaybackUpdate : tinyfsm::Event {
uint32_t seconds_elapsed; uint32_t seconds_elapsed;
uint32_t seconds_total;
}; };
struct QueueUpdate : tinyfsm::Event {};
struct VolumeChanged : tinyfsm::Event {};
namespace internal {
struct InputFileOpened : tinyfsm::Event {}; struct InputFileOpened : tinyfsm::Event {};
struct InputFileClosed : tinyfsm::Event {};
struct InputFileFinished : tinyfsm::Event {}; struct InputFileFinished : tinyfsm::Event {};
struct AudioPipelineIdle : tinyfsm::Event {}; struct AudioPipelineIdle : tinyfsm::Event {};
struct VolumeChanged : tinyfsm::Event {}; } // namespace internal
} // namespace audio } // namespace audio

@ -22,13 +22,15 @@
#include "track.hpp" #include "track.hpp"
#include "system_events.hpp" #include "system_events.hpp"
#include "track_queue.hpp"
namespace audio { namespace audio {
class AudioState : public tinyfsm::Fsm<AudioState> { class AudioState : public tinyfsm::Fsm<AudioState> {
public: public:
static auto Init(drivers::IGpios* gpio_expander, static auto Init(drivers::IGpios* gpio_expander,
std::weak_ptr<database::Database>) -> bool; std::weak_ptr<database::Database>,
TrackQueue* queue) -> bool;
virtual ~AudioState() {} virtual ~AudioState() {}
@ -45,14 +47,14 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
void react(const system_fsm::HasPhonesChanged&); void react(const system_fsm::HasPhonesChanged&);
virtual void react(const system_fsm::BootComplete&) {} 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 PlaybackUpdate&) {}
virtual void react(const InputFileOpened&) {} virtual void react(const internal::InputFileOpened&) {}
virtual void react(const InputFileFinished&) {} virtual void react(const internal::InputFileClosed&) {}
virtual void react(const AudioPipelineIdle&) {} virtual void react(const internal::InputFileFinished&) {}
virtual void react(const internal::AudioPipelineIdle&) {}
protected: protected:
static drivers::IGpios* sIGpios; static drivers::IGpios* sIGpios;
@ -63,8 +65,7 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
static std::unique_ptr<I2SAudioOutput> sI2SOutput; static std::unique_ptr<I2SAudioOutput> sI2SOutput;
static std::vector<std::unique_ptr<IAudioElement>> sPipeline; static std::vector<std::unique_ptr<IAudioElement>> sPipeline;
typedef std::variant<database::TrackId, std::string> EnqueuedItem; static TrackQueue* sTrackQueue;
static std::deque<EnqueuedItem> sTrackQueue;
}; };
namespace states { namespace states {
@ -77,9 +78,8 @@ class Uninitialised : public AudioState {
class Standby : public AudioState { class Standby : public AudioState {
public: public:
void react(const InputFileOpened&) override; void react(const internal::InputFileOpened&) override;
void react(const PlayTrack&) override; void react(const QueueUpdate&) override;
void react(const PlayFile&) override;
using AudioState::react; using AudioState::react;
}; };
@ -89,14 +89,13 @@ class Playback : public AudioState {
void entry() override; void entry() override;
void exit() override; void exit() override;
void react(const PlayTrack&) override; void react(const QueueUpdate&) override;
void react(const PlayFile&) override;
void react(const PlaybackUpdate&) override; void react(const PlaybackUpdate&) override;
void react(const InputFileOpened&) override; void react(const internal::InputFileOpened&) override;
void react(const InputFileFinished&) override; void react(const internal::InputFileClosed&) override;
void react(const AudioPipelineIdle&) override; void react(const internal::InputFileFinished&) override;
void react(const internal::AudioPipelineIdle&) override;
using AudioState::react; using AudioState::react;
}; };

@ -34,6 +34,7 @@ class FatfsAudioInput : public IAudioElement {
FatfsAudioInput(); FatfsAudioInput();
~FatfsAudioInput(); ~FatfsAudioInput();
auto CurrentFile() -> std::optional<std::string> { return current_path_; }
auto OpenFile(std::future<std::optional<std::string>>&& path) -> void; auto OpenFile(std::future<std::optional<std::string>>&& path) -> void;
auto OpenFile(const std::string& path) -> bool; auto OpenFile(const std::string& path) -> bool;
@ -50,6 +51,7 @@ class FatfsAudioInput : public IAudioElement {
-> std::optional<codecs::StreamType>; -> std::optional<codecs::StreamType>;
std::optional<std::future<std::optional<std::string>>> pending_path_; std::optional<std::future<std::optional<std::string>>> pending_path_;
std::optional<std::string> current_path_;
FIL current_file_; FIL current_file_;
bool is_file_open_; bool is_file_open_;
bool has_prepared_output_; bool has_prepared_output_;

@ -30,13 +30,16 @@ struct StreamInfo {
bool is_consumer_finished = true; bool is_consumer_finished = true;
// std::optional<std::uint32_t> duration_seconds;
std::optional<uint32_t> seek_to_seconds{};
std::optional<std::uint32_t> seek_to_seconds{};
struct Encoded { struct Encoded {
// The codec that this stream is associated with. // The codec that this stream is associated with.
codecs::StreamType type; codecs::StreamType type;
std::optional<std::size_t> duration_bytes;
bool operator==(const Encoded&) const = default; bool operator==(const Encoded&) const = default;
}; };
@ -95,6 +98,8 @@ class OutputStream {
bool prepare(const StreamInfo::Format& new_format); bool prepare(const StreamInfo::Format& new_format);
void set_duration(std::size_t);
const StreamInfo& info() const; const StreamInfo& info() const;
cpp::span<std::byte> data() const; cpp::span<std::byte> data() const;

@ -0,0 +1,85 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#include <deque>
#include <mutex>
#include <vector>
#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<database::TrackId>;
/* Returns, in order, tracks that have been queued to be played next. */
auto GetUpcoming(std::size_t limit) const -> std::vector<database::TrackId>;
/*
* 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<database::TrackId>&) -> 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<database::TrackId>&) -> 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<database::TrackId> played_;
std::deque<database::TrackId> upcoming_;
std::optional<database::TrackId> current_;
};
} // namespace audio

@ -64,6 +64,10 @@ bool OutputStream::prepare(const StreamInfo::Format& new_format) {
return false; return false;
} }
void OutputStream::set_duration(std::size_t seconds) {
raw_->info->duration_seconds = seconds;
}
const StreamInfo& OutputStream::info() const { const StreamInfo& OutputStream::info() const {
return *raw_->info; return *raw_->info;
} }

@ -0,0 +1,128 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#include "track_queue.hpp"
#include <algorithm>
#include <mutex>
#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<database::TrackId> {
const std::lock_guard<std::mutex> lock(mutex_);
return current_;
}
auto TrackQueue::GetUpcoming(std::size_t limit) const
-> std::vector<database::TrackId> {
const std::lock_guard<std::mutex> lock(mutex_);
std::vector<database::TrackId> 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<std::mutex> lock(mutex_);
if (!current_) {
current_ = t;
} else {
upcoming_.push_front(t);
}
events::Dispatch<QueueUpdate, AudioState, ui::UiState>({});
}
auto TrackQueue::AddNext(const std::vector<database::TrackId>& t) -> void {
const std::lock_guard<std::mutex> 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<QueueUpdate, AudioState, ui::UiState>({});
}
auto TrackQueue::AddLast(database::TrackId t) -> void {
const std::lock_guard<std::mutex> lock(mutex_);
if (!current_) {
current_ = t;
} else {
upcoming_.push_back(t);
}
events::Dispatch<QueueUpdate, AudioState, ui::UiState>({});
}
auto TrackQueue::AddLast(const std::vector<database::TrackId>& t) -> void {
const std::lock_guard<std::mutex> 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<QueueUpdate, AudioState, ui::UiState>({});
}
auto TrackQueue::Next() -> void {
const std::lock_guard<std::mutex> lock(mutex_);
if (current_) {
played_.push_front(*current_);
}
if (!upcoming_.empty()) {
current_ = upcoming_.front();
upcoming_.pop_front();
} else {
current_.reset();
}
events::Dispatch<QueueUpdate, AudioState, ui::UiState>({});
}
auto TrackQueue::Previous() -> void {
const std::lock_guard<std::mutex> lock(mutex_);
if (current_) {
upcoming_.push_front(*current_);
}
if (!played_.empty()) {
current_ = played_.front();
played_.pop_front();
} else {
current_.reset();
}
events::Dispatch<QueueUpdate, AudioState, ui::UiState>({});
}
auto TrackQueue::Clear() -> void {
const std::lock_guard<std::mutex> lock(mutex_);
played_.clear();
upcoming_.clear();
current_.reset();
events::Dispatch<QueueUpdate, AudioState, ui::UiState>({});
}
auto TrackQueue::RemoveUpcoming(database::TrackId t) -> void {
const std::lock_guard<std::mutex> lock(mutex_);
for (auto it = upcoming_.begin(); it != upcoming_.end(); it++) {
if (*it == t) {
upcoming_.erase(it);
return;
}
}
events::Dispatch<QueueUpdate, AudioState, ui::UiState>({});
}
} // namespace audio

@ -43,6 +43,7 @@ auto FoxenFlacDecoder::BeginStream(const cpp::span<const std::byte> input)
.num_channels = static_cast<uint8_t>(channels), .num_channels = static_cast<uint8_t>(channels),
.bits_per_sample = 32, // libfoxenflac output is fixed-size. .bits_per_sample = 32, // libfoxenflac output is fixed-size.
.sample_rate_hz = static_cast<uint32_t>(fs), .sample_rate_hz = static_cast<uint32_t>(fs),
.duration_seconds = {},
}}; }};
} }

@ -7,6 +7,7 @@
#pragma once #pragma once
#include <stdint.h> #include <stdint.h>
#include <sys/_stdint.h>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
@ -50,6 +51,9 @@ class ICodec {
uint8_t num_channels; uint8_t num_channels;
uint8_t bits_per_sample; uint8_t bits_per_sample;
uint32_t sample_rate_hz; uint32_t sample_rate_hz;
std::optional<uint32_t> duration_seconds;
std::optional<uint32_t> bits_per_second;
}; };
/* /*

@ -6,6 +6,7 @@
#include "mad.hpp" #include "mad.hpp"
#include <stdint.h> #include <stdint.h>
#include <sys/_stdint.h>
#include <cstdint> #include <cstdint>
#include <optional> #include <optional>
@ -79,12 +80,19 @@ auto MadMp3Decoder::BeginStream(const cpp::span<const std::byte> input)
} }
uint8_t channels = MAD_NCHANNELS(&header); uint8_t channels = MAD_NCHANNELS(&header);
return {GetBytesUsed(input.size_bytes()), OutputFormat output{
OutputFormat{
.num_channels = channels, .num_channels = channels,
.bits_per_sample = 24, // We always scale to 24 bits .bits_per_sample = 24, // We always scale to 24 bits
.sample_rate_hz = header.samplerate, .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<const std::byte> input, auto MadMp3Decoder::ContinueStream(cpp::span<const std::byte> input,

@ -268,6 +268,62 @@ auto Database::GetTrackPath(TrackId id)
}); });
} }
auto Database::GetTrack(TrackId id) -> std::future<std::optional<Track>> {
return worker_task_->Dispatch<std::optional<Track>>(
[=, this]() -> std::optional<Track> {
std::optional<TrackData> 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<TrackId> ids)
-> std::future<std::vector<std::optional<Track>>> {
return worker_task_->Dispatch<std::vector<std::optional<Track>>>(
[=, this]() -> std::vector<std::optional<Track>> {
std::map<TrackId, Track> 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<TrackId> 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> track =
ParseRecord<Track>(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<std::optional<Track>> 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<IndexInfo> { auto Database::GetIndexes() -> std::vector<IndexInfo> {
// TODO(jacqueline): This probably needs to be async? When we have runtime // TODO(jacqueline): This probably needs to be async? When we have runtime
// configurable indexes, they will need to come from somewhere. // configurable indexes, they will need to come from somewhere.

@ -100,6 +100,15 @@ class Database {
auto GetTrackPath(TrackId id) -> std::future<std::optional<std::string>>; auto GetTrackPath(TrackId id) -> std::future<std::optional<std::string>>;
auto GetTrack(TrackId id) -> std::future<std::optional<Track>>;
/*
* Fetches data for multiple tracks more efficiently than multiple calls to
* GetTrack.
*/
auto GetBulkTracks(std::vector<TrackId> id)
-> std::future<std::vector<std::optional<Track>>>;
auto GetIndexes() -> std::vector<IndexInfo>; auto GetIndexes() -> std::vector<IndexInfo>;
auto GetTracksByIndex(const IndexInfo& index, std::size_t page_size) auto GetTracksByIndex(const IndexInfo& index, std::size_t page_size)
-> std::future<Result<IndexRecord>*>; -> std::future<Result<IndexRecord>*>;

@ -0,0 +1,62 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#include <memory>
#include <utility>
#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 <typename T>
class FutureFetcher {
public:
explicit FutureFetcher(std::future<T>&& 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<T> {
assert(!is_consumed_);
if (is_consumed_) {
return {};
}
is_consumed_ = true;
if (!fut_.valid()) {
return {};
}
return fut_.get();
}
private:
bool is_consumed_;
std::future<T> fut_;
};
} // namespace database

@ -50,6 +50,7 @@ enum class Tag {
kAlbum = 2, kAlbum = 2,
kAlbumTrack = 3, kAlbumTrack = 3,
kGenre = 4, kGenre = 4,
kDuration = 5,
}; };
/* /*
@ -67,6 +68,8 @@ class TrackTags {
std::optional<int> sample_rate; std::optional<int> sample_rate;
std::optional<int> bits_per_sample; std::optional<int> bits_per_sample;
std::optional<int> duration;
auto set(const Tag& key, const std::string& val) -> void; auto set(const Tag& key, const std::string& val) -> void;
auto at(const Tag& key) const -> std::optional<shared_string>; auto at(const Tag& key) const -> std::optional<shared_string>;
auto operator[](const Tag& key) const -> std::optional<shared_string>; auto operator[](const Tag& key) const -> std::optional<shared_string>;

@ -156,6 +156,9 @@ auto TagParserImpl::ReadAndParseTags(const std::string& path, TrackTags* out)
if (ctx.bitrate > 0) { if (ctx.bitrate > 0) {
out->bits_per_sample = ctx.bitrate; out->bits_per_sample = ctx.bitrate;
} }
if (ctx.duration > 0) {
out->duration = ctx.duration;
}
return true; return true;
} }

@ -17,6 +17,7 @@
#include "spi.hpp" #include "spi.hpp"
#include "system_events.hpp" #include "system_events.hpp"
#include "system_fsm.hpp" #include "system_fsm.hpp"
#include "track_queue.hpp"
#include "ui_fsm.hpp" #include "ui_fsm.hpp"
#include "i2c.hpp" #include "i2c.hpp"
@ -48,8 +49,9 @@ auto Booting::entry() -> void {
sGpios->set_listener(&sGpiosCallback); sGpios->set_listener(&sGpiosCallback);
// Start bringing up LVGL now, since we have all of its prerequisites. // Start bringing up LVGL now, since we have all of its prerequisites.
sTrackQueue.reset(new audio::TrackQueue());
ESP_LOGI(kTag, "starting ui"); ESP_LOGI(kTag, "starting ui");
if (!ui::UiState::Init(sGpios.get())) { if (!ui::UiState::Init(sGpios.get(), sTrackQueue.get())) {
events::Dispatch<FatalError, SystemState, ui::UiState, audio::AudioState>( events::Dispatch<FatalError, SystemState, ui::UiState, audio::AudioState>(
FatalError()); FatalError());
return; return;
@ -70,7 +72,7 @@ auto Booting::entry() -> void {
// state machines and inform them that the system is ready. // state machines and inform them that the system is ready.
ESP_LOGI(kTag, "starting audio"); ESP_LOGI(kTag, "starting audio");
if (!audio::AudioState::Init(sGpios.get(), sDatabase)) { if (!audio::AudioState::Init(sGpios.get(), sDatabase, sTrackQueue.get())) {
events::Dispatch<FatalError, SystemState, ui::UiState, audio::AudioState>( events::Dispatch<FatalError, SystemState, ui::UiState, audio::AudioState>(
FatalError()); FatalError());
return; return;
@ -83,6 +85,7 @@ auto Booting::entry() -> void {
auto Booting::exit() -> void { auto Booting::exit() -> void {
// TODO(jacqueline): Gate this on something. Debug flag? Flashing mode? // TODO(jacqueline): Gate this on something. Debug flag? Flashing mode?
sAppConsole = new console::AppConsole(); sAppConsole = new console::AppConsole();
sAppConsole->sTrackQueue = sTrackQueue.get();
sAppConsole->Launch(); sAppConsole->Launch();
} }

@ -20,6 +20,7 @@
#include "touchwheel.hpp" #include "touchwheel.hpp"
#include "system_events.hpp" #include "system_events.hpp"
#include "track_queue.hpp"
namespace system_fsm { namespace system_fsm {
@ -58,6 +59,8 @@ class SystemState : public tinyfsm::Fsm<SystemState> {
static std::shared_ptr<drivers::Display> sDisplay; static std::shared_ptr<drivers::Display> sDisplay;
static std::shared_ptr<database::Database> sDatabase; static std::shared_ptr<database::Database> sDatabase;
static std::shared_ptr<audio::TrackQueue> sTrackQueue;
static console::AppConsole* sAppConsole; static console::AppConsole* sAppConsole;
}; };

@ -9,6 +9,7 @@
#include "event_queue.hpp" #include "event_queue.hpp"
#include "relative_wheel.hpp" #include "relative_wheel.hpp"
#include "system_events.hpp" #include "system_events.hpp"
#include "track_queue.hpp"
namespace system_fsm { namespace system_fsm {
@ -22,6 +23,8 @@ std::shared_ptr<drivers::SdStorage> SystemState::sStorage;
std::shared_ptr<drivers::Display> SystemState::sDisplay; std::shared_ptr<drivers::Display> SystemState::sDisplay;
std::shared_ptr<database::Database> SystemState::sDatabase; std::shared_ptr<database::Database> SystemState::sDatabase;
std::shared_ptr<audio::TrackQueue> SystemState::sTrackQueue;
console::AppConsole* SystemState::sAppConsole; console::AppConsole* SystemState::sAppConsole;
void SystemState::react(const FatalError& err) { void SystemState::react(const FatalError& err) {

@ -15,14 +15,24 @@
namespace ui { 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 { class Screen {
public: public:
Screen() : root_(lv_obj_create(NULL)), group_(lv_group_create()) {} Screen() : root_(lv_obj_create(NULL)), group_(lv_group_create()) {}
virtual ~Screen() { virtual ~Screen() {
lv_obj_del(root_); lv_obj_del(root_);
lv_group_del(group_); 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 {} virtual auto Tick() -> void {}
auto root() -> lv_obj_t* { return root_; } auto root() -> lv_obj_t* { return root_; }

@ -6,25 +6,55 @@
#pragma once #pragma once
#include <stdint.h>
#include <sys/_stdint.h>
#include <memory> #include <memory>
#include <vector>
#include "lvgl.h" #include "lvgl.h"
#include "database.hpp" #include "database.hpp"
#include "future_fetcher.hpp"
#include "screen.hpp" #include "screen.hpp"
#include "track.hpp"
#include "track_queue.hpp"
namespace ui { namespace ui {
namespace screens { namespace screens {
/*
* The 'Now Playing' / 'Currently Playing' screen that contains information
* about the current track, as well as playback controls.
*/
class Playing : public Screen { class Playing : public Screen {
public: public:
explicit Playing(database::Track t); explicit Playing(std::weak_ptr<database::Database> db,
audio::TrackQueue* queue);
~Playing(); ~Playing();
auto BindTrack(database::Track t) -> void; auto Tick() -> void override;
// 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: private:
database::Track track_; auto BindTrack(const database::Track& track) -> void;
auto ApplyNextUp(const std::vector<database::Track>& tracks) -> void;
std::weak_ptr<database::Database> db_;
audio::TrackQueue* queue_;
std::optional<database::Track> track_;
std::vector<database::Track> next_tracks_;
std::unique_ptr<database::FutureFetcher<std::optional<database::Track>>>
new_track_;
std::unique_ptr<
database::FutureFetcher<std::vector<std::optional<database::Track>>>>
new_next_tracks_;
lv_obj_t* artist_label_; lv_obj_t* artist_label_;
lv_obj_t* album_label_; lv_obj_t* album_label_;

@ -9,7 +9,9 @@
#include <memory> #include <memory>
#include <stack> #include <stack>
#include "audio_events.hpp"
#include "relative_wheel.hpp" #include "relative_wheel.hpp"
#include "screen_playing.hpp"
#include "tinyfsm.hpp" #include "tinyfsm.hpp"
#include "display.hpp" #include "display.hpp"
@ -17,13 +19,14 @@
#include "storage.hpp" #include "storage.hpp"
#include "system_events.hpp" #include "system_events.hpp"
#include "touchwheel.hpp" #include "touchwheel.hpp"
#include "track_queue.hpp"
#include "ui_events.hpp" #include "ui_events.hpp"
namespace ui { namespace ui {
class UiState : public tinyfsm::Fsm<UiState> { class UiState : public tinyfsm::Fsm<UiState> {
public: public:
static auto Init(drivers::IGpios* gpio_expander) -> bool; static auto Init(drivers::IGpios*, audio::TrackQueue*) -> bool;
virtual ~UiState() {} virtual ~UiState() {}
@ -37,10 +40,14 @@ class UiState : public tinyfsm::Fsm<UiState> {
/* Fallback event handler. Does nothing. */ /* Fallback event handler. Does nothing. */
void react(const tinyfsm::Event& ev) {} void react(const tinyfsm::Event& ev) {}
virtual void react(const system_fsm::KeyLockChanged&){}; virtual void react(const audio::PlaybackStarted&) {}
virtual void react(const audio::PlaybackUpdate&) {}
virtual void react(const audio::QueueUpdate&) {}
virtual void react(const internal::RecordSelected&){}; virtual void react(const system_fsm::KeyLockChanged&) {}
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::DisplayReady&) {}
virtual void react(const system_fsm::BootComplete&) {} virtual void react(const system_fsm::BootComplete&) {}
@ -48,8 +55,11 @@ class UiState : public tinyfsm::Fsm<UiState> {
protected: protected:
void PushScreen(std::shared_ptr<Screen>); void PushScreen(std::shared_ptr<Screen>);
void PopScreen();
static drivers::IGpios* sIGpios; static drivers::IGpios* sIGpios;
static audio::TrackQueue* sQueue;
static std::shared_ptr<drivers::TouchWheel> sTouchWheel; static std::shared_ptr<drivers::TouchWheel> sTouchWheel;
static std::shared_ptr<drivers::RelativeWheel> sRelativeWheel; static std::shared_ptr<drivers::RelativeWheel> sRelativeWheel;
static std::shared_ptr<drivers::Display> sDisplay; static std::shared_ptr<drivers::Display> sDisplay;
@ -68,7 +78,7 @@ class Splash : public UiState {
using UiState::react; using UiState::react;
}; };
class Interactive : public UiState { class Browse : public UiState {
void entry() override; void entry() override;
void react(const internal::RecordSelected&) override; void react(const internal::RecordSelected&) override;
@ -76,6 +86,17 @@ class Interactive : public UiState {
void react(const system_fsm::KeyLockChanged&) override; void react(const system_fsm::KeyLockChanged&) override;
void react(const system_fsm::StorageMounted&) 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::PlaybackStarted&) override;
void react(const audio::PlaybackUpdate&) override;
void react(const audio::QueueUpdate&) override;
using UiState::react;
}; };
class FatalError : public UiState {}; class FatalError : public UiState {};

@ -5,12 +5,17 @@
*/ */
#include "screen_playing.hpp" #include "screen_playing.hpp"
#include <sys/_stdint.h>
#include <memory>
#include "core/lv_obj.h" #include "core/lv_obj.h"
#include "core/lv_obj_tree.h"
#include "database.hpp"
#include "esp_log.h" #include "esp_log.h"
#include "extra/layouts/flex/lv_flex.h" #include "extra/layouts/flex/lv_flex.h"
#include "extra/layouts/grid/lv_grid.h" #include "extra/layouts/grid/lv_grid.h"
#include "font/lv_symbol_def.h" #include "font/lv_symbol_def.h"
#include "future_fetcher.hpp"
#include "lvgl.h" #include "lvgl.h"
#include "core/lv_group.h" #include "core/lv_group.h"
@ -19,8 +24,10 @@
#include "extra/widgets/list/lv_list.h" #include "extra/widgets/list/lv_list.h"
#include "extra/widgets/menu/lv_menu.h" #include "extra/widgets/menu/lv_menu.h"
#include "extra/widgets/spinner/lv_spinner.h" #include "extra/widgets/spinner/lv_spinner.h"
#include "future_fetcher.hpp"
#include "hal/lv_hal_disp.h" #include "hal/lv_hal_disp.h"
#include "index.hpp" #include "index.hpp"
#include "misc/lv_anim.h"
#include "misc/lv_area.h" #include "misc/lv_area.h"
#include "misc/lv_color.h" #include "misc/lv_color.h"
#include "track.hpp" #include "track.hpp"
@ -34,6 +41,8 @@
namespace ui { namespace ui {
namespace screens { namespace screens {
static constexpr std::size_t kMaxUpcoming = 10;
static lv_style_t scrubber_style; static lv_style_t scrubber_style;
auto info_label(lv_obj_t* parent) -> lv_obj_t* { auto info_label(lv_obj_t* parent) -> lv_obj_t* {
@ -59,7 +68,13 @@ auto next_up_label(lv_obj_t* parent, const std::string& text) -> lv_obj_t* {
return label; return label;
} }
Playing::Playing(database::Track track) : track_(track) { Playing::Playing(std::weak_ptr<database::Database> 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_layout(root_, LV_LAYOUT_FLEX);
lv_obj_set_size(root_, lv_pct(100), lv_pct(200)); lv_obj_set_size(root_, lv_pct(100), lv_pct(200));
lv_obj_set_flex_flow(root_, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(root_, LV_FLEX_FLOW_COLUMN);
@ -130,17 +145,72 @@ Playing::Playing(database::Track track) : track_(track) {
lv_obj_set_flex_align(next_up_container_, LV_FLEX_ALIGN_CENTER, lv_obj_set_flex_align(next_up_container_, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_END); LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_END);
lv_group_add_obj(group_, next_up_label(root_, "Song 2")); OnTrackUpdate();
lv_group_add_obj(group_, next_up_label(root_, "Song 3")); OnQueueUpdate();
lv_group_add_obj(
group_, next_up_label(root_, "Another song that has a very long name"));
BindTrack(track);
} }
Playing::~Playing() {} 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<std::optional<database::Track>>(
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<std::vector<std::optional<database::Track>>>(
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<database::Track> filtered;
for (const auto& t : *res) {
if (t) {
filtered.push_back(*t);
}
}
ApplyNextUp(filtered);
}
}
}
auto Playing::BindTrack(const database::Track& t) -> void {
track_ = t; track_ = t;
lv_label_set_text(artist_label_, lv_label_set_text(artist_label_,
@ -148,6 +218,25 @@ auto Playing::BindTrack(database::Track t) -> void {
lv_label_set_text(album_label_, lv_label_set_text(album_label_,
t.tags().at(database::Tag::kAlbum).value_or("").c_str()); t.tags().at(database::Tag::kAlbum).value_or("").c_str());
lv_label_set_text(title_label_, t.TitleOrFilename().c_str()); lv_label_set_text(title_label_, t.TitleOrFilename().c_str());
std::optional<int> 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::ApplyNextUp(const std::vector<database::Track>& 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 } // namespace screens

@ -19,6 +19,7 @@
#include "screen_track_browser.hpp" #include "screen_track_browser.hpp"
#include "system_events.hpp" #include "system_events.hpp"
#include "touchwheel.hpp" #include "touchwheel.hpp"
#include "track_queue.hpp"
namespace ui { namespace ui {
@ -27,6 +28,8 @@ static constexpr char kTag[] = "ui_fsm";
static const std::size_t kRecordsPerPage = 10; static const std::size_t kRecordsPerPage = 10;
drivers::IGpios* UiState::sIGpios; drivers::IGpios* UiState::sIGpios;
audio::TrackQueue* UiState::sQueue;
std::shared_ptr<drivers::TouchWheel> UiState::sTouchWheel; std::shared_ptr<drivers::TouchWheel> UiState::sTouchWheel;
std::shared_ptr<drivers::RelativeWheel> UiState::sRelativeWheel; std::shared_ptr<drivers::RelativeWheel> UiState::sRelativeWheel;
std::shared_ptr<drivers::Display> UiState::sDisplay; std::shared_ptr<drivers::Display> UiState::sDisplay;
@ -35,8 +38,10 @@ std::weak_ptr<database::Database> UiState::sDb;
std::stack<std::shared_ptr<Screen>> UiState::sScreens; std::stack<std::shared_ptr<Screen>> UiState::sScreens;
std::shared_ptr<Screen> UiState::sCurrentScreen; std::shared_ptr<Screen> UiState::sCurrentScreen;
auto UiState::Init(drivers::IGpios* gpio_expander) -> bool { auto UiState::Init(drivers::IGpios* gpio_expander, audio::TrackQueue* queue)
-> bool {
sIGpios = gpio_expander; sIGpios = gpio_expander;
sQueue = queue;
lv_init(); lv_init();
sDisplay.reset( sDisplay.reset(
@ -69,6 +74,14 @@ void UiState::PushScreen(std::shared_ptr<Screen> screen) {
sCurrentScreen = screen; sCurrentScreen = screen;
} }
void UiState::PopScreen() {
if (sScreens.empty()) {
return;
}
sCurrentScreen = sScreens.top();
sScreens.pop();
}
namespace states { namespace states {
void Splash::exit() { void Splash::exit() {
@ -78,16 +91,16 @@ void Splash::exit() {
} }
void Splash::react(const system_fsm::BootComplete& ev) { void Splash::react(const system_fsm::BootComplete& ev) {
transit<Interactive>(); transit<Browse>();
} }
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); sDisplay->SetDisplayOn(ev.falling);
} }
void Interactive::react(const system_fsm::StorageMounted& ev) { void Browse::react(const system_fsm::StorageMounted& ev) {
sDb = ev.db; sDb = ev.db;
auto db = ev.db.lock(); auto db = ev.db.lock();
if (!db) { if (!db) {
@ -97,7 +110,7 @@ void Interactive::react(const system_fsm::StorageMounted& ev) {
PushScreen(std::make_shared<screens::Menu>(db->GetIndexes())); PushScreen(std::make_shared<screens::Menu>(db->GetIndexes()));
} }
void Interactive::react(const internal::RecordSelected& ev) { void Browse::react(const internal::RecordSelected& ev) {
auto db = sDb.lock(); auto db = sDb.lock();
if (!db) { if (!db) {
return; return;
@ -106,12 +119,9 @@ void Interactive::react(const internal::RecordSelected& ev) {
if (ev.record.track()) { if (ev.record.track()) {
ESP_LOGI(kTag, "selected track '%s'", ev.record.text()->c_str()); ESP_LOGI(kTag, "selected track '%s'", ev.record.text()->c_str());
// TODO(jacqueline): We should also send some kind of playlist info here. // TODO(jacqueline): We should also send some kind of playlist info here.
auto track = ev.record.track().value(); sQueue->Clear();
events::Dispatch<audio::PlayTrack, audio::AudioState>(audio::PlayTrack{ sQueue->AddLast(ev.record.track()->data().id());
.id = track.data().id(), transit<Playing>();
.data = track.data(),
});
PushScreen(std::make_shared<screens::Playing>(track));
} else { } else {
ESP_LOGI(kTag, "selected record '%s'", ev.record.text()->c_str()); ESP_LOGI(kTag, "selected record '%s'", ev.record.text()->c_str());
auto cont = ev.record.Expand(kRecordsPerPage); auto cont = ev.record.Expand(kRecordsPerPage);
@ -125,7 +135,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(); auto db = sDb.lock();
if (!db) { if (!db) {
return; return;
@ -137,6 +147,30 @@ void Interactive::react(const internal::IndexSelected& ev) {
std::move(query))); std::move(query)));
} }
static std::shared_ptr<screens::Playing> 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 } // namespace states
} // namespace ui } // namespace ui

Loading…
Cancel
Save