WIP rewrie audio pipeline+fsm guts for more reliability

custom
jacqueline 1 year ago
parent 5c985afd25
commit 175bfc4e3e
  1. 2
      dependencies.lock
  2. 18
      src/app_console/app_console.cpp
  3. 51
      src/audio/audio_converter.cpp
  4. 78
      src/audio/audio_decoder.cpp
  5. 315
      src/audio/audio_fsm.cpp
  6. 5
      src/audio/include/audio_converter.hpp
  7. 20
      src/audio/include/audio_decoder.hpp
  8. 108
      src/audio/include/audio_events.hpp
  9. 53
      src/audio/include/audio_fsm.hpp
  10. 2
      src/audio/track_queue.cpp
  11. 2
      src/lua/include/property.hpp
  12. 23
      src/lua/property.cpp
  13. 4
      src/system_fsm/include/system_fsm.hpp
  14. 2
      src/system_fsm/running.cpp
  15. 2
      src/ui/include/ui_fsm.hpp
  16. 27
      src/ui/ui_fsm.cpp

@ -4,6 +4,6 @@ dependencies:
source: source:
type: idf type: idf
version: 5.1.1 version: 5.1.1
manifest_hash: b9761e0028130d307b778c710e5dd39fb3c942d8084ed429d448d938957fb0e6 manifest_hash: 9e4320e6f25503854c6c93bcbfa9b80f780485bcf066bdbad31a820544492538
target: esp32 target: esp32
version: 1.0.0 version: 1.0.0

@ -53,10 +53,15 @@ namespace console {
std::shared_ptr<system_fsm::ServiceLocator> AppConsole::sServices; std::shared_ptr<system_fsm::ServiceLocator> AppConsole::sServices;
int CmdVersion(int argc, char** argv) { int CmdVersion(int argc, char** argv) {
std::cout << "firmware-version=" << esp_app_get_description()->version << std::endl; std::cout << "firmware-version=" << esp_app_get_description()->version
std::cout << "samd-version=" << AppConsole::sServices->samd().Version() << std::endl; << std::endl;
std::cout << "collation=" << AppConsole::sServices->collator().Describe().value_or("") << std::endl; std::cout << "samd-version=" << AppConsole::sServices->samd().Version()
std::cout << "database-schema=" << uint32_t(database::kCurrentDbVersion) << std::endl; << std::endl;
std::cout << "collation="
<< AppConsole::sServices->collator().Describe().value_or("")
<< std::endl;
std::cout << "database-schema=" << uint32_t(database::kCurrentDbVersion)
<< std::endl;
return 0; return 0;
} }
@ -148,7 +153,7 @@ int CmdPlayFile(int argc, char** argv) {
database::TrackId id = std::atoi(argv[1]); database::TrackId id = std::atoi(argv[1]);
AppConsole::sServices->track_queue().append(id); AppConsole::sServices->track_queue().append(id);
} else { } else {
std::pmr::string path{&memory::kSpiRamResource}; std::string path;
path += '/'; path += '/';
path += argv[1]; path += argv[1];
for (int i = 2; i < argc; i++) { for (int i = 2; i < argc; i++) {
@ -156,8 +161,7 @@ int CmdPlayFile(int argc, char** argv) {
path += argv[i]; path += argv[i];
} }
events::Audio().Dispatch( events::Audio().Dispatch(audio::SetTrack{.new_track = path});
audio::PlayFile{.filename = {path.data(), path.size()}});
} }
return 0; return 0;

@ -5,14 +5,17 @@
*/ */
#include "audio_converter.hpp" #include "audio_converter.hpp"
#include <stdint.h>
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include "audio_events.hpp"
#include "audio_sink.hpp" #include "audio_sink.hpp"
#include "esp_heap_caps.h" #include "esp_heap_caps.h"
#include "esp_log.h" #include "esp_log.h"
#include "event_queue.hpp"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
#include "freertos/projdefs.h" #include "freertos/projdefs.h"
#include "i2s_dac.hpp" #include "i2s_dac.hpp"
@ -35,7 +38,9 @@ SampleConverter::SampleConverter()
resampler_(nullptr), resampler_(nullptr),
source_(xStreamBufferCreateWithCaps(kSourceBufferLength, source_(xStreamBufferCreateWithCaps(kSourceBufferLength,
sizeof(sample::Sample) * 2, sizeof(sample::Sample) * 2,
MALLOC_CAP_DMA)) { MALLOC_CAP_DMA)),
leftover_bytes_(0),
samples_sunk_(0) {
input_buffer_ = { input_buffer_ = {
reinterpret_cast<sample::Sample*>(heap_caps_calloc( reinterpret_cast<sample::Sample*>(heap_caps_calloc(
kSampleBufferLength, sizeof(sample::Sample), MALLOC_CAP_DMA)), kSampleBufferLength, sizeof(sample::Sample), MALLOC_CAP_DMA)),
@ -107,6 +112,19 @@ auto SampleConverter::Main() -> void {
sink_->Configure(new_target); sink_->Configure(new_target);
} }
target_format_ = new_target; target_format_ = new_target;
// Send a final sample count for the previous sample rate.
if (samples_sunk_ > 0) {
events::Audio().Dispatch(internal::ConverterProgress{
.samples_sunk = samples_sunk_,
});
}
samples_sunk_ = 0;
events::Audio().Dispatch(internal::ConverterConfigurationChanged{
.src_format = source_format_,
.dst_format = target_format_,
});
} }
// Loop until we finish reading all the bytes indicated. There might be // Loop until we finish reading all the bytes indicated. There might be
@ -154,9 +172,8 @@ auto SampleConverter::HandleSamples(cpp::span<sample::Sample> input,
if (source_format_ == target_format_) { if (source_format_ == target_format_) {
// The happiest possible case: the input format matches the output // The happiest possible case: the input format matches the output
// format already. // format already.
std::size_t bytes_sent = xStreamBufferSend( SendToSink(input);
sink_->stream(), input.data(), input.size_bytes(), portMAX_DELAY); return input.size();
return bytes_sent / sizeof(sample::Sample);
} }
size_t samples_used = 0; size_t samples_used = 0;
@ -186,16 +203,26 @@ auto SampleConverter::HandleSamples(cpp::span<sample::Sample> input,
samples_used = input.size(); samples_used = input.size();
} }
size_t bytes_sent = 0; SendToSink(output_source);
size_t bytes_to_send = output_source.size_bytes();
while (bytes_sent < bytes_to_send) {
bytes_sent += xStreamBufferSend(
sink_->stream(),
reinterpret_cast<std::byte*>(output_source.data()) + bytes_sent,
bytes_to_send - bytes_sent, portMAX_DELAY);
}
} }
return samples_used; return samples_used;
} }
auto SampleConverter::SendToSink(cpp::span<sample::Sample> samples) -> void {
// Update the number of samples sunk so far *before* actually sinking them,
// since writing to the stream buffer will block when the buffer gets full.
samples_sunk_ += samples.size();
if (samples_sunk_ >=
target_format_.sample_rate * target_format_.num_channels) {
events::Audio().Dispatch(internal::ConverterProgress{
.samples_sunk = samples_sunk_,
});
samples_sunk_ = 0;
}
xStreamBufferSend(sink_->stream(),
reinterpret_cast<std::byte*>(samples.data()),
samples.size_bytes(), portMAX_DELAY);
}
} // namespace audio } // namespace audio

