wire up the playing screen with some real data

Includes implementing song duration calculation for CBR MP3 files
custom
jacqueline 2 years ago
parent 2f16d23002
commit 39f7545cd5
  1. 7
      src/app_console/app_console.cpp
  2. 2
      src/app_console/include/app_console.hpp
  3. 2
      src/audio/CMakeLists.txt
  4. 14
      src/audio/audio_decoder.cpp
  5. 82
      src/audio/audio_fsm.cpp
  6. 14
      src/audio/audio_task.cpp
  7. 20
      src/audio/fatfs_audio_input.cpp
  8. 3
      src/audio/include/audio_decoder.hpp
  9. 21
      src/audio/include/audio_events.hpp
  10. 33
      src/audio/include/audio_fsm.hpp
  11. 2
      src/audio/include/fatfs_audio_input.hpp
  12. 9
      src/audio/include/stream_info.hpp
  13. 85
      src/audio/include/track_queue.hpp
  14. 4
      src/audio/stream_info.cpp
  15. 128
      src/audio/track_queue.cpp
  16. 1
      src/codecs/foxenflac.cpp
  17. 4
      src/codecs/include/codec.hpp
  18. 20
      src/codecs/mad.cpp
  19. 56
      src/database/database.cpp
  20. 9
      src/database/include/database.hpp
  21. 62
      src/database/include/future_fetcher.hpp
  22. 2
      src/database/include/track.hpp
  23. 3
      src/database/tag_parser.cpp
  24. 7
      src/system_fsm/booting.cpp
  25. 3
      src/system_fsm/include/system_fsm.hpp
  26. 3
      src/system_fsm/system_fsm.cpp
  27. 10
      src/ui/include/screen.hpp
  28. 35
      src/ui/include/screen_playing.hpp
  29. 24
      src/ui/include/ui_fsm.hpp
  30. 95
      src/ui/screen_playing.cpp
  31. 49
      src/ui/ui_fsm.cpp

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

@ -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.

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

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

@ -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 QueueUpdate : tinyfsm::Event {};
struct VolumeChanged : tinyfsm::Event {};
namespace internal {
struct InputFileOpened : tinyfsm::Event {};
struct InputFileClosed : tinyfsm::Event {};
struct InputFileFinished : tinyfsm::Event {};
struct AudioPipelineIdle : tinyfsm::Event {};
struct VolumeChanged : tinyfsm::Event {};
} // namespace internal
} // namespace audio

@ -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;
};
/*

@ -6,6 +6,7 @@
#include "mad.hpp"
#include <stdint.h>
#include <sys/_stdint.h>
#include <cstdint>
#include <optional>
@ -79,12 +80,19 @@ 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 = {},
};
// 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,

@ -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

@ -68,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_; }

@ -7,30 +7,54 @@
#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;
auto UpdateTime(uint32_t) -> void;
auto UpdateNextUp(std::vector<database::Track> tracks) -> void;
// Callbacks invoked by the UI state machine in response to audio events.
auto OnTrackUpdate() -> void;
auto OnPlaybackUpdate(uint32_t, uint32_t) -> void;
auto OnQueueUpdate() -> void;
private:
database::Track track_;
auto BindTrack(const database::Track& track) -> void;
auto ApplyNextUp(const std::vector<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_;
@ -40,7 +64,6 @@ class Playing : public Screen {
lv_obj_t* play_pause_control_;
lv_obj_t* next_up_container_;
std::vector<database::Track> next_tracks_;
};
} // namespace screens

@ -19,13 +19,14 @@
#include "storage.hpp"
#include "system_events.hpp"
#include "touchwheel.hpp"
#include "track_queue.hpp"
#include "ui_events.hpp"
namespace ui {
class UiState : public tinyfsm::Fsm<UiState> {
public:
static auto Init(drivers::IGpios* gpio_expander) -> bool;
static auto Init(drivers::IGpios*, audio::TrackQueue*) -> bool;
virtual ~UiState() {}
@ -39,12 +40,14 @@ class UiState : public tinyfsm::Fsm<UiState> {
/* Fallback event handler. Does nothing. */
void react(const tinyfsm::Event& ev) {}
virtual void react(const audio::PlaybackUpdate){};
virtual void react(const audio::PlaybackStarted&) {}
virtual void react(const audio::PlaybackUpdate&) {}
virtual void react(const audio::QueueUpdate&) {}
virtual void react(const system_fsm::KeyLockChanged&){};
virtual void react(const system_fsm::KeyLockChanged&) {}
virtual void react(const internal::RecordSelected&){};
virtual void react(const internal::IndexSelected&){};
virtual void react(const internal::RecordSelected&) {}
virtual void react(const internal::IndexSelected&) {}
virtual void react(const system_fsm::DisplayReady&) {}
virtual void react(const system_fsm::BootComplete&) {}
@ -52,8 +55,11 @@ class UiState : public tinyfsm::Fsm<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;
@ -61,7 +67,6 @@ class UiState : public tinyfsm::Fsm<UiState> {
static std::stack<std::shared_ptr<Screen>> sScreens;
static std::shared_ptr<Screen> sCurrentScreen;
static std::unique_ptr<screens::Playing> sPlayingScreen;
};
namespace states {
@ -81,12 +86,17 @@ class Browse : public UiState {
void react(const system_fsm::KeyLockChanged&) override;
void react(const system_fsm::StorageMounted&) override;
using UiState::react;
};
class Playing : public UiState {
void entry() override;
void exit() override;
void react(const audio::PlaybackUpdate) override;
void react(const audio::PlaybackStarted&) override;
void react(const audio::PlaybackUpdate&) override;
void react(const audio::QueueUpdate&) override;
using UiState::react;
};
class FatalError : public UiState {};

@ -5,13 +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"
@ -20,8 +24,10 @@
#include "extra/widgets/list/lv_list.h"
#include "extra/widgets/menu/lv_menu.h"
#include "extra/widgets/spinner/lv_spinner.h"
#include "future_fetcher.hpp"
#include "hal/lv_hal_disp.h"
#include "index.hpp"
#include "misc/lv_anim.h"
#include "misc/lv_area.h"
#include "misc/lv_color.h"
#include "track.hpp"
@ -35,6 +41,8 @@
namespace ui {
namespace screens {
static constexpr std::size_t kMaxUpcoming = 10;
static lv_style_t scrubber_style;
auto info_label(lv_obj_t* parent) -> lv_obj_t* {
@ -60,7 +68,13 @@ auto next_up_label(lv_obj_t* parent, const std::string& text) -> lv_obj_t* {
return label;
}
Playing::Playing(database::Track track) : track_(track) {
Playing::Playing(std::weak_ptr<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);
@ -131,12 +145,72 @@ Playing::Playing(database::Track track) : track_(track) {
lv_obj_set_flex_align(next_up_container_, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_END);
BindTrack(track);
OnTrackUpdate();
OnQueueUpdate();
}
Playing::~Playing() {}
auto Playing::BindTrack(database::Track t) -> void {
auto Playing::OnTrackUpdate() -> void {
auto current = queue_->GetCurrent();
if (!current) {
return;
}
if (track_ && track_->data().id() == *current) {
return;
}
auto db = db_.lock();
if (!db) {
return;
}
new_track_.reset(new database::FutureFetcher<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_,
@ -145,16 +219,12 @@ auto Playing::BindTrack(database::Track t) -> void {
t.tags().at(database::Tag::kAlbum).value_or("").c_str());
lv_label_set_text(title_label_, t.TitleOrFilename().c_str());
// TODO.
lv_bar_set_range(scrubber_, 0, 0);
std::optional<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::UpdateTime(uint32_t time) -> void {
lv_bar_set_value(scrubber_, time, LV_ANIM_OFF);
}
auto Playing::UpdateNextUp(std::vector<database::Track> tracks) -> void {
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) {
@ -163,8 +233,9 @@ auto Playing::UpdateNextUp(std::vector<database::Track> tracks) -> void {
}
next_tracks_ = tracks;
for (const auto &track : next_tracks_) {
lv_group_add_obj(group_, next_up_label(next_up_container_, track.TitleOrFilename()));
for (const auto& track : next_tracks_) {
lv_group_add_obj(
group_, next_up_label(next_up_container_, track.TitleOrFilename()));
}
}

@ -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;
@ -34,10 +37,11 @@ std::weak_ptr<database::Database> UiState::sDb;
std::stack<std::shared_ptr<Screen>> UiState::sScreens;
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;
sQueue = queue;
lv_init();
sDisplay.reset(
@ -70,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() {
@ -107,12 +119,9 @@ void Browse::react(const internal::RecordSelected& ev) {
if (ev.record.track()) {
ESP_LOGI(kTag, "selected track '%s'", ev.record.text()->c_str());
// TODO(jacqueline): We should also send some kind of playlist info here.
auto track = ev.record.track().value();
events::Dispatch<audio::PlayTrack, 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);
@ -138,8 +147,28 @@ void Browse::react(const internal::IndexSelected& ev) {
std::move(query)));
}
void Playing::react(const audio::PlaybackUpdate ev) {
sPlayingScreen->UpdateTime(ev.seconds_elapsed);
static std::shared_ptr<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

Loading…
Cancel
Save