summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjacqueline <me@jacqueline.id.au>2024-03-26 16:45:20 +1100
committerjacqueline <me@jacqueline.id.au>2024-03-26 16:45:20 +1100
commit4cec85af2d779ea8f6e3b46dfbea61ef5b0419f8 (patch)
tree05ff61516ea62e0dea7017d7801f29f9320ca260
parent078b77d0f796be3c787f62b9b830512e38d3b076 (diff)
downloadtangara-fw-4cec85af2d779ea8f6e3b46dfbea61ef5b0419f8.tar.gz
implement handling of stream/playback ending
-rw-r--r--src/audio/audio_converter.cpp1
-rw-r--r--src/audio/audio_fsm.cpp119
-rw-r--r--src/audio/i2s_audio_output.cpp3
-rw-r--r--src/audio/include/audio_fsm.hpp6
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;