From 175bfc4e3e9f7aa39e084d3f1625347f1d5711ec Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 25 Mar 2024 17:34:41 +1100 Subject: [PATCH] WIP rewrie audio pipeline+fsm guts for more reliability --- dependencies.lock | 2 +- src/app_console/app_console.cpp | 18 +- src/audio/audio_converter.cpp | 51 ++++- src/audio/audio_decoder.cpp | 78 ++----- src/audio/audio_fsm.cpp | 315 +++++++++++++++----------- src/audio/include/audio_converter.hpp | 5 + src/audio/include/audio_decoder.hpp | 20 -- src/audio/include/audio_events.hpp | 108 +++++++-- src/audio/include/audio_fsm.hpp | 53 ++--- src/audio/track_queue.cpp | 2 +- src/lua/include/property.hpp | 2 +- src/lua/property.cpp | 23 +- src/system_fsm/include/system_fsm.hpp | 4 +- src/system_fsm/running.cpp | 2 +- src/ui/include/ui_fsm.hpp | 2 - src/ui/ui_fsm.cpp | 27 +-- 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 AppConsole::sServices; int CmdVersion(int argc, char** argv) { - std::cout << "firmware-version=" << esp_app_get_description()->version << std::endl; - std::cout << "samd-version=" << AppConsole::sServices->samd().Version() << std::endl; - std::cout << "collation=" << AppConsole::sServices->collator().Describe().value_or("") << std::endl; - std::cout << "database-schema=" << uint32_t(database::kCurrentDbVersion) << std::endl; + std::cout << "firmware-version=" << esp_app_get_description()->version + << std::endl; + std::cout << "samd-version=" << AppConsole::sServices->samd().Version() + << std::endl; + std::cout << "collation=" + << AppConsole::sServices->collator().Describe().value_or("") + << std::endl; + std::cout << "database-schema=" << uint32_t(database::kCurrentDbVersion) + << std::endl; return 0; } @@ -148,7 +153,7 @@ int CmdPlayFile(int argc, char** argv) { database::TrackId id = std::atoi(argv[1]); AppConsole::sServices->track_queue().append(id); } else { - std::pmr::string path{&memory::kSpiRamResource}; + std::string path; path += '/'; path += argv[1]; for (int i = 2; i < argc; i++) { @@ -156,8 +161,7 @@ int CmdPlayFile(int argc, char** argv) { path += argv[i]; } - events::Audio().Dispatch( - audio::PlayFile{.filename = {path.data(), path.size()}}); + events::Audio().Dispatch(audio::SetTrack{.new_track = path}); } return 0; diff --git a/src/audio/audio_converter.cpp b/src/audio/audio_converter.cpp index 946a0b63..1b233731 100644 --- a/src/audio/audio_converter.cpp +++ b/src/audio/audio_converter.cpp @@ -5,14 +5,17 @@ */ #include "audio_converter.hpp" +#include #include #include #include +#include "audio_events.hpp" #include "audio_sink.hpp" #include "esp_heap_caps.h" #include "esp_log.h" +#include "event_queue.hpp" #include "freertos/portmacro.h" #include "freertos/projdefs.h" #include "i2s_dac.hpp" @@ -35,7 +38,9 @@ SampleConverter::SampleConverter() resampler_(nullptr), source_(xStreamBufferCreateWithCaps(kSourceBufferLength, sizeof(sample::Sample) * 2, - MALLOC_CAP_DMA)) { + MALLOC_CAP_DMA)), + leftover_bytes_(0), + samples_sunk_(0) { input_buffer_ = { reinterpret_cast(heap_caps_calloc( kSampleBufferLength, sizeof(sample::Sample), MALLOC_CAP_DMA)), @@ -107,6 +112,19 @@ auto SampleConverter::Main() -> void { sink_->Configure(new_target); } target_format_ = new_target; + + // Send a final sample count for the previous sample rate. + if (samples_sunk_ > 0) { + events::Audio().Dispatch(internal::ConverterProgress{ + .samples_sunk = samples_sunk_, + }); + } + + samples_sunk_ = 0; + events::Audio().Dispatch(internal::ConverterConfigurationChanged{ + .src_format = source_format_, + .dst_format = target_format_, + }); } // Loop until we finish reading all the bytes indicated. There might be @@ -154,9 +172,8 @@ auto SampleConverter::HandleSamples(cpp::span input, if (source_format_ == target_format_) { // The happiest possible case: the input format matches the output // format already. - std::size_t bytes_sent = xStreamBufferSend( - sink_->stream(), input.data(), input.size_bytes(), portMAX_DELAY); - return bytes_sent / sizeof(sample::Sample); + SendToSink(input); + return input.size(); } size_t samples_used = 0; @@ -186,16 +203,26 @@ auto SampleConverter::HandleSamples(cpp::span input, samples_used = input.size(); } - size_t bytes_sent = 0; - size_t bytes_to_send = output_source.size_bytes(); - while (bytes_sent < bytes_to_send) { - bytes_sent += xStreamBufferSend( - sink_->stream(), - reinterpret_cast(output_source.data()) + bytes_sent, - bytes_to_send - bytes_sent, portMAX_DELAY); - } + SendToSink(output_source); } return samples_used; } +auto SampleConverter::SendToSink(cpp::span samples) -> void { + // Update the number of samples sunk so far *before* actually sinking them, + // since writing to the stream buffer will block when the buffer gets full. + samples_sunk_ += samples.size(); + if (samples_sunk_ >= + target_format_.sample_rate * target_format_.num_channels) { + events::Audio().Dispatch(internal::ConverterProgress{ + .samples_sunk = samples_sunk_, + }); + samples_sunk_ = 0; + } + + xStreamBufferSend(sink_->stream(), + reinterpret_cast(samples.data()), + samples.size_bytes(), portMAX_DELAY); +} + } // namespace audio diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp index 68a8a86b..55ebc0ec 100644 --- a/src/audio/audio_decoder.cpp +++ b/src/audio/audio_decoder.cpp @@ -5,6 +5,7 @@ */ #include "audio_decoder.hpp" +#include #include #include @@ -50,39 +51,6 @@ namespace audio { static constexpr std::size_t kCodecBufferLength = drivers::kI2SBufferLengthFrames * sizeof(sample::Sample); -Timer::Timer(std::shared_ptr t, - const codecs::ICodec::OutputFormat& format, - uint32_t current_seconds) - : track_(t), - current_seconds_(current_seconds), - current_sample_in_second_(0), - samples_per_second_(format.sample_rate_hz * format.num_channels), - total_duration_seconds_(format.total_samples.value_or(0) / - format.num_channels / format.sample_rate_hz) { - track_->duration = total_duration_seconds_; -} - -auto Timer::AddSamples(std::size_t samples) -> void { - bool incremented = false; - current_sample_in_second_ += samples; - while (current_sample_in_second_ >= samples_per_second_) { - current_seconds_++; - current_sample_in_second_ -= samples_per_second_; - incremented = true; - } - - if (incremented) { - if (total_duration_seconds_ < current_seconds_) { - total_duration_seconds_ = current_seconds_; - track_->duration = total_duration_seconds_; - } - - PlaybackUpdate ev{.seconds_elapsed = current_seconds_, .track = track_}; - events::Audio().Dispatch(ev); - events::Ui().Dispatch(ev); - } -} - auto Decoder::Start(std::shared_ptr source, std::shared_ptr sink) -> Decoder* { Decoder* task = new Decoder(source, sink); @@ -92,11 +60,7 @@ auto Decoder::Start(std::shared_ptr source, Decoder::Decoder(std::shared_ptr source, std::shared_ptr mixer) - : source_(source), - converter_(mixer), - codec_(), - timer_(), - current_format_() { + : source_(source), converter_(mixer), codec_(), current_format_() { ESP_LOGI(kTag, "allocating codec buffer, %u KiB", kCodecBufferLength / 1024); codec_buffer_ = { reinterpret_cast(heap_caps_calloc( @@ -117,7 +81,6 @@ void Decoder::Main() { } if (ContinueDecoding()) { - events::Audio().Dispatch(internal::InputFileFinished{}); stream_.reset(); } } @@ -129,6 +92,7 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { codec_.reset(codecs::CreateCodecForType(stream->type()).value_or(nullptr)); if (!codec_) { ESP_LOGE(kTag, "no codec found"); + events::Audio().Dispatch(internal::DecoderError{}); return false; } @@ -136,6 +100,7 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { if (open_res.has_error()) { ESP_LOGE(kTag, "codec failed to start: %s", codecs::ICodec::ErrorString(open_res.error()).c_str()); + events::Audio().Dispatch(internal::DecoderError{}); return false; } stream->SetPreambleFinished(); @@ -146,20 +111,23 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { }; ESP_LOGI(kTag, "stream started ok"); - events::Audio().Dispatch(internal::InputFileOpened{}); - - auto tags = std::make_shared(Track{ - .tags = stream->tags(), - .db_info = {}, - .bitrate_kbps = open_res->sample_rate_hz, - .encoding = stream->type(), - .filepath = stream->Filepath(), - }); - timer_.reset(new Timer(tags, open_res.value(), stream->Offset())); - PlaybackUpdate ev{.seconds_elapsed = stream->Offset(), .track = tags}; - events::Audio().Dispatch(ev); - events::Ui().Dispatch(ev); + std::optional duration; + if (open_res->total_samples) { + duration = open_res->total_samples.value() / open_res->num_channels / + open_res->sample_rate_hz; + } + + events::Audio().Dispatch(internal::DecoderOpened{ + .track = std::make_shared(TrackInfo{ + .tags = stream->tags(), + .uri = stream->Filepath(), + .duration = duration, + .start_offset = stream->Offset(), + .bitrate_kbps = open_res->sample_rate_hz, + .encoding = stream->type(), + }), + }); return true; } @@ -167,6 +135,7 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { auto Decoder::ContinueDecoding() -> bool { auto res = codec_->DecodeTo(codec_buffer_); if (res.has_error()) { + events::Audio().Dispatch(internal::DecoderError{}); return true; } @@ -176,11 +145,8 @@ auto Decoder::ContinueDecoding() -> bool { res->is_stream_finished); } - if (timer_) { - timer_->AddSamples(res->samples_written); - } - if (res->is_stream_finished) { + events::Audio().Dispatch(internal::DecoderClosed{}); codec_.reset(); } diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 05c7c216..7a138cba 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -36,6 +36,7 @@ #include "sample.hpp" #include "service_locator.hpp" #include "system_events.hpp" +#include "tinyfsm.hpp" #include "track.hpp" #include "track_queue.hpp" #include "wm8523.hpp" @@ -54,13 +55,158 @@ std::shared_ptr AudioState::sBtOutput; std::shared_ptr AudioState::sOutput; // Two seconds of samples for two channels, at a representative sample rate. -constexpr size_t kDrainBufferSize = sizeof(sample::Sample) * 48000 * 4; +constexpr size_t kDrainLatencySamples = 48000; +constexpr size_t kDrainBufferSize = + sizeof(sample::Sample) * kDrainLatencySamples * 4; + StreamBufferHandle_t AudioState::sDrainBuffer; -std::optional AudioState::sCurrentTrack; -bool AudioState::sIsPlaybackAllowed; +std::shared_ptr AudioState::sCurrentTrack; +uint64_t AudioState::sCurrentSamples; +std::optional AudioState::sCurrentFormat; + +std::shared_ptr AudioState::sNextTrack; +uint64_t AudioState::sNextTrackCueSamples; + +bool AudioState::sIsResampling; +bool AudioState::sIsPaused = true; + +auto AudioState::currentPositionSeconds() -> std::optional { + if (!sCurrentTrack || !sCurrentFormat) { + return {}; + } + return sCurrentSamples / + (sCurrentFormat->num_channels * sCurrentFormat->sample_rate); +} + +void AudioState::react(const QueueUpdate& ev) { + if (!ev.current_changed && ev.reason != QueueUpdate::kRepeatingLastTrack) { + return; + } + + SetTrack::Transition transition; + switch (ev.reason) { + case QueueUpdate::kExplicitUpdate: + transition = SetTrack::Transition::kHardCut; + break; + case QueueUpdate::kRepeatingLastTrack: + case QueueUpdate::kTrackFinished: + transition = SetTrack::Transition::kGapless; + break; + case QueueUpdate::kDeserialised: + default: + // The current track is deserialised separately in order to retain seek + // position. + return; + } + + SetTrack cmd{ + .new_track = {}, + .seek_to_second = 0, + .transition = transition, + }; -static std::optional> sLastTrackUpdate; + auto current = sServices->track_queue().current(); + if (current) { + cmd.new_track = *current; + } + + tinyfsm::FsmList::dispatch(cmd); +} + +void AudioState::react(const SetTrack& ev) { + if (ev.transition == SetTrack::Transition::kHardCut) { + clearDrainBuffer(); + } + + // Move the rest of the work to a background worker, since it may require db + // lookups to resolve a track id into a path. + auto new_track = ev.new_track; + uint32_t seek_to = ev.seek_to_second.value_or(0); + sServices->bg_worker().Dispatch([=]() { + std::optional path; + if (std::holds_alternative(new_track)) { + auto db = sServices->database().lock(); + if (db) { + path = db->getTrackPath(std::get(new_track)); + } + } else if (std::holds_alternative(new_track)) { + path = std::get(new_track); + } + + if (path) { + sFileSource->SetPath(*path, seek_to); + } else { + sFileSource->SetPath(); + } + }); +} + +void AudioState::react(const TogglePlayPause& ev) { + sIsPaused = !ev.set_to.value_or(sIsPaused); + if (!sIsPaused && is_in_state() && sCurrentTrack) { + transit(); + } else if (sIsPaused && is_in_state()) { + transit(); + } +} + +void AudioState::react(const internal::DecoderOpened& ev) { + ESP_LOGI(kTag, "decoder opened %s", ev.track->uri.c_str()); + sNextTrack = ev.track; + sNextTrackCueSamples = sCurrentSamples + kDrainLatencySamples; +} + +void AudioState::react(const internal::DecoderClosed&) { + ESP_LOGI(kTag, "decoder closed"); + // FIXME: only when we were playing the current track + sServices->track_queue().finish(); +} + +void AudioState::react(const internal::DecoderError&) { + ESP_LOGW(kTag, "decoder errored"); + // FIXME: only when we were playing the current track + sServices->track_queue().finish(); +} + +void AudioState::react(const internal::ConverterConfigurationChanged& ev) { + sCurrentFormat = ev.dst_format; + sIsResampling = ev.src_format != ev.dst_format; + ESP_LOGI(kTag, "output format now %u ch @ %lu hz (resample=%i)", + sCurrentFormat->num_channels, sCurrentFormat->sample_rate, + sIsResampling); +} + +void AudioState::react(const internal::ConverterProgress& ev) { + ESP_LOGI(kTag, "sample converter sunk %lu samples", ev.samples_sunk); + sCurrentSamples += ev.samples_sunk; + + if (sNextTrack && sCurrentSamples >= sNextTrackCueSamples) { + ESP_LOGI(kTag, "next track is now sinking"); + sCurrentTrack = sNextTrack; + sCurrentSamples -= sNextTrackCueSamples; + sCurrentSamples += + sNextTrack->start_offset.value_or(0) * + (sCurrentFormat->num_channels * sCurrentFormat->sample_rate); + + sNextTrack.reset(); + sNextTrackCueSamples = 0; + } + + PlaybackUpdate event{ + .current_track = sCurrentTrack, + .track_position = currentPositionSeconds(), + .paused = !is_in_state(), + }; + + events::System().Dispatch(event); + events::Ui().Dispatch(event); + + if (sCurrentTrack && !sIsPaused && !is_in_state()) { + ESP_LOGI(kTag, "ready to play!"); + transit(); + } +} void AudioState::react(const system_fsm::BluetoothEvent& ev) { if (ev.event != drivers::bluetooth::Event::kConnectionStateChanged) { @@ -184,17 +330,6 @@ auto AudioState::clearDrainBuffer() -> void { } } -auto AudioState::playTrack(database::TrackId id) -> void { - sCurrentTrack = id; - sServices->bg_worker().Dispatch([=]() { - auto db = sServices->database().lock(); - if (!db) { - return; - } - sFileSource->SetPath(db->getTrackPath(id)); - }); -} - auto AudioState::commitVolume() -> void { auto mode = sServices->nvs().OutputMode(); auto vol = sOutput->GetVolume(); @@ -209,23 +344,6 @@ auto AudioState::commitVolume() -> void { } } -auto AudioState::readyToPlay() -> bool { - return sCurrentTrack.has_value() && sIsPlaybackAllowed; -} - -void AudioState::react(const TogglePlayPause& ev) { - sIsPlaybackAllowed = !sIsPlaybackAllowed; - if (readyToPlay()) { - if (!is_in_state()) { - transit(); - } - } else { - if (!is_in_state()) { - transit(); - } - } -} - namespace states { void Uninitialised::react(const system_fsm::BootComplete& ev) { @@ -283,44 +401,6 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { transit(); } -void Standby::react(const PlayFile& ev) { - sCurrentTrack = 0; - sIsPlaybackAllowed = true; - sFileSource->SetPath(ev.filename); -} - -void Playback::react(const PlayFile& ev) { - sFileSource->SetPath(ev.filename); -} - -void Standby::react(const SeekFile& ev) { - clearDrainBuffer(); - sFileSource->SetPath(ev.filename, ev.offset); -} - -void Playback::react(const SeekFile& ev) { - clearDrainBuffer(); - sFileSource->SetPath(ev.filename, ev.offset); -} - -void Standby::react(const internal::InputFileOpened& ev) { - if (readyToPlay()) { - transit(); - } -} - -void Standby::react(const QueueUpdate& ev) { - auto current_track = sServices->track_queue().current(); - if (!current_track || (sCurrentTrack && (*sCurrentTrack == *current_track))) { - return; - } - if (ev.reason == QueueUpdate::Reason::kDeserialised && sLastTrackUpdate) { - return; - } - clearDrainBuffer(); - playTrack(*current_track); -} - static const char kQueueKey[] = "audio:queue"; static const char kCurrentFileKey[] = "audio:current"; @@ -328,7 +408,7 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) { if (!ev.locking) { return; } - sServices->bg_worker().Dispatch([]() { + sServices->bg_worker().Dispatch([this]() { auto db = sServices->database().lock(); if (!db) { return; @@ -341,10 +421,10 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) { } db->put(kQueueKey, queue.serialise()); - if (sLastTrackUpdate) { + if (sCurrentTrack) { cppbor::Array current_track{ - cppbor::Tstr{sLastTrackUpdate->first}, - cppbor::Uint{sLastTrackUpdate->second}, + cppbor::Tstr{sCurrentTrack->uri}, + cppbor::Uint{currentPositionSeconds().value_or(0)}, }; db->put(kCurrentFileKey, current_track.toString()); } @@ -371,8 +451,12 @@ void Standby::react(const system_fsm::StorageMounted& ev) { if (parsed->type() == cppbor::ARRAY) { std::string filename = parsed->asArray()->get(0)->asTstr()->value(); uint32_t pos = parsed->asArray()->get(1)->asUint()->value(); - sLastTrackUpdate = std::make_pair(filename, pos); - sFileSource->SetPath(filename, pos); + + events::Audio().Dispatch(SetTrack{ + .new_track = filename, + .seek_to_second = pos, + .transition = SetTrack::Transition::kHardCut, + }); } } @@ -388,76 +472,31 @@ void Standby::react(const system_fsm::StorageMounted& ev) { } void Playback::entry() { - ESP_LOGI(kTag, "beginning playback"); + ESP_LOGI(kTag, "audio output resumed"); sOutput->mode(IAudioOutput::Modes::kOnPlaying); - events::System().Dispatch(PlaybackStarted{}); - events::Ui().Dispatch(PlaybackStarted{}); + PlaybackUpdate event{ + .current_track = sCurrentTrack, + .track_position = currentPositionSeconds(), + .paused = false, + }; + + events::System().Dispatch(event); + events::Ui().Dispatch(event); } void Playback::exit() { - ESP_LOGI(kTag, "finishing playback"); + ESP_LOGI(kTag, "audio output paused"); sOutput->mode(IAudioOutput::Modes::kOnPaused); - // Stash the current volume now, in case it changed during playback, since - // we might be powering off soon. - commitVolume(); - - events::System().Dispatch(PlaybackStopped{}); - events::Ui().Dispatch(PlaybackStopped{}); -} - -void Playback::react(const system_fsm::HasPhonesChanged& ev) { - if (!ev.has_headphones) { - transit(); - } -} - -void Playback::react(const QueueUpdate& ev) { - if (!ev.current_changed) { - return; - } - // Cut the current track immediately. - if (ev.reason == QueueUpdate::Reason::kExplicitUpdate) { - clearDrainBuffer(); - } - auto current_track = sServices->track_queue().current(); - if (!current_track) { - sFileSource->SetPath(); - sCurrentTrack.reset(); - transit(); - return; - } - playTrack(*current_track); -} - -void Playback::react(const PlaybackUpdate& ev) { - ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed, - ev.track->duration); - sLastTrackUpdate = std::make_pair(ev.track->filepath, ev.seconds_elapsed); -} - -void Playback::react(const internal::InputFileOpened& ev) {} - -void Playback::react(const internal::InputFileClosed& ev) {} + PlaybackUpdate event{ + .current_track = sCurrentTrack, + .track_position = currentPositionSeconds(), + .paused = true, + }; -void Playback::react(const internal::InputFileFinished& ev) { - ESP_LOGI(kTag, "finished playing file"); - sLastTrackUpdate.reset(); - sServices->track_queue().finish(); - if (!sServices->track_queue().current()) { - for (int i = 0; i < 20; i++) { - if (xStreamBufferIsEmpty(sDrainBuffer)) { - break; - } - vTaskDelay(pdMS_TO_TICKS(200)); - } - transit(); - } -} - -void Playback::react(const internal::AudioPipelineIdle& ev) { - transit(); + events::System().Dispatch(event); + events::Ui().Dispatch(event); } } // namespace states diff --git a/src/audio/include/audio_converter.hpp b/src/audio/include/audio_converter.hpp index c2ebde60..dcd068b5 100644 --- a/src/audio/include/audio_converter.hpp +++ b/src/audio/include/audio_converter.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include #include @@ -40,6 +41,8 @@ class SampleConverter { auto SetTargetFormat(const IAudioOutput::Format& format) -> void; auto HandleSamples(cpp::span, bool) -> size_t; + auto SendToSink(cpp::span) -> void; + struct Args { IAudioOutput::Format format; size_t samples_available; @@ -59,6 +62,8 @@ class SampleConverter { IAudioOutput::Format source_format_; IAudioOutput::Format target_format_; size_t leftover_bytes_; + + uint32_t samples_sunk_; }; } // namespace audio diff --git a/src/audio/include/audio_decoder.hpp b/src/audio/include/audio_decoder.hpp index b8aac710..89f0f43c 100644 --- a/src/audio/include/audio_decoder.hpp +++ b/src/audio/include/audio_decoder.hpp @@ -19,25 +19,6 @@ namespace audio { -/* - * Sample-based timer for the current elapsed playback time. - */ -class Timer { - public: - Timer(std::shared_ptr, const codecs::ICodec::OutputFormat& format, uint32_t current_seconds = 0); - - auto AddSamples(std::size_t) -> void; - - private: - std::shared_ptr track_; - - uint32_t current_seconds_; - uint32_t current_sample_in_second_; - uint32_t samples_per_second_; - - uint32_t total_duration_seconds_; -}; - /* * Handle to a persistent task that takes bytes from the given source, decodes * them into sample::Sample (normalised to 16 bit signed PCM), and then @@ -65,7 +46,6 @@ class Decoder { std::shared_ptr stream_; std::unique_ptr codec_; - std::unique_ptr timer_; std::optional current_format_; std::optional current_sink_format_; diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp index a8533646..9af30467 100644 --- a/src/audio/include/audio_events.hpp +++ b/src/audio/include/audio_events.hpp @@ -9,8 +9,10 @@ #include #include #include +#include #include +#include "audio_sink.hpp" #include "tinyfsm.hpp" #include "track.hpp" @@ -18,24 +20,80 @@ namespace audio { -struct Track { +/* + * Struct encapsulating information about the decoder's current track. + */ +struct TrackInfo { + /* + * Audio tags extracted from the file. May be absent for files without any + * parseable tags. + */ std::shared_ptr tags; - std::shared_ptr db_info; - uint32_t duration; - uint32_t bitrate_kbps; + /* + * URI that the current track was retrieved from. This is currently always a + * file path on the SD card. + */ + std::string uri; + + /* + * The length of this track in seconds. This is either retrieved from the + * track's tags, or sometimes computed. It may therefore sometimes be + * inaccurate or missing. + */ + std::optional duration; + + /* The offset in seconds that this file's decoding started from. */ + std::optional start_offset; + + /* The approximate bitrate of this track in its original encoded form. */ + std::optional bitrate_kbps; + + /* The encoded format of the this track. */ codecs::StreamType encoding; - std::string filepath; }; -struct PlaybackStarted : tinyfsm::Event {}; - +/* + * Event emitted by the audio FSM when the state of the audio pipeline has + * changed. This is usually once per second while a track is playing, plus one + * event each when a track starts or finishes. + */ struct PlaybackUpdate : tinyfsm::Event { - uint32_t seconds_elapsed; - std::shared_ptr track; + /* + * The track that is currently being decoded by the audio pipeline. May be + * absent if there is no current track. + */ + std::shared_ptr current_track; + + /* + * How long the current track has been playing for, in seconds. Will always + * be present if current_track is present. + */ + std::optional track_position; + + /* Whether or not the current track is currently being output to a sink. */ + bool paused; +}; + +/* + * Sets a new track to be decoded by the audio pipeline, replacing any + * currently playing track. + */ +struct SetTrack : tinyfsm::Event { + std::variant new_track; + std::optional seek_to_second; + + enum Transition { + kHardCut, + kGapless, + // TODO: kCrossFade + }; + Transition transition; }; -struct PlaybackStopped : tinyfsm::Event {}; +struct TogglePlayPause : tinyfsm::Event { + std::optional set_to; +}; struct QueueUpdate : tinyfsm::Event { bool current_changed; @@ -49,15 +107,6 @@ struct QueueUpdate : tinyfsm::Event { Reason reason; }; -struct PlayFile : tinyfsm::Event { - std::string filename; -}; - -struct SeekFile : tinyfsm::Event { - uint32_t offset; - std::string filename; -}; - struct StepUpVolume : tinyfsm::Event {}; struct StepDownVolume : tinyfsm::Event {}; struct SetVolume : tinyfsm::Event { @@ -83,17 +132,26 @@ struct SetVolumeLimit : tinyfsm::Event { int limit_db; }; -struct TogglePlayPause : tinyfsm::Event {}; - struct OutputModeChanged : tinyfsm::Event {}; namespace internal { -struct InputFileOpened : tinyfsm::Event {}; -struct InputFileClosed : tinyfsm::Event {}; -struct InputFileFinished : tinyfsm::Event {}; +struct DecoderOpened : tinyfsm::Event { + std::shared_ptr track; +}; + +struct DecoderClosed : tinyfsm::Event {}; + +struct DecoderError : tinyfsm::Event {}; -struct AudioPipelineIdle : tinyfsm::Event {}; +struct ConverterConfigurationChanged : tinyfsm::Event { + IAudioOutput::Format src_format; + IAudioOutput::Format dst_format; +}; + +struct ConverterProgress : tinyfsm::Event { + uint32_t samples_sunk; +}; } // namespace internal diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp index 13e241be..62bb4786 100644 --- a/src/audio/include/audio_fsm.hpp +++ b/src/audio/include/audio_fsm.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -41,6 +42,17 @@ class AudioState : public tinyfsm::Fsm { /* Fallback event handler. Does nothing. */ void react(const tinyfsm::Event& ev) {} + void react(const QueueUpdate&); + void react(const SetTrack&); + void react(const TogglePlayPause&); + + void react(const internal::DecoderOpened&); + void react(const internal::DecoderClosed&); + void react(const internal::DecoderError&); + + void react(const internal::ConverterConfigurationChanged&); + void react(const internal::ConverterProgress&); + void react(const StepUpVolume&); void react(const StepDownVolume&); virtual void react(const system_fsm::HasPhonesChanged&); @@ -56,17 +68,6 @@ class AudioState : public tinyfsm::Fsm { virtual void react(const system_fsm::StorageMounted&) {} virtual void react(const system_fsm::BluetoothEvent&); - virtual void react(const PlayFile&) {} - virtual void react(const SeekFile&) {} - virtual void react(const QueueUpdate&) {} - virtual void react(const PlaybackUpdate&) {} - void react(const TogglePlayPause&); - - virtual void react(const internal::InputFileOpened&) {} - virtual void react(const internal::InputFileClosed&) {} - virtual void react(const internal::InputFileFinished&) {} - virtual void react(const internal::AudioPipelineIdle&) {} - protected: auto clearDrainBuffer() -> void; auto playTrack(database::TrackId id) -> void; @@ -83,10 +84,17 @@ class AudioState : public tinyfsm::Fsm { static StreamBufferHandle_t sDrainBuffer; - static std::optional sCurrentTrack; + static std::shared_ptr sCurrentTrack; + static uint64_t sCurrentSamples; + static std::optional sCurrentFormat; - auto readyToPlay() -> bool; - static bool sIsPlaybackAllowed; + static std::shared_ptr sNextTrack; + static uint64_t sNextTrackCueSamples; + + static bool sIsResampling; + static bool sIsPaused; + + auto currentPositionSeconds() -> std::optional; }; namespace states { @@ -94,7 +102,6 @@ namespace states { class Uninitialised : public AudioState { public: void react(const system_fsm::BootComplete&) override; - void react(const system_fsm::BluetoothEvent&) override{}; using AudioState::react; @@ -102,10 +109,6 @@ class Uninitialised : public AudioState { class Standby : public AudioState { public: - void react(const PlayFile&) override; - void react(const SeekFile&) override; - void react(const internal::InputFileOpened&) override; - void react(const QueueUpdate&) override; void react(const system_fsm::KeyLockChanged&) override; void react(const system_fsm::StorageMounted&) override; @@ -117,18 +120,6 @@ class Playback : public AudioState { void entry() override; void exit() override; - void react(const system_fsm::HasPhonesChanged&) override; - - void react(const PlayFile&) override; - void react(const SeekFile&) override; - void react(const QueueUpdate&) override; - void react(const PlaybackUpdate&) override; - - void react(const internal::InputFileOpened&) override; - void react(const internal::InputFileClosed&) override; - void react(const internal::InputFileFinished&) override; - void react(const internal::AudioPipelineIdle&) override; - using AudioState::react; }; diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp index a3f4c815..dbe283c4 100644 --- a/src/audio/track_queue.cpp +++ b/src/audio/track_queue.cpp @@ -136,7 +136,7 @@ auto TrackQueue::insert(Item i, size_t index) -> void { { const std::shared_lock lock(mutex_); was_queue_empty = pos_ == tracks_.size(); - current_changed = pos_ == was_queue_empty || index == pos_; + current_changed = was_queue_empty || index == pos_; } auto update_shuffler = [=, this]() { diff --git a/src/lua/include/property.hpp b/src/lua/include/property.hpp index 7d160fba..f19fdeec 100644 --- a/src/lua/include/property.hpp +++ b/src/lua/include/property.hpp @@ -23,7 +23,7 @@ using LuaValue = std::variant>; diff --git a/src/lua/property.cpp b/src/lua/property.cpp index f721f9ce..200f4d5c 100644 --- a/src/lua/property.cpp +++ b/src/lua/property.cpp @@ -221,7 +221,7 @@ static auto pushTagValue(lua_State* L, const database::TagValue& val) -> void { val); } -static void pushTrack(lua_State* L, const audio::Track& track) { +static void pushTrack(lua_State* L, const audio::TrackInfo& track) { lua_newtable(L); for (const auto& tag : track.tags->allPresent()) { @@ -229,19 +229,18 @@ static void pushTrack(lua_State* L, const audio::Track& track) { pushTagValue(L, track.tags->get(tag)); lua_settable(L, -3); } - if (track.db_info) { - lua_pushliteral(L, "id"); - lua_pushinteger(L, track.db_info->id); + + if (track.duration) { + lua_pushliteral(L, "duration"); + lua_pushinteger(L, track.duration.value()); lua_settable(L, -3); } - lua_pushliteral(L, "duration"); - lua_pushinteger(L, track.duration); - lua_settable(L, -3); - - lua_pushliteral(L, "bitrate_kbps"); - lua_pushinteger(L, track.bitrate_kbps); - lua_settable(L, -3); + if (track.bitrate_kbps) { + lua_pushliteral(L, "bitrate_kbps"); + lua_pushinteger(L, track.bitrate_kbps.value()); + lua_settable(L, -3); + } lua_pushliteral(L, "encoding"); lua_pushstring(L, codecs::StreamTypeToString(track.encoding).c_str()); @@ -289,7 +288,7 @@ auto Property::PushValue(lua_State& s) -> int { lua_pushboolean(&s, arg); } else if constexpr (std::is_same_v) { lua_pushstring(&s, arg.c_str()); - } else if constexpr (std::is_same_v) { + } else if constexpr (std::is_same_v) { pushTrack(&s, arg); } else if constexpr (std::is_same_v) { pushDevice(&s, arg); diff --git a/src/system_fsm/include/system_fsm.hpp b/src/system_fsm/include/system_fsm.hpp index cc60e43b..a129829e 100644 --- a/src/system_fsm/include/system_fsm.hpp +++ b/src/system_fsm/include/system_fsm.hpp @@ -63,7 +63,7 @@ class SystemState : public tinyfsm::Fsm { virtual void react(const SdDetectChanged&) {} virtual void react(const SamdUsbMscChanged&) {} virtual void react(const database::event::UpdateFinished&) {} - virtual void react(const audio::PlaybackStopped&) {} + virtual void react(const audio::PlaybackUpdate&) {} virtual void react(const internal::IdleTimeout&) {} virtual void react(const internal::UnmountTimeout&) {} @@ -101,7 +101,7 @@ class Running : public SystemState { void react(const KeyLockChanged&) override; void react(const SdDetectChanged&) override; - void react(const audio::PlaybackStopped&) override; + void react(const audio::PlaybackUpdate&) override; void react(const database::event::UpdateFinished&) override; void react(const SamdUsbMscChanged&) override; void react(const internal::UnmountTimeout&) override; diff --git a/src/system_fsm/running.cpp b/src/system_fsm/running.cpp index d80809e6..a6ab5d47 100644 --- a/src/system_fsm/running.cpp +++ b/src/system_fsm/running.cpp @@ -56,7 +56,7 @@ void Running::react(const KeyLockChanged& ev) { checkIdle(); } -void Running::react(const audio::PlaybackStopped& ev) { +void Running::react(const audio::PlaybackUpdate& ev) { checkIdle(); } diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index f7fde1dd..5e1cc487 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -57,8 +57,6 @@ class UiState : public tinyfsm::Fsm { virtual void react(const system_fsm::StorageMounted&) {} void react(const system_fsm::BatteryStateChanged&); - void react(const audio::PlaybackStarted&); - void react(const audio::PlaybackStopped&); void react(const audio::PlaybackUpdate&); void react(const audio::QueueUpdate&); diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index a913a339..42c6a99c 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -114,14 +114,11 @@ lua::Property UiState::sBluetoothDevices{ lua::Property UiState::sPlaybackPlaying{ false, [](const lua::LuaValue& val) { - bool current_val = std::get(sPlaybackPlaying.Get()); if (!std::holds_alternative(val)) { return false; } bool new_val = std::get(val); - if (current_val != new_val) { - events::Audio().Dispatch(audio::TogglePlayPause{}); - } + events::Audio().Dispatch(audio::TogglePlayPause{.set_to = new_val}); return true; }}; @@ -135,12 +132,13 @@ lua::Property UiState::sPlaybackPosition{ int new_val = std::get(val); if (current_val != new_val) { auto track = sPlaybackTrack.Get(); - if (!std::holds_alternative(track)) { + if (!std::holds_alternative(track)) { return false; } - events::Audio().Dispatch(audio::SeekFile{ - .offset = (uint32_t)new_val, - .filename = std::get(track).filepath}); + events::Audio().Dispatch(audio::SetTrack{ + .new_track = std::get(track).uri, + .seek_to_second = (uint32_t)new_val, + }); } return true; }}; @@ -393,17 +391,10 @@ void UiState::react(const audio::QueueUpdate&) { sQueueReplay.Update(queue.replay()); } -void UiState::react(const audio::PlaybackStarted& ev) { - sPlaybackPlaying.Update(true); -} - void UiState::react(const audio::PlaybackUpdate& ev) { - sPlaybackTrack.Update(*ev.track); - sPlaybackPosition.Update(static_cast(ev.seconds_elapsed)); -} - -void UiState::react(const audio::PlaybackStopped&) { - sPlaybackPlaying.Update(false); + sPlaybackTrack.Update(*ev.current_track); + sPlaybackPlaying.Update(!ev.paused); + sPlaybackPosition.Update(static_cast(ev.track_position.value_or(0))); } void UiState::react(const audio::VolumeChanged& ev) {