@ -5,6 +5,7 @@
*/ */
#include "audio_decoder.hpp" #include "audio_decoder.hpp"
#include <stdint.h>
#include <cstdint> #include <cstdint>
#include <cstdlib> #include <cstdlib>
@ -50,39 +51,6 @@ namespace audio {
static constexpr std::size_t kCodecBufferLength = static constexpr std::size_t kCodecBufferLength =
drivers::kI2SBufferLengthFrames * sizeof(sample::Sample); drivers::kI2SBufferLengthFrames * sizeof(sample::Sample);
Timer::Timer(std::shared_ptr<Track> t,
const codecs::ICodec::OutputFormat& format,
uint32_t current_seconds)
: track_(t),
current_seconds_(current_seconds),
current_sample_in_second_(0),
samples_per_second_(format.sample_rate_hz * format.num_channels),
total_duration_seconds_(format.total_samples.value_or(0) /
format.num_channels / format.sample_rate_hz) {
track_->duration = total_duration_seconds_;
}
auto Timer::AddSamples(std::size_t samples) -> void {
bool incremented = false;
current_sample_in_second_ += samples;
while (current_sample_in_second_ >= samples_per_second_) {
current_seconds_++;
current_sample_in_second_ -= samples_per_second_;
incremented = true;
}
if (incremented) {
if (total_duration_seconds_ < current_seconds_) {
total_duration_seconds_ = current_seconds_;
track_->duration = total_duration_seconds_;
}
PlaybackUpdate ev{.seconds_elapsed = current_seconds_, .track = track_};
events::Audio().Dispatch(ev);
events::Ui().Dispatch(ev);
}
}
auto Decoder::Start(std::shared_ptr<IAudioSource> source, auto Decoder::Start(std::shared_ptr<IAudioSource> source,
std::shared_ptr<SampleConverter> sink) -> Decoder* { std::shared_ptr<SampleConverter> sink) -> Decoder* {
Decoder* task = new Decoder(source, sink); Decoder* task = new Decoder(source, sink);
@ -92,11 +60,7 @@ auto Decoder::Start(std::shared_ptr<IAudioSource> source,
Decoder::Decoder(std::shared_ptr<IAudioSource> source, Decoder::Decoder(std::shared_ptr<IAudioSource> source,
std::shared_ptr<SampleConverter> mixer) std::shared_ptr<SampleConverter> mixer)
: source_(source), : source_(source), converter_(mixer), codec_(), current_format_() {
converter_(mixer),
codec_(),
timer_(),
current_format_() {
ESP_LOGI(kTag, "allocating codec buffer, %u KiB", kCodecBufferLength / 1024); ESP_LOGI(kTag, "allocating codec buffer, %u KiB", kCodecBufferLength / 1024);
codec_buffer_ = { codec_buffer_ = {
reinterpret_cast<sample::Sample*>(heap_caps_calloc( reinterpret_cast<sample::Sample*>(heap_caps_calloc(
@ -117,7 +81,6 @@ void Decoder::Main() {
} }
if (ContinueDecoding()) { if (ContinueDecoding()) {
events::Audio().Dispatch(internal::InputFileFinished{});
stream_.reset(); stream_.reset();
} }
} }
@ -129,6 +92,7 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool {
codec_.reset(codecs::CreateCodecForType(stream->type()).value_or(nullptr)); codec_.reset(codecs::CreateCodecForType(stream->type()).value_or(nullptr));
if (!codec_) { if (!codec_) {
ESP_LOGE(kTag, "no codec found"); ESP_LOGE(kTag, "no codec found");
events::Audio().Dispatch(internal::DecoderError{});
return false; return false;
} }
@ -136,6 +100,7 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool {
if (open_res.has_error()) { if (open_res.has_error()) {
ESP_LOGE(kTag, "codec failed to start: %s", ESP_LOGE(kTag, "codec failed to start: %s",
codecs::ICodec::ErrorString(open_res.error()).c_str()); codecs::ICodec::ErrorString(open_res.error()).c_str());
events::Audio().Dispatch(internal::DecoderError{});
return false; return false;
} }
stream->SetPreambleFinished(); stream->SetPreambleFinished();
@ -146,20 +111,23 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool {
}; };
ESP_LOGI(kTag, "stream started ok"); ESP_LOGI(kTag, "stream started ok");
events::Audio().Dispatch(internal::InputFileOpened{});
auto tags = std::make_shared<Track>(Track{
.tags = stream->tags(),
.db_info = {},
.bitrate_kbps = open_res->sample_rate_hz,
.encoding = stream->type(),
.filepath = stream->Filepath(),
});
timer_.reset(new Timer(tags, open_res.value(), stream->Offset()));
PlaybackUpdate ev{.seconds_elapsed = stream->Offset(), .track = tags}; std::optional<uint32_t> duration;
events::Audio().Dispatch(ev); if (open_res->total_samples) {
events::Ui().Dispatch(ev); duration = open_res->total_samples.value() / open_res->num_channels /
open_res->sample_rate_hz;
}
events::Audio().Dispatch(internal::DecoderOpened{
.track = std::make_shared<TrackInfo>(TrackInfo{
.tags = stream->tags(),
.uri = stream->Filepath(),
.duration = duration,
.start_offset = stream->Offset(),
.bitrate_kbps = open_res->sample_rate_hz,
.encoding = stream->type(),
}),
});
return true; return true;
} }
@ -167,6 +135,7 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool {
auto Decoder::ContinueDecoding() -> bool { auto Decoder::ContinueDecoding() -> bool {
auto res = codec_->DecodeTo(codec_buffer_); auto res = codec_->DecodeTo(codec_buffer_);
if (res.has_error()) { if (res.has_error()) {
events::Audio().Dispatch(internal::DecoderError{});
return true; return true;
} }
@ -176,11 +145,8 @@ auto Decoder::ContinueDecoding() -> bool {
res->is_stream_finished); res->is_stream_finished);
} }
if (timer_) {
timer_->AddSamples(res->samples_written);
}
if (res->is_stream_finished) { if (res->is_stream_finished) {
events::Audio().Dispatch(internal::DecoderClosed{});
codec_.reset(); codec_.reset();
} }

@ -36,6 +36,7 @@
#include "sample.hpp" #include "sample.hpp"
#include "service_locator.hpp" #include "service_locator.hpp"
#include "system_events.hpp" #include "system_events.hpp"
#include "tinyfsm.hpp"
#include "track.hpp" #include "track.hpp"
#include "track_queue.hpp" #include "track_queue.hpp"
#include "wm8523.hpp" #include "wm8523.hpp"
@ -54,13 +55,158 @@ std::shared_ptr<BluetoothAudioOutput> AudioState::sBtOutput;
std::shared_ptr<IAudioOutput> AudioState::sOutput; std::shared_ptr<IAudioOutput> AudioState::sOutput;
// Two seconds of samples for two channels, at a representative sample rate. // 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; StreamBufferHandle_t AudioState::sDrainBuffer;
std::optional<database::TrackId> AudioState::sCurrentTrack; std::shared_ptr<TrackInfo> AudioState::sCurrentTrack;
bool AudioState::sIsPlaybackAllowed; uint64_t AudioState::sCurrentSamples;
std::optional<IAudioOutput::Format> AudioState::sCurrentFormat;
std::shared_ptr<TrackInfo> AudioState::sNextTrack;
uint64_t AudioState::sNextTrackCueSamples;
bool AudioState::sIsResampling;
bool AudioState::sIsPaused = true;
auto AudioState::currentPositionSeconds() -> std::optional<uint32_t> {
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<std::pair<std::string, uint32_t>> sLastTrackUpdate; 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) {
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<void>([=]() {
std::optional<std::string> path;
if (std::holds_alternative<database::TrackId>(new_track)) {
auto db = sServices->database().lock();
if (db) {
path = db->getTrackPath(std::get<database::TrackId>(new_track));
}
} else if (std::holds_alternative<std::string>(new_track)) {
path = std::get<std::string>(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<states::Standby>() && sCurrentTrack) {
transit<states::Playback>();
} else if (sIsPaused && is_in_state<states::Playback>()) {
transit<states::Standby>();
}
}
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<states::Playback>(),
};
events::System().Dispatch(event);
events::Ui().Dispatch(event);
if (sCurrentTrack && !sIsPaused && !is_in_state<states::Playback>()) {
ESP_LOGI(kTag, "ready to play!");
transit<states::Playback>();
}
}
void AudioState::react(const system_fsm::BluetoothEvent& ev) { void AudioState::react(const system_fsm::BluetoothEvent& ev) {
if (ev.event != drivers::bluetooth::Event::kConnectionStateChanged) { 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<void>([=]() {
auto db = sServices->database().lock();
if (!db) {
return;
}
sFileSource->SetPath(db->getTrackPath(id));
});
}
auto AudioState::commitVolume() -> void { auto AudioState::commitVolume() -> void {
auto mode = sServices->nvs().OutputMode(); auto mode = sServices->nvs().OutputMode();
auto vol = sOutput->GetVolume(); 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<states::Playback>()) {
transit<states::Playback>();
}
} else {
if (!is_in_state<states::Standby>()) {
transit<states::Standby>();
}
}
}
namespace states { namespace states {
void Uninitialised::react(const system_fsm::BootComplete& ev) { void Uninitialised::react(const system_fsm::BootComplete& ev) {
@ -283,44 +401,6 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) {
transit<Standby>(); transit<Standby>();
} }
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<Playback>();
}
}
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 kQueueKey[] = "audio:queue";
static const char kCurrentFileKey[] = "audio:current"; static const char kCurrentFileKey[] = "audio:current";
@ -328,7 +408,7 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) {
if (!ev.locking) { if (!ev.locking) {
return; return;
} }
sServices->bg_worker().Dispatch<void>([]() { sServices->bg_worker().Dispatch<void>([this]() {
auto db = sServices->database().lock(); auto db = sServices->database().lock();
if (!db) { if (!db) {
return; return;
@ -341,10 +421,10 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) {
} }
db->put(kQueueKey, queue.serialise()); db->put(kQueueKey, queue.serialise());
if (sLastTrackUpdate) { if (sCurrentTrack) {
cppbor::Array current_track{ cppbor::Array current_track{
cppbor::Tstr{sLastTrackUpdate->first}, cppbor::Tstr{sCurrentTrack->uri},
cppbor::Uint{sLastTrackUpdate->second}, cppbor::Uint{currentPositionSeconds().value_or(0)},
}; };
db->put(kCurrentFileKey, current_track.toString()); db->put(kCurrentFileKey, current_track.toString());
} }
@ -371,8 +451,12 @@ void Standby::react(const system_fsm::StorageMounted& ev) {
if (parsed->type() == cppbor::ARRAY) { if (parsed->type() == cppbor::ARRAY) {
std::string filename = parsed->asArray()->get(0)->asTstr()->value(); std::string filename = parsed->asArray()->get(0)->asTstr()->value();
uint32_t pos = parsed->asArray()->get(1)->asUint()->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() { void Playback::entry() {
ESP_LOGI(kTag, "beginning playback"); ESP_LOGI(kTag, "audio output resumed");
sOutput->mode(IAudioOutput::Modes::kOnPlaying); sOutput->mode(IAudioOutput::Modes::kOnPlaying);
events::System().Dispatch(PlaybackStarted{}); PlaybackUpdate event{
events::Ui().Dispatch(PlaybackStarted{}); .current_track = sCurrentTrack,
.track_position = currentPositionSeconds(),
.paused = false,
};
events::System().Dispatch(event);
events::Ui().Dispatch(event);
} }
void Playback::exit() { void Playback::exit() {
ESP_LOGI(kTag, "finishing playback"); ESP_LOGI(kTag, "audio output paused");
sOutput->mode(IAudioOutput::Modes::kOnPaused); sOutput->mode(IAudioOutput::Modes::kOnPaused);
// Stash the current volume now, in case it changed during playback, since PlaybackUpdate event{
// we might be powering off soon. .current_track = sCurrentTrack,
commitVolume(); .track_position = currentPositionSeconds(),
.paused = true,
events::System().Dispatch(PlaybackStopped{}); };
events::Ui().Dispatch(PlaybackStopped{});
}
void Playback::react(const system_fsm::HasPhonesChanged& ev) {
if (!ev.has_headphones) {
transit<Standby>();
}
}
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<Standby>();
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) {}
void Playback::react(const internal::InputFileFinished& ev) { events::System().Dispatch(event);
ESP_LOGI(kTag, "finished playing file"); events::Ui().Dispatch(event);
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<Standby>();
}
}
void Playback::react(const internal::AudioPipelineIdle& ev) {
transit<Standby>();
} }
} // namespace states } // namespace states

@ -6,6 +6,7 @@
#pragma once #pragma once
#include <stdint.h>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
@ -40,6 +41,8 @@ class SampleConverter {
auto SetTargetFormat(const IAudioOutput::Format& format) -> void; auto SetTargetFormat(const IAudioOutput::Format& format) -> void;
auto HandleSamples(cpp::span<sample::Sample>, bool) -> size_t; auto HandleSamples(cpp::span<sample::Sample>, bool) -> size_t;
auto SendToSink(cpp::span<sample::Sample>) -> void;
struct Args { struct Args {
IAudioOutput::Format format; IAudioOutput::Format format;
size_t samples_available; size_t samples_available;
@ -59,6 +62,8 @@ class SampleConverter {
IAudioOutput::Format source_format_; IAudioOutput::Format source_format_;
IAudioOutput::Format target_format_; IAudioOutput::Format target_format_;
size_t leftover_bytes_; size_t leftover_bytes_;
uint32_t samples_sunk_;
}; };
} // namespace audio } // namespace audio

@ -19,25 +19,6 @@
namespace audio { namespace audio {
/*
* Sample-based timer for the current elapsed playback time.
*/
class Timer {
public:
Timer(std::shared_ptr<Track>, const codecs::ICodec::OutputFormat& format, uint32_t current_seconds = 0);
auto AddSamples(std::size_t) -> void;
private:
std::shared_ptr<Track> track_;
uint32_t current_seconds_;
uint32_t current_sample_in_second_;
uint32_t samples_per_second_;
uint32_t total_duration_seconds_;
};
/* /*
* Handle to a persistent task that takes bytes from the given source, decodes * Handle to a persistent task that takes bytes from the given source, decodes
* them into sample::Sample (normalised to 16 bit signed PCM), and then * them into sample::Sample (normalised to 16 bit signed PCM), and then
@ -65,7 +46,6 @@ class Decoder {
std::shared_ptr<codecs::IStream> stream_; std::shared_ptr<codecs::IStream> stream_;
std::unique_ptr<codecs::ICodec> codec_; std::unique_ptr<codecs::ICodec> codec_;
std::unique_ptr<Timer> timer_;
std::optional<codecs::ICodec::OutputFormat> current_format_; std::optional<codecs::ICodec::OutputFormat> current_format_;
std::optional<IAudioOutput::Format> current_sink_format_; std::optional<IAudioOutput::Format> current_sink_format_;

@ -9,8 +9,10 @@
#include <stdint.h> #include <stdint.h>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <optional>
#include <string> #include <string>
#include "audio_sink.hpp"
#include "tinyfsm.hpp" #include "tinyfsm.hpp"
#include "track.hpp" #include "track.hpp"
@ -18,24 +20,80 @@
namespace audio { namespace audio {
struct Track { /*
* Struct encapsulating information about the decoder's current track.
*/
struct TrackInfo {
/*
* Audio tags extracted from the file. May be absent for files without any
* parseable tags.
*/
std::shared_ptr<database::TrackTags> tags; std::shared_ptr<database::TrackTags> tags;
std::shared_ptr<database::TrackData> db_info;
uint32_t duration; /*
uint32_t bitrate_kbps; * URI that the current track was retrieved from. This is currently always a
* file path on the SD card.
*/
std::string uri;
/*
* The length of this track in seconds. This is either retrieved from the
* track's tags, or sometimes computed. It may therefore sometimes be
* inaccurate or missing.
*/
std::optional<uint32_t> duration;
/* The offset in seconds that this file's decoding started from. */
std::optional<uint32_t> start_offset;
/* The approximate bitrate of this track in its original encoded form. */
std::optional<uint32_t> bitrate_kbps;
/* The encoded format of the this track. */
codecs::StreamType encoding; codecs::StreamType encoding;
std::string filepath;
}; };
struct PlaybackStarted : tinyfsm::Event {}; /*
* Event emitted by the audio FSM when the state of the audio pipeline has
* changed. This is usually once per second while a track is playing, plus one
* event each when a track starts or finishes.
*/
struct PlaybackUpdate : tinyfsm::Event { struct PlaybackUpdate : tinyfsm::Event {
uint32_t seconds_elapsed; /*
std::shared_ptr<Track> track; * The track that is currently being decoded by the audio pipeline. May be
* absent if there is no current track.
*/
std::shared_ptr<TrackInfo> current_track;
/*
* How long the current track has been playing for, in seconds. Will always
* be present if current_track is present.
*/
std::optional<uint32_t> track_position;
/* Whether or not the current track is currently being output to a sink. */
bool paused;
};
/*
* Sets a new track to be decoded by the audio pipeline, replacing any
* currently playing track.
*/
struct SetTrack : tinyfsm::Event {
std::variant<std::string, database::TrackId, std::monostate> new_track;
std::optional<uint32_t> seek_to_second;
enum Transition {
kHardCut,
kGapless,
// TODO: kCrossFade
};
Transition transition;
}; };
struct PlaybackStopped : tinyfsm::Event {}; struct TogglePlayPause : tinyfsm::Event {
std::optional<bool> set_to;
};
struct QueueUpdate : tinyfsm::Event { struct QueueUpdate : tinyfsm::Event {
bool current_changed; bool current_changed;
@ -49,15 +107,6 @@ struct QueueUpdate : tinyfsm::Event {
Reason reason; Reason reason;
}; };
struct PlayFile : tinyfsm::Event {
std::string filename;
};
struct SeekFile : tinyfsm::Event {
uint32_t offset;
std::string filename;
};
struct StepUpVolume : tinyfsm::Event {}; struct StepUpVolume : tinyfsm::Event {};
struct StepDownVolume : tinyfsm::Event {}; struct StepDownVolume : tinyfsm::Event {};
struct SetVolume : tinyfsm::Event { struct SetVolume : tinyfsm::Event {
@ -83,17 +132,26 @@ struct SetVolumeLimit : tinyfsm::Event {
int limit_db; int limit_db;
}; };
struct TogglePlayPause : tinyfsm::Event {};
struct OutputModeChanged : tinyfsm::Event {}; struct OutputModeChanged : tinyfsm::Event {};
namespace internal { namespace internal {
struct InputFileOpened : tinyfsm::Event {}; struct DecoderOpened : tinyfsm::Event {
struct InputFileClosed : tinyfsm::Event {}; std::shared_ptr<TrackInfo> track;
struct InputFileFinished : tinyfsm::Event {}; };
struct DecoderClosed : tinyfsm::Event {};
struct DecoderError : tinyfsm::Event {};
struct AudioPipelineIdle : tinyfsm::Event {}; struct ConverterConfigurationChanged : tinyfsm::Event {
IAudioOutput::Format src_format;
IAudioOutput::Format dst_format;
};
struct ConverterProgress : tinyfsm::Event {
uint32_t samples_sunk;
};
} // namespace internal } // namespace internal

@ -6,6 +6,7 @@
#pragma once #pragma once
#include <stdint.h>
#include <deque> #include <deque>
#include <memory> #include <memory>
#include <vector> #include <vector>
@ -41,6 +42,17 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
/* Fallback event handler. Does nothing. */ /* Fallback event handler. Does nothing. */
void react(const tinyfsm::Event& ev) {} void react(const tinyfsm::Event& ev) {}
void react(const QueueUpdate&);
void react(const SetTrack&);
void react(const TogglePlayPause&);
void react(const internal::DecoderOpened&);
void react(const internal::DecoderClosed&);
void react(const internal::DecoderError&);
void react(const internal::ConverterConfigurationChanged&);
void react(const internal::ConverterProgress&);
void react(const StepUpVolume&); void react(const StepUpVolume&);
void react(const StepDownVolume&); void react(const StepDownVolume&);
virtual void react(const system_fsm::HasPhonesChanged&); virtual void react(const system_fsm::HasPhonesChanged&);
@ -56,17 +68,6 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
virtual void react(const system_fsm::StorageMounted&) {} virtual void react(const system_fsm::StorageMounted&) {}
virtual void react(const system_fsm::BluetoothEvent&); virtual void react(const system_fsm::BluetoothEvent&);
virtual void react(const PlayFile&) {}
virtual void react(const SeekFile&) {}
virtual void react(const QueueUpdate&) {}
virtual void react(const PlaybackUpdate&) {}
void react(const TogglePlayPause&);
virtual void react(const internal::InputFileOpened&) {}
virtual void react(const internal::InputFileClosed&) {}
virtual void react(const internal::InputFileFinished&) {}
virtual void react(const internal::AudioPipelineIdle&) {}
protected: protected:
auto clearDrainBuffer() -> void; auto clearDrainBuffer() -> void;
auto playTrack(database::TrackId id) -> void; auto playTrack(database::TrackId id) -> void;
@ -83,10 +84,17 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
static StreamBufferHandle_t sDrainBuffer; static StreamBufferHandle_t sDrainBuffer;
static std::optional<database::TrackId> sCurrentTrack; static std::shared_ptr<TrackInfo> sCurrentTrack;
static uint64_t sCurrentSamples;
static std::optional<IAudioOutput::Format> sCurrentFormat;
auto readyToPlay() -> bool; static std::shared_ptr<TrackInfo> sNextTrack;
static bool sIsPlaybackAllowed; static uint64_t sNextTrackCueSamples;
static bool sIsResampling;
static bool sIsPaused;
auto currentPositionSeconds() -> std::optional<uint32_t>;
}; };
namespace states { namespace states {
@ -94,7 +102,6 @@ namespace states {
class Uninitialised : public AudioState { class Uninitialised : public AudioState {
public: public:
void react(const system_fsm::BootComplete&) override; void react(const system_fsm::BootComplete&) override;
void react(const system_fsm::BluetoothEvent&) override{}; void react(const system_fsm::BluetoothEvent&) override{};
using AudioState::react; using AudioState::react;
@ -102,10 +109,6 @@ class Uninitialised : public AudioState {
class Standby : public AudioState { class Standby : public AudioState {
public: public:
void react(const PlayFile&) override;
void react(const SeekFile&) override;
void react(const internal::InputFileOpened&) override;
void react(const QueueUpdate&) override;
void react(const system_fsm::KeyLockChanged&) override; void react(const system_fsm::KeyLockChanged&) override;
void react(const system_fsm::StorageMounted&) override; void react(const system_fsm::StorageMounted&) override;
@ -117,18 +120,6 @@ class Playback : public AudioState {
void entry() override; void entry() override;
void exit() override; void exit() override;
void react(const system_fsm::HasPhonesChanged&) override;
void react(const PlayFile&) override;
void react(const SeekFile&) override;
void react(const QueueUpdate&) override;
void react(const PlaybackUpdate&) override;
void react(const internal::InputFileOpened&) override;
void react(const internal::InputFileClosed&) override;
void react(const internal::InputFileFinished&) override;
void react(const internal::AudioPipelineIdle&) override;
using AudioState::react; using AudioState::react;
}; };

@ -136,7 +136,7 @@ auto TrackQueue::insert(Item i, size_t index) -> void {
{ {
const std::shared_lock<std::shared_mutex> lock(mutex_); const std::shared_lock<std::shared_mutex> lock(mutex_);
was_queue_empty = pos_ == tracks_.size(); was_queue_empty = pos_ == tracks_.size();
current_changed = pos_ == was_queue_empty || index == pos_; current_changed = was_queue_empty || index == pos_;
} }
auto update_shuffler = [=, this]() { auto update_shuffler = [=, this]() {

@ -23,7 +23,7 @@ using LuaValue = std::variant<std::monostate,
int, int,
bool, bool,
std::string, std::string,
audio::Track, audio::TrackInfo,
drivers::bluetooth::Device, drivers::bluetooth::Device,
std::vector<drivers::bluetooth::Device>>; std::vector<drivers::bluetooth::Device>>;

@ -221,7 +221,7 @@ static auto pushTagValue(lua_State* L, const database::TagValue& val) -> void {
val); val);
} }
static void pushTrack(lua_State* L, const audio::Track& track) { static void pushTrack(lua_State* L, const audio::TrackInfo& track) {
lua_newtable(L); lua_newtable(L);
for (const auto& tag : track.tags->allPresent()) { for (const auto& tag : track.tags->allPresent()) {
@ -229,19 +229,18 @@ static void pushTrack(lua_State* L, const audio::Track& track) {
pushTagValue(L, track.tags->get(tag)); pushTagValue(L, track.tags->get(tag));
lua_settable(L, -3); lua_settable(L, -3);
} }
if (track.db_info) {
lua_pushliteral(L, "id"); if (track.duration) {
lua_pushinteger(L, track.db_info->id); lua_pushliteral(L, "duration");
lua_pushinteger(L, track.duration.value());
lua_settable(L, -3); lua_settable(L, -3);
} }
lua_pushliteral(L, "duration"); if (track.bitrate_kbps) {
lua_pushinteger(L, track.duration); lua_pushliteral(L, "bitrate_kbps");
lua_settable(L, -3); lua_pushinteger(L, track.bitrate_kbps.value());
lua_settable(L, -3);
lua_pushliteral(L, "bitrate_kbps"); }
lua_pushinteger(L, track.bitrate_kbps);
lua_settable(L, -3);
lua_pushliteral(L, "encoding"); lua_pushliteral(L, "encoding");
lua_pushstring(L, codecs::StreamTypeToString(track.encoding).c_str()); lua_pushstring(L, codecs::StreamTypeToString(track.encoding).c_str());
@ -289,7 +288,7 @@ auto Property::PushValue(lua_State& s) -> int {
lua_pushboolean(&s, arg); lua_pushboolean(&s, arg);
} else if constexpr (std::is_same_v<T, std::string>) { } else if constexpr (std::is_same_v<T, std::string>) {
lua_pushstring(&s, arg.c_str()); lua_pushstring(&s, arg.c_str());
} else if constexpr (std::is_same_v<T, audio::Track>) { } else if constexpr (std::is_same_v<T, audio::TrackInfo>) {
pushTrack(&s, arg); pushTrack(&s, arg);
} else if constexpr (std::is_same_v<T, drivers::bluetooth::Device>) { } else if constexpr (std::is_same_v<T, drivers::bluetooth::Device>) {
pushDevice(&s, arg); pushDevice(&s, arg);

@ -63,7 +63,7 @@ class SystemState : public tinyfsm::Fsm<SystemState> {
virtual void react(const SdDetectChanged&) {} virtual void react(const SdDetectChanged&) {}
virtual void react(const SamdUsbMscChanged&) {} virtual void react(const SamdUsbMscChanged&) {}
virtual void react(const database::event::UpdateFinished&) {} virtual void react(const database::event::UpdateFinished&) {}
virtual void react(const audio::PlaybackStopped&) {} virtual void react(const audio::PlaybackUpdate&) {}
virtual void react(const internal::IdleTimeout&) {} virtual void react(const internal::IdleTimeout&) {}
virtual void react(const internal::UnmountTimeout&) {} virtual void react(const internal::UnmountTimeout&) {}
@ -101,7 +101,7 @@ class Running : public SystemState {
void react(const KeyLockChanged&) override; void react(const KeyLockChanged&) override;
void react(const SdDetectChanged&) override; void react(const SdDetectChanged&) override;
void react(const audio::PlaybackStopped&) override; void react(const audio::PlaybackUpdate&) override;
void react(const database::event::UpdateFinished&) override; void react(const database::event::UpdateFinished&) override;
void react(const SamdUsbMscChanged&) override; void react(const SamdUsbMscChanged&) override;
void react(const internal::UnmountTimeout&) override; void react(const internal::UnmountTimeout&) override;

@ -56,7 +56,7 @@ void Running::react(const KeyLockChanged& ev) {
checkIdle(); checkIdle();
} }
void Running::react(const audio::PlaybackStopped& ev) { void Running::react(const audio::PlaybackUpdate& ev) {
checkIdle(); checkIdle();
} }

@ -57,8 +57,6 @@ class UiState : public tinyfsm::Fsm<UiState> {
virtual void react(const system_fsm::StorageMounted&) {} virtual void react(const system_fsm::StorageMounted&) {}
void react(const system_fsm::BatteryStateChanged&); void react(const system_fsm::BatteryStateChanged&);
void react(const audio::PlaybackStarted&);
void react(const audio::PlaybackStopped&);
void react(const audio::PlaybackUpdate&); void react(const audio::PlaybackUpdate&);
void react(const audio::QueueUpdate&); void react(const audio::QueueUpdate&);

@ -114,14 +114,11 @@ lua::Property UiState::sBluetoothDevices{
lua::Property UiState::sPlaybackPlaying{ lua::Property UiState::sPlaybackPlaying{
false, [](const lua::LuaValue& val) { false, [](const lua::LuaValue& val) {
bool current_val = std::get<bool>(sPlaybackPlaying.Get());
if (!std::holds_alternative<bool>(val)) { if (!std::holds_alternative<bool>(val)) {
return false; return false;
} }
bool new_val = std::get<bool>(val); bool new_val = std::get<bool>(val);
if (current_val != new_val) { events::Audio().Dispatch(audio::TogglePlayPause{.set_to = new_val});
events::Audio().Dispatch(audio::TogglePlayPause{});
}
return true; return true;
}}; }};
@ -135,12 +132,13 @@ lua::Property UiState::sPlaybackPosition{
int new_val = std::get<int>(val); int new_val = std::get<int>(val);
if (current_val != new_val) { if (current_val != new_val) {
auto track = sPlaybackTrack.Get(); auto track = sPlaybackTrack.Get();
if (!std::holds_alternative<audio::Track>(track)) { if (!std::holds_alternative<audio::TrackInfo>(track)) {
return false; return false;
} }
events::Audio().Dispatch(audio::SeekFile{ events::Audio().Dispatch(audio::SetTrack{
.offset = (uint32_t)new_val, .new_track = std::get<audio::TrackInfo>(track).uri,
.filename = std::get<audio::Track>(track).filepath}); .seek_to_second = (uint32_t)new_val,
});
} }
return true; return true;
}}; }};
@ -393,17 +391,10 @@ void UiState::react(const audio::QueueUpdate&) {
sQueueReplay.Update(queue.replay()); sQueueReplay.Update(queue.replay());
} }
void UiState::react(const audio::PlaybackStarted& ev) {
sPlaybackPlaying.Update(true);
}
void UiState::react(const audio::PlaybackUpdate& ev) { void UiState::react(const audio::PlaybackUpdate& ev) {
sPlaybackTrack.Update(*ev.track); sPlaybackTrack.Update(*ev.current_track);
sPlaybackPosition.Update(static_cast<int>(ev.seconds_elapsed)); sPlaybackPlaying.Update(!ev.paused);
} sPlaybackPosition.Update(static_cast<int>(ev.track_position.value_or(0)));
void UiState::react(const audio::PlaybackStopped&) {
sPlaybackPlaying.Update(false);
} }
void UiState::react(const audio::VolumeChanged& ev) { void UiState::react(const audio::VolumeChanged& ev) {

Loading…
Cancel
Save