diff options
| author | jacqueline <me@jacqueline.id.au> | 2024-03-26 16:45:20 +1100 |
|---|---|---|
| committer | jacqueline <me@jacqueline.id.au> | 2024-03-26 16:45:20 +1100 |
| commit | 4cec85af2d779ea8f6e3b46dfbea61ef5b0419f8 (patch) | |
| tree | 05ff61516ea62e0dea7017d7801f29f9320ca260 | |
| parent | 078b77d0f796be3c787f62b9b830512e38d3b076 (diff) | |
| download | tangara-fw-4cec85af2d779ea8f6e3b46dfbea61ef5b0419f8.tar.gz | |
implement handling of stream/playback ending
| -rw-r--r-- | src/audio/audio_converter.cpp | 1 | ||||
| -rw-r--r-- | src/audio/audio_fsm.cpp | 119 | ||||
| -rw-r--r-- | src/audio/i2s_audio_output.cpp | 3 | ||||
| -rw-r--r-- | 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<IAudioOutput::Format> AudioState::sDrainFormat; std::shared_ptr<TrackInfo> AudioState::sCurrentTrack; uint64_t AudioState::sCurrentSamples; -std::optional<IAudioOutput::Format> AudioState::sCurrentFormat; +bool AudioState::sCurrentTrackIsFromQueue; std::shared_ptr<TrackInfo> AudioState::sNextTrack; uint64_t AudioState::sNextTrackCueSamples; +bool AudioState::sNextTrackIsFromQueue; bool AudioState::sIsResampling; bool AudioState::sIsPaused = true; auto AudioState::currentPositionSeconds() -> std::optional<uint32_t> { - if (!sCurrentTrack || !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<AudioState>::dispatch(cmd); } void AudioState::react(const SetTrack& ev) { if (ev.transition == SetTrack::Transition::kHardCut) { + sCurrentTrack.reset(); + sCurrentSamples = 0; + sCurrentTrackIsFromQueue = false; clearDrainBuffer(); } + if (std::holds_alternative<std::monostate>(ev.new_track)) { + ESP_LOGI(kTag, "playback finished, awaiting drain"); + sFileSource->SetPath(); + awaitEmptyDrainBuffer(); + sCurrentTrack.reset(); + sDrainFormat.reset(); + sCurrentSamples = 0; + sCurrentTrackIsFromQueue = false; + transit<states::Standby>(); + return; + } + // Move the rest of the work to a background worker, since it may require db // lookups to resolve a track id into a path. auto new_track = ev.new_track; @@ -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<AudioState>::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<states::Playback>(), - }; - - events::System().Dispatch(event); - events::Ui().Dispatch(event); + if (sCurrentTrack) { + PlaybackUpdate event{ + .current_track = sCurrentTrack, + .track_position = currentPositionSeconds(), + .paused = !is_in_state<states::Playback>(), + }; + events::System().Dispatch(event); + events::Ui().Dispatch(event); + } if (sCurrentTrack && !sIsPaused && !is_in_state<states::Playback>()) { ESP_LOGI(kTag, "ready to play!"); @@ -321,6 +355,17 @@ auto AudioState::clearDrainBuffer() -> void { } } +auto AudioState::awaitEmptyDrainBuffer() -> void { + if (is_in_state<states::Playback>()) { + 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<AudioState> { 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<AudioState> { static std::shared_ptr<TrackInfo> sCurrentTrack; static uint64_t sCurrentSamples; - static std::optional<IAudioOutput::Format> sCurrentFormat; + static std::optional<IAudioOutput::Format> sDrainFormat; + static bool sCurrentTrackIsFromQueue; static std::shared_ptr<TrackInfo> sNextTrack; static uint64_t sNextTrackCueSamples; + static bool sNextTrackIsFromQueue; static bool sIsResampling; static bool sIsPaused; |
