summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app_console/app_console.cpp7
-rw-r--r--src/app_console/include/app_console.hpp2
-rw-r--r--src/audio/CMakeLists.txt2
-rw-r--r--src/audio/audio_decoder.cpp14
-rw-r--r--src/audio/audio_fsm.cpp82
-rw-r--r--src/audio/audio_task.cpp14
-rw-r--r--src/audio/fatfs_audio_input.cpp20
-rw-r--r--src/audio/include/audio_decoder.hpp3
-rw-r--r--src/audio/include/audio_events.hpp21
-rw-r--r--src/audio/include/audio_fsm.hpp33
-rw-r--r--src/audio/include/fatfs_audio_input.hpp2
-rw-r--r--src/audio/include/stream_info.hpp9
-rw-r--r--src/audio/include/track_queue.hpp85
-rw-r--r--src/audio/stream_info.cpp4
-rw-r--r--src/audio/track_queue.cpp128
-rw-r--r--src/codecs/foxenflac.cpp1
-rw-r--r--src/codecs/include/codec.hpp4
-rw-r--r--src/codecs/include/mad.hpp2
-rw-r--r--src/codecs/mad.cpp82
-rw-r--r--src/database/database.cpp56
-rw-r--r--src/database/include/database.hpp9
-rw-r--r--src/database/include/future_fetcher.hpp62
-rw-r--r--src/database/include/track.hpp3
-rw-r--r--src/database/tag_parser.cpp3
-rw-r--r--src/system_fsm/booting.cpp7
-rw-r--r--src/system_fsm/include/system_fsm.hpp3
-rw-r--r--src/system_fsm/system_fsm.cpp3
-rw-r--r--src/ui/include/screen.hpp10
-rw-r--r--src/ui/include/screen_playing.hpp36
-rw-r--r--src/ui/include/ui_fsm.hpp31
-rw-r--r--src/ui/screen_playing.cpp105
-rw-r--r--src/ui/ui_fsm.cpp60
32 files changed, 790 insertions, 113 deletions
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<database::Database> 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, audio::AudioState>(
- 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, audio::AudioState>(
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<database::Database> 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<StreamInfo::Encoded>(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<InputStream>& 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<InputStream>& 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..5f4f8783 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<FatfsAudioInput> AudioState::sFileSource;
std::unique_ptr<I2SAudioOutput> AudioState::sI2SOutput;
std::vector<std::unique_ptr<IAudioElement>> AudioState::sPipeline;
-std::deque<AudioState::EnqueuedItem> AudioState::sTrackQueue;
+TrackQueue* AudioState::sTrackQueue;
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;
+ sTrackQueue = queue;
auto dac = drivers::I2SDac::create(gpio_expander);
if (!dac) {
@@ -94,26 +97,23 @@ void Uninitialised::react(const system_fsm::BootComplete&) {
transit<Standby>();
}
-void Standby::react(const InputFileOpened& ev) {
+void Standby::react(const internal::InputFileOpened& ev) {
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();
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<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();
- if (!db) {
- ESP_LOGW(kTag, "database not open; ignoring play request");
- return;
- }
- sFileSource->OpenFile(
- db->GetTrackPath(std::get<database::TrackId>(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<Standby>();
}
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> pipeline, IAudioSink* sink) {
}
if (previously_had_work && !has_work) {
- events::Dispatch<AudioPipelineIdle, AudioState>({});
+ events::Dispatch<internal::AudioPipelineIdle, AudioState>({});
}
previously_had_work = has_work;
@@ -136,6 +137,10 @@ void AudioTaskMain(std::unique_ptr<Pipeline> 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<internal::InputFileFinished, AudioState>({});
+ }
+
current_second = 0;
previous_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;
}
if (previous_second != current_second) {
- events::Dispatch<PlaybackUpdate, AudioState>(
- {.seconds_elapsed = current_second});
+ events::Dispatch<PlaybackUpdate, AudioState, ui::UiState>({
+ .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<std::optional<std::string>>&& path)
}
auto FatfsAudioInput::OpenFile(const std::string& path) -> bool {
+ current_path_.reset();
if (is_file_open_) {
f_close(&current_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<uint32_t>(*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(&current_file_, path.c_str(), FA_READ);
@@ -104,7 +113,8 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool {
return false;
}
- events::Dispatch<InputFileOpened, AudioState>({});
+ events::Dispatch<internal::InputFileOpened, AudioState>({});
+ current_path_ = path;
is_file_open_ = true;
return true;
}
@@ -124,9 +134,10 @@ auto FatfsAudioInput::Process(const std::vector<InputStream>& 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<InputStream>& inputs,
f_close(&current_file_);
is_file_open_ = false;
+ current_path_.reset();
has_prepared_output_ = false;
output->mark_producer_finished();
- events::Dispatch<InputFileFinished, AudioState>({});
+ events::Dispatch<internal::InputFileClosed, AudioState>({});
}
}
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<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<std::size_t> duration_seconds_from_decoder_;
std::optional<std::size_t> 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<database::TrackData> 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<AudioState> {
public:
static auto Init(drivers::IGpios* gpio_expander,
- std::weak_ptr<database::Database>) -> bool;
+ std::weak_ptr<database::Database>,
+ TrackQueue* queue) -> bool;
virtual ~AudioState() {}
@@ -45,14 +47,14 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
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<AudioState> {
static std::unique_ptr<I2SAudioOutput> sI2SOutput;
static std::vector<std::unique_ptr<IAudioElement>> sPipeline;
- typedef std::variant<database::TrackId, std::string> EnqueuedItem;
- static std::deque<EnqueuedItem> 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<std::string> { return current_path_; }
auto OpenFile(std::future<std::optional<std::string>>&& path) -> void;
auto OpenFile(const std::string& path) -> bool;
@@ -50,6 +51,7 @@ class FatfsAudioInput : public IAudioElement {
-> std::optional<codecs::StreamType>;
std::optional<std::future<std::optional<std::string>>> pending_path_;
+ std::optional<std::string> 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<uint32_t> seek_to_seconds{};
+ std::optional<std::uint32_t> duration_seconds;
+
+ std::optional<std::uint32_t> seek_to_seconds{};
struct Encoded {
// The codec that this stream is associated with.
codecs::StreamType type;
+ std::optional<std::size_t> 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<std::byte> 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 <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
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 <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
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<const std::byte> input)
.num_channels = static_cast<uint8_t>(channels),
.bits_per_sample = 32, // libfoxenflac output is fixed-size.
.sample_rate_hz = static_cast<uint32_t>(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 <stdint.h>
+#include <sys/_stdint.h>
#include <cstddef>
#include <cstdint>
@@ -50,6 +51,9 @@ class ICodec {
uint8_t num_channels;
uint8_t bits_per_sample;
uint32_t sample_rate_hz;
+
+ std::optional<uint32_t> duration_seconds;
+ std::optional<uint32_t> bits_per_second;
};
/*
diff --git a/src/codecs/include/mad.hpp b/src/codecs/include/mad.hpp
index d2643230..fbae560c 100644
--- a/src/codecs/include/mad.hpp
+++ b/src/codecs/include/mad.hpp
@@ -42,6 +42,8 @@ class MadMp3Decoder : public ICodec {
-> Result<void> override;
private:
+ auto GetVbrLength(const mad_header& header) -> std::optional<uint32_t>;
+
mad_stream stream_;
mad_frame frame_;
mad_synth synth_;
diff --git a/src/codecs/mad.cpp b/src/codecs/mad.cpp
index 23b4ccf6..ca9a0f6e 100644
--- a/src/codecs/mad.cpp
+++ b/src/codecs/mad.cpp
@@ -6,8 +6,10 @@
#include "mad.hpp"
#include <stdint.h>
+#include <sys/_stdint.h>
#include <cstdint>
+#include <cstring>
#include <optional>
#include "mad.h"
@@ -79,12 +81,22 @@ auto MadMp3Decoder::BeginStream(const cpp::span<const std::byte> 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 = {},
+ };
+
+ auto vbr_length = GetVbrLength(header);
+ if (vbr_length) {
+ output.duration_seconds = vbr_length;
+ } else {
+ output.bits_per_second = header.bitrate;
+ }
+
+ return {GetBytesUsed(input.size_bytes()), output};
}
auto MadMp3Decoder::ContinueStream(cpp::span<const std::byte> input,
@@ -219,4 +231,62 @@ auto MadMp3Decoder::SeekStream(cpp::span<const std::byte> input,
}
}
+/*
+ * Implementation taken from SDL_mixer and modified. Original is zlib-licensed,
+ * copyright (C) 1997-2022 Sam Lantinga <slouken@libsdl.org>
+ */
+auto MadMp3Decoder::GetVbrLength(const mad_header& header)
+ -> std::optional<uint32_t> {
+ if (!stream_.this_frame || !stream_.next_frame ||
+ stream_.next_frame <= stream_.this_frame ||
+ (stream_.next_frame - stream_.this_frame) < 48) {
+ return {};
+ }
+
+ int mpeg_version = (stream_.this_frame[1] >> 3) & 0x03;
+
+ int xing_offset = 0;
+ switch (mpeg_version) {
+ case 0x03: /* MPEG1 */
+ if (header.mode == MAD_MODE_SINGLE_CHANNEL) {
+ xing_offset = 4 + 17;
+ } else {
+ xing_offset = 4 + 32;
+ }
+ break;
+ default: /* MPEG2 and MPEG2.5 */
+ if (header.mode == MAD_MODE_SINGLE_CHANNEL) {
+ xing_offset = 4 + 17;
+ } else {
+ xing_offset = 4 + 9;
+ }
+ break;
+ }
+
+ uint32_t samples_per_frame = 32 * MAD_NSBSAMPLES(&header);
+
+ unsigned char const* frames_count_raw;
+ uint32_t frames_count = 0;
+ if (std::memcmp(stream_.this_frame + xing_offset, "Xing", 4) == 0 ||
+ std::memcmp(stream_.this_frame + xing_offset, "Info", 4) == 0) {
+ /* Xing header to get the count of frames for VBR */
+ frames_count_raw = stream_.this_frame + xing_offset + 8;
+ frames_count = ((uint32_t)frames_count_raw[0] << 24) +
+ ((uint32_t)frames_count_raw[1] << 16) +
+ ((uint32_t)frames_count_raw[2] << 8) +
+ ((uint32_t)frames_count_raw[3]);
+ } else if (std::memcmp(stream_.this_frame + xing_offset, "VBRI", 4) == 0) {
+ /* VBRI header to get the count of frames for VBR */
+ frames_count_raw = stream_.this_frame + xing_offset + 14;
+ frames_count = ((uint32_t)frames_count_raw[0] << 24) +
+ ((uint32_t)frames_count_raw[1] << 16) +
+ ((uint32_t)frames_count_raw[2] << 8) +
+ ((uint32_t)frames_count_raw[3]);
+ } else {
+ return {};
+ }
+
+ return (double)(frames_count * samples_per_frame) / header.samplerate;
+}
+
} // namespace codecs
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<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> {
// 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<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 GetTracksByIndex(const IndexInfo& index, std::size_t page_size)
-> std::future<Result<IndexRecord>*>;
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 <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
diff --git a/src/database/include/track.hpp b/src/database/include/track.hpp
index e3f94db4..620fc59e 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,
};
/*
@@ -67,6 +68,8 @@ class TrackTags {
std::optional<int> sample_rate;
std::optional<int> bits_per_sample;
+ std::optional<int> duration;
+
auto set(const Tag& key, const std::string& val) -> void;
auto at(const Tag& key) const -> std::optional<shared_string>;
auto operator[](const Tag& key) const -> std::optional<shared_string>;
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, SystemState, ui::UiState, audio::AudioState>(
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, SystemState, ui::UiState, audio::AudioState>(
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<SystemState> {
static std::shared_ptr<drivers::Display> sDisplay;
static std::shared_ptr<database::Database> sDatabase;
+ static std::shared_ptr<audio::TrackQueue> 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<drivers::SdStorage> SystemState::sStorage;
std::shared_ptr<drivers::Display> SystemState::sDisplay;
std::shared_ptr<database::Database> SystemState::sDatabase;
+std::shared_ptr<audio::TrackQueue> 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 3eae32a7..148f2774 100644
--- a/src/ui/include/screen_playing.hpp
+++ b/src/ui/include/screen_playing.hpp
@@ -6,25 +6,55 @@
#pragma once
+#include <stdint.h>
+#include <sys/_stdint.h>
#include <memory>
+#include <vector>
#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<database::Database> db,
+ audio::TrackQueue* queue);
~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:
- 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* album_label_;
diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp
index 32275fab..2fc6db4e 100644
--- a/src/ui/include/ui_fsm.hpp
+++ b/src/ui/include/ui_fsm.hpp
@@ -9,7 +9,9 @@
#include <memory>
#include <stack>
+#include "audio_events.hpp"
#include "relative_wheel.hpp"
+#include "screen_playing.hpp"
#include "tinyfsm.hpp"
#include "display.hpp"
@@ -17,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<UiState> {
public:
- static auto Init(drivers::IGpios* gpio_expander) -> bool;
+ static auto Init(drivers::IGpios*, audio::TrackQueue*) -> bool;
virtual ~UiState() {}
@@ -37,10 +40,14 @@ class UiState : public tinyfsm::Fsm<UiState> {
/* Fallback event handler. Does nothing. */
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 internal::IndexSelected&){};
+ virtual void react(const system_fsm::KeyLockChanged&) {}
+
+ 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&) {}
@@ -48,8 +55,11 @@ class UiState : public tinyfsm::Fsm<UiState> {
protected:
void PushScreen(std::shared_ptr<Screen>);
+ void PopScreen();
static drivers::IGpios* sIGpios;
+ static audio::TrackQueue* sQueue;
+
static std::shared_ptr<drivers::TouchWheel> sTouchWheel;
static std::shared_ptr<drivers::RelativeWheel> sRelativeWheel;
static std::shared_ptr<drivers::Display> sDisplay;
@@ -68,7 +78,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;
@@ -76,6 +86,17 @@ class Interactive : 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::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 1ac8ad5a..50b1d33a 100644
--- a/src/ui/screen_playing.cpp
+++ b/src/ui/screen_playing.cpp
@@ -5,12 +5,17 @@
*/
#include "screen_playing.hpp"
+#include <sys/_stdint.h>
+#include <memory>
#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"
@@ -19,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"
@@ -34,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* {
@@ -59,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<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_size(root_, lv_pct(100), lv_pct(200));
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_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);
+ 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<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;
lv_label_set_text(artist_label_,
@@ -148,6 +218,25 @@ 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());
+
+ 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
diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp
index 58b1f641..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<drivers::TouchWheel> UiState::sTouchWheel;
std::shared_ptr<drivers::RelativeWheel> UiState::sRelativeWheel;
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::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;
+ sQueue = queue;
lv_init();
sDisplay.reset(
@@ -69,6 +74,14 @@ void UiState::PushScreen(std::shared_ptr<Screen> screen) {
sCurrentScreen = screen;
}
+void UiState::PopScreen() {
+ if (sScreens.empty()) {
+ return;
+ }
+ sCurrentScreen = sScreens.top();
+ sScreens.pop();
+}
+
namespace states {
void Splash::exit() {
@@ -78,16 +91,16 @@ void Splash::exit() {
}
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);
}
-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 +110,7 @@ void Interactive::react(const system_fsm::StorageMounted& ev) {
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();
if (!db) {
return;
@@ -106,12 +119,9 @@ void Interactive::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, audio::AudioState>(audio::PlayTrack{
- .id = track.data().id(),
- .data = track.data(),
- });
- PushScreen(std::make_shared<screens::Playing>(track));
+ sQueue->Clear();
+ sQueue->AddLast(ev.record.track()->data().id());
+ transit<Playing>();
} else {
ESP_LOGI(kTag, "selected record '%s'", ev.record.text()->c_str());
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();
if (!db) {
return;
@@ -137,6 +147,30 @@ void Interactive::react(const internal::IndexSelected& ev) {
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 ui