summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
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.cpp346
-rw-r--r--src/audio/bt_audio_output.cpp2
-rw-r--r--src/audio/fatfs_audio_input.cpp1
-rw-r--r--src/audio/i2s_audio_output.cpp3
-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.hpp105
-rw-r--r--src/audio/include/audio_fsm.hpp54
-rw-r--r--src/audio/include/audio_sink.hpp1
-rw-r--r--src/audio/readahead_source.cpp1
-rw-r--r--src/audio/track_queue.cpp2
-rw-r--r--src/database/database.cpp50
-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/include/haptics.hpp3
-rw-r--r--src/lua/include/property.hpp2
-rw-r--r--src/lua/lua_screen.cpp14
-rw-r--r--src/lua/property.cpp23
-rw-r--r--src/main/main.cpp1
-rw-r--r--src/system_fsm/booting.cpp2
-rw-r--r--src/system_fsm/idle.cpp9
-rw-r--r--src/system_fsm/include/system_fsm.hpp4
-rw-r--r--src/system_fsm/running.cpp8
-rw-r--r--src/ui/include/screen.hpp2
-rw-r--r--src/ui/include/screen_lua.hpp2
-rw-r--r--src/ui/include/screen_splash.hpp2
-rw-r--r--src/ui/include/ui_fsm.hpp4
-rw-r--r--src/ui/screen_lua.cpp19
-rw-r--r--src/ui/ui_fsm.cpp49
34 files changed, 644 insertions, 475 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 946a0b63..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, pdMS_TO_TICKS(100));
- }
+auto SampleConverter::endStream() -> void {
+ Args args{
+ .track = nullptr,
+ .samples_available = 0,
+ .is_end_of_stream = true,
+ };
+ xQueueSend(commands_, &args, portMAX_DELAY);
}
auto SampleConverter::Main() -> void {
@@ -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 68a8a86b..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,39 +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,
- uint32_t current_seconds)
- : track_(t),
- current_seconds_(current_seconds),
- current_sample_in_second_(0),
- samples_per_second_(format.sample_rate_hz * format.num_channels),
- total_duration_seconds_(format.total_samples.value_or(0) /
- format.num_channels / format.sample_rate_hz) {
- track_->duration = total_duration_seconds_;
-}
-
-auto Timer::AddSamples(std::size_t samples) -> void {
- bool incremented = false;
- current_sample_in_second_ += samples;
- while (current_sample_in_second_ >= samples_per_second_) {
- current_seconds_++;
- current_sample_in_second_ -= samples_per_second_;
- incremented = true;
- }
-
- if (incremented) {
- if (total_duration_seconds_ < current_seconds_) {
- total_duration_seconds_ = current_seconds_;
- track_->duration = total_duration_seconds_;
- }
-
- PlaybackUpdate ev{.seconds_elapsed = current_seconds_, .track = track_};
- events::Audio().Dispatch(ev);
- events::Ui().Dispatch(ev);
- }
-}
-
auto Decoder::Start(std::shared_ptr<IAudioSource> source,
std::shared_ptr<SampleConverter> sink) -> Decoder* {
Decoder* task = new Decoder(source, sink);
@@ -92,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(
@@ -108,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 {
@@ -117,7 +80,6 @@ void Decoder::Main() {
}
if (ContinueDecoding()) {
- events::Audio().Dispatch(internal::InputFileFinished{});
stream_.reset();
}
}
@@ -128,7 +90,7 @@ 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;
}
@@ -145,21 +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(),
- .filepath = stream->Filepath(),
- });
- timer_.reset(new Timer(tags, open_res.value(), stream->Offset()));
-
- PlaybackUpdate ev{.seconds_elapsed = stream->Offset(), .track = tags};
- events::Audio().Dispatch(ev);
- events::Ui().Dispatch(ev);
+ .format = *current_sink_format_,
+ }));
return true;
}
@@ -167,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 05c7c216..424b0eff 100644
--- a/src/audio/audio_fsm.cpp
+++ b/src/audio/audio_fsm.cpp
@@ -31,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"
@@ -54,13 +54,183 @@ std::shared_ptr<BluetoothAudioOutput> AudioState::sBtOutput;
std::shared_ptr<IAudioOutput> AudioState::sOutput;
// Two seconds of samples for two channels, at a representative sample rate.
-constexpr size_t kDrainBufferSize = sizeof(sample::Sample) * 48000 * 4;
+constexpr size_t kDrainLatencySamples = 48000 * 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);
+}
-std::optional<database::TrackId> AudioState::sCurrentTrack;
-bool AudioState::sIsPlaybackAllowed;
+void AudioState::react(const internal::StreamEnded&) {
+ ESP_LOGI(kTag, "stream ended");
-static std::optional<std::pair<std::string, uint32_t>> sLastTrackUpdate;
+ 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) {
@@ -184,15 +354,15 @@ auto AudioState::clearDrainBuffer() -> void {
}
}
-auto AudioState::playTrack(database::TrackId id) -> void {
- sCurrentTrack = id;
- sServices->bg_worker().Dispatch<void>([=]() {
- auto db = sServices->database().lock();
- if (!db) {
- return;
+auto AudioState::awaitEmptyDrainBuffer() -> void {
+ if (is_in_state<states::Playback>()) {
+ for (int i = 0; i < 10 && !xStreamBufferIsEmpty(sDrainBuffer); i++) {
+ vTaskDelay(pdMS_TO_TICKS(250));
}
- sFileSource->SetPath(db->getTrackPath(id));
- });
+ }
+ if (!xStreamBufferIsEmpty(sDrainBuffer)) {
+ clearDrainBuffer();
+ }
}
auto AudioState::commitVolume() -> void {
@@ -209,23 +379,6 @@ auto AudioState::commitVolume() -> void {
}
}
-auto AudioState::readyToPlay() -> bool {
- return sCurrentTrack.has_value() && sIsPlaybackAllowed;
-}
-
-void AudioState::react(const TogglePlayPause& ev) {
- sIsPlaybackAllowed = !sIsPlaybackAllowed;
- if (readyToPlay()) {
- if (!is_in_state<states::Playback>()) {
- transit<states::Playback>();
- }
- } else {
- if (!is_in_state<states::Standby>()) {
- transit<states::Standby>();
- }
- }
-}
-
namespace states {
void Uninitialised::react(const system_fsm::BootComplete& ev) {
@@ -283,44 +436,6 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) {
transit<Standby>();
}
-void Standby::react(const PlayFile& ev) {
- sCurrentTrack = 0;
- sIsPlaybackAllowed = true;
- sFileSource->SetPath(ev.filename);
-}
-
-void Playback::react(const PlayFile& ev) {
- sFileSource->SetPath(ev.filename);
-}
-
-void Standby::react(const SeekFile& ev) {
- clearDrainBuffer();
- sFileSource->SetPath(ev.filename, ev.offset);
-}
-
-void Playback::react(const SeekFile& ev) {
- clearDrainBuffer();
- sFileSource->SetPath(ev.filename, ev.offset);
-}
-
-void Standby::react(const internal::InputFileOpened& ev) {
- if (readyToPlay()) {
- transit<Playback>();
- }
-}
-
-void Standby::react(const QueueUpdate& ev) {
- auto current_track = sServices->track_queue().current();
- if (!current_track || (sCurrentTrack && (*sCurrentTrack == *current_track))) {
- return;
- }
- if (ev.reason == QueueUpdate::Reason::kDeserialised && sLastTrackUpdate) {
- return;
- }
- clearDrainBuffer();
- playTrack(*current_track);
-}
-
static const char kQueueKey[] = "audio:queue";
static const char kCurrentFileKey[] = "audio:current";
@@ -328,7 +443,7 @@ 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;
@@ -341,10 +456,10 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) {
}
db->put(kQueueKey, queue.serialise());
- if (sLastTrackUpdate) {
+ if (sCurrentTrack) {
cppbor::Array current_track{
- cppbor::Tstr{sLastTrackUpdate->first},
- cppbor::Uint{sLastTrackUpdate->second},
+ cppbor::Tstr{sCurrentTrack->uri},
+ cppbor::Uint{currentPositionSeconds().value_or(0)},
};
db->put(kCurrentFileKey, current_track.toString());
}
@@ -371,8 +486,12 @@ void Standby::react(const system_fsm::StorageMounted& ev) {
if (parsed->type() == cppbor::ARRAY) {
std::string filename = parsed->asArray()->get(0)->asTstr()->value();
uint32_t pos = parsed->asArray()->get(1)->asUint()->value();
- sLastTrackUpdate = std::make_pair(filename, pos);
- sFileSource->SetPath(filename, pos);
+
+ events::Audio().Dispatch(SetTrack{
+ .new_track = filename,
+ .seek_to_second = pos,
+ .transition = SetTrack::Transition::kHardCut,
+ });
}
}
@@ -388,76 +507,31 @@ void Standby::react(const system_fsm::StorageMounted& ev) {
}
void Playback::entry() {
- ESP_LOGI(kTag, "beginning playback");
+ ESP_LOGI(kTag, "audio output resumed");
sOutput->mode(IAudioOutput::Modes::kOnPlaying);
- events::System().Dispatch(PlaybackStarted{});
- events::Ui().Dispatch(PlaybackStarted{});
+ PlaybackUpdate event{
+ .current_track = sCurrentTrack,
+ .track_position = currentPositionSeconds(),
+ .paused = false,
+ };
+
+ events::System().Dispatch(event);
+ events::Ui().Dispatch(event);
}
void Playback::exit() {
- ESP_LOGI(kTag, "finishing playback");
+ ESP_LOGI(kTag, "audio output paused");
sOutput->mode(IAudioOutput::Modes::kOnPaused);
- // Stash the current volume now, in case it changed during playback, since
- // we might be powering off soon.
- commitVolume();
-
- events::System().Dispatch(PlaybackStopped{});
- events::Ui().Dispatch(PlaybackStopped{});
-}
-
-void Playback::react(const system_fsm::HasPhonesChanged& ev) {
- if (!ev.has_headphones) {
- transit<Standby>();
- }
-}
-
-void Playback::react(const QueueUpdate& ev) {
- if (!ev.current_changed) {
- return;
- }
- // Cut the current track immediately.
- if (ev.reason == QueueUpdate::Reason::kExplicitUpdate) {
- clearDrainBuffer();
- }
- auto current_track = sServices->track_queue().current();
- if (!current_track) {
- sFileSource->SetPath();
- sCurrentTrack.reset();
- transit<Standby>();
- return;
- }
- playTrack(*current_track);
-}
-
-void Playback::react(const PlaybackUpdate& ev) {
- ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed,
- ev.track->duration);
- sLastTrackUpdate = std::make_pair(ev.track->filepath, ev.seconds_elapsed);
-}
-
-void Playback::react(const internal::InputFileOpened& ev) {}
+ PlaybackUpdate event{
+ .current_track = sCurrentTrack,
+ .track_position = currentPositionSeconds(),
+ .paused = true,
+ };
-void Playback::react(const internal::InputFileClosed& ev) {}
-
-void Playback::react(const internal::InputFileFinished& ev) {
- ESP_LOGI(kTag, "finished playing file");
- sLastTrackUpdate.reset();
- sServices->track_queue().finish();
- if (!sServices->track_queue().current()) {
- for (int i = 0; i < 20; i++) {
- 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/bt_audio_output.cpp b/src/audio/bt_audio_output.cpp
index dff98e36..04daf71f 100644
--- a/src/audio/bt_audio_output.cpp
+++ b/src/audio/bt_audio_output.cpp
@@ -83,7 +83,7 @@ auto BluetoothAudioOutput::PrepareFormat(const Format& orig) -> Format {
// ESP-IDF's current Bluetooth implementation currently handles SBC encoding,
// but requires a fixed input format.
return Format{
- .sample_rate = 44100,
+ .sample_rate = 48000,
.num_channels = 2,
.bits_per_sample = 16,
};
diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp
index 74c1154b..29d32390 100644
--- a/src/audio/fatfs_audio_input.cpp
+++ b/src/audio/fatfs_audio_input.cpp
@@ -22,7 +22,6 @@
#include "ff.h"
#include "freertos/portmacro.h"
#include "freertos/projdefs.h"
-#include "idf_additions.h"
#include "readahead_source.hpp"
#include "span.hpp"
diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp
index cd61d97f..3fb99159 100644
--- a/src/audio/i2s_audio_output.cpp
+++ b/src/audio/i2s_audio_output.cpp
@@ -152,9 +152,6 @@ auto I2SAudioOutput::Configure(const Format& fmt) -> void {
return;
}
- ESP_LOGI(kTag, "incoming audio stream: %u ch %u bpp @ %lu Hz",
- fmt.num_channels, fmt.bits_per_sample, fmt.sample_rate);
-
drivers::I2SDac::Channels ch;
switch (fmt.num_channels) {
case 1:
diff --git a/src/audio/include/audio_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 b8aac710..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, uint32_t current_seconds = 0);
-
- 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 a8533646..b8a0dba6 100644
--- a/src/audio/include/audio_events.hpp
+++ b/src/audio/include/audio_events.hpp
@@ -9,8 +9,10 @@
#include <stdint.h>
#include <cstdint>
#include <memory>
+#include <optional>
#include <string>
+#include "audio_sink.hpp"
#include "tinyfsm.hpp"
#include "track.hpp"
@@ -18,24 +20,82 @@
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;
- std::string filepath;
-};
-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;
+};
+
+/*
+ * 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 PlaybackStopped : tinyfsm::Event {};
+struct TogglePlayPause : tinyfsm::Event {
+ std::optional<bool> set_to;
+};
struct QueueUpdate : tinyfsm::Event {
bool current_changed;
@@ -49,15 +109,6 @@ struct QueueUpdate : tinyfsm::Event {
Reason reason;
};
-struct PlayFile : tinyfsm::Event {
- std::string filename;
-};
-
-struct SeekFile : tinyfsm::Event {
- uint32_t offset;
- std::string filename;
-};
-
struct StepUpVolume : tinyfsm::Event {};
struct StepDownVolume : tinyfsm::Event {};
struct SetVolume : tinyfsm::Event {
@@ -83,17 +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 13e241be..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&);
@@ -56,19 +65,10 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
virtual void react(const system_fsm::StorageMounted&) {}
virtual void react(const system_fsm::BluetoothEvent&);
- virtual void react(const PlayFile&) {}
- virtual void react(const SeekFile&) {}
- virtual void react(const QueueUpdate&) {}
- virtual void react(const PlaybackUpdate&) {}
- void react(const TogglePlayPause&);
-
- virtual void react(const internal::InputFileOpened&) {}
- virtual void react(const internal::InputFileClosed&) {}
- virtual void react(const internal::InputFileFinished&) {}
- virtual void react(const internal::AudioPipelineIdle&) {}
-
protected:
auto clearDrainBuffer() -> void;
+ auto awaitEmptyDrainBuffer() -> void;
+
auto playTrack(database::TrackId id) -> void;
auto commitVolume() -> void;
@@ -83,10 +83,19 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
static StreamBufferHandle_t sDrainBuffer;
- static std::optional<database::TrackId> sCurrentTrack;
+ static std::shared_ptr<TrackInfo> sCurrentTrack;
+ static uint64_t sCurrentSamples;
+ static std::optional<IAudioOutput::Format> sDrainFormat;
+ static bool sCurrentTrackIsFromQueue;
- auto readyToPlay() -> bool;
- static bool sIsPlaybackAllowed;
+ 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 {
@@ -94,7 +103,6 @@ namespace states {
class Uninitialised : public AudioState {
public:
void react(const system_fsm::BootComplete&) override;
-
void react(const system_fsm::BluetoothEvent&) override{};
using AudioState::react;
@@ -102,10 +110,6 @@ class Uninitialised : public AudioState {
class Standby : public AudioState {
public:
- void react(const PlayFile&) override;
- void react(const SeekFile&) override;
- void react(const internal::InputFileOpened&) override;
- void react(const QueueUpdate&) override;
void react(const system_fsm::KeyLockChanged&) override;
void react(const system_fsm::StorageMounted&) override;
@@ -117,18 +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 SeekFile&) override;
- void react(const QueueUpdate&) override;
- void react(const PlaybackUpdate&) override;
-
- void react(const internal::InputFileOpened&) override;
- void react(const internal::InputFileClosed&) override;
- void react(const internal::InputFileFinished&) override;
- void react(const internal::AudioPipelineIdle&) override;
-
using AudioState::react;
};
diff --git a/src/audio/include/audio_sink.hpp b/src/audio/include/audio_sink.hpp
index 85c23f5c..e11f3ce0 100644
--- a/src/audio/include/audio_sink.hpp
+++ b/src/audio/include/audio_sink.hpp
@@ -11,7 +11,6 @@
#include "esp_heap_caps.h"
#include "freertos/FreeRTOS.h"
-#include "idf_additions.h"
namespace audio {
diff --git a/src/audio/readahead_source.cpp b/src/audio/readahead_source.cpp
index c7b960d2..fe7ac3bd 100644
--- a/src/audio/readahead_source.cpp
+++ b/src/audio/readahead_source.cpp
@@ -17,7 +17,6 @@
#include "audio_source.hpp"
#include "codec.hpp"
#include "freertos/portmacro.h"
-#include "idf_additions.h"
#include "spi.hpp"
#include "tasks.hpp"
#include "types.hpp"
diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp
index a3f4c815..dbe283c4 100644
--- a/src/audio/track_queue.cpp
+++ b/src/audio/track_queue.cpp
@@ -136,7 +136,7 @@ auto TrackQueue::insert(Item i, size_t index) -> void {
{
const std::shared_lock<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]() {
diff --git a/src/database/database.cpp b/src/database/database.cpp
index ca92cf6b..06138983 100644
--- a/src/database/database.cpp
+++ b/src/database/database.cpp
@@ -60,7 +60,6 @@ static const char kKeyDbVersion[] = "schema_version";
static const char kKeyCustom[] = "U\0";
static const char kKeyCollator[] = "collator";
static const char kKeyTrackId[] = "next_track_id";
-static const char kKeyLastUpdate[] = "last_update";
static std::atomic<bool> sIsDbOpen(false);
@@ -302,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");
{
@@ -360,6 +355,7 @@ auto Database::updateIndexes() -> void {
dbRemoveIndexes(track);
track->is_tombstoned = true;
dbPutTrackData(*track);
+ db_->Delete(leveldb::WriteOptions{}, EncodePathKey(track->filepath));
continue;
}
@@ -386,7 +382,6 @@ auto Database::updateIndexes() -> void {
// Stage 2: search for newly added files.
ESP_LOGI(kTag, "scanning for new tracks");
uint64_t num_processed = 0;
- std::pair<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{
@@ -394,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) {
@@ -415,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.
@@ -432,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;
}
@@ -447,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;
}
@@ -457,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",
@@ -465,42 +467,12 @@ auto Database::updateIndexes() -> void {
tags->album().value_or("no album").c_str());
}
});
- dbSetLastUpdate(newest_track);
- ESP_LOGI(kTag, "newest track was at %u,%u", newest_track.first,
- newest_track.second);
}
auto Database::isUpdating() -> bool {
return is_updating_;
}
-auto Database::dbGetLastUpdate() -> std::pair<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/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/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
index 27843bc7..f17f6b1a 100644
--- a/src/lua/lua_screen.cpp
+++ b/src/lua/lua_screen.cpp
@@ -50,11 +50,15 @@ static auto screen_noop(lua_State* state) -> int {
return 0;
}
-static const struct luaL_Reg kScreenFuncs[] = {{"new", screen_new},
- {"createUi", screen_noop},
- {"onShown", screen_noop},
- {"onHidden", screen_noop},
- {NULL, NULL}};
+static auto screen_true(lua_State* state) -> int {
+ lua_pushboolean(state, true);
+ return 1;
+}
+
+static const struct luaL_Reg kScreenFuncs[] = {
+ {"new", screen_new}, {"createUi", screen_noop},
+ {"onShown", screen_noop}, {"onHidden", screen_noop},
+ {"canPop", screen_true}, {NULL, NULL}};
static auto lua_screen(lua_State* state) -> int {
luaL_newlib(state, kScreenFuncs);
diff --git a/src/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 eb931192..bd394428 100644
--- a/src/system_fsm/booting.cpp
+++ b/src/system_fsm/booting.cpp
@@ -63,7 +63,7 @@ auto Booting::entry() -> void {
std::unique_ptr<drivers::NvsStorage>(drivers::NvsStorage::OpenSync()));
// HACK: fix up the switch polarity on newer dev units
- sServices->nvs().LockPolarity(false);
+ // sServices->nvs().LockPolarity(false);
// I2C and SPI are both always needed. We can't even power down or show an
// error without these.
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_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/ui/include/screen.hpp b/src/ui/include/screen.hpp
index 4241c712..40284fda 100644
--- a/src/ui/include/screen.hpp
+++ b/src/ui/include/screen.hpp
@@ -43,6 +43,8 @@ class Screen {
return group_;
}
+ virtual auto canPop() -> bool = 0;
+
protected:
lv_obj_t* const root_;
lv_obj_t* content_;
diff --git a/src/ui/include/screen_lua.hpp b/src/ui/include/screen_lua.hpp
index 0ed3a508..41d97a1e 100644
--- a/src/ui/include/screen_lua.hpp
+++ b/src/ui/include/screen_lua.hpp
@@ -21,6 +21,8 @@ class Lua : public Screen {
auto onShown() -> void override;
auto onHidden() -> void override;
+ auto canPop() -> bool override;
+
auto SetObjRef(lua_State*) -> void;
private:
diff --git a/src/ui/include/screen_splash.hpp b/src/ui/include/screen_splash.hpp
index 1ee7dd89..6e746345 100644
--- a/src/ui/include/screen_splash.hpp
+++ b/src/ui/include/screen_splash.hpp
@@ -20,6 +20,8 @@ class Splash : public Screen {
Splash();
~Splash();
+ auto canPop() -> bool override { return false; }
+
private:
lv_obj_t* container_;
lv_obj_t* label_;
diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp
index 579cc2bb..5e1cc487 100644
--- a/src/ui/include/ui_fsm.hpp
+++ b/src/ui/include/ui_fsm.hpp
@@ -57,8 +57,6 @@ class UiState : public tinyfsm::Fsm<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&);
@@ -132,6 +130,8 @@ class UiState : public tinyfsm::Fsm<UiState> {
static lua::Property sLockSwitch;
static lua::Property sDatabaseUpdating;
+
+ static lua::Property sUsbMassStorageEnabled;
};
namespace states {
diff --git a/src/ui/screen_lua.cpp b/src/ui/screen_lua.cpp
index 1ad4a8e8..d43c7ee7 100644
--- a/src/ui/screen_lua.cpp
+++ b/src/ui/screen_lua.cpp
@@ -61,6 +61,25 @@ auto Lua::onHidden() -> void {
lua_pop(s_, 1);
}
+auto Lua::canPop() -> bool {
+ if (!s_ || !obj_ref_) {
+ return true;
+ }
+ lua_rawgeti(s_, LUA_REGISTRYINDEX, *obj_ref_);
+ lua_pushliteral(s_, "canPop");
+
+ if (lua_gettable(s_, -2) == LUA_TFUNCTION) {
+ // If we got a callback instead of a value, then invoke it to turn it into
+ // value.
+ lua_pushvalue(s_, -2);
+ lua::CallProtected(s_, 1, 1);
+ }
+ bool ret = lua_toboolean(s_, -1);
+
+ lua_pop(s_, 2);
+ return ret;
+}
+
auto Lua::SetObjRef(lua_State* s) -> void {
assert(s_ == nullptr);
s_ = s;
diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp
index 5582dabe..835da19e 100644
--- a/src/ui/ui_fsm.cpp
+++ b/src/ui/ui_fsm.cpp
@@ -114,14 +114,11 @@ lua::Property UiState::sBluetoothDevices{
lua::Property UiState::sPlaybackPlaying{
false, [](const lua::LuaValue& val) {
- bool current_val = std::get<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;
}};
@@ -135,12 +132,13 @@ lua::Property UiState::sPlaybackPosition{
int new_val = std::get<int>(val);
if (current_val != new_val) {
auto track = sPlaybackTrack.Get();
- if (!std::holds_alternative<audio::Track>(track)) {
+ if (!std::holds_alternative<audio::TrackInfo>(track)) {
return false;
}
- events::Audio().Dispatch(audio::SeekFile{
- .offset = (uint32_t)new_val,
- .filename = std::get<audio::Track>(track).filepath});
+ events::Audio().Dispatch(audio::SetTrack{
+ .new_track = std::get<audio::TrackInfo>(track).uri,
+ .seek_to_second = (uint32_t)new_val,
+ });
}
return true;
}};
@@ -285,6 +283,17 @@ lua::Property UiState::sLockSwitch{false};
lua::Property UiState::sDatabaseUpdating{false};
+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.
@@ -382,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) {
@@ -554,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());
@@ -611,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());