summaryrefslogtreecommitdiff
path: root/src/tangara/audio/audio_fsm.cpp
diff options
context:
space:
mode:
authorjacqueline <me@jacqueline.id.au>2024-05-08 16:03:03 +1000
committerjacqueline <me@jacqueline.id.au>2024-05-08 16:03:03 +1000
commit265049c5192cf0ce862c7db7b4745636afb6c17b (patch)
tree6729bc947c6c2d2d196c9e7baa994a35aeea76ee /src/tangara/audio/audio_fsm.cpp
parentb242ba998699208c87dc066158964de0866b61e2 (diff)
downloadtangara-fw-265049c5192cf0ce862c7db7b4745636afb6c17b.tar.gz
Count samples going in and out of the drain buffer
This is a more accurate way of knowing which track is playing when, and also simplifies a lot of fragile logic in audio_fsm
Diffstat (limited to 'src/tangara/audio/audio_fsm.cpp')
-rw-r--r--src/tangara/audio/audio_fsm.cpp261
1 files changed, 92 insertions, 169 deletions
diff --git a/src/tangara/audio/audio_fsm.cpp b/src/tangara/audio/audio_fsm.cpp
index d437cdbf..a7c006f5 100644
--- a/src/tangara/audio/audio_fsm.cpp
+++ b/src/tangara/audio/audio_fsm.cpp
@@ -5,8 +5,8 @@
*/
#include "audio/audio_fsm.hpp"
-#include <stdint.h>
+#include <cstdint>
#include <future>
#include <memory>
#include <variant>
@@ -18,6 +18,7 @@
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
#include "freertos/projdefs.h"
+#include "tinyfsm.hpp"
#include "audio/audio_decoder.hpp"
#include "audio/audio_events.hpp"
@@ -25,6 +26,7 @@
#include "audio/bt_audio_output.hpp"
#include "audio/fatfs_stream_factory.hpp"
#include "audio/i2s_audio_output.hpp"
+#include "audio/stream_cues.hpp"
#include "audio/track_queue.hpp"
#include "database/future_fetcher.hpp"
#include "database/track.hpp"
@@ -38,7 +40,6 @@
#include "sample.hpp"
#include "system_fsm/service_locator.hpp"
#include "system_fsm/system_events.hpp"
-#include "tinyfsm.hpp"
namespace audio {
@@ -47,11 +48,13 @@ namespace audio {
std::shared_ptr<system_fsm::ServiceLocator> AudioState::sServices;
std::shared_ptr<FatfsStreamFactory> AudioState::sStreamFactory;
+
std::unique_ptr<Decoder> AudioState::sDecoder;
std::shared_ptr<SampleProcessor> AudioState::sSampleProcessor;
+
+std::shared_ptr<IAudioOutput> AudioState::sOutput;
std::shared_ptr<I2SAudioOutput> AudioState::sI2SOutput;
std::shared_ptr<BluetoothAudioOutput> AudioState::sBtOutput;
-std::shared_ptr<IAudioOutput> AudioState::sOutput;
// Two seconds of samples for two channels, at a representative sample rate.
constexpr size_t kDrainLatencySamples = 48000 * 2 * 2;
@@ -61,30 +64,33 @@ constexpr size_t kDrainBufferSize =
StreamBufferHandle_t AudioState::sDrainBuffer;
std::optional<IAudioOutput::Format> AudioState::sDrainFormat;
-std::shared_ptr<TrackInfo> AudioState::sCurrentTrack;
-uint64_t AudioState::sCurrentSamples;
-bool AudioState::sCurrentTrackIsFromQueue;
+StreamCues AudioState::sStreamCues;
-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 || !sDrainFormat) {
- return {};
+auto AudioState::emitPlaybackUpdate(bool paused) -> void {
+ std::optional<uint32_t> position;
+ auto current = sStreamCues.current();
+ if (current.first && sDrainFormat) {
+ position = (current.second /
+ (sDrainFormat->num_channels * sDrainFormat->sample_rate)) +
+ current.first->start_offset.value_or(0);
}
- return sCurrentSamples /
- (sDrainFormat->num_channels * sDrainFormat->sample_rate);
+
+ PlaybackUpdate event{
+ .current_track = current.first,
+ .track_position = position,
+ .paused = paused,
+ };
+
+ events::System().Dispatch(event);
+ events::Ui().Dispatch(event);
}
void AudioState::react(const QueueUpdate& ev) {
SetTrack cmd{
.new_track = std::monostate{},
.seek_to_second = {},
- .transition = SetTrack::Transition::kHardCut,
};
auto current = sServices->track_queue().current();
@@ -97,20 +103,13 @@ void AudioState::react(const QueueUpdate& ev) {
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:
if (!ev.current_changed) {
cmd.new_track = std::monostate{};
- } else {
- sNextTrackIsFromQueue = true;
}
- cmd.transition = SetTrack::Transition::kGapless;
break;
case QueueUpdate::kDeserialised:
default:
@@ -123,32 +122,9 @@ void AudioState::react(const QueueUpdate& ev) {
}
void AudioState::react(const SetTrack& ev) {
- // Remember the current track if there is one, since we need to preserve some
- // of the state if it turns out this SetTrack event corresponds to seeking
- // within the current track.
- std::string prev_uri;
- bool prev_from_queue = false;
- if (sCurrentTrack) {
- prev_uri = sCurrentTrack->uri;
- prev_from_queue = sCurrentTrackIsFromQueue;
- }
-
- 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");
sDecoder->open({});
- awaitEmptyDrainBuffer();
- sCurrentTrack.reset();
- sDrainFormat.reset();
- sCurrentSamples = 0;
- sCurrentTrackIsFromQueue = false;
- transit<states::Standby>();
return;
}
@@ -166,81 +142,62 @@ void AudioState::react(const SetTrack& ev) {
sStreamFactory->create(std::get<std::string>(new_track), seek_to);
}
- // This was a seek or replay within the same track; don't forget where
- // the track originally came from.
- // FIXME:
- // sNextTrackIsFromQueue = prev_from_queue;
sDecoder->open(stream);
});
}
void AudioState::react(const TogglePlayPause& ev) {
sIsPaused = !ev.set_to.value_or(sIsPaused);
- if (!sIsPaused && is_in_state<states::Standby>() && sCurrentTrack) {
+ if (!sIsPaused && is_in_state<states::Standby>() &&
+ sStreamCues.current().first) {
transit<states::Playback>();
} else if (sIsPaused && is_in_state<states::Playback>()) {
transit<states::Standby>();
}
}
-void AudioState::react(const internal::StreamStarted& ev) {
- sDrainFormat = ev.dst_format;
- sIsResampling = ev.src_format != ev.dst_format;
-
- sNextTrack = ev.track;
- sNextTrackCueSamples = sCurrentSamples + (kDrainLatencySamples / 2);
-
- ESP_LOGI(kTag, "new stream %s %u ch @ %lu hz (resample=%i)",
- ev.track->uri.c_str(), sDrainFormat->num_channels,
- sDrainFormat->sample_rate, sIsResampling);
-}
-
-void AudioState::react(const internal::StreamEnded&) {
- ESP_LOGI(kTag, "stream ended");
-
- 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::DecodingFinished& ev) {
+ // If we just finished playing whatever's at the front of the queue, then we
+ // need to advanve and start playing the next one ASAP in order to continue
+ // gaplessly.
+ sServices->bg_worker().Dispatch<void>([=]() {
+ auto& queue = sServices->track_queue();
+ auto current = queue.current();
+ if (!current) {
+ return;
+ }
+ auto db = sServices->database().lock();
+ if (!db) {
+ return;
+ }
+ auto path = db->getTrackPath(*current);
+ if (!path) {
+ return;
+ }
+ if (*path == ev.track->uri) {
+ queue.finish();
+ }
+ });
}
-void AudioState::react(const internal::StreamUpdate& ev) {
- 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) *
- (sDrainFormat->num_channels * sDrainFormat->sample_rate);
- sCurrentTrackIsFromQueue = sNextTrackIsFromQueue;
-
- sNextTrack.reset();
- sNextTrackCueSamples = 0;
- sNextTrackIsFromQueue = false;
+void AudioState::react(const internal::StreamStarted& ev) {
+ if (sDrainFormat != ev.sink_format) {
+ sDrainFormat = ev.sink_format;
+ ESP_LOGI(kTag, "sink_format=%u ch @ %lu hz", sDrainFormat->num_channels,
+ sDrainFormat->sample_rate);
}
- if (sCurrentTrack) {
- PlaybackUpdate event{
- .current_track = sCurrentTrack,
- .track_position = currentPositionSeconds(),
- .paused = !is_in_state<states::Playback>(),
- };
- events::System().Dispatch(event);
- events::Ui().Dispatch(event);
- }
+ sStreamCues.addCue(ev.track, ev.cue_at_sample);
- if (sCurrentTrack && !sIsPaused && !is_in_state<states::Playback>()) {
- ESP_LOGI(kTag, "ready to play!");
+ if (!sIsPaused && !is_in_state<states::Playback>()) {
transit<states::Playback>();
}
}
+void AudioState::react(const internal::StreamEnded& ev) {
+ sStreamCues.addCue({}, ev.cue_at_sample);
+}
+
void AudioState::react(const system_fsm::BluetoothEvent& ev) {
if (ev.event != drivers::bluetooth::Event::kConnectionStateChanged) {
return;
@@ -276,14 +233,6 @@ void AudioState::react(const StepDownVolume& ev) {
}
}
-void AudioState::react(const system_fsm::HasPhonesChanged& ev) {
- if (ev.has_headphones) {
- ESP_LOGI(kTag, "headphones in!");
- } else {
- ESP_LOGI(kTag, "headphones out!");
- }
-}
-
void AudioState::react(const SetVolume& ev) {
if (ev.db.has_value()) {
if (sOutput->SetVolumeDb(ev.db.value())) {
@@ -354,43 +303,6 @@ 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.
- sDecoder->open({});
-
- 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::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();
@@ -455,7 +367,7 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) {
.left_bias = nvs.AmpLeftBias(),
});
- sSampleProcessor.reset(new SampleProcessor());
+ sSampleProcessor.reset(new SampleProcessor(sDrainBuffer));
sSampleProcessor->SetOutput(sOutput);
sDecoder.reset(Decoder::Start(sSampleProcessor));
@@ -470,7 +382,8 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) {
if (!ev.locking) {
return;
}
- sServices->bg_worker().Dispatch<void>([this]() {
+ auto current = sStreamCues.current();
+ sServices->bg_worker().Dispatch<void>([=]() {
auto db = sServices->database().lock();
if (!db) {
return;
@@ -483,10 +396,13 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) {
}
db->put(kQueueKey, queue.serialise());
- if (sCurrentTrack) {
+ if (current.first && sDrainFormat) {
+ uint32_t seconds = (current.second / (sDrainFormat->num_channels *
+ sDrainFormat->sample_rate)) +
+ current.first->start_offset.value_or(0);
cppbor::Array current_track{
- cppbor::Tstr{sCurrentTrack->uri},
- cppbor::Uint{currentPositionSeconds().value_or(0)},
+ cppbor::Tstr{current.first->uri},
+ cppbor::Uint{seconds},
};
db->put(kCurrentFileKey, current_track.toString());
}
@@ -521,7 +437,6 @@ void Standby::react(const system_fsm::SdStateChanged& ev) {
events::Audio().Dispatch(SetTrack{
.new_track = filename,
.seek_to_second = pos,
- .transition = SetTrack::Transition::kHardCut,
});
}
}
@@ -537,32 +452,29 @@ void Standby::react(const system_fsm::SdStateChanged& ev) {
});
}
+static TimerHandle_t sHeartbeatTimer;
+
+static void heartbeat(TimerHandle_t) {
+ events::Audio().Dispatch(internal::StreamHeartbeat{});
+}
+
void Playback::entry() {
ESP_LOGI(kTag, "audio output resumed");
sOutput->mode(IAudioOutput::Modes::kOnPlaying);
+ emitPlaybackUpdate(false);
- PlaybackUpdate event{
- .current_track = sCurrentTrack,
- .track_position = currentPositionSeconds(),
- .paused = false,
- };
-
- events::System().Dispatch(event);
- events::Ui().Dispatch(event);
+ if (!sHeartbeatTimer) {
+ sHeartbeatTimer =
+ xTimerCreate("stream", pdMS_TO_TICKS(250), true, NULL, heartbeat);
+ }
+ xTimerStart(sHeartbeatTimer, portMAX_DELAY);
}
void Playback::exit() {
ESP_LOGI(kTag, "audio output paused");
+ xTimerStop(sHeartbeatTimer, portMAX_DELAY);
sOutput->mode(IAudioOutput::Modes::kOnPaused);
-
- PlaybackUpdate event{
- .current_track = sCurrentTrack,
- .track_position = currentPositionSeconds(),
- .paused = true,
- };
-
- events::System().Dispatch(event);
- events::Ui().Dispatch(event);
+ emitPlaybackUpdate(true);
}
void Playback::react(const system_fsm::SdStateChanged& ev) {
@@ -571,6 +483,17 @@ void Playback::react(const system_fsm::SdStateChanged& ev) {
}
}
+void Playback::react(const internal::StreamHeartbeat& ev) {
+ sStreamCues.update(sOutput->samplesUsed());
+ auto current = sStreamCues.current();
+
+ if (!current.first) {
+ transit<Standby>();
+ } else {
+ emitPlaybackUpdate(false);
+ }
+}
+
} // namespace states
} // namespace audio