From 4cec85af2d779ea8f6e3b46dfbea61ef5b0419f8 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 26 Mar 2024 16:45:20 +1100 Subject: [PATCH] implement handling of stream/playback ending --- src/audio/audio_converter.cpp | 1 + src/audio/audio_fsm.cpp | 119 ++++++++++++++++++++++---------- src/audio/i2s_audio_output.cpp | 3 - src/audio/include/audio_fsm.hpp | 6 +- 4 files changed, 88 insertions(+), 41 deletions(-) diff --git a/src/audio/audio_converter.cpp b/src/audio/audio_converter.cpp index ebbd405f..015be6a3 100644 --- a/src/audio/audio_converter.cpp +++ b/src/audio/audio_converter.cpp @@ -241,6 +241,7 @@ auto SampleConverter::handleEndStream() -> void { }); samples_sunk_ = 0; } + leftover_bytes_ = 0; events::Audio().Dispatch(internal::StreamEnded{}); } 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(); diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp index cd61d97f..3fb99159 100644 --- a/src/audio/i2s_audio_output.cpp +++ b/src/audio/i2s_audio_output.cpp @@ -152,9 +152,6 @@ auto I2SAudioOutput::Configure(const Format& fmt) -> void { return; } - ESP_LOGI(kTag, "incoming audio stream: %u ch %u bpp @ %lu Hz", - fmt.num_channels, fmt.bits_per_sample, fmt.sample_rate); - drivers::I2SDac::Channels ch; switch (fmt.num_channels) { case 1: diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp index c00813ac..60afb321 100644 --- a/src/audio/include/audio_fsm.hpp +++ b/src/audio/include/audio_fsm.hpp @@ -67,6 +67,8 @@ class AudioState : public tinyfsm::Fsm { protected: auto clearDrainBuffer() -> void; + auto awaitEmptyDrainBuffer() -> void; + auto playTrack(database::TrackId id) -> void; auto commitVolume() -> void; @@ -83,10 +85,12 @@ class AudioState : public tinyfsm::Fsm { static std::shared_ptr sCurrentTrack; static uint64_t sCurrentSamples; - static std::optional sCurrentFormat; + static std::optional sDrainFormat; + static bool sCurrentTrackIsFromQueue; static std::shared_ptr sNextTrack; static uint64_t sNextTrackCueSamples; + static bool sNextTrackIsFromQueue; static bool sIsResampling; static bool sIsPaused;