Merge branch 'main' of git.sr.ht:~jacqueline/tangara-fw
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
+45
-37
@@ -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 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>();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(¤t_file_);
|
||||
is_file_open_ = false;
|
||||
@@ -68,6 +69,11 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool {
|
||||
|
||||
ESP_LOGI(kTag, "opening file %s", path.c_str());
|
||||
|
||||
FILINFO info;
|
||||
if (f_stat(path.c_str(), &info) != FR_OK) {
|
||||
ESP_LOGE(kTag, "failed to stat file");
|
||||
}
|
||||
|
||||
database::TagParserImpl tag_parser;
|
||||
database::TrackTags tags;
|
||||
if (!tag_parser.ReadAndParseTags(path, &tags)) {
|
||||
@@ -95,7 +101,10 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool {
|
||||
.sample_rate = static_cast<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(¤t_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(¤t_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>({});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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 InputFileOpened : tinyfsm::Event {};
|
||||
struct InputFileFinished : tinyfsm::Event {};
|
||||
struct AudioPipelineIdle : tinyfsm::Event {};
|
||||
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 {};
|
||||
|
||||
} // namespace internal
|
||||
|
||||
} // namespace audio
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
void OutputStream::set_duration(std::size_t seconds) {
|
||||
raw_->info->duration_seconds = seconds;
|
||||
}
|
||||
|
||||
const StreamInfo& OutputStream::info() const {
|
||||
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),
|
||||
.bits_per_sample = 32, // libfoxenflac output is fixed-size.
|
||||
.sample_rate_hz = static_cast<uint32_t>(fs),
|
||||
.duration_seconds = {},
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
@@ -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_;
|
||||
|
||||
+76
-6
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>*>;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "database.hpp"
|
||||
|
||||
namespace database {
|
||||
|
||||
/*
|
||||
* Utility to simplify waiting for a std::future to complete without blocking.
|
||||
* Each instance is good for a single future, and does not directly own anything
|
||||
* other than the future itself.
|
||||
*/
|
||||
template <typename T>
|
||||
class FutureFetcher {
|
||||
public:
|
||||
explicit FutureFetcher(std::future<T>&& fut)
|
||||
: is_consumed_(false), fut_(std::move(fut)) {}
|
||||
|
||||
/*
|
||||
* Returns whether or not the underlying future is still awaiting async work.
|
||||
*/
|
||||
auto Finished() -> bool {
|
||||
if (!fut_.valid()) {
|
||||
return true;
|
||||
}
|
||||
if (fut_.wait_for(std::chrono::seconds(0)) != std::future_status::ready) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns the result of the future, and releases ownership of the underling
|
||||
* resource. Will return an absent value if the future became invalid (e.g.
|
||||
* the promise associated with it was destroyed.)
|
||||
*/
|
||||
auto Result() -> std::optional<T> {
|
||||
assert(!is_consumed_);
|
||||
if (is_consumed_) {
|
||||
return {};
|
||||
}
|
||||
is_consumed_ = true;
|
||||
if (!fut_.valid()) {
|
||||
return {};
|
||||
}
|
||||
return fut_.get();
|
||||
}
|
||||
|
||||
private:
|
||||
bool is_consumed_;
|
||||
std::future<T> fut_;
|
||||
};
|
||||
|
||||
} // namespace database
|
||||
@@ -50,6 +50,7 @@ enum class Tag {
|
||||
kAlbum = 2,
|
||||
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>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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
|
||||
|
||||
+47
-13
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user