From 62f6179abe24339c2e5b7350528afbcad4c52067 Mon Sep 17 00:00:00 2001 From: ailurux Date: Thu, 15 Feb 2024 16:12:07 +1100 Subject: Added offset for track seeking, wav impl. only rn --- src/audio/audio_fsm.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'src/audio/audio_fsm.cpp') diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index ba6e5ffe..c67cfc7a 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -244,11 +244,19 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { } void Standby::react(const PlayFile& ev) { - sFileSource->SetPath(ev.filename); + sFileSource->SetPath(ev.filename, 10); } void Playback::react(const PlayFile& ev) { - sFileSource->SetPath(ev.filename); + sFileSource->SetPath(ev.filename, 15); +} + +void Standby::react(const SeekFile& ev) { + sFileSource->SetPath(ev.filename, ev.offset); +} + +void Playback::react(const SeekFile& ev) { + sFileSource->SetPath(ev.filename, ev.offset); } void Standby::react(const internal::InputFileOpened& ev) { -- cgit v1.2.3 From a49d754da6c293445be16ac643d10849c01ea96b Mon Sep 17 00:00:00 2001 From: ailurux Date: Fri, 16 Feb 2024 10:57:47 +1100 Subject: Seeking working with hardcoded event, wav only --- src/audio/audio_fsm.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/audio/audio_fsm.cpp') diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index c67cfc7a..75e3c24a 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -244,11 +244,13 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { } void Standby::react(const PlayFile& ev) { + sCurrentTrack = 0; + sIsPlaybackAllowed = true; sFileSource->SetPath(ev.filename, 10); } void Playback::react(const PlayFile& ev) { - sFileSource->SetPath(ev.filename, 15); + sFileSource->SetPath(ev.filename, 10); } void Standby::react(const SeekFile& ev) { -- cgit v1.2.3 From 665679b8854d34c13d8eb92167aa8a4691619d8b Mon Sep 17 00:00:00 2001 From: ailurux Date: Fri, 16 Feb 2024 12:55:11 +1100 Subject: WIP: seeking in lua example --- src/audio/audio_fsm.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'src/audio/audio_fsm.cpp') diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 75e3c24a..0e213b6e 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -246,14 +246,16 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { void Standby::react(const PlayFile& ev) { sCurrentTrack = 0; sIsPlaybackAllowed = true; - sFileSource->SetPath(ev.filename, 10); + sFileSource->SetPath(ev.filename); } void Playback::react(const PlayFile& ev) { - sFileSource->SetPath(ev.filename, 10); + sFileSource->SetPath(ev.filename); } void Standby::react(const SeekFile& ev) { + sCurrentTrack = 0; + sIsPlaybackAllowed = true; sFileSource->SetPath(ev.filename, ev.offset); } -- cgit v1.2.3 From c60bb9ee42eea2c88ef90228274bd28350a87ae4 Mon Sep 17 00:00:00 2001 From: ailurux Date: Fri, 16 Feb 2024 16:19:12 +1100 Subject: Fix issue with seeking whilst paused --- src/audio/audio_fsm.cpp | 2 -- 1 file changed, 2 deletions(-) (limited to 'src/audio/audio_fsm.cpp') diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 0e213b6e..bb7d33dc 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -254,8 +254,6 @@ void Playback::react(const PlayFile& ev) { } void Standby::react(const SeekFile& ev) { - sCurrentTrack = 0; - sIsPlaybackAllowed = true; sFileSource->SetPath(ev.filename, ev.offset); } -- cgit v1.2.3 From 173b09b0151ae765b1a8e69dfb60d14d502801f6 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 29 Feb 2024 15:47:21 +1100 Subject: Clear the drain buffer when skipping between tracks --- src/audio/audio_fsm.cpp | 53 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 11 deletions(-) (limited to 'src/audio/audio_fsm.cpp') diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index ea0315eb..08a0941a 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -51,6 +51,10 @@ std::shared_ptr AudioState::sI2SOutput; std::shared_ptr AudioState::sBtOutput; std::shared_ptr AudioState::sOutput; +// Two seconds of samples for two channels, at a representative sample rate. +constexpr size_t kDrainBufferSize = sizeof(sample::Sample) * 48000 * 4; +StreamBufferHandle_t AudioState::sDrainBuffer; + std::optional AudioState::sCurrentTrack; bool AudioState::sIsPlaybackAllowed; @@ -129,7 +133,7 @@ void AudioState::react(const SetVolumeBalance& ev) { void AudioState::react(const OutputModeChanged& ev) { ESP_LOGI(kTag, "output mode changed"); auto new_mode = sServices->nvs().OutputMode(); - sOutput->SetMode(IAudioOutput::Modes::kOff); + sOutput->mode(IAudioOutput::Modes::kOff); switch (new_mode) { case drivers::NvsStorage::Output::kBluetooth: sOutput = sBtOutput; @@ -138,7 +142,7 @@ void AudioState::react(const OutputModeChanged& ev) { sOutput = sI2SOutput; break; } - sOutput->SetMode(IAudioOutput::Modes::kOnPaused); + sOutput->mode(IAudioOutput::Modes::kOnPaused); sSampleConverter->SetOutput(sOutput); // Bluetooth volume isn't 'changed' until we've connected to a device. @@ -150,6 +154,32 @@ void AudioState::react(const OutputModeChanged& ev) { } } +auto AudioState::clearDrainBuffer() -> void { + // Tell the decoder to stop adding new samples. This might not take effect + // immediately, since the decoder might currently be stuck waiting for space + // to become available in the drain buffer. + sFileSource->SetPath(); + + auto mode = sOutput->mode(); + if (mode == IAudioOutput::Modes::kOnPlaying) { + // If we're currently playing, then the drain buffer will be actively + // draining on its own. Just keep trying to reset until it works. + while (xStreamBufferReset(sDrainBuffer) != pdPASS) { + } + } else { + // If we're not currently playing, then we need to actively pull samples + // out of the drain buffer to unblock the decoder. + while (!xStreamBufferIsEmpty(sDrainBuffer)) { + // Read a little to unblock the decoder. + uint8_t drain[2048]; + xStreamBufferReceive(sDrainBuffer, drain, sizeof(drain), 0); + + // Try to quickly discard the rest. + xStreamBufferReset(sDrainBuffer); + } + } +} + auto AudioState::playTrack(database::TrackId id) -> void { sCurrentTrack = id; sServices->bg_worker().Dispatch([=]() { @@ -194,10 +224,6 @@ void AudioState::react(const TogglePlayPause& ev) { namespace states { -// Two seconds of samples for two channels, at a representative sample rate. -constexpr size_t kDrainBufferSize = sizeof(sample::Sample) * 48000 * 4; -static StreamBufferHandle_t sDrainBuffer; - void Uninitialised::react(const system_fsm::BootComplete& ev) { sServices = ev.services; @@ -229,7 +255,7 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { } else { sOutput = sBtOutput; } - sOutput->SetMode(IAudioOutput::Modes::kOnPaused); + sOutput->mode(IAudioOutput::Modes::kOnPaused); events::Ui().Dispatch(VolumeLimitChanged{ .new_limit_db = @@ -272,6 +298,7 @@ void Standby::react(const QueueUpdate& ev) { if (!current_track || (sCurrentTrack && (*sCurrentTrack == *current_track))) { return; } + clearDrainBuffer(); playTrack(*current_track); } @@ -315,7 +342,7 @@ void Standby::react(const system_fsm::StorageMounted& ev) { void Playback::entry() { ESP_LOGI(kTag, "beginning playback"); - sOutput->SetMode(IAudioOutput::Modes::kOnPlaying); + sOutput->mode(IAudioOutput::Modes::kOnPlaying); events::System().Dispatch(PlaybackStarted{}); events::Ui().Dispatch(PlaybackStarted{}); @@ -323,10 +350,10 @@ void Playback::entry() { void Playback::exit() { ESP_LOGI(kTag, "finishing playback"); - sOutput->SetMode(IAudioOutput::Modes::kOnPaused); + sOutput->mode(IAudioOutput::Modes::kOnPaused); - // Stash the current volume now, in case it changed during playback, since we - // might be powering off soon. + // Stash the current volume now, in case it changed during playback, since + // we might be powering off soon. commitVolume(); events::System().Dispatch(PlaybackStopped{}); @@ -343,6 +370,10 @@ void Playback::react(const QueueUpdate& ev) { if (!ev.current_changed) { return; } + // Cut the current track immediately. + if (ev.reason == QueueUpdate::Reason::kExplicitUpdate) { + clearDrainBuffer(); + } auto current_track = sServices->track_queue().current(); if (!current_track) { sFileSource->SetPath(); -- cgit v1.2.3 From b2f0e6d3a45083b04e85feccb3f7742a35d6e41f Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 29 Feb 2024 16:30:17 +1100 Subject: Clear the drain buffer also when seeking --- src/audio/audio_fsm.cpp | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/audio/audio_fsm.cpp') diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 50f18452..d4272c3d 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -290,10 +290,12 @@ void Playback::react(const PlayFile& ev) { } void Standby::react(const SeekFile& ev) { + clearDrainBuffer(); sFileSource->SetPath(ev.filename, ev.offset); } void Playback::react(const SeekFile& ev) { + clearDrainBuffer(); sFileSource->SetPath(ev.filename, ev.offset); } -- cgit v1.2.3 From 14552881900bb3ed0e9ed2d4a732e4104b32ccfa Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 6 Mar 2024 13:59:33 +1100 Subject: Restore the previous track position when booting --- src/audio/audio_fsm.cpp | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) (limited to 'src/audio/audio_fsm.cpp') diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index d4272c3d..05c7c216 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -13,6 +13,8 @@ #include "audio_sink.hpp" #include "bluetooth_types.hpp" +#include "cppbor.h" +#include "cppbor_parse.h" #include "esp_heap_caps.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" @@ -58,6 +60,8 @@ StreamBufferHandle_t AudioState::sDrainBuffer; std::optional AudioState::sCurrentTrack; bool AudioState::sIsPlaybackAllowed; +static std::optional> sLastTrackUpdate; + void AudioState::react(const system_fsm::BluetoothEvent& ev) { if (ev.event != drivers::bluetooth::Event::kConnectionStateChanged) { return; @@ -310,11 +314,15 @@ void Standby::react(const QueueUpdate& ev) { if (!current_track || (sCurrentTrack && (*sCurrentTrack == *current_track))) { return; } + if (ev.reason == QueueUpdate::Reason::kDeserialised && sLastTrackUpdate) { + return; + } clearDrainBuffer(); playTrack(*current_track); } static const char kQueueKey[] = "audio:queue"; +static const char kCurrentFileKey[] = "audio:current"; void Standby::react(const system_fsm::KeyLockChanged& ev) { if (!ev.locking) { @@ -332,6 +340,14 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) { return; } db->put(kQueueKey, queue.serialise()); + + if (sLastTrackUpdate) { + cppbor::Array current_track{ + cppbor::Tstr{sLastTrackUpdate->first}, + cppbor::Uint{sLastTrackUpdate->second}, + }; + db->put(kCurrentFileKey, current_track.toString()); + } }); } @@ -341,13 +357,32 @@ void Standby::react(const system_fsm::StorageMounted& ev) { if (!db) { return; } - auto res = db->get(kQueueKey); - if (res) { + + // Restore the currently playing file before restoring the queue. This way, + // we can fall back to restarting the queue's current track if there's any + // issue restoring the current file. + auto current = db->get(kCurrentFileKey); + if (current) { + // Again, ensure we don't boot-loop by trying to play a track that causes + // a crash over and over again. + db->put(kCurrentFileKey, ""); + auto [parsed, unused, err] = cppbor::parse( + reinterpret_cast(current->data()), current->size()); + if (parsed->type() == cppbor::ARRAY) { + std::string filename = parsed->asArray()->get(0)->asTstr()->value(); + uint32_t pos = parsed->asArray()->get(1)->asUint()->value(); + sLastTrackUpdate = std::make_pair(filename, pos); + sFileSource->SetPath(filename, pos); + } + } + + auto queue = db->get(kQueueKey); + if (queue) { // Don't restore the same queue again. This ideally should do nothing, // but guards against bad edge cases where restoring the queue ends up // causing a crash. db->put(kQueueKey, ""); - sServices->track_queue().deserialise(*res); + sServices->track_queue().deserialise(*queue); } }); } @@ -399,6 +434,7 @@ void Playback::react(const QueueUpdate& ev) { void Playback::react(const PlaybackUpdate& ev) { ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed, ev.track->duration); + sLastTrackUpdate = std::make_pair(ev.track->filepath, ev.seconds_elapsed); } void Playback::react(const internal::InputFileOpened& ev) {} @@ -407,6 +443,7 @@ void Playback::react(const internal::InputFileClosed& ev) {} void Playback::react(const internal::InputFileFinished& ev) { ESP_LOGI(kTag, "finished playing file"); + sLastTrackUpdate.reset(); sServices->track_queue().finish(); if (!sServices->track_queue().current()) { for (int i = 0; i < 20; i++) { -- cgit v1.2.3 From 175bfc4e3e9f7aa39e084d3f1625347f1d5711ec Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 25 Mar 2024 17:34:41 +1100 Subject: WIP rewrie audio pipeline+fsm guts for more reliability --- src/audio/audio_fsm.cpp | 315 +++++++++++++++++++++++++++--------------------- 1 file changed, 177 insertions(+), 138 deletions(-) (limited to 'src/audio/audio_fsm.cpp') 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 -- cgit v1.2.3 From 078b77d0f796be3c787f62b9b830512e38d3b076 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 26 Mar 2024 12:12:42 +1100 Subject: pass stream start/update/end events through the whole pipeline --- src/audio/audio_fsm.cpp | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) (limited to 'src/audio/audio_fsm.cpp') diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 7a138cba..a6f4f4d1 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -55,9 +55,9 @@ std::shared_ptr AudioState::sBtOutput; std::shared_ptr AudioState::sOutput; // Two seconds of samples for two channels, at a representative sample rate. -constexpr size_t kDrainLatencySamples = 48000; +constexpr size_t kDrainLatencySamples = 48000 * 2 * 2; constexpr size_t kDrainBufferSize = - sizeof(sample::Sample) * kDrainLatencySamples * 4; + sizeof(sample::Sample) * kDrainLatencySamples; StreamBufferHandle_t AudioState::sDrainBuffer; @@ -151,33 +151,24 @@ void AudioState::react(const TogglePlayPause& ev) { } } -void AudioState::react(const internal::DecoderOpened& ev) { - ESP_LOGI(kTag, "decoder opened %s", ev.track->uri.c_str()); +void AudioState::react(const internal::StreamStarted& ev) { + sCurrentFormat = ev.dst_format; + sIsResampling = ev.src_format != ev.dst_format; sNextTrack = ev.track; sNextTrackCueSamples = sCurrentSamples + kDrainLatencySamples; -} -void AudioState::react(const internal::DecoderClosed&) { - ESP_LOGI(kTag, "decoder closed"); - // FIXME: only when we were playing the current track - sServices->track_queue().finish(); + ESP_LOGI(kTag, "new stream %s %u ch @ %lu hz (resample=%i)", + ev.track->uri.c_str(), sCurrentFormat->num_channels, + sCurrentFormat->sample_rate, sIsResampling); } -void AudioState::react(const internal::DecoderError&) { - ESP_LOGW(kTag, "decoder errored"); +void AudioState::react(const internal::StreamEnded&) { + ESP_LOGI(kTag, "stream ended"); // FIXME: only when we were playing the current track sServices->track_queue().finish(); } -void AudioState::react(const internal::ConverterConfigurationChanged& ev) { - sCurrentFormat = ev.dst_format; - sIsResampling = ev.src_format != ev.dst_format; - ESP_LOGI(kTag, "output format now %u ch @ %lu hz (resample=%i)", - sCurrentFormat->num_channels, sCurrentFormat->sample_rate, - sIsResampling); -} - -void AudioState::react(const internal::ConverterProgress& ev) { +void AudioState::react(const internal::StreamUpdate& ev) { ESP_LOGI(kTag, "sample converter sunk %lu samples", ev.samples_sunk); sCurrentSamples += ev.samples_sunk; -- cgit v1.2.3 From 4cec85af2d779ea8f6e3b46dfbea61ef5b0419f8 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 26 Mar 2024 16:45:20 +1100 Subject: implement handling of stream/playback ending --- src/audio/audio_fsm.cpp | 119 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 82 insertions(+), 37 deletions(-) (limited to 'src/audio/audio_fsm.cpp') diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index a6f4f4d1..07737872 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -60,38 +60,58 @@ constexpr size_t kDrainBufferSize = sizeof(sample::Sample) * kDrainLatencySamples; StreamBufferHandle_t AudioState::sDrainBuffer; +std::optional AudioState::sDrainFormat; std::shared_ptr AudioState::sCurrentTrack; uint64_t AudioState::sCurrentSamples; -std::optional AudioState::sCurrentFormat; +bool AudioState::sCurrentTrackIsFromQueue; std::shared_ptr AudioState::sNextTrack; uint64_t AudioState::sNextTrackCueSamples; +bool AudioState::sNextTrackIsFromQueue; bool AudioState::sIsResampling; bool AudioState::sIsPaused = true; auto AudioState::currentPositionSeconds() -> std::optional { - if (!sCurrentTrack || !sCurrentFormat) { + if (!sCurrentTrack || !sDrainFormat) { return {}; } return sCurrentSamples / - (sCurrentFormat->num_channels * sCurrentFormat->sample_rate); + (sDrainFormat->num_channels * sDrainFormat->sample_rate); } void AudioState::react(const QueueUpdate& ev) { - if (!ev.current_changed && ev.reason != QueueUpdate::kRepeatingLastTrack) { - return; + SetTrack cmd{ + .new_track = std::monostate{}, + .seek_to_second = {}, + .transition = SetTrack::Transition::kHardCut, + }; + + auto current = sServices->track_queue().current(); + if (current) { + cmd.new_track = *current; } - SetTrack::Transition transition; switch (ev.reason) { case QueueUpdate::kExplicitUpdate: - transition = SetTrack::Transition::kHardCut; + if (!ev.current_changed) { + return; + } + sNextTrackIsFromQueue = true; + cmd.transition = SetTrack::Transition::kHardCut; break; case QueueUpdate::kRepeatingLastTrack: + sNextTrackIsFromQueue = true; + cmd.transition = SetTrack::Transition::kGapless; + break; case QueueUpdate::kTrackFinished: - transition = SetTrack::Transition::kGapless; + if (!ev.current_changed) { + cmd.new_track = std::monostate{}; + } else { + sNextTrackIsFromQueue = true; + } + cmd.transition = SetTrack::Transition::kGapless; break; case QueueUpdate::kDeserialised: default: @@ -100,25 +120,29 @@ void AudioState::react(const QueueUpdate& ev) { return; } - SetTrack cmd{ - .new_track = {}, - .seek_to_second = 0, - .transition = transition, - }; - - auto current = sServices->track_queue().current(); - if (current) { - cmd.new_track = *current; - } - tinyfsm::FsmList::dispatch(cmd); } void AudioState::react(const SetTrack& ev) { if (ev.transition == SetTrack::Transition::kHardCut) { + sCurrentTrack.reset(); + sCurrentSamples = 0; + sCurrentTrackIsFromQueue = false; clearDrainBuffer(); } + if (std::holds_alternative(ev.new_track)) { + ESP_LOGI(kTag, "playback finished, awaiting drain"); + sFileSource->SetPath(); + awaitEmptyDrainBuffer(); + sCurrentTrack.reset(); + sDrainFormat.reset(); + sCurrentSamples = 0; + sCurrentTrackIsFromQueue = false; + transit(); + return; + } + // Move the rest of the work to a background worker, since it may require db // lookups to resolve a track id into a path. auto new_track = ev.new_track; @@ -152,46 +176,56 @@ void AudioState::react(const TogglePlayPause& ev) { } void AudioState::react(const internal::StreamStarted& ev) { - sCurrentFormat = ev.dst_format; + sDrainFormat = ev.dst_format; sIsResampling = ev.src_format != ev.dst_format; + sNextTrack = ev.track; - sNextTrackCueSamples = sCurrentSamples + kDrainLatencySamples; + sNextTrackCueSamples = sCurrentSamples + (kDrainLatencySamples / 2); ESP_LOGI(kTag, "new stream %s %u ch @ %lu hz (resample=%i)", - ev.track->uri.c_str(), sCurrentFormat->num_channels, - sCurrentFormat->sample_rate, sIsResampling); + ev.track->uri.c_str(), sDrainFormat->num_channels, + sDrainFormat->sample_rate, sIsResampling); } void AudioState::react(const internal::StreamEnded&) { ESP_LOGI(kTag, "stream ended"); - // FIXME: only when we were playing the current track - sServices->track_queue().finish(); + + if (sCurrentTrackIsFromQueue) { + sServices->track_queue().finish(); + } else { + tinyfsm::FsmList::dispatch(SetTrack{ + .new_track = std::monostate{}, + .seek_to_second = {}, + .transition = SetTrack::Transition::kGapless, + }); + } } void AudioState::react(const internal::StreamUpdate& ev) { - ESP_LOGI(kTag, "sample converter sunk %lu samples", ev.samples_sunk); sCurrentSamples += ev.samples_sunk; if (sNextTrack && sCurrentSamples >= sNextTrackCueSamples) { ESP_LOGI(kTag, "next track is now sinking"); sCurrentTrack = sNextTrack; sCurrentSamples -= sNextTrackCueSamples; - sCurrentSamples += - sNextTrack->start_offset.value_or(0) * - (sCurrentFormat->num_channels * sCurrentFormat->sample_rate); + sCurrentSamples += sNextTrack->start_offset.value_or(0) * + (sDrainFormat->num_channels * sDrainFormat->sample_rate); + sCurrentTrackIsFromQueue = sNextTrackIsFromQueue; sNextTrack.reset(); sNextTrackCueSamples = 0; + sNextTrackIsFromQueue = false; } - PlaybackUpdate event{ - .current_track = sCurrentTrack, - .track_position = currentPositionSeconds(), - .paused = !is_in_state(), - }; - - events::System().Dispatch(event); - events::Ui().Dispatch(event); + if (sCurrentTrack) { + PlaybackUpdate event{ + .current_track = sCurrentTrack, + .track_position = currentPositionSeconds(), + .paused = !is_in_state(), + }; + events::System().Dispatch(event); + events::Ui().Dispatch(event); + } if (sCurrentTrack && !sIsPaused && !is_in_state()) { ESP_LOGI(kTag, "ready to play!"); @@ -321,6 +355,17 @@ auto AudioState::clearDrainBuffer() -> void { } } +auto AudioState::awaitEmptyDrainBuffer() -> void { + if (is_in_state()) { + for (int i = 0; i < 10 && !xStreamBufferIsEmpty(sDrainBuffer); i++) { + vTaskDelay(pdMS_TO_TICKS(250)); + } + } + if (!xStreamBufferIsEmpty(sDrainBuffer)) { + clearDrainBuffer(); + } +} + auto AudioState::commitVolume() -> void { auto mode = sServices->nvs().OutputMode(); auto vol = sOutput->GetVolume(); -- cgit v1.2.3 From 239e6d89507a24c849385f4bfa93ac4ad58e5de5 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 28 Mar 2024 13:30:24 +1100 Subject: bump esp-idf to 5.2.1 --- src/audio/audio_fsm.cpp | 1 - 1 file changed, 1 deletion(-) (limited to 'src/audio/audio_fsm.cpp') diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 07737872..424b0eff 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -31,7 +31,6 @@ #include "future_fetcher.hpp" #include "i2s_audio_output.hpp" #include "i2s_dac.hpp" -#include "idf_additions.h" #include "nvs.hpp" #include "sample.hpp" #include "service_locator.hpp" -- cgit v1.2.3