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. 70
      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. 17
      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:
type: idf
version: 5.1.1
manifest_hash: b9761e0028130d307b778c710e5dd39fb3c942d8084ed429d448d938957fb0e6
manifest_hash: 9e4320e6f25503854c6c93bcbfa9b80f780485bcf066bdbad31a820544492538
target: esp32
version: 1.0.0

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

@ -5,14 +5,17 @@
*/
#include "audio_converter.hpp"
#include <stdint.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include "audio_events.hpp"
#include "audio_sink.hpp"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "event_queue.hpp"
#include "freertos/portmacro.h"
#include "freertos/projdefs.h"
#include "i2s_dac.hpp"
@ -35,7 +38,9 @@ SampleConverter::SampleConverter()
resampler_(nullptr),
source_(xStreamBufferCreateWithCaps(kSourceBufferLength,
sizeof(sample::Sample) * 2,
MALLOC_CAP_DMA)) {
MALLOC_CAP_DMA)),
leftover_bytes_(0),
samples_sunk_(0) {
input_buffer_ = {
reinterpret_cast<sample::Sample*>(heap_caps_calloc(
kSampleBufferLength, sizeof(sample::Sample), MALLOC_CAP_DMA)),
@ -107,6 +112,19 @@ auto SampleConverter::Main() -> void {
sink_->Configure(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
@ -154,9 +172,8 @@ auto SampleConverter::HandleSamples(cpp::span<sample::Sample> input,
if (source_format_ == target_format_) {
// The happiest possible case: the input format matches the output
// format already.
std::size_t bytes_sent = xStreamBufferSend(
sink_->stream(), input.data(), input.size_bytes(), portMAX_DELAY);
return bytes_sent / sizeof(sample::Sample);
SendToSink(input);
return input.size();
}
size_t samples_used = 0;
@ -186,16 +203,26 @@ auto SampleConverter::HandleSamples(cpp::span<sample::Sample> input,
samples_used = input.size();
}
size_t bytes_sent = 0;
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);
}
SendToSink(output_source);
}
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

@ -5,6 +5,7 @@
*/
#include "audio_decoder.hpp"
#include <stdint.h>
#include <cstdint>
#include <cstdlib>
@ -50,39 +51,6 @@ namespace audio {
static constexpr std::size_t kCodecBufferLength =
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,
std::shared_ptr<SampleConverter> sink) -> Decoder* {
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,
std::shared_ptr<SampleConverter> mixer)
: source_(source),
converter_(mixer),
codec_(),
timer_(),
current_format_() {
: source_(source), converter_(mixer), codec_(), current_format_() {
ESP_LOGI(kTag, "allocating codec buffer, %u KiB", kCodecBufferLength / 1024);
codec_buffer_ = {
reinterpret_cast<sample::Sample*>(heap_caps_calloc(
@ -117,7 +81,6 @@ void Decoder::Main() {
}
if (ContinueDecoding()) {
events::Audio().Dispatch(internal::InputFileFinished{});
stream_.reset();
}
}
@ -129,6 +92,7 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool {
codec_.reset(codecs::CreateCodecForType(stream->type()).value_or(nullptr));
if (!codec_) {
ESP_LOGE(kTag, "no codec found");
events::Audio().Dispatch(internal::DecoderError{});
return false;
}
@ -136,6 +100,7 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool {
if (open_res.has_error()) {
ESP_LOGE(kTag, "codec failed to start: %s",
codecs::ICodec::ErrorString(open_res.error()).c_str());
events::Audio().Dispatch(internal::DecoderError{});
return false;
}
stream->SetPreambleFinished();
@ -146,20 +111,23 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool {
};
ESP_LOGI(kTag, "stream started ok");
events::Audio().Dispatch(internal::InputFileOpened{});
auto tags = std::make_shared<Track>(Track{
std::optional<uint32_t> duration;
if (open_res->total_samples) {
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(),
.db_info = {},
.uri = stream->Filepath(),
.duration = duration,
.start_offset = stream->Offset(),
.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};
events::Audio().Dispatch(ev);
events::Ui().Dispatch(ev);
return true;
}
@ -167,6 +135,7 @@ auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool {
auto Decoder::ContinueDecoding() -> bool {
auto res = codec_->DecodeTo(codec_buffer_);
if (res.has_error()) {
events::Audio().Dispatch(internal::DecoderError{});
return true;
}
@ -176,11 +145,8 @@ auto Decoder::ContinueDecoding() -> bool {
res->is_stream_finished);
}
if (timer_) {
timer_->AddSamples(res->samples_written);
}
if (res->is_stream_finished) {
events::Audio().Dispatch(internal::DecoderClosed{});
codec_.reset();
}

@ -36,6 +36,7 @@
#include "sample.hpp"
#include "service_locator.hpp"
#include "system_events.hpp"
#include "tinyfsm.hpp"
#include "track.hpp"
#include "track_queue.hpp"
#include "wm8523.hpp"
@ -54,13 +55,158 @@ 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 kDrainBufferSize = sizeof(sample::Sample) * 48000 * 4;
constexpr size_t kDrainLatencySamples = 48000;
constexpr size_t kDrainBufferSize =
sizeof(sample::Sample) * kDrainLatencySamples * 4;
StreamBufferHandle_t AudioState::sDrainBuffer;
std::optional<database::TrackId> AudioState::sCurrentTrack;
bool AudioState::sIsPlaybackAllowed;
std::shared_ptr<TrackInfo> AudioState::sCurrentTrack;
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,
};
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);
static std::optional<std::pair<std::string, uint32_t>> sLastTrackUpdate;
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) {
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 mode = sServices->nvs().OutputMode();
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 {
void Uninitialised::react(const system_fsm::BootComplete& ev) {
@ -283,44 +401,6 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) {
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 kCurrentFileKey[] = "audio:current";
@ -328,7 +408,7 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) {
if (!ev.locking) {
return;
}
sServices->bg_worker().Dispatch<void>([]() {
sServices->bg_worker().Dispatch<void>([this]() {
auto db = sServices->database().lock();
if (!db) {
return;
@ -341,10 +421,10 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) {
}
db->put(kQueueKey, queue.serialise());
if (sLastTrackUpdate) {
if (sCurrentTrack) {
cppbor::Array current_track{
cppbor::Tstr{sLastTrackUpdate->first},
cppbor::Uint{sLastTrackUpdate->second},
cppbor::Tstr{sCurrentTrack->uri},
cppbor::Uint{currentPositionSeconds().value_or(0)},
};
db->put(kCurrentFileKey, current_track.toString());
}
@ -371,8 +451,12 @@ void Standby::react(const system_fsm::StorageMounted& ev) {
if (parsed->type() == cppbor::ARRAY) {
std::string filename = parsed->asArray()->get(0)->asTstr()->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() {
ESP_LOGI(kTag, "beginning playback");
ESP_LOGI(kTag, "audio output resumed");
sOutput->mode(IAudioOutput::Modes::kOnPlaying);
events::System().Dispatch(PlaybackStarted{});
events::Ui().Dispatch(PlaybackStarted{});
PlaybackUpdate event{
.current_track = sCurrentTrack,
.track_position = currentPositionSeconds(),
.paused = false,
};
events::System().Dispatch(event);
events::Ui().Dispatch(event);
}
void Playback::exit() {
ESP_LOGI(kTag, "finishing playback");
ESP_LOGI(kTag, "audio output paused");
sOutput->mode(IAudioOutput::Modes::kOnPaused);
// Stash the current volume now, in case it changed during playback, since
// we might be powering off soon.
commitVolume();
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) {
ESP_LOGI(kTag, "finished playing file");
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>();
}
}
PlaybackUpdate event{
.current_track = sCurrentTrack,
.track_position = currentPositionSeconds(),
.paused = true,
};
void Playback::react(const internal::AudioPipelineIdle& ev) {
transit<Standby>();
events::System().Dispatch(event);
events::Ui().Dispatch(event);
}
} // namespace states

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

@ -19,25 +19,6 @@
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
* 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::unique_ptr<codecs::ICodec> codec_;
std::unique_ptr<Timer> timer_;
std::optional<codecs::ICodec::OutputFormat> current_format_;
std::optional<IAudioOutput::Format> current_sink_format_;

@ -9,8 +9,10 @@
#include <stdint.h>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include "audio_sink.hpp"
#include "tinyfsm.hpp"
#include "track.hpp"
@ -18,24 +20,80 @@
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::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;
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 {
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;
};
struct PlaybackStopped : tinyfsm::Event {};
/*
* 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 TogglePlayPause : tinyfsm::Event {
std::optional<bool> set_to;
};
struct QueueUpdate : tinyfsm::Event {
bool current_changed;
@ -49,15 +107,6 @@ struct QueueUpdate : tinyfsm::Event {
Reason reason;
};
struct PlayFile : tinyfsm::Event {
std::string filename;
};
struct SeekFile : tinyfsm::Event {
uint32_t offset;
std::string filename;
};
struct StepUpVolume : tinyfsm::Event {};
struct StepDownVolume : tinyfsm::Event {};
struct SetVolume : tinyfsm::Event {
@ -83,17 +132,26 @@ struct SetVolumeLimit : tinyfsm::Event {
int limit_db;
};
struct TogglePlayPause : tinyfsm::Event {};
struct OutputModeChanged : tinyfsm::Event {};
namespace internal {
struct InputFileOpened : tinyfsm::Event {};
struct InputFileClosed : tinyfsm::Event {};
struct InputFileFinished : tinyfsm::Event {};
struct DecoderOpened : tinyfsm::Event {
std::shared_ptr<TrackInfo> track;
};
struct DecoderClosed : tinyfsm::Event {};
struct AudioPipelineIdle : tinyfsm::Event {};
struct DecoderError : tinyfsm::Event {};
struct ConverterConfigurationChanged : tinyfsm::Event {
IAudioOutput::Format src_format;
IAudioOutput::Format dst_format;
};
struct ConverterProgress : tinyfsm::Event {
uint32_t samples_sunk;
};
} // namespace internal

@ -6,6 +6,7 @@
#pragma once
#include <stdint.h>
#include <deque>
#include <memory>
#include <vector>
@ -41,6 +42,17 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
/* Fallback event handler. Does nothing. */
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 StepDownVolume&);
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::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:
auto clearDrainBuffer() -> void;
auto playTrack(database::TrackId id) -> void;
@ -83,10 +84,17 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
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 bool sIsPlaybackAllowed;
static std::shared_ptr<TrackInfo> sNextTrack;
static uint64_t sNextTrackCueSamples;
static bool sIsResampling;
static bool sIsPaused;
auto currentPositionSeconds() -> std::optional<uint32_t>;
};
namespace states {
@ -94,7 +102,6 @@ namespace states {
class Uninitialised : public AudioState {
public:
void react(const system_fsm::BootComplete&) override;
void react(const system_fsm::BluetoothEvent&) override{};
using AudioState::react;
@ -102,10 +109,6 @@ class Uninitialised : public AudioState {
class Standby : public AudioState {
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::StorageMounted&) override;
@ -117,18 +120,6 @@ class Playback : public AudioState {
void entry() 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;
};

@ -136,7 +136,7 @@ auto TrackQueue::insert(Item i, size_t index) -> void {
{
const std::shared_lock<std::shared_mutex> lock(mutex_);
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]() {

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

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

@ -63,7 +63,7 @@ class SystemState : public tinyfsm::Fsm<SystemState> {
virtual void react(const SdDetectChanged&) {}
virtual void react(const SamdUsbMscChanged&) {}
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::UnmountTimeout&) {}
@ -101,7 +101,7 @@ class Running : public SystemState {
void react(const KeyLockChanged&) 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 SamdUsbMscChanged&) override;
void react(const internal::UnmountTimeout&) override;

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

@ -57,8 +57,6 @@ class UiState : public tinyfsm::Fsm<UiState> {
virtual void react(const system_fsm::StorageMounted&) {}
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::QueueUpdate&);

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

Loading…
Cancel
Save