summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjacqueline <me@jacqueline.id.au>2024-03-25 17:34:41 +1100
committerjacqueline <me@jacqueline.id.au>2024-03-25 17:34:41 +1100
commit175bfc4e3e9f7aa39e084d3f1625347f1d5711ec (patch)
treef71b458f19acca855815ab876944d48a3c5acbcb
parent5c985afd258a96b68d6bd5a4fade17ed998d2c07 (diff)
downloadtangara-fw-175bfc4e3e9f7aa39e084d3f1625347f1d5711ec.tar.gz
WIP rewrie audio pipeline+fsm guts for more reliability
-rw-r--r--dependencies.lock2
-rw-r--r--src/app_console/app_console.cpp18
-rw-r--r--src/audio/audio_converter.cpp51
-rw-r--r--src/audio/audio_decoder.cpp78
-rw-r--r--src/audio/audio_fsm.cpp315
-rw-r--r--src/audio/include/audio_converter.hpp5
-rw-r--r--src/audio/include/audio_decoder.hpp20
-rw-r--r--src/audio/include/audio_events.hpp108
-rw-r--r--src/audio/include/audio_fsm.hpp53
-rw-r--r--src/audio/track_queue.cpp2
-rw-r--r--src/lua/include/property.hpp2
-rw-r--r--src/lua/property.cpp23
-rw-r--r--src/system_fsm/include/system_fsm.hpp4
-rw-r--r--src/system_fsm/running.cpp2
-rw-r--r--src/ui/include/ui_fsm.hpp2
-rw-r--r--src/ui/ui_fsm.cpp27
16 files changed, 385 insertions, 327 deletions
diff --git a/dependencies.lock b/dependencies.lock
index a9723d1e..f4489997 100644
--- a/dependencies.lock
+++ b/dependencies.lock
@@ -4,6 +4,6 @@ dependencies:
source:
type: idf
version: 5.1.1
-manifest_hash: b9761e0028130d307b778c710e5dd39fb3c942d8084ed429d448d938957fb0e6
+manifest_hash: 9e4320e6f25503854c6c93bcbfa9b80f780485bcf066bdbad31a820544492538
target: esp32
version: 1.0.0
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..1b233731 100644
--- a/src/audio/audio_converter.cpp
+++ b/src/audio/audio_converter.cpp
@@ -5,14 +5,17 @@
*/
#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"
@@ -35,7 +38,9 @@ SampleConverter::SampleConverter()
resampler_(nullptr),
source_(xStreamBufferCreateWithCaps(kSourceBufferLength,
sizeof(sample::Sample) * 2,
- MALLOC_CAP_DMA)) {
+ MALLOC_CAP_DMA)),
+ leftover_bytes_(0),
+ samples_sunk_(0) {
input_buffer_ = {
reinterpret_cast<sample::Sample*>(heap_caps_calloc(
kSampleBufferLength, sizeof(sample::Sample), MALLOC_CAP_DMA)),
@@ -107,6 +112,19 @@ auto SampleConverter::Main() -> void {
sink_->Configure(new_target);
}
target_format_ = new_target;
+
+ // Send a final sample count for the previous sample rate.
+ if (samples_sunk_ > 0) {
+ events::Audio().Dispatch(internal::ConverterProgress{
+ .samples_sunk = samples_sunk_,
+ });
+ }
+
+ samples_sunk_ = 0;
+ events::Audio().Dispatch(internal::ConverterConfigurationChanged{
+ .src_format = source_format_,
+ .dst_format = target_format_,
+ });
}
// Loop until we finish reading all the bytes indicated. There might be
@@ -154,9 +172,8 @@ auto SampleConverter::HandleSamples(cpp::span<sample::Sample> input,
if (source_format_ == target_format_) {
// The happiest possible case: the input format matches the output
// format already.
- std::size_t bytes_sent = xStreamBufferSend(
- sink_->stream(), input.data(), input.size_bytes(), portMAX_DELAY);
- return bytes_sent / sizeof(sample::Sample);
+ SendToSink(input);
+ return input.size();
}
size_t samples_used = 0;
@@ -186,16 +203,26 @@ auto SampleConverter::HandleSamples(cpp::span<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::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::ConverterProgress{
+ .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..55ebc0ec 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(
@@ -117,7 +81,6 @@ void Decoder::Main() {
}
if (ContinueDecoding()) {
- events::Audio().Dispatch(internal::InputFileFinished{});
stream_.reset();
}
}
@@ -129,6 +92,7 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool {
codec_.reset(codecs::CreateCodecForType(stream->type()).value_or(nullptr));
if (!codec_) {
ESP_LOGE(kTag, "no codec found");
+ events::Audio().Dispatch(internal::DecoderError{});
return false;
}
@@ -136,6 +100,7 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool {
if (open_res.has_error()) {
ESP_LOGE(kTag, "codec failed to start: %s",
codecs::ICodec::ErrorString(open_res.error()).c_str());
+ events::Audio().Dispatch(internal::DecoderError{});
return false;
}
stream->SetPreambleFinished();
@@ -146,20 +111,23 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool {
};
ESP_LOGI(kTag, "stream started ok");
- events::Audio().Dispatch(internal::InputFileOpened{});
-
- auto tags = std::make_shared<Track>(Track{
- .tags = stream->tags(),
- .db_info = {},
- .bitrate_kbps = open_res->sample_rate_hz,
- .encoding = stream->type(),
- .filepath = stream->Filepath(),
- });
- timer_.reset(new Timer(tags, open_res.value(), stream->Offset()));
- PlaybackUpdate ev{.seconds_elapsed = stream->Offset(), .track = tags};
- events::Audio().Dispatch(ev);
- events::Ui().Dispatch(ev);
+ std::optional<uint32_t> duration;
+ if (open_res->total_samples) {
+ duration = open_res->total_samples.value() / open_res->num_channels /
+ open_res->sample_rate_hz;
+ }
+
+ events::Audio().Dispatch(internal::DecoderOpened{
+ .track = std::make_shared<TrackInfo>(TrackInfo{
+ .tags = stream->tags(),
+ .uri = stream->Filepath(),
+ .duration = duration,
+ .start_offset = stream->Offset(),
+ .bitrate_kbps = open_res->sample_rate_hz,
+ .encoding = stream->type(),
+ }),
+ });
return true;
}
@@ -167,6 +135,7 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool {
auto Decoder::ContinueDecoding() -> bool {
auto res = codec_->DecodeTo(codec_buffer_);
if (res.has_error()) {
+ events::Audio().Dispatch(internal::DecoderError{});
return true;
}
@@ -176,11 +145,8 @@ auto Decoder::ContinueDecoding() -> bool {
res->is_stream_finished);
}
- if (timer_) {
- timer_->AddSamples(res->samples_written);
- }
-
if (res->is_stream_finished) {
+ events::Audio().Dispatch(internal::DecoderClosed{});
codec_.reset();
}
diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp
index 05c7c216..7a138cba 100644
--- a/src/audio/audio_fsm.cpp
+++ b/src/audio/audio_fsm.cpp
@@ -36,6 +36,7 @@
#include "sample.hpp"
#include "service_locator.hpp"
#include "system_events.hpp"
+#include "tinyfsm.hpp"
#include "track.hpp"
#include "track_queue.hpp"
#include "wm8523.hpp"
@@ -54,13 +55,158 @@ std::shared_ptr<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;
+constexpr size_t kDrainBufferSize =
+ sizeof(sample::Sample) * kDrainLatencySamples * 4;
+
StreamBufferHandle_t AudioState::sDrainBuffer;
-std::optional<database::TrackId> AudioState::sCurrentTrack;
-bool AudioState::sIsPlaybackAllowed;
+std::shared_ptr<TrackInfo> AudioState::sCurrentTrack;
+uint64_t AudioState::sCurrentSamples;
+std::optional<IAudioOutput::Format> AudioState::sCurrentFormat;
+
+std::shared_ptr<TrackInfo> AudioState::sNextTrack;
+uint64_t AudioState::sNextTrackCueSamples;
+
+bool AudioState::sIsResampling;
+bool AudioState::sIsPaused = true;
+
+auto AudioState::currentPositionSeconds() -> std::optional<uint32_t> {
+ if (!sCurrentTrack || !sCurrentFormat) {
+ return {};
+ }
+ return sCurrentSamples /
+ (sCurrentFormat->num_channels * sCurrentFormat->sample_rate);
+}
+
+void AudioState::react(const QueueUpdate& ev) {
+ if (!ev.current_changed && ev.reason != QueueUpdate::kRepeatingLastTrack) {
+ return;
+ }
+
+ SetTrack::Transition transition;
+ switch (ev.reason) {
+ case QueueUpdate::kExplicitUpdate:
+ transition = SetTrack::Transition::kHardCut;
+ break;
+ case QueueUpdate::kRepeatingLastTrack:
+ case QueueUpdate::kTrackFinished:
+ transition = SetTrack::Transition::kGapless;
+ break;
+ case QueueUpdate::kDeserialised:
+ default:
+ // The current track is deserialised separately in order to retain seek
+ // position.
+ return;
+ }
+
+ SetTrack cmd{
+ .new_track = {},
+ .seek_to_second = 0,
+ .transition = transition,
+ };
-static std::optional<std::pair<std::string, uint32_t>> sLastTrackUpdate;
+ auto current = sServices->track_queue().current();
+ if (current) {
+ cmd.new_track = *current;
+ }
+
+ tinyfsm::FsmList<AudioState>::dispatch(cmd);
+}
+
+void AudioState::react(const SetTrack& ev) {
+ if (ev.transition == SetTrack::Transition::kHardCut) {
+ clearDrainBuffer();
+ }
+
+ // Move the rest of the work to a background worker, since it may require db
+ // lookups to resolve a track id into a path.
+ auto new_track = ev.new_track;
+ uint32_t seek_to = ev.seek_to_second.value_or(0);
+ sServices->bg_worker().Dispatch<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::DecoderOpened& ev) {
+ ESP_LOGI(kTag, "decoder opened %s", ev.track->uri.c_str());
+ sNextTrack = ev.track;
+ sNextTrackCueSamples = sCurrentSamples + kDrainLatencySamples;
+}
+
+void AudioState::react(const internal::DecoderClosed&) {
+ ESP_LOGI(kTag, "decoder closed");
+ // FIXME: only when we were playing the current track
+ sServices->track_queue().finish();
+}
+
+void AudioState::react(const internal::DecoderError&) {
+ ESP_LOGW(kTag, "decoder errored");
+ // FIXME: only when we were playing the current track
+ sServices->track_queue().finish();
+}
+
+void AudioState::react(const internal::ConverterConfigurationChanged& ev) {
+ sCurrentFormat = ev.dst_format;
+ sIsResampling = ev.src_format != ev.dst_format;
+ ESP_LOGI(kTag, "output format now %u ch @ %lu hz (resample=%i)",
+ sCurrentFormat->num_channels, sCurrentFormat->sample_rate,
+ sIsResampling);
+}
+
+void AudioState::react(const internal::ConverterProgress& ev) {
+ ESP_LOGI(kTag, "sample converter sunk %lu samples", ev.samples_sunk);
+ sCurrentSamples += ev.samples_sunk;
+
+ if (sNextTrack && sCurrentSamples >= sNextTrackCueSamples) {
+ ESP_LOGI(kTag, "next track is now sinking");
+ sCurrentTrack = sNextTrack;
+ sCurrentSamples -= sNextTrackCueSamples;
+ sCurrentSamples +=
+ sNextTrack->start_offset.value_or(0) *
+ (sCurrentFormat->num_channels * sCurrentFormat->sample_rate);
+
+ sNextTrack.reset();
+ sNextTrackCueSamples = 0;
+ }
+
+ PlaybackUpdate event{
+ .current_track = sCurrentTrack,
+ .track_position = currentPositionSeconds(),
+ .paused = !is_in_state<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,17 +330,6 @@ 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;
- }
- sFileSource->SetPath(db->getTrackPath(id));
- });
-}
-
auto AudioState::commitVolume() -> void {
auto mode = sServices->nvs().OutputMode();
auto vol = sOutput->GetVolume();
@@ -209,23 +344,6 @@ auto AudioState::commitVolume() -> void {
}
}
-auto AudioState::readyToPlay() -> bool {
- return sCurrentTrack.has_value() && sIsPlaybackAllowed;
-}
-
-void AudioState::react(const TogglePlayPause& ev) {
- sIsPlaybackAllowed = !sIsPlaybackAllowed;
- if (readyToPlay()) {
- if (!is_in_state<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 +401,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 +408,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 +421,10 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) {
}
db->put(kQueueKey, queue.serialise());
- if (sLastTrackUpdate) {
+ if (sCurrentTrack) {
cppbor::Array current_track{
- cppbor::Tstr{sLastTrackUpdate->first},
- cppbor::Uint{sLastTrackUpdate->second},
+ cppbor::Tstr{sCurrentTrack->uri},
+ cppbor::Uint{currentPositionSeconds().value_or(0)},
};
db->put(kCurrentFileKey, current_track.toString());
}
@@ -371,8 +451,12 @@ void Standby::react(const system_fsm::StorageMounted& ev) {
if (parsed->type() == cppbor::ARRAY) {
std::string filename = parsed->asArray()->get(0)->asTstr()->value();
uint32_t pos = parsed->asArray()->get(1)->asUint()->value();
- sLastTrackUpdate = std::make_pair(filename, pos);
- sFileSource->SetPath(filename, pos);
+
+ events::Audio().Dispatch(SetTrack{
+ .new_track = filename,
+ .seek_to_second = pos,
+ .transition = SetTrack::Transition::kHardCut,
+ });
}
}
@@ -388,76 +472,31 @@ void Standby::react(const system_fsm::StorageMounted& ev) {
}
void Playback::entry() {
- ESP_LOGI(kTag, "beginning playback");
+ ESP_LOGI(kTag, "audio output resumed");
sOutput->mode(IAudioOutput::Modes::kOnPlaying);
- events::System().Dispatch(PlaybackStarted{});
- events::Ui().Dispatch(PlaybackStarted{});
+ PlaybackUpdate event{
+ .current_track = sCurrentTrack,
+ .track_position = currentPositionSeconds(),
+ .paused = false,
+ };
+
+ events::System().Dispatch(event);
+ events::Ui().Dispatch(event);
}
void Playback::exit() {
- ESP_LOGI(kTag, "finishing playback");
+ ESP_LOGI(kTag, "audio output paused");
sOutput->mode(IAudioOutput::Modes::kOnPaused);
- // Stash the current volume now, in case it changed during playback, since
- // we might be powering off soon.
- commitVolume();
-
- events::System().Dispatch(PlaybackStopped{});
- events::Ui().Dispatch(PlaybackStopped{});
-}
-
-void Playback::react(const system_fsm::HasPhonesChanged& ev) {
- if (!ev.has_headphones) {
- transit<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) {}
-
-void Playback::react(const internal::InputFileClosed& ev) {}
+ PlaybackUpdate event{
+ .current_track = sCurrentTrack,
+ .track_position = currentPositionSeconds(),
+ .paused = true,
+ };
-void Playback::react(const internal::InputFileFinished& ev) {
- ESP_LOGI(kTag, "finished playing file");
- sLastTrackUpdate.reset();
- sServices->track_queue().finish();
- if (!sServices->track_queue().current()) {
- for (int i = 0; i < 20; i++) {
- if (xStreamBufferIsEmpty(sDrainBuffer)) {
- break;
- }
- vTaskDelay(pdMS_TO_TICKS(200));
- }
- transit<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/include/audio_converter.hpp b/src/audio/include/audio_converter.hpp
index c2ebde60..dcd068b5 100644
--- a/src/audio/include/audio_converter.hpp
+++ b/src/audio/include/audio_converter.hpp
@@ -6,6 +6,7 @@
#pragma once
+#include <stdint.h>
#include <cstdint>
#include <memory>
@@ -40,6 +41,8 @@ class SampleConverter {
auto SetTargetFormat(const IAudioOutput::Format& format) -> void;
auto HandleSamples(cpp::span<sample::Sample>, bool) -> size_t;
+ auto SendToSink(cpp::span<sample::Sample>) -> void;
+
struct Args {
IAudioOutput::Format format;
size_t samples_available;
@@ -59,6 +62,8 @@ class SampleConverter {
IAudioOutput::Format source_format_;
IAudioOutput::Format target_format_;
size_t leftover_bytes_;
+
+ uint32_t samples_sunk_;
};
} // namespace audio
diff --git a/src/audio/include/audio_decoder.hpp b/src/audio/include/audio_decoder.hpp
index b8aac710..89f0f43c 100644
--- a/src/audio/include/audio_decoder.hpp
+++ b/src/audio/include/audio_decoder.hpp
@@ -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..9af30467 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,80 @@
namespace audio {
-struct Track {
+/*
+ * Struct encapsulating information about the decoder's current track.
+ */
+struct TrackInfo {
+ /*
+ * Audio tags extracted from the file. May be absent for files without any
+ * parseable tags.
+ */
std::shared_ptr<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 {};
-
+/*
+ * 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 +107,6 @@ struct QueueUpdate : tinyfsm::Event {
Reason reason;
};
-struct PlayFile : tinyfsm::Event {
- std::string filename;
-};
-
-struct SeekFile : tinyfsm::Event {
- uint32_t offset;
- std::string filename;
-};
-
struct StepUpVolume : tinyfsm::Event {};
struct StepDownVolume : tinyfsm::Event {};
struct SetVolume : tinyfsm::Event {
@@ -83,17 +132,26 @@ struct SetVolumeLimit : tinyfsm::Event {
int limit_db;
};
-struct TogglePlayPause : tinyfsm::Event {};
-
struct OutputModeChanged : tinyfsm::Event {};
namespace internal {
-struct InputFileOpened : tinyfsm::Event {};
-struct InputFileClosed : tinyfsm::Event {};
-struct InputFileFinished : tinyfsm::Event {};
+struct DecoderOpened : tinyfsm::Event {
+ std::shared_ptr<TrackInfo> track;
+};
+
+struct DecoderClosed : tinyfsm::Event {};
+
+struct DecoderError : tinyfsm::Event {};
-struct AudioPipelineIdle : tinyfsm::Event {};
+struct ConverterConfigurationChanged : tinyfsm::Event {
+ IAudioOutput::Format src_format;
+ IAudioOutput::Format dst_format;
+};
+
+struct ConverterProgress : tinyfsm::Event {
+ uint32_t samples_sunk;
+};
} // namespace internal
diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp
index 13e241be..62bb4786 100644
--- a/src/audio/include/audio_fsm.hpp
+++ b/src/audio/include/audio_fsm.hpp
@@ -6,6 +6,7 @@
#pragma once
+#include <stdint.h>
#include <deque>
#include <memory>
#include <vector>
@@ -41,6 +42,17 @@ 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::DecoderOpened&);
+ void react(const internal::DecoderClosed&);
+ void react(const internal::DecoderError&);
+
+ void react(const internal::ConverterConfigurationChanged&);
+ void react(const internal::ConverterProgress&);
+
void react(const StepUpVolume&);
void react(const StepDownVolume&);
virtual void react(const system_fsm::HasPhonesChanged&);
@@ -56,17 +68,6 @@ class AudioState : public tinyfsm::Fsm<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 playTrack(database::TrackId id) -> void;
@@ -83,10 +84,17 @@ 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> sCurrentFormat;
- auto readyToPlay() -> bool;
- static bool sIsPlaybackAllowed;
+ static std::shared_ptr<TrackInfo> sNextTrack;
+ static uint64_t sNextTrackCueSamples;
+
+ static bool sIsResampling;
+ static bool sIsPaused;
+
+ auto currentPositionSeconds() -> std::optional<uint32_t>;
};
namespace states {
@@ -94,7 +102,6 @@ namespace states {
class Uninitialised : public AudioState {
public:
void react(const system_fsm::BootComplete&) override;
-
void react(const system_fsm::BluetoothEvent&) override{};
using AudioState::react;
@@ -102,10 +109,6 @@ class Uninitialised : public AudioState {
class Standby : public AudioState {
public:
- void react(const PlayFile&) override;
- void react(const SeekFile&) override;
- void react(const internal::InputFileOpened&) override;
- void react(const QueueUpdate&) override;
void react(const system_fsm::KeyLockChanged&) override;
void react(const system_fsm::StorageMounted&) override;
@@ -117,18 +120,6 @@ class Playback : public AudioState {
void entry() override;
void exit() override;
- void react(const system_fsm::HasPhonesChanged&) override;
-
- void react(const PlayFile&) override;
- void react(const SeekFile&) override;
- void react(const QueueUpdate&) override;
- void react(const PlaybackUpdate&) override;
-
- void react(const internal::InputFileOpened&) override;
- void react(const internal::InputFileClosed&) override;
- void react(const internal::InputFileFinished&) override;
- void react(const internal::AudioPipelineIdle&) override;
-
using AudioState::react;
};
diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp
index a3f4c815..dbe283c4 100644
--- a/src/audio/track_queue.cpp
+++ b/src/audio/track_queue.cpp
@@ -136,7 +136,7 @@ auto TrackQueue::insert(Item i, size_t index) -> void {
{
const std::shared_lock<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/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/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/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 d80809e6..a6ab5d47 100644
--- a/src/system_fsm/running.cpp
+++ b/src/system_fsm/running.cpp
@@ -56,7 +56,7 @@ void Running::react(const KeyLockChanged& ev) {
checkIdle();
}
-void Running::react(const audio::PlaybackStopped& ev) {
+void Running::react(const audio::PlaybackUpdate& ev) {
checkIdle();
}
diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp
index f7fde1dd..5e1cc487 100644
--- a/src/ui/include/ui_fsm.hpp
+++ b/src/ui/include/ui_fsm.hpp
@@ -57,8 +57,6 @@ class UiState : public tinyfsm::Fsm<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&);
diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp
index a913a339..42c6a99c 100644
--- a/src/ui/ui_fsm.cpp
+++ b/src/ui/ui_fsm.cpp
@@ -114,14 +114,11 @@ lua::Property UiState::sBluetoothDevices{
lua::Property UiState::sPlaybackPlaying{
false, [](const lua::LuaValue& val) {
- bool current_val = std::get<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;
}};
@@ -393,17 +391,10 @@ void UiState::react(const audio::QueueUpdate&) {
sQueueReplay.Update(queue.replay());
}
-void UiState::react(const audio::PlaybackStarted& ev) {
- sPlaybackPlaying.Update(true);
-}
-
void UiState::react(const audio::PlaybackUpdate& ev) {
- sPlaybackTrack.Update(*ev.track);
- sPlaybackPosition.Update(static_cast<int>(ev.seconds_elapsed));
-}
-
-void UiState::react(const audio::PlaybackStopped&) {
- sPlaybackPlaying.Update(false);
+ sPlaybackTrack.Update(*ev.current_track);
+ sPlaybackPlaying.Update(!ev.paused);
+ sPlaybackPosition.Update(static_cast<int>(ev.track_position.value_or(0)));
}
void UiState::react(const audio::VolumeChanged& ev) {