wire up the playing screen with some real data
Includes implementing song duration calculation for CBR MP3 files
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
+41
-33
@@ -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 AudioPipelineIdle& ev) {
|
void Playback::react(const internal::InputFileFinished& ev) {
|
||||||
|
ESP_LOGI(kTag, "finished playing file");
|
||||||
|
sTrackQueue->Next();
|
||||||
|
}
|
||||||
|
|
||||||
|
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(¤t_file_);
|
f_close(¤t_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(¤t_file_, path.c_str(), FA_READ);
|
FRESULT res = f_open(¤t_file_, path.c_str(), FA_READ);
|
||||||
@@ -104,7 +113,8 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool {
|
|||||||
return false;
|
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(¤t_file_);
|
f_close(¤t_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 InputFileOpened : tinyfsm::Event {};
|
struct QueueUpdate : tinyfsm::Event {};
|
||||||
struct InputFileFinished : tinyfsm::Event {};
|
|
||||||
struct AudioPipelineIdle : tinyfsm::Event {};
|
|
||||||
|
|
||||||
struct VolumeChanged : tinyfsm::Event {};
|
struct VolumeChanged : tinyfsm::Event {};
|
||||||
|
|
||||||
|
namespace internal {
|
||||||
|
|
||||||
|
struct InputFileOpened : tinyfsm::Event {};
|
||||||
|
struct InputFileClosed : tinyfsm::Event {};
|
||||||
|
struct InputFileFinished : tinyfsm::Event {};
|
||||||
|
|
||||||
|
struct AudioPipelineIdle : 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
+11
-3
@@ -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
|
||||||
@@ -68,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_; }
|
||||||
|
|||||||
@@ -7,30 +7,54 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include <sys/_stdint.h>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <vector>
|
#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.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;
|
||||||
|
|
||||||
auto UpdateTime(uint32_t) -> void;
|
// Callbacks invoked by the UI state machine in response to audio events.
|
||||||
auto UpdateNextUp(std::vector<database::Track> tracks) -> void;
|
|
||||||
|
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_;
|
||||||
@@ -40,7 +64,6 @@ class Playing : public Screen {
|
|||||||
lv_obj_t* play_pause_control_;
|
lv_obj_t* play_pause_control_;
|
||||||
|
|
||||||
lv_obj_t* next_up_container_;
|
lv_obj_t* next_up_container_;
|
||||||
std::vector<database::Track> next_tracks_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace screens
|
} // namespace screens
|
||||||
|
|||||||
@@ -19,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() {}
|
||||||
|
|
||||||
@@ -39,12 +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 audio::PlaybackUpdate){};
|
virtual void react(const audio::PlaybackStarted&) {}
|
||||||
|
virtual void react(const audio::PlaybackUpdate&) {}
|
||||||
|
virtual void react(const audio::QueueUpdate&) {}
|
||||||
|
|
||||||
virtual void react(const system_fsm::KeyLockChanged&){};
|
virtual void react(const system_fsm::KeyLockChanged&) {}
|
||||||
|
|
||||||
virtual void react(const internal::RecordSelected&){};
|
virtual void react(const internal::RecordSelected&) {}
|
||||||
virtual void react(const internal::IndexSelected&){};
|
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&) {}
|
||||||
@@ -52,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;
|
||||||
@@ -61,7 +67,6 @@ class UiState : public tinyfsm::Fsm<UiState> {
|
|||||||
|
|
||||||
static std::stack<std::shared_ptr<Screen>> sScreens;
|
static std::stack<std::shared_ptr<Screen>> sScreens;
|
||||||
static std::shared_ptr<Screen> sCurrentScreen;
|
static std::shared_ptr<Screen> sCurrentScreen;
|
||||||
static std::unique_ptr<screens::Playing> sPlayingScreen;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace states {
|
namespace states {
|
||||||
@@ -81,12 +86,17 @@ class Browse : 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 {
|
class Playing : public UiState {
|
||||||
void entry() override;
|
void entry() override;
|
||||||
|
void exit() override;
|
||||||
|
|
||||||
void react(const audio::PlaybackUpdate) override;
|
void react(const audio::PlaybackStarted&) override;
|
||||||
|
void react(const audio::PlaybackUpdate&) override;
|
||||||
|
void react(const audio::QueueUpdate&) override;
|
||||||
|
using UiState::react;
|
||||||
};
|
};
|
||||||
|
|
||||||
class FatalError : public UiState {};
|
class FatalError : public UiState {};
|
||||||
|
|||||||
+82
-11
@@ -5,13 +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 "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"
|
||||||
@@ -20,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"
|
||||||
@@ -35,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* {
|
||||||
@@ -60,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);
|
||||||
@@ -131,12 +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);
|
||||||
|
|
||||||
BindTrack(track);
|
OnTrackUpdate();
|
||||||
|
OnQueueUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
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_,
|
||||||
@@ -145,16 +219,12 @@ auto Playing::BindTrack(database::Track t) -> void {
|
|||||||
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());
|
||||||
|
|
||||||
// TODO.
|
std::optional<int> duration = t.tags().duration;
|
||||||
lv_bar_set_range(scrubber_, 0, 0);
|
lv_bar_set_range(scrubber_, 0, duration.value_or(1));
|
||||||
lv_bar_set_value(scrubber_, 0, LV_ANIM_OFF);
|
lv_bar_set_value(scrubber_, 0, LV_ANIM_OFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Playing::UpdateTime(uint32_t time) -> void {
|
auto Playing::ApplyNextUp(const std::vector<database::Track>& tracks) -> void {
|
||||||
lv_bar_set_value(scrubber_, time, LV_ANIM_OFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto Playing::UpdateNextUp(std::vector<database::Track> tracks) -> void {
|
|
||||||
// TODO(jacqueline): Do a proper diff to maintain selection.
|
// TODO(jacqueline): Do a proper diff to maintain selection.
|
||||||
int children = lv_obj_get_child_cnt(next_up_container_);
|
int children = lv_obj_get_child_cnt(next_up_container_);
|
||||||
while (children > 0) {
|
while (children > 0) {
|
||||||
@@ -164,7 +234,8 @@ auto Playing::UpdateNextUp(std::vector<database::Track> tracks) -> void {
|
|||||||
|
|
||||||
next_tracks_ = tracks;
|
next_tracks_ = tracks;
|
||||||
for (const auto& track : next_tracks_) {
|
for (const auto& track : next_tracks_) {
|
||||||
lv_group_add_obj(group_, next_up_label(next_up_container_, track.TitleOrFilename()));
|
lv_group_add_obj(
|
||||||
|
group_, next_up_label(next_up_container_, track.TitleOrFilename()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+39
-10
@@ -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;
|
||||||
@@ -34,10 +37,11 @@ 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;
|
||||||
std::unique_ptr<screens::Playing> UiState::sPlayingScreen;
|
|
||||||
|
|
||||||
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(
|
||||||
@@ -70,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() {
|
||||||
@@ -107,12 +119,9 @@ void Browse::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);
|
||||||
@@ -138,8 +147,28 @@ void Browse::react(const internal::IndexSelected& ev) {
|
|||||||
std::move(query)));
|
std::move(query)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Playing::react(const audio::PlaybackUpdate ev) {
|
static std::shared_ptr<screens::Playing> sPlayingScreen;
|
||||||
sPlayingScreen->UpdateTime(ev.seconds_elapsed);
|
|
||||||
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user