From 62f6179abe24339c2e5b7350528afbcad4c52067 Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 15 Feb 2024 16:12:07 +1100 Subject: Added offset for track seeking, wav impl. only rn --- src/audio/audio_decoder.cpp | 11 +++++++---- src/audio/audio_fsm.cpp | 12 ++++++++++-- src/audio/audio_source.cpp | 9 +++++++-- src/audio/fatfs_audio_input.cpp | 8 ++++---- src/audio/include/audio_decoder.hpp | 2 +- src/audio/include/audio_events.hpp | 5 +++++ src/audio/include/audio_fsm.hpp | 3 +++ src/audio/include/audio_source.hpp | 6 +++++- src/audio/include/fatfs_audio_input.hpp | 4 ++-- src/codecs/include/codec.hpp | 2 +- src/codecs/include/mad.hpp | 2 +- src/codecs/include/miniflac.hpp | 2 +- src/codecs/include/opus.hpp | 2 +- src/codecs/include/vorbis.hpp | 2 +- src/codecs/include/wav.hpp | 2 +- src/codecs/mad.cpp | 2 +- src/codecs/miniflac.cpp | 2 +- src/codecs/opus.cpp | 2 +- src/codecs/vorbis.cpp | 2 +- src/codecs/wav.cpp | 6 ++++-- 20 files changed, 58 insertions(+), 28 deletions(-) (limited to 'src') diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp index b0a973d9..02cf27e3 100644 --- a/src/audio/audio_decoder.cpp +++ b/src/audio/audio_decoder.cpp @@ -51,9 +51,10 @@ static constexpr std::size_t kCodecBufferLength = drivers::kI2SBufferLengthFrames * sizeof(sample::Sample); Timer::Timer(std::shared_ptr t, - const codecs::ICodec::OutputFormat& format) + const codecs::ICodec::OutputFormat& format, + uint32_t current_seconds) : track_(t), - current_seconds_(0), + current_seconds_(current_seconds), current_sample_in_second_(0), samples_per_second_(format.sample_rate_hz * format.num_channels), total_duration_seconds_(format.total_samples.value_or(0) / @@ -131,7 +132,7 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { return false; } - auto open_res = codec_->OpenStream(stream); + auto open_res = codec_->OpenStream(stream, stream->Offset()); if (open_res.has_error()) { ESP_LOGE(kTag, "codec failed to start: %s", codecs::ICodec::ErrorString(open_res.error()).c_str()); @@ -147,6 +148,7 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { ESP_LOGI(kTag, "stream started ok"); events::Audio().Dispatch(internal::InputFileOpened{}); + // TODO: How does this need to change? auto tags = std::make_shared(Track{ .tags = stream->tags(), .db_info = {}, @@ -155,7 +157,8 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { }); timer_.reset(new Timer(tags, open_res.value())); - PlaybackUpdate ev{.seconds_elapsed = 0, .track = tags}; + // TODO: How does *this?* need to change? + PlaybackUpdate ev{.seconds_elapsed = stream->Offset(), .track = tags}; events::Audio().Dispatch(ev); events::Ui().Dispatch(ev); diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index ba6e5ffe..c67cfc7a 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -244,11 +244,19 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { } void Standby::react(const PlayFile& ev) { - sFileSource->SetPath(ev.filename); + sFileSource->SetPath(ev.filename, 10); } void Playback::react(const PlayFile& ev) { - sFileSource->SetPath(ev.filename); + sFileSource->SetPath(ev.filename, 15); +} + +void Standby::react(const SeekFile& ev) { + sFileSource->SetPath(ev.filename, ev.offset); +} + +void Playback::react(const SeekFile& ev) { + sFileSource->SetPath(ev.filename, ev.offset); } void Standby::react(const internal::InputFileOpened& ev) { diff --git a/src/audio/audio_source.cpp b/src/audio/audio_source.cpp index 44de1d1b..2543db44 100644 --- a/src/audio/audio_source.cpp +++ b/src/audio/audio_source.cpp @@ -11,8 +11,9 @@ namespace audio { TaggedStream::TaggedStream(std::shared_ptr t, - std::unique_ptr w) - : codecs::IStream(w->type()), tags_(t), wrapped_(std::move(w)) {} + std::unique_ptr w, + uint32_t offset) + : codecs::IStream(w->type()), tags_(t), wrapped_(std::move(w)), offset_(offset) {} auto TaggedStream::tags() -> std::shared_ptr { return tags_; @@ -38,6 +39,10 @@ auto TaggedStream::Size() -> std::optional { return wrapped_->Size(); } +auto TaggedStream::Offset() -> uint32_t { + return offset_; +} + auto TaggedStream::SetPreambleFinished() -> void { wrapped_->SetPreambleFinished(); } diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp index 7726a94a..665e8c1d 100644 --- a/src/audio/fatfs_audio_input.cpp +++ b/src/audio/fatfs_audio_input.cpp @@ -62,9 +62,9 @@ auto FatfsAudioInput::SetPath(std::optional path) -> void { } } -auto FatfsAudioInput::SetPath(const std::string& path) -> void { +auto FatfsAudioInput::SetPath(const std::string& path,uint32_t offset) -> void { std::lock_guard guard{new_stream_mutex_}; - if (OpenFile(path)) { + if (OpenFile(path, offset)) { has_new_stream_ = true; has_new_stream_.notify_one(); } @@ -103,7 +103,7 @@ auto FatfsAudioInput::NextStream() -> std::shared_ptr { } } -auto FatfsAudioInput::OpenFile(const std::string& path) -> bool { +auto FatfsAudioInput::OpenFile(const std::string& path,uint32_t offset) -> bool { ESP_LOGI(kTag, "opening file %s", path.c_str()); auto tags = tag_parser_.ReadAndParseTags(path); @@ -136,7 +136,7 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool { auto source = std::make_unique(stream_type.value(), std::move(file)); - new_stream_.reset(new TaggedStream(tags, std::move(source))); + new_stream_.reset(new TaggedStream(tags, std::move(source), offset)); return true; } diff --git a/src/audio/include/audio_decoder.hpp b/src/audio/include/audio_decoder.hpp index 318e6fd4..b8aac710 100644 --- a/src/audio/include/audio_decoder.hpp +++ b/src/audio/include/audio_decoder.hpp @@ -24,7 +24,7 @@ namespace audio { */ class Timer { public: - Timer(std::shared_ptr, const codecs::ICodec::OutputFormat& format); + Timer(std::shared_ptr, const codecs::ICodec::OutputFormat& format, uint32_t current_seconds = 0); auto AddSamples(std::size_t) -> void; diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp index 03584062..8459333f 100644 --- a/src/audio/include/audio_events.hpp +++ b/src/audio/include/audio_events.hpp @@ -45,6 +45,11 @@ struct PlayFile : tinyfsm::Event { std::string filename; }; +struct SeekFile : tinyfsm::Event { + std::string filename; + uint32_t offset; +}; + struct StepUpVolume : tinyfsm::Event {}; struct StepDownVolume : tinyfsm::Event {}; struct SetVolume : tinyfsm::Event { diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp index 29ec489a..71cd2701 100644 --- a/src/audio/include/audio_fsm.hpp +++ b/src/audio/include/audio_fsm.hpp @@ -57,6 +57,7 @@ class AudioState : public tinyfsm::Fsm { virtual void react(const system_fsm::BluetoothEvent&); virtual void react(const PlayFile&) {} + virtual void react(const SeekFile&) {} virtual void react(const QueueUpdate&) {} virtual void react(const PlaybackUpdate&) {} void react(const TogglePlayPause&); @@ -99,6 +100,7 @@ class Uninitialised : public AudioState { class Standby : public AudioState { public: void react(const PlayFile&) override; + void react(const SeekFile&) override; void react(const internal::InputFileOpened&) override; void react(const QueueUpdate&) override; void react(const system_fsm::KeyLockChanged&) override; @@ -115,6 +117,7 @@ class Playback : public AudioState { void react(const system_fsm::HasPhonesChanged&) override; void react(const PlayFile&) override; + void react(const SeekFile&) override; void react(const QueueUpdate&) override; void react(const PlaybackUpdate&) override; diff --git a/src/audio/include/audio_source.hpp b/src/audio/include/audio_source.hpp index 68145f5b..b2fd173d 100644 --- a/src/audio/include/audio_source.hpp +++ b/src/audio/include/audio_source.hpp @@ -16,7 +16,8 @@ namespace audio { class TaggedStream : public codecs::IStream { public: TaggedStream(std::shared_ptr, - std::unique_ptr wrapped); + std::unique_ptr wrapped, + uint32_t offset = 0); auto tags() -> std::shared_ptr; @@ -30,11 +31,14 @@ class TaggedStream : public codecs::IStream { auto Size() -> std::optional override; + auto Offset() -> uint32_t; + auto SetPreambleFinished() -> void override; private: std::shared_ptr tags_; std::unique_ptr wrapped_; + int32_t offset_; }; class IAudioSource { diff --git a/src/audio/include/fatfs_audio_input.hpp b/src/audio/include/fatfs_audio_input.hpp index 4cccbb46..10b7433e 100644 --- a/src/audio/include/fatfs_audio_input.hpp +++ b/src/audio/include/fatfs_audio_input.hpp @@ -39,7 +39,7 @@ class FatfsAudioInput : public IAudioSource { * given file path. */ auto SetPath(std::optional) -> void; - auto SetPath(const std::string&) -> void; + auto SetPath(const std::string&,uint32_t offset = 0) -> void; auto SetPath() -> void; auto HasNewStream() -> bool override; @@ -49,7 +49,7 @@ class FatfsAudioInput : public IAudioSource { FatfsAudioInput& operator=(const FatfsAudioInput&) = delete; private: - auto OpenFile(const std::string& path) -> bool; + auto OpenFile(const std::string& path,uint32_t offset) -> bool; auto ContainerToStreamType(database::Container) -> std::optional; diff --git a/src/codecs/include/codec.hpp b/src/codecs/include/codec.hpp index 8aa391b6..fb1ec771 100644 --- a/src/codecs/include/codec.hpp +++ b/src/codecs/include/codec.hpp @@ -117,7 +117,7 @@ class ICodec { * Decodes metadata or headers from the given input stream, and returns the * format for the samples that will be decoded from it. */ - virtual auto OpenStream(std::shared_ptr input) + virtual auto OpenStream(std::shared_ptr input,uint32_t offset) -> cpp::result = 0; struct OutputInfo { diff --git a/src/codecs/include/mad.hpp b/src/codecs/include/mad.hpp index 813aa86d..35e3284d 100644 --- a/src/codecs/include/mad.hpp +++ b/src/codecs/include/mad.hpp @@ -26,7 +26,7 @@ class MadMp3Decoder : public ICodec { MadMp3Decoder(); ~MadMp3Decoder(); - auto OpenStream(std::shared_ptr input) + auto OpenStream(std::shared_ptr input,uint32_t offset) -> cpp::result override; auto DecodeTo(cpp::span destination) diff --git a/src/codecs/include/miniflac.hpp b/src/codecs/include/miniflac.hpp index d57b08a3..d1daca2f 100644 --- a/src/codecs/include/miniflac.hpp +++ b/src/codecs/include/miniflac.hpp @@ -28,7 +28,7 @@ class MiniFlacDecoder : public ICodec { MiniFlacDecoder(); ~MiniFlacDecoder(); - auto OpenStream(std::shared_ptr input) + auto OpenStream(std::shared_ptr input,uint32_t offset) -> cpp::result override; auto DecodeTo(cpp::span destination) diff --git a/src/codecs/include/opus.hpp b/src/codecs/include/opus.hpp index 45b1b07a..1431fa54 100644 --- a/src/codecs/include/opus.hpp +++ b/src/codecs/include/opus.hpp @@ -26,7 +26,7 @@ class XiphOpusDecoder : public ICodec { XiphOpusDecoder(); ~XiphOpusDecoder(); - auto OpenStream(std::shared_ptr input) + auto OpenStream(std::shared_ptr input,uint32_t offset) -> cpp::result override; auto DecodeTo(cpp::span destination) diff --git a/src/codecs/include/vorbis.hpp b/src/codecs/include/vorbis.hpp index 2f93c37e..b32ef8d5 100644 --- a/src/codecs/include/vorbis.hpp +++ b/src/codecs/include/vorbis.hpp @@ -28,7 +28,7 @@ class TremorVorbisDecoder : public ICodec { TremorVorbisDecoder(); ~TremorVorbisDecoder(); - auto OpenStream(std::shared_ptr input) + auto OpenStream(std::shared_ptr input,uint32_t offset) -> cpp::result override; auto DecodeTo(cpp::span destination) diff --git a/src/codecs/include/wav.hpp b/src/codecs/include/wav.hpp index 896976dd..e884a9bb 100644 --- a/src/codecs/include/wav.hpp +++ b/src/codecs/include/wav.hpp @@ -31,7 +31,7 @@ class WavDecoder : public ICodec { WavDecoder(); ~WavDecoder(); - auto OpenStream(std::shared_ptr input) + auto OpenStream(std::shared_ptr input,uint32_t offset) -> cpp::result override; auto DecodeTo(cpp::span destination) diff --git a/src/codecs/mad.cpp b/src/codecs/mad.cpp index f36636a1..0617295f 100644 --- a/src/codecs/mad.cpp +++ b/src/codecs/mad.cpp @@ -58,7 +58,7 @@ auto MadMp3Decoder::GetBytesUsed() -> std::size_t { } } -auto MadMp3Decoder::OpenStream(std::shared_ptr input) +auto MadMp3Decoder::OpenStream(std::shared_ptr input,uint32_t offset) -> cpp::result { input_ = input; diff --git a/src/codecs/miniflac.cpp b/src/codecs/miniflac.cpp index ace73466..d15410fe 100644 --- a/src/codecs/miniflac.cpp +++ b/src/codecs/miniflac.cpp @@ -42,7 +42,7 @@ MiniFlacDecoder::~MiniFlacDecoder() { } } -auto MiniFlacDecoder::OpenStream(std::shared_ptr input) +auto MiniFlacDecoder::OpenStream(std::shared_ptr input,uint32_t offset) -> cpp::result { input_ = input; diff --git a/src/codecs/opus.cpp b/src/codecs/opus.cpp index e4917a33..2f700510 100644 --- a/src/codecs/opus.cpp +++ b/src/codecs/opus.cpp @@ -78,7 +78,7 @@ XiphOpusDecoder::~XiphOpusDecoder() { } } -auto XiphOpusDecoder::OpenStream(std::shared_ptr input) +auto XiphOpusDecoder::OpenStream(std::shared_ptr input,uint32_t offset) -> cpp::result { input_ = input; diff --git a/src/codecs/vorbis.cpp b/src/codecs/vorbis.cpp index 3b3798cb..66237c28 100644 --- a/src/codecs/vorbis.cpp +++ b/src/codecs/vorbis.cpp @@ -84,7 +84,7 @@ TremorVorbisDecoder::~TremorVorbisDecoder() { ov_clear(&vorbis_); } -auto TremorVorbisDecoder::OpenStream(std::shared_ptr input) +auto TremorVorbisDecoder::OpenStream(std::shared_ptr input,uint32_t offset) -> cpp::result { int res = ov_open_callbacks(input.get(), &vorbis_, NULL, 0, kCallbacks); if (res < 0) { diff --git a/src/codecs/wav.cpp b/src/codecs/wav.cpp index a67f3ff4..652f4f88 100644 --- a/src/codecs/wav.cpp +++ b/src/codecs/wav.cpp @@ -84,7 +84,7 @@ WavDecoder::WavDecoder() : input_(), buffer_() {} WavDecoder::~WavDecoder() {} -auto WavDecoder::OpenStream(std::shared_ptr input) +auto WavDecoder::OpenStream(std::shared_ptr input,uint32_t offset) -> cpp::result { input_ = input; @@ -199,8 +199,10 @@ auto WavDecoder::OpenStream(std::shared_ptr input) return cpp::fail(Error::kUnsupportedFormat); } + auto data_offset = offset * samples_per_second * bits_per_sample; + // Seek track to start of data - input->SeekTo(data_chunk_index + 8, IStream::SeekFrom::kStartOfStream); + input->SeekTo(data_chunk_index + 8 + data_offset, IStream::SeekFrom::kStartOfStream); output_format_ = {.num_channels = (uint8_t)num_channels_, .sample_rate_hz = samples_per_second, -- cgit v1.2.3 From a49d754da6c293445be16ac643d10849c01ea96b Mon Sep 17 00:00:00 2001 From: ailurux Date: Fri, 16 Feb 2024 10:57:47 +1100 Subject: Seeking working with hardcoded event, wav only --- src/audio/audio_decoder.cpp | 2 +- src/audio/audio_fsm.cpp | 4 +++- src/codecs/wav.cpp | 8 +++++++- 3 files changed, 11 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp index 02cf27e3..eaa9ff9c 100644 --- a/src/audio/audio_decoder.cpp +++ b/src/audio/audio_decoder.cpp @@ -155,7 +155,7 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { .bitrate_kbps = open_res->sample_rate_hz, .encoding = stream->type(), }); - timer_.reset(new Timer(tags, open_res.value())); + timer_.reset(new Timer(tags, open_res.value(), stream->Offset())); // TODO: How does *this?* need to change? PlaybackUpdate ev{.seconds_elapsed = stream->Offset(), .track = tags}; diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index c67cfc7a..75e3c24a 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -244,11 +244,13 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { } void Standby::react(const PlayFile& ev) { + sCurrentTrack = 0; + sIsPlaybackAllowed = true; sFileSource->SetPath(ev.filename, 10); } void Playback::react(const PlayFile& ev) { - sFileSource->SetPath(ev.filename, 15); + sFileSource->SetPath(ev.filename, 10); } void Standby::react(const SeekFile& ev) { diff --git a/src/codecs/wav.cpp b/src/codecs/wav.cpp index 652f4f88..22cbd49c 100644 --- a/src/codecs/wav.cpp +++ b/src/codecs/wav.cpp @@ -199,7 +199,7 @@ auto WavDecoder::OpenStream(std::shared_ptr input,uint32_t offset) return cpp::fail(Error::kUnsupportedFormat); } - auto data_offset = offset * samples_per_second * bits_per_sample; + int64_t data_offset = offset * samples_per_second * bytes_per_sample_; // Seek track to start of data input->SeekTo(data_chunk_index + 8 + data_offset, IStream::SeekFrom::kStartOfStream); @@ -218,6 +218,7 @@ auto WavDecoder::DecodeTo(cpp::span output) buffer_.ConsumeBytes([&](cpp::span buf) -> size_t { size_t bytes_read = buf.size_bytes(); + ESP_LOGI(kTag, "Bytes read: %d", bytes_read); size_t frames_read = bytes_read / bytes_per_sample_ / output_format_.num_channels; @@ -243,6 +244,11 @@ auto WavDecoder::DecodeTo(cpp::span output) return samples_written * bytes_per_sample_; }); + ESP_LOGI(kTag, "Samples written %d", samples_written); + if (is_eof) { + ESP_LOGI(kTag, "EOF"); + } + return OutputInfo{.samples_written = samples_written, .is_stream_finished = samples_written == 0 && is_eof}; } -- cgit v1.2.3 From 665679b8854d34c13d8eb92167aa8a4691619d8b Mon Sep 17 00:00:00 2001 From: ailurux Date: Fri, 16 Feb 2024 12:55:11 +1100 Subject: WIP: seeking in lua example --- src/audio/audio_decoder.cpp | 3 +-- src/audio/audio_fsm.cpp | 6 ++++-- src/audio/audio_source.cpp | 7 ++++++- src/audio/fatfs_audio_input.cpp | 2 +- src/audio/include/audio_events.hpp | 3 ++- src/audio/include/audio_source.hpp | 7 ++++++- src/codecs/wav.cpp | 5 ----- src/ui/ui_fsm.cpp | 16 +++++++++++++++- 8 files changed, 35 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp index eaa9ff9c..68a8a86b 100644 --- a/src/audio/audio_decoder.cpp +++ b/src/audio/audio_decoder.cpp @@ -148,16 +148,15 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { ESP_LOGI(kTag, "stream started ok"); events::Audio().Dispatch(internal::InputFileOpened{}); - // TODO: How does this need to change? auto tags = std::make_shared(Track{ .tags = stream->tags(), .db_info = {}, .bitrate_kbps = open_res->sample_rate_hz, .encoding = stream->type(), + .filepath = stream->Filepath(), }); timer_.reset(new Timer(tags, open_res.value(), stream->Offset())); - // TODO: How does *this?* need to change? PlaybackUpdate ev{.seconds_elapsed = stream->Offset(), .track = tags}; events::Audio().Dispatch(ev); events::Ui().Dispatch(ev); diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 75e3c24a..0e213b6e 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -246,14 +246,16 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { void Standby::react(const PlayFile& ev) { sCurrentTrack = 0; sIsPlaybackAllowed = true; - sFileSource->SetPath(ev.filename, 10); + sFileSource->SetPath(ev.filename); } void Playback::react(const PlayFile& ev) { - sFileSource->SetPath(ev.filename, 10); + sFileSource->SetPath(ev.filename); } void Standby::react(const SeekFile& ev) { + sCurrentTrack = 0; + sIsPlaybackAllowed = true; sFileSource->SetPath(ev.filename, ev.offset); } diff --git a/src/audio/audio_source.cpp b/src/audio/audio_source.cpp index 2543db44..d9e8e04a 100644 --- a/src/audio/audio_source.cpp +++ b/src/audio/audio_source.cpp @@ -12,8 +12,9 @@ namespace audio { TaggedStream::TaggedStream(std::shared_ptr t, std::unique_ptr w, + std::string filepath, uint32_t offset) - : codecs::IStream(w->type()), tags_(t), wrapped_(std::move(w)), offset_(offset) {} + : codecs::IStream(w->type()), tags_(t), wrapped_(std::move(w)), filepath_(filepath), offset_(offset) {} auto TaggedStream::tags() -> std::shared_ptr { return tags_; @@ -43,6 +44,10 @@ auto TaggedStream::Offset() -> uint32_t { return offset_; } +auto TaggedStream::Filepath() -> std::string { + return filepath_; +} + auto TaggedStream::SetPreambleFinished() -> void { wrapped_->SetPreambleFinished(); } diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp index 665e8c1d..74c1154b 100644 --- a/src/audio/fatfs_audio_input.cpp +++ b/src/audio/fatfs_audio_input.cpp @@ -136,7 +136,7 @@ auto FatfsAudioInput::OpenFile(const std::string& path,uint32_t offset) -> bool auto source = std::make_unique(stream_type.value(), std::move(file)); - new_stream_.reset(new TaggedStream(tags, std::move(source), offset)); + new_stream_.reset(new TaggedStream(tags, std::move(source), path, offset)); return true; } diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp index 8459333f..96e77987 100644 --- a/src/audio/include/audio_events.hpp +++ b/src/audio/include/audio_events.hpp @@ -26,6 +26,7 @@ struct Track { uint32_t duration; uint32_t bitrate_kbps; codecs::StreamType encoding; + std::string filepath; }; struct PlaybackStarted : tinyfsm::Event {}; @@ -46,8 +47,8 @@ struct PlayFile : tinyfsm::Event { }; struct SeekFile : tinyfsm::Event { + uint32_t offset; std::string filename; - uint32_t offset; }; struct StepUpVolume : tinyfsm::Event {}; diff --git a/src/audio/include/audio_source.hpp b/src/audio/include/audio_source.hpp index b2fd173d..b38acd7a 100644 --- a/src/audio/include/audio_source.hpp +++ b/src/audio/include/audio_source.hpp @@ -17,7 +17,9 @@ class TaggedStream : public codecs::IStream { public: TaggedStream(std::shared_ptr, std::unique_ptr wrapped, - uint32_t offset = 0); + std::string path, + uint32_t offset = 0 + ); auto tags() -> std::shared_ptr; @@ -33,11 +35,14 @@ class TaggedStream : public codecs::IStream { auto Offset() -> uint32_t; + auto Filepath() -> std::string; + auto SetPreambleFinished() -> void override; private: std::shared_ptr tags_; std::unique_ptr wrapped_; + std::string filepath_; int32_t offset_; }; diff --git a/src/codecs/wav.cpp b/src/codecs/wav.cpp index 22cbd49c..5dd6f031 100644 --- a/src/codecs/wav.cpp +++ b/src/codecs/wav.cpp @@ -218,7 +218,6 @@ auto WavDecoder::DecodeTo(cpp::span output) buffer_.ConsumeBytes([&](cpp::span buf) -> size_t { size_t bytes_read = buf.size_bytes(); - ESP_LOGI(kTag, "Bytes read: %d", bytes_read); size_t frames_read = bytes_read / bytes_per_sample_ / output_format_.num_channels; @@ -244,10 +243,6 @@ auto WavDecoder::DecodeTo(cpp::span output) return samples_written * bytes_per_sample_; }); - ESP_LOGI(kTag, "Samples written %d", samples_written); - if (is_eof) { - ESP_LOGI(kTag, "EOF"); - } return OutputInfo{.samples_written = samples_written, .is_stream_finished = samples_written == 0 && is_eof}; diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 24145ead..3e85c36e 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -123,7 +123,21 @@ lua::Property UiState::sPlaybackPlaying{ }}; lua::Property UiState::sPlaybackTrack{}; -lua::Property UiState::sPlaybackPosition{0}; +lua::Property UiState::sPlaybackPosition{0, [](const lua::LuaValue& val) { + int current_val = std::get(sPlaybackPosition.Get()); + if (!std::holds_alternative(val)) { + return false; + } + int new_val = std::get(val); + if (current_val != new_val) { + auto track = sPlaybackTrack.Get(); + if (!std::holds_alternative(track)) { + return false; + } + events::Audio().Dispatch(audio::SeekFile{.offset = (uint32_t)new_val, .filename = std::get(track).filepath}); + } + return true; +}}; lua::Property UiState::sQueuePosition{0}; lua::Property UiState::sQueueSize{0}; -- cgit v1.2.3 From c60bb9ee42eea2c88ef90228274bd28350a87ae4 Mon Sep 17 00:00:00 2001 From: ailurux Date: Fri, 16 Feb 2024 16:19:12 +1100 Subject: Fix issue with seeking whilst paused --- src/audio/audio_fsm.cpp | 2 -- 1 file changed, 2 deletions(-) (limited to 'src') diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 0e213b6e..bb7d33dc 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -254,8 +254,6 @@ void Playback::react(const PlayFile& ev) { } void Standby::react(const SeekFile& ev) { - sCurrentTrack = 0; - sIsPlaybackAllowed = true; sFileSource->SetPath(ev.filename, ev.offset); } -- cgit v1.2.3 From aece1c6b58587aaeda0d1ce082df13e409f930f1 Mon Sep 17 00:00:00 2001 From: ailurux Date: Fri, 16 Feb 2024 16:48:53 +1100 Subject: Opus seeking impl --- src/codecs/opus.cpp | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src') diff --git a/src/codecs/opus.cpp b/src/codecs/opus.cpp index 2f700510..ec587bc7 100644 --- a/src/codecs/opus.cpp +++ b/src/codecs/opus.cpp @@ -128,6 +128,10 @@ auto XiphOpusDecoder::OpenStream(std::shared_ptr input,uint32_t offset) length = l * 2; } + if (offset) { + SeekTo(offset * 48000); + } + return OutputFormat{ .num_channels = 2, .sample_rate_hz = 48000, -- cgit v1.2.3 From 0baad11b188e7bac6b968017716186d9da0e492f Mon Sep 17 00:00:00 2001 From: ailurux Date: Fri, 16 Feb 2024 17:01:22 +1100 Subject: WIP: Vorbis seeking --- src/codecs/vorbis.cpp | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src') diff --git a/src/codecs/vorbis.cpp b/src/codecs/vorbis.cpp index 66237c28..65783dac 100644 --- a/src/codecs/vorbis.cpp +++ b/src/codecs/vorbis.cpp @@ -124,6 +124,10 @@ auto TremorVorbisDecoder::OpenStream(std::shared_ptr input,uint32_t off length = l * info->channels; } + if (offset) { + ov_time_seek(&vorbis_, offset*1000); + } + return OutputFormat{ .num_channels = static_cast(info->channels), .sample_rate_hz = static_cast(info->rate), -- cgit v1.2.3 From 912060de1b31721103efc0ee321dfb1f80f8ccc4 Mon Sep 17 00:00:00 2001 From: ailurux Date: Fri, 16 Feb 2024 17:08:45 +1100 Subject: Vorbis seeking impl --- src/codecs/vorbis.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/codecs/vorbis.cpp b/src/codecs/vorbis.cpp index e51c6bca..ada92fb6 100644 --- a/src/codecs/vorbis.cpp +++ b/src/codecs/vorbis.cpp @@ -118,7 +118,7 @@ auto TremorVorbisDecoder::OpenStream(std::shared_ptr input,uint32_t off } if (offset) { - ov_time_seek(&vorbis_, offset*1000); + ov_time_seek(vorbis_.get(), offset*1000); } return OutputFormat{ -- cgit v1.2.3 From f54347794f45261e0c0fde1104a70d1063c77305 Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 22 Feb 2024 14:37:14 +1100 Subject: WIP: Flac not working-- coming back to this later --- src/audio/audio_decoder.cpp | 1 + src/codecs/CMakeLists.txt | 2 +- src/codecs/include/source_buffer.hpp | 1 + src/codecs/miniflac copy.cpp | 207 +++++++++++++++++++++++++++ src/codecs/miniflac.cpp | 130 ++++++++++++++++- src/codecs/miniflac.cpp.bak2 | 266 +++++++++++++++++++++++++++++++++++ src/codecs/source_buffer.cpp | 5 + 7 files changed, 610 insertions(+), 2 deletions(-) create mode 100644 src/codecs/miniflac copy.cpp create mode 100644 src/codecs/miniflac.cpp.bak2 (limited to 'src') diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp index 68a8a86b..6c26dec8 100644 --- a/src/audio/audio_decoder.cpp +++ b/src/audio/audio_decoder.cpp @@ -167,6 +167,7 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { auto Decoder::ContinueDecoding() -> bool { auto res = codec_->DecodeTo(codec_buffer_); if (res.has_error()) { + ESP_LOGI(kTag, "RAN INTO DECODING ERROR"); return true; } diff --git a/src/codecs/CMakeLists.txt b/src/codecs/CMakeLists.txt index 1ef4bb2d..4b296c84 100644 --- a/src/codecs/CMakeLists.txt +++ b/src/codecs/CMakeLists.txt @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-only idf_component_register( - SRCS "codec.cpp" "mad.cpp" "miniflac.cpp" "opus.cpp" "vorbis.cpp" + SRCS "miniflac.cpp" "codec.cpp" "mad.cpp" "opus.cpp" "vorbis.cpp" "source_buffer.cpp" "sample.cpp" "wav.cpp" INCLUDE_DIRS "include" REQUIRES "result" "span" "libmad" "miniflac" "tremor" "opusfile" "memory" "util" diff --git a/src/codecs/include/source_buffer.hpp b/src/codecs/include/source_buffer.hpp index d0d7635a..7834834d 100644 --- a/src/codecs/include/source_buffer.hpp +++ b/src/codecs/include/source_buffer.hpp @@ -24,6 +24,7 @@ class SourceBuffer { auto Refill(IStream* src) -> bool; auto AddBytes(std::function)> writer) -> void; auto ConsumeBytes(std::function)> reader) -> void; + auto Empty() -> void; SourceBuffer(const SourceBuffer&) = delete; SourceBuffer& operator=(const SourceBuffer&) = delete; diff --git a/src/codecs/miniflac copy.cpp b/src/codecs/miniflac copy.cpp new file mode 100644 index 00000000..866cb49b --- /dev/null +++ b/src/codecs/miniflac copy.cpp @@ -0,0 +1,207 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "miniflac.hpp" + +#include +#include + +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "miniflac.h" +#include "result.hpp" +#include "sample.hpp" + +namespace codecs { + +[[maybe_unused]] static const char kTag[] = "flac"; + +static constexpr size_t kMaxFrameSize = 4608; + +MiniFlacDecoder::MiniFlacDecoder() + : input_(), + buffer_(), + flac_(reinterpret_cast( + heap_caps_malloc(sizeof(miniflac_t), + MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT))), + current_sample_() { + miniflac_init(flac_.get(), MINIFLAC_CONTAINER_UNKNOWN); + for (int i = 0; i < samples_by_channel_.size(); i++) { + uint32_t caps; + if (i == 0) { + caps = MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL; + } else { + // FIXME: We can *almost* fit two channels into internal ram, but we're a + // few KiB shy of being able to do it safely. + caps = MALLOC_CAP_SPIRAM; + } + samples_by_channel_[i] = reinterpret_cast( + heap_caps_malloc(kMaxFrameSize * sizeof(int32_t), caps)); + } +} + +MiniFlacDecoder::~MiniFlacDecoder() { + for (int i = 0; i < samples_by_channel_.size(); i++) { + heap_caps_free(samples_by_channel_[i]); + } +} + +auto MiniFlacDecoder::OpenStream(std::shared_ptr input,uint32_t offset) + -> cpp::result { + input_ = input; + + MINIFLAC_RESULT res; + auto read_until_result = [&](auto fn) { + while (true) { + bool eof = buffer_.Refill(input_.get()); + buffer_.ConsumeBytes(fn); + if (res == MINIFLAC_CONTINUE && !eof) { + continue; + } + break; + } + }; + + uint32_t sample_rate = 0; + + read_until_result([&](cpp::span buf) -> size_t { + uint32_t bytes_used = 0; + res = miniflac_streaminfo_sample_rate( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_used, &sample_rate); + return bytes_used; + }); + + if (res != MINIFLAC_OK) { + return cpp::fail(Error::kMalformedData); + } + + uint8_t channels = 0; + + read_until_result([&](cpp::span buf) -> size_t { + uint32_t bytes_used = 0; + res = miniflac_streaminfo_channels( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_used, &channels); + return bytes_used; + }); + + if (res != MINIFLAC_OK) { + return cpp::fail(Error::kMalformedData); + } + + uint64_t total_samples = 0; + + read_until_result([&](cpp::span buf) -> size_t { + uint32_t bytes_used = 0; + res = miniflac_streaminfo_total_samples( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_used, &total_samples); + return bytes_used; + }); + + if (res != MINIFLAC_OK) { + return cpp::fail(Error::kMalformedData); + } + + if (channels == 0 || channels > 2) { + return cpp::fail(Error::kMalformedData); + } + + if (offset) { + uint64_t samples_count = 0; + uint32_t offset_count = 0; + while (offset_count < offset) { + read_until_result([&](cpp::span buf) -> size_t { + uint32_t bytes_used = 0; + res = miniflac_sync( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_used); + return bytes_used; + }); + if (res != MINIFLAC_OK) { + return cpp::fail(Error::kMalformedData); + } + + uint32_t frame_samplerate = flac_.get()->frame.header.sample_rate; + uint16_t frame_blocksize = flac_.get()->frame.header.block_size; + if (!frame_samplerate || !frame_blocksize) { + continue; + } + + samples_count += frame_blocksize; + offset_count = samples_count / sample_rate; + } + } + + OutputFormat format{ + .num_channels = static_cast(channels), + .sample_rate_hz = static_cast(sample_rate), + .total_samples = total_samples * channels, + }; + + return format; +} + +auto MiniFlacDecoder::DecodeTo(cpp::span output) + -> cpp::result { + bool is_eof = false; + + if (!current_sample_) { + MINIFLAC_RESULT res = MINIFLAC_CONTINUE; + while (res == MINIFLAC_CONTINUE && !is_eof) { + is_eof = buffer_.Refill(input_.get()); + buffer_.ConsumeBytes([&](cpp::span buf) -> size_t { + // FIXME: We should do a miniflac_sync first, in order to check that + // our sample buffers have enough space for the next frame. + uint32_t bytes_read = 0; + res = miniflac_decode( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_read, samples_by_channel_.data()); + return bytes_read; + }); + } + + if (res == MINIFLAC_OK) { + current_sample_ = 0; + } else if (is_eof) { + return OutputInfo{ + .samples_written = 0, + .is_stream_finished = true, + }; + } else { + return cpp::fail(Error::kMalformedData); + } + } + + size_t samples_written = 0; + if (current_sample_) { + while (*current_sample_ < flac_->frame.header.block_size) { + if (samples_written + flac_->frame.header.channels >= output.size()) { + // We can't fit the next full PCM frame into the buffer. + return OutputInfo{.samples_written = samples_written, + .is_stream_finished = false}; + } + + for (int channel = 0; channel < flac_->frame.header.channels; channel++) { + output[samples_written++] = + sample::FromSigned(samples_by_channel_[channel][*current_sample_], + flac_->frame.header.bps); + } + (*current_sample_)++; + } + } + + current_sample_.reset(); + return OutputInfo{.samples_written = samples_written, + .is_stream_finished = samples_written == 0 && is_eof}; +} + +auto MiniFlacDecoder::SeekTo(size_t target) -> cpp::result { + return {}; +} + +} // namespace codecs diff --git a/src/codecs/miniflac.cpp b/src/codecs/miniflac.cpp index d0b40f96..8ec12df5 100644 --- a/src/codecs/miniflac.cpp +++ b/src/codecs/miniflac.cpp @@ -111,6 +111,131 @@ auto MiniFlacDecoder::OpenStream(std::shared_ptr input,uint32_t offset) return cpp::fail(Error::kMalformedData); } + // Seeking + offset = 50; + if (offset) { + // TODO: This assumes a constant sample_rate + uint64_t target_sample = sample_rate * offset; + uint32_t num_seekpoints; + read_until_result([&](cpp::span buf) -> size_t { + uint32_t bytes_used = 0; + res = miniflac_seektable_seekpoints( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_used, &num_seekpoints); + return bytes_used; + }); + if (res != MINIFLAC_OK) { + // TODO: Not having a seektable is not malformed + // but currently seeking will not work without it. + return cpp::fail(Error::kMalformedData); + } + // Loop over the seek table + ESP_LOGI(kTag, "Found seektable with %lu points", num_seekpoints); + uint64_t sample_number; + uint64_t sample_offset_bytes; + uint16_t num_samples_in_target; + for (uint32_t i = 0; i < num_seekpoints; i++) { + // Get sample number + read_until_result([&](cpp::span buf) -> size_t { + uint32_t bytes_used = 0; + res = miniflac_seektable_sample_number( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_used, &sample_number); + return bytes_used; + }); + if (res != MINIFLAC_OK) { + return cpp::fail(Error::kMalformedData); + } + read_until_result([&](cpp::span buf) -> size_t { + uint32_t bytes_used = 0; + res = miniflac_seektable_sample_offset( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_used, &sample_offset_bytes); + return bytes_used; + }); + if (res != MINIFLAC_OK) { + return cpp::fail(Error::kMalformedData); + } + read_until_result([&](cpp::span buf) -> size_t { + uint32_t bytes_used = 0; + res = miniflac_seektable_samples( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_used, &num_samples_in_target); + return bytes_used; + }); + if (res != MINIFLAC_OK) { + return cpp::fail(Error::kMalformedData); + } + + ESP_LOGI(kTag, "Seektable entry %lu", i); + + // Check if we want to seek to this seektable position? + if (sample_number + num_samples_in_target >= target_sample) { + ESP_LOGI(kTag, "Break on Seektable entry %lu", i); + break; + } + } + + // Seek forward to target_sample + if (sample_number > 0) { + ESP_LOGI(kTag, "total samples: %llu", total_samples); + ESP_LOGI(kTag, "TARGET SAMPLE: %llu", target_sample); + ESP_LOGI(kTag, "SAMPLE NUMBER: %llu", sample_number); + } + uint64_t byte_offset = sample_offset_bytes; + ESP_LOGI(kTag, "Byte offset is forward %llu bytes", byte_offset); + ESP_LOGI(kTag, "Decoder state pre: %d", flac_->state); + while(flac_.get()->state == MINIFLAC_METADATA) { + read_until_result([&](cpp::span buf) -> size_t { + uint32_t bytes_used = 0; + res = miniflac_sync( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_used); + return bytes_used; + }); + if (res != MINIFLAC_OK) { + ESP_LOGI(kTag, "Decoder error 1 %d", res); + } + } + ESP_LOGI(kTag, "Decoder state post: %d", flac_->state); + + + ESP_LOGI(kTag, "Going to skip forward %llu bytes", byte_offset); + if (input_.get()->CanSeek()) { + ESP_LOGI(kTag, "Skipping forward %llu bytes", byte_offset); + buffer_.Empty(); + input_.get()->SeekTo(byte_offset, IStream::SeekFrom::kCurrentPosition); + ESP_LOGI(kTag, "Skipped %llu bytes", byte_offset); + } + + + ESP_LOGI(kTag, "Pre-refill"); + buffer_.Refill(input_.get()); + ESP_LOGI(kTag, "Post-refill"); + + + read_until_result([&](cpp::span buf) -> size_t { + uint32_t bytes_used = 0; + res = miniflac_sync( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_used); + return bytes_used; + }); + if (res != MINIFLAC_OK) { + ESP_LOGI(kTag, "Decoder error 1 %d", res); + } + ESP_LOGI(kTag, "JOB'S DONE"); + } + + ESP_LOGI(kTag, "Decoder state: %d", flac_->state); + ESP_LOGI(kTag, "Frame header state: %d", flac_->frame.header.state); + + // TODO: Sample number is not guaranteed, could be block index. + ESP_LOGI(kTag, "Ended up... at sample %llu", flac_->frame.header.sample_number); + ESP_LOGI(kTag, "and block index: %lu", flac_->frame.header.frame_number); + ESP_LOGI(kTag, "total samples: %llu", total_samples); + + OutputFormat format{ .num_channels = static_cast(channels), .sample_rate_hz = static_cast(sample_rate), @@ -128,6 +253,7 @@ auto MiniFlacDecoder::DecodeTo(cpp::span output) MINIFLAC_RESULT res = MINIFLAC_CONTINUE; while (res == MINIFLAC_CONTINUE && !is_eof) { is_eof = buffer_.Refill(input_.get()); + ESP_LOGI(kTag, "EOF? %s", is_eof ? "true" : "false"); buffer_.ConsumeBytes([&](cpp::span buf) -> size_t { // FIXME: We should do a miniflac_sync first, in order to check that // our sample buffers have enough space for the next frame. @@ -147,7 +273,8 @@ auto MiniFlacDecoder::DecodeTo(cpp::span output) .is_stream_finished = true, }; } else { - return cpp::fail(Error::kMalformedData); + ESP_LOGI(kTag, "Failed: decoder result: %d", res); + // return cpp::fail(Error::kMalformedData); } } @@ -170,6 +297,7 @@ auto MiniFlacDecoder::DecodeTo(cpp::span output) } current_sample_.reset(); + ESP_LOGI(kTag, "Samples written %lu", (uint32_t)samples_written); return OutputInfo{.samples_written = samples_written, .is_stream_finished = samples_written == 0 && is_eof}; } diff --git a/src/codecs/miniflac.cpp.bak2 b/src/codecs/miniflac.cpp.bak2 new file mode 100644 index 00000000..843c3003 --- /dev/null +++ b/src/codecs/miniflac.cpp.bak2 @@ -0,0 +1,266 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "miniflac.hpp" + +#include +#include + +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "miniflac.h" +#include "result.hpp" +#include "sample.hpp" + +namespace codecs { + +[[maybe_unused]] static const char kTag[] = "flac"; + +static constexpr size_t kMaxFrameSize = 4608; + +MiniFlacDecoder::MiniFlacDecoder() + : input_(), + buffer_(), + flac_(reinterpret_cast( + heap_caps_malloc(sizeof(miniflac_t), + MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT))), + current_sample_() { + miniflac_init(flac_.get(), MINIFLAC_CONTAINER_UNKNOWN); + for (int i = 0; i < samples_by_channel_.size(); i++) { + uint32_t caps; + if (i == 0) { + caps = MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL; + } else { + // FIXME: We can *almost* fit two channels into internal ram, but we're a + // few KiB shy of being able to do it safely. + caps = MALLOC_CAP_SPIRAM; + } + samples_by_channel_[i] = reinterpret_cast( + heap_caps_malloc(kMaxFrameSize * sizeof(int32_t), caps)); + } +} + +MiniFlacDecoder::~MiniFlacDecoder() { + for (int i = 0; i < samples_by_channel_.size(); i++) { + heap_caps_free(samples_by_channel_[i]); + } +} + +auto MiniFlacDecoder::OpenStream(std::shared_ptr input,uint32_t offset) + -> cpp::result { + input_ = input; + + MINIFLAC_RESULT res; + bool is_eof; + auto read_until_result = [&](auto fn) { + while (true) { + is_eof = buffer_.Refill(input_.get()); + buffer_.ConsumeBytes(fn); + if (res == MINIFLAC_CONTINUE && !eof) { + continue; + } + break; + } + }; + + uint16_t min_block_size = 0; // In samples + + read_until_result([&](cpp::span buf) -> size_t { + uint32_t bytes_used = 0; + res = miniflac_streaminfo_min_block_size( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_used, &min_block_size); + return bytes_used; + }); + + if (res != MINIFLAC_OK) { + return cpp::fail(Error::kMalformedData); + } + + uint16_t max_block_size = 0; // In samples + read_until_result([&](cpp::span buf) -> size_t { + uint32_t bytes_used = 0; + res = miniflac_streaminfo_min_block_size( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_used, &max_block_size); + return bytes_used; + }); + + if (res != MINIFLAC_OK) { + return cpp::fail(Error::kMalformedData); + } + + ESP_LOGI(kTag, "Blocksize min: %u max %u", min_block_size, max_block_size); + + uint32_t sample_rate = 0; + + read_until_result([&](cpp::span buf) -> size_t { + uint32_t bytes_used = 0; + res = miniflac_streaminfo_sample_rate( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_used, &sample_rate); + return bytes_used; + }); + + if (res != MINIFLAC_OK) { + return cpp::fail(Error::kMalformedData); + } + + uint8_t channels = 0; + + read_until_result([&](cpp::span buf) -> size_t { + uint32_t bytes_used = 0; + res = miniflac_streaminfo_channels( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_used, &channels); + return bytes_used; + }); + + if (res != MINIFLAC_OK) { + return cpp::fail(Error::kMalformedData); + } + + uint64_t total_samples = 0; + + read_until_result([&](cpp::span buf) -> size_t { + uint32_t bytes_used = 0; + res = miniflac_streaminfo_total_samples( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_used, &total_samples); + return bytes_used; + }); + + if (res != MINIFLAC_OK) { + return cpp::fail(Error::kMalformedData); + } + + if (channels == 0 || channels > 2) { + return cpp::fail(Error::kMalformedData); + } + + // Seeking + offset = 0; + if (offset) { + // Super dumb approach, but lets try it first + // Go to the first frame + while(flac_.get()->state == MINIFLAC_METADATA) { + read_until_result([&](cpp::span buf) -> size_t { + uint32_t bytes_used = 0; + res = miniflac_sync( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_used); + return bytes_used; + }); + if (res != MINIFLAC_OK) { + ESP_LOGI(kTag, "IT HAPPENED"); + } + } + ESP_LOGI(kTag, "Flac state: %d", flac_->state); + + // Naive approach + uint64_t byte_offset = offset; // TODO + + ESP_LOGI(kTag, "Going to skip forward %llu bytes", byte_offset); + if (input_.get()->CanSeek()) { + ESP_LOGI(kTag, "Skipping forward %llu bytes", byte_offset); + buffer_.Empty(); + input_.get()->SeekTo(byte_offset, IStream::SeekFrom::kCurrentPosition); + } + // buffer_.Refill(input_.get()); + + // // Sync again + // read_until_result([&](cpp::span buf) -> size_t { + // uint32_t bytes_used = 0; + // res = miniflac_sync( + // flac_.get(), reinterpret_cast(buf.data()), + // buf.size_bytes(), &bytes_used); + // return bytes_used; + // }); + // if (res != MINIFLAC_OK) { + // ESP_LOGI(kTag, "IT HAPPENED HERE! %d", res); + // } + + // ESP_LOGI(kTag, "Decoder state: %d", flac_->state); + // ESP_LOGI(kTag, "Frame header state: %d", flac_->frame.header.state); + + // // TODO: Sample number is not guaranteed, could be block index. + // ESP_LOGI(kTag, "Ended up... at sample %llu", flac_->frame.header.sample_number); + // ESP_LOGI(kTag, "and block index: %lu", flac_->frame.header.frame_number); + // ESP_LOGI(kTag, "total samples: %llu", total_samples); + } + + + OutputFormat format{ + .num_channels = static_cast(channels), + .sample_rate_hz = static_cast(sample_rate), + .total_samples = total_samples * channels, + }; + + return format; +} + +auto MiniFlacDecoder::DecodeTo(cpp::span output) + -> cpp::result { + bool is_eof = false; + + if (!current_sample_) { + MINIFLAC_RESULT res = MINIFLAC_CONTINUE; + while (res == MINIFLAC_CONTINUE && !is_eof) { + is_eof = buffer_.Refill(input_.get()); + ESP_LOGI(kTag, "EOF? %s", is_eof ? "true" : "false"); + buffer_.ConsumeBytes([&](cpp::span buf) -> size_t { + // FIXME: We should do a miniflac_sync first, in order to check that + // our sample buffers have enough space for the next frame. + uint32_t bytes_read = 0; + res = miniflac_decode( + flac_.get(), reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_read, samples_by_channel_.data()); + return bytes_read; + }); + } + + if (res == MINIFLAC_OK) { + current_sample_ = 0; + } else if (is_eof) { + return OutputInfo{ + .samples_written = 0, + .is_stream_finished = true, + }; + } else { + ESP_LOGI(kTag, "Failed: decoder result: %d", res); + return cpp::fail(Error::kMalformedData); + } + } + + size_t samples_written = 0; + if (current_sample_) { + while (*current_sample_ < flac_->frame.header.block_size) { + if (samples_written + flac_->frame.header.channels >= output.size()) { + // We can't fit the next full PCM frame into the buffer. + return OutputInfo{.samples_written = samples_written, + .is_stream_finished = false}; + } + + for (int channel = 0; channel < flac_->frame.header.channels; channel++) { + output[samples_written++] = + sample::FromSigned(samples_by_channel_[channel][*current_sample_], + flac_->frame.header.bps); + } + (*current_sample_)++; + } + } + + current_sample_.reset(); + ESP_LOGI(kTag, "Samples written %lu", (uint32_t)samples_written); + return OutputInfo{.samples_written = samples_written, + .is_stream_finished = samples_written == 0 && is_eof}; +} + +auto MiniFlacDecoder::SeekTo(size_t target) -> cpp::result { + return {}; +} + +} // namespace codecs diff --git a/src/codecs/source_buffer.cpp b/src/codecs/source_buffer.cpp index 3b455038..0a986bc3 100644 --- a/src/codecs/source_buffer.cpp +++ b/src/codecs/source_buffer.cpp @@ -74,4 +74,9 @@ auto SourceBuffer::ConsumeBytes( } } +auto SourceBuffer::Empty() -> void { + offset_of_bytes_ = 0; + bytes_in_buffer_ = 0; +} + } // namespace codecs -- cgit v1.2.3 From 77145e56f4062cd060ee7fa0af9ad1a2e46df5b1 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 28 Feb 2024 21:21:23 +1100 Subject: basic working flac and mp3 seeking flac impl is fairly slow as it doesn't use the seek tables; for some reason miniflac seems to get *really* upset if you seek the stream. --- src/audio/audio_decoder.cpp | 1 - src/codecs/mad.cpp | 46 ++++++++++++++- src/codecs/miniflac.cpp | 140 +++++++------------------------------------- 3 files changed, 65 insertions(+), 122 deletions(-) (limited to 'src') diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp index 6c26dec8..68a8a86b 100644 --- a/src/audio/audio_decoder.cpp +++ b/src/audio/audio_decoder.cpp @@ -167,7 +167,6 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { auto Decoder::ContinueDecoding() -> bool { auto res = codec_->DecodeTo(codec_buffer_); if (res.has_error()) { - ESP_LOGI(kTag, "RAN INTO DECODING ERROR"); return true; } diff --git a/src/codecs/mad.cpp b/src/codecs/mad.cpp index 0617295f..b11821f0 100644 --- a/src/codecs/mad.cpp +++ b/src/codecs/mad.cpp @@ -44,6 +44,7 @@ MadMp3Decoder::MadMp3Decoder() mad_frame_init(frame_.get()); mad_synth_init(synth_.get()); } + MadMp3Decoder::~MadMp3Decoder() { mad_stream_finish(stream_.get()); mad_frame_finish(frame_.get()); @@ -58,7 +59,7 @@ auto MadMp3Decoder::GetBytesUsed() -> std::size_t { } } -auto MadMp3Decoder::OpenStream(std::shared_ptr input,uint32_t offset) +auto MadMp3Decoder::OpenStream(std::shared_ptr input, uint32_t offset) -> cpp::result { input_ = input; @@ -113,6 +114,45 @@ auto MadMp3Decoder::OpenStream(std::shared_ptr input,uint32_t offset) auto cbr_length = input->Size().value() / (header.bitrate / 8); output.total_samples = cbr_length * output.sample_rate_hz * channels; } + + mad_timer_t timer; + mad_timer_reset(&timer); + bool need_refill = false; + bool seek_err = false; + + while (mad_timer_count(timer, MAD_UNITS_SECONDS) < offset) { + if (seek_err) { + return cpp::fail(ICodec::Error::kMalformedData); + } + + if (need_refill && buffer_.Refill(input_.get())) { + return cpp::fail(ICodec::Error::kMalformedData); + } + need_refill = false; + + buffer_.ConsumeBytes([&](cpp::span buf) -> size_t { + mad_stream_buffer(stream_.get(), + reinterpret_cast(buf.data()), + buf.size()); + + while (mad_header_decode(&header, stream_.get()) < 0) { + if (MAD_RECOVERABLE(stream_->error)) { + continue; + } + if (stream_->error == MAD_ERROR_BUFLEN) { + need_refill = true; + return GetBytesUsed(); + } + // The error is unrecoverable. Give up. + seek_err = true; + return 0; + } + + mad_timer_add(&timer, header.duration); + return GetBytesUsed(); + }); + } + return output; } @@ -222,8 +262,8 @@ auto MadMp3Decoder::SkipID3Tags(IStream& stream) -> void { } /* - * Implementation taken from SDL_mixer and modified. Original is zlib-licensed, - * copyright (C) 1997-2022 Sam Lantinga + * Implementation taken from SDL_mixer and modified. Original is + * zlib-licensed, copyright (C) 1997-2022 Sam Lantinga */ auto MadMp3Decoder::GetVbrLength(const mad_header& header) -> std::optional { diff --git a/src/codecs/miniflac.cpp b/src/codecs/miniflac.cpp index 8ec12df5..45e063c7 100644 --- a/src/codecs/miniflac.cpp +++ b/src/codecs/miniflac.cpp @@ -5,6 +5,7 @@ */ #include "miniflac.hpp" +#include #include #include @@ -49,16 +50,16 @@ MiniFlacDecoder::~MiniFlacDecoder() { } } -auto MiniFlacDecoder::OpenStream(std::shared_ptr input,uint32_t offset) +auto MiniFlacDecoder::OpenStream(std::shared_ptr input, + uint32_t offset) -> cpp::result { input_ = input; MINIFLAC_RESULT res; auto read_until_result = [&](auto fn) { while (true) { - bool eof = buffer_.Refill(input_.get()); buffer_.ConsumeBytes(fn); - if (res == MINIFLAC_CONTINUE && !eof) { + if (res == MINIFLAC_CONTINUE && !buffer_.Refill(input_.get())) { continue; } break; @@ -111,137 +112,43 @@ auto MiniFlacDecoder::OpenStream(std::shared_ptr input,uint32_t offset) return cpp::fail(Error::kMalformedData); } - // Seeking - offset = 50; if (offset) { - // TODO: This assumes a constant sample_rate - uint64_t target_sample = sample_rate * offset; - uint32_t num_seekpoints; - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_seektable_seekpoints( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used, &num_seekpoints); - return bytes_used; - }); - if (res != MINIFLAC_OK) { - // TODO: Not having a seektable is not malformed - // but currently seeking will not work without it. - return cpp::fail(Error::kMalformedData); - } - // Loop over the seek table - ESP_LOGI(kTag, "Found seektable with %lu points", num_seekpoints); - uint64_t sample_number; - uint64_t sample_offset_bytes; - uint16_t num_samples_in_target; - for (uint32_t i = 0; i < num_seekpoints; i++) { - // Get sample number - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_seektable_sample_number( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used, &sample_number); - return bytes_used; - }); - if (res != MINIFLAC_OK) { - return cpp::fail(Error::kMalformedData); - } - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_seektable_sample_offset( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used, &sample_offset_bytes); - return bytes_used; - }); - if (res != MINIFLAC_OK) { - return cpp::fail(Error::kMalformedData); - } + // TODO: This assumes a constant sample_rate + uint64_t target_sample = sample_rate * offset; + ESP_LOGI(kTag, "seeking to %lu seconds (sample=%llu)", offset, + target_sample); + + while (true) { read_until_result([&](cpp::span buf) -> size_t { uint32_t bytes_used = 0; - res = miniflac_seektable_samples( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used, &num_samples_in_target); + res = miniflac_sync(flac_.get(), + reinterpret_cast(buf.data()), + buf.size_bytes(), &bytes_used); return bytes_used; }); + if (res != MINIFLAC_OK) { return cpp::fail(Error::kMalformedData); } - ESP_LOGI(kTag, "Seektable entry %lu", i); + if (!miniflac_is_frame(flac_.get())) { + continue; + } - // Check if we want to seek to this seektable position? - if (sample_number + num_samples_in_target >= target_sample) { - ESP_LOGI(kTag, "Break on Seektable entry %lu", i); + uint64_t samples_in_frame = miniflac_frame_block_size(flac_.get()); + if (samples_in_frame <= target_sample) { + target_sample -= samples_in_frame; + } else { break; } } - - // Seek forward to target_sample - if (sample_number > 0) { - ESP_LOGI(kTag, "total samples: %llu", total_samples); - ESP_LOGI(kTag, "TARGET SAMPLE: %llu", target_sample); - ESP_LOGI(kTag, "SAMPLE NUMBER: %llu", sample_number); - } - uint64_t byte_offset = sample_offset_bytes; - ESP_LOGI(kTag, "Byte offset is forward %llu bytes", byte_offset); - ESP_LOGI(kTag, "Decoder state pre: %d", flac_->state); - while(flac_.get()->state == MINIFLAC_METADATA) { - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_sync( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used); - return bytes_used; - }); - if (res != MINIFLAC_OK) { - ESP_LOGI(kTag, "Decoder error 1 %d", res); - } - } - ESP_LOGI(kTag, "Decoder state post: %d", flac_->state); - - - ESP_LOGI(kTag, "Going to skip forward %llu bytes", byte_offset); - if (input_.get()->CanSeek()) { - ESP_LOGI(kTag, "Skipping forward %llu bytes", byte_offset); - buffer_.Empty(); - input_.get()->SeekTo(byte_offset, IStream::SeekFrom::kCurrentPosition); - ESP_LOGI(kTag, "Skipped %llu bytes", byte_offset); - } - - - ESP_LOGI(kTag, "Pre-refill"); - buffer_.Refill(input_.get()); - ESP_LOGI(kTag, "Post-refill"); - - - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_sync( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used); - return bytes_used; - }); - if (res != MINIFLAC_OK) { - ESP_LOGI(kTag, "Decoder error 1 %d", res); - } - ESP_LOGI(kTag, "JOB'S DONE"); } - ESP_LOGI(kTag, "Decoder state: %d", flac_->state); - ESP_LOGI(kTag, "Frame header state: %d", flac_->frame.header.state); - - // TODO: Sample number is not guaranteed, could be block index. - ESP_LOGI(kTag, "Ended up... at sample %llu", flac_->frame.header.sample_number); - ESP_LOGI(kTag, "and block index: %lu", flac_->frame.header.frame_number); - ESP_LOGI(kTag, "total samples: %llu", total_samples); - - OutputFormat format{ .num_channels = static_cast(channels), .sample_rate_hz = static_cast(sample_rate), .total_samples = total_samples * channels, }; - return format; } @@ -253,7 +160,6 @@ auto MiniFlacDecoder::DecodeTo(cpp::span output) MINIFLAC_RESULT res = MINIFLAC_CONTINUE; while (res == MINIFLAC_CONTINUE && !is_eof) { is_eof = buffer_.Refill(input_.get()); - ESP_LOGI(kTag, "EOF? %s", is_eof ? "true" : "false"); buffer_.ConsumeBytes([&](cpp::span buf) -> size_t { // FIXME: We should do a miniflac_sync first, in order to check that // our sample buffers have enough space for the next frame. @@ -273,8 +179,7 @@ auto MiniFlacDecoder::DecodeTo(cpp::span output) .is_stream_finished = true, }; } else { - ESP_LOGI(kTag, "Failed: decoder result: %d", res); - // return cpp::fail(Error::kMalformedData); + return cpp::fail(Error::kMalformedData); } } @@ -297,7 +202,6 @@ auto MiniFlacDecoder::DecodeTo(cpp::span output) } current_sample_.reset(); - ESP_LOGI(kTag, "Samples written %lu", (uint32_t)samples_written); return OutputInfo{.samples_written = samples_written, .is_stream_finished = samples_written == 0 && is_eof}; } -- cgit v1.2.3 From d41f9f703375171d5766840c9edec32ff47bb25d Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 29 Feb 2024 12:08:12 +1100 Subject: Use drflac instead of miniflac This one is fast as hell! Does seeking really good too. Thank u Doctor Flac. --- src/audio/audio_converter.cpp | 6 +- src/codecs/CMakeLists.txt | 4 +- src/codecs/codec.cpp | 4 +- src/codecs/dr_flac.cpp | 119 ++++++++++++++++++++++ src/codecs/include/dr_flac.hpp | 46 +++++++++ src/codecs/include/miniflac.hpp | 51 ---------- src/codecs/miniflac.cpp | 213 ---------------------------------------- 7 files changed, 172 insertions(+), 271 deletions(-) create mode 100644 src/codecs/dr_flac.cpp create mode 100644 src/codecs/include/dr_flac.hpp delete mode 100644 src/codecs/include/miniflac.hpp delete mode 100644 src/codecs/miniflac.cpp (limited to 'src') diff --git a/src/audio/audio_converter.cpp b/src/audio/audio_converter.cpp index dc2fef95..946a0b63 100644 --- a/src/audio/audio_converter.cpp +++ b/src/audio/audio_converter.cpp @@ -77,9 +77,9 @@ auto SampleConverter::ConvertSamples(cpp::span input, reinterpret_cast(input.data()), input.size_bytes()}; size_t bytes_sent = 0; while (bytes_sent < input_as_bytes.size()) { - bytes_sent += - xStreamBufferSend(source_, input_as_bytes.subspan(bytes_sent).data(), - input_as_bytes.size() - bytes_sent, portMAX_DELAY); + bytes_sent += xStreamBufferSend( + source_, input_as_bytes.subspan(bytes_sent).data(), + input_as_bytes.size() - bytes_sent, pdMS_TO_TICKS(100)); } } diff --git a/src/codecs/CMakeLists.txt b/src/codecs/CMakeLists.txt index 4b296c84..b6481bd1 100644 --- a/src/codecs/CMakeLists.txt +++ b/src/codecs/CMakeLists.txt @@ -3,10 +3,10 @@ # SPDX-License-Identifier: GPL-3.0-only idf_component_register( - SRCS "miniflac.cpp" "codec.cpp" "mad.cpp" "opus.cpp" "vorbis.cpp" + SRCS "dr_flac.cpp" "codec.cpp" "mad.cpp" "opus.cpp" "vorbis.cpp" "source_buffer.cpp" "sample.cpp" "wav.cpp" INCLUDE_DIRS "include" - REQUIRES "result" "span" "libmad" "miniflac" "tremor" "opusfile" "memory" "util" + REQUIRES "result" "span" "libmad" "drflac" "tremor" "opusfile" "memory" "util" "komihash") target_compile_options("${COMPONENT_LIB}" PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/codecs/codec.cpp b/src/codecs/codec.cpp index 7bc591aa..a51c40d6 100644 --- a/src/codecs/codec.cpp +++ b/src/codecs/codec.cpp @@ -10,7 +10,7 @@ #include #include "mad.hpp" -#include "miniflac.hpp" +#include "dr_flac.hpp" #include "opus.hpp" #include "types.hpp" #include "vorbis.hpp" @@ -42,7 +42,7 @@ auto CreateCodecForType(StreamType type) -> std::optional { case StreamType::kVorbis: return new TremorVorbisDecoder(); case StreamType::kFlac: - return new MiniFlacDecoder(); + return new DrFlacDecoder(); case StreamType::kOpus: return new XiphOpusDecoder(); case StreamType::kWav: diff --git a/src/codecs/dr_flac.cpp b/src/codecs/dr_flac.cpp new file mode 100644 index 00000000..cacf7a6e --- /dev/null +++ b/src/codecs/dr_flac.cpp @@ -0,0 +1,119 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "dr_flac.hpp" + +#include +#include + +#include "codec.hpp" +#include "dr_flac.h" +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "result.hpp" +#include "sample.hpp" + +namespace codecs { + +[[maybe_unused]] static const char kTag[] = "flac"; + +static void* onMalloc(size_t sz, void* pUserData) { + return heap_caps_malloc(sz, MALLOC_CAP_SPIRAM); +} + +static void* onRealloc(void* p, size_t sz, void* pUserData) { + return heap_caps_realloc(p, sz, MALLOC_CAP_SPIRAM); +} + +static void onFree(void* p, void* pUserData) { + heap_caps_free(p); +} + +static drflac_allocation_callbacks kAllocCallbacks{ + .pUserData = nullptr, + .onMalloc = onMalloc, + .onRealloc = onRealloc, + .onFree = onFree, +}; + +static size_t readProc(void* pUserData, void* pBufferOut, size_t bytesToRead) { + IStream* stream = reinterpret_cast(pUserData); + ssize_t res = + stream->Read({reinterpret_cast(pBufferOut), bytesToRead}); + return res < 0 ? 0 : res; +} + +static drflac_bool32 seekProc(void* pUserData, + int offset, + drflac_seek_origin origin) { + IStream* stream = reinterpret_cast(pUserData); + if (!stream->CanSeek()) { + return DRFLAC_FALSE; + } + + IStream::SeekFrom seek_from; + switch (origin) { + case drflac_seek_origin_start: + seek_from = IStream::SeekFrom::kStartOfStream; + break; + case drflac_seek_origin_current: + seek_from = IStream::SeekFrom::kCurrentPosition; + break; + default: + return DRFLAC_FALSE; + } + stream->SeekTo(offset, seek_from); + + // FIXME: Detect falling off the end of the file. + return DRFLAC_TRUE; +} + +DrFlacDecoder::DrFlacDecoder() : input_(), flac_() {} + +DrFlacDecoder::~DrFlacDecoder() { + if (flac_) { + drflac_free(flac_, &kAllocCallbacks); + } +} + +auto DrFlacDecoder::OpenStream(std::shared_ptr input, uint32_t offset) + -> cpp::result { + input_ = input; + + flac_ = drflac_open(readProc, seekProc, input_.get(), &kAllocCallbacks); + if (!flac_) { + return cpp::fail(Error::kMalformedData); + } + + if (offset && !drflac_seek_to_pcm_frame(flac_, offset * flac_->sampleRate)) { + return cpp::fail(Error::kMalformedData); + } + + OutputFormat format{ + .num_channels = static_cast(flac_->channels), + .sample_rate_hz = static_cast(flac_->sampleRate), + .total_samples = flac_->totalPCMFrameCount * flac_->channels, + }; + return format; +} + +auto DrFlacDecoder::DecodeTo(cpp::span output) + -> cpp::result { + size_t frames_to_read = output.size() / flac_->channels / 2; + + auto frames_written = drflac_read_pcm_frames_s16( + flac_, output.size() / flac_->channels, output.data()); + + return OutputInfo{ + .samples_written = static_cast(frames_written * flac_->channels), + .is_stream_finished = frames_written < frames_to_read}; +} + +auto DrFlacDecoder::SeekTo(size_t target) -> cpp::result { + return {}; +} + +} // namespace codecs diff --git a/src/codecs/include/dr_flac.hpp b/src/codecs/include/dr_flac.hpp new file mode 100644 index 00000000..8dcfdaf5 --- /dev/null +++ b/src/codecs/include/dr_flac.hpp @@ -0,0 +1,46 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "dr_flac.h" +#include "sample.hpp" +#include "source_buffer.hpp" +#include "span.hpp" + +#include "codec.hpp" + +namespace codecs { + +class DrFlacDecoder : public ICodec { + public: + DrFlacDecoder(); + ~DrFlacDecoder(); + + auto OpenStream(std::shared_ptr input,uint32_t offset) + -> cpp::result override; + + auto DecodeTo(cpp::span destination) + -> cpp::result override; + + auto SeekTo(std::size_t target_sample) -> cpp::result override; + + DrFlacDecoder(const DrFlacDecoder&) = delete; + DrFlacDecoder& operator=(const DrFlacDecoder&) = delete; + + private: + std::shared_ptr input_; + drflac *flac_; +}; + +} // namespace codecs diff --git a/src/codecs/include/miniflac.hpp b/src/codecs/include/miniflac.hpp deleted file mode 100644 index d1daca2f..00000000 --- a/src/codecs/include/miniflac.hpp +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "miniflac.h" -#include "sample.hpp" -#include "source_buffer.hpp" -#include "span.hpp" - -#include "codec.hpp" - -namespace codecs { - -class MiniFlacDecoder : public ICodec { - public: - MiniFlacDecoder(); - ~MiniFlacDecoder(); - - auto OpenStream(std::shared_ptr input,uint32_t offset) - -> cpp::result override; - - auto DecodeTo(cpp::span destination) - -> cpp::result override; - - auto SeekTo(std::size_t target_sample) -> cpp::result override; - - MiniFlacDecoder(const MiniFlacDecoder&) = delete; - MiniFlacDecoder& operator=(const MiniFlacDecoder&) = delete; - - private: - std::shared_ptr input_; - SourceBuffer buffer_; - - std::unique_ptr flac_; - std::array samples_by_channel_; - std::optional current_sample_; -}; - -} // namespace codecs diff --git a/src/codecs/miniflac.cpp b/src/codecs/miniflac.cpp deleted file mode 100644 index 45e063c7..00000000 --- a/src/codecs/miniflac.cpp +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#include "miniflac.hpp" -#include - -#include -#include - -#include "esp_heap_caps.h" -#include "esp_log.h" -#include "miniflac.h" -#include "result.hpp" -#include "sample.hpp" - -namespace codecs { - -[[maybe_unused]] static const char kTag[] = "flac"; - -static constexpr size_t kMaxFrameSize = 4608; - -MiniFlacDecoder::MiniFlacDecoder() - : input_(), - buffer_(), - flac_(reinterpret_cast( - heap_caps_malloc(sizeof(miniflac_t), - MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT))), - current_sample_() { - miniflac_init(flac_.get(), MINIFLAC_CONTAINER_UNKNOWN); - for (int i = 0; i < samples_by_channel_.size(); i++) { - uint32_t caps; - if (i == 0) { - caps = MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL; - } else { - // FIXME: We can *almost* fit two channels into internal ram, but we're a - // few KiB shy of being able to do it safely. - caps = MALLOC_CAP_SPIRAM; - } - samples_by_channel_[i] = reinterpret_cast( - heap_caps_malloc(kMaxFrameSize * sizeof(int32_t), caps)); - } -} - -MiniFlacDecoder::~MiniFlacDecoder() { - for (int i = 0; i < samples_by_channel_.size(); i++) { - heap_caps_free(samples_by_channel_[i]); - } -} - -auto MiniFlacDecoder::OpenStream(std::shared_ptr input, - uint32_t offset) - -> cpp::result { - input_ = input; - - MINIFLAC_RESULT res; - auto read_until_result = [&](auto fn) { - while (true) { - buffer_.ConsumeBytes(fn); - if (res == MINIFLAC_CONTINUE && !buffer_.Refill(input_.get())) { - continue; - } - break; - } - }; - - uint32_t sample_rate = 0; - - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_streaminfo_sample_rate( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used, &sample_rate); - return bytes_used; - }); - - if (res != MINIFLAC_OK) { - return cpp::fail(Error::kMalformedData); - } - - uint8_t channels = 0; - - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_streaminfo_channels( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used, &channels); - return bytes_used; - }); - - if (res != MINIFLAC_OK) { - return cpp::fail(Error::kMalformedData); - } - - uint64_t total_samples = 0; - - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_streaminfo_total_samples( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used, &total_samples); - return bytes_used; - }); - - if (res != MINIFLAC_OK) { - return cpp::fail(Error::kMalformedData); - } - - if (channels == 0 || channels > 2) { - return cpp::fail(Error::kMalformedData); - } - - if (offset) { - // TODO: This assumes a constant sample_rate - uint64_t target_sample = sample_rate * offset; - ESP_LOGI(kTag, "seeking to %lu seconds (sample=%llu)", offset, - target_sample); - - while (true) { - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_sync(flac_.get(), - reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used); - return bytes_used; - }); - - if (res != MINIFLAC_OK) { - return cpp::fail(Error::kMalformedData); - } - - if (!miniflac_is_frame(flac_.get())) { - continue; - } - - uint64_t samples_in_frame = miniflac_frame_block_size(flac_.get()); - if (samples_in_frame <= target_sample) { - target_sample -= samples_in_frame; - } else { - break; - } - } - } - - OutputFormat format{ - .num_channels = static_cast(channels), - .sample_rate_hz = static_cast(sample_rate), - .total_samples = total_samples * channels, - }; - return format; -} - -auto MiniFlacDecoder::DecodeTo(cpp::span output) - -> cpp::result { - bool is_eof = false; - - if (!current_sample_) { - MINIFLAC_RESULT res = MINIFLAC_CONTINUE; - while (res == MINIFLAC_CONTINUE && !is_eof) { - is_eof = buffer_.Refill(input_.get()); - buffer_.ConsumeBytes([&](cpp::span buf) -> size_t { - // FIXME: We should do a miniflac_sync first, in order to check that - // our sample buffers have enough space for the next frame. - uint32_t bytes_read = 0; - res = miniflac_decode( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_read, samples_by_channel_.data()); - return bytes_read; - }); - } - - if (res == MINIFLAC_OK) { - current_sample_ = 0; - } else if (is_eof) { - return OutputInfo{ - .samples_written = 0, - .is_stream_finished = true, - }; - } else { - return cpp::fail(Error::kMalformedData); - } - } - - size_t samples_written = 0; - if (current_sample_) { - while (*current_sample_ < flac_->frame.header.block_size) { - if (samples_written + flac_->frame.header.channels >= output.size()) { - // We can't fit the next full PCM frame into the buffer. - return OutputInfo{.samples_written = samples_written, - .is_stream_finished = false}; - } - - for (int channel = 0; channel < flac_->frame.header.channels; channel++) { - output[samples_written++] = - sample::FromSigned(samples_by_channel_[channel][*current_sample_], - flac_->frame.header.bps); - } - (*current_sample_)++; - } - } - - current_sample_.reset(); - return OutputInfo{.samples_written = samples_written, - .is_stream_finished = samples_written == 0 && is_eof}; -} - -auto MiniFlacDecoder::SeekTo(size_t target) -> cpp::result { - return {}; -} - -} // namespace codecs -- cgit v1.2.3 From e7e6c70fb31d33ae1e79f9841f5b6fe227f6ebf3 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 29 Feb 2024 12:18:17 +1100 Subject: Remove unused 'SeekTo' method on codecs --- src/codecs/dr_flac.cpp | 4 - src/codecs/include/codec.hpp | 2 - src/codecs/include/dr_flac.hpp | 2 - src/codecs/include/mad.hpp | 2 - src/codecs/include/opus.hpp | 2 - src/codecs/include/vorbis.hpp | 2 - src/codecs/include/wav.hpp | 2 - src/codecs/mad.cpp | 5 - src/codecs/miniflac copy.cpp | 207 -------------------------------- src/codecs/miniflac.cpp.bak2 | 266 ----------------------------------------- src/codecs/opus.cpp | 14 +-- src/codecs/vorbis.cpp | 14 +-- src/codecs/wav.cpp | 4 - 13 files changed, 8 insertions(+), 518 deletions(-) delete mode 100644 src/codecs/miniflac copy.cpp delete mode 100644 src/codecs/miniflac.cpp.bak2 (limited to 'src') diff --git a/src/codecs/dr_flac.cpp b/src/codecs/dr_flac.cpp index cacf7a6e..2f9acf8c 100644 --- a/src/codecs/dr_flac.cpp +++ b/src/codecs/dr_flac.cpp @@ -112,8 +112,4 @@ auto DrFlacDecoder::DecodeTo(cpp::span output) .is_stream_finished = frames_written < frames_to_read}; } -auto DrFlacDecoder::SeekTo(size_t target) -> cpp::result { - return {}; -} - } // namespace codecs diff --git a/src/codecs/include/codec.hpp b/src/codecs/include/codec.hpp index fb1ec771..e48e3c58 100644 --- a/src/codecs/include/codec.hpp +++ b/src/codecs/include/codec.hpp @@ -130,8 +130,6 @@ class ICodec { */ virtual auto DecodeTo(cpp::span destination) -> cpp::result = 0; - - virtual auto SeekTo(size_t target_sample) -> cpp::result = 0; }; auto CreateCodecForType(StreamType type) -> std::optional; diff --git a/src/codecs/include/dr_flac.hpp b/src/codecs/include/dr_flac.hpp index 8dcfdaf5..547876f4 100644 --- a/src/codecs/include/dr_flac.hpp +++ b/src/codecs/include/dr_flac.hpp @@ -33,8 +33,6 @@ class DrFlacDecoder : public ICodec { auto DecodeTo(cpp::span destination) -> cpp::result override; - auto SeekTo(std::size_t target_sample) -> cpp::result override; - DrFlacDecoder(const DrFlacDecoder&) = delete; DrFlacDecoder& operator=(const DrFlacDecoder&) = delete; diff --git a/src/codecs/include/mad.hpp b/src/codecs/include/mad.hpp index 35e3284d..ead0b2a2 100644 --- a/src/codecs/include/mad.hpp +++ b/src/codecs/include/mad.hpp @@ -32,8 +32,6 @@ class MadMp3Decoder : public ICodec { auto DecodeTo(cpp::span destination) -> cpp::result override; - auto SeekTo(std::size_t target_sample) -> cpp::result override; - MadMp3Decoder(const MadMp3Decoder&) = delete; MadMp3Decoder& operator=(const MadMp3Decoder&) = delete; diff --git a/src/codecs/include/opus.hpp b/src/codecs/include/opus.hpp index 1431fa54..de2f7131 100644 --- a/src/codecs/include/opus.hpp +++ b/src/codecs/include/opus.hpp @@ -32,8 +32,6 @@ class XiphOpusDecoder : public ICodec { auto DecodeTo(cpp::span destination) -> cpp::result override; - auto SeekTo(std::size_t target_sample) -> cpp::result override; - XiphOpusDecoder(const XiphOpusDecoder&) = delete; XiphOpusDecoder& operator=(const XiphOpusDecoder&) = delete; diff --git a/src/codecs/include/vorbis.hpp b/src/codecs/include/vorbis.hpp index 94868c1a..3cf0f9ce 100644 --- a/src/codecs/include/vorbis.hpp +++ b/src/codecs/include/vorbis.hpp @@ -32,8 +32,6 @@ class TremorVorbisDecoder : public ICodec { auto DecodeTo(cpp::span destination) -> cpp::result override; - auto SeekTo(std::size_t target_sample) -> cpp::result override; - TremorVorbisDecoder(const TremorVorbisDecoder&) = delete; TremorVorbisDecoder& operator=(const TremorVorbisDecoder&) = delete; diff --git a/src/codecs/include/wav.hpp b/src/codecs/include/wav.hpp index e884a9bb..40138968 100644 --- a/src/codecs/include/wav.hpp +++ b/src/codecs/include/wav.hpp @@ -37,8 +37,6 @@ class WavDecoder : public ICodec { auto DecodeTo(cpp::span destination) -> cpp::result override; - auto SeekTo(std::size_t target_sample) -> cpp::result override; - WavDecoder(const WavDecoder&) = delete; WavDecoder& operator=(const WavDecoder&) = delete; diff --git a/src/codecs/mad.cpp b/src/codecs/mad.cpp index b11821f0..e44e9922 100644 --- a/src/codecs/mad.cpp +++ b/src/codecs/mad.cpp @@ -230,11 +230,6 @@ auto MadMp3Decoder::DecodeTo(cpp::span output) .is_stream_finished = is_eos_}; } -auto MadMp3Decoder::SeekTo(std::size_t target_sample) - -> cpp::result { - return {}; -} - auto MadMp3Decoder::SkipID3Tags(IStream& stream) -> void { // First check that the file actually does start with ID3 tags. std::array magic_buf{}; diff --git a/src/codecs/miniflac copy.cpp b/src/codecs/miniflac copy.cpp deleted file mode 100644 index 866cb49b..00000000 --- a/src/codecs/miniflac copy.cpp +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#include "miniflac.hpp" - -#include -#include - -#include "esp_heap_caps.h" -#include "esp_log.h" -#include "miniflac.h" -#include "result.hpp" -#include "sample.hpp" - -namespace codecs { - -[[maybe_unused]] static const char kTag[] = "flac"; - -static constexpr size_t kMaxFrameSize = 4608; - -MiniFlacDecoder::MiniFlacDecoder() - : input_(), - buffer_(), - flac_(reinterpret_cast( - heap_caps_malloc(sizeof(miniflac_t), - MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT))), - current_sample_() { - miniflac_init(flac_.get(), MINIFLAC_CONTAINER_UNKNOWN); - for (int i = 0; i < samples_by_channel_.size(); i++) { - uint32_t caps; - if (i == 0) { - caps = MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL; - } else { - // FIXME: We can *almost* fit two channels into internal ram, but we're a - // few KiB shy of being able to do it safely. - caps = MALLOC_CAP_SPIRAM; - } - samples_by_channel_[i] = reinterpret_cast( - heap_caps_malloc(kMaxFrameSize * sizeof(int32_t), caps)); - } -} - -MiniFlacDecoder::~MiniFlacDecoder() { - for (int i = 0; i < samples_by_channel_.size(); i++) { - heap_caps_free(samples_by_channel_[i]); - } -} - -auto MiniFlacDecoder::OpenStream(std::shared_ptr input,uint32_t offset) - -> cpp::result { - input_ = input; - - MINIFLAC_RESULT res; - auto read_until_result = [&](auto fn) { - while (true) { - bool eof = buffer_.Refill(input_.get()); - buffer_.ConsumeBytes(fn); - if (res == MINIFLAC_CONTINUE && !eof) { - continue; - } - break; - } - }; - - uint32_t sample_rate = 0; - - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_streaminfo_sample_rate( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used, &sample_rate); - return bytes_used; - }); - - if (res != MINIFLAC_OK) { - return cpp::fail(Error::kMalformedData); - } - - uint8_t channels = 0; - - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_streaminfo_channels( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used, &channels); - return bytes_used; - }); - - if (res != MINIFLAC_OK) { - return cpp::fail(Error::kMalformedData); - } - - uint64_t total_samples = 0; - - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_streaminfo_total_samples( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used, &total_samples); - return bytes_used; - }); - - if (res != MINIFLAC_OK) { - return cpp::fail(Error::kMalformedData); - } - - if (channels == 0 || channels > 2) { - return cpp::fail(Error::kMalformedData); - } - - if (offset) { - uint64_t samples_count = 0; - uint32_t offset_count = 0; - while (offset_count < offset) { - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_sync( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used); - return bytes_used; - }); - if (res != MINIFLAC_OK) { - return cpp::fail(Error::kMalformedData); - } - - uint32_t frame_samplerate = flac_.get()->frame.header.sample_rate; - uint16_t frame_blocksize = flac_.get()->frame.header.block_size; - if (!frame_samplerate || !frame_blocksize) { - continue; - } - - samples_count += frame_blocksize; - offset_count = samples_count / sample_rate; - } - } - - OutputFormat format{ - .num_channels = static_cast(channels), - .sample_rate_hz = static_cast(sample_rate), - .total_samples = total_samples * channels, - }; - - return format; -} - -auto MiniFlacDecoder::DecodeTo(cpp::span output) - -> cpp::result { - bool is_eof = false; - - if (!current_sample_) { - MINIFLAC_RESULT res = MINIFLAC_CONTINUE; - while (res == MINIFLAC_CONTINUE && !is_eof) { - is_eof = buffer_.Refill(input_.get()); - buffer_.ConsumeBytes([&](cpp::span buf) -> size_t { - // FIXME: We should do a miniflac_sync first, in order to check that - // our sample buffers have enough space for the next frame. - uint32_t bytes_read = 0; - res = miniflac_decode( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_read, samples_by_channel_.data()); - return bytes_read; - }); - } - - if (res == MINIFLAC_OK) { - current_sample_ = 0; - } else if (is_eof) { - return OutputInfo{ - .samples_written = 0, - .is_stream_finished = true, - }; - } else { - return cpp::fail(Error::kMalformedData); - } - } - - size_t samples_written = 0; - if (current_sample_) { - while (*current_sample_ < flac_->frame.header.block_size) { - if (samples_written + flac_->frame.header.channels >= output.size()) { - // We can't fit the next full PCM frame into the buffer. - return OutputInfo{.samples_written = samples_written, - .is_stream_finished = false}; - } - - for (int channel = 0; channel < flac_->frame.header.channels; channel++) { - output[samples_written++] = - sample::FromSigned(samples_by_channel_[channel][*current_sample_], - flac_->frame.header.bps); - } - (*current_sample_)++; - } - } - - current_sample_.reset(); - return OutputInfo{.samples_written = samples_written, - .is_stream_finished = samples_written == 0 && is_eof}; -} - -auto MiniFlacDecoder::SeekTo(size_t target) -> cpp::result { - return {}; -} - -} // namespace codecs diff --git a/src/codecs/miniflac.cpp.bak2 b/src/codecs/miniflac.cpp.bak2 deleted file mode 100644 index 843c3003..00000000 --- a/src/codecs/miniflac.cpp.bak2 +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#include "miniflac.hpp" - -#include -#include - -#include "esp_heap_caps.h" -#include "esp_log.h" -#include "miniflac.h" -#include "result.hpp" -#include "sample.hpp" - -namespace codecs { - -[[maybe_unused]] static const char kTag[] = "flac"; - -static constexpr size_t kMaxFrameSize = 4608; - -MiniFlacDecoder::MiniFlacDecoder() - : input_(), - buffer_(), - flac_(reinterpret_cast( - heap_caps_malloc(sizeof(miniflac_t), - MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT))), - current_sample_() { - miniflac_init(flac_.get(), MINIFLAC_CONTAINER_UNKNOWN); - for (int i = 0; i < samples_by_channel_.size(); i++) { - uint32_t caps; - if (i == 0) { - caps = MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL; - } else { - // FIXME: We can *almost* fit two channels into internal ram, but we're a - // few KiB shy of being able to do it safely. - caps = MALLOC_CAP_SPIRAM; - } - samples_by_channel_[i] = reinterpret_cast( - heap_caps_malloc(kMaxFrameSize * sizeof(int32_t), caps)); - } -} - -MiniFlacDecoder::~MiniFlacDecoder() { - for (int i = 0; i < samples_by_channel_.size(); i++) { - heap_caps_free(samples_by_channel_[i]); - } -} - -auto MiniFlacDecoder::OpenStream(std::shared_ptr input,uint32_t offset) - -> cpp::result { - input_ = input; - - MINIFLAC_RESULT res; - bool is_eof; - auto read_until_result = [&](auto fn) { - while (true) { - is_eof = buffer_.Refill(input_.get()); - buffer_.ConsumeBytes(fn); - if (res == MINIFLAC_CONTINUE && !eof) { - continue; - } - break; - } - }; - - uint16_t min_block_size = 0; // In samples - - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_streaminfo_min_block_size( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used, &min_block_size); - return bytes_used; - }); - - if (res != MINIFLAC_OK) { - return cpp::fail(Error::kMalformedData); - } - - uint16_t max_block_size = 0; // In samples - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_streaminfo_min_block_size( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used, &max_block_size); - return bytes_used; - }); - - if (res != MINIFLAC_OK) { - return cpp::fail(Error::kMalformedData); - } - - ESP_LOGI(kTag, "Blocksize min: %u max %u", min_block_size, max_block_size); - - uint32_t sample_rate = 0; - - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_streaminfo_sample_rate( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used, &sample_rate); - return bytes_used; - }); - - if (res != MINIFLAC_OK) { - return cpp::fail(Error::kMalformedData); - } - - uint8_t channels = 0; - - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_streaminfo_channels( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used, &channels); - return bytes_used; - }); - - if (res != MINIFLAC_OK) { - return cpp::fail(Error::kMalformedData); - } - - uint64_t total_samples = 0; - - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_streaminfo_total_samples( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used, &total_samples); - return bytes_used; - }); - - if (res != MINIFLAC_OK) { - return cpp::fail(Error::kMalformedData); - } - - if (channels == 0 || channels > 2) { - return cpp::fail(Error::kMalformedData); - } - - // Seeking - offset = 0; - if (offset) { - // Super dumb approach, but lets try it first - // Go to the first frame - while(flac_.get()->state == MINIFLAC_METADATA) { - read_until_result([&](cpp::span buf) -> size_t { - uint32_t bytes_used = 0; - res = miniflac_sync( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_used); - return bytes_used; - }); - if (res != MINIFLAC_OK) { - ESP_LOGI(kTag, "IT HAPPENED"); - } - } - ESP_LOGI(kTag, "Flac state: %d", flac_->state); - - // Naive approach - uint64_t byte_offset = offset; // TODO - - ESP_LOGI(kTag, "Going to skip forward %llu bytes", byte_offset); - if (input_.get()->CanSeek()) { - ESP_LOGI(kTag, "Skipping forward %llu bytes", byte_offset); - buffer_.Empty(); - input_.get()->SeekTo(byte_offset, IStream::SeekFrom::kCurrentPosition); - } - // buffer_.Refill(input_.get()); - - // // Sync again - // read_until_result([&](cpp::span buf) -> size_t { - // uint32_t bytes_used = 0; - // res = miniflac_sync( - // flac_.get(), reinterpret_cast(buf.data()), - // buf.size_bytes(), &bytes_used); - // return bytes_used; - // }); - // if (res != MINIFLAC_OK) { - // ESP_LOGI(kTag, "IT HAPPENED HERE! %d", res); - // } - - // ESP_LOGI(kTag, "Decoder state: %d", flac_->state); - // ESP_LOGI(kTag, "Frame header state: %d", flac_->frame.header.state); - - // // TODO: Sample number is not guaranteed, could be block index. - // ESP_LOGI(kTag, "Ended up... at sample %llu", flac_->frame.header.sample_number); - // ESP_LOGI(kTag, "and block index: %lu", flac_->frame.header.frame_number); - // ESP_LOGI(kTag, "total samples: %llu", total_samples); - } - - - OutputFormat format{ - .num_channels = static_cast(channels), - .sample_rate_hz = static_cast(sample_rate), - .total_samples = total_samples * channels, - }; - - return format; -} - -auto MiniFlacDecoder::DecodeTo(cpp::span output) - -> cpp::result { - bool is_eof = false; - - if (!current_sample_) { - MINIFLAC_RESULT res = MINIFLAC_CONTINUE; - while (res == MINIFLAC_CONTINUE && !is_eof) { - is_eof = buffer_.Refill(input_.get()); - ESP_LOGI(kTag, "EOF? %s", is_eof ? "true" : "false"); - buffer_.ConsumeBytes([&](cpp::span buf) -> size_t { - // FIXME: We should do a miniflac_sync first, in order to check that - // our sample buffers have enough space for the next frame. - uint32_t bytes_read = 0; - res = miniflac_decode( - flac_.get(), reinterpret_cast(buf.data()), - buf.size_bytes(), &bytes_read, samples_by_channel_.data()); - return bytes_read; - }); - } - - if (res == MINIFLAC_OK) { - current_sample_ = 0; - } else if (is_eof) { - return OutputInfo{ - .samples_written = 0, - .is_stream_finished = true, - }; - } else { - ESP_LOGI(kTag, "Failed: decoder result: %d", res); - return cpp::fail(Error::kMalformedData); - } - } - - size_t samples_written = 0; - if (current_sample_) { - while (*current_sample_ < flac_->frame.header.block_size) { - if (samples_written + flac_->frame.header.channels >= output.size()) { - // We can't fit the next full PCM frame into the buffer. - return OutputInfo{.samples_written = samples_written, - .is_stream_finished = false}; - } - - for (int channel = 0; channel < flac_->frame.header.channels; channel++) { - output[samples_written++] = - sample::FromSigned(samples_by_channel_[channel][*current_sample_], - flac_->frame.header.bps); - } - (*current_sample_)++; - } - } - - current_sample_.reset(); - ESP_LOGI(kTag, "Samples written %lu", (uint32_t)samples_written); - return OutputInfo{.samples_written = samples_written, - .is_stream_finished = samples_written == 0 && is_eof}; -} - -auto MiniFlacDecoder::SeekTo(size_t target) -> cpp::result { - return {}; -} - -} // namespace codecs diff --git a/src/codecs/opus.cpp b/src/codecs/opus.cpp index ec587bc7..a5220c4b 100644 --- a/src/codecs/opus.cpp +++ b/src/codecs/opus.cpp @@ -78,7 +78,8 @@ XiphOpusDecoder::~XiphOpusDecoder() { } } -auto XiphOpusDecoder::OpenStream(std::shared_ptr input,uint32_t offset) +auto XiphOpusDecoder::OpenStream(std::shared_ptr input, + uint32_t offset) -> cpp::result { input_ = input; @@ -128,8 +129,8 @@ auto XiphOpusDecoder::OpenStream(std::shared_ptr input,uint32_t offset) length = l * 2; } - if (offset) { - SeekTo(offset * 48000); + if (offset && op_pcm_seek(opus_, offset * 48000) != 0) { + return cpp::fail(Error::kInternalError); } return OutputFormat{ @@ -155,11 +156,4 @@ auto XiphOpusDecoder::DecodeTo(cpp::span output) }; } -auto XiphOpusDecoder::SeekTo(size_t target) -> cpp::result { - if (op_pcm_seek(opus_, target) != 0) { - return cpp::fail(Error::kInternalError); - } - return {}; -} - } // namespace codecs diff --git a/src/codecs/vorbis.cpp b/src/codecs/vorbis.cpp index ada92fb6..9131451b 100644 --- a/src/codecs/vorbis.cpp +++ b/src/codecs/vorbis.cpp @@ -77,7 +77,8 @@ TremorVorbisDecoder::~TremorVorbisDecoder() { ov_clear(vorbis_.get()); } -auto TremorVorbisDecoder::OpenStream(std::shared_ptr input,uint32_t offset) +auto TremorVorbisDecoder::OpenStream(std::shared_ptr input, + uint32_t offset) -> cpp::result { int res = ov_open_callbacks(input.get(), vorbis_.get(), NULL, 0, kCallbacks); if (res < 0) { @@ -117,8 +118,8 @@ auto TremorVorbisDecoder::OpenStream(std::shared_ptr input,uint32_t off length = l * info->channels; } - if (offset) { - ov_time_seek(vorbis_.get(), offset*1000); + if (offset && ov_time_seek(vorbis_.get(), offset * 1000) != 0) { + return cpp::fail(Error::kInternalError); } return OutputFormat{ @@ -149,11 +150,4 @@ auto TremorVorbisDecoder::DecodeTo(cpp::span output) }; } -auto TremorVorbisDecoder::SeekTo(size_t target) -> cpp::result { - if (ov_pcm_seek(vorbis_.get(), target) != 0) { - return cpp::fail(Error::kInternalError); - } - return {}; -} - } // namespace codecs diff --git a/src/codecs/wav.cpp b/src/codecs/wav.cpp index 5dd6f031..143a7a4b 100644 --- a/src/codecs/wav.cpp +++ b/src/codecs/wav.cpp @@ -248,10 +248,6 @@ auto WavDecoder::DecodeTo(cpp::span output) .is_stream_finished = samples_written == 0 && is_eof}; } -auto WavDecoder::SeekTo(size_t target) -> cpp::result { - return {}; -} - auto codecs::WavDecoder::GetFormat() const -> uint16_t { if (wave_format_ == kWaveFormatExtensible) { return subformat_; -- cgit v1.2.3 From 173b09b0151ae765b1a8e69dfb60d14d502801f6 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 29 Feb 2024 15:47:21 +1100 Subject: Clear the drain buffer when skipping between tracks --- src/audio/audio_fsm.cpp | 53 +++++++++++++++++++++++++++------- src/audio/bt_audio_output.cpp | 2 +- src/audio/i2s_audio_output.cpp | 2 +- src/audio/include/audio_events.hpp | 8 ++++- src/audio/include/audio_fsm.hpp | 7 +++-- src/audio/include/audio_sink.hpp | 17 +++++++++-- src/audio/include/bt_audio_output.hpp | 5 ++-- src/audio/include/i2s_audio_output.hpp | 5 ++-- src/audio/include/track_queue.hpp | 3 ++ src/audio/track_queue.cpp | 43 ++++++++++++++++----------- 10 files changed, 106 insertions(+), 39 deletions(-) (limited to 'src') diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index ea0315eb..08a0941a 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -51,6 +51,10 @@ std::shared_ptr AudioState::sI2SOutput; std::shared_ptr AudioState::sBtOutput; std::shared_ptr AudioState::sOutput; +// Two seconds of samples for two channels, at a representative sample rate. +constexpr size_t kDrainBufferSize = sizeof(sample::Sample) * 48000 * 4; +StreamBufferHandle_t AudioState::sDrainBuffer; + std::optional AudioState::sCurrentTrack; bool AudioState::sIsPlaybackAllowed; @@ -129,7 +133,7 @@ void AudioState::react(const SetVolumeBalance& ev) { void AudioState::react(const OutputModeChanged& ev) { ESP_LOGI(kTag, "output mode changed"); auto new_mode = sServices->nvs().OutputMode(); - sOutput->SetMode(IAudioOutput::Modes::kOff); + sOutput->mode(IAudioOutput::Modes::kOff); switch (new_mode) { case drivers::NvsStorage::Output::kBluetooth: sOutput = sBtOutput; @@ -138,7 +142,7 @@ void AudioState::react(const OutputModeChanged& ev) { sOutput = sI2SOutput; break; } - sOutput->SetMode(IAudioOutput::Modes::kOnPaused); + sOutput->mode(IAudioOutput::Modes::kOnPaused); sSampleConverter->SetOutput(sOutput); // Bluetooth volume isn't 'changed' until we've connected to a device. @@ -150,6 +154,32 @@ void AudioState::react(const OutputModeChanged& ev) { } } +auto AudioState::clearDrainBuffer() -> void { + // Tell the decoder to stop adding new samples. This might not take effect + // immediately, since the decoder might currently be stuck waiting for space + // to become available in the drain buffer. + sFileSource->SetPath(); + + auto mode = sOutput->mode(); + if (mode == IAudioOutput::Modes::kOnPlaying) { + // If we're currently playing, then the drain buffer will be actively + // draining on its own. Just keep trying to reset until it works. + while (xStreamBufferReset(sDrainBuffer) != pdPASS) { + } + } else { + // If we're not currently playing, then we need to actively pull samples + // out of the drain buffer to unblock the decoder. + while (!xStreamBufferIsEmpty(sDrainBuffer)) { + // Read a little to unblock the decoder. + uint8_t drain[2048]; + xStreamBufferReceive(sDrainBuffer, drain, sizeof(drain), 0); + + // Try to quickly discard the rest. + xStreamBufferReset(sDrainBuffer); + } + } +} + auto AudioState::playTrack(database::TrackId id) -> void { sCurrentTrack = id; sServices->bg_worker().Dispatch([=]() { @@ -194,10 +224,6 @@ void AudioState::react(const TogglePlayPause& ev) { namespace states { -// Two seconds of samples for two channels, at a representative sample rate. -constexpr size_t kDrainBufferSize = sizeof(sample::Sample) * 48000 * 4; -static StreamBufferHandle_t sDrainBuffer; - void Uninitialised::react(const system_fsm::BootComplete& ev) { sServices = ev.services; @@ -229,7 +255,7 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { } else { sOutput = sBtOutput; } - sOutput->SetMode(IAudioOutput::Modes::kOnPaused); + sOutput->mode(IAudioOutput::Modes::kOnPaused); events::Ui().Dispatch(VolumeLimitChanged{ .new_limit_db = @@ -272,6 +298,7 @@ void Standby::react(const QueueUpdate& ev) { if (!current_track || (sCurrentTrack && (*sCurrentTrack == *current_track))) { return; } + clearDrainBuffer(); playTrack(*current_track); } @@ -315,7 +342,7 @@ void Standby::react(const system_fsm::StorageMounted& ev) { void Playback::entry() { ESP_LOGI(kTag, "beginning playback"); - sOutput->SetMode(IAudioOutput::Modes::kOnPlaying); + sOutput->mode(IAudioOutput::Modes::kOnPlaying); events::System().Dispatch(PlaybackStarted{}); events::Ui().Dispatch(PlaybackStarted{}); @@ -323,10 +350,10 @@ void Playback::entry() { void Playback::exit() { ESP_LOGI(kTag, "finishing playback"); - sOutput->SetMode(IAudioOutput::Modes::kOnPaused); + sOutput->mode(IAudioOutput::Modes::kOnPaused); - // Stash the current volume now, in case it changed during playback, since we - // might be powering off soon. + // Stash the current volume now, in case it changed during playback, since + // we might be powering off soon. commitVolume(); events::System().Dispatch(PlaybackStopped{}); @@ -343,6 +370,10 @@ void Playback::react(const QueueUpdate& ev) { if (!ev.current_changed) { return; } + // Cut the current track immediately. + if (ev.reason == QueueUpdate::Reason::kExplicitUpdate) { + clearDrainBuffer(); + } auto current_track = sServices->track_queue().current(); if (!current_track) { sFileSource->SetPath(); diff --git a/src/audio/bt_audio_output.cpp b/src/audio/bt_audio_output.cpp index 41c89069..dff98e36 100644 --- a/src/audio/bt_audio_output.cpp +++ b/src/audio/bt_audio_output.cpp @@ -35,7 +35,7 @@ BluetoothAudioOutput::BluetoothAudioOutput(StreamBufferHandle_t s, BluetoothAudioOutput::~BluetoothAudioOutput() {} -auto BluetoothAudioOutput::SetMode(Modes mode) -> void { +auto BluetoothAudioOutput::changeMode(Modes mode) -> void { if (mode == Modes::kOnPlaying) { bluetooth_.SetSource(stream()); } else { diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp index 4043574e..cd61d97f 100644 --- a/src/audio/i2s_audio_output.cpp +++ b/src/audio/i2s_audio_output.cpp @@ -58,7 +58,7 @@ I2SAudioOutput::~I2SAudioOutput() { dac_->SetSource(nullptr); } -auto I2SAudioOutput::SetMode(Modes mode) -> void { +auto I2SAudioOutput::changeMode(Modes mode) -> void { if (mode == current_mode_) { return; } diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp index 03584062..a79ca4ec 100644 --- a/src/audio/include/audio_events.hpp +++ b/src/audio/include/audio_events.hpp @@ -14,7 +14,6 @@ #include "tinyfsm.hpp" #include "track.hpp" -#include "track_queue.hpp" #include "types.hpp" namespace audio { @@ -39,6 +38,13 @@ struct PlaybackStopped : tinyfsm::Event {}; struct QueueUpdate : tinyfsm::Event { bool current_changed; + + enum Reason { + kExplicitUpdate, + kRepeatingLastTrack, + kTrackFinished, + }; + Reason reason; }; struct PlayFile : tinyfsm::Event { diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp index 29ec489a..2d335e74 100644 --- a/src/audio/include/audio_fsm.hpp +++ b/src/audio/include/audio_fsm.hpp @@ -52,7 +52,7 @@ class AudioState : public tinyfsm::Fsm { void react(const OutputModeChanged&); virtual void react(const system_fsm::BootComplete&) {} - virtual void react(const system_fsm::KeyLockChanged&) {}; + virtual void react(const system_fsm::KeyLockChanged&){}; virtual void react(const system_fsm::StorageMounted&) {} virtual void react(const system_fsm::BluetoothEvent&); @@ -67,6 +67,7 @@ class AudioState : public tinyfsm::Fsm { virtual void react(const internal::AudioPipelineIdle&) {} protected: + auto clearDrainBuffer() -> void; auto playTrack(database::TrackId id) -> void; auto commitVolume() -> void; @@ -79,6 +80,8 @@ class AudioState : public tinyfsm::Fsm { static std::shared_ptr sBtOutput; static std::shared_ptr sOutput; + static StreamBufferHandle_t sDrainBuffer; + static std::optional sCurrentTrack; auto readyToPlay() -> bool; @@ -91,7 +94,7 @@ class Uninitialised : public AudioState { public: void react(const system_fsm::BootComplete&) override; - void react(const system_fsm::BluetoothEvent&) override {}; + void react(const system_fsm::BluetoothEvent&) override{}; using AudioState::react; }; diff --git a/src/audio/include/audio_sink.hpp b/src/audio/include/audio_sink.hpp index eba55eb5..85c23f5c 100644 --- a/src/audio/include/audio_sink.hpp +++ b/src/audio/include/audio_sink.hpp @@ -27,7 +27,8 @@ class IAudioOutput { StreamBufferHandle_t stream_; public: - IAudioOutput(StreamBufferHandle_t stream) : stream_(stream) {} + IAudioOutput(StreamBufferHandle_t stream) + : stream_(stream), mode_(Modes::kOff) {} virtual ~IAudioOutput() {} @@ -41,7 +42,14 @@ class IAudioOutput { * Indicates whether this output is currently being sent samples. If this is * false, the output should place itself into a low power state. */ - virtual auto SetMode(Modes) -> void = 0; + auto mode(Modes m) -> void { + if (mode_ == m) { + return; + } + changeMode(m); + mode_ = m; + } + auto mode() -> Modes { return mode_; } virtual auto SetVolumeImbalance(int_fast8_t balance) -> void = 0; @@ -67,6 +75,11 @@ class IAudioOutput { virtual auto Configure(const Format& format) -> void = 0; auto stream() -> StreamBufferHandle_t { return stream_; } + + protected: + Modes mode_; + + virtual auto changeMode(Modes new_mode) -> void = 0; }; } // namespace audio diff --git a/src/audio/include/bt_audio_output.hpp b/src/audio/include/bt_audio_output.hpp index f6d2200c..a61e718a 100644 --- a/src/audio/include/bt_audio_output.hpp +++ b/src/audio/include/bt_audio_output.hpp @@ -28,8 +28,6 @@ class BluetoothAudioOutput : public IAudioOutput { tasks::WorkerPool&); ~BluetoothAudioOutput(); - auto SetMode(Modes) -> void override; - auto SetVolumeImbalance(int_fast8_t balance) -> void override; auto SetVolume(uint16_t) -> void override; @@ -48,6 +46,9 @@ class BluetoothAudioOutput : public IAudioOutput { BluetoothAudioOutput(const BluetoothAudioOutput&) = delete; BluetoothAudioOutput& operator=(const BluetoothAudioOutput&) = delete; + protected: + auto changeMode(Modes) -> void override; + private: drivers::Bluetooth& bluetooth_; tasks::WorkerPool& bg_worker_; diff --git a/src/audio/include/i2s_audio_output.hpp b/src/audio/include/i2s_audio_output.hpp index 7c297106..5f3fc3ff 100644 --- a/src/audio/include/i2s_audio_output.hpp +++ b/src/audio/include/i2s_audio_output.hpp @@ -23,8 +23,6 @@ class I2SAudioOutput : public IAudioOutput { I2SAudioOutput(StreamBufferHandle_t, drivers::IGpios& expander); ~I2SAudioOutput(); - auto SetMode(Modes) -> void override; - auto SetMaxVolume(uint16_t) -> void; auto SetVolumeDb(uint16_t) -> void; @@ -46,6 +44,9 @@ class I2SAudioOutput : public IAudioOutput { I2SAudioOutput(const I2SAudioOutput&) = delete; I2SAudioOutput& operator=(const I2SAudioOutput&) = delete; + protected: + auto changeMode(Modes) -> void override; + private: drivers::IGpios& expander_; std::unique_ptr dac_; diff --git a/src/audio/include/track_queue.hpp b/src/audio/include/track_queue.hpp index e4fd7881..5b7c9448 100644 --- a/src/audio/include/track_queue.hpp +++ b/src/audio/include/track_queue.hpp @@ -12,6 +12,7 @@ #include #include +#include "audio_events.hpp" #include "cppbor_parse.h" #include "database.hpp" #include "tasks.hpp" @@ -120,6 +121,8 @@ class TrackQueue { TrackQueue& operator=(const TrackQueue&) = delete; private: + auto next(QueueUpdate::Reason r) -> void; + mutable std::shared_mutex mutex_; tasks::WorkerPool& bg_worker_; diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp index b75230fc..ccadd3a6 100644 --- a/src/audio/track_queue.cpp +++ b/src/audio/track_queue.cpp @@ -33,6 +33,8 @@ namespace audio { [[maybe_unused]] static constexpr char kTag[] = "tracks"; +using Reason = QueueUpdate::Reason; + RandomIterator::RandomIterator() : seed_(0), pos_(0), size_(0), replay_(false) {} @@ -72,8 +74,11 @@ auto RandomIterator::replay(bool r) -> void { replay_ = r; } -auto notifyChanged(bool current_changed) -> void { - QueueUpdate ev{.current_changed = current_changed}; +auto notifyChanged(bool current_changed, Reason reason) -> void { + QueueUpdate ev{ + .current_changed = current_changed, + .reason = reason, + }; events::Ui().Dispatch(ev); events::Audio().Dispatch(ev); } @@ -157,7 +162,7 @@ auto TrackQueue::insert(Item i, size_t index) -> void { update_shuffler(); } } - notifyChanged(current_changed); + notifyChanged(current_changed, Reason::kExplicitUpdate); } else if (std::holds_alternative(i)) { // Iterators can be very large, and retrieving items from them often // requires disk i/o. Handle them asynchronously so that inserting them @@ -185,7 +190,7 @@ auto TrackQueue::insert(Item i, size_t index) -> void { const std::unique_lock lock(mutex_); update_shuffler(); } - notifyChanged(current_changed); + notifyChanged(current_changed, Reason::kExplicitUpdate); }); } } @@ -200,6 +205,10 @@ auto TrackQueue::append(Item i) -> void { } auto TrackQueue::next() -> void { + next(Reason::kExplicitUpdate); +} + +auto TrackQueue::next(Reason r) -> void { bool changed = true; { @@ -221,7 +230,7 @@ auto TrackQueue::next() -> void { } } - notifyChanged(changed); + notifyChanged(changed, r); } auto TrackQueue::previous() -> void { @@ -245,22 +254,22 @@ auto TrackQueue::previous() -> void { } } - notifyChanged(changed); + notifyChanged(changed, Reason::kExplicitUpdate); } auto TrackQueue::finish() -> void { if (repeat_) { - notifyChanged(true); + notifyChanged(true, Reason::kRepeatingLastTrack); } else { - next(); + next(Reason::kTrackFinished); } } auto TrackQueue::skipTo(database::TrackId id) -> void { // Defer this work to the background not because it's particularly - // long-running (although it could be), but because we want to ensure we only - // search for the given id after any previously pending iterator insertions - // have finished. + // long-running (although it could be), but because we want to ensure we + // only search for the given id after any previously pending iterator + // insertions have finished. bg_worker_.Dispatch([=, this]() { bool found = false; { @@ -274,7 +283,7 @@ auto TrackQueue::skipTo(database::TrackId id) -> void { } } if (found) { - notifyChanged(true); + notifyChanged(true, Reason::kExplicitUpdate); } }); } @@ -294,7 +303,7 @@ auto TrackQueue::clear() -> void { } } - notifyChanged(true); + notifyChanged(true, Reason::kExplicitUpdate); } auto TrackQueue::random(bool en) -> void { @@ -311,7 +320,7 @@ auto TrackQueue::random(bool en) -> void { } // Current track doesn't get randomised until next(). - notifyChanged(false); + notifyChanged(false, Reason::kExplicitUpdate); } auto TrackQueue::random() const -> bool { @@ -325,7 +334,7 @@ auto TrackQueue::repeat(bool en) -> void { repeat_ = en; } - notifyChanged(false); + notifyChanged(false, Reason::kExplicitUpdate); } auto TrackQueue::repeat() const -> bool { @@ -341,7 +350,7 @@ auto TrackQueue::replay(bool en) -> void { shuffle_->replay(en); } } - notifyChanged(false); + notifyChanged(false, Reason::kExplicitUpdate); } auto TrackQueue::replay() const -> bool { @@ -477,7 +486,7 @@ auto TrackQueue::deserialise(const std::string& s) -> void { QueueParseClient client{*this}; const uint8_t* data = reinterpret_cast(s.data()); cppbor::parse(data, data + s.size(), &client); - notifyChanged(true); + notifyChanged(true, Reason::kExplicitUpdate); } } // namespace audio -- cgit v1.2.3 From b2f0e6d3a45083b04e85feccb3f7742a35d6e41f Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 29 Feb 2024 16:30:17 +1100 Subject: Clear the drain buffer also when seeking --- src/audio/audio_fsm.cpp | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 50f18452..d4272c3d 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -290,10 +290,12 @@ void Playback::react(const PlayFile& ev) { } void Standby::react(const SeekFile& ev) { + clearDrainBuffer(); sFileSource->SetPath(ev.filename, ev.offset); } void Playback::react(const SeekFile& ev) { + clearDrainBuffer(); sFileSource->SetPath(ev.filename, ev.offset); } -- cgit v1.2.3 From 14552881900bb3ed0e9ed2d4a732e4104b32ccfa Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 6 Mar 2024 13:59:33 +1100 Subject: Restore the previous track position when booting --- src/audio/audio_fsm.cpp | 43 +++++++++++++++++++++++++++++++++++--- src/audio/include/audio_events.hpp | 1 + src/audio/track_queue.cpp | 2 +- src/database/database.cpp | 8 +++++-- 4 files changed, 48 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index d4272c3d..05c7c216 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -13,6 +13,8 @@ #include "audio_sink.hpp" #include "bluetooth_types.hpp" +#include "cppbor.h" +#include "cppbor_parse.h" #include "esp_heap_caps.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" @@ -58,6 +60,8 @@ StreamBufferHandle_t AudioState::sDrainBuffer; std::optional AudioState::sCurrentTrack; bool AudioState::sIsPlaybackAllowed; +static std::optional> sLastTrackUpdate; + void AudioState::react(const system_fsm::BluetoothEvent& ev) { if (ev.event != drivers::bluetooth::Event::kConnectionStateChanged) { return; @@ -310,11 +314,15 @@ void Standby::react(const QueueUpdate& ev) { if (!current_track || (sCurrentTrack && (*sCurrentTrack == *current_track))) { return; } + if (ev.reason == QueueUpdate::Reason::kDeserialised && sLastTrackUpdate) { + return; + } clearDrainBuffer(); playTrack(*current_track); } static const char kQueueKey[] = "audio:queue"; +static const char kCurrentFileKey[] = "audio:current"; void Standby::react(const system_fsm::KeyLockChanged& ev) { if (!ev.locking) { @@ -332,6 +340,14 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) { return; } db->put(kQueueKey, queue.serialise()); + + if (sLastTrackUpdate) { + cppbor::Array current_track{ + cppbor::Tstr{sLastTrackUpdate->first}, + cppbor::Uint{sLastTrackUpdate->second}, + }; + db->put(kCurrentFileKey, current_track.toString()); + } }); } @@ -341,13 +357,32 @@ void Standby::react(const system_fsm::StorageMounted& ev) { if (!db) { return; } - auto res = db->get(kQueueKey); - if (res) { + + // Restore the currently playing file before restoring the queue. This way, + // we can fall back to restarting the queue's current track if there's any + // issue restoring the current file. + auto current = db->get(kCurrentFileKey); + if (current) { + // Again, ensure we don't boot-loop by trying to play a track that causes + // a crash over and over again. + db->put(kCurrentFileKey, ""); + auto [parsed, unused, err] = cppbor::parse( + reinterpret_cast(current->data()), current->size()); + if (parsed->type() == cppbor::ARRAY) { + std::string filename = parsed->asArray()->get(0)->asTstr()->value(); + uint32_t pos = parsed->asArray()->get(1)->asUint()->value(); + sLastTrackUpdate = std::make_pair(filename, pos); + sFileSource->SetPath(filename, pos); + } + } + + auto queue = db->get(kQueueKey); + if (queue) { // Don't restore the same queue again. This ideally should do nothing, // but guards against bad edge cases where restoring the queue ends up // causing a crash. db->put(kQueueKey, ""); - sServices->track_queue().deserialise(*res); + sServices->track_queue().deserialise(*queue); } }); } @@ -399,6 +434,7 @@ void Playback::react(const QueueUpdate& ev) { void Playback::react(const PlaybackUpdate& ev) { ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed, ev.track->duration); + sLastTrackUpdate = std::make_pair(ev.track->filepath, ev.seconds_elapsed); } void Playback::react(const internal::InputFileOpened& ev) {} @@ -407,6 +443,7 @@ void Playback::react(const internal::InputFileClosed& ev) {} void Playback::react(const internal::InputFileFinished& ev) { ESP_LOGI(kTag, "finished playing file"); + sLastTrackUpdate.reset(); sServices->track_queue().finish(); if (!sServices->track_queue().current()) { for (int i = 0; i < 20; i++) { diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp index d55e4e0d..a8533646 100644 --- a/src/audio/include/audio_events.hpp +++ b/src/audio/include/audio_events.hpp @@ -44,6 +44,7 @@ struct QueueUpdate : tinyfsm::Event { kExplicitUpdate, kRepeatingLastTrack, kTrackFinished, + kDeserialised, }; Reason reason; }; diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp index ccadd3a6..a3f4c815 100644 --- a/src/audio/track_queue.cpp +++ b/src/audio/track_queue.cpp @@ -486,7 +486,7 @@ auto TrackQueue::deserialise(const std::string& s) -> void { QueueParseClient client{*this}; const uint8_t* data = reinterpret_cast(s.data()); cppbor::parse(data, data + s.size(), &client); - notifyChanged(true, Reason::kExplicitUpdate); + notifyChanged(true, Reason::kDeserialised); } } // namespace audio diff --git a/src/database/database.cpp b/src/database/database.cpp index ec11455b..ca92cf6b 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -229,13 +229,17 @@ auto Database::sizeOnDiskBytes() -> size_t { } auto Database::put(const std::string& key, const std::string& val) -> void { - db_->Put(leveldb::WriteOptions{}, kKeyCustom + key, val); + if (val.empty()) { + db_->Delete(leveldb::WriteOptions{}, kKeyCustom + key); + } else { + db_->Put(leveldb::WriteOptions{}, kKeyCustom + key, val); + } } auto Database::get(const std::string& key) -> std::optional { std::string val; auto res = db_->Get(leveldb::ReadOptions{}, kKeyCustom + key, &val); - if (!res.ok()) { + if (!res.ok() || val.empty()) { return {}; } return val; -- cgit v1.2.3 From ef72b25660912ff247997089abfb93e9f0b52809 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 7 Mar 2024 10:59:03 +1100 Subject: use prototype inheritance for lua screens, rather than functions this gives us a way to give each screen nice little hooks, like 'onShown' and 'onHidden'. later we can use these hooks to disable bindings for screens that aren't in-use. --- src/lua/CMakeLists.txt | 1 + src/lua/bridge.cpp | 2 ++ src/lua/include/lua_screen.hpp | 15 +++++++++ src/lua/lua_screen.cpp | 75 ++++++++++++++++++++++++++++++++++++++++++ src/ui/include/screen.hpp | 3 ++ src/ui/include/screen_lua.hpp | 3 ++ src/ui/screen_lua.cpp | 36 ++++++++++++++++++++ src/ui/ui_fsm.cpp | 58 ++++++++++++++++++++------------ 8 files changed, 172 insertions(+), 21 deletions(-) create mode 100644 src/lua/include/lua_screen.hpp create mode 100644 src/lua/lua_screen.cpp (limited to 'src') diff --git a/src/lua/CMakeLists.txt b/src/lua/CMakeLists.txt index ff0831c9..cee738bd 100644 --- a/src/lua/CMakeLists.txt +++ b/src/lua/CMakeLists.txt @@ -5,6 +5,7 @@ idf_component_register( SRCS "lua_thread.cpp" "bridge.cpp" "property.cpp" "lua_database.cpp" "lua_queue.cpp" "lua_version.cpp" "lua_controls.cpp" "registry.cpp" + "lua_screen.cpp" INCLUDE_DIRS "include" REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "esp-idf-lua" "luavgl" "lua-linenoise" "lua-term" diff --git a/src/lua/bridge.cpp b/src/lua/bridge.cpp index a26f74bb..2bef1c30 100644 --- a/src/lua/bridge.cpp +++ b/src/lua/bridge.cpp @@ -19,6 +19,7 @@ #include "lua_controls.hpp" #include "lua_database.hpp" #include "lua_queue.hpp" +#include "lua_screen.hpp" #include "lua_version.hpp" #include "lvgl.h" @@ -84,6 +85,7 @@ auto Bridge::installBaseModules(lua_State* L) -> void { RegisterDatabaseModule(L); RegisterQueueModule(L); RegisterVersionModule(L); + RegisterScreenModule(L); } auto Bridge::installLvgl(lua_State* L) -> void { diff --git a/src/lua/include/lua_screen.hpp b/src/lua/include/lua_screen.hpp new file mode 100644 index 00000000..1c3bed1a --- /dev/null +++ b/src/lua/include/lua_screen.hpp @@ -0,0 +1,15 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include "lua.hpp" + +namespace lua { + +auto RegisterScreenModule(lua_State*) -> void; + +} // namespace lua diff --git a/src/lua/lua_screen.cpp b/src/lua/lua_screen.cpp new file mode 100644 index 00000000..27843bc7 --- /dev/null +++ b/src/lua/lua_screen.cpp @@ -0,0 +1,75 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "lua_screen.hpp" + +#include +#include + +#include "lua.hpp" + +#include "esp_log.h" +#include "lauxlib.h" +#include "lua.h" +#include "lvgl.h" + +#include "bridge.hpp" +#include "database.hpp" +#include "event_queue.hpp" +#include "index.hpp" +#include "property.hpp" +#include "service_locator.hpp" +#include "track.hpp" +#include "track_queue.hpp" +#include "ui_events.hpp" + +namespace lua { + +static auto screen_new(lua_State* L) -> int { + // o = o or {} + if (lua_gettop(L) != 2) { + lua_settop(L, 1); + lua_newtable(L); + } + // Swap o and self on the stack. + lua_insert(L, 1); + + lua_pushliteral(L, "__index"); + lua_pushvalue(L, 1); + lua_settable(L, 1); // self.__index = self + + lua_setmetatable(L, 1); // setmetatable(o, self) + + return 1; // return o +} + +static auto screen_noop(lua_State* state) -> int { + return 0; +} + +static const struct luaL_Reg kScreenFuncs[] = {{"new", screen_new}, + {"createUi", screen_noop}, + {"onShown", screen_noop}, + {"onHidden", screen_noop}, + {NULL, NULL}}; + +static auto lua_screen(lua_State* state) -> int { + luaL_newlib(state, kScreenFuncs); + + lua_pushliteral(state, "__index"); + lua_pushvalue(state, -2); + lua_rawset(state, -3); + + return 1; +} + +auto RegisterScreenModule(lua_State* s) -> void { + luaL_requiref(s, "screen", lua_screen, true); + + lua_pop(s, 1); +} + +} // namespace lua diff --git a/src/ui/include/screen.hpp b/src/ui/include/screen.hpp index 60939660..4241c712 100644 --- a/src/ui/include/screen.hpp +++ b/src/ui/include/screen.hpp @@ -27,6 +27,9 @@ class Screen { Screen(); virtual ~Screen(); + virtual auto onShown() -> void {} + virtual auto onHidden() -> void {} + auto root() -> lv_obj_t* { return root_; } auto content() -> lv_obj_t* { return content_; } auto alert() -> lv_obj_t* { return alert_; } diff --git a/src/ui/include/screen_lua.hpp b/src/ui/include/screen_lua.hpp index ee9f6813..0ed3a508 100644 --- a/src/ui/include/screen_lua.hpp +++ b/src/ui/include/screen_lua.hpp @@ -18,6 +18,9 @@ class Lua : public Screen { Lua(); ~Lua(); + auto onShown() -> void override; + auto onHidden() -> void override; + auto SetObjRef(lua_State*) -> void; private: diff --git a/src/ui/screen_lua.cpp b/src/ui/screen_lua.cpp index 5130b4f7..d6c7a26f 100644 --- a/src/ui/screen_lua.cpp +++ b/src/ui/screen_lua.cpp @@ -7,8 +7,10 @@ #include "screen_lua.hpp" #include "core/lv_obj_tree.h" +#include "lua.h" #include "lua.hpp" +#include "lua_thread.hpp" #include "luavgl.h" namespace ui { @@ -22,6 +24,40 @@ Lua::~Lua() { } } +auto Lua::onShown() -> void { + if (!s_ || !obj_ref_) { + return; + } + lua_rawgeti(s_, LUA_REGISTRYINDEX, *obj_ref_); + lua_pushliteral(s_, "onShown"); + + if (lua_gettable(s_, -2) == LUA_TFUNCTION) { + lua_pushvalue(s_, -2); + lua::CallProtected(s_, 1, 0); + } else { + lua_pop(s_, 1); + } + + lua_pop(s_, 1); +} + +auto Lua::onHidden() -> void { + if (!s_ || !obj_ref_) { + return; + } + lua_rawgeti(s_, LUA_REGISTRYINDEX, *obj_ref_); + lua_pushliteral(s_, "onHidden"); + + if (lua_gettable(s_, -2) == LUA_TFUNCTION) { + lua_pushvalue(s_, -2); + lua::CallProtected(s_, 1, 0); + } else { + lua_pop(s_, 1); + } + + lua_pop(s_, 1); +} + auto Lua::SetObjRef(lua_State* s) -> void { assert(s_ == nullptr); s_ = s; diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index d98e435d..5c22e90e 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -125,21 +125,24 @@ lua::Property UiState::sPlaybackPlaying{ }}; lua::Property UiState::sPlaybackTrack{}; -lua::Property UiState::sPlaybackPosition{0, [](const lua::LuaValue& val) { - int current_val = std::get(sPlaybackPosition.Get()); - if (!std::holds_alternative(val)) { - return false; - } - int new_val = std::get(val); - if (current_val != new_val) { - auto track = sPlaybackTrack.Get(); - if (!std::holds_alternative(track)) { +lua::Property UiState::sPlaybackPosition{ + 0, [](const lua::LuaValue& val) { + int current_val = std::get(sPlaybackPosition.Get()); + if (!std::holds_alternative(val)) { return false; } - events::Audio().Dispatch(audio::SeekFile{.offset = (uint32_t)new_val, .filename = std::get(track).filepath}); - } - return true; -}}; + int new_val = std::get(val); + if (current_val != new_val) { + auto track = sPlaybackTrack.Get(); + if (!std::holds_alternative(track)) { + return false; + } + events::Audio().Dispatch(audio::SeekFile{ + .offset = (uint32_t)new_val, + .filename = std::get(track).filepath}); + } + return true; + }}; lua::Property UiState::sQueuePosition{0}; lua::Property UiState::sQueueSize{0}; @@ -294,21 +297,29 @@ auto UiState::InitBootSplash(drivers::IGpios& gpios) -> bool { } void UiState::PushScreen(std::shared_ptr screen) { + lv_obj_set_parent(sAlertContainer, screen->alert()); + if (sCurrentScreen) { + sCurrentScreen->onHidden(); sScreens.push(sCurrentScreen); } sCurrentScreen = screen; - lv_obj_set_parent(sAlertContainer, sCurrentScreen->alert()); + sCurrentScreen->onShown(); } int UiState::PopScreen() { if (sScreens.empty()) { return 0; } - sCurrentScreen = sScreens.top(); - lv_obj_set_parent(sAlertContainer, sCurrentScreen->alert()); + lv_obj_set_parent(sAlertContainer, sScreens.top()->alert()); + + sCurrentScreen->onHidden(); + sCurrentScreen = sScreens.top(); sScreens.pop(); + + sCurrentScreen->onShown(); + return sScreens.size(); } @@ -539,7 +550,7 @@ void Lua::entry() { auto Lua::PushLuaScreen(lua_State* s) -> int { // Ensure the arg looks right before continuing. - luaL_checktype(s, 1, LUA_TFUNCTION); + luaL_checktype(s, 1, LUA_TTABLE); // First, create a new plain old Screen object. We will use its root and // group for the Lua screen. Allocate it in external ram so that arbitrarily @@ -554,10 +565,15 @@ auto Lua::PushLuaScreen(lua_State* s) -> int { lv_group_set_default(new_screen->group()); // Call the constructor for this screen. - lua_settop(s, 1); // Make sure the function is actually at top of stack - lua::CallProtected(s, 0, 1); + // lua_settop(s, 1); // Make sure the screen is actually at top of stack + lua_pushliteral(s, "createUi"); + if (lua_gettable(s, 1) == LUA_TFUNCTION) { + lua_pushvalue(s, 1); + lua::CallProtected(s, 1, 0); + } - // Store the reference for the table the constructor returned. + // Store the reference for this screen's table. + lua_settop(s, 1); new_screen->SetObjRef(s); // Finally, push the now-initialised screen as if it were a regular C++ @@ -585,7 +601,7 @@ auto Lua::PopLuaScreen(lua_State* s) -> int { } auto Lua::Ticks(lua_State* s) -> int { - lua_pushinteger(s, esp_timer_get_time()/1000); + lua_pushinteger(s, esp_timer_get_time() / 1000); return 1; } -- cgit v1.2.3 From eba5adeb8cc606b4d685132248c6481c0aca53f6 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 7 Mar 2024 11:16:56 +1100 Subject: Show the now playing screen after being locked for a while --- src/ui/include/ui_fsm.hpp | 1 + src/ui/ui_fsm.cpp | 4 ++++ 2 files changed, 5 insertions(+) (limited to 'src') diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index 6cf2ba4c..07937559 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -129,6 +129,7 @@ class UiState : public tinyfsm::Fsm { static lua::Property sControlsScheme; static lua::Property sScrollSensitivity; + static lua::Property sLockSwitch; static lua::Property sDatabaseUpdating; }; diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 5c22e90e..1a9f01b4 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -280,6 +280,8 @@ lua::Property UiState::sScrollSensitivity{ return true; }}; +lua::Property UiState::sLockSwitch{false}; + lua::Property UiState::sDatabaseUpdating{false}; auto UiState::InitBootSplash(drivers::IGpios& gpios) -> bool { @@ -326,6 +328,7 @@ int UiState::PopScreen() { void UiState::react(const system_fsm::KeyLockChanged& ev) { sDisplay->SetDisplayOn(!ev.locking); sInput->lock(ev.locking); + sLockSwitch.Update(ev.locking); } void UiState::react(const internal::ControlSchemeChanged&) { @@ -516,6 +519,7 @@ void Lua::entry() { { {"scheme", &sControlsScheme}, {"scroll_sensitivity", &sScrollSensitivity}, + {"lock_switch", &sLockSwitch}, }); registry.AddPropertyModule( -- cgit v1.2.3 From a78614a5806c9800956f10f993e1c70b74fbf323 Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 7 Mar 2024 12:12:32 +1100 Subject: WIP: Getting styles from lua --- src/lua/CMakeLists.txt | 4 +- src/lua/bridge.cpp | 2 + src/lua/include/lua_theme.hpp | 15 ++++++++ src/lua/lua_theme.cpp | 89 +++++++++++++++++++++++++++++++++++++++++++ src/ui/CMakeLists.txt | 2 +- 5 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 src/lua/include/lua_theme.hpp create mode 100644 src/lua/lua_theme.cpp (limited to 'src') diff --git a/src/lua/CMakeLists.txt b/src/lua/CMakeLists.txt index ff0831c9..72e48aa0 100644 --- a/src/lua/CMakeLists.txt +++ b/src/lua/CMakeLists.txt @@ -3,8 +3,8 @@ # SPDX-License-Identifier: GPL-3.0-only idf_component_register( - SRCS "lua_thread.cpp" "bridge.cpp" "property.cpp" "lua_database.cpp" - "lua_queue.cpp" "lua_version.cpp" "lua_controls.cpp" "registry.cpp" + SRCS "lua_theme.cpp" "lua_thread.cpp" "bridge.cpp" "property.cpp" "lua_database.cpp" + "lua_queue.cpp" "lua_version.cpp" "lua_theme.cpp" "lua_controls.cpp" "registry.cpp" INCLUDE_DIRS "include" REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "esp-idf-lua" "luavgl" "lua-linenoise" "lua-term" diff --git a/src/lua/bridge.cpp b/src/lua/bridge.cpp index a26f74bb..44be06f8 100644 --- a/src/lua/bridge.cpp +++ b/src/lua/bridge.cpp @@ -20,6 +20,7 @@ #include "lua_database.hpp" #include "lua_queue.hpp" #include "lua_version.hpp" +#include "lua_theme.hpp" #include "lvgl.h" #include "font/lv_font_loader.h" @@ -84,6 +85,7 @@ auto Bridge::installBaseModules(lua_State* L) -> void { RegisterDatabaseModule(L); RegisterQueueModule(L); RegisterVersionModule(L); + RegisterThemeModule(L); } auto Bridge::installLvgl(lua_State* L) -> void { diff --git a/src/lua/include/lua_theme.hpp b/src/lua/include/lua_theme.hpp new file mode 100644 index 00000000..fed710e0 --- /dev/null +++ b/src/lua/include/lua_theme.hpp @@ -0,0 +1,15 @@ +/* + * Copyright 2024 ailurux + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include "lua.hpp" + +namespace lua { + +auto RegisterThemeModule(lua_State*) -> void; + +} // namespace lua diff --git a/src/lua/lua_theme.cpp b/src/lua/lua_theme.cpp new file mode 100644 index 00000000..a95e634b --- /dev/null +++ b/src/lua/lua_theme.cpp @@ -0,0 +1,89 @@ + +/* + * Copyright 2023 ailurux + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "lua_version.hpp" + +#include + +#include "bridge.hpp" +#include "lua.hpp" + +#include "esp_app_desc.h" +#include "esp_log.h" +#include "lauxlib.h" +#include "lua.h" +#include "lua_thread.hpp" +#include "luavgl.h" +#include "themes.hpp" + +namespace lua { + +static auto set_theme(lua_State* L) -> int { + // lv_style_t* style = luavgl_to_style(L, -1); + // if (style == NULL) { + // ESP_LOGI("DANIEL", "Style was null or malformed??"); + // return 0; + // } + + // ESP_LOGI("DANIEL", "GOT ONE!"); + // themes::Theme::instance()->...; + + /* table is in the stack at index 't' */ + std::string class_name; + lua_pushnil(L); /* first key */ + while (lua_next(L, -2) != 0) { + /* uses 'key' (at index -2) and 'value' (at index -1) */ + if (lua_type(L, -2) == LUA_TSTRING) { + class_name = lua_tostring(L, -2); + } + if (lua_type(L, -1) == LUA_TTABLE) { + // Nesting + lua_pushnil(L); // First key + while (lua_next(L, -2) != 0) { + // Nesting the second + int selector = -1; + lv_style_t* style = NULL; + lua_pushnil(L); // First key + while (lua_next(L, -2) != 0) { + int idx = lua_tointeger(L, -2); + if (idx == 1) { + // Selector + selector = lua_tointeger(L, -1); + } else if (idx == 2) { + // Style + lv_style_t* style = luavgl_to_style(L, -1); + if (style == NULL) { + ESP_LOGI("DANIEL", "Style was null or malformed??"); + return 0; + } else { + ESP_LOGI("DANIEL", "Got style for class %s with selector %d", class_name.c_str(), selector); + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); + } + } + /* removes 'value'; keeps 'key' for next iteration */ + lua_pop(L, 1); + } + return 0; +} + +static const struct luaL_Reg kThemeFuncs[] = {{"set", set_theme}, {NULL, NULL}}; + +static auto lua_theme(lua_State* L) -> int { + luaL_newlib(L, kThemeFuncs); + return 1; +} + +auto RegisterThemeModule(lua_State* L) -> void { + luaL_requiref(L, "theme", lua_theme, true); + lua_pop(L, 1); +} + +} // namespace lua diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 6d45fc9f..81bd983b 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-only idf_component_register( - SRCS "lvgl_task.cpp" "ui_fsm.cpp" "screen_splash.cpp" "encoder_input.cpp" + SRCS "themes copy.cpp" "lvgl_task.cpp" "ui_fsm.cpp" "screen_splash.cpp" "encoder_input.cpp" "themes.cpp" "screen.cpp" "modal.cpp" "screen_lua.cpp" "splash.c" "font_fusion_12.c" "font_fusion_10.c" INCLUDE_DIRS "include" -- cgit v1.2.3 From 312b70f9f6a2e3d7d387dfe3502f12f091e8fe37 Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 7 Mar 2024 14:20:06 +1100 Subject: WIP: Base styles are applied --- src/lua/lua_theme.cpp | 1 + src/ui/CMakeLists.txt | 2 +- src/ui/include/themes.hpp | 25 ++----- src/ui/themes.cpp | 179 +++++----------------------------------------- 4 files changed, 27 insertions(+), 180 deletions(-) (limited to 'src') diff --git a/src/lua/lua_theme.cpp b/src/lua/lua_theme.cpp index a95e634b..7b007f4d 100644 --- a/src/lua/lua_theme.cpp +++ b/src/lua/lua_theme.cpp @@ -60,6 +60,7 @@ static auto set_theme(lua_State* L) -> int { ESP_LOGI("DANIEL", "Style was null or malformed??"); return 0; } else { + ui::themes::Theme::instance()->AddStyle(class_name, selector, style); ESP_LOGI("DANIEL", "Got style for class %s with selector %d", class_name.c_str(), selector); } } diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 81bd983b..6d45fc9f 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-only idf_component_register( - SRCS "themes copy.cpp" "lvgl_task.cpp" "ui_fsm.cpp" "screen_splash.cpp" "encoder_input.cpp" + SRCS "lvgl_task.cpp" "ui_fsm.cpp" "screen_splash.cpp" "encoder_input.cpp" "themes.cpp" "screen.cpp" "modal.cpp" "screen_lua.cpp" "splash.c" "font_fusion_12.c" "font_fusion_10.c" INCLUDE_DIRS "include" diff --git a/src/ui/include/themes.hpp b/src/ui/include/themes.hpp index 11680c0d..65462f65 100644 --- a/src/ui/include/themes.hpp +++ b/src/ui/include/themes.hpp @@ -1,5 +1,8 @@ #pragma once +#include +#include +#include #include "lvgl.h" namespace ui { @@ -21,29 +24,15 @@ class Theme { void Callback(lv_obj_t* obj); void ApplyStyle(lv_obj_t* obj, Style style); + void AddStyle(std::string key, int selector, lv_style_t* style); + static auto instance() -> Theme*; private: Theme(); - - lv_style_t base_style_; - lv_style_t base_focused_style_; - - lv_style_t button_style_; - lv_style_t bar_style_; - lv_style_t dropdown_style_; - lv_style_t dropdown_list_style_; - - lv_style_t slider_indicator_style_; - lv_style_t slider_knob_style_; - lv_style_t slider_knob_focused_style_; - - lv_style_t switch_style_; - lv_style_t switch_indicator_style_; - lv_style_t switch_indicator_checked_style_; - lv_style_t switch_knob_style_; - + std::map>> style_map; lv_theme_t theme_; + }; } // namespace themes } // namespace ui diff --git a/src/ui/themes.cpp b/src/ui/themes.cpp index f8390570..87b1f92b 100644 --- a/src/ui/themes.cpp +++ b/src/ui/themes.cpp @@ -19,84 +19,6 @@ static void theme_apply_cb(lv_theme_t* th, lv_obj_t* obj) { } Theme::Theme() { - lv_style_init(&base_style_); - lv_style_set_bg_opa(&base_style_, LV_OPA_TRANSP); - lv_style_set_text_font(&base_style_, &font_fusion_12); - lv_style_set_text_color(&base_style_, lv_color_black()); - - lv_style_init(&base_focused_style_); - lv_style_set_bg_opa(&base_focused_style_, LV_OPA_COVER); - lv_style_set_bg_color(&base_focused_style_, - lv_palette_lighten(LV_PALETTE_BLUE, 5)); - - lv_style_init(&button_style_); - lv_style_set_pad_left(&button_style_, 2); - lv_style_set_pad_right(&button_style_, 2); - lv_style_set_pad_top(&button_style_, 1); - lv_style_set_pad_bottom(&button_style_, 1); - lv_style_set_bg_color(&button_style_, lv_color_white()); - lv_style_set_radius(&button_style_, 5); - - lv_style_init(&bar_style_); - lv_style_set_bg_opa(&bar_style_, LV_OPA_COVER); - lv_style_set_radius(&bar_style_, LV_RADIUS_CIRCLE); - - lv_style_init(&slider_indicator_style_); - lv_style_set_radius(&slider_indicator_style_, LV_RADIUS_CIRCLE); - lv_style_set_bg_color(&slider_indicator_style_, - lv_palette_main(LV_PALETTE_BLUE)); - - lv_style_init(&slider_knob_style_); - lv_style_set_radius(&slider_knob_style_, LV_RADIUS_CIRCLE); - lv_style_set_pad_all(&slider_knob_style_, 2); - lv_style_set_bg_color(&slider_knob_style_, lv_color_white()); - lv_style_set_shadow_width(&slider_knob_style_, 5); - lv_style_set_shadow_opa(&slider_knob_style_, LV_OPA_COVER); - - lv_style_init(&slider_knob_focused_style_); - lv_style_set_bg_color(&slider_knob_focused_style_, - lv_palette_lighten(LV_PALETTE_BLUE, 4)); - - lv_style_init(&switch_style_); - lv_style_set_width(&switch_style_, 28); - lv_style_set_height(&switch_style_, 18); - lv_style_set_radius(&switch_style_, LV_RADIUS_CIRCLE); - - lv_style_init(&switch_knob_style_); - lv_style_set_pad_all(&switch_knob_style_, -2); - lv_style_set_radius(&switch_knob_style_, LV_RADIUS_CIRCLE); - lv_style_set_bg_opa(&switch_knob_style_, LV_OPA_COVER); - lv_style_set_bg_color(&switch_knob_style_, lv_color_white()); - - lv_style_init(&slider_knob_focused_style_); - lv_style_set_bg_color(&slider_knob_focused_style_, - lv_palette_lighten(LV_PALETTE_BLUE, 4)); - - lv_style_init(&switch_indicator_style_); - lv_style_set_radius(&switch_indicator_style_, LV_RADIUS_CIRCLE); - lv_style_set_bg_opa(&switch_indicator_style_, LV_OPA_COVER); - lv_style_set_bg_color(&switch_indicator_style_, - lv_palette_main(LV_PALETTE_GREY)); - - lv_style_init(&switch_indicator_checked_style_); - lv_style_set_bg_color(&switch_indicator_checked_style_, - lv_palette_main(LV_PALETTE_BLUE)); - - lv_style_init(&dropdown_style_); - lv_style_set_radius(&dropdown_style_, 2); - lv_style_set_pad_all(&dropdown_style_, 2); - lv_style_set_border_width(&dropdown_style_, 1); - lv_style_set_border_color(&dropdown_style_, lv_palette_main(LV_PALETTE_BLUE)); - lv_style_set_border_side(&dropdown_style_, LV_BORDER_SIDE_FULL); - - lv_style_init(&dropdown_list_style_); - lv_style_set_radius(&dropdown_list_style_, 2); - lv_style_set_border_width(&dropdown_list_style_, 1); - lv_style_set_border_color(&dropdown_list_style_, lv_palette_main(LV_PALETTE_BLUE_GREY)); - lv_style_set_bg_opa(&dropdown_list_style_, LV_OPA_COVER); - lv_style_set_bg_color(&dropdown_list_style_, lv_color_white()); - lv_style_set_pad_all(&dropdown_list_style_, 2); - lv_theme_t* parent_theme = lv_disp_get_theme(NULL); theme_ = *parent_theme; theme_.user_data = this; @@ -111,98 +33,33 @@ void Theme::Apply(void) { } void Theme::Callback(lv_obj_t* obj) { - lv_obj_add_style(obj, &base_style_, LV_PART_MAIN); - lv_obj_add_style(obj, &base_focused_style_, LV_PART_SELECTED); - lv_obj_add_style(obj, &base_focused_style_, LV_STATE_FOCUSED); - - if (lv_obj_check_type(obj, &lv_btn_class)) { - lv_obj_add_style(obj, &button_style_, LV_PART_MAIN); - } else if (lv_obj_check_type(obj, &lv_bar_class)) { - lv_obj_add_style(obj, &bar_style_, LV_PART_MAIN); - } else if (lv_obj_check_type(obj, &lv_slider_class)) { - lv_obj_add_style(obj, &bar_style_, LV_PART_MAIN); - lv_obj_add_style(obj, &slider_indicator_style_, LV_PART_INDICATOR); - lv_obj_add_style(obj, &slider_knob_style_, LV_PART_KNOB); - lv_obj_add_style(obj, &slider_knob_focused_style_, LV_STATE_FOCUSED); - } else if (lv_obj_check_type(obj, &lv_switch_class)) { - lv_obj_add_style(obj, &switch_style_, LV_PART_MAIN); - lv_obj_add_style(obj, &switch_indicator_style_, LV_PART_INDICATOR); - lv_obj_add_style(obj, &switch_indicator_checked_style_, - LV_PART_INDICATOR | LV_STATE_CHECKED); - lv_obj_add_style(obj, &switch_knob_style_, LV_PART_KNOB); - } else if (lv_obj_check_type(obj, &lv_dropdown_class)) { - lv_obj_add_style(obj, &dropdown_style_, LV_PART_MAIN); - } else if (lv_obj_check_type(obj, &lv_dropdownlist_class)) { - lv_obj_add_style(obj, &dropdown_list_style_, LV_PART_MAIN); + // Find and apply base styles + if (auto search = style_map.find("base"); search != style_map.end()) { + for (const auto& pair : search->second) { + lv_obj_add_style(obj, pair.second, pair.first); + } } -} - -void Theme::ApplyStyle(lv_obj_t* obj, Style style) { - switch (style) { - case Style::kTopBar: - lv_obj_set_style_pad_bottom(obj, 1, LV_PART_MAIN); - - lv_obj_set_style_shadow_width(obj, 6, LV_PART_MAIN); - lv_obj_set_style_shadow_opa(obj, LV_OPA_COVER, LV_PART_MAIN); - lv_obj_set_style_shadow_ofs_x(obj, 0, LV_PART_MAIN); - break; - case Style::kPopup: - lv_obj_set_style_shadow_width(obj, 6, LV_PART_MAIN); - lv_obj_set_style_shadow_opa(obj, LV_OPA_COVER, LV_PART_MAIN); - lv_obj_set_style_shadow_ofs_x(obj, 0, LV_PART_MAIN); - lv_obj_set_style_shadow_ofs_y(obj, 0, LV_PART_MAIN); - - lv_obj_set_style_radius(obj, 5, LV_PART_MAIN); - - lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, LV_PART_MAIN); - lv_obj_set_style_bg_color(obj, lv_color_white(), LV_PART_MAIN); - - lv_obj_set_style_pad_top(obj, 2, LV_PART_MAIN); - lv_obj_set_style_pad_bottom(obj, 2, LV_PART_MAIN); - lv_obj_set_style_pad_left(obj, 2, LV_PART_MAIN); - lv_obj_set_style_pad_right(obj, 2, LV_PART_MAIN); - break; - case Style::kTab: - lv_obj_set_style_radius(obj, 0, LV_PART_MAIN); - lv_obj_set_style_border_width(obj, 1, LV_STATE_CHECKED); - lv_obj_set_style_border_color(obj, lv_palette_main(LV_PALETTE_BLUE), - LV_STATE_CHECKED); - lv_obj_set_style_border_side(obj, LV_BORDER_SIDE_BOTTOM, - LV_STATE_CHECKED); - break; - case Style::kButtonPrimary: - lv_obj_set_style_border_width(obj, 1, LV_PART_MAIN); - lv_obj_set_style_border_color(obj, lv_palette_main(LV_PALETTE_BLUE), - LV_PART_MAIN); - lv_obj_set_style_border_side(obj, LV_BORDER_SIDE_FULL, LV_PART_MAIN); - break; - case Style::kMenuSubheadFirst: - case Style::kMenuSubhead: - lv_obj_set_style_text_color(obj, lv_palette_darken(LV_PALETTE_GREY, 3), - LV_PART_MAIN); - lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN); + // TODO: Apply widget style - lv_obj_set_style_border_width(obj, 1, LV_PART_MAIN); - lv_obj_set_style_border_color(obj, lv_palette_lighten(LV_PALETTE_GREY, 3), - LV_PART_MAIN); - - if (style == Style::kMenuSubhead) { - lv_obj_set_style_border_side( - obj, LV_BORDER_SIDE_TOP | LV_BORDER_SIDE_BOTTOM, LV_PART_MAIN); - } else { - lv_obj_set_style_border_side(obj, LV_BORDER_SIDE_BOTTOM, LV_PART_MAIN); - } - break; - default: - break; - } } +void Theme::ApplyStyle(lv_obj_t* obj, Style style) {} + auto Theme::instance() -> Theme* { static Theme sTheme{}; return &sTheme; } +void Theme::AddStyle(std::string key, int selector, lv_style_t* style) { + style_map.try_emplace(key, std::vector>{}); + if (auto search = style_map.find(key); search != style_map.end()) { + // Key exists + auto &vec = search->second; + // Add it to the list + vec.push_back(std::make_pair(selector, style)); + } +} + } // namespace themes } // namespace ui -- cgit v1.2.3 From dc74bc1de9dd56c4146232622140b56e90dcc43d Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 7 Mar 2024 15:46:42 +1100 Subject: Add other styles to lua theme --- src/ui/themes.cpp | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/ui/themes.cpp b/src/ui/themes.cpp index 87b1f92b..4fd477ab 100644 --- a/src/ui/themes.cpp +++ b/src/ui/themes.cpp @@ -40,11 +40,92 @@ void Theme::Callback(lv_obj_t* obj) { } } - // TODO: Apply widget style + // Determine class name + std::string class_name; + if (lv_obj_check_type(obj, &lv_btn_class)) { + class_name = "button"; + } else if (lv_obj_check_type(obj, &lv_bar_class)) { + class_name = "bar"; + } else if (lv_obj_check_type(obj, &lv_slider_class)) { + class_name = "slider"; + } else if (lv_obj_check_type(obj, &lv_switch_class)) { + class_name = "switch"; + } else if (lv_obj_check_type(obj, &lv_dropdown_class)) { + class_name = "dropdown"; + } else if (lv_obj_check_type(obj, &lv_dropdownlist_class)) { + class_name = "dropdownlist"; + } + + // Apply all styles from class + if (auto search = style_map.find(class_name); search != style_map.end()) { + for (const auto& pair : search->second) { + lv_obj_add_style(obj, pair.second, pair.first); + } + } } -void Theme::ApplyStyle(lv_obj_t* obj, Style style) {} +void Theme::ApplyStyle(lv_obj_t* obj, Style style) { + switch (style) { + case Style::kTopBar: + lv_obj_set_style_pad_bottom(obj, 1, LV_PART_MAIN); + + lv_obj_set_style_shadow_width(obj, 6, LV_PART_MAIN); + lv_obj_set_style_shadow_opa(obj, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_shadow_ofs_x(obj, 0, LV_PART_MAIN); + break; + case Style::kPopup: + lv_obj_set_style_shadow_width(obj, 6, LV_PART_MAIN); + lv_obj_set_style_shadow_opa(obj, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_shadow_ofs_x(obj, 0, LV_PART_MAIN); + lv_obj_set_style_shadow_ofs_y(obj, 0, LV_PART_MAIN); + + lv_obj_set_style_radius(obj, 5, LV_PART_MAIN); + + lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_bg_color(obj, lv_color_white(), LV_PART_MAIN); + + lv_obj_set_style_pad_top(obj, 2, LV_PART_MAIN); + lv_obj_set_style_pad_bottom(obj, 2, LV_PART_MAIN); + lv_obj_set_style_pad_left(obj, 2, LV_PART_MAIN); + lv_obj_set_style_pad_right(obj, 2, LV_PART_MAIN); + break; + case Style::kTab: + lv_obj_set_style_radius(obj, 0, LV_PART_MAIN); + + lv_obj_set_style_border_width(obj, 1, LV_STATE_CHECKED); + lv_obj_set_style_border_color(obj, lv_palette_main(LV_PALETTE_BLUE), + LV_STATE_CHECKED); + lv_obj_set_style_border_side(obj, LV_BORDER_SIDE_BOTTOM, + LV_STATE_CHECKED); + break; + case Style::kButtonPrimary: + lv_obj_set_style_border_width(obj, 1, LV_PART_MAIN); + lv_obj_set_style_border_color(obj, lv_palette_main(LV_PALETTE_BLUE), + LV_PART_MAIN); + lv_obj_set_style_border_side(obj, LV_BORDER_SIDE_FULL, LV_PART_MAIN); + break; + case Style::kMenuSubheadFirst: + case Style::kMenuSubhead: + lv_obj_set_style_text_color(obj, lv_palette_darken(LV_PALETTE_GREY, 3), + LV_PART_MAIN); + lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN); + + lv_obj_set_style_border_width(obj, 1, LV_PART_MAIN); + lv_obj_set_style_border_color(obj, lv_palette_lighten(LV_PALETTE_GREY, 3), + LV_PART_MAIN); + + if (style == Style::kMenuSubhead) { + lv_obj_set_style_border_side( + obj, LV_BORDER_SIDE_TOP | LV_BORDER_SIDE_BOTTOM, LV_PART_MAIN); + } else { + lv_obj_set_style_border_side(obj, LV_BORDER_SIDE_BOTTOM, LV_PART_MAIN); + } + break; + default: + break; + } +} auto Theme::instance() -> Theme* { static Theme sTheme{}; -- cgit v1.2.3 From 20c2816a7b2497c2ab0d07a65fb640050a929371 Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 7 Mar 2024 17:52:39 +1100 Subject: Remove the White Square --- src/ui/screen.cpp | 2 +- src/ui/ui_fsm.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/ui/screen.cpp b/src/ui/screen.cpp index 3e4f8e42..bacce3f9 100644 --- a/src/ui/screen.cpp +++ b/src/ui/screen.cpp @@ -33,7 +33,7 @@ Screen::Screen() lv_obj_center(alert_); lv_obj_set_style_bg_opa(modal_content_, LV_OPA_TRANSP, 0); - lv_obj_set_style_bg_color(modal_content_, lv_color_black(), 0); + lv_obj_set_style_bg_opa(alert_, LV_OPA_TRANSP, 0); // Disable wrapping by default, since it's confusing and generally makes it // harder to navigate quickly. diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index d98e435d..25ae9817 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -457,6 +457,7 @@ void Lua::entry() { sAlertTimer = xTimerCreate("ui_alerts", pdMS_TO_TICKS(1000), false, NULL, alert_timer_callback); sAlertContainer = lv_obj_create(sCurrentScreen->alert()); + lv_obj_set_style_bg_opa(sAlertContainer, LV_OPA_TRANSP, 0); auto& registry = lua::Registry::instance(*sServices); sLua = registry.uiThread(); -- cgit v1.2.3 From 1133d4621508b7ec6bac4ab8731f3493066ceeee Mon Sep 17 00:00:00 2001 From: ailurux Date: Sun, 10 Mar 2024 13:20:17 +1100 Subject: WIP Lua Theming- style classes --- src/lua/lua_theme.cpp | 25 +++++++++--------- src/ui/include/themes.hpp | 2 +- src/ui/modal.cpp | 2 -- src/ui/screen_lua.cpp | 5 +++- src/ui/themes.cpp | 64 +++++------------------------------------------ 5 files changed, 24 insertions(+), 74 deletions(-) (limited to 'src') diff --git a/src/lua/lua_theme.cpp b/src/lua/lua_theme.cpp index 7b007f4d..2fcd71e5 100644 --- a/src/lua/lua_theme.cpp +++ b/src/lua/lua_theme.cpp @@ -22,17 +22,19 @@ namespace lua { -static auto set_theme(lua_State* L) -> int { - // lv_style_t* style = luavgl_to_style(L, -1); - // if (style == NULL) { - // ESP_LOGI("DANIEL", "Style was null or malformed??"); - // return 0; - // } - - // ESP_LOGI("DANIEL", "GOT ONE!"); - // themes::Theme::instance()->...; +static auto set_style(lua_State* L) -> int { + // Get the object and class name from the stack + if (lua_type(L, -1) == LUA_TSTRING) { + std::string class_name = lua_tostring(L, -1); + lv_obj_t* obj = luavgl_to_obj(L, -2); + if (obj != NULL) { + ui::themes::Theme::instance()->ApplyStyle(obj, class_name); + } + } + return 0; +} - /* table is in the stack at index 't' */ +static auto set_theme(lua_State* L) -> int { std::string class_name; lua_pushnil(L); /* first key */ while (lua_next(L, -2) != 0) { @@ -61,7 +63,6 @@ static auto set_theme(lua_State* L) -> int { return 0; } else { ui::themes::Theme::instance()->AddStyle(class_name, selector, style); - ESP_LOGI("DANIEL", "Got style for class %s with selector %d", class_name.c_str(), selector); } } lua_pop(L, 1); @@ -75,7 +76,7 @@ static auto set_theme(lua_State* L) -> int { return 0; } -static const struct luaL_Reg kThemeFuncs[] = {{"set", set_theme}, {NULL, NULL}}; +static const struct luaL_Reg kThemeFuncs[] = {{"set", set_theme}, {"set_style", set_style}, {NULL, NULL}}; static auto lua_theme(lua_State* L) -> int { luaL_newlib(L, kThemeFuncs); diff --git a/src/ui/include/themes.hpp b/src/ui/include/themes.hpp index 65462f65..09b9cdce 100644 --- a/src/ui/include/themes.hpp +++ b/src/ui/include/themes.hpp @@ -22,7 +22,7 @@ class Theme { public: void Apply(void); void Callback(lv_obj_t* obj); - void ApplyStyle(lv_obj_t* obj, Style style); + void ApplyStyle(lv_obj_t* obj, std::string style_key); void AddStyle(std::string key, int selector, lv_style_t* style); diff --git a/src/ui/modal.cpp b/src/ui/modal.cpp index 88f6d3ef..ec541914 100644 --- a/src/ui/modal.cpp +++ b/src/ui/modal.cpp @@ -41,8 +41,6 @@ Modal::Modal(Screen* host) lv_obj_set_style_bg_opa(root_, LV_OPA_COVER, 0); lv_obj_set_style_bg_color(root_, lv_color_white(), 0); - themes::Theme::instance()->ApplyStyle(root_, themes::Style::kPopup); - host_->modal_group(group_); } diff --git a/src/ui/screen_lua.cpp b/src/ui/screen_lua.cpp index 5130b4f7..b3554241 100644 --- a/src/ui/screen_lua.cpp +++ b/src/ui/screen_lua.cpp @@ -8,13 +8,16 @@ #include "core/lv_obj_tree.h" #include "lua.hpp" +#include "themes.hpp" #include "luavgl.h" namespace ui { namespace screens { -Lua::Lua() : s_(nullptr), obj_ref_() {} +Lua::Lua() : s_(nullptr), obj_ref_() { + themes::Theme::instance()->ApplyStyle(root_, "root"); +} Lua::~Lua() { if (s_ && obj_ref_) { diff --git a/src/ui/themes.cpp b/src/ui/themes.cpp index 4fd477ab..88f45b1b 100644 --- a/src/ui/themes.cpp +++ b/src/ui/themes.cpp @@ -44,6 +44,8 @@ void Theme::Callback(lv_obj_t* obj) { std::string class_name; if (lv_obj_check_type(obj, &lv_btn_class)) { class_name = "button"; + } else if (lv_obj_check_type(obj, &lv_list_btn_class)) { + class_name = "listbutton"; } else if (lv_obj_check_type(obj, &lv_bar_class)) { class_name = "bar"; } else if (lv_obj_check_type(obj, &lv_slider_class)) { @@ -65,65 +67,11 @@ void Theme::Callback(lv_obj_t* obj) { } -void Theme::ApplyStyle(lv_obj_t* obj, Style style) { - switch (style) { - case Style::kTopBar: - lv_obj_set_style_pad_bottom(obj, 1, LV_PART_MAIN); - - lv_obj_set_style_shadow_width(obj, 6, LV_PART_MAIN); - lv_obj_set_style_shadow_opa(obj, LV_OPA_COVER, LV_PART_MAIN); - lv_obj_set_style_shadow_ofs_x(obj, 0, LV_PART_MAIN); - break; - case Style::kPopup: - lv_obj_set_style_shadow_width(obj, 6, LV_PART_MAIN); - lv_obj_set_style_shadow_opa(obj, LV_OPA_COVER, LV_PART_MAIN); - lv_obj_set_style_shadow_ofs_x(obj, 0, LV_PART_MAIN); - lv_obj_set_style_shadow_ofs_y(obj, 0, LV_PART_MAIN); - - lv_obj_set_style_radius(obj, 5, LV_PART_MAIN); - - lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, LV_PART_MAIN); - lv_obj_set_style_bg_color(obj, lv_color_white(), LV_PART_MAIN); - - lv_obj_set_style_pad_top(obj, 2, LV_PART_MAIN); - lv_obj_set_style_pad_bottom(obj, 2, LV_PART_MAIN); - lv_obj_set_style_pad_left(obj, 2, LV_PART_MAIN); - lv_obj_set_style_pad_right(obj, 2, LV_PART_MAIN); - break; - case Style::kTab: - lv_obj_set_style_radius(obj, 0, LV_PART_MAIN); - - lv_obj_set_style_border_width(obj, 1, LV_STATE_CHECKED); - lv_obj_set_style_border_color(obj, lv_palette_main(LV_PALETTE_BLUE), - LV_STATE_CHECKED); - lv_obj_set_style_border_side(obj, LV_BORDER_SIDE_BOTTOM, - LV_STATE_CHECKED); - break; - case Style::kButtonPrimary: - lv_obj_set_style_border_width(obj, 1, LV_PART_MAIN); - lv_obj_set_style_border_color(obj, lv_palette_main(LV_PALETTE_BLUE), - LV_PART_MAIN); - lv_obj_set_style_border_side(obj, LV_BORDER_SIDE_FULL, LV_PART_MAIN); - break; - case Style::kMenuSubheadFirst: - case Style::kMenuSubhead: - lv_obj_set_style_text_color(obj, lv_palette_darken(LV_PALETTE_GREY, 3), - LV_PART_MAIN); - lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN); - - lv_obj_set_style_border_width(obj, 1, LV_PART_MAIN); - lv_obj_set_style_border_color(obj, lv_palette_lighten(LV_PALETTE_GREY, 3), - LV_PART_MAIN); - - if (style == Style::kMenuSubhead) { - lv_obj_set_style_border_side( - obj, LV_BORDER_SIDE_TOP | LV_BORDER_SIDE_BOTTOM, LV_PART_MAIN); - } else { - lv_obj_set_style_border_side(obj, LV_BORDER_SIDE_BOTTOM, LV_PART_MAIN); +void Theme::ApplyStyle(lv_obj_t* obj, std::string style_key) { + if (auto search = style_map.find(style_key); search != style_map.end()) { + for (const auto& pair : search->second) { + lv_obj_add_style(obj, pair.second, pair.first); } - break; - default: - break; } } -- cgit v1.2.3 From 2eb3fd53d7bf32cffb1569d6b2b0c1fbc5b9ed2c Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 13 Mar 2024 17:07:03 +1100 Subject: Update the battery status when plugging/unplugging --- src/battery/battery.cpp | 5 ++++- src/system_fsm/include/system_events.hpp | 1 - src/system_fsm/system_fsm.cpp | 6 ++---- 3 files changed, 6 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/battery/battery.cpp b/src/battery/battery.cpp index 95f2d17b..debef9e6 100644 --- a/src/battery/battery.cpp +++ b/src/battery/battery.cpp @@ -73,7 +73,10 @@ auto Battery::Update() -> void { } else { is_charging = *charge_state == ChargeStatus::kChargingRegular || *charge_state == ChargeStatus::kChargingFast || - *charge_state == ChargeStatus::kFullCharge; + *charge_state == ChargeStatus::kFullCharge || + // Treat 'no battery' as charging because, for UI purposes, + // we're *kind of* at full charge if u think about it. + *charge_state == ChargeStatus::kNoBattery; } if (state_ && state_->is_charging == is_charging && diff --git a/src/system_fsm/include/system_events.hpp b/src/system_fsm/include/system_events.hpp index 32394958..1be03f82 100644 --- a/src/system_fsm/include/system_events.hpp +++ b/src/system_fsm/include/system_events.hpp @@ -57,7 +57,6 @@ struct SamdUsbMscChanged : tinyfsm::Event { bool en; }; -struct ChargingStatusChanged : tinyfsm::Event {}; struct BatteryStateChanged : tinyfsm::Event { battery::Battery::BatteryState new_state; }; diff --git a/src/system_fsm/system_fsm.cpp b/src/system_fsm/system_fsm.cpp index 977f4a6d..5a1ccf8c 100644 --- a/src/system_fsm/system_fsm.cpp +++ b/src/system_fsm/system_fsm.cpp @@ -84,10 +84,8 @@ void SystemState::react(const internal::SamdInterrupt&) { auto charge_status = samd.GetChargeStatus(); auto usb_status = samd.GetUsbStatus(); - if (charge_status != prev_charge_status) { - ChargingStatusChanged ev{}; - events::System().Dispatch(ev); - events::Ui().Dispatch(ev); + if (charge_status != prev_charge_status && sServices) { + sServices->battery().Update(); } if (usb_status != prev_usb_status) { ESP_LOGI(kTag, "usb status changed"); -- cgit v1.2.3 From 1ea25ab9d3bc9d7ad85bccd3a3c9e4f56c71a80f Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 14 Mar 2024 14:40:36 +1100 Subject: Use a simpler method of determining charge status idk why the smarter way of doing it doesn't work reliably, but im sick of it --- src/drivers/samd.cpp | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/drivers/samd.cpp b/src/drivers/samd.cpp index f12a18de..b631b4fb 100644 --- a/src/drivers/samd.cpp +++ b/src/drivers/samd.cpp @@ -77,29 +77,16 @@ auto Samd::UpdateChargeStatus() -> void { return; } + // FIXME: Ideally we should be using the three 'charge status' bits to work + // out whether we're actually charging, or if we've got a full charge, + // critically low charge, etc. uint8_t usb_state = raw_res & 0b11; - uint8_t charge_state = (raw_res >> 2) & 0b111; - switch (charge_state) { - case 0b000: - case 0b011: - charge_status_ = ChargeStatus::kNoBattery; - break; - case 0b001: - charge_status_ = usb_state == 1 ? ChargeStatus::kChargingRegular - : ChargeStatus::kChargingFast; - break; - case 0b010: - charge_status_ = ChargeStatus::kFullCharge; - break; - case 0b100: - charge_status_ = ChargeStatus::kBatteryCritical; - break; - case 0b101: - charge_status_ = ChargeStatus::kDischarging; - break; - default: - charge_status_ = {}; - break; + if (usb_state == 0) { + charge_status_ = ChargeStatus::kDischarging; + } else if (usb_state == 1) { + charge_status_ = ChargeStatus::kChargingRegular; + } else { + charge_status_ = ChargeStatus::kChargingFast; } } -- cgit v1.2.3 From c4238173345ae65b69535c21b282448d5f012d43 Mon Sep 17 00:00:00 2001 From: Nano Date: Sun, 17 Mar 2024 20:16:31 -0700 Subject: fixes for my green-tab adafruit ST7735R --- src/drivers/display.cpp | 4 ++-- src/ui/screen.cpp | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/drivers/display.cpp b/src/drivers/display.cpp index cb3ee3a0..026e1346 100644 --- a/src/drivers/display.cpp +++ b/src/drivers/display.cpp @@ -40,8 +40,8 @@ [[maybe_unused]] static const char* kTag = "DISPLAY"; // TODO(jacqueline): Encode width and height variations in the init data. -static const uint8_t kDisplayHeight = 128 + 2; -static const uint8_t kDisplayWidth = 160 + 1; +static const uint8_t kDisplayHeight = 128; +static const uint8_t kDisplayWidth = 160; static const uint8_t kTransactionQueueSize = 2; static const gpio_num_t kDisplayDr = GPIO_NUM_33; diff --git a/src/ui/screen.cpp b/src/ui/screen.cpp index 3e4f8e42..aafc982c 100644 --- a/src/ui/screen.cpp +++ b/src/ui/screen.cpp @@ -35,6 +35,9 @@ Screen::Screen() lv_obj_set_style_bg_opa(modal_content_, LV_OPA_TRANSP, 0); lv_obj_set_style_bg_color(modal_content_, lv_color_black(), 0); + lv_obj_set_scrollbar_mode(root_, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF); + // Disable wrapping by default, since it's confusing and generally makes it // harder to navigate quickly. lv_group_set_wrap(group_, false); -- cgit v1.2.3 From 4cd3c187f92d8f5b73c2c985da308280c18465cf Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 20 Mar 2024 11:08:35 +1100 Subject: Add exact display size to nvs, since it can vary --- src/drivers/display.cpp | 15 ++++++--------- src/drivers/display_init.cpp | 2 ++ src/drivers/include/display_init.hpp | 3 +++ src/drivers/include/nvs.hpp | 8 ++++++++ src/drivers/nvs.cpp | 21 +++++++++++++++++++++ src/system_fsm/booting.cpp | 8 +++++--- src/ui/include/ui_fsm.hpp | 2 +- src/ui/ui_fsm.cpp | 16 ++++++++++++++-- 8 files changed, 60 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/drivers/display.cpp b/src/drivers/display.cpp index 026e1346..c16fc148 100644 --- a/src/drivers/display.cpp +++ b/src/drivers/display.cpp @@ -39,9 +39,6 @@ [[maybe_unused]] static const char* kTag = "DISPLAY"; -// TODO(jacqueline): Encode width and height variations in the init data. -static const uint8_t kDisplayHeight = 128; -static const uint8_t kDisplayWidth = 160; static const uint8_t kTransactionQueueSize = 2; static const gpio_num_t kDisplayDr = GPIO_NUM_33; @@ -51,9 +48,11 @@ static const gpio_num_t kDisplayCs = GPIO_NUM_22; /* * The size of each of our two display buffers. This is fundamentally a balance * between performance and memory usage. LVGL docs recommend a buffer 1/10th the - * size of the screen is the best tradeoff + * size of the screen is the best tradeoff. + 8 + * The 160x128 is the nominal size of our standard faceplate's display. */ -static const int kDisplayBufferSize = kDisplayWidth * kDisplayHeight / 10; +static const int kDisplayBufferSize = 160 * 128 / 10; DMA_ATTR static lv_color_t kDisplayBuffer[kDisplayBufferSize]; namespace drivers { @@ -154,10 +153,8 @@ auto Display::Create(IGpios& expander, lv_disp_drv_init(&display->driver_); display->driver_.draw_buf = &display->buffers_; - display->driver_.hor_res = kDisplayWidth; - display->driver_.ver_res = kDisplayHeight; - // display->driver_.sw_rotate = 1; - // display->driver_.rotated = LV_DISP_ROT_270; + display->driver_.hor_res = init_data.width; + display->driver_.ver_res = init_data.height; display->driver_.sw_rotate = 0; display->driver_.rotated = LV_DISP_ROT_NONE; display->driver_.antialiasing = 0; diff --git a/src/drivers/display_init.cpp b/src/drivers/display_init.cpp index 833ea6a4..a69826fa 100644 --- a/src/drivers/display_init.cpp +++ b/src/drivers/display_init.cpp @@ -101,6 +101,8 @@ static const uint8_t kST7735RCommonFooter[]{ // clang-format on const InitialisationData kST7735R = { + .width = 160, + .height = 128, .num_sequences = 3, .sequences = {kST7735RCommonHeader, kST7735RCommonGreen, kST7735RCommonFooter}}; diff --git a/src/drivers/include/display_init.hpp b/src/drivers/include/display_init.hpp index f6c28b54..9bf5b3f5 100644 --- a/src/drivers/include/display_init.hpp +++ b/src/drivers/include/display_init.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include namespace drivers { @@ -14,6 +15,8 @@ namespace displays { extern const uint8_t kDelayBit; struct InitialisationData { + uint16_t width; + uint16_t height; uint8_t num_sequences; const uint8_t* sequences[4]; }; diff --git a/src/drivers/include/nvs.hpp b/src/drivers/include/nvs.hpp index 5bd825e5..f288f8e2 100644 --- a/src/drivers/include/nvs.hpp +++ b/src/drivers/include/nvs.hpp @@ -71,6 +71,11 @@ class NvsStorage { auto LockPolarity() -> bool; auto LockPolarity(bool) -> void; + auto DisplaySize() + -> std::pair, std::optional>; + auto DisplaySize(std::pair, std::optional>) + -> void; + auto PreferredBluetoothDevice() -> std::optional; auto PreferredBluetoothDevice(std::optional) -> void; @@ -120,6 +125,9 @@ class NvsStorage { nvs_handle_t handle_; Setting lock_polarity_; + Setting display_cols_; + Setting display_rows_; + Setting brightness_; Setting sensitivity_; Setting amp_max_vol_; diff --git a/src/drivers/nvs.cpp b/src/drivers/nvs.cpp index 875059be..28cb542c 100644 --- a/src/drivers/nvs.cpp +++ b/src/drivers/nvs.cpp @@ -37,6 +37,8 @@ static constexpr char kKeyAmpLeftBias[] = "hp_bias"; static constexpr char kKeyPrimaryInput[] = "in_pri"; static constexpr char kKeyScrollSensitivity[] = "scroll"; static constexpr char kKeyLockPolarity[] = "lockpol"; +static constexpr char kKeyDisplayCols[] = "dispcols"; +static constexpr char kKeyDisplayRows[] = "disprows"; static auto nvs_get_string(nvs_handle_t nvs, const char* key) -> std::optional { @@ -161,6 +163,8 @@ auto NvsStorage::OpenSync() -> NvsStorage* { NvsStorage::NvsStorage(nvs_handle_t handle) : handle_(handle), lock_polarity_(kKeyLockPolarity), + display_cols_(kKeyDisplayCols), + display_rows_(kKeyDisplayRows), brightness_(kKeyBrightness), sensitivity_(kKeyScrollSensitivity), amp_max_vol_(kKeyAmpMaxVolume), @@ -180,6 +184,8 @@ NvsStorage::~NvsStorage() { auto NvsStorage::Read() -> void { std::lock_guard lock{mutex_}; lock_polarity_.read(handle_); + display_cols_.read(handle_); + display_rows_.read(handle_); brightness_.read(handle_); sensitivity_.read(handle_); amp_max_vol_.read(handle_); @@ -194,6 +200,8 @@ auto NvsStorage::Read() -> void { auto NvsStorage::Write() -> bool { std::lock_guard lock{mutex_}; lock_polarity_.write(handle_); + display_cols_.write(handle_); + display_rows_.write(handle_); brightness_.write(handle_); sensitivity_.write(handle_); amp_max_vol_.write(handle_); @@ -231,6 +239,19 @@ auto NvsStorage::LockPolarity(bool p) -> void { lock_polarity_.set(p); } +auto NvsStorage::DisplaySize() + -> std::pair, std::optional> { + std::lock_guard lock{mutex_}; + return std::make_pair(display_cols_.get(), display_rows_.get()); +} + +auto NvsStorage::DisplaySize( + std::pair, std::optional> size) -> void { + std::lock_guard lock{mutex_}; + display_cols_.set(std::move(size.first)); + display_rows_.set(std::move(size.second)); +} + auto NvsStorage::PreferredBluetoothDevice() -> std::optional { std::lock_guard lock{mutex_}; diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp index 41f46df2..14fed92e 100644 --- a/src/system_fsm/booting.cpp +++ b/src/system_fsm/booting.cpp @@ -62,8 +62,12 @@ auto Booting::entry() -> void { ESP_ERROR_CHECK(drivers::init_spi()); sServices->gpios(std::unique_ptr(drivers::Gpios::Create())); + // NVS is needed early so that we can correctly initialise the display. + sServices->nvs( + std::unique_ptr(drivers::NvsStorage::OpenSync())); + ESP_LOGI(kTag, "starting ui"); - if (!ui::UiState::InitBootSplash(sServices->gpios())) { + if (!ui::UiState::InitBootSplash(sServices->gpios(), sServices->nvs())) { events::System().Dispatch(FatalError{}); return; } @@ -74,8 +78,6 @@ auto Booting::entry() -> void { ESP_LOGI(kTag, "installing remaining drivers"); drivers::spiffs_mount(); sServices->samd(std::unique_ptr(drivers::Samd::Create())); - sServices->nvs( - std::unique_ptr(drivers::NvsStorage::OpenSync())); sServices->touchwheel( std::unique_ptr{drivers::TouchWheel::Create()}); sServices->haptics(std::make_unique()); diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index 07937559..579cc2bb 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -36,7 +36,7 @@ namespace ui { class UiState : public tinyfsm::Fsm { public: - static auto InitBootSplash(drivers::IGpios&) -> bool; + static auto InitBootSplash(drivers::IGpios&, drivers::NvsStorage&) -> bool; virtual ~UiState() {} diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 1a9f01b4..0ed012a0 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -12,6 +12,7 @@ #include "bluetooth_types.hpp" #include "db_events.hpp" +#include "display_init.hpp" #include "freertos/portmacro.h" #include "freertos/projdefs.h" #include "lua.h" @@ -284,10 +285,21 @@ lua::Property UiState::sLockSwitch{false}; lua::Property UiState::sDatabaseUpdating{false}; -auto UiState::InitBootSplash(drivers::IGpios& gpios) -> bool { +auto UiState::InitBootSplash(drivers::IGpios& gpios, drivers::NvsStorage& nvs) + -> bool { // Init LVGL first, since the display driver registers itself with LVGL. lv_init(); - sDisplay.reset(drivers::Display::Create(gpios, drivers::displays::kST7735R)); + + drivers::displays::InitialisationData init_data = drivers::displays::kST7735R; + + // HACK: correct the display size for our prototypes. + // nvs.DisplaySize({161, 130}); + + auto actual_size = nvs.DisplaySize(); + init_data.width = actual_size.first.value_or(init_data.width); + init_data.height = actual_size.second.value_or(init_data.height); + + sDisplay.reset(drivers::Display::Create(gpios, init_data)); if (sDisplay == nullptr) { return false; } -- cgit v1.2.3 From 46e67437717975c10c62775a3f40bec84ac4f44c Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 20 Mar 2024 11:15:33 +1100 Subject: version bump, plus prep for lock switch hack --- src/system_fsm/booting.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp index 14fed92e..e911932c 100644 --- a/src/system_fsm/booting.cpp +++ b/src/system_fsm/booting.cpp @@ -102,7 +102,7 @@ auto Booting::entry() -> void { sServices->bluetooth().Enable(); } - sServices->nvs().LockPolarity(true); + sServices->nvs().LockPolarity(false); BootComplete ev{.services = sServices}; events::Audio().Dispatch(ev); -- cgit v1.2.3 From a05d93a1e26181237a76da5ce398c6b08497d591 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 20 Mar 2024 11:43:33 +1100 Subject: Start using the lock switch polarity bit in nvs --- src/drivers/gpios.cpp | 18 +++++++++++++++--- src/drivers/include/gpios.hpp | 9 ++++++--- src/system_fsm/booting.cpp | 17 ++++++++++------- 3 files changed, 31 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/drivers/gpios.cpp b/src/drivers/gpios.cpp index 5c255204..aab932a7 100644 --- a/src/drivers/gpios.cpp +++ b/src/drivers/gpios.cpp @@ -63,8 +63,8 @@ constexpr std::pair unpack(uint16_t ba) { static constexpr gpio_num_t kIntPin = GPIO_NUM_34; -auto Gpios::Create() -> Gpios* { - Gpios* instance = new Gpios(); +auto Gpios::Create(bool invert_lock) -> Gpios* { + Gpios* instance = new Gpios(invert_lock); // Read and write initial values on initialisation so that we do not have a // strange partially-initialised state. if (!instance->Flush() || !instance->Read()) { @@ -73,7 +73,10 @@ auto Gpios::Create() -> Gpios* { return instance; } -Gpios::Gpios() : ports_(pack(kPortADefault, kPortBDefault)), inputs_(0) { +Gpios::Gpios(bool invert_lock) + : ports_(pack(kPortADefault, kPortBDefault)), + inputs_(0), + invert_lock_switch_(invert_lock) { gpio_set_direction(kIntPin, GPIO_MODE_INPUT); } @@ -108,6 +111,15 @@ auto Gpios::Get(Pin pin) const -> bool { return (inputs_ & (1 << static_cast(pin))) > 0; } +auto Gpios::IsLocked() const -> bool { + bool pin = Get(Pin::kKeyLock); + if (invert_lock_switch_) { + return pin; + } else { + return !pin; + } +} + auto Gpios::Read() -> bool { uint8_t input_a, input_b; diff --git a/src/drivers/include/gpios.hpp b/src/drivers/include/gpios.hpp index 55486be7..e27a3ade 100644 --- a/src/drivers/include/gpios.hpp +++ b/src/drivers/include/gpios.hpp @@ -79,12 +79,12 @@ class IGpios { */ virtual auto Get(Pin) const -> bool = 0; - virtual auto IsLocked() const -> bool { return Get(Pin::kKeyLock); } + virtual auto IsLocked() const -> bool = 0; }; class Gpios : public IGpios { public: - static auto Create() -> Gpios*; + static auto Create(bool invert_lock_switch) -> Gpios*; ~Gpios(); /* @@ -106,6 +106,8 @@ class Gpios : public IGpios { auto Get(Pin) const -> bool override; + auto IsLocked() const -> bool override; + /** * Reads from the GPIO expander, populating `inputs` with the most recent * values. @@ -118,10 +120,11 @@ class Gpios : public IGpios { Gpios& operator=(const Gpios&) = delete; private: - Gpios(); + Gpios(bool invert_lock); std::atomic ports_; std::atomic inputs_; + const bool invert_lock_switch_; }; } // namespace drivers diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp index e911932c..eb931192 100644 --- a/src/system_fsm/booting.cpp +++ b/src/system_fsm/booting.cpp @@ -57,14 +57,19 @@ auto Booting::entry() -> void { sServices.reset(new ServiceLocator()); ESP_LOGI(kTag, "installing early drivers"); + // NVS is needed first because it contains information about what specific + // hardware configuration we're running on. + sServices->nvs( + std::unique_ptr(drivers::NvsStorage::OpenSync())); + + // HACK: fix up the switch polarity on newer dev units + sServices->nvs().LockPolarity(false); + // I2C and SPI are both always needed. We can't even power down or show an // error without these. ESP_ERROR_CHECK(drivers::init_spi()); - sServices->gpios(std::unique_ptr(drivers::Gpios::Create())); - - // NVS is needed early so that we can correctly initialise the display. - sServices->nvs( - std::unique_ptr(drivers::NvsStorage::OpenSync())); + sServices->gpios(std::unique_ptr( + drivers::Gpios::Create(sServices->nvs().LockPolarity()))); ESP_LOGI(kTag, "starting ui"); if (!ui::UiState::InitBootSplash(sServices->gpios(), sServices->nvs())) { @@ -102,8 +107,6 @@ auto Booting::entry() -> void { sServices->bluetooth().Enable(); } - sServices->nvs().LockPolarity(false); - BootComplete ev{.services = sServices}; events::Audio().Dispatch(ev); events::Ui().Dispatch(ev); -- cgit v1.2.3 From 21ae6a962623c9128fbb4a599cc50a2c616e9884 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 21 Mar 2024 09:15:44 +1100 Subject: comment out dev unit hack the nano code bug has received the hack, so now we don't need it applying by default anymore --- src/system_fsm/booting.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp index eb931192..bd394428 100644 --- a/src/system_fsm/booting.cpp +++ b/src/system_fsm/booting.cpp @@ -63,7 +63,7 @@ auto Booting::entry() -> void { std::unique_ptr(drivers::NvsStorage::OpenSync())); // HACK: fix up the switch polarity on newer dev units - sServices->nvs().LockPolarity(false); + // sServices->nvs().LockPolarity(false); // I2C and SPI are both always needed. We can't even power down or show an // error without these. -- cgit v1.2.3 From 684ff50ef4931aeb8cfb15c5a2d62e55520f04a5 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 21 Mar 2024 10:50:23 +1100 Subject: Add support for screens declaring that they can't be popped Needed as prep for usb msc support; you really shouldn't leave the MSC settings screen until you've disabled usb msc. --- src/lua/lua_screen.cpp | 14 +++++++++----- src/ui/include/screen.hpp | 2 ++ src/ui/include/screen_lua.hpp | 2 ++ src/ui/include/screen_splash.hpp | 2 ++ src/ui/screen_lua.cpp | 19 +++++++++++++++++++ src/ui/ui_fsm.cpp | 3 +++ 6 files changed, 37 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/lua/lua_screen.cpp b/src/lua/lua_screen.cpp index 27843bc7..f17f6b1a 100644 --- a/src/lua/lua_screen.cpp +++ b/src/lua/lua_screen.cpp @@ -50,11 +50,15 @@ static auto screen_noop(lua_State* state) -> int { return 0; } -static const struct luaL_Reg kScreenFuncs[] = {{"new", screen_new}, - {"createUi", screen_noop}, - {"onShown", screen_noop}, - {"onHidden", screen_noop}, - {NULL, NULL}}; +static auto screen_true(lua_State* state) -> int { + lua_pushboolean(state, true); + return 1; +} + +static const struct luaL_Reg kScreenFuncs[] = { + {"new", screen_new}, {"createUi", screen_noop}, + {"onShown", screen_noop}, {"onHidden", screen_noop}, + {"canPop", screen_true}, {NULL, NULL}}; static auto lua_screen(lua_State* state) -> int { luaL_newlib(state, kScreenFuncs); diff --git a/src/ui/include/screen.hpp b/src/ui/include/screen.hpp index 4241c712..40284fda 100644 --- a/src/ui/include/screen.hpp +++ b/src/ui/include/screen.hpp @@ -43,6 +43,8 @@ class Screen { return group_; } + virtual auto canPop() -> bool = 0; + protected: lv_obj_t* const root_; lv_obj_t* content_; diff --git a/src/ui/include/screen_lua.hpp b/src/ui/include/screen_lua.hpp index 0ed3a508..41d97a1e 100644 --- a/src/ui/include/screen_lua.hpp +++ b/src/ui/include/screen_lua.hpp @@ -21,6 +21,8 @@ class Lua : public Screen { auto onShown() -> void override; auto onHidden() -> void override; + auto canPop() -> bool override; + auto SetObjRef(lua_State*) -> void; private: diff --git a/src/ui/include/screen_splash.hpp b/src/ui/include/screen_splash.hpp index 1ee7dd89..6e746345 100644 --- a/src/ui/include/screen_splash.hpp +++ b/src/ui/include/screen_splash.hpp @@ -20,6 +20,8 @@ class Splash : public Screen { Splash(); ~Splash(); + auto canPop() -> bool override { return false; } + private: lv_obj_t* container_; lv_obj_t* label_; diff --git a/src/ui/screen_lua.cpp b/src/ui/screen_lua.cpp index d6c7a26f..55eef119 100644 --- a/src/ui/screen_lua.cpp +++ b/src/ui/screen_lua.cpp @@ -58,6 +58,25 @@ auto Lua::onHidden() -> void { lua_pop(s_, 1); } +auto Lua::canPop() -> bool { + if (!s_ || !obj_ref_) { + return true; + } + lua_rawgeti(s_, LUA_REGISTRYINDEX, *obj_ref_); + lua_pushliteral(s_, "canPop"); + + if (lua_gettable(s_, -2) == LUA_TFUNCTION) { + // If we got a callback instead of a value, then invoke it to turn it into + // value. + lua_pushvalue(s_, -2); + lua::CallProtected(s_, 1, 1); + } + bool ret = lua_toboolean(s_, -1); + + lua_pop(s_, 2); + return ret; +} + auto Lua::SetObjRef(lua_State* s) -> void { assert(s_ == nullptr); s_ = s; diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 0ed012a0..c11f66a7 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -610,6 +610,9 @@ auto Lua::QueuePrevious(lua_State*) -> int { } auto Lua::PopLuaScreen(lua_State* s) -> int { + if (!sCurrentScreen->canPop()) { + return 0; + } PopScreen(); luavgl_set_root(s, sCurrentScreen->content()); lv_group_set_default(sCurrentScreen->group()); -- cgit v1.2.3 From dadac304dd930ddf4c5aebcc069c5d9f881b2b60 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 21 Mar 2024 11:51:48 +1100 Subject: Add very basic usb msc ui --- src/system_fsm/idle.cpp | 10 +++++++++- src/system_fsm/running.cpp | 6 +++++- src/ui/include/ui_fsm.hpp | 2 ++ src/ui/ui_fsm.cpp | 15 +++++++++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/system_fsm/idle.cpp b/src/system_fsm/idle.cpp index b6bb2572..980f0c94 100644 --- a/src/system_fsm/idle.cpp +++ b/src/system_fsm/idle.cpp @@ -13,6 +13,7 @@ #include "audio_fsm.hpp" #include "event_queue.hpp" +#include "samd.hpp" #include "storage.hpp" #include "system_events.hpp" #include "system_fsm.hpp" @@ -40,7 +41,7 @@ void Idle::entry() { events::Audio().Dispatch(OnIdle{}); events::Ui().Dispatch(OnIdle{}); - sIdleTimeout = xTimerCreate("idle_timeout", kTicksBeforeSleep, false, NULL, + sIdleTimeout = xTimerCreate("idle_timeout", kTicksBeforeSleep, true, NULL, timer_callback); xTimerStart(sIdleTimeout, portMAX_DELAY); } @@ -63,6 +64,13 @@ void Idle::react(const internal::IdleTimeout& ev) { transit(); return; } + auto s = static_cast(sServices->samd().GetUsbStatus()); + ESP_LOGI(kTag, "usb status is %i", s); + if (sServices->samd().GetUsbStatus() != drivers::Samd::UsbStatus::kDetached) { + // Stay powered on if we're plugged in, in order to charge faster, sync + // files, flash updates, etc. + return; + } ESP_LOGI(kTag, "system shutting down"); // FIXME: It would be neater to just free a bunch of our pointers, deinit the diff --git a/src/system_fsm/running.cpp b/src/system_fsm/running.cpp index d1d02fab..d80809e6 100644 --- a/src/system_fsm/running.cpp +++ b/src/system_fsm/running.cpp @@ -41,7 +41,11 @@ void Running::entry() { sUnmountTimer = xTimerCreate("unmount_timeout", kTicksBeforeUnmount, false, NULL, timer_callback); } - mountStorage(); + // Only mount our storage immediately if we know it's not currently in use + // by the SAMD. + if (!sServices->samd().UsbMassStorage()) { + mountStorage(); + } } void Running::exit() { diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index 579cc2bb..f7fde1dd 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -132,6 +132,8 @@ class UiState : public tinyfsm::Fsm { static lua::Property sLockSwitch; static lua::Property sDatabaseUpdating; + + static lua::Property sUsbMassStorageEnabled; }; namespace states { diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index c11f66a7..a913a339 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -285,6 +285,17 @@ lua::Property UiState::sLockSwitch{false}; lua::Property UiState::sDatabaseUpdating{false}; +lua::Property UiState::sUsbMassStorageEnabled{ + false, [](const lua::LuaValue& val) { + if (!std::holds_alternative(val)) { + return false; + } + bool enable = std::get(val); + // FIXME: Check for system busy. + events::System().Dispatch(system_fsm::SamdUsbMscChanged{.en = enable}); + return true; + }}; + auto UiState::InitBootSplash(drivers::IGpios& gpios, drivers::NvsStorage& nvs) -> bool { // Init LVGL first, since the display driver registers itself with LVGL. @@ -553,6 +564,10 @@ void Lua::entry() { registry.AddPropertyModule("database", { {"updating", &sDatabaseUpdating}, }); + registry.AddPropertyModule("usb", + { + {"msc_enabled", &sUsbMassStorageEnabled}, + }); auto bt = sServices->bluetooth(); sBluetoothEnabled.Update(bt.IsEnabled()); -- cgit v1.2.3 From 3dc0989c7fd6d3d508fa9c5e5269dc45396c09e9 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 21 Mar 2024 12:57:11 +1100 Subject: fix db key prefix format, and use per-file modification times --- src/database/database.cpp | 50 +++++++++------------------------------ src/database/include/database.hpp | 5 +--- src/database/include/records.hpp | 2 ++ src/database/records.cpp | 29 ++++++++++++++++------- 4 files changed, 35 insertions(+), 51 deletions(-) (limited to 'src') diff --git a/src/database/database.cpp b/src/database/database.cpp index ca92cf6b..06138983 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -60,7 +60,6 @@ static const char kKeyDbVersion[] = "schema_version"; static const char kKeyCustom[] = "U\0"; static const char kKeyCollator[] = "collator"; static const char kKeyTrackId[] = "next_track_id"; -static const char kKeyLastUpdate[] = "last_update"; static std::atomic sIsDbOpen(false); @@ -302,10 +301,6 @@ auto Database::updateIndexes() -> void { leveldb::ReadOptions read_options; read_options.fill_cache = false; - std::pair last_update = dbGetLastUpdate(); - ESP_LOGI(kTag, "last update was at %u,%u", last_update.first, - last_update.second); - // Stage 1: verify all existing tracks are still valid. ESP_LOGI(kTag, "verifying existing tracks"); { @@ -360,6 +355,7 @@ auto Database::updateIndexes() -> void { dbRemoveIndexes(track); track->is_tombstoned = true; dbPutTrackData(*track); + db_->Delete(leveldb::WriteOptions{}, EncodePathKey(track->filepath)); continue; } @@ -386,7 +382,6 @@ auto Database::updateIndexes() -> void { // Stage 2: search for newly added files. ESP_LOGI(kTag, "scanning for new tracks"); uint64_t num_processed = 0; - std::pair newest_track = last_update; file_gatherer_.FindFiles("", [&](std::string_view path, const FILINFO& info) { num_processed++; events::Ui().Dispatch(event::UpdateProgress{ @@ -394,11 +389,11 @@ auto Database::updateIndexes() -> void { .val = num_processed, }); - std::pair modified{info.fdate, info.ftime}; - if (modified < last_update) { + std::string unused; + if (db_->Get(read_options, EncodePathKey(path), &unused).ok()) { + // This file is already in the database; skip it. return; } - newest_track = std::max(modified, newest_track); std::shared_ptr tags = tag_parser_.ReadAndParseTags(path); if (!tags || tags->encoding() == Container::kUnsupported) { @@ -415,6 +410,7 @@ auto Database::updateIndexes() -> void { existing_hash = ParseHashValue(raw_entry); } + std::pair modified{info.fdate, info.ftime}; if (!existing_hash) { // We've never met this track before! Or we have, but the entry is // malformed. Either way, record this as a new track. @@ -432,6 +428,8 @@ auto Database::updateIndexes() -> void { dbPutHash(hash, id); auto t = std::make_shared(data, tags); dbCreateIndexesForTrack(*t); + db_->Put(leveldb::WriteOptions{}, EncodePathKey(path), + TrackIdToBytes(id)); return; } @@ -447,6 +445,8 @@ auto Database::updateIndexes() -> void { dbPutTrackData(*new_data); auto t = std::make_shared(new_data, tags); dbCreateIndexesForTrack(*t); + db_->Put(leveldb::WriteOptions{}, EncodePathKey(path), + TrackIdToBytes(new_data->id)); return; } @@ -457,6 +457,8 @@ auto Database::updateIndexes() -> void { dbPutTrackData(*existing_data); auto t = std::make_shared(existing_data, tags); dbCreateIndexesForTrack(*t); + db_->Put(leveldb::WriteOptions{}, EncodePathKey(path), + TrackIdToBytes(existing_data->id)); } else if (existing_data->filepath != std::pmr::string{path.data(), path.size()}) { ESP_LOGW(kTag, "hash collision: %s, %s, %s", @@ -465,42 +467,12 @@ auto Database::updateIndexes() -> void { tags->album().value_or("no album").c_str()); } }); - dbSetLastUpdate(newest_track); - ESP_LOGI(kTag, "newest track was at %u,%u", newest_track.first, - newest_track.second); } auto Database::isUpdating() -> bool { return is_updating_; } -auto Database::dbGetLastUpdate() -> std::pair { - std::string raw; - if (!db_->Get(leveldb::ReadOptions{}, kKeyLastUpdate, &raw).ok()) { - return {0, 0}; - } - auto [res, unused, err] = cppbor::parseWithViews( - reinterpret_cast(raw.data()), raw.size()); - if (!res || res->type() != cppbor::ARRAY) { - return {0, 0}; - } - auto as_arr = res->asArray(); - if (as_arr->size() != 2 || as_arr->get(0)->type() != cppbor::UINT || - as_arr->get(1)->type() != cppbor::UINT) { - return {0, 0}; - } - return {as_arr->get(0)->asUint()->unsignedValue(), - as_arr->get(1)->asUint()->unsignedValue()}; -} - -auto Database::dbSetLastUpdate(std::pair time) -> void { - auto encoding = cppbor::Array{ - cppbor::Uint{time.first}, - cppbor::Uint{time.second}, - }; - db_->Put(leveldb::WriteOptions{}, kKeyLastUpdate, encoding.toString()); -} - auto Database::dbMintNewTrackId() -> TrackId { TrackId next_id = 1; std::string val; diff --git a/src/database/include/database.hpp b/src/database/include/database.hpp index 0aec4c44..35b76a13 100644 --- a/src/database/include/database.hpp +++ b/src/database/include/database.hpp @@ -35,7 +35,7 @@ namespace database { -const uint8_t kCurrentDbVersion = 5; +const uint8_t kCurrentDbVersion = 6; struct SearchKey; class Record; @@ -107,9 +107,6 @@ class Database { ITagParser& tag_parser, locale::ICollator& collator); - auto dbGetLastUpdate() -> std::pair; - auto dbSetLastUpdate(std::pair) -> void; - auto dbMintNewTrackId() -> TrackId; auto dbEntomb(TrackId track, uint64_t hash) -> void; diff --git a/src/database/include/records.hpp b/src/database/include/records.hpp index 09764ed0..87034059 100644 --- a/src/database/include/records.hpp +++ b/src/database/include/records.hpp @@ -21,6 +21,8 @@ namespace database { +auto EncodePathKey(std::string_view path) -> std::string; + /* * Returns the prefix added to every TrackData key. This can be used to iterate * over every data record in the database. diff --git a/src/database/records.cpp b/src/database/records.cpp index af81dc5c..a1efb568 100644 --- a/src/database/records.cpp +++ b/src/database/records.cpp @@ -47,15 +47,30 @@ namespace database { [[maybe_unused]] static const char* kTag = "RECORDS"; +static const char kPathPrefix = 'P'; static const char kDataPrefix = 'D'; static const char kHashPrefix = 'H'; -[[maybe_unused]] static const char kTagHashPrefix = 'T'; +static const char kTagHashPrefix = 'T'; static const char kIndexPrefix = 'I'; static const char kFieldSeparator = '\0'; +static constexpr auto makePrefix(char p) -> std::string { + std::string str; + str += p; + str += kFieldSeparator; + return str; +} + +auto EncodePathKey(std::string_view path) -> std::string { + std::stringstream out{}; + out << makePrefix(kPathPrefix); + out << path; + return out.str(); +} + /* 'D/' */ auto EncodeDataPrefix() -> std::string { - return {kDataPrefix, kFieldSeparator}; + return makePrefix(kDataPrefix); } /* 'D/ 0xACAB' */ @@ -116,8 +131,7 @@ auto ParseDataValue(const leveldb::Slice& slice) -> std::shared_ptr { /* 'H/ 0xBEEF' */ auto EncodeHashKey(const uint64_t& hash) -> std::string { - return std::string{kHashPrefix, kFieldSeparator} + - cppbor::Uint{hash}.toString(); + return makePrefix(kHashPrefix) + cppbor::Uint{hash}.toString(); } auto ParseHashValue(const leveldb::Slice& slice) -> std::optional { @@ -130,18 +144,17 @@ auto EncodeHashValue(TrackId id) -> std::string { /* 'T/ 0xBEEF' */ auto EncodeTagHashKey(const uint64_t& hash) -> std::string { - return std::string{kTagHashPrefix, kFieldSeparator} + - cppbor::Uint{hash}.toString(); + return makePrefix(kTagHashPrefix) + cppbor::Uint{hash}.toString(); } /* 'I/' */ auto EncodeAllIndexesPrefix() -> std::string { - return {kIndexPrefix, kFieldSeparator}; + return makePrefix(kIndexPrefix); } auto EncodeIndexPrefix(const IndexKey::Header& header) -> std::string { std::ostringstream out; - out.put(kIndexPrefix).put(kFieldSeparator); + out << makePrefix(kIndexPrefix); cppbor::Array val{ cppbor::Uint{header.id}, cppbor::Uint{header.depth}, -- cgit v1.2.3 From d11eea7e0b93178824e804b5fd152e7793c4c277 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 21 Mar 2024 13:32:45 +1100 Subject: use charge status for staying on, not usb status usb status doesnt seem to be updating properly yet --- src/system_fsm/idle.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/system_fsm/idle.cpp b/src/system_fsm/idle.cpp index 980f0c94..e28864b3 100644 --- a/src/system_fsm/idle.cpp +++ b/src/system_fsm/idle.cpp @@ -64,9 +64,8 @@ void Idle::react(const internal::IdleTimeout& ev) { transit(); return; } - auto s = static_cast(sServices->samd().GetUsbStatus()); - ESP_LOGI(kTag, "usb status is %i", s); - if (sServices->samd().GetUsbStatus() != drivers::Samd::UsbStatus::kDetached) { + if (sServices->samd().GetChargeStatus() != + drivers::Samd::ChargeStatus::kDischarging) { // Stay powered on if we're plugged in, in order to charge faster, sync // files, flash updates, etc. return; -- cgit v1.2.3 From 175bfc4e3e9f7aa39e084d3f1625347f1d5711ec Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 25 Mar 2024 17:34:41 +1100 Subject: WIP rewrie audio pipeline+fsm guts for more reliability --- src/app_console/app_console.cpp | 18 +- src/audio/audio_converter.cpp | 51 ++++-- src/audio/audio_decoder.cpp | 78 +++------ src/audio/audio_fsm.cpp | 315 +++++++++++++++++++--------------- src/audio/include/audio_converter.hpp | 5 + src/audio/include/audio_decoder.hpp | 20 --- src/audio/include/audio_events.hpp | 108 +++++++++--- src/audio/include/audio_fsm.hpp | 53 +++--- src/audio/track_queue.cpp | 2 +- src/lua/include/property.hpp | 2 +- src/lua/property.cpp | 23 ++- src/system_fsm/include/system_fsm.hpp | 4 +- src/system_fsm/running.cpp | 2 +- src/ui/include/ui_fsm.hpp | 2 - src/ui/ui_fsm.cpp | 27 +-- 15 files changed, 384 insertions(+), 326 deletions(-) (limited to 'src') diff --git a/src/app_console/app_console.cpp b/src/app_console/app_console.cpp index 94a48955..7c7c1abc 100644 --- a/src/app_console/app_console.cpp +++ b/src/app_console/app_console.cpp @@ -53,10 +53,15 @@ namespace console { std::shared_ptr AppConsole::sServices; int CmdVersion(int argc, char** argv) { - std::cout << "firmware-version=" << esp_app_get_description()->version << std::endl; - std::cout << "samd-version=" << AppConsole::sServices->samd().Version() << std::endl; - std::cout << "collation=" << AppConsole::sServices->collator().Describe().value_or("") << std::endl; - std::cout << "database-schema=" << uint32_t(database::kCurrentDbVersion) << std::endl; + std::cout << "firmware-version=" << esp_app_get_description()->version + << std::endl; + std::cout << "samd-version=" << AppConsole::sServices->samd().Version() + << std::endl; + std::cout << "collation=" + << AppConsole::sServices->collator().Describe().value_or("") + << std::endl; + std::cout << "database-schema=" << uint32_t(database::kCurrentDbVersion) + << std::endl; return 0; } @@ -148,7 +153,7 @@ int CmdPlayFile(int argc, char** argv) { database::TrackId id = std::atoi(argv[1]); AppConsole::sServices->track_queue().append(id); } else { - std::pmr::string path{&memory::kSpiRamResource}; + std::string path; path += '/'; path += argv[1]; for (int i = 2; i < argc; i++) { @@ -156,8 +161,7 @@ int CmdPlayFile(int argc, char** argv) { path += argv[i]; } - events::Audio().Dispatch( - audio::PlayFile{.filename = {path.data(), path.size()}}); + events::Audio().Dispatch(audio::SetTrack{.new_track = path}); } return 0; diff --git a/src/audio/audio_converter.cpp b/src/audio/audio_converter.cpp index 946a0b63..1b233731 100644 --- a/src/audio/audio_converter.cpp +++ b/src/audio/audio_converter.cpp @@ -5,14 +5,17 @@ */ #include "audio_converter.hpp" +#include #include #include #include +#include "audio_events.hpp" #include "audio_sink.hpp" #include "esp_heap_caps.h" #include "esp_log.h" +#include "event_queue.hpp" #include "freertos/portmacro.h" #include "freertos/projdefs.h" #include "i2s_dac.hpp" @@ -35,7 +38,9 @@ SampleConverter::SampleConverter() resampler_(nullptr), source_(xStreamBufferCreateWithCaps(kSourceBufferLength, sizeof(sample::Sample) * 2, - MALLOC_CAP_DMA)) { + MALLOC_CAP_DMA)), + leftover_bytes_(0), + samples_sunk_(0) { input_buffer_ = { reinterpret_cast(heap_caps_calloc( kSampleBufferLength, sizeof(sample::Sample), MALLOC_CAP_DMA)), @@ -107,6 +112,19 @@ auto SampleConverter::Main() -> void { sink_->Configure(new_target); } target_format_ = new_target; + + // Send a final sample count for the previous sample rate. + if (samples_sunk_ > 0) { + events::Audio().Dispatch(internal::ConverterProgress{ + .samples_sunk = samples_sunk_, + }); + } + + samples_sunk_ = 0; + events::Audio().Dispatch(internal::ConverterConfigurationChanged{ + .src_format = source_format_, + .dst_format = target_format_, + }); } // Loop until we finish reading all the bytes indicated. There might be @@ -154,9 +172,8 @@ auto SampleConverter::HandleSamples(cpp::span input, if (source_format_ == target_format_) { // The happiest possible case: the input format matches the output // format already. - std::size_t bytes_sent = xStreamBufferSend( - sink_->stream(), input.data(), input.size_bytes(), portMAX_DELAY); - return bytes_sent / sizeof(sample::Sample); + SendToSink(input); + return input.size(); } size_t samples_used = 0; @@ -186,16 +203,26 @@ auto SampleConverter::HandleSamples(cpp::span input, samples_used = input.size(); } - size_t bytes_sent = 0; - size_t bytes_to_send = output_source.size_bytes(); - while (bytes_sent < bytes_to_send) { - bytes_sent += xStreamBufferSend( - sink_->stream(), - reinterpret_cast(output_source.data()) + bytes_sent, - bytes_to_send - bytes_sent, portMAX_DELAY); - } + SendToSink(output_source); } return samples_used; } +auto SampleConverter::SendToSink(cpp::span samples) -> void { + // Update the number of samples sunk so far *before* actually sinking them, + // since writing to the stream buffer will block when the buffer gets full. + samples_sunk_ += samples.size(); + if (samples_sunk_ >= + target_format_.sample_rate * target_format_.num_channels) { + events::Audio().Dispatch(internal::ConverterProgress{ + .samples_sunk = samples_sunk_, + }); + samples_sunk_ = 0; + } + + xStreamBufferSend(sink_->stream(), + reinterpret_cast(samples.data()), + samples.size_bytes(), portMAX_DELAY); +} + } // namespace audio diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp index 68a8a86b..55ebc0ec 100644 --- a/src/audio/audio_decoder.cpp +++ b/src/audio/audio_decoder.cpp @@ -5,6 +5,7 @@ */ #include "audio_decoder.hpp" +#include #include #include @@ -50,39 +51,6 @@ namespace audio { static constexpr std::size_t kCodecBufferLength = drivers::kI2SBufferLengthFrames * sizeof(sample::Sample); -Timer::Timer(std::shared_ptr t, - const codecs::ICodec::OutputFormat& format, - uint32_t current_seconds) - : track_(t), - current_seconds_(current_seconds), - current_sample_in_second_(0), - samples_per_second_(format.sample_rate_hz * format.num_channels), - total_duration_seconds_(format.total_samples.value_or(0) / - format.num_channels / format.sample_rate_hz) { - track_->duration = total_duration_seconds_; -} - -auto Timer::AddSamples(std::size_t samples) -> void { - bool incremented = false; - current_sample_in_second_ += samples; - while (current_sample_in_second_ >= samples_per_second_) { - current_seconds_++; - current_sample_in_second_ -= samples_per_second_; - incremented = true; - } - - if (incremented) { - if (total_duration_seconds_ < current_seconds_) { - total_duration_seconds_ = current_seconds_; - track_->duration = total_duration_seconds_; - } - - PlaybackUpdate ev{.seconds_elapsed = current_seconds_, .track = track_}; - events::Audio().Dispatch(ev); - events::Ui().Dispatch(ev); - } -} - auto Decoder::Start(std::shared_ptr source, std::shared_ptr sink) -> Decoder* { Decoder* task = new Decoder(source, sink); @@ -92,11 +60,7 @@ auto Decoder::Start(std::shared_ptr source, Decoder::Decoder(std::shared_ptr source, std::shared_ptr mixer) - : source_(source), - converter_(mixer), - codec_(), - timer_(), - current_format_() { + : source_(source), converter_(mixer), codec_(), current_format_() { ESP_LOGI(kTag, "allocating codec buffer, %u KiB", kCodecBufferLength / 1024); codec_buffer_ = { reinterpret_cast(heap_caps_calloc( @@ -117,7 +81,6 @@ void Decoder::Main() { } if (ContinueDecoding()) { - events::Audio().Dispatch(internal::InputFileFinished{}); stream_.reset(); } } @@ -129,6 +92,7 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { codec_.reset(codecs::CreateCodecForType(stream->type()).value_or(nullptr)); if (!codec_) { ESP_LOGE(kTag, "no codec found"); + events::Audio().Dispatch(internal::DecoderError{}); return false; } @@ -136,6 +100,7 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { if (open_res.has_error()) { ESP_LOGE(kTag, "codec failed to start: %s", codecs::ICodec::ErrorString(open_res.error()).c_str()); + events::Audio().Dispatch(internal::DecoderError{}); return false; } stream->SetPreambleFinished(); @@ -146,20 +111,23 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { }; ESP_LOGI(kTag, "stream started ok"); - events::Audio().Dispatch(internal::InputFileOpened{}); - - auto tags = std::make_shared(Track{ - .tags = stream->tags(), - .db_info = {}, - .bitrate_kbps = open_res->sample_rate_hz, - .encoding = stream->type(), - .filepath = stream->Filepath(), - }); - timer_.reset(new Timer(tags, open_res.value(), stream->Offset())); - PlaybackUpdate ev{.seconds_elapsed = stream->Offset(), .track = tags}; - events::Audio().Dispatch(ev); - events::Ui().Dispatch(ev); + std::optional duration; + if (open_res->total_samples) { + duration = open_res->total_samples.value() / open_res->num_channels / + open_res->sample_rate_hz; + } + + events::Audio().Dispatch(internal::DecoderOpened{ + .track = std::make_shared(TrackInfo{ + .tags = stream->tags(), + .uri = stream->Filepath(), + .duration = duration, + .start_offset = stream->Offset(), + .bitrate_kbps = open_res->sample_rate_hz, + .encoding = stream->type(), + }), + }); return true; } @@ -167,6 +135,7 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { auto Decoder::ContinueDecoding() -> bool { auto res = codec_->DecodeTo(codec_buffer_); if (res.has_error()) { + events::Audio().Dispatch(internal::DecoderError{}); return true; } @@ -176,11 +145,8 @@ auto Decoder::ContinueDecoding() -> bool { res->is_stream_finished); } - if (timer_) { - timer_->AddSamples(res->samples_written); - } - if (res->is_stream_finished) { + events::Audio().Dispatch(internal::DecoderClosed{}); codec_.reset(); } diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 05c7c216..7a138cba 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -36,6 +36,7 @@ #include "sample.hpp" #include "service_locator.hpp" #include "system_events.hpp" +#include "tinyfsm.hpp" #include "track.hpp" #include "track_queue.hpp" #include "wm8523.hpp" @@ -54,13 +55,158 @@ std::shared_ptr AudioState::sBtOutput; std::shared_ptr AudioState::sOutput; // Two seconds of samples for two channels, at a representative sample rate. -constexpr size_t kDrainBufferSize = sizeof(sample::Sample) * 48000 * 4; +constexpr size_t kDrainLatencySamples = 48000; +constexpr size_t kDrainBufferSize = + sizeof(sample::Sample) * kDrainLatencySamples * 4; + StreamBufferHandle_t AudioState::sDrainBuffer; -std::optional AudioState::sCurrentTrack; -bool AudioState::sIsPlaybackAllowed; +std::shared_ptr AudioState::sCurrentTrack; +uint64_t AudioState::sCurrentSamples; +std::optional AudioState::sCurrentFormat; + +std::shared_ptr AudioState::sNextTrack; +uint64_t AudioState::sNextTrackCueSamples; + +bool AudioState::sIsResampling; +bool AudioState::sIsPaused = true; + +auto AudioState::currentPositionSeconds() -> std::optional { + if (!sCurrentTrack || !sCurrentFormat) { + return {}; + } + return sCurrentSamples / + (sCurrentFormat->num_channels * sCurrentFormat->sample_rate); +} + +void AudioState::react(const QueueUpdate& ev) { + if (!ev.current_changed && ev.reason != QueueUpdate::kRepeatingLastTrack) { + return; + } + + SetTrack::Transition transition; + switch (ev.reason) { + case QueueUpdate::kExplicitUpdate: + transition = SetTrack::Transition::kHardCut; + break; + case QueueUpdate::kRepeatingLastTrack: + case QueueUpdate::kTrackFinished: + transition = SetTrack::Transition::kGapless; + break; + case QueueUpdate::kDeserialised: + default: + // The current track is deserialised separately in order to retain seek + // position. + return; + } + + SetTrack cmd{ + .new_track = {}, + .seek_to_second = 0, + .transition = transition, + }; -static std::optional> sLastTrackUpdate; + auto current = sServices->track_queue().current(); + if (current) { + cmd.new_track = *current; + } + + tinyfsm::FsmList::dispatch(cmd); +} + +void AudioState::react(const SetTrack& ev) { + if (ev.transition == SetTrack::Transition::kHardCut) { + clearDrainBuffer(); + } + + // Move the rest of the work to a background worker, since it may require db + // lookups to resolve a track id into a path. + auto new_track = ev.new_track; + uint32_t seek_to = ev.seek_to_second.value_or(0); + sServices->bg_worker().Dispatch([=]() { + std::optional path; + if (std::holds_alternative(new_track)) { + auto db = sServices->database().lock(); + if (db) { + path = db->getTrackPath(std::get(new_track)); + } + } else if (std::holds_alternative(new_track)) { + path = std::get(new_track); + } + + if (path) { + sFileSource->SetPath(*path, seek_to); + } else { + sFileSource->SetPath(); + } + }); +} + +void AudioState::react(const TogglePlayPause& ev) { + sIsPaused = !ev.set_to.value_or(sIsPaused); + if (!sIsPaused && is_in_state() && sCurrentTrack) { + transit(); + } else if (sIsPaused && is_in_state()) { + transit(); + } +} + +void AudioState::react(const internal::DecoderOpened& ev) { + ESP_LOGI(kTag, "decoder opened %s", ev.track->uri.c_str()); + sNextTrack = ev.track; + sNextTrackCueSamples = sCurrentSamples + kDrainLatencySamples; +} + +void AudioState::react(const internal::DecoderClosed&) { + ESP_LOGI(kTag, "decoder closed"); + // FIXME: only when we were playing the current track + sServices->track_queue().finish(); +} + +void AudioState::react(const internal::DecoderError&) { + ESP_LOGW(kTag, "decoder errored"); + // FIXME: only when we were playing the current track + sServices->track_queue().finish(); +} + +void AudioState::react(const internal::ConverterConfigurationChanged& ev) { + sCurrentFormat = ev.dst_format; + sIsResampling = ev.src_format != ev.dst_format; + ESP_LOGI(kTag, "output format now %u ch @ %lu hz (resample=%i)", + sCurrentFormat->num_channels, sCurrentFormat->sample_rate, + sIsResampling); +} + +void AudioState::react(const internal::ConverterProgress& ev) { + ESP_LOGI(kTag, "sample converter sunk %lu samples", ev.samples_sunk); + sCurrentSamples += ev.samples_sunk; + + if (sNextTrack && sCurrentSamples >= sNextTrackCueSamples) { + ESP_LOGI(kTag, "next track is now sinking"); + sCurrentTrack = sNextTrack; + sCurrentSamples -= sNextTrackCueSamples; + sCurrentSamples += + sNextTrack->start_offset.value_or(0) * + (sCurrentFormat->num_channels * sCurrentFormat->sample_rate); + + sNextTrack.reset(); + sNextTrackCueSamples = 0; + } + + PlaybackUpdate event{ + .current_track = sCurrentTrack, + .track_position = currentPositionSeconds(), + .paused = !is_in_state(), + }; + + events::System().Dispatch(event); + events::Ui().Dispatch(event); + + if (sCurrentTrack && !sIsPaused && !is_in_state()) { + ESP_LOGI(kTag, "ready to play!"); + transit(); + } +} void AudioState::react(const system_fsm::BluetoothEvent& ev) { if (ev.event != drivers::bluetooth::Event::kConnectionStateChanged) { @@ -184,17 +330,6 @@ auto AudioState::clearDrainBuffer() -> void { } } -auto AudioState::playTrack(database::TrackId id) -> void { - sCurrentTrack = id; - sServices->bg_worker().Dispatch([=]() { - auto db = sServices->database().lock(); - if (!db) { - return; - } - sFileSource->SetPath(db->getTrackPath(id)); - }); -} - auto AudioState::commitVolume() -> void { auto mode = sServices->nvs().OutputMode(); auto vol = sOutput->GetVolume(); @@ -209,23 +344,6 @@ auto AudioState::commitVolume() -> void { } } -auto AudioState::readyToPlay() -> bool { - return sCurrentTrack.has_value() && sIsPlaybackAllowed; -} - -void AudioState::react(const TogglePlayPause& ev) { - sIsPlaybackAllowed = !sIsPlaybackAllowed; - if (readyToPlay()) { - if (!is_in_state()) { - transit(); - } - } else { - if (!is_in_state()) { - transit(); - } - } -} - namespace states { void Uninitialised::react(const system_fsm::BootComplete& ev) { @@ -283,44 +401,6 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { transit(); } -void Standby::react(const PlayFile& ev) { - sCurrentTrack = 0; - sIsPlaybackAllowed = true; - sFileSource->SetPath(ev.filename); -} - -void Playback::react(const PlayFile& ev) { - sFileSource->SetPath(ev.filename); -} - -void Standby::react(const SeekFile& ev) { - clearDrainBuffer(); - sFileSource->SetPath(ev.filename, ev.offset); -} - -void Playback::react(const SeekFile& ev) { - clearDrainBuffer(); - sFileSource->SetPath(ev.filename, ev.offset); -} - -void Standby::react(const internal::InputFileOpened& ev) { - if (readyToPlay()) { - transit(); - } -} - -void Standby::react(const QueueUpdate& ev) { - auto current_track = sServices->track_queue().current(); - if (!current_track || (sCurrentTrack && (*sCurrentTrack == *current_track))) { - return; - } - if (ev.reason == QueueUpdate::Reason::kDeserialised && sLastTrackUpdate) { - return; - } - clearDrainBuffer(); - playTrack(*current_track); -} - static const char kQueueKey[] = "audio:queue"; static const char kCurrentFileKey[] = "audio:current"; @@ -328,7 +408,7 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) { if (!ev.locking) { return; } - sServices->bg_worker().Dispatch([]() { + sServices->bg_worker().Dispatch([this]() { auto db = sServices->database().lock(); if (!db) { return; @@ -341,10 +421,10 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) { } db->put(kQueueKey, queue.serialise()); - if (sLastTrackUpdate) { + if (sCurrentTrack) { cppbor::Array current_track{ - cppbor::Tstr{sLastTrackUpdate->first}, - cppbor::Uint{sLastTrackUpdate->second}, + cppbor::Tstr{sCurrentTrack->uri}, + cppbor::Uint{currentPositionSeconds().value_or(0)}, }; db->put(kCurrentFileKey, current_track.toString()); } @@ -371,8 +451,12 @@ void Standby::react(const system_fsm::StorageMounted& ev) { if (parsed->type() == cppbor::ARRAY) { std::string filename = parsed->asArray()->get(0)->asTstr()->value(); uint32_t pos = parsed->asArray()->get(1)->asUint()->value(); - sLastTrackUpdate = std::make_pair(filename, pos); - sFileSource->SetPath(filename, pos); + + events::Audio().Dispatch(SetTrack{ + .new_track = filename, + .seek_to_second = pos, + .transition = SetTrack::Transition::kHardCut, + }); } } @@ -388,76 +472,31 @@ void Standby::react(const system_fsm::StorageMounted& ev) { } void Playback::entry() { - ESP_LOGI(kTag, "beginning playback"); + ESP_LOGI(kTag, "audio output resumed"); sOutput->mode(IAudioOutput::Modes::kOnPlaying); - events::System().Dispatch(PlaybackStarted{}); - events::Ui().Dispatch(PlaybackStarted{}); + PlaybackUpdate event{ + .current_track = sCurrentTrack, + .track_position = currentPositionSeconds(), + .paused = false, + }; + + events::System().Dispatch(event); + events::Ui().Dispatch(event); } void Playback::exit() { - ESP_LOGI(kTag, "finishing playback"); + ESP_LOGI(kTag, "audio output paused"); sOutput->mode(IAudioOutput::Modes::kOnPaused); - // Stash the current volume now, in case it changed during playback, since - // we might be powering off soon. - commitVolume(); - - events::System().Dispatch(PlaybackStopped{}); - events::Ui().Dispatch(PlaybackStopped{}); -} - -void Playback::react(const system_fsm::HasPhonesChanged& ev) { - if (!ev.has_headphones) { - transit(); - } -} - -void Playback::react(const QueueUpdate& ev) { - if (!ev.current_changed) { - return; - } - // Cut the current track immediately. - if (ev.reason == QueueUpdate::Reason::kExplicitUpdate) { - clearDrainBuffer(); - } - auto current_track = sServices->track_queue().current(); - if (!current_track) { - sFileSource->SetPath(); - sCurrentTrack.reset(); - transit(); - return; - } - playTrack(*current_track); -} - -void Playback::react(const PlaybackUpdate& ev) { - ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed, - ev.track->duration); - sLastTrackUpdate = std::make_pair(ev.track->filepath, ev.seconds_elapsed); -} - -void Playback::react(const internal::InputFileOpened& ev) {} - -void Playback::react(const internal::InputFileClosed& ev) {} + PlaybackUpdate event{ + .current_track = sCurrentTrack, + .track_position = currentPositionSeconds(), + .paused = true, + }; -void Playback::react(const internal::InputFileFinished& ev) { - ESP_LOGI(kTag, "finished playing file"); - sLastTrackUpdate.reset(); - sServices->track_queue().finish(); - if (!sServices->track_queue().current()) { - for (int i = 0; i < 20; i++) { - if (xStreamBufferIsEmpty(sDrainBuffer)) { - break; - } - vTaskDelay(pdMS_TO_TICKS(200)); - } - transit(); - } -} - -void Playback::react(const internal::AudioPipelineIdle& ev) { - transit(); + events::System().Dispatch(event); + events::Ui().Dispatch(event); } } // namespace states diff --git a/src/audio/include/audio_converter.hpp b/src/audio/include/audio_converter.hpp index c2ebde60..dcd068b5 100644 --- a/src/audio/include/audio_converter.hpp +++ b/src/audio/include/audio_converter.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include #include @@ -40,6 +41,8 @@ class SampleConverter { auto SetTargetFormat(const IAudioOutput::Format& format) -> void; auto HandleSamples(cpp::span, bool) -> size_t; + auto SendToSink(cpp::span) -> void; + struct Args { IAudioOutput::Format format; size_t samples_available; @@ -59,6 +62,8 @@ class SampleConverter { IAudioOutput::Format source_format_; IAudioOutput::Format target_format_; size_t leftover_bytes_; + + uint32_t samples_sunk_; }; } // namespace audio diff --git a/src/audio/include/audio_decoder.hpp b/src/audio/include/audio_decoder.hpp index b8aac710..89f0f43c 100644 --- a/src/audio/include/audio_decoder.hpp +++ b/src/audio/include/audio_decoder.hpp @@ -19,25 +19,6 @@ namespace audio { -/* - * Sample-based timer for the current elapsed playback time. - */ -class Timer { - public: - Timer(std::shared_ptr, const codecs::ICodec::OutputFormat& format, uint32_t current_seconds = 0); - - auto AddSamples(std::size_t) -> void; - - private: - std::shared_ptr track_; - - uint32_t current_seconds_; - uint32_t current_sample_in_second_; - uint32_t samples_per_second_; - - uint32_t total_duration_seconds_; -}; - /* * Handle to a persistent task that takes bytes from the given source, decodes * them into sample::Sample (normalised to 16 bit signed PCM), and then @@ -65,7 +46,6 @@ class Decoder { std::shared_ptr stream_; std::unique_ptr codec_; - std::unique_ptr timer_; std::optional current_format_; std::optional current_sink_format_; diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp index a8533646..9af30467 100644 --- a/src/audio/include/audio_events.hpp +++ b/src/audio/include/audio_events.hpp @@ -9,8 +9,10 @@ #include #include #include +#include #include +#include "audio_sink.hpp" #include "tinyfsm.hpp" #include "track.hpp" @@ -18,24 +20,80 @@ namespace audio { -struct Track { +/* + * Struct encapsulating information about the decoder's current track. + */ +struct TrackInfo { + /* + * Audio tags extracted from the file. May be absent for files without any + * parseable tags. + */ std::shared_ptr tags; - std::shared_ptr db_info; - uint32_t duration; - uint32_t bitrate_kbps; + /* + * URI that the current track was retrieved from. This is currently always a + * file path on the SD card. + */ + std::string uri; + + /* + * The length of this track in seconds. This is either retrieved from the + * track's tags, or sometimes computed. It may therefore sometimes be + * inaccurate or missing. + */ + std::optional duration; + + /* The offset in seconds that this file's decoding started from. */ + std::optional start_offset; + + /* The approximate bitrate of this track in its original encoded form. */ + std::optional bitrate_kbps; + + /* The encoded format of the this track. */ codecs::StreamType encoding; - std::string filepath; }; -struct PlaybackStarted : tinyfsm::Event {}; - +/* + * Event emitted by the audio FSM when the state of the audio pipeline has + * changed. This is usually once per second while a track is playing, plus one + * event each when a track starts or finishes. + */ struct PlaybackUpdate : tinyfsm::Event { - uint32_t seconds_elapsed; - std::shared_ptr track; + /* + * The track that is currently being decoded by the audio pipeline. May be + * absent if there is no current track. + */ + std::shared_ptr current_track; + + /* + * How long the current track has been playing for, in seconds. Will always + * be present if current_track is present. + */ + std::optional track_position; + + /* Whether or not the current track is currently being output to a sink. */ + bool paused; +}; + +/* + * Sets a new track to be decoded by the audio pipeline, replacing any + * currently playing track. + */ +struct SetTrack : tinyfsm::Event { + std::variant new_track; + std::optional seek_to_second; + + enum Transition { + kHardCut, + kGapless, + // TODO: kCrossFade + }; + Transition transition; }; -struct PlaybackStopped : tinyfsm::Event {}; +struct TogglePlayPause : tinyfsm::Event { + std::optional set_to; +}; struct QueueUpdate : tinyfsm::Event { bool current_changed; @@ -49,15 +107,6 @@ struct QueueUpdate : tinyfsm::Event { Reason reason; }; -struct PlayFile : tinyfsm::Event { - std::string filename; -}; - -struct SeekFile : tinyfsm::Event { - uint32_t offset; - std::string filename; -}; - struct StepUpVolume : tinyfsm::Event {}; struct StepDownVolume : tinyfsm::Event {}; struct SetVolume : tinyfsm::Event { @@ -83,17 +132,26 @@ struct SetVolumeLimit : tinyfsm::Event { int limit_db; }; -struct TogglePlayPause : tinyfsm::Event {}; - struct OutputModeChanged : tinyfsm::Event {}; namespace internal { -struct InputFileOpened : tinyfsm::Event {}; -struct InputFileClosed : tinyfsm::Event {}; -struct InputFileFinished : tinyfsm::Event {}; +struct DecoderOpened : tinyfsm::Event { + std::shared_ptr track; +}; + +struct DecoderClosed : tinyfsm::Event {}; + +struct DecoderError : tinyfsm::Event {}; -struct AudioPipelineIdle : tinyfsm::Event {}; +struct ConverterConfigurationChanged : tinyfsm::Event { + IAudioOutput::Format src_format; + IAudioOutput::Format dst_format; +}; + +struct ConverterProgress : tinyfsm::Event { + uint32_t samples_sunk; +}; } // namespace internal diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp index 13e241be..62bb4786 100644 --- a/src/audio/include/audio_fsm.hpp +++ b/src/audio/include/audio_fsm.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -41,6 +42,17 @@ class AudioState : public tinyfsm::Fsm { /* Fallback event handler. Does nothing. */ void react(const tinyfsm::Event& ev) {} + void react(const QueueUpdate&); + void react(const SetTrack&); + void react(const TogglePlayPause&); + + void react(const internal::DecoderOpened&); + void react(const internal::DecoderClosed&); + void react(const internal::DecoderError&); + + void react(const internal::ConverterConfigurationChanged&); + void react(const internal::ConverterProgress&); + void react(const StepUpVolume&); void react(const StepDownVolume&); virtual void react(const system_fsm::HasPhonesChanged&); @@ -56,17 +68,6 @@ class AudioState : public tinyfsm::Fsm { virtual void react(const system_fsm::StorageMounted&) {} virtual void react(const system_fsm::BluetoothEvent&); - virtual void react(const PlayFile&) {} - virtual void react(const SeekFile&) {} - virtual void react(const QueueUpdate&) {} - virtual void react(const PlaybackUpdate&) {} - void react(const TogglePlayPause&); - - 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: auto clearDrainBuffer() -> void; auto playTrack(database::TrackId id) -> void; @@ -83,10 +84,17 @@ class AudioState : public tinyfsm::Fsm { static StreamBufferHandle_t sDrainBuffer; - static std::optional sCurrentTrack; + static std::shared_ptr sCurrentTrack; + static uint64_t sCurrentSamples; + static std::optional sCurrentFormat; - auto readyToPlay() -> bool; - static bool sIsPlaybackAllowed; + static std::shared_ptr sNextTrack; + static uint64_t sNextTrackCueSamples; + + static bool sIsResampling; + static bool sIsPaused; + + auto currentPositionSeconds() -> std::optional; }; namespace states { @@ -94,7 +102,6 @@ namespace states { class Uninitialised : public AudioState { public: void react(const system_fsm::BootComplete&) override; - void react(const system_fsm::BluetoothEvent&) override{}; using AudioState::react; @@ -102,10 +109,6 @@ class Uninitialised : public AudioState { class Standby : public AudioState { public: - void react(const PlayFile&) override; - void react(const SeekFile&) override; - void react(const internal::InputFileOpened&) override; - void react(const QueueUpdate&) override; void react(const system_fsm::KeyLockChanged&) override; void react(const system_fsm::StorageMounted&) override; @@ -117,18 +120,6 @@ class Playback : public AudioState { void entry() override; void exit() override; - void react(const system_fsm::HasPhonesChanged&) override; - - void react(const PlayFile&) override; - void react(const SeekFile&) override; - void react(const QueueUpdate&) override; - void react(const PlaybackUpdate&) override; - - void react(const internal::InputFileOpened&) override; - void react(const internal::InputFileClosed&) override; - void react(const internal::InputFileFinished&) override; - void react(const internal::AudioPipelineIdle&) override; - using AudioState::react; }; diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp index a3f4c815..dbe283c4 100644 --- a/src/audio/track_queue.cpp +++ b/src/audio/track_queue.cpp @@ -136,7 +136,7 @@ auto TrackQueue::insert(Item i, size_t index) -> void { { const std::shared_lock lock(mutex_); was_queue_empty = pos_ == tracks_.size(); - current_changed = pos_ == was_queue_empty || index == pos_; + current_changed = was_queue_empty || index == pos_; } auto update_shuffler = [=, this]() { diff --git a/src/lua/include/property.hpp b/src/lua/include/property.hpp index 7d160fba..f19fdeec 100644 --- a/src/lua/include/property.hpp +++ b/src/lua/include/property.hpp @@ -23,7 +23,7 @@ using LuaValue = std::variant>; diff --git a/src/lua/property.cpp b/src/lua/property.cpp index f721f9ce..200f4d5c 100644 --- a/src/lua/property.cpp +++ b/src/lua/property.cpp @@ -221,7 +221,7 @@ static auto pushTagValue(lua_State* L, const database::TagValue& val) -> void { val); } -static void pushTrack(lua_State* L, const audio::Track& track) { +static void pushTrack(lua_State* L, const audio::TrackInfo& track) { lua_newtable(L); for (const auto& tag : track.tags->allPresent()) { @@ -229,19 +229,18 @@ static void pushTrack(lua_State* L, const audio::Track& track) { pushTagValue(L, track.tags->get(tag)); lua_settable(L, -3); } - if (track.db_info) { - lua_pushliteral(L, "id"); - lua_pushinteger(L, track.db_info->id); + + if (track.duration) { + lua_pushliteral(L, "duration"); + lua_pushinteger(L, track.duration.value()); lua_settable(L, -3); } - lua_pushliteral(L, "duration"); - lua_pushinteger(L, track.duration); - lua_settable(L, -3); - - lua_pushliteral(L, "bitrate_kbps"); - lua_pushinteger(L, track.bitrate_kbps); - lua_settable(L, -3); + if (track.bitrate_kbps) { + lua_pushliteral(L, "bitrate_kbps"); + lua_pushinteger(L, track.bitrate_kbps.value()); + lua_settable(L, -3); + } lua_pushliteral(L, "encoding"); lua_pushstring(L, codecs::StreamTypeToString(track.encoding).c_str()); @@ -289,7 +288,7 @@ auto Property::PushValue(lua_State& s) -> int { lua_pushboolean(&s, arg); } else if constexpr (std::is_same_v) { lua_pushstring(&s, arg.c_str()); - } else if constexpr (std::is_same_v) { + } else if constexpr (std::is_same_v) { pushTrack(&s, arg); } else if constexpr (std::is_same_v) { pushDevice(&s, arg); diff --git a/src/system_fsm/include/system_fsm.hpp b/src/system_fsm/include/system_fsm.hpp index cc60e43b..a129829e 100644 --- a/src/system_fsm/include/system_fsm.hpp +++ b/src/system_fsm/include/system_fsm.hpp @@ -63,7 +63,7 @@ class SystemState : public tinyfsm::Fsm { virtual void react(const SdDetectChanged&) {} virtual void react(const SamdUsbMscChanged&) {} virtual void react(const database::event::UpdateFinished&) {} - virtual void react(const audio::PlaybackStopped&) {} + virtual void react(const audio::PlaybackUpdate&) {} virtual void react(const internal::IdleTimeout&) {} virtual void react(const internal::UnmountTimeout&) {} @@ -101,7 +101,7 @@ class Running : public SystemState { void react(const KeyLockChanged&) override; void react(const SdDetectChanged&) override; - void react(const audio::PlaybackStopped&) override; + void react(const audio::PlaybackUpdate&) override; void react(const database::event::UpdateFinished&) override; void react(const SamdUsbMscChanged&) override; void react(const internal::UnmountTimeout&) override; diff --git a/src/system_fsm/running.cpp b/src/system_fsm/running.cpp index d80809e6..a6ab5d47 100644 --- a/src/system_fsm/running.cpp +++ b/src/system_fsm/running.cpp @@ -56,7 +56,7 @@ void Running::react(const KeyLockChanged& ev) { checkIdle(); } -void Running::react(const audio::PlaybackStopped& ev) { +void Running::react(const audio::PlaybackUpdate& ev) { checkIdle(); } diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index f7fde1dd..5e1cc487 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -57,8 +57,6 @@ class UiState : public tinyfsm::Fsm { virtual void react(const system_fsm::StorageMounted&) {} void react(const system_fsm::BatteryStateChanged&); - void react(const audio::PlaybackStarted&); - void react(const audio::PlaybackStopped&); void react(const audio::PlaybackUpdate&); void react(const audio::QueueUpdate&); diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index a913a339..42c6a99c 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -114,14 +114,11 @@ lua::Property UiState::sBluetoothDevices{ lua::Property UiState::sPlaybackPlaying{ false, [](const lua::LuaValue& val) { - bool current_val = std::get(sPlaybackPlaying.Get()); if (!std::holds_alternative(val)) { return false; } bool new_val = std::get(val); - if (current_val != new_val) { - events::Audio().Dispatch(audio::TogglePlayPause{}); - } + events::Audio().Dispatch(audio::TogglePlayPause{.set_to = new_val}); return true; }}; @@ -135,12 +132,13 @@ lua::Property UiState::sPlaybackPosition{ int new_val = std::get(val); if (current_val != new_val) { auto track = sPlaybackTrack.Get(); - if (!std::holds_alternative(track)) { + if (!std::holds_alternative(track)) { return false; } - events::Audio().Dispatch(audio::SeekFile{ - .offset = (uint32_t)new_val, - .filename = std::get(track).filepath}); + events::Audio().Dispatch(audio::SetTrack{ + .new_track = std::get(track).uri, + .seek_to_second = (uint32_t)new_val, + }); } return true; }}; @@ -393,17 +391,10 @@ void UiState::react(const audio::QueueUpdate&) { sQueueReplay.Update(queue.replay()); } -void UiState::react(const audio::PlaybackStarted& ev) { - sPlaybackPlaying.Update(true); -} - void UiState::react(const audio::PlaybackUpdate& ev) { - sPlaybackTrack.Update(*ev.track); - sPlaybackPosition.Update(static_cast(ev.seconds_elapsed)); -} - -void UiState::react(const audio::PlaybackStopped&) { - sPlaybackPlaying.Update(false); + sPlaybackTrack.Update(*ev.current_track); + sPlaybackPlaying.Update(!ev.paused); + sPlaybackPosition.Update(static_cast(ev.track_position.value_or(0))); } void UiState::react(const audio::VolumeChanged& ev) { -- cgit v1.2.3 From 078b77d0f796be3c787f62b9b830512e38d3b076 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 26 Mar 2024 12:12:42 +1100 Subject: pass stream start/update/end events through the whole pipeline --- src/audio/audio_converter.cpp | 209 ++++++++++++++++++++-------------- src/audio/audio_decoder.cpp | 34 +++--- src/audio/audio_fsm.cpp | 31 ++--- src/audio/include/audio_converter.hpp | 18 +-- src/audio/include/audio_events.hpp | 15 +-- src/audio/include/audio_fsm.hpp | 9 +- src/ui/ui_fsm.cpp | 6 +- 7 files changed, 172 insertions(+), 150 deletions(-) (limited to 'src') diff --git a/src/audio/audio_converter.cpp b/src/audio/audio_converter.cpp index 1b233731..ebbd405f 100644 --- a/src/audio/audio_converter.cpp +++ b/src/audio/audio_converter.cpp @@ -28,7 +28,7 @@ [[maybe_unused]] static constexpr char kTag[] = "mixer"; static constexpr std::size_t kSampleBufferLength = - drivers::kI2SBufferLengthFrames * sizeof(sample::Sample); + drivers::kI2SBufferLengthFrames * sizeof(sample::Sample) * 2; static constexpr std::size_t kSourceBufferLength = kSampleBufferLength * 2; namespace audio { @@ -68,24 +68,32 @@ auto SampleConverter::SetOutput(std::shared_ptr output) -> void { sink_ = output; } -auto SampleConverter::ConvertSamples(cpp::span input, - const IAudioOutput::Format& format, - bool is_eos) -> void { +auto SampleConverter::beginStream(std::shared_ptr track) -> void { Args args{ - .format = format, + .track = new std::shared_ptr(track), + .samples_available = 0, + .is_end_of_stream = false, + }; + xQueueSend(commands_, &args, portMAX_DELAY); +} + +auto SampleConverter::continueStream(cpp::span input) -> void { + Args args{ + .track = nullptr, .samples_available = input.size(), - .is_end_of_stream = is_eos, + .is_end_of_stream = false, }; xQueueSend(commands_, &args, portMAX_DELAY); + xStreamBufferSend(source_, input.data(), input.size_bytes(), portMAX_DELAY); +} - cpp::span input_as_bytes = { - reinterpret_cast(input.data()), input.size_bytes()}; - size_t bytes_sent = 0; - while (bytes_sent < input_as_bytes.size()) { - bytes_sent += xStreamBufferSend( - source_, input_as_bytes.subspan(bytes_sent).data(), - input_as_bytes.size() - bytes_sent, pdMS_TO_TICKS(100)); - } +auto SampleConverter::endStream() -> void { + Args args{ + .track = nullptr, + .samples_available = 0, + .is_end_of_stream = true, + }; + xQueueSend(commands_, &args, portMAX_DELAY); } auto SampleConverter::Main() -> void { @@ -93,86 +101,93 @@ auto SampleConverter::Main() -> void { Args args; while (!xQueueReceive(commands_, &args, portMAX_DELAY)) { } - if (args.format != source_format_) { - resampler_.reset(); - source_format_ = args.format; - leftover_bytes_ = 0; - - auto new_target = sink_->PrepareFormat(args.format); - if (new_target != target_format_) { - // The new format is different to the old one. Wait for the sink to - // drain before continuing. - while (!xStreamBufferIsEmpty(sink_->stream())) { - ESP_LOGI(kTag, "waiting for sink stream to drain..."); - // TODO(jacqueline): Get the sink drain ISR to notify us of this - // via semaphore instead of busy-ish waiting. - vTaskDelay(pdMS_TO_TICKS(10)); - } - - sink_->Configure(new_target); - } - target_format_ = new_target; - // Send a final sample count for the previous sample rate. - if (samples_sunk_ > 0) { - events::Audio().Dispatch(internal::ConverterProgress{ - .samples_sunk = samples_sunk_, - }); + if (args.track) { + handleBeginStream(*args.track); + delete args.track; + } + if (args.samples_available) { + handleContinueStream(args.samples_available); + } + if (args.is_end_of_stream) { + handleEndStream(); + } + } +} + +auto SampleConverter::handleBeginStream(std::shared_ptr track) + -> void { + if (track->format != source_format_) { + resampler_.reset(); + source_format_ = track->format; + leftover_bytes_ = 0; + + auto new_target = sink_->PrepareFormat(track->format); + if (new_target != target_format_) { + // The new format is different to the old one. Wait for the sink to + // drain before continuing. + while (!xStreamBufferIsEmpty(sink_->stream())) { + ESP_LOGI(kTag, "waiting for sink stream to drain..."); + // TODO(jacqueline): Get the sink drain ISR to notify us of this + // via semaphore instead of busy-ish waiting. + vTaskDelay(pdMS_TO_TICKS(10)); } - samples_sunk_ = 0; - events::Audio().Dispatch(internal::ConverterConfigurationChanged{ - .src_format = source_format_, - .dst_format = target_format_, - }); + sink_->Configure(new_target); } + target_format_ = new_target; + } - // Loop until we finish reading all the bytes indicated. There might be - // leftovers from each iteration, and from this process as a whole, - // depending on the resampling stage. - size_t bytes_read = 0; - size_t bytes_to_read = args.samples_available * sizeof(sample::Sample); - while (bytes_read < bytes_to_read) { - // First top up the input buffer, taking care not to overwrite anything - // remaining from a previous iteration. - size_t bytes_read_this_it = xStreamBufferReceive( - source_, input_buffer_as_bytes_.subspan(leftover_bytes_).data(), - std::min(input_buffer_as_bytes_.size() - leftover_bytes_, - bytes_to_read - bytes_read), - portMAX_DELAY); - bytes_read += bytes_read_this_it; - - // Calculate the number of whole samples that are now in the input buffer. - size_t bytes_in_buffer = bytes_read_this_it + leftover_bytes_; - size_t samples_in_buffer = bytes_in_buffer / sizeof(sample::Sample); - - size_t samples_used = - HandleSamples(input_buffer_.first(samples_in_buffer), - args.is_end_of_stream && bytes_read == bytes_to_read); - - // Maybe the resampler didn't consume everything. Maybe the last few - // bytes we read were half a frame. Either way, we need to calculate the - // size of the remainder in bytes, then move it to the front of our - // buffer. - size_t bytes_used = samples_used * sizeof(sample::Sample); - assert(bytes_used <= bytes_in_buffer); - - leftover_bytes_ = bytes_in_buffer - bytes_used; - if (leftover_bytes_ > 0) { - std::memmove(input_buffer_as_bytes_.data(), - input_buffer_as_bytes_.data() + bytes_used, - leftover_bytes_); - } + samples_sunk_ = 0; + events::Audio().Dispatch(internal::StreamStarted{ + .track = track, + .src_format = source_format_, + .dst_format = target_format_, + }); +} + +auto SampleConverter::handleContinueStream(size_t samples_available) -> void { + // Loop until we finish reading all the bytes indicated. There might be + // leftovers from each iteration, and from this process as a whole, + // depending on the resampling stage. + size_t bytes_read = 0; + size_t bytes_to_read = samples_available * sizeof(sample::Sample); + while (bytes_read < bytes_to_read) { + // First top up the input buffer, taking care not to overwrite anything + // remaining from a previous iteration. + size_t bytes_read_this_it = xStreamBufferReceive( + source_, input_buffer_as_bytes_.subspan(leftover_bytes_).data(), + std::min(input_buffer_as_bytes_.size() - leftover_bytes_, + bytes_to_read - bytes_read), + portMAX_DELAY); + bytes_read += bytes_read_this_it; + + // Calculate the number of whole samples that are now in the input buffer. + size_t bytes_in_buffer = bytes_read_this_it + leftover_bytes_; + size_t samples_in_buffer = bytes_in_buffer / sizeof(sample::Sample); + + size_t samples_used = handleSamples(input_buffer_.first(samples_in_buffer)); + + // Maybe the resampler didn't consume everything. Maybe the last few + // bytes we read were half a frame. Either way, we need to calculate the + // size of the remainder in bytes, then move it to the front of our + // buffer. + size_t bytes_used = samples_used * sizeof(sample::Sample); + assert(bytes_used <= bytes_in_buffer); + + leftover_bytes_ = bytes_in_buffer - bytes_used; + if (leftover_bytes_ > 0) { + std::memmove(input_buffer_as_bytes_.data(), + input_buffer_as_bytes_.data() + bytes_used, leftover_bytes_); } } } -auto SampleConverter::HandleSamples(cpp::span input, - bool is_eos) -> size_t { +auto SampleConverter::handleSamples(cpp::span input) -> size_t { if (source_format_ == target_format_) { // The happiest possible case: the input format matches the output // format already. - SendToSink(input); + sendToSink(input); return input.size(); } @@ -190,7 +205,7 @@ auto SampleConverter::HandleSamples(cpp::span input, size_t read, written; std::tie(read, written) = resampler_->Process(input.subspan(samples_used), - resampled_buffer_, is_eos); + resampled_buffer_, false); samples_used += read; if (read == 0 && written == 0) { @@ -203,18 +218,40 @@ auto SampleConverter::HandleSamples(cpp::span input, samples_used = input.size(); } - SendToSink(output_source); + sendToSink(output_source); } + return samples_used; } -auto SampleConverter::SendToSink(cpp::span samples) -> void { +auto SampleConverter::handleEndStream() -> void { + if (resampler_) { + size_t read, written; + std::tie(read, written) = resampler_->Process({}, resampled_buffer_, true); + + if (written > 0) { + sendToSink(resampled_buffer_.first(written)); + } + } + + // Send a final update to finish off this stream's samples. + if (samples_sunk_ > 0) { + events::Audio().Dispatch(internal::StreamUpdate{ + .samples_sunk = samples_sunk_, + }); + samples_sunk_ = 0; + } + + events::Audio().Dispatch(internal::StreamEnded{}); +} + +auto SampleConverter::sendToSink(cpp::span samples) -> void { // Update the number of samples sunk so far *before* actually sinking them, // since writing to the stream buffer will block when the buffer gets full. samples_sunk_ += samples.size(); if (samples_sunk_ >= target_format_.sample_rate * target_format_.num_channels) { - events::Audio().Dispatch(internal::ConverterProgress{ + events::Audio().Dispatch(internal::StreamUpdate{ .samples_sunk = samples_sunk_, }); samples_sunk_ = 0; diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp index 55ebc0ec..90c69c16 100644 --- a/src/audio/audio_decoder.cpp +++ b/src/audio/audio_decoder.cpp @@ -72,7 +72,6 @@ void Decoder::Main() { for (;;) { if (source_->HasNewStream() || !stream_) { std::shared_ptr new_stream = source_->NextStream(); - ESP_LOGI(kTag, "decoder has new stream"); if (new_stream && BeginDecoding(new_stream)) { stream_ = new_stream; } else { @@ -91,8 +90,7 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { codec_.reset(); codec_.reset(codecs::CreateCodecForType(stream->type()).value_or(nullptr)); if (!codec_) { - ESP_LOGE(kTag, "no codec found"); - events::Audio().Dispatch(internal::DecoderError{}); + ESP_LOGE(kTag, "no codec found for stream"); return false; } @@ -100,7 +98,6 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { if (open_res.has_error()) { ESP_LOGE(kTag, "codec failed to start: %s", codecs::ICodec::ErrorString(open_res.error()).c_str()); - events::Audio().Dispatch(internal::DecoderError{}); return false; } stream->SetPreambleFinished(); @@ -110,24 +107,21 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { .bits_per_sample = 16, }; - ESP_LOGI(kTag, "stream started ok"); - std::optional duration; if (open_res->total_samples) { duration = open_res->total_samples.value() / open_res->num_channels / open_res->sample_rate_hz; } - events::Audio().Dispatch(internal::DecoderOpened{ - .track = std::make_shared(TrackInfo{ - .tags = stream->tags(), - .uri = stream->Filepath(), - .duration = duration, - .start_offset = stream->Offset(), - .bitrate_kbps = open_res->sample_rate_hz, - .encoding = stream->type(), - }), - }); + converter_->beginStream(std::make_shared(TrackInfo{ + .tags = stream->tags(), + .uri = stream->Filepath(), + .duration = duration, + .start_offset = stream->Offset(), + .bitrate_kbps = open_res->sample_rate_hz, + .encoding = stream->type(), + .format = *current_sink_format_, + })); return true; } @@ -135,18 +129,16 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { auto Decoder::ContinueDecoding() -> bool { auto res = codec_->DecodeTo(codec_buffer_); if (res.has_error()) { - events::Audio().Dispatch(internal::DecoderError{}); + converter_->endStream(); return true; } if (res->samples_written > 0) { - converter_->ConvertSamples(codec_buffer_.first(res->samples_written), - current_sink_format_.value(), - res->is_stream_finished); + converter_->continueStream(codec_buffer_.first(res->samples_written)); } if (res->is_stream_finished) { - events::Audio().Dispatch(internal::DecoderClosed{}); + converter_->endStream(); codec_.reset(); } diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 7a138cba..a6f4f4d1 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -55,9 +55,9 @@ std::shared_ptr AudioState::sBtOutput; std::shared_ptr AudioState::sOutput; // Two seconds of samples for two channels, at a representative sample rate. -constexpr size_t kDrainLatencySamples = 48000; +constexpr size_t kDrainLatencySamples = 48000 * 2 * 2; constexpr size_t kDrainBufferSize = - sizeof(sample::Sample) * kDrainLatencySamples * 4; + sizeof(sample::Sample) * kDrainLatencySamples; StreamBufferHandle_t AudioState::sDrainBuffer; @@ -151,33 +151,24 @@ void AudioState::react(const TogglePlayPause& ev) { } } -void AudioState::react(const internal::DecoderOpened& ev) { - ESP_LOGI(kTag, "decoder opened %s", ev.track->uri.c_str()); +void AudioState::react(const internal::StreamStarted& ev) { + sCurrentFormat = ev.dst_format; + sIsResampling = ev.src_format != ev.dst_format; sNextTrack = ev.track; sNextTrackCueSamples = sCurrentSamples + kDrainLatencySamples; -} -void AudioState::react(const internal::DecoderClosed&) { - ESP_LOGI(kTag, "decoder closed"); - // FIXME: only when we were playing the current track - sServices->track_queue().finish(); + ESP_LOGI(kTag, "new stream %s %u ch @ %lu hz (resample=%i)", + ev.track->uri.c_str(), sCurrentFormat->num_channels, + sCurrentFormat->sample_rate, sIsResampling); } -void AudioState::react(const internal::DecoderError&) { - ESP_LOGW(kTag, "decoder errored"); +void AudioState::react(const internal::StreamEnded&) { + ESP_LOGI(kTag, "stream ended"); // FIXME: only when we were playing the current track sServices->track_queue().finish(); } -void AudioState::react(const internal::ConverterConfigurationChanged& ev) { - sCurrentFormat = ev.dst_format; - sIsResampling = ev.src_format != ev.dst_format; - ESP_LOGI(kTag, "output format now %u ch @ %lu hz (resample=%i)", - sCurrentFormat->num_channels, sCurrentFormat->sample_rate, - sIsResampling); -} - -void AudioState::react(const internal::ConverterProgress& ev) { +void AudioState::react(const internal::StreamUpdate& ev) { ESP_LOGI(kTag, "sample converter sunk %lu samples", ev.samples_sunk); sCurrentSamples += ev.samples_sunk; diff --git a/src/audio/include/audio_converter.hpp b/src/audio/include/audio_converter.hpp index dcd068b5..232b5d8e 100644 --- a/src/audio/include/audio_converter.hpp +++ b/src/audio/include/audio_converter.hpp @@ -10,6 +10,7 @@ #include #include +#include "audio_events.hpp" #include "audio_sink.hpp" #include "audio_source.hpp" #include "codec.hpp" @@ -31,20 +32,23 @@ class SampleConverter { auto SetOutput(std::shared_ptr) -> void; - auto ConvertSamples(cpp::span, - const IAudioOutput::Format& format, - bool is_eos) -> void; + auto beginStream(std::shared_ptr) -> void; + auto continueStream(cpp::span) -> void; + auto endStream() -> void; private: auto Main() -> void; - auto SetTargetFormat(const IAudioOutput::Format& format) -> void; - auto HandleSamples(cpp::span, bool) -> size_t; + auto handleBeginStream(std::shared_ptr) -> void; + auto handleContinueStream(size_t samples_available) -> void; + auto handleEndStream() -> void; - auto SendToSink(cpp::span) -> void; + auto handleSamples(cpp::span) -> size_t; + + auto sendToSink(cpp::span) -> void; struct Args { - IAudioOutput::Format format; + std::shared_ptr* track; size_t samples_available; bool is_end_of_stream; }; diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp index 9af30467..b8a0dba6 100644 --- a/src/audio/include/audio_events.hpp +++ b/src/audio/include/audio_events.hpp @@ -51,6 +51,8 @@ struct TrackInfo { /* The encoded format of the this track. */ codecs::StreamType encoding; + + IAudioOutput::Format format; }; /* @@ -136,23 +138,18 @@ struct OutputModeChanged : tinyfsm::Event {}; namespace internal { -struct DecoderOpened : tinyfsm::Event { +struct StreamStarted : tinyfsm::Event { std::shared_ptr track; -}; - -struct DecoderClosed : tinyfsm::Event {}; - -struct DecoderError : tinyfsm::Event {}; - -struct ConverterConfigurationChanged : tinyfsm::Event { IAudioOutput::Format src_format; IAudioOutput::Format dst_format; }; -struct ConverterProgress : tinyfsm::Event { +struct StreamUpdate : tinyfsm::Event { uint32_t samples_sunk; }; +struct StreamEnded : tinyfsm::Event {}; + } // namespace internal } // namespace audio diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp index 62bb4786..c00813ac 100644 --- a/src/audio/include/audio_fsm.hpp +++ b/src/audio/include/audio_fsm.hpp @@ -46,12 +46,9 @@ class AudioState : public tinyfsm::Fsm { void react(const SetTrack&); void react(const TogglePlayPause&); - void react(const internal::DecoderOpened&); - void react(const internal::DecoderClosed&); - void react(const internal::DecoderError&); - - void react(const internal::ConverterConfigurationChanged&); - void react(const internal::ConverterProgress&); + void react(const internal::StreamStarted&); + void react(const internal::StreamUpdate&); + void react(const internal::StreamEnded&); void react(const StepUpVolume&); void react(const StepDownVolume&); diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 42c6a99c..acc1bf10 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -392,7 +392,11 @@ void UiState::react(const audio::QueueUpdate&) { } void UiState::react(const audio::PlaybackUpdate& ev) { - sPlaybackTrack.Update(*ev.current_track); + if (ev.current_track) { + sPlaybackTrack.Update(*ev.current_track); + } else { + sPlaybackTrack.Update(std::monostate{}); + } sPlaybackPlaying.Update(!ev.paused); sPlaybackPosition.Update(static_cast(ev.track_position.value_or(0))); } -- cgit v1.2.3 From 4cec85af2d779ea8f6e3b46dfbea61ef5b0419f8 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 26 Mar 2024 16:45:20 +1100 Subject: implement handling of stream/playback ending --- src/audio/audio_converter.cpp | 1 + src/audio/audio_fsm.cpp | 119 +++++++++++++++++++++++++++------------- src/audio/i2s_audio_output.cpp | 3 - src/audio/include/audio_fsm.hpp | 6 +- 4 files changed, 88 insertions(+), 41 deletions(-) (limited to 'src') diff --git a/src/audio/audio_converter.cpp b/src/audio/audio_converter.cpp index ebbd405f..015be6a3 100644 --- a/src/audio/audio_converter.cpp +++ b/src/audio/audio_converter.cpp @@ -241,6 +241,7 @@ auto SampleConverter::handleEndStream() -> void { }); samples_sunk_ = 0; } + leftover_bytes_ = 0; events::Audio().Dispatch(internal::StreamEnded{}); } diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index a6f4f4d1..07737872 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -60,38 +60,58 @@ constexpr size_t kDrainBufferSize = sizeof(sample::Sample) * kDrainLatencySamples; StreamBufferHandle_t AudioState::sDrainBuffer; +std::optional AudioState::sDrainFormat; std::shared_ptr AudioState::sCurrentTrack; uint64_t AudioState::sCurrentSamples; -std::optional AudioState::sCurrentFormat; +bool AudioState::sCurrentTrackIsFromQueue; std::shared_ptr AudioState::sNextTrack; uint64_t AudioState::sNextTrackCueSamples; +bool AudioState::sNextTrackIsFromQueue; bool AudioState::sIsResampling; bool AudioState::sIsPaused = true; auto AudioState::currentPositionSeconds() -> std::optional { - if (!sCurrentTrack || !sCurrentFormat) { + if (!sCurrentTrack || !sDrainFormat) { return {}; } return sCurrentSamples / - (sCurrentFormat->num_channels * sCurrentFormat->sample_rate); + (sDrainFormat->num_channels * sDrainFormat->sample_rate); } void AudioState::react(const QueueUpdate& ev) { - if (!ev.current_changed && ev.reason != QueueUpdate::kRepeatingLastTrack) { - return; + SetTrack cmd{ + .new_track = std::monostate{}, + .seek_to_second = {}, + .transition = SetTrack::Transition::kHardCut, + }; + + auto current = sServices->track_queue().current(); + if (current) { + cmd.new_track = *current; } - SetTrack::Transition transition; switch (ev.reason) { case QueueUpdate::kExplicitUpdate: - transition = SetTrack::Transition::kHardCut; + if (!ev.current_changed) { + return; + } + sNextTrackIsFromQueue = true; + cmd.transition = SetTrack::Transition::kHardCut; break; case QueueUpdate::kRepeatingLastTrack: + sNextTrackIsFromQueue = true; + cmd.transition = SetTrack::Transition::kGapless; + break; case QueueUpdate::kTrackFinished: - transition = SetTrack::Transition::kGapless; + if (!ev.current_changed) { + cmd.new_track = std::monostate{}; + } else { + sNextTrackIsFromQueue = true; + } + cmd.transition = SetTrack::Transition::kGapless; break; case QueueUpdate::kDeserialised: default: @@ -100,25 +120,29 @@ void AudioState::react(const QueueUpdate& ev) { return; } - SetTrack cmd{ - .new_track = {}, - .seek_to_second = 0, - .transition = transition, - }; - - auto current = sServices->track_queue().current(); - if (current) { - cmd.new_track = *current; - } - tinyfsm::FsmList::dispatch(cmd); } void AudioState::react(const SetTrack& ev) { if (ev.transition == SetTrack::Transition::kHardCut) { + sCurrentTrack.reset(); + sCurrentSamples = 0; + sCurrentTrackIsFromQueue = false; clearDrainBuffer(); } + if (std::holds_alternative(ev.new_track)) { + ESP_LOGI(kTag, "playback finished, awaiting drain"); + sFileSource->SetPath(); + awaitEmptyDrainBuffer(); + sCurrentTrack.reset(); + sDrainFormat.reset(); + sCurrentSamples = 0; + sCurrentTrackIsFromQueue = false; + transit(); + return; + } + // Move the rest of the work to a background worker, since it may require db // lookups to resolve a track id into a path. auto new_track = ev.new_track; @@ -152,46 +176,56 @@ void AudioState::react(const TogglePlayPause& ev) { } void AudioState::react(const internal::StreamStarted& ev) { - sCurrentFormat = ev.dst_format; + sDrainFormat = ev.dst_format; sIsResampling = ev.src_format != ev.dst_format; + sNextTrack = ev.track; - sNextTrackCueSamples = sCurrentSamples + kDrainLatencySamples; + sNextTrackCueSamples = sCurrentSamples + (kDrainLatencySamples / 2); ESP_LOGI(kTag, "new stream %s %u ch @ %lu hz (resample=%i)", - ev.track->uri.c_str(), sCurrentFormat->num_channels, - sCurrentFormat->sample_rate, sIsResampling); + ev.track->uri.c_str(), sDrainFormat->num_channels, + sDrainFormat->sample_rate, sIsResampling); } void AudioState::react(const internal::StreamEnded&) { ESP_LOGI(kTag, "stream ended"); - // FIXME: only when we were playing the current track - sServices->track_queue().finish(); + + if (sCurrentTrackIsFromQueue) { + sServices->track_queue().finish(); + } else { + tinyfsm::FsmList::dispatch(SetTrack{ + .new_track = std::monostate{}, + .seek_to_second = {}, + .transition = SetTrack::Transition::kGapless, + }); + } } void AudioState::react(const internal::StreamUpdate& ev) { - ESP_LOGI(kTag, "sample converter sunk %lu samples", ev.samples_sunk); sCurrentSamples += ev.samples_sunk; if (sNextTrack && sCurrentSamples >= sNextTrackCueSamples) { ESP_LOGI(kTag, "next track is now sinking"); sCurrentTrack = sNextTrack; sCurrentSamples -= sNextTrackCueSamples; - sCurrentSamples += - sNextTrack->start_offset.value_or(0) * - (sCurrentFormat->num_channels * sCurrentFormat->sample_rate); + sCurrentSamples += sNextTrack->start_offset.value_or(0) * + (sDrainFormat->num_channels * sDrainFormat->sample_rate); + sCurrentTrackIsFromQueue = sNextTrackIsFromQueue; sNextTrack.reset(); sNextTrackCueSamples = 0; + sNextTrackIsFromQueue = false; } - PlaybackUpdate event{ - .current_track = sCurrentTrack, - .track_position = currentPositionSeconds(), - .paused = !is_in_state(), - }; - - events::System().Dispatch(event); - events::Ui().Dispatch(event); + if (sCurrentTrack) { + PlaybackUpdate event{ + .current_track = sCurrentTrack, + .track_position = currentPositionSeconds(), + .paused = !is_in_state(), + }; + events::System().Dispatch(event); + events::Ui().Dispatch(event); + } if (sCurrentTrack && !sIsPaused && !is_in_state()) { ESP_LOGI(kTag, "ready to play!"); @@ -321,6 +355,17 @@ auto AudioState::clearDrainBuffer() -> void { } } +auto AudioState::awaitEmptyDrainBuffer() -> void { + if (is_in_state()) { + for (int i = 0; i < 10 && !xStreamBufferIsEmpty(sDrainBuffer); i++) { + vTaskDelay(pdMS_TO_TICKS(250)); + } + } + if (!xStreamBufferIsEmpty(sDrainBuffer)) { + clearDrainBuffer(); + } +} + auto AudioState::commitVolume() -> void { auto mode = sServices->nvs().OutputMode(); auto vol = sOutput->GetVolume(); diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp index cd61d97f..3fb99159 100644 --- a/src/audio/i2s_audio_output.cpp +++ b/src/audio/i2s_audio_output.cpp @@ -152,9 +152,6 @@ auto I2SAudioOutput::Configure(const Format& fmt) -> void { return; } - ESP_LOGI(kTag, "incoming audio stream: %u ch %u bpp @ %lu Hz", - fmt.num_channels, fmt.bits_per_sample, fmt.sample_rate); - drivers::I2SDac::Channels ch; switch (fmt.num_channels) { case 1: diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp index c00813ac..60afb321 100644 --- a/src/audio/include/audio_fsm.hpp +++ b/src/audio/include/audio_fsm.hpp @@ -67,6 +67,8 @@ class AudioState : public tinyfsm::Fsm { protected: auto clearDrainBuffer() -> void; + auto awaitEmptyDrainBuffer() -> void; + auto playTrack(database::TrackId id) -> void; auto commitVolume() -> void; @@ -83,10 +85,12 @@ class AudioState : public tinyfsm::Fsm { static std::shared_ptr sCurrentTrack; static uint64_t sCurrentSamples; - static std::optional sCurrentFormat; + static std::optional sDrainFormat; + static bool sCurrentTrackIsFromQueue; static std::shared_ptr sNextTrack; static uint64_t sNextTrackCueSamples; + static bool sNextTrackIsFromQueue; static bool sIsResampling; static bool sIsPaused; -- cgit v1.2.3 From bf58cb7acf402420158f3ac2530f62ddc3057914 Mon Sep 17 00:00:00 2001 From: ailurux Date: Wed, 27 Mar 2024 16:11:36 +1100 Subject: Minor fixes --- src/lua/lua_theme.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/lua/lua_theme.cpp b/src/lua/lua_theme.cpp index 2fcd71e5..32d6f660 100644 --- a/src/lua/lua_theme.cpp +++ b/src/lua/lua_theme.cpp @@ -59,7 +59,7 @@ static auto set_theme(lua_State* L) -> int { // Style lv_style_t* style = luavgl_to_style(L, -1); if (style == NULL) { - ESP_LOGI("DANIEL", "Style was null or malformed??"); + ESP_LOGI(kTag, "Style was null or malformed"); return 0; } else { ui::themes::Theme::instance()->AddStyle(class_name, selector, style); -- cgit v1.2.3 From dd3346d381d0db6e179a1127a746deb9b096d5bc Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 28 Mar 2024 12:51:24 +1100 Subject: HACK: auto-accept CFM pairing, and increase the connection timeout This gets Tangara connecting to most non-trivial devices, including car headunits and my laptop. We can add a real UI / better timeout handling for this later (likely via a new BluetoothState?) --- src/drivers/bluetooth.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/drivers/bluetooth.cpp b/src/drivers/bluetooth.cpp index 84c81de0..d15ffd7f 100644 --- a/src/drivers/bluetooth.cpp +++ b/src/drivers/bluetooth.cpp @@ -515,7 +515,7 @@ static void timeoutCallback(TimerHandle_t) { } void Connecting::entry() { - sTimeoutTimer = xTimerCreate("bt_timeout", pdMS_TO_TICKS(5000), false, NULL, + sTimeoutTimer = xTimerCreate("bt_timeout", pdMS_TO_TICKS(15000), false, NULL, timeoutCallback); xTimerStart(sTimeoutTimer, portMAX_DELAY); @@ -568,8 +568,9 @@ void Connecting::react(const events::internal::Gap& ev) { transit(); break; case ESP_BT_GAP_CFM_REQ_EVT: - ESP_LOGW(kTag, "user needs to do cfm. idk man."); - transit(); + // FIXME: Expose a UI for this instead of auto-accepting. + ESP_LOGW(kTag, "CFM request, PIN is: %lu", ev.param->cfm_req.num_val); + esp_bt_gap_ssp_confirm_reply(ev.param->cfm_req.bda, true); break; case ESP_BT_GAP_KEY_NOTIF_EVT: ESP_LOGW(kTag, "the device is telling us a password??"); -- cgit v1.2.3 From 239e6d89507a24c849385f4bfa93ac4ad58e5de5 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 28 Mar 2024 13:30:24 +1100 Subject: bump esp-idf to 5.2.1 --- src/audio/audio_converter.cpp | 1 - src/audio/audio_fsm.cpp | 1 - src/audio/fatfs_audio_input.cpp | 1 - src/audio/include/audio_sink.hpp | 1 - src/audio/readahead_source.cpp | 1 - src/drivers/include/haptics.hpp | 3 ++- src/main/main.cpp | 1 + 7 files changed, 3 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/audio/audio_converter.cpp b/src/audio/audio_converter.cpp index 015be6a3..eb1cde80 100644 --- a/src/audio/audio_converter.cpp +++ b/src/audio/audio_converter.cpp @@ -19,7 +19,6 @@ #include "freertos/portmacro.h" #include "freertos/projdefs.h" #include "i2s_dac.hpp" -#include "idf_additions.h" #include "resample.hpp" #include "sample.hpp" diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 07737872..424b0eff 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -31,7 +31,6 @@ #include "future_fetcher.hpp" #include "i2s_audio_output.hpp" #include "i2s_dac.hpp" -#include "idf_additions.h" #include "nvs.hpp" #include "sample.hpp" #include "service_locator.hpp" diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp index 74c1154b..29d32390 100644 --- a/src/audio/fatfs_audio_input.cpp +++ b/src/audio/fatfs_audio_input.cpp @@ -22,7 +22,6 @@ #include "ff.h" #include "freertos/portmacro.h" #include "freertos/projdefs.h" -#include "idf_additions.h" #include "readahead_source.hpp" #include "span.hpp" diff --git a/src/audio/include/audio_sink.hpp b/src/audio/include/audio_sink.hpp index 85c23f5c..e11f3ce0 100644 --- a/src/audio/include/audio_sink.hpp +++ b/src/audio/include/audio_sink.hpp @@ -11,7 +11,6 @@ #include "esp_heap_caps.h" #include "freertos/FreeRTOS.h" -#include "idf_additions.h" namespace audio { diff --git a/src/audio/readahead_source.cpp b/src/audio/readahead_source.cpp index c7b960d2..fe7ac3bd 100644 --- a/src/audio/readahead_source.cpp +++ b/src/audio/readahead_source.cpp @@ -17,7 +17,6 @@ #include "audio_source.hpp" #include "codec.hpp" #include "freertos/portmacro.h" -#include "idf_additions.h" #include "spi.hpp" #include "tasks.hpp" #include "types.hpp" diff --git a/src/drivers/include/haptics.hpp b/src/drivers/include/haptics.hpp index dfafa2eb..6cfcbb0d 100644 --- a/src/drivers/include/haptics.hpp +++ b/src/drivers/include/haptics.hpp @@ -6,10 +6,11 @@ #pragma once -#include +#include #include #include #include +#include namespace drivers { diff --git a/src/main/main.cpp b/src/main/main.cpp index ddd9cad0..cf27b132 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-only */ +#include "freertos/FreeRTOS.h" #include "freertos/portmacro.h" #include "i2c.hpp" -- cgit v1.2.3 From 35a822fe602cdc9e3a3482df3913ea33af6fc8c2 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 28 Mar 2024 14:46:09 +1100 Subject: Use 48kHz SBC instead of 44.1 --- src/audio/bt_audio_output.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/audio/bt_audio_output.cpp b/src/audio/bt_audio_output.cpp index dff98e36..04daf71f 100644 --- a/src/audio/bt_audio_output.cpp +++ b/src/audio/bt_audio_output.cpp @@ -83,7 +83,7 @@ auto BluetoothAudioOutput::PrepareFormat(const Format& orig) -> Format { // ESP-IDF's current Bluetooth implementation currently handles SBC encoding, // but requires a fixed input format. return Format{ - .sample_rate = 44100, + .sample_rate = 48000, .num_channels = 2, .bits_per_sample = 16, }; -- cgit v1.2.3 From 10441162c4323d6e41f54425a8d7641fda18f711 Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 28 Mar 2024 15:36:16 +1100 Subject: Fix for adding multiple styles with the same key --- src/ui/themes.cpp | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/ui/themes.cpp b/src/ui/themes.cpp index 88f45b1b..b13f226a 100644 --- a/src/ui/themes.cpp +++ b/src/ui/themes.cpp @@ -70,6 +70,7 @@ void Theme::Callback(lv_obj_t* obj) { void Theme::ApplyStyle(lv_obj_t* obj, std::string style_key) { if (auto search = style_map.find(style_key); search != style_map.end()) { for (const auto& pair : search->second) { + lv_obj_remove_style(obj, pair.second, pair.first); lv_obj_add_style(obj, pair.second, pair.first); } } -- cgit v1.2.3 From 78c708e9390047ef24ccd8fbe0cd2d38ff758654 Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 28 Mar 2024 15:36:27 +1100 Subject: Fix log message --- src/lua/lua_theme.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/lua/lua_theme.cpp b/src/lua/lua_theme.cpp index 32d6f660..fe69aa6a 100644 --- a/src/lua/lua_theme.cpp +++ b/src/lua/lua_theme.cpp @@ -59,7 +59,7 @@ static auto set_theme(lua_State* L) -> int { // Style lv_style_t* style = luavgl_to_style(L, -1); if (style == NULL) { - ESP_LOGI(kTag, "Style was null or malformed"); + ESP_LOGI("lua_theme", "Style was null or malformed"); return 0; } else { ui::themes::Theme::instance()->AddStyle(class_name, selector, style); -- cgit v1.2.3 From f1c8866b815a92aeda3133fd27051ce7c873cc57 Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 28 Mar 2024 16:03:37 +1100 Subject: Check type is actually a table --- src/lua/lua_theme.cpp | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/lua/lua_theme.cpp b/src/lua/lua_theme.cpp index fe69aa6a..4eb46499 100644 --- a/src/lua/lua_theme.cpp +++ b/src/lua/lua_theme.cpp @@ -36,6 +36,7 @@ static auto set_style(lua_State* L) -> int { static auto set_theme(lua_State* L) -> int { std::string class_name; + luaL_checktype(L, -1, LUA_TTABLE); lua_pushnil(L); /* first key */ while (lua_next(L, -2) != 0) { /* uses 'key' (at index -2) and 'value' (at index -1) */ -- cgit v1.2.3 From 79b6c3b393a1ff351b437ef59b2a4e472da0c38c Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 28 Mar 2024 16:29:23 +1100 Subject: Use luaL_checkstring in set_style --- src/lua/lua_theme.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/lua/lua_theme.cpp b/src/lua/lua_theme.cpp index 4eb46499..d7f099cc 100644 --- a/src/lua/lua_theme.cpp +++ b/src/lua/lua_theme.cpp @@ -24,12 +24,10 @@ namespace lua { static auto set_style(lua_State* L) -> int { // Get the object and class name from the stack - if (lua_type(L, -1) == LUA_TSTRING) { - std::string class_name = lua_tostring(L, -1); - lv_obj_t* obj = luavgl_to_obj(L, -2); - if (obj != NULL) { - ui::themes::Theme::instance()->ApplyStyle(obj, class_name); - } + std::string class_name = luaL_checkstring(L, -1); + lv_obj_t* obj = luavgl_to_obj(L, -2); + if (obj != NULL) { + ui::themes::Theme::instance()->ApplyStyle(obj, class_name); } return 0; } -- cgit v1.2.3 From 7c5dae84175aa750ca1b8beeb066f5607ca73181 Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 28 Mar 2024 16:34:04 +1100 Subject: Remove unused variable --- src/lua/lua_theme.cpp | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/lua/lua_theme.cpp b/src/lua/lua_theme.cpp index d7f099cc..72434d97 100644 --- a/src/lua/lua_theme.cpp +++ b/src/lua/lua_theme.cpp @@ -47,7 +47,6 @@ static auto set_theme(lua_State* L) -> int { while (lua_next(L, -2) != 0) { // Nesting the second int selector = -1; - lv_style_t* style = NULL; lua_pushnil(L); // First key while (lua_next(L, -2) != 0) { int idx = lua_tointeger(L, -2); -- cgit v1.2.3