summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorailurux <ailuruxx@gmail.com>2024-04-02 11:13:50 +1100
committerailurux <ailuruxx@gmail.com>2024-04-02 11:13:50 +1100
commite20ebe7574db5aedc73f07b7bb3a0a01eae93c84 (patch)
tree34c93ec8a80e282f3ce3e47dd60c41e46de0f8b3 /src
parenta750af35aa6afda40aadca8f7cf8db75f41a43b2 (diff)
parent0d0c4b2307cac8436fea7276956f293262b265ed (diff)
downloadtangara-fw-e20ebe7574db5aedc73f07b7bb3a0a01eae93c84.tar.gz
Merge branch 'main' into lua-volume
Diffstat (limited to 'src')
-rw-r--r--src/app_console/app_console.cpp18
-rw-r--r--src/audio/audio_converter.cpp232
-rw-r--r--src/audio/audio_decoder.cpp76
-rw-r--r--src/audio/audio_fsm.cpp392
-rw-r--r--src/audio/audio_source.cpp14
-rw-r--r--src/audio/bt_audio_output.cpp4
-rw-r--r--src/audio/fatfs_audio_input.cpp9
-rw-r--r--src/audio/i2s_audio_output.cpp5
-rw-r--r--src/audio/include/audio_converter.hpp21
-rw-r--r--src/audio/include/audio_decoder.hpp20
-rw-r--r--src/audio/include/audio_events.hpp106
-rw-r--r--src/audio/include/audio_fsm.hpp58
-rw-r--r--src/audio/include/audio_sink.hpp18
-rw-r--r--src/audio/include/audio_source.hpp11
-rw-r--r--src/audio/include/bt_audio_output.hpp5
-rw-r--r--src/audio/include/fatfs_audio_input.hpp4
-rw-r--r--src/audio/include/i2s_audio_output.hpp5
-rw-r--r--src/audio/include/track_queue.hpp3
-rw-r--r--src/audio/readahead_source.cpp1
-rw-r--r--src/audio/track_queue.cpp45
-rw-r--r--src/battery/battery.cpp5
-rw-r--r--src/codecs/CMakeLists.txt4
-rw-r--r--src/codecs/codec.cpp4
-rw-r--r--src/codecs/dr_flac.cpp115
-rw-r--r--src/codecs/include/codec.hpp4
-rw-r--r--src/codecs/include/dr_flac.hpp (renamed from src/codecs/include/miniflac.hpp)23
-rw-r--r--src/codecs/include/mad.hpp4
-rw-r--r--src/codecs/include/opus.hpp4
-rw-r--r--src/codecs/include/source_buffer.hpp1
-rw-r--r--src/codecs/include/vorbis.hpp4
-rw-r--r--src/codecs/include/wav.hpp4
-rw-r--r--src/codecs/mad.cpp51
-rw-r--r--src/codecs/miniflac.cpp181
-rw-r--r--src/codecs/opus.cpp14
-rw-r--r--src/codecs/source_buffer.cpp5
-rw-r--r--src/codecs/vorbis.cpp14
-rw-r--r--src/codecs/wav.cpp11
-rw-r--r--src/database/database.cpp58
-rw-r--r--src/database/include/database.hpp5
-rw-r--r--src/database/include/records.hpp2
-rw-r--r--src/database/records.cpp29
-rw-r--r--src/drivers/bluetooth.cpp7
-rw-r--r--src/drivers/display.cpp15
-rw-r--r--src/drivers/display_init.cpp2
-rw-r--r--src/drivers/gpios.cpp18
-rw-r--r--src/drivers/include/display_init.hpp3
-rw-r--r--src/drivers/include/gpios.hpp9
-rw-r--r--src/drivers/include/haptics.hpp3
-rw-r--r--src/drivers/include/nvs.hpp8
-rw-r--r--src/drivers/nvs.cpp21
-rw-r--r--src/drivers/samd.cpp31
-rw-r--r--src/lua/CMakeLists.txt5
-rw-r--r--src/lua/bridge.cpp4
-rw-r--r--src/lua/include/lua_screen.hpp15
-rw-r--r--src/lua/include/lua_theme.hpp15
-rw-r--r--src/lua/include/property.hpp2
-rw-r--r--src/lua/lua_screen.cpp79
-rw-r--r--src/lua/lua_theme.cpp89
-rw-r--r--src/lua/property.cpp23
-rw-r--r--src/main/main.cpp1
-rw-r--r--src/system_fsm/booting.cpp17
-rw-r--r--src/system_fsm/idle.cpp9
-rw-r--r--src/system_fsm/include/system_events.hpp1
-rw-r--r--src/system_fsm/include/system_fsm.hpp4
-rw-r--r--src/system_fsm/running.cpp8
-rw-r--r--src/system_fsm/system_fsm.cpp6
-rw-r--r--src/ui/include/screen.hpp5
-rw-r--r--src/ui/include/screen_lua.hpp5
-rw-r--r--src/ui/include/screen_splash.hpp2
-rw-r--r--src/ui/include/themes.hpp27
-rw-r--r--src/ui/include/ui_fsm.hpp7
-rw-r--r--src/ui/modal.cpp2
-rw-r--r--src/ui/screen.cpp5
-rw-r--r--src/ui/screen_lua.cpp60
-rw-r--r--src/ui/themes.cpp189
-rw-r--r--src/ui/ui_fsm.cpp110
76 files changed, 1438 insertions, 958 deletions
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<system_fsm::ServiceLocator> 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 dc2fef95..eb1cde80 100644
--- a/src/audio/audio_converter.cpp
+++ b/src/audio/audio_converter.cpp
@@ -5,18 +5,20 @@
*/
#include "audio_converter.hpp"
+#include <stdint.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
+#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"
-#include "idf_additions.h"
#include "resample.hpp"
#include "sample.hpp"
@@ -25,7 +27,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 {
@@ -35,7 +37,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<sample::Sample*>(heap_caps_calloc(
kSampleBufferLength, sizeof(sample::Sample), MALLOC_CAP_DMA)),
@@ -63,24 +67,32 @@ auto SampleConverter::SetOutput(std::shared_ptr<IAudioOutput> output) -> void {
sink_ = output;
}
-auto SampleConverter::ConvertSamples(cpp::span<sample::Sample> input,
- const IAudioOutput::Format& format,
- bool is_eos) -> void {
+auto SampleConverter::beginStream(std::shared_ptr<TrackInfo> track) -> void {
Args args{
- .format = format,
+ .track = new std::shared_ptr<TrackInfo>(track),
+ .samples_available = 0,
+ .is_end_of_stream = false,
+ };
+ xQueueSend(commands_, &args, portMAX_DELAY);
+}
+
+auto SampleConverter::continueStream(cpp::span<sample::Sample> 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<std::byte> input_as_bytes = {
- reinterpret_cast<std::byte*>(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);
- }
+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 {
@@ -88,75 +100,94 @@ 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;
+
+ if (args.track) {
+ handleBeginStream(*args.track);
+ delete args.track;
+ }
+ if (args.samples_available) {
+ handleContinueStream(args.samples_available);
+ }
+ if (args.is_end_of_stream) {
+ handleEndStream();
}
+ }
+}
- // 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_);
+auto SampleConverter::handleBeginStream(std::shared_ptr<TrackInfo> 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));
}
+
+ sink_->Configure(new_target);
}
+ target_format_ = new_target;
}
+
+ samples_sunk_ = 0;
+ events::Audio().Dispatch(internal::StreamStarted{
+ .track = track,
+ .src_format = source_format_,
+ .dst_format = target_format_,
+ });
}
-auto SampleConverter::HandleSamples(cpp::span<sample::Sample> input,
- bool is_eos) -> size_t {
+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<sample::Sample> input) -> size_t {
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;
@@ -173,7 +204,7 @@ auto SampleConverter::HandleSamples(cpp::span<sample::Sample> 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) {
@@ -186,16 +217,49 @@ auto SampleConverter::HandleSamples(cpp::span<sample::Sample> 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<std::byte*>(output_source.data()) + bytes_sent,
- bytes_to_send - bytes_sent, portMAX_DELAY);
- }
+ sendToSink(output_source);
}
+
return samples_used;
}
+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;
+ }
+ leftover_bytes_ = 0;
+
+ events::Audio().Dispatch(internal::StreamEnded{});
+}
+
+auto SampleConverter::sendToSink(cpp::span<sample::Sample> 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::StreamUpdate{
+ .samples_sunk = samples_sunk_,
+ });
+ samples_sunk_ = 0;
+ }
+
+ xStreamBufferSend(sink_->stream(),
+ reinterpret_cast<std::byte*>(samples.data()),
+ samples.size_bytes(), portMAX_DELAY);
+}
+
} // namespace audio
diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp
index b0a973d9..90c69c16 100644
--- a/src/audio/audio_decoder.cpp
+++ b/src/audio/audio_decoder.cpp
@@ -5,6 +5,7 @@
*/
#include "audio_decoder.hpp"
+#include <stdint.h>
#include <cstdint>
#include <cstdlib>
@@ -50,38 +51,6 @@ namespace audio {
static constexpr std::size_t kCodecBufferLength =
drivers::kI2SBufferLengthFrames * sizeof(sample::Sample);
-Timer::Timer(std::shared_ptr<Track> t,
- const codecs::ICodec::OutputFormat& format)
- : track_(t),
- current_seconds_(0),
- 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<IAudioSource> source,
std::shared_ptr<SampleConverter> sink) -> Decoder* {
Decoder* task = new Decoder(source, sink);
@@ -91,11 +60,7 @@ auto Decoder::Start(std::shared_ptr<IAudioSource> source,
Decoder::Decoder(std::shared_ptr<IAudioSource> source,
std::shared_ptr<SampleConverter> 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<sample::Sample*>(heap_caps_calloc(
@@ -107,7 +72,6 @@ void Decoder::Main() {
for (;;) {
if (source_->HasNewStream() || !stream_) {
std::shared_ptr<TaggedStream> new_stream = source_->NextStream();
- ESP_LOGI(kTag, "decoder has new stream");
if (new_stream && BeginDecoding(new_stream)) {
stream_ = new_stream;
} else {
@@ -116,7 +80,6 @@ void Decoder::Main() {
}
if (ContinueDecoding()) {
- events::Audio().Dispatch(internal::InputFileFinished{});
stream_.reset();
}
}
@@ -127,11 +90,11 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool {
codec_.reset();
codec_.reset(codecs::CreateCodecForType(stream->type()).value_or(nullptr));
if (!codec_) {
- ESP_LOGE(kTag, "no codec found");
+ ESP_LOGE(kTag, "no codec found for stream");
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());
@@ -144,20 +107,21 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool {
.bits_per_sample = 16,
};
- ESP_LOGI(kTag, "stream started ok");
- events::Audio().Dispatch(internal::InputFileOpened{});
+ std::optional<uint32_t> duration;
+ if (open_res->total_samples) {
+ duration = open_res->total_samples.value() / open_res->num_channels /
+ open_res->sample_rate_hz;
+ }
- auto tags = std::make_shared<Track>(Track{
+ converter_->beginStream(std::make_shared<TrackInfo>(TrackInfo{
.tags = stream->tags(),
- .db_info = {},
+ .uri = stream->Filepath(),
+ .duration = duration,
+ .start_offset = stream->Offset(),
.bitrate_kbps = open_res->sample_rate_hz,
.encoding = stream->type(),
- });
- timer_.reset(new Timer(tags, open_res.value()));
-
- PlaybackUpdate ev{.seconds_elapsed = 0, .track = tags};
- events::Audio().Dispatch(ev);
- events::Ui().Dispatch(ev);
+ .format = *current_sink_format_,
+ }));
return true;
}
@@ -165,20 +129,16 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool {
auto Decoder::ContinueDecoding() -> bool {
auto res = codec_->DecodeTo(codec_buffer_);
if (res.has_error()) {
+ 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);
- }
-
- if (timer_) {
- timer_->AddSamples(res->samples_written);
+ converter_->continueStream(codec_buffer_.first(res->samples_written));
}
if (res->is_stream_finished) {
+ converter_->endStream();
codec_.reset();
}
diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp
index 7800802e..a8f1260f 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"
@@ -29,11 +31,11 @@
#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"
#include "system_events.hpp"
+#include "tinyfsm.hpp"
#include "track.hpp"
#include "track_queue.hpp"
#include "wm8523.hpp"
@@ -51,8 +53,184 @@ std::shared_ptr<I2SAudioOutput> AudioState::sI2SOutput;
std::shared_ptr<BluetoothAudioOutput> AudioState::sBtOutput;
std::shared_ptr<IAudioOutput> AudioState::sOutput;
-std::optional<database::TrackId> AudioState::sCurrentTrack;
-bool AudioState::sIsPlaybackAllowed;
+// Two seconds of samples for two channels, at a representative sample rate.
+constexpr size_t kDrainLatencySamples = 48000 * 2 * 2;
+constexpr size_t kDrainBufferSize =
+ sizeof(sample::Sample) * kDrainLatencySamples;
+
+StreamBufferHandle_t AudioState::sDrainBuffer;
+std::optional<IAudioOutput::Format> AudioState::sDrainFormat;
+
+std::shared_ptr<TrackInfo> AudioState::sCurrentTrack;
+uint64_t AudioState::sCurrentSamples;
+bool AudioState::sCurrentTrackIsFromQueue;
+
+std::shared_ptr<TrackInfo> AudioState::sNextTrack;
+uint64_t AudioState::sNextTrackCueSamples;
+bool AudioState::sNextTrackIsFromQueue;
+
+bool AudioState::sIsResampling;
+bool AudioState::sIsPaused = true;
+
+auto AudioState::currentPositionSeconds() -> std::optional<uint32_t> {
+ if (!sCurrentTrack || !sDrainFormat) {
+ return {};
+ }
+ return sCurrentSamples /
+ (sDrainFormat->num_channels * sDrainFormat->sample_rate);
+}
+
+void AudioState::react(const QueueUpdate& ev) {
+ 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;
+ }
+
+ switch (ev.reason) {
+ case QueueUpdate::kExplicitUpdate:
+ 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:
+ if (!ev.current_changed) {
+ cmd.new_track = std::monostate{};
+ } else {
+ sNextTrackIsFromQueue = true;
+ }
+ cmd.transition = SetTrack::Transition::kGapless;
+ break;
+ case QueueUpdate::kDeserialised:
+ default:
+ // The current track is deserialised separately in order to retain seek
+ // position.
+ return;
+ }
+
+ tinyfsm::FsmList<AudioState>::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<std::monostate>(ev.new_track)) {
+ ESP_LOGI(kTag, "playback finished, awaiting drain");
+ sFileSource->SetPath();
+ awaitEmptyDrainBuffer();
+ sCurrentTrack.reset();
+ sDrainFormat.reset();
+ sCurrentSamples = 0;
+ sCurrentTrackIsFromQueue = false;
+ transit<states::Standby>();
+ 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;
+ uint32_t seek_to = ev.seek_to_second.value_or(0);
+ sServices->bg_worker().Dispatch<void>([=]() {
+ std::optional<std::string> path;
+ if (std::holds_alternative<database::TrackId>(new_track)) {
+ auto db = sServices->database().lock();
+ if (db) {
+ path = db->getTrackPath(std::get<database::TrackId>(new_track));
+ }
+ } else if (std::holds_alternative<std::string>(new_track)) {
+ path = std::get<std::string>(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<states::Standby>() && sCurrentTrack) {
+ transit<states::Playback>();
+ } else if (sIsPaused && is_in_state<states::Playback>()) {
+ transit<states::Standby>();
+ }
+}
+
+void AudioState::react(const internal::StreamStarted& ev) {
+ sDrainFormat = ev.dst_format;
+ sIsResampling = ev.src_format != ev.dst_format;
+
+ sNextTrack = ev.track;
+ sNextTrackCueSamples = sCurrentSamples + (kDrainLatencySamples / 2);
+
+ ESP_LOGI(kTag, "new stream %s %u ch @ %lu hz (resample=%i)",
+ ev.track->uri.c_str(), sDrainFormat->num_channels,
+ sDrainFormat->sample_rate, sIsResampling);
+}
+
+void AudioState::react(const internal::StreamEnded&) {
+ ESP_LOGI(kTag, "stream ended");
+
+ if (sCurrentTrackIsFromQueue) {
+ sServices->track_queue().finish();
+ } else {
+ tinyfsm::FsmList<AudioState>::dispatch(SetTrack{
+ .new_track = std::monostate{},
+ .seek_to_second = {},
+ .transition = SetTrack::Transition::kGapless,
+ });
+ }
+}
+
+void AudioState::react(const internal::StreamUpdate& ev) {
+ 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) *
+ (sDrainFormat->num_channels * sDrainFormat->sample_rate);
+ sCurrentTrackIsFromQueue = sNextTrackIsFromQueue;
+
+ sNextTrack.reset();
+ sNextTrackCueSamples = 0;
+ sNextTrackIsFromQueue = false;
+ }
+
+ if (sCurrentTrack) {
+ PlaybackUpdate event{
+ .current_track = sCurrentTrack,
+ .track_position = currentPositionSeconds(),
+ .paused = !is_in_state<states::Playback>(),
+ };
+ events::System().Dispatch(event);
+ events::Ui().Dispatch(event);
+ }
+
+ if (sCurrentTrack && !sIsPaused && !is_in_state<states::Playback>()) {
+ ESP_LOGI(kTag, "ready to play!");
+ transit<states::Playback>();
+ }
+}
void AudioState::react(const system_fsm::BluetoothEvent& ev) {
if (ev.event != drivers::bluetooth::Event::kConnectionStateChanged) {
@@ -146,7 +324,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;
@@ -155,7 +333,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.
@@ -167,15 +345,41 @@ void AudioState::react(const OutputModeChanged& ev) {
}
}
-auto AudioState::playTrack(database::TrackId id) -> void {
- sCurrentTrack = id;
- sServices->bg_worker().Dispatch<void>([=]() {
- auto db = sServices->database().lock();
- if (!db) {
- return;
+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) {
}
- sFileSource->SetPath(db->getTrackPath(id));
- });
+ } 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::awaitEmptyDrainBuffer() -> void {
+ if (is_in_state<states::Playback>()) {
+ for (int i = 0; i < 10 && !xStreamBufferIsEmpty(sDrainBuffer); i++) {
+ vTaskDelay(pdMS_TO_TICKS(250));
+ }
+ }
+ if (!xStreamBufferIsEmpty(sDrainBuffer)) {
+ clearDrainBuffer();
+ }
}
auto AudioState::commitVolume() -> void {
@@ -192,29 +396,8 @@ 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<states::Playback>()) {
- transit<states::Playback>();
- }
- } else {
- if (!is_in_state<states::Standby>()) {
- transit<states::Standby>();
- }
- }
-}
-
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;
@@ -246,7 +429,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 =
@@ -270,35 +453,14 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) {
transit<Standby>();
}
-void Standby::react(const PlayFile& ev) {
- sFileSource->SetPath(ev.filename);
-}
-
-void Playback::react(const PlayFile& ev) {
- sFileSource->SetPath(ev.filename);
-}
-
-void Standby::react(const internal::InputFileOpened& ev) {
- if (readyToPlay()) {
- transit<Playback>();
- }
-}
-
-void Standby::react(const QueueUpdate& ev) {
- auto current_track = sServices->track_queue().current();
- if (!current_track || (sCurrentTrack && (*sCurrentTrack == *current_track))) {
- return;
- }
- 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) {
return;
}
- sServices->bg_worker().Dispatch<void>([]() {
+ sServices->bg_worker().Dispatch<void>([this]() {
auto db = sServices->database().lock();
if (!db) {
return;
@@ -310,6 +472,14 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) {
return;
}
db->put(kQueueKey, queue.serialise());
+
+ if (sCurrentTrack) {
+ cppbor::Array current_track{
+ cppbor::Tstr{sCurrentTrack->uri},
+ cppbor::Uint{currentPositionSeconds().value_or(0)},
+ };
+ db->put(kCurrentFileKey, current_track.toString());
+ }
});
}
@@ -319,82 +489,66 @@ 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<uint8_t*>(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();
+
+ events::Audio().Dispatch(SetTrack{
+ .new_track = filename,
+ .seek_to_second = pos,
+ .transition = SetTrack::Transition::kHardCut,
+ });
+ }
+ }
+
+ 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);
}
});
}
void Playback::entry() {
- ESP_LOGI(kTag, "beginning playback");
- sOutput->SetMode(IAudioOutput::Modes::kOnPlaying);
-
- events::System().Dispatch(PlaybackStarted{});
- events::Ui().Dispatch(PlaybackStarted{});
-}
-
-void Playback::exit() {
- ESP_LOGI(kTag, "finishing playback");
- sOutput->SetMode(IAudioOutput::Modes::kOnPaused);
-
- // Stash the current volume now, in case it changed during playback, since we
- // might be powering off soon.
- commitVolume();
+ ESP_LOGI(kTag, "audio output resumed");
+ sOutput->mode(IAudioOutput::Modes::kOnPlaying);
- events::System().Dispatch(PlaybackStopped{});
- events::Ui().Dispatch(PlaybackStopped{});
-}
+ PlaybackUpdate event{
+ .current_track = sCurrentTrack,
+ .track_position = currentPositionSeconds(),
+ .paused = false,
+ };
-void Playback::react(const system_fsm::HasPhonesChanged& ev) {
- if (!ev.has_headphones) {
- transit<Standby>();
- }
+ events::System().Dispatch(event);
+ events::Ui().Dispatch(event);
}
-void Playback::react(const QueueUpdate& ev) {
- if (!ev.current_changed) {
- return;
- }
- auto current_track = sServices->track_queue().current();
- if (!current_track) {
- sFileSource->SetPath();
- sCurrentTrack.reset();
- transit<Standby>();
- return;
- }
- playTrack(*current_track);
-}
-
-void Playback::react(const PlaybackUpdate& ev) {
- ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed,
- ev.track->duration);
-}
-
-void Playback::react(const internal::InputFileOpened& ev) {}
+void Playback::exit() {
+ ESP_LOGI(kTag, "audio output paused");
+ sOutput->mode(IAudioOutput::Modes::kOnPaused);
-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");
- 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<Standby>();
- }
-}
-
-void Playback::react(const internal::AudioPipelineIdle& ev) {
- transit<Standby>();
+ events::System().Dispatch(event);
+ events::Ui().Dispatch(event);
}
} // namespace states
diff --git a/src/audio/audio_source.cpp b/src/audio/audio_source.cpp
index 44de1d1b..d9e8e04a 100644
--- a/src/audio/audio_source.cpp
+++ b/src/audio/audio_source.cpp
@@ -11,8 +11,10 @@
namespace audio {
TaggedStream::TaggedStream(std::shared_ptr<database::TrackTags> t,
- std::unique_ptr<codecs::IStream> w)
- : codecs::IStream(w->type()), tags_(t), wrapped_(std::move(w)) {}
+ std::unique_ptr<codecs::IStream> w,
+ std::string filepath,
+ uint32_t offset)
+ : codecs::IStream(w->type()), tags_(t), wrapped_(std::move(w)), filepath_(filepath), offset_(offset) {}
auto TaggedStream::tags() -> std::shared_ptr<database::TrackTags> {
return tags_;
@@ -38,6 +40,14 @@ auto TaggedStream::Size() -> std::optional<int64_t> {
return wrapped_->Size();
}
+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/bt_audio_output.cpp b/src/audio/bt_audio_output.cpp
index f431a49b..20ed7bb3 100644
--- a/src/audio/bt_audio_output.cpp
+++ b/src/audio/bt_audio_output.cpp
@@ -36,7 +36,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 {
@@ -99,7 +99,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,
};
diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp
index 7726a94a..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"
@@ -62,9 +61,9 @@ auto FatfsAudioInput::SetPath(std::optional<std::string> path) -> void {
}
}
-auto FatfsAudioInput::SetPath(const std::string& path) -> void {
+auto FatfsAudioInput::SetPath(const std::string& path,uint32_t offset) -> void {
std::lock_guard<std::mutex> guard{new_stream_mutex_};
- if (OpenFile(path)) {
+ if (OpenFile(path, offset)) {
has_new_stream_ = true;
has_new_stream_.notify_one();
}
@@ -103,7 +102,7 @@ auto FatfsAudioInput::NextStream() -> std::shared_ptr<TaggedStream> {
}
}
-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 +135,7 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool {
auto source =
std::make_unique<FatfsSource>(stream_type.value(), std::move(file));
- new_stream_.reset(new TaggedStream(tags, std::move(source)));
+ new_stream_.reset(new TaggedStream(tags, std::move(source), path, offset));
return true;
}
diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp
index 7739fa17..2a251685 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;
}
@@ -166,9 +166,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_converter.hpp b/src/audio/include/audio_converter.hpp
index c2ebde60..232b5d8e 100644
--- a/src/audio/include/audio_converter.hpp
+++ b/src/audio/include/audio_converter.hpp
@@ -6,9 +6,11 @@
#pragma once
+#include <stdint.h>
#include <cstdint>
#include <memory>
+#include "audio_events.hpp"
#include "audio_sink.hpp"
#include "audio_source.hpp"
#include "codec.hpp"
@@ -30,18 +32,23 @@ class SampleConverter {
auto SetOutput(std::shared_ptr<IAudioOutput>) -> void;
- auto ConvertSamples(cpp::span<sample::Sample>,
- const IAudioOutput::Format& format,
- bool is_eos) -> void;
+ auto beginStream(std::shared_ptr<TrackInfo>) -> void;
+ auto continueStream(cpp::span<sample::Sample>) -> void;
+ auto endStream() -> void;
private:
auto Main() -> void;
- auto SetTargetFormat(const IAudioOutput::Format& format) -> void;
- auto HandleSamples(cpp::span<sample::Sample>, bool) -> size_t;
+ auto handleBeginStream(std::shared_ptr<TrackInfo>) -> void;
+ auto handleContinueStream(size_t samples_available) -> void;
+ auto handleEndStream() -> void;
+
+ auto handleSamples(cpp::span<sample::Sample>) -> size_t;
+
+ auto sendToSink(cpp::span<sample::Sample>) -> void;
struct Args {
- IAudioOutput::Format format;
+ std::shared_ptr<TrackInfo>* track;
size_t samples_available;
bool is_end_of_stream;
};
@@ -59,6 +66,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 318e6fd4..89f0f43c 100644
--- a/src/audio/include/audio_decoder.hpp
+++ b/src/audio/include/audio_decoder.hpp
@@ -20,25 +20,6 @@
namespace audio {
/*
- * Sample-based timer for the current elapsed playback time.
- */
-class Timer {
- public:
- Timer(std::shared_ptr<Track>, const codecs::ICodec::OutputFormat& format);
-
- auto AddSamples(std::size_t) -> void;
-
- private:
- std::shared_ptr<Track> 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
* forwards the resulting stream to the given converter.
@@ -65,7 +46,6 @@ class Decoder {
std::shared_ptr<codecs::IStream> stream_;
std::unique_ptr<codecs::ICodec> codec_;
- std::unique_ptr<Timer> timer_;
std::optional<codecs::ICodec::OutputFormat> current_format_;
std::optional<IAudioOutput::Format> current_sink_format_;
diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp
index 03584062..b8a0dba6 100644
--- a/src/audio/include/audio_events.hpp
+++ b/src/audio/include/audio_events.hpp
@@ -9,40 +9,104 @@
#include <stdint.h>
#include <cstdint>
#include <memory>
+#include <optional>
#include <string>
+#include "audio_sink.hpp"
#include "tinyfsm.hpp"
#include "track.hpp"
-#include "track_queue.hpp"
#include "types.hpp"
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<database::TrackTags> tags;
- std::shared_ptr<database::TrackData> 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<uint32_t> duration;
+
+ /* The offset in seconds that this file's decoding started from. */
+ std::optional<uint32_t> start_offset;
+
+ /* The approximate bitrate of this track in its original encoded form. */
+ std::optional<uint32_t> bitrate_kbps;
+
+ /* The encoded format of the this track. */
codecs::StreamType encoding;
-};
-struct PlaybackStarted : tinyfsm::Event {};
+ IAudioOutput::Format format;
+};
+/*
+ * 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> track;
+ /*
+ * The track that is currently being decoded by the audio pipeline. May be
+ * absent if there is no current track.
+ */
+ std::shared_ptr<TrackInfo> current_track;
+
+ /*
+ * How long the current track has been playing for, in seconds. Will always
+ * be present if current_track is present.
+ */
+ std::optional<uint32_t> track_position;
+
+ /* Whether or not the current track is currently being output to a sink. */
+ bool paused;
};
-struct PlaybackStopped : tinyfsm::Event {};
+/*
+ * Sets a new track to be decoded by the audio pipeline, replacing any
+ * currently playing track.
+ */
+struct SetTrack : tinyfsm::Event {
+ std::variant<std::string, database::TrackId, std::monostate> new_track;
+ std::optional<uint32_t> seek_to_second;
+
+ enum Transition {
+ kHardCut,
+ kGapless,
+ // TODO: kCrossFade
+ };
+ Transition transition;
+};
+
+struct TogglePlayPause : tinyfsm::Event {
+ std::optional<bool> set_to;
+};
struct QueueUpdate : tinyfsm::Event {
bool current_changed;
-};
-struct PlayFile : tinyfsm::Event {
- std::string filename;
+ enum Reason {
+ kExplicitUpdate,
+ kRepeatingLastTrack,
+ kTrackFinished,
+ kDeserialised,
+ };
+ Reason reason;
};
struct StepUpVolume : tinyfsm::Event {};
@@ -70,17 +134,21 @@ 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 StreamStarted : tinyfsm::Event {
+ std::shared_ptr<TrackInfo> track;
+ IAudioOutput::Format src_format;
+ IAudioOutput::Format dst_format;
+};
+
+struct StreamUpdate : tinyfsm::Event {
+ uint32_t samples_sunk;
+};
-struct AudioPipelineIdle : tinyfsm::Event {};
+struct StreamEnded : tinyfsm::Event {};
} // namespace internal
diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp
index 29ec489a..60afb321 100644
--- a/src/audio/include/audio_fsm.hpp
+++ b/src/audio/include/audio_fsm.hpp
@@ -6,6 +6,7 @@
#pragma once
+#include <stdint.h>
#include <deque>
#include <memory>
#include <vector>
@@ -41,6 +42,14 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
/* 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::StreamStarted&);
+ void react(const internal::StreamUpdate&);
+ void react(const internal::StreamEnded&);
+
void react(const StepUpVolume&);
void react(const StepDownVolume&);
virtual void react(const system_fsm::HasPhonesChanged&);
@@ -52,21 +61,14 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
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&);
- virtual void react(const PlayFile&) {}
- 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 awaitEmptyDrainBuffer() -> void;
+
auto playTrack(database::TrackId id) -> void;
auto commitVolume() -> void;
@@ -79,10 +81,21 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
static std::shared_ptr<BluetoothAudioOutput> sBtOutput;
static std::shared_ptr<IAudioOutput> sOutput;
- static std::optional<database::TrackId> sCurrentTrack;
+ static StreamBufferHandle_t sDrainBuffer;
- auto readyToPlay() -> bool;
- static bool sIsPlaybackAllowed;
+ static std::shared_ptr<TrackInfo> sCurrentTrack;
+ static uint64_t sCurrentSamples;
+ static std::optional<IAudioOutput::Format> sDrainFormat;
+ static bool sCurrentTrackIsFromQueue;
+
+ static std::shared_ptr<TrackInfo> sNextTrack;
+ static uint64_t sNextTrackCueSamples;
+ static bool sNextTrackIsFromQueue;
+
+ static bool sIsResampling;
+ static bool sIsPaused;
+
+ auto currentPositionSeconds() -> std::optional<uint32_t>;
};
namespace states {
@@ -90,17 +103,13 @@ namespace states {
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;
};
class Standby : public AudioState {
public:
- void react(const PlayFile&) 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;
@@ -112,17 +121,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 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/include/audio_sink.hpp b/src/audio/include/audio_sink.hpp
index 116410f6..f31d0d75 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 {
@@ -27,7 +26,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 +41,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;
@@ -70,6 +77,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/audio_source.hpp b/src/audio/include/audio_source.hpp
index 68145f5b..b38acd7a 100644
--- a/src/audio/include/audio_source.hpp
+++ b/src/audio/include/audio_source.hpp
@@ -16,7 +16,10 @@ namespace audio {
class TaggedStream : public codecs::IStream {
public:
TaggedStream(std::shared_ptr<database::TrackTags>,
- std::unique_ptr<codecs::IStream> wrapped);
+ std::unique_ptr<codecs::IStream> wrapped,
+ std::string path,
+ uint32_t offset = 0
+ );
auto tags() -> std::shared_ptr<database::TrackTags>;
@@ -30,11 +33,17 @@ class TaggedStream : public codecs::IStream {
auto Size() -> std::optional<int64_t> override;
+ auto Offset() -> uint32_t;
+
+ auto Filepath() -> std::string;
+
auto SetPreambleFinished() -> void override;
private:
std::shared_ptr<database::TrackTags> tags_;
std::unique_ptr<codecs::IStream> wrapped_;
+ std::string filepath_;
+ int32_t offset_;
};
class IAudioSource {
diff --git a/src/audio/include/bt_audio_output.hpp b/src/audio/include/bt_audio_output.hpp
index dff25131..74b0301a 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;
@@ -50,6 +48,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/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<std::string>) -> 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<codecs::StreamType>;
diff --git a/src/audio/include/i2s_audio_output.hpp b/src/audio/include/i2s_audio_output.hpp
index 538eafb6..7954257a 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;
@@ -48,6 +46,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<drivers::I2SDac> 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 <shared_mutex>
#include <vector>
+#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/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/audio/track_queue.cpp b/src/audio/track_queue.cpp
index b75230fc..dbe283c4 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);
}
@@ -131,7 +136,7 @@ auto TrackQueue::insert(Item i, size_t index) -> void {
{
const std::shared_lock<std::shared_mutex> 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]() {
@@ -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<database::TrackIterator>(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<std::shared_mutex> 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<void>([=, 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<const uint8_t*>(s.data());
cppbor::parse(data, data + s.size(), &client);
- notifyChanged(true);
+ notifyChanged(true, Reason::kDeserialised);
}
} // namespace audio
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/codecs/CMakeLists.txt b/src/codecs/CMakeLists.txt
index 1ef4bb2d..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 "codec.cpp" "mad.cpp" "miniflac.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 <optional>
#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<ICodec*> {
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..2f9acf8c
--- /dev/null
+++ b/src/codecs/dr_flac.cpp
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "dr_flac.hpp"
+
+#include <cstdint>
+#include <cstdlib>
+
+#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<IStream*>(pUserData);
+ ssize_t res =
+ stream->Read({reinterpret_cast<std::byte*>(pBufferOut), bytesToRead});
+ return res < 0 ? 0 : res;
+}
+
+static drflac_bool32 seekProc(void* pUserData,
+ int offset,
+ drflac_seek_origin origin) {
+ IStream* stream = reinterpret_cast<IStream*>(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<IStream> input, uint32_t offset)
+ -> cpp::result<OutputFormat, Error> {
+ 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<uint8_t>(flac_->channels),
+ .sample_rate_hz = static_cast<uint32_t>(flac_->sampleRate),
+ .total_samples = flac_->totalPCMFrameCount * flac_->channels,
+ };
+ return format;
+}
+
+auto DrFlacDecoder::DecodeTo(cpp::span<sample::Sample> output)
+ -> cpp::result<OutputInfo, Error> {
+ 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<size_t>(frames_written * flac_->channels),
+ .is_stream_finished = frames_written < frames_to_read};
+}
+
+} // namespace codecs
diff --git a/src/codecs/include/codec.hpp b/src/codecs/include/codec.hpp
index 8aa391b6..e48e3c58 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<IStream> input)
+ virtual auto OpenStream(std::shared_ptr<IStream> input,uint32_t offset)
-> cpp::result<OutputFormat, Error> = 0;
struct OutputInfo {
@@ -130,8 +130,6 @@ class ICodec {
*/
virtual auto DecodeTo(cpp::span<sample::Sample> destination)
-> cpp::result<OutputInfo, Error> = 0;
-
- virtual auto SeekTo(size_t target_sample) -> cpp::result<void, Error> = 0;
};
auto CreateCodecForType(StreamType type) -> std::optional<ICodec*>;
diff --git a/src/codecs/include/miniflac.hpp b/src/codecs/include/dr_flac.hpp
index d57b08a3..547876f4 100644
--- a/src/codecs/include/miniflac.hpp
+++ b/src/codecs/include/dr_flac.hpp
@@ -6,7 +6,6 @@
#pragma once
-#include <sys/_stdint.h>
#include <cstddef>
#include <cstdint>
#include <memory>
@@ -14,7 +13,7 @@
#include <string>
#include <utility>
-#include "miniflac.h"
+#include "dr_flac.h"
#include "sample.hpp"
#include "source_buffer.hpp"
#include "span.hpp"
@@ -23,29 +22,23 @@
namespace codecs {
-class MiniFlacDecoder : public ICodec {
+class DrFlacDecoder : public ICodec {
public:
- MiniFlacDecoder();
- ~MiniFlacDecoder();
+ DrFlacDecoder();
+ ~DrFlacDecoder();
- auto OpenStream(std::shared_ptr<IStream> input)
+ auto OpenStream(std::shared_ptr<IStream> input,uint32_t offset)
-> cpp::result<OutputFormat, Error> override;
auto DecodeTo(cpp::span<sample::Sample> destination)
-> cpp::result<OutputInfo, Error> override;
- auto SeekTo(std::size_t target_sample) -> cpp::result<void, Error> override;
-
- MiniFlacDecoder(const MiniFlacDecoder&) = delete;
- MiniFlacDecoder& operator=(const MiniFlacDecoder&) = delete;
+ DrFlacDecoder(const DrFlacDecoder&) = delete;
+ DrFlacDecoder& operator=(const DrFlacDecoder&) = delete;
private:
std::shared_ptr<IStream> input_;
- SourceBuffer buffer_;
-
- std::unique_ptr<miniflac_t> flac_;
- std::array<int32_t*, 2> samples_by_channel_;
- std::optional<size_t> current_sample_;
+ drflac *flac_;
};
} // namespace codecs
diff --git a/src/codecs/include/mad.hpp b/src/codecs/include/mad.hpp
index 813aa86d..ead0b2a2 100644
--- a/src/codecs/include/mad.hpp
+++ b/src/codecs/include/mad.hpp
@@ -26,14 +26,12 @@ class MadMp3Decoder : public ICodec {
MadMp3Decoder();
~MadMp3Decoder();
- auto OpenStream(std::shared_ptr<IStream> input)
+ auto OpenStream(std::shared_ptr<IStream> input,uint32_t offset)
-> cpp::result<OutputFormat, Error> override;
auto DecodeTo(cpp::span<sample::Sample> destination)
-> cpp::result<OutputInfo, Error> override;
- auto SeekTo(std::size_t target_sample) -> cpp::result<void, Error> 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 45b1b07a..de2f7131 100644
--- a/src/codecs/include/opus.hpp
+++ b/src/codecs/include/opus.hpp
@@ -26,14 +26,12 @@ class XiphOpusDecoder : public ICodec {
XiphOpusDecoder();
~XiphOpusDecoder();
- auto OpenStream(std::shared_ptr<IStream> input)
+ auto OpenStream(std::shared_ptr<IStream> input,uint32_t offset)
-> cpp::result<OutputFormat, Error> override;
auto DecodeTo(cpp::span<sample::Sample> destination)
-> cpp::result<OutputInfo, Error> override;
- auto SeekTo(std::size_t target_sample) -> cpp::result<void, Error> override;
-
XiphOpusDecoder(const XiphOpusDecoder&) = delete;
XiphOpusDecoder& operator=(const XiphOpusDecoder&) = delete;
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<size_t(cpp::span<std::byte>)> writer) -> void;
auto ConsumeBytes(std::function<size_t(cpp::span<std::byte>)> reader) -> void;
+ auto Empty() -> void;
SourceBuffer(const SourceBuffer&) = delete;
SourceBuffer& operator=(const SourceBuffer&) = delete;
diff --git a/src/codecs/include/vorbis.hpp b/src/codecs/include/vorbis.hpp
index b96a0407..3cf0f9ce 100644
--- a/src/codecs/include/vorbis.hpp
+++ b/src/codecs/include/vorbis.hpp
@@ -26,14 +26,12 @@ class TremorVorbisDecoder : public ICodec {
TremorVorbisDecoder();
~TremorVorbisDecoder();
- auto OpenStream(std::shared_ptr<IStream> input)
+ auto OpenStream(std::shared_ptr<IStream> input,uint32_t offset)
-> cpp::result<OutputFormat, Error> override;
auto DecodeTo(cpp::span<sample::Sample> destination)
-> cpp::result<OutputInfo, Error> override;
- auto SeekTo(std::size_t target_sample) -> cpp::result<void, Error> 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 896976dd..40138968 100644
--- a/src/codecs/include/wav.hpp
+++ b/src/codecs/include/wav.hpp
@@ -31,14 +31,12 @@ class WavDecoder : public ICodec {
WavDecoder();
~WavDecoder();
- auto OpenStream(std::shared_ptr<IStream> input)
+ auto OpenStream(std::shared_ptr<IStream> input,uint32_t offset)
-> cpp::result<OutputFormat, Error> override;
auto DecodeTo(cpp::span<sample::Sample> destination)
-> cpp::result<OutputInfo, Error> override;
- auto SeekTo(std::size_t target_sample) -> cpp::result<void, Error> override;
-
WavDecoder(const WavDecoder&) = delete;
WavDecoder& operator=(const WavDecoder&) = delete;
diff --git a/src/codecs/mad.cpp b/src/codecs/mad.cpp
index f36636a1..e44e9922 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<IStream> input)
+auto MadMp3Decoder::OpenStream(std::shared_ptr<IStream> input, uint32_t offset)
-> cpp::result<OutputFormat, ICodec::Error> {
input_ = input;
@@ -113,6 +114,45 @@ auto MadMp3Decoder::OpenStream(std::shared_ptr<IStream> input)
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<std::byte> buf) -> size_t {
+ mad_stream_buffer(stream_.get(),
+ reinterpret_cast<const unsigned char*>(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;
}
@@ -190,11 +230,6 @@ auto MadMp3Decoder::DecodeTo(cpp::span<sample::Sample> output)
.is_stream_finished = is_eos_};
}
-auto MadMp3Decoder::SeekTo(std::size_t target_sample)
- -> cpp::result<void, Error> {
- return {};
-}
-
auto MadMp3Decoder::SkipID3Tags(IStream& stream) -> void {
// First check that the file actually does start with ID3 tags.
std::array<std::byte, 3> magic_buf{};
@@ -222,8 +257,8 @@ auto MadMp3Decoder::SkipID3Tags(IStream& stream) -> void {
}
/*
- * Implementation taken from SDL_mixer and modified. Original is zlib-licensed,
- * copyright (C) 1997-2022 Sam Lantinga <slouken@libsdl.org>
+ * Implementation taken from SDL_mixer and modified. Original is
+ * zlib-licensed, copyright (C) 1997-2022 Sam Lantinga <slouken@libsdl.org>
*/
auto MadMp3Decoder::GetVbrLength(const mad_header& header)
-> std::optional<uint32_t> {
diff --git a/src/codecs/miniflac.cpp b/src/codecs/miniflac.cpp
deleted file mode 100644
index 74eafb3b..00000000
--- a/src/codecs/miniflac.cpp
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#include "miniflac.hpp"
-
-#include <cstdint>
-#include <cstdlib>
-
-#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<miniflac_t*>(
- 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<int32_t*>(
- 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<IStream> input)
- -> cpp::result<OutputFormat, Error> {
- 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<std::byte> buf) -> size_t {
- uint32_t bytes_used = 0;
- res = miniflac_streaminfo_sample_rate(
- flac_.get(), reinterpret_cast<const uint8_t*>(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<std::byte> buf) -> size_t {
- uint32_t bytes_used = 0;
- res = miniflac_streaminfo_channels(
- flac_.get(), reinterpret_cast<const uint8_t*>(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<std::byte> buf) -> size_t {
- uint32_t bytes_used = 0;
- res = miniflac_streaminfo_total_samples(
- flac_.get(), reinterpret_cast<const uint8_t*>(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);
- }
-
- OutputFormat format{
- .num_channels = static_cast<uint8_t>(channels),
- .sample_rate_hz = static_cast<uint32_t>(sample_rate),
- .total_samples = total_samples * channels,
- };
-
- return format;
-}
-
-auto MiniFlacDecoder::DecodeTo(cpp::span<sample::Sample> output)
- -> cpp::result<OutputInfo, Error> {
- 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<std::byte> 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<const uint8_t*>(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<void, Error> {
- return {};
-}
-
-} // namespace codecs
diff --git a/src/codecs/opus.cpp b/src/codecs/opus.cpp
index e4917a33..a5220c4b 100644
--- a/src/codecs/opus.cpp
+++ b/src/codecs/opus.cpp
@@ -78,7 +78,8 @@ XiphOpusDecoder::~XiphOpusDecoder() {
}
}
-auto XiphOpusDecoder::OpenStream(std::shared_ptr<IStream> input)
+auto XiphOpusDecoder::OpenStream(std::shared_ptr<IStream> input,
+ uint32_t offset)
-> cpp::result<OutputFormat, Error> {
input_ = input;
@@ -128,6 +129,10 @@ auto XiphOpusDecoder::OpenStream(std::shared_ptr<IStream> input)
length = l * 2;
}
+ if (offset && op_pcm_seek(opus_, offset * 48000) != 0) {
+ return cpp::fail(Error::kInternalError);
+ }
+
return OutputFormat{
.num_channels = 2,
.sample_rate_hz = 48000,
@@ -151,11 +156,4 @@ auto XiphOpusDecoder::DecodeTo(cpp::span<sample::Sample> output)
};
}
-auto XiphOpusDecoder::SeekTo(size_t target) -> cpp::result<void, Error> {
- if (op_pcm_seek(opus_, target) != 0) {
- return cpp::fail(Error::kInternalError);
- }
- 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
diff --git a/src/codecs/vorbis.cpp b/src/codecs/vorbis.cpp
index 7fb53f1b..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<IStream> input)
+auto TremorVorbisDecoder::OpenStream(std::shared_ptr<IStream> input,
+ uint32_t offset)
-> cpp::result<OutputFormat, Error> {
int res = ov_open_callbacks(input.get(), vorbis_.get(), NULL, 0, kCallbacks);
if (res < 0) {
@@ -117,6 +118,10 @@ auto TremorVorbisDecoder::OpenStream(std::shared_ptr<IStream> input)
length = l * info->channels;
}
+ if (offset && ov_time_seek(vorbis_.get(), offset * 1000) != 0) {
+ return cpp::fail(Error::kInternalError);
+ }
+
return OutputFormat{
.num_channels = static_cast<uint8_t>(info->channels),
.sample_rate_hz = static_cast<uint32_t>(info->rate),
@@ -145,11 +150,4 @@ auto TremorVorbisDecoder::DecodeTo(cpp::span<sample::Sample> output)
};
}
-auto TremorVorbisDecoder::SeekTo(size_t target) -> cpp::result<void, Error> {
- 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 a67f3ff4..143a7a4b 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<IStream> input)
+auto WavDecoder::OpenStream(std::shared_ptr<IStream> input,uint32_t offset)
-> cpp::result<OutputFormat, Error> {
input_ = input;
@@ -199,8 +199,10 @@ auto WavDecoder::OpenStream(std::shared_ptr<IStream> input)
return cpp::fail(Error::kUnsupportedFormat);
}
+ int64_t data_offset = offset * samples_per_second * bytes_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,
@@ -241,14 +243,11 @@ auto WavDecoder::DecodeTo(cpp::span<sample::Sample> output)
return samples_written * bytes_per_sample_;
});
+
return OutputInfo{.samples_written = samples_written,
.is_stream_finished = samples_written == 0 && is_eof};
}
-auto WavDecoder::SeekTo(size_t target) -> cpp::result<void, Error> {
- return {};
-}
-
auto codecs::WavDecoder::GetFormat() const -> uint16_t {
if (wave_format_ == kWaveFormatExtensible) {
return subformat_;
diff --git a/src/database/database.cpp b/src/database/database.cpp
index ec11455b..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<bool> sIsDbOpen(false);
@@ -229,13 +228,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> {
std::string val;
auto res = db_->Get(leveldb::ReadOptions{}, kKeyCustom + key, &val);
- if (!res.ok()) {
+ if (!res.ok() || val.empty()) {
return {};
}
return val;
@@ -298,10 +301,6 @@ auto Database::updateIndexes() -> void {
leveldb::ReadOptions read_options;
read_options.fill_cache = false;
- std::pair<uint16_t, uint16_t> 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");
{
@@ -356,6 +355,7 @@ auto Database::updateIndexes() -> void {
dbRemoveIndexes(track);
track->is_tombstoned = true;
dbPutTrackData(*track);
+ db_->Delete(leveldb::WriteOptions{}, EncodePathKey(track->filepath));
continue;
}
@@ -382,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<uint16_t, uint16_t> newest_track = last_update;
file_gatherer_.FindFiles("", [&](std::string_view path, const FILINFO& info) {
num_processed++;
events::Ui().Dispatch(event::UpdateProgress{
@@ -390,11 +389,11 @@ auto Database::updateIndexes() -> void {
.val = num_processed,
});
- std::pair<uint16_t, uint16_t> 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<TrackTags> tags = tag_parser_.ReadAndParseTags(path);
if (!tags || tags->encoding() == Container::kUnsupported) {
@@ -411,6 +410,7 @@ auto Database::updateIndexes() -> void {
existing_hash = ParseHashValue(raw_entry);
}
+ std::pair<uint16_t, uint16_t> 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.
@@ -428,6 +428,8 @@ auto Database::updateIndexes() -> void {
dbPutHash(hash, id);
auto t = std::make_shared<Track>(data, tags);
dbCreateIndexesForTrack(*t);
+ db_->Put(leveldb::WriteOptions{}, EncodePathKey(path),
+ TrackIdToBytes(id));
return;
}
@@ -443,6 +445,8 @@ auto Database::updateIndexes() -> void {
dbPutTrackData(*new_data);
auto t = std::make_shared<Track>(new_data, tags);
dbCreateIndexesForTrack(*t);
+ db_->Put(leveldb::WriteOptions{}, EncodePathKey(path),
+ TrackIdToBytes(new_data->id));
return;
}
@@ -453,6 +457,8 @@ auto Database::updateIndexes() -> void {
dbPutTrackData(*existing_data);
auto t = std::make_shared<Track>(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",
@@ -461,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<uint16_t, uint16_t> {
- std::string raw;
- if (!db_->Get(leveldb::ReadOptions{}, kKeyLastUpdate, &raw).ok()) {
- return {0, 0};
- }
- auto [res, unused, err] = cppbor::parseWithViews(
- reinterpret_cast<const uint8_t*>(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<uint16_t, uint16_t> 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<uint16_t, uint16_t>;
- auto dbSetLastUpdate(std::pair<uint16_t, uint16_t>) -> 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<TrackData> {
/* '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<TrackId> {
@@ -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},
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<Idle>();
break;
case ESP_BT_GAP_CFM_REQ_EVT:
- ESP_LOGW(kTag, "user needs to do cfm. idk man.");
- transit<Idle>();
+ // 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??");
diff --git a/src/drivers/display.cpp b/src/drivers/display.cpp
index cb3ee3a0..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 + 2;
-static const uint8_t kDisplayWidth = 160 + 1;
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/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<uint8_t, uint8_t> 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<int>(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/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 <stdint.h>
#include <cstdint>
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/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<uint16_t> ports_;
std::atomic<uint16_t> inputs_;
+ const bool invert_lock_switch_;
};
} // namespace drivers
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 <stdint.h>
+#include <cstdint>
#include <initializer_list>
#include <mutex>
#include <optional>
+#include <string>
namespace drivers {
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<uint16_t>, std::optional<uint16_t>>;
+ auto DisplaySize(std::pair<std::optional<uint16_t>, std::optional<uint16_t>>)
+ -> void;
+
auto PreferredBluetoothDevice() -> std::optional<bluetooth::MacAndName>;
auto PreferredBluetoothDevice(std::optional<bluetooth::MacAndName>) -> void;
@@ -120,6 +125,9 @@ class NvsStorage {
nvs_handle_t handle_;
Setting<uint8_t> lock_polarity_;
+ Setting<uint16_t> display_cols_;
+ Setting<uint16_t> display_rows_;
+
Setting<uint8_t> brightness_;
Setting<uint8_t> sensitivity_;
Setting<uint16_t> 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<std::string> {
@@ -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<std::mutex> 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<std::mutex> 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<uint16_t>, std::optional<uint16_t>> {
+ std::lock_guard<std::mutex> lock{mutex_};
+ return std::make_pair(display_cols_.get(), display_rows_.get());
+}
+
+auto NvsStorage::DisplaySize(
+ std::pair<std::optional<uint16_t>, std::optional<uint16_t>> size) -> void {
+ std::lock_guard<std::mutex> lock{mutex_};
+ display_cols_.set(std::move(size.first));
+ display_rows_.set(std::move(size.second));
+}
+
auto NvsStorage::PreferredBluetoothDevice()
-> std::optional<bluetooth::MacAndName> {
std::lock_guard<std::mutex> lock{mutex_};
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;
}
}
diff --git a/src/lua/CMakeLists.txt b/src/lua/CMakeLists.txt
index ff0831c9..0240a50c 100644
--- a/src/lua/CMakeLists.txt
+++ b/src/lua/CMakeLists.txt
@@ -3,8 +3,9 @@
# 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"
+ "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..cfa9d5f7 100644
--- a/src/lua/bridge.cpp
+++ b/src/lua/bridge.cpp
@@ -19,7 +19,9 @@
#include "lua_controls.hpp"
#include "lua_database.hpp"
#include "lua_queue.hpp"
+#include "lua_screen.hpp"
#include "lua_version.hpp"
+#include "lua_theme.hpp"
#include "lvgl.h"
#include "font/lv_font_loader.h"
@@ -84,6 +86,8 @@ auto Bridge::installBaseModules(lua_State* L) -> void {
RegisterDatabaseModule(L);
RegisterQueueModule(L);
RegisterVersionModule(L);
+ RegisterThemeModule(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 <me@jacqueline.id.au>
+ *
+ * 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/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 <ailuruxx@gmail.com>
+ *
+ * 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/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<std::monostate,
int,
bool,
std::string,
- audio::Track,
+ audio::TrackInfo,
drivers::bluetooth::Device,
std::vector<drivers::bluetooth::Device>>;
diff --git a/src/lua/lua_screen.cpp b/src/lua/lua_screen.cpp
new file mode 100644
index 00000000..f17f6b1a
--- /dev/null
+++ b/src/lua/lua_screen.cpp
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "lua_screen.hpp"
+
+#include <memory>
+#include <string>
+
+#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 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);
+
+ 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/lua/lua_theme.cpp b/src/lua/lua_theme.cpp
new file mode 100644
index 00000000..72434d97
--- /dev/null
+++ b/src/lua/lua_theme.cpp
@@ -0,0 +1,89 @@
+
+/*
+ * Copyright 2023 ailurux <ailuruxx@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "lua_version.hpp"
+
+#include <string>
+
+#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_style(lua_State* L) -> int {
+ // Get the object and class name from the stack
+ 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;
+}
+
+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) */
+ 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;
+ 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("lua_theme", "Style was null or malformed");
+ return 0;
+ } else {
+ ui::themes::Theme::instance()->AddStyle(class_name, selector, style);
+ }
+ }
+ 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}, {"set_style", set_style}, {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/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<T, std::string>) {
lua_pushstring(&s, arg.c_str());
- } else if constexpr (std::is_same_v<T, audio::Track>) {
+ } else if constexpr (std::is_same_v<T, audio::TrackInfo>) {
pushTrack(&s, arg);
} else if constexpr (std::is_same_v<T, drivers::bluetooth::Device>) {
pushDevice(&s, arg);
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"
diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp
index 41f46df2..bd394428 100644
--- a/src/system_fsm/booting.cpp
+++ b/src/system_fsm/booting.cpp
@@ -57,13 +57,22 @@ 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>(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>(drivers::Gpios::Create()));
+ sServices->gpios(std::unique_ptr<drivers::Gpios>(
+ drivers::Gpios::Create(sServices->nvs().LockPolarity())));
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 +83,6 @@ auto Booting::entry() -> void {
ESP_LOGI(kTag, "installing remaining drivers");
drivers::spiffs_mount();
sServices->samd(std::unique_ptr<drivers::Samd>(drivers::Samd::Create()));
- sServices->nvs(
- std::unique_ptr<drivers::NvsStorage>(drivers::NvsStorage::OpenSync()));
sServices->touchwheel(
std::unique_ptr<drivers::TouchWheel>{drivers::TouchWheel::Create()});
sServices->haptics(std::make_unique<drivers::Haptics>());
@@ -100,8 +107,6 @@ auto Booting::entry() -> void {
sServices->bluetooth().Enable();
}
- sServices->nvs().LockPolarity(true);
-
BootComplete ev{.services = sServices};
events::Audio().Dispatch(ev);
events::Ui().Dispatch(ev);
diff --git a/src/system_fsm/idle.cpp b/src/system_fsm/idle.cpp
index b6bb2572..e28864b3 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,12 @@ void Idle::react(const internal::IdleTimeout& ev) {
transit<Running>();
return;
}
+ 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;
+ }
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/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/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<SystemState> {
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 d1d02fab..a6ab5d47 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() {
@@ -52,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/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");
diff --git a/src/ui/include/screen.hpp b/src/ui/include/screen.hpp
index 60939660..40284fda 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_; }
@@ -40,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 ee9f6813..41d97a1e 100644
--- a/src/ui/include/screen_lua.hpp
+++ b/src/ui/include/screen_lua.hpp
@@ -18,6 +18,11 @@ class Lua : public Screen {
Lua();
~Lua();
+ 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/include/themes.hpp b/src/ui/include/themes.hpp
index 11680c0d..09b9cdce 100644
--- a/src/ui/include/themes.hpp
+++ b/src/ui/include/themes.hpp
@@ -1,5 +1,8 @@
#pragma once
+#include <string>
+#include <map>
+#include <vector>
#include "lvgl.h"
namespace ui {
@@ -19,31 +22,17 @@ 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);
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<std::string, std::vector<std::pair<int, lv_style_t*>>> style_map;
lv_theme_t theme_;
+
};
} // namespace themes
} // namespace ui
diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp
index 6cf2ba4c..5e1cc487 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<UiState> {
public:
- static auto InitBootSplash(drivers::IGpios&) -> bool;
+ static auto InitBootSplash(drivers::IGpios&, drivers::NvsStorage&) -> bool;
virtual ~UiState() {}
@@ -57,8 +57,6 @@ class UiState : public tinyfsm::Fsm<UiState> {
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&);
@@ -129,8 +127,11 @@ class UiState : public tinyfsm::Fsm<UiState> {
static lua::Property sControlsScheme;
static lua::Property sScrollSensitivity;
+ static lua::Property sLockSwitch;
static lua::Property sDatabaseUpdating;
+
+ static lua::Property sUsbMassStorageEnabled;
};
namespace states {
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.cpp b/src/ui/screen.cpp
index 3e4f8e42..a39aaf7e 100644
--- a/src/ui/screen.cpp
+++ b/src/ui/screen.cpp
@@ -33,7 +33,10 @@ 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);
+
+ 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.
diff --git a/src/ui/screen_lua.cpp b/src/ui/screen_lua.cpp
index 5130b4f7..d43c7ee7 100644
--- a/src/ui/screen_lua.cpp
+++ b/src/ui/screen_lua.cpp
@@ -7,14 +7,19 @@
#include "screen_lua.hpp"
#include "core/lv_obj_tree.h"
+#include "lua.h"
#include "lua.hpp"
+#include "themes.hpp"
+#include "lua_thread.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_) {
@@ -22,6 +27,59 @@ 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::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/themes.cpp b/src/ui/themes.cpp
index f8390570..b13f226a 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,91 +33,46 @@ 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);
+ // 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);
+ }
+ }
+ // Determine class name
+ std::string class_name;
if (lv_obj_check_type(obj, &lv_btn_class)) {
- lv_obj_add_style(obj, &button_style_, LV_PART_MAIN);
+ 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)) {
- lv_obj_add_style(obj, &bar_style_, LV_PART_MAIN);
+ class_name = "bar";
} 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);
+ class_name = "slider";
} 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);
+ class_name = "switch";
} else if (lv_obj_check_type(obj, &lv_dropdown_class)) {
- lv_obj_add_style(obj, &dropdown_style_, LV_PART_MAIN);
+ class_name = "dropdown";
} else if (lv_obj_check_type(obj, &lv_dropdownlist_class)) {
- lv_obj_add_style(obj, &dropdown_list_style_, LV_PART_MAIN);
+ class_name = "dropdownlist";
}
-}
-
-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);
+ // 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);
+ }
+ }
- 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_remove_style(obj, pair.second, pair.first);
+ lv_obj_add_style(obj, pair.second, pair.first);
}
- break;
- default:
- break;
}
}
@@ -204,5 +81,15 @@ auto Theme::instance() -> Theme* {
return &sTheme;
}
+void Theme::AddStyle(std::string key, int selector, lv_style_t* style) {
+ style_map.try_emplace(key, std::vector<std::pair<int, lv_style_t*>>{});
+ 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
diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp
index f5288882..835da19e 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"
@@ -113,19 +114,34 @@ lua::Property UiState::sBluetoothDevices{
lua::Property UiState::sPlaybackPlaying{
false, [](const lua::LuaValue& val) {
- bool current_val = std::get<bool>(sPlaybackPlaying.Get());
if (!std::holds_alternative<bool>(val)) {
return false;
}
bool new_val = std::get<bool>(val);
- if (current_val != new_val) {
- events::Audio().Dispatch(audio::TogglePlayPause{});
- }
+ events::Audio().Dispatch(audio::TogglePlayPause{.set_to = new_val});
return true;
}};
lua::Property UiState::sPlaybackTrack{};
-lua::Property UiState::sPlaybackPosition{0};
+lua::Property UiState::sPlaybackPosition{
+ 0, [](const lua::LuaValue& val) {
+ int current_val = std::get<int>(sPlaybackPosition.Get());
+ if (!std::holds_alternative<int>(val)) {
+ return false;
+ }
+ int new_val = std::get<int>(val);
+ if (current_val != new_val) {
+ auto track = sPlaybackTrack.Get();
+ if (!std::holds_alternative<audio::TrackInfo>(track)) {
+ return false;
+ }
+ events::Audio().Dispatch(audio::SetTrack{
+ .new_track = std::get<audio::TrackInfo>(track).uri,
+ .seek_to_second = (uint32_t)new_val,
+ });
+ }
+ return true;
+ }};
lua::Property UiState::sQueuePosition{0};
lua::Property UiState::sQueueSize{0};
@@ -263,12 +279,36 @@ lua::Property UiState::sScrollSensitivity{
return true;
}};
+lua::Property UiState::sLockSwitch{false};
+
lua::Property UiState::sDatabaseUpdating{false};
-auto UiState::InitBootSplash(drivers::IGpios& gpios) -> bool {
+lua::Property UiState::sUsbMassStorageEnabled{
+ false, [](const lua::LuaValue& val) {
+ if (!std::holds_alternative<bool>(val)) {
+ return false;
+ }
+ bool enable = std::get<bool>(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.
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;
}
@@ -280,27 +320,36 @@ auto UiState::InitBootSplash(drivers::IGpios& gpios) -> bool {
}
void UiState::PushScreen(std::shared_ptr<Screen> 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();
}
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&) {
@@ -342,17 +391,14 @@ 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<int>(ev.seconds_elapsed));
-}
-
-void UiState::react(const audio::PlaybackStopped&) {
- sPlaybackPlaying.Update(false);
+ if (ev.current_track) {
+ sPlaybackTrack.Update(*ev.current_track);
+ } else {
+ sPlaybackTrack.Update(std::monostate{});
+ }
+ sPlaybackPlaying.Update(!ev.paused);
+ sPlaybackPosition.Update(static_cast<int>(ev.track_position.value_or(0)));
}
void UiState::react(const audio::VolumeChanged& ev) {
@@ -443,6 +489,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();
@@ -491,6 +538,7 @@ void Lua::entry() {
{
{"scheme", &sControlsScheme},
{"scroll_sensitivity", &sScrollSensitivity},
+ {"lock_switch", &sLockSwitch},
});
registry.AddPropertyModule(
@@ -512,6 +560,10 @@ void Lua::entry() {
registry.AddPropertyModule("database", {
{"updating", &sDatabaseUpdating},
});
+ registry.AddPropertyModule("usb",
+ {
+ {"msc_enabled", &sUsbMassStorageEnabled},
+ });
auto bt = sServices->bluetooth();
sBluetoothEnabled.Update(bt.IsEnabled());
@@ -525,7 +577,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
@@ -540,10 +592,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++
@@ -564,6 +621,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());
@@ -571,7 +631,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;
}