From 1573a8c4cde1cd9528b422b2dcc598e37ffe94a7 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 2 May 2024 19:12:26 +1000 Subject: WIP merge cyclically dependent components into one big component --- src/tangara/audio/audio_decoder.cpp | 144 ++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/tangara/audio/audio_decoder.cpp (limited to 'src/tangara/audio/audio_decoder.cpp') diff --git a/src/tangara/audio/audio_decoder.cpp b/src/tangara/audio/audio_decoder.cpp new file mode 100644 index 00000000..bf2d3fbe --- /dev/null +++ b/src/tangara/audio/audio_decoder.cpp @@ -0,0 +1,144 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "audio_decoder.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "esp_err.h" +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "freertos/portmacro.h" +#include "freertos/projdefs.h" +#include "freertos/queue.h" +#include "freertos/ringbuf.h" + +#include "audio_converter.hpp" +#include "audio_events.hpp" +#include "audio_fsm.hpp" +#include "audio_sink.hpp" +#include "audio_source.hpp" +#include "codec.hpp" +#include "event_queue.hpp" +#include "fatfs_audio_input.hpp" +#include "i2s_dac.hpp" +#include "sample.hpp" +#include "tasks.hpp" +#include "track.hpp" +#include "types.hpp" +#include "ui_fsm.hpp" + +namespace audio { + +[[maybe_unused]] static const char* kTag = "audio_dec"; + +static constexpr std::size_t kCodecBufferLength = + drivers::kI2SBufferLengthFrames * sizeof(sample::Sample); + +auto Decoder::Start(std::shared_ptr source, + std::shared_ptr sink) -> Decoder* { + Decoder* task = new Decoder(source, sink); + tasks::StartPersistent([=]() { task->Main(); }); + return task; +} + +Decoder::Decoder(std::shared_ptr source, + std::shared_ptr mixer) + : source_(source), converter_(mixer), codec_(), current_format_() { + ESP_LOGI(kTag, "allocating codec buffer, %u KiB", kCodecBufferLength / 1024); + codec_buffer_ = { + reinterpret_cast(heap_caps_calloc( + kCodecBufferLength, sizeof(sample::Sample), MALLOC_CAP_DMA)), + kCodecBufferLength}; +} + +void Decoder::Main() { + for (;;) { + if (source_->HasNewStream() || !stream_) { + std::shared_ptr new_stream = source_->NextStream(); + if (new_stream && BeginDecoding(new_stream)) { + stream_ = new_stream; + } else { + continue; + } + } + + if (ContinueDecoding()) { + stream_.reset(); + } + } +} + +auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { + // Ensure any previous codec is freed before creating a new one. + codec_.reset(); + codec_.reset(codecs::CreateCodecForType(stream->type()).value_or(nullptr)); + if (!codec_) { + ESP_LOGE(kTag, "no codec found for stream"); + return false; + } + + auto open_res = codec_->OpenStream(stream, stream->Offset()); + if (open_res.has_error()) { + ESP_LOGE(kTag, "codec failed to start: %s", + codecs::ICodec::ErrorString(open_res.error()).c_str()); + return false; + } + stream->SetPreambleFinished(); + current_sink_format_ = IAudioOutput::Format{ + .sample_rate = open_res->sample_rate_hz, + .num_channels = open_res->num_channels, + .bits_per_sample = 16, + }; + + std::optional duration; + if (open_res->total_samples) { + duration = open_res->total_samples.value() / open_res->num_channels / + open_res->sample_rate_hz; + } + + converter_->beginStream(std::make_shared(TrackInfo{ + .tags = stream->tags(), + .uri = stream->Filepath(), + .duration = duration, + .start_offset = stream->Offset(), + .bitrate_kbps = open_res->sample_rate_hz, + .encoding = stream->type(), + .format = *current_sink_format_, + })); + + return true; +} + +auto Decoder::ContinueDecoding() -> bool { + auto res = codec_->DecodeTo(codec_buffer_); + if (res.has_error()) { + converter_->endStream(); + return true; + } + + if (res->samples_written > 0) { + converter_->continueStream(codec_buffer_.first(res->samples_written)); + } + + if (res->is_stream_finished) { + converter_->endStream(); + codec_.reset(); + } + + return res->is_stream_finished; +} + +} // namespace audio -- cgit v1.2.3 From 7d7f7755d17e1e0a2348d75d797097f166b70471 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 2 May 2024 21:41:56 +1000 Subject: start moving include files into subdirs --- src/tangara/audio/audio_decoder.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'src/tangara/audio/audio_decoder.cpp') diff --git a/src/tangara/audio/audio_decoder.cpp b/src/tangara/audio/audio_decoder.cpp index bf2d3fbe..1ef30b2c 100644 --- a/src/tangara/audio/audio_decoder.cpp +++ b/src/tangara/audio/audio_decoder.cpp @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-only */ -#include "audio_decoder.hpp" +#include "audio/audio_decoder.hpp" #include #include @@ -25,20 +25,20 @@ #include "freertos/queue.h" #include "freertos/ringbuf.h" -#include "audio_converter.hpp" -#include "audio_events.hpp" -#include "audio_fsm.hpp" -#include "audio_sink.hpp" -#include "audio_source.hpp" +#include "audio/audio_converter.hpp" +#include "audio/audio_events.hpp" +#include "audio/audio_fsm.hpp" +#include "audio/audio_sink.hpp" +#include "audio/audio_source.hpp" +#include "audio/fatfs_audio_input.hpp" #include "codec.hpp" -#include "event_queue.hpp" -#include "fatfs_audio_input.hpp" +#include "database/track.hpp" +#include "events/event_queue.hpp" #include "i2s_dac.hpp" #include "sample.hpp" #include "tasks.hpp" -#include "track.hpp" #include "types.hpp" -#include "ui_fsm.hpp" +#include "ui/ui_fsm.hpp" namespace audio { -- cgit v1.2.3 From 26eb580043ad176bdc58d996f30d470e1073ef00 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 2 May 2024 21:52:59 +1000 Subject: move driver includes into a subdir as well --- src/tangara/audio/audio_decoder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/tangara/audio/audio_decoder.cpp') diff --git a/src/tangara/audio/audio_decoder.cpp b/src/tangara/audio/audio_decoder.cpp index 1ef30b2c..ae54a11c 100644 --- a/src/tangara/audio/audio_decoder.cpp +++ b/src/tangara/audio/audio_decoder.cpp @@ -33,8 +33,8 @@ #include "audio/fatfs_audio_input.hpp" #include "codec.hpp" #include "database/track.hpp" +#include "drivers/i2s_dac.hpp" #include "events/event_queue.hpp" -#include "i2s_dac.hpp" #include "sample.hpp" #include "tasks.hpp" #include "types.hpp" -- cgit v1.2.3 From b242ba998699208c87dc066158964de0866b61e2 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 7 May 2024 14:19:19 +1000 Subject: Improve decoder's interface to accept streams --- src/tangara/audio/audio_decoder.cpp | 151 ++++++++++++++++++++++++------------ 1 file changed, 101 insertions(+), 50 deletions(-) (limited to 'src/tangara/audio/audio_decoder.cpp') diff --git a/src/tangara/audio/audio_decoder.cpp b/src/tangara/audio/audio_decoder.cpp index ae54a11c..0e38bca8 100644 --- a/src/tangara/audio/audio_decoder.cpp +++ b/src/tangara/audio/audio_decoder.cpp @@ -6,7 +6,7 @@ #include "audio/audio_decoder.hpp" -#include +#include #include #include #include @@ -23,14 +23,12 @@ #include "freertos/portmacro.h" #include "freertos/projdefs.h" #include "freertos/queue.h" -#include "freertos/ringbuf.h" -#include "audio/audio_converter.hpp" #include "audio/audio_events.hpp" #include "audio/audio_fsm.hpp" #include "audio/audio_sink.hpp" #include "audio/audio_source.hpp" -#include "audio/fatfs_audio_input.hpp" +#include "audio/processor.hpp" #include "codec.hpp" #include "database/track.hpp" #include "drivers/i2s_dac.hpp" @@ -42,21 +40,33 @@ namespace audio { -[[maybe_unused]] static const char* kTag = "audio_dec"; +static const char* kTag = "decoder"; +/* + * The size of the buffer used for holding decoded samples. This buffer is + * allocated in internal memory for greater speed, so be careful when + * increasing its size. + */ static constexpr std::size_t kCodecBufferLength = drivers::kI2SBufferLengthFrames * sizeof(sample::Sample); -auto Decoder::Start(std::shared_ptr source, - std::shared_ptr sink) -> Decoder* { - Decoder* task = new Decoder(source, sink); +auto Decoder::Start(std::shared_ptr sink) -> Decoder* { + Decoder* task = new Decoder(sink); tasks::StartPersistent([=]() { task->Main(); }); return task; } -Decoder::Decoder(std::shared_ptr source, - std::shared_ptr mixer) - : source_(source), converter_(mixer), codec_(), current_format_() { +auto Decoder::open(std::shared_ptr stream) -> void { + NextStream* next = new NextStream(); + next->stream = stream; + // The decoder services its queue very quickly, so blocking on this write + // should be fine. If we discover contention here, then adding more space for + // items to next_stream_ should be fine too. + xQueueSend(next_stream_, &next, portMAX_DELAY); +} + +Decoder::Decoder(std::shared_ptr processor) + : processor_(processor), next_stream_(xQueueCreate(1, sizeof(void*))) { ESP_LOGI(kTag, "allocating codec buffer, %u KiB", kCodecBufferLength / 1024); codec_buffer_ = { reinterpret_cast(heap_caps_calloc( @@ -64,81 +74,122 @@ Decoder::Decoder(std::shared_ptr source, kCodecBufferLength}; } +/* + * Main decoding loop. Handles watching for new streams, or continuing to nudge + * along the current stream if we have one. + */ void Decoder::Main() { for (;;) { - if (source_->HasNewStream() || !stream_) { - std::shared_ptr new_stream = source_->NextStream(); - if (new_stream && BeginDecoding(new_stream)) { - stream_ = new_stream; - } else { + // Check whether there's a new stream to begin. If we're idle, then we + // simply park and wait forever for a stream to arrive. + TickType_t wait_time = stream_ ? 0 : portMAX_DELAY; + NextStream* next; + if (xQueueReceive(next_stream_, &next, wait_time)) { + // Copy the data out of the queue, then clean up the item. + std::shared_ptr new_stream = next->stream; + delete next; + + // If we were already decoding, then make sure we finish up the current + // file gracefully. + if (stream_) { + finishDecode(); + } + + // Ensure there's actually stream data; we might have been given nullptr + // as a signal to stop. + if (!new_stream) { continue; } + + // Start decoding the new stream. + prepareDecode(new_stream); } - if (ContinueDecoding()) { - stream_.reset(); + if (!continueDecode()) { + finishDecode(); } } } -auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { - // Ensure any previous codec is freed before creating a new one. - codec_.reset(); +auto Decoder::prepareDecode(std::shared_ptr stream) -> void { + auto stub_track = std::make_shared(TrackInfo{ + .tags = stream->tags(), + .uri = stream->Filepath(), + .duration = {}, + .start_offset = {}, + .bitrate_kbps = {}, + .encoding = stream->type(), + .format = {}, + }); + codec_.reset(codecs::CreateCodecForType(stream->type()).value_or(nullptr)); if (!codec_) { ESP_LOGE(kTag, "no codec found for stream"); - return false; + events::Audio().Dispatch( + internal::DecodingFailedToStart{.track = stub_track}); + return; } auto open_res = codec_->OpenStream(stream, stream->Offset()); if (open_res.has_error()) { ESP_LOGE(kTag, "codec failed to start: %s", codecs::ICodec::ErrorString(open_res.error()).c_str()); - return false; - } - stream->SetPreambleFinished(); - current_sink_format_ = IAudioOutput::Format{ - .sample_rate = open_res->sample_rate_hz, - .num_channels = open_res->num_channels, - .bits_per_sample = 16, - }; - - std::optional 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::DecodingFailedToStart{.track = stub_track}); + return; } - converter_->beginStream(std::make_shared(TrackInfo{ + // Decoding started okay! Fill out the rest of the track info for this + // stream. + stream_ = stream; + track_ = std::make_shared(TrackInfo{ .tags = stream->tags(), .uri = stream->Filepath(), - .duration = duration, + .duration = {}, .start_offset = stream->Offset(), - .bitrate_kbps = open_res->sample_rate_hz, + .bitrate_kbps = {}, .encoding = stream->type(), - .format = *current_sink_format_, - })); + .format = + { + .sample_rate = open_res->sample_rate_hz, + .num_channels = open_res->num_channels, + .bits_per_sample = 16, + }, + }); + + if (open_res->total_samples) { + track_->duration = open_res->total_samples.value() / + open_res->num_channels / open_res->sample_rate_hz; + } - return true; + events::Audio().Dispatch(internal::DecodingStarted{.track = track_}); + processor_->beginStream(track_); } -auto Decoder::ContinueDecoding() -> bool { +auto Decoder::continueDecode() -> bool { auto res = codec_->DecodeTo(codec_buffer_); if (res.has_error()) { - converter_->endStream(); - return true; + return false; } if (res->samples_written > 0) { - converter_->continueStream(codec_buffer_.first(res->samples_written)); + processor_->continueStream(codec_buffer_.first(res->samples_written)); } - if (res->is_stream_finished) { - converter_->endStream(); - codec_.reset(); - } + return !res->is_stream_finished; +} - return res->is_stream_finished; +auto Decoder::finishDecode() -> void { + assert(track_); + + // Tell everyone we're finished. + events::Audio().Dispatch(internal::DecodingFinished{.track = track_}); + processor_->endStream(); + + // Clean up after ourselves. + stream_.reset(); + codec_.reset(); + track_.reset(); } } // namespace audio -- cgit v1.2.3 From 265049c5192cf0ce862c7db7b4745636afb6c17b Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 8 May 2024 16:03:03 +1000 Subject: 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 --- src/tangara/audio/audio_decoder.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) (limited to 'src/tangara/audio/audio_decoder.cpp') diff --git a/src/tangara/audio/audio_decoder.cpp b/src/tangara/audio/audio_decoder.cpp index 0e38bca8..ee06d984 100644 --- a/src/tangara/audio/audio_decoder.cpp +++ b/src/tangara/audio/audio_decoder.cpp @@ -92,7 +92,7 @@ void Decoder::Main() { // If we were already decoding, then make sure we finish up the current // file gracefully. if (stream_) { - finishDecode(); + finishDecode(true); } // Ensure there's actually stream data; we might have been given nullptr @@ -106,7 +106,7 @@ void Decoder::Main() { } if (!continueDecode()) { - finishDecode(); + finishDecode(false); } } } @@ -179,12 +179,16 @@ auto Decoder::continueDecode() -> bool { return !res->is_stream_finished; } -auto Decoder::finishDecode() -> void { +auto Decoder::finishDecode(bool cancel) -> void { assert(track_); // Tell everyone we're finished. - events::Audio().Dispatch(internal::DecodingFinished{.track = track_}); - processor_->endStream(); + if (cancel) { + events::Audio().Dispatch(internal::DecodingCancelled{.track = track_}); + } else { + events::Audio().Dispatch(internal::DecodingFinished{.track = track_}); + } + processor_->endStream(cancel); // Clean up after ourselves. stream_.reset(); -- cgit v1.2.3