diff options
| author | jacqueline <me@jacqueline.id.au> | 2023-06-06 10:20:46 +1000 |
|---|---|---|
| committer | jacqueline <me@jacqueline.id.au> | 2023-06-06 10:20:46 +1000 |
| commit | 8a2a2d226558d099243eea0aa9ae22b2791e0e0e (patch) | |
| tree | 16c3defdb167ea7f452e5ce96118b165acf6602f /src/audio | |
| parent | ee5f662f9bb150138545ca35ef5c4896eb74daea (diff) | |
| download | tangara-fw-8a2a2d226558d099243eea0aa9ae22b2791e0e0e.tar.gz | |
Get basic audio playback going again
Diffstat (limited to 'src/audio')
| -rw-r--r-- | src/audio/audio_decoder.cpp | 11 | ||||
| -rw-r--r-- | src/audio/audio_fsm.cpp | 32 | ||||
| -rw-r--r-- | src/audio/audio_task.cpp | 102 | ||||
| -rw-r--r-- | src/audio/fatfs_audio_input.cpp | 4 | ||||
| -rw-r--r-- | src/audio/include/audio_decoder.hpp | 3 | ||||
| -rw-r--r-- | src/audio/include/audio_element.hpp | 3 | ||||
| -rw-r--r-- | src/audio/include/fatfs_audio_input.hpp | 2 | ||||
| -rw-r--r-- | src/audio/pipeline.cpp | 2 |
8 files changed, 117 insertions, 42 deletions
diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp index 981cca6f..5a2c75c7 100644 --- a/src/audio/audio_decoder.cpp +++ b/src/audio/audio_decoder.cpp @@ -14,7 +14,6 @@ #include <memory> #include <variant> -#include "cbor/tinycbor/src/cborinternal_p.h" #include "freertos/FreeRTOS.h" #include "esp_heap_caps.h" @@ -36,7 +35,8 @@ AudioDecoder::AudioDecoder() current_codec_(), current_input_format_(), current_output_format_(), - has_samples_to_send_(false) {} + has_samples_to_send_(false), + has_input_remaining_(false) {} AudioDecoder::~AudioDecoder() {} @@ -70,6 +70,10 @@ auto AudioDecoder::ProcessStreamInfo(const StreamInfo& info) -> bool { return true; } +auto AudioDecoder::NeedsToProcess() const -> bool { + return has_samples_to_send_ || has_input_remaining_; +} + auto AudioDecoder::Process(const std::vector<InputStream>& inputs, OutputStream* output) -> void { auto input = inputs.begin(); @@ -124,7 +128,8 @@ auto AudioDecoder::Process(const std::vector<InputStream>& inputs, return; } - if (res.value()) { + has_input_remaining_ = !res.value(); + if (!has_input_remaining_) { // We're out of useable data in this buffer. Finish immediately; there's // nothing to send. input->mark_incomplete(); diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index a1bb956f..9c7af6fe 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -29,29 +29,29 @@ auto AudioState::Init(drivers::GpioExpander* gpio_expander, sGpioExpander = gpio_expander; sDac = dac; sDatabase = database; -} -namespace states { + sFileSource.reset(new FatfsAudioInput()); + sI2SOutput.reset(new I2SAudioOutput(sGpioExpander, sDac)); -void Uninitialised::react(const system_fsm::BootComplete&) { - transit<Standby>([&]() { - sFileSource.reset(new FatfsAudioInput()); - sI2SOutput.reset(new I2SAudioOutput(sGpioExpander, sDac)); + // Perform initial pipeline configuration. + // TODO(jacqueline): Factor this out once we have any kind of dynamic + // reconfiguration. + AudioDecoder* codec = new AudioDecoder(); + sPipeline.emplace_back(codec); - // Perform initial pipeline configuration. - // TODO(jacqueline): Factor this out once we have any kind of dynamic - // reconfiguration. - AudioDecoder* codec = new AudioDecoder(); - sPipeline.emplace_back(codec); + Pipeline* pipeline = new Pipeline(sPipeline.front().get()); + pipeline->AddInput(sFileSource.get()); - Pipeline* pipeline = new Pipeline(sPipeline.front().get()); - pipeline->AddInput(sFileSource.get()); + task::StartPipeline(pipeline, sI2SOutput.get()); +} - task::StartPipeline(pipeline, sI2SOutput.get()); - }); +namespace states { + +void Uninitialised::react(const system_fsm::BootComplete&) { + transit<Standby>(); } -void Standby::react(const PlayFile &ev) { +void Standby::react(const PlayFile& ev) { if (sFileSource->OpenFile(ev.filename)) { transit<Playback>(); } diff --git a/src/audio/audio_task.cpp b/src/audio/audio_task.cpp index 6cc2d927..343281a7 100644 --- a/src/audio/audio_task.cpp +++ b/src/audio/audio_task.cpp @@ -43,20 +43,64 @@ namespace task { static const char* kTag = "task"; +// The default amount of time to wait between pipeline iterations for a single +// song. +static constexpr uint_fast16_t kDefaultDelayTicks = pdMS_TO_TICKS(5); +static constexpr uint_fast16_t kMaxDelayTicks = pdMS_TO_TICKS(10); +static constexpr uint_fast16_t kMinDelayTicks = pdMS_TO_TICKS(1); + void AudioTaskMain(std::unique_ptr<Pipeline> pipeline, IAudioSink* sink) { + // The stream format for bytes currently in the sink buffer. std::optional<StreamInfo::Format> output_format; - uint_fast16_t delay_ticks = pdMS_TO_TICKS(5); - std::vector<Pipeline*> elements = pipeline->GetIterationOrder(); + // How long to wait between pipeline iterations. This is reset for each song, + // and readjusted on the fly to maintain a reasonable amount playback buffer. + // Buffering too much will mean we process samples inefficiently, wasting CPU + // time, whilst buffering too little will affect the quality of the output. + uint_fast16_t delay_ticks = kDefaultDelayTicks; + + std::vector<Pipeline*> all_elements = pipeline->GetIterationOrder(); - events::EventQueue &event_queue = events::EventQueue::GetInstance(); + events::EventQueue& event_queue = events::EventQueue::GetInstance(); while (1) { - event_queue.ServiceAudio(delay_ticks); + // First, see if we actually have any pipeline work to do in this iteration. + bool has_work = false; + // We always have work to do if there's still bytes to be sunk. + has_work = all_elements.back()->OutStream().info->bytes_in_stream > 0; + if (!has_work) { + for (Pipeline* p : all_elements) { + has_work = p->OutputElement()->NeedsToProcess(); + if (has_work) { + break; + } + } + } + + // See if there's any new events. + event_queue.ServiceAudio(has_work ? delay_ticks : portMAX_DELAY); + + if (!has_work) { + // See if we've been given work by this event. + for (Pipeline* p : all_elements) { + has_work = p->OutputElement()->NeedsToProcess(); + if (has_work) { + delay_ticks = kDefaultDelayTicks; + break; + } + } + if (!has_work) { + continue; + } + } + + // We have work to do! Allow each element in the pipeline to process one + // chunk. We iterate from input nodes first, so this should result in + // samples in the output buffer. - for (int i = 0; i < elements.size(); i++) { + for (int i = 0; i < all_elements.size(); i++) { std::vector<RawStream> raw_in_streams; - elements.at(i)->InStreams(&raw_in_streams); - RawStream raw_out_stream = elements.at(i)->OutStream(); + all_elements.at(i)->InStreams(&raw_in_streams); + RawStream raw_out_stream = all_elements.at(i)->OutStream(); // Crop the input and output streams to the ranges that are safe to // touch. For the input streams, this is the region that contains @@ -67,14 +111,14 @@ void AudioTaskMain(std::unique_ptr<Pipeline> pipeline, IAudioSink* sink) { [&](RawStream& s) { in_streams.emplace_back(&s); }); OutputStream out_stream(&raw_out_stream); - elements.at(i)->OutputElement()->Process(in_streams, &out_stream); + all_elements.at(i)->OutputElement()->Process(in_streams, &out_stream); } - RawStream raw_sink_stream = elements.front()->OutStream(); + RawStream raw_sink_stream = all_elements.back()->OutStream(); InputStream sink_stream(&raw_sink_stream); if (sink_stream.info().bytes_in_stream == 0) { - vTaskDelay(pdMS_TO_TICKS(100)); + // No new bytes to sink, so skip sinking completely. continue; } @@ -86,24 +130,36 @@ void AudioTaskMain(std::unique_ptr<Pipeline> pipeline, IAudioSink* sink) { ESP_LOGI(kTag, "reconfiguring dac"); output_format = sink_stream.info().format; sink->Configure(*output_format); + } else { + continue; } } // We've reconfigured the sink, or it was already configured correctly. // Send through some data. - if (output_format == sink_stream.info().format && - !std::holds_alternative<std::monostate>(*output_format)) { - std::size_t sent = - xStreamBufferSend(sink->buffer(), sink_stream.data().data(), - sink_stream.data().size_bytes(), 0); - if (sent > 0) { - ESP_LOGI( - kTag, "sunk %u bytes out of %u (%d %%)", sent, - sink_stream.info().bytes_in_stream, - (int)(((float)sent / (float)sink_stream.info().bytes_in_stream) * - 100)); - } - sink_stream.consume(sent); + std::size_t bytes_sunk = + xStreamBufferSend(sink->buffer(), sink_stream.data().data(), + sink_stream.data().size_bytes(), 0); + + // Adjust how long we wait for the next iteration if we're getting too far + // ahead or behind. + float sunk_percent = static_cast<float>(bytes_sunk) / + static_cast<float>(sink_stream.info().bytes_in_stream); + + if (sunk_percent > 0.66f) { + // We're sinking a lot of the output buffer per iteration, so we need to + // be running faster. + delay_ticks--; + } else if (sunk_percent < 0.33f) { + // We're not sinking much of the output buffer per iteration, so we can + // slow down to save some cycles. + delay_ticks++; + } + delay_ticks = std::clamp(delay_ticks, kMinDelayTicks, kMaxDelayTicks); + + // Finally, actually mark the bytes we sunk as consumed. + if (bytes_sunk > 0) { + sink_stream.consume(bytes_sunk); } } } diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp index ed5db315..8abc7d32 100644 --- a/src/audio/fatfs_audio_input.cpp +++ b/src/audio/fatfs_audio_input.cpp @@ -51,6 +51,10 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool { return true; } +auto FatfsAudioInput::NeedsToProcess() const -> bool { + return is_file_open_; +} + auto FatfsAudioInput::Process(const std::vector<InputStream>& inputs, OutputStream* output) -> void { if (!is_file_open_) { diff --git a/src/audio/include/audio_decoder.hpp b/src/audio/include/audio_decoder.hpp index 5927e150..3cda0305 100644 --- a/src/audio/include/audio_decoder.hpp +++ b/src/audio/include/audio_decoder.hpp @@ -30,6 +30,8 @@ class AudioDecoder : public IAudioElement { AudioDecoder(); ~AudioDecoder(); + auto NeedsToProcess() const -> bool override; + auto Process(const std::vector<InputStream>& inputs, OutputStream* output) -> void override; @@ -41,6 +43,7 @@ class AudioDecoder : public IAudioElement { std::optional<StreamInfo::Format> current_input_format_; std::optional<StreamInfo::Format> current_output_format_; bool has_samples_to_send_; + bool has_input_remaining_; auto ProcessStreamInfo(const StreamInfo& info) -> bool; }; diff --git a/src/audio/include/audio_element.hpp b/src/audio/include/audio_element.hpp index 133a6ae7..7e3d9d7d 100644 --- a/src/audio/include/audio_element.hpp +++ b/src/audio/include/audio_element.hpp @@ -10,6 +10,7 @@ #include <cstdint> #include <deque> #include <memory> +#include <vector> #include "freertos/FreeRTOS.h" @@ -46,6 +47,8 @@ class IAudioElement { IAudioElement() {} virtual ~IAudioElement() {} + virtual auto NeedsToProcess() const -> bool = 0; + virtual auto Process(const std::vector<InputStream>& inputs, OutputStream* output) -> void = 0; }; diff --git a/src/audio/include/fatfs_audio_input.hpp b/src/audio/include/fatfs_audio_input.hpp index 98ad4d2b..bfc0064e 100644 --- a/src/audio/include/fatfs_audio_input.hpp +++ b/src/audio/include/fatfs_audio_input.hpp @@ -33,6 +33,8 @@ class FatfsAudioInput : public IAudioElement { auto OpenFile(const std::string& path) -> bool; + auto NeedsToProcess() const -> bool override; + auto Process(const std::vector<InputStream>& inputs, OutputStream* output) -> void override; diff --git a/src/audio/pipeline.cpp b/src/audio/pipeline.cpp index 60598151..f54f8d11 100644 --- a/src/audio/pipeline.cpp +++ b/src/audio/pipeline.cpp @@ -5,6 +5,7 @@ */ #include "pipeline.hpp" +#include <algorithm> #include <memory> #include "stream_info.hpp" @@ -53,6 +54,7 @@ auto Pipeline::GetIterationOrder() -> std::vector<Pipeline*> { } } + std::reverse(found.begin(), found.end()); return found; } |
