mostly single task pipeline
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS "audio_decoder.cpp" "audio_task.cpp" "chunk.cpp" "fatfs_audio_input.cpp"
|
SRCS "audio_decoder.cpp" "audio_task.cpp" "chunk.cpp" "fatfs_audio_input.cpp"
|
||||||
"stream_message.cpp" "i2s_audio_output.cpp" "stream_buffer.cpp"
|
"stream_message.cpp" "i2s_audio_output.cpp" "stream_buffer.cpp"
|
||||||
"audio_playback.cpp" "stream_event.cpp" "audio_element.cpp"
|
"audio_playback.cpp" "stream_event.cpp" "audio_element.cpp" "pipeline.cpp"
|
||||||
INCLUDE_DIRS "include"
|
INCLUDE_DIRS "include"
|
||||||
REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory")
|
REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory")
|
||||||
|
|
||||||
|
|||||||
+58
-75
@@ -2,13 +2,16 @@
|
|||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
#include "cbor/tinycbor/src/cborinternal_p.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
|
|
||||||
#include "esp_heap_caps.h"
|
#include "esp_heap_caps.h"
|
||||||
|
#include "esp_log.h"
|
||||||
#include "freertos/message_buffer.h"
|
#include "freertos/message_buffer.h"
|
||||||
#include "freertos/portmacro.h"
|
#include "freertos/portmacro.h"
|
||||||
|
|
||||||
@@ -21,126 +24,106 @@ namespace audio {
|
|||||||
|
|
||||||
static const char* kTag = "DEC";
|
static const char* kTag = "DEC";
|
||||||
|
|
||||||
static const std::size_t kChunkSize = 1024;
|
|
||||||
static const std::size_t kReadahead = 8;
|
|
||||||
|
|
||||||
AudioDecoder::AudioDecoder()
|
AudioDecoder::AudioDecoder()
|
||||||
: IAudioElement(),
|
: IAudioElement(),
|
||||||
arena_(kChunkSize, kReadahead, MALLOC_CAP_SPIRAM),
|
|
||||||
stream_info_({}),
|
stream_info_({}),
|
||||||
has_samples_to_send_(false),
|
has_samples_to_send_(false),
|
||||||
needs_more_input_(true) {}
|
needs_more_input_(true) {}
|
||||||
|
|
||||||
AudioDecoder::~AudioDecoder() {}
|
AudioDecoder::~AudioDecoder() {}
|
||||||
|
|
||||||
auto AudioDecoder::HasUnprocessedInput() -> bool {
|
auto AudioDecoder::ProcessStreamInfo(const StreamInfo& info) -> bool {
|
||||||
return !needs_more_input_ || has_samples_to_send_;
|
if (!std::holds_alternative<StreamInfo::Encoded>(info.data)) {
|
||||||
}
|
return false;
|
||||||
|
|
||||||
auto AudioDecoder::IsOverBuffered() -> bool {
|
|
||||||
return arena_.BlocksFree() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto AudioDecoder::ProcessStreamInfo(const StreamInfo& info) -> void {
|
|
||||||
stream_info_ = info;
|
|
||||||
|
|
||||||
if (info.chunk_size) {
|
|
||||||
chunk_reader_.emplace(info.chunk_size.value());
|
|
||||||
} else {
|
|
||||||
ESP_LOGE(kTag, "no chunk size given");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
const auto& encoded = std::get<StreamInfo::Encoded>(info.data);
|
||||||
|
|
||||||
// Reuse the existing codec if we can. This will help with gapless playback,
|
// Reuse the existing codec if we can. This will help with gapless playback,
|
||||||
// since we can potentially just continue to decode as we were before,
|
// since we can potentially just continue to decode as we were before,
|
||||||
// without any setup overhead.
|
// without any setup overhead.
|
||||||
|
// TODO: use audio type from stream
|
||||||
if (current_codec_ != nullptr &&
|
if (current_codec_ != nullptr &&
|
||||||
current_codec_->CanHandleFile(info.path.value_or(""))) {
|
current_codec_->CanHandleType(encoded.type)) {
|
||||||
current_codec_->ResetForNewStream();
|
current_codec_->ResetForNewStream();
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto result = codecs::CreateCodecForFile(info.path.value_or(""));
|
// TODO: use audio type from stream
|
||||||
|
auto result = codecs::CreateCodecForType(encoded.type);
|
||||||
if (result.has_value()) {
|
if (result.has_value()) {
|
||||||
current_codec_ = std::move(result.value());
|
current_codec_ = std::move(result.value());
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGE(kTag, "no codec for this file");
|
ESP_LOGE(kTag, "no codec for this file");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
stream_info_ = info;
|
return true;
|
||||||
has_sent_stream_info_ = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto AudioDecoder::ProcessChunk(const cpp::span<std::byte>& chunk) -> void {
|
auto AudioDecoder::Process(std::vector<Stream>* inputs, MutableStream* output)
|
||||||
if (current_codec_ == nullptr || !chunk_reader_) {
|
-> void {
|
||||||
// Should never happen, but fail explicitly anyway.
|
// We don't really expect multiple inputs, so just pick the first that
|
||||||
ESP_LOGW(kTag, "received chunk without chunk size or codec");
|
// contains data. If none of them contain data, then we can still flush
|
||||||
return;
|
// pending samples.
|
||||||
}
|
auto input =
|
||||||
|
std::find_if(inputs->begin(), inputs->end(),
|
||||||
|
[](const Stream& s) { return s.data.size_bytes() > 0; });
|
||||||
|
|
||||||
ESP_LOGI(kTag, "received new chunk (size %u)", chunk.size());
|
if (input != inputs->end()) {
|
||||||
current_codec_->SetInput(chunk_reader_->HandleNewData(chunk));
|
const StreamInfo* info = input->info;
|
||||||
needs_more_input_ = false;
|
if (!stream_info_ || *stream_info_ != *info) {
|
||||||
}
|
// The input stream has changed! Immediately throw everything away and
|
||||||
|
// start from scratch.
|
||||||
auto AudioDecoder::ProcessEndOfStream() -> void {
|
// TODO: special case gapless playback? needs thought.
|
||||||
|
stream_info_ = *info;
|
||||||
has_samples_to_send_ = false;
|
has_samples_to_send_ = false;
|
||||||
needs_more_input_ = true;
|
has_set_stream_info_ = false;
|
||||||
current_codec_.reset();
|
|
||||||
|
|
||||||
SendOrBufferEvent(std::unique_ptr<StreamEvent>(
|
ProcessStreamInfo(*info);
|
||||||
StreamEvent::CreateEndOfStream(input_events_)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto AudioDecoder::Process() -> void {
|
current_codec_->SetInput(input->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
if (has_samples_to_send_) {
|
if (has_samples_to_send_) {
|
||||||
// Writing samples is relatively quick (it's just a bunch of memcopy's), so
|
if (!has_set_stream_info_) {
|
||||||
// do them all at once.
|
has_set_stream_info_ = true;
|
||||||
while (has_samples_to_send_ && !IsOverBuffered()) {
|
|
||||||
if (!has_sent_stream_info_) {
|
|
||||||
has_sent_stream_info_ = true;
|
|
||||||
auto format = current_codec_->GetOutputFormat();
|
auto format = current_codec_->GetOutputFormat();
|
||||||
stream_info_->bits_per_sample = format.bits_per_sample;
|
output->info->data.emplace<StreamInfo::Pcm>(
|
||||||
stream_info_->sample_rate = format.sample_rate_hz;
|
format.bits_per_sample, format.sample_rate_hz, format.num_channels);
|
||||||
stream_info_->channels = format.num_channels;
|
|
||||||
stream_info_->chunk_size = kChunkSize;
|
|
||||||
|
|
||||||
auto event =
|
|
||||||
StreamEvent::CreateStreamInfo(input_events_, *stream_info_);
|
|
||||||
SendOrBufferEvent(std::unique_ptr<StreamEvent>(event));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto block = arena_.Acquire();
|
auto write_res = current_codec_->WriteOutputSamples(
|
||||||
if (!block) {
|
output->data.subspan(output->info->bytes_in_stream));
|
||||||
return;
|
output->info->bytes_in_stream += write_res.first;
|
||||||
}
|
|
||||||
|
|
||||||
auto write_res =
|
|
||||||
current_codec_->WriteOutputSamples({block->start, block->size});
|
|
||||||
block->used_size = write_res.first;
|
|
||||||
has_samples_to_send_ = !write_res.second;
|
has_samples_to_send_ = !write_res.second;
|
||||||
|
|
||||||
auto chunk = std::unique_ptr<StreamEvent>(
|
if (has_samples_to_send_) {
|
||||||
StreamEvent::CreateArenaChunk(input_events_, *block));
|
// We weren't able to fit all the generated samples into the output
|
||||||
if (!SendOrBufferEvent(std::move(chunk))) {
|
// buffer. Stop trying; we'll finish up during the next pass.
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// We will process the next frame during the next call to this method.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!needs_more_input_) {
|
if (input != inputs->end()) {
|
||||||
auto res = current_codec_->ProcessNextFrame();
|
auto res = current_codec_->ProcessNextFrame();
|
||||||
if (res.has_error()) {
|
if (res.has_error()) {
|
||||||
// TODO(jacqueline): Handle errors.
|
// TODO(jacqueline): Handle errors.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
needs_more_input_ = res.value();
|
input->data = input->data.subspan(current_codec_->GetInputPosition());
|
||||||
has_samples_to_send_ = true;
|
|
||||||
|
|
||||||
if (needs_more_input_) {
|
if (res.value()) {
|
||||||
chunk_reader_->HandleBytesUsed(current_codec_->GetInputPosition());
|
// We're out of data in this buffer. Finish immediately; there's nothing
|
||||||
|
// to send.
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
has_samples_to_send_ = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No input; nothing to do.
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "fatfs_audio_input.hpp"
|
#include "fatfs_audio_input.hpp"
|
||||||
#include "gpio_expander.hpp"
|
#include "gpio_expander.hpp"
|
||||||
#include "i2s_audio_output.hpp"
|
#include "i2s_audio_output.hpp"
|
||||||
|
#include "pipeline.hpp"
|
||||||
#include "storage.hpp"
|
#include "storage.hpp"
|
||||||
#include "stream_buffer.hpp"
|
#include "stream_buffer.hpp"
|
||||||
#include "stream_info.hpp"
|
#include "stream_info.hpp"
|
||||||
@@ -20,11 +21,10 @@
|
|||||||
|
|
||||||
namespace audio {
|
namespace audio {
|
||||||
|
|
||||||
auto AudioPlayback::create(drivers::GpioExpander* expander,
|
auto AudioPlayback::create(drivers::GpioExpander* expander)
|
||||||
std::shared_ptr<drivers::SdStorage> storage)
|
|
||||||
-> cpp::result<std::unique_ptr<AudioPlayback>, Error> {
|
-> cpp::result<std::unique_ptr<AudioPlayback>, Error> {
|
||||||
// Create everything
|
// Create everything
|
||||||
auto source = std::make_shared<FatfsAudioInput>(storage);
|
auto source = std::make_shared<FatfsAudioInput>();
|
||||||
auto codec = std::make_shared<AudioDecoder>();
|
auto codec = std::make_shared<AudioDecoder>();
|
||||||
|
|
||||||
auto sink_res = I2SAudioOutput::create(expander);
|
auto sink_res = I2SAudioOutput::create(expander);
|
||||||
@@ -35,41 +35,47 @@ auto AudioPlayback::create(drivers::GpioExpander* expander,
|
|||||||
|
|
||||||
auto playback = std::make_unique<AudioPlayback>();
|
auto playback = std::make_unique<AudioPlayback>();
|
||||||
|
|
||||||
// Configure the pipeline
|
Pipeline *pipeline = new Pipeline(sink.get());
|
||||||
playback->ConnectElements(source.get(), codec.get());
|
pipeline->AddInput(codec.get())->AddInput(source.get());
|
||||||
playback->ConnectElements(codec.get(), sink.get());
|
|
||||||
|
|
||||||
// Launch!
|
task::Start(pipeline);
|
||||||
StartAudioTask("src", {}, source);
|
|
||||||
StartAudioTask("dec", {}, codec);
|
|
||||||
StartAudioTask("sink", 0, sink);
|
|
||||||
|
|
||||||
playback->input_handle_ = source->InputEventQueue();
|
|
||||||
|
|
||||||
return playback;
|
return playback;
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioPlayback::AudioPlayback() {}
|
AudioPlayback::AudioPlayback() {
|
||||||
|
// Create everything
|
||||||
|
auto source = std::make_shared<FatfsAudioInput>();
|
||||||
|
auto codec = std::make_shared<AudioDecoder>();
|
||||||
|
|
||||||
AudioPlayback::~AudioPlayback() {}
|
auto sink_res = I2SAudioOutput::create(expander);
|
||||||
|
if (sink_res.has_error()) {
|
||||||
|
return cpp::fail(ERR_INIT_ELEMENT);
|
||||||
|
}
|
||||||
|
auto sink = sink_res.value();
|
||||||
|
|
||||||
|
auto playback = std::make_unique<AudioPlayback>();
|
||||||
|
|
||||||
|
Pipeline *pipeline = new Pipeline(sink.get());
|
||||||
|
pipeline->AddInput(codec.get())->AddInput(source.get());
|
||||||
|
|
||||||
|
task::Start(pipeline);
|
||||||
|
|
||||||
|
return playback;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioPlayback::~AudioPlayback() {
|
||||||
|
pipeline_->Quit();
|
||||||
|
}
|
||||||
|
|
||||||
auto AudioPlayback::Play(const std::string& filename) -> void {
|
auto AudioPlayback::Play(const std::string& filename) -> void {
|
||||||
StreamInfo info;
|
// TODO: concurrency, yo!
|
||||||
info.path = filename;
|
file_source->OpenFile(filename);
|
||||||
auto event = StreamEvent::CreateStreamInfo(input_handle_, info);
|
pipeline_->Play();
|
||||||
xQueueSend(input_handle_, &event, portMAX_DELAY);
|
|
||||||
event = StreamEvent::CreateEndOfStream(input_handle_);
|
|
||||||
xQueueSend(input_handle_, &event, portMAX_DELAY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto AudioPlayback::LogStatus() -> void {
|
auto AudioPlayback::LogStatus() -> void {
|
||||||
auto event = StreamEvent::CreateLogStatus();
|
// TODO.
|
||||||
xQueueSendToFront(input_handle_, &event, portMAX_DELAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto AudioPlayback::ConnectElements(IAudioElement* src, IAudioElement* sink)
|
|
||||||
-> void {
|
|
||||||
src->OutputEventQueue(sink->InputEventQueue());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace audio
|
} // namespace audio
|
||||||
|
|||||||
+98
-116
@@ -2,16 +2,20 @@
|
|||||||
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "cbor.h"
|
#include "cbor.h"
|
||||||
|
#include "esp_err.h"
|
||||||
#include "esp_heap_caps.h"
|
#include "esp_heap_caps.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "freertos/portmacro.h"
|
#include "freertos/portmacro.h"
|
||||||
#include "freertos/projdefs.h"
|
#include "freertos/projdefs.h"
|
||||||
#include "freertos/queue.h"
|
#include "freertos/queue.h"
|
||||||
|
#include "pipeline.hpp"
|
||||||
#include "span.hpp"
|
#include "span.hpp"
|
||||||
|
|
||||||
#include "arena.hpp"
|
#include "arena.hpp"
|
||||||
@@ -25,25 +29,26 @@
|
|||||||
|
|
||||||
namespace audio {
|
namespace audio {
|
||||||
|
|
||||||
static const char* kTag = "task";
|
namespace task {
|
||||||
|
|
||||||
auto StartAudioTask(const std::string& name,
|
static const char* kTag = "task";
|
||||||
std::optional<BaseType_t> core_id,
|
static const std::size_t kStackSize = 24 * 1024;
|
||||||
std::shared_ptr<IAudioElement> element) -> void {
|
static const uint8_t kAudioCore = 0;
|
||||||
auto task_handle = std::make_unique<TaskHandle_t>();
|
|
||||||
|
auto Start(Pipeline* pipeline) -> Handle* {
|
||||||
|
auto input_queue = xQueueCreate(8, 1);
|
||||||
|
|
||||||
// Newly created task will free this.
|
// Newly created task will free this.
|
||||||
AudioTaskArgs* args = new AudioTaskArgs{.element = element};
|
AudioTaskArgs* args = new AudioTaskArgs{
|
||||||
|
.pipeline = pipeline,
|
||||||
|
.input = input_queue,
|
||||||
|
};
|
||||||
|
|
||||||
ESP_LOGI(kTag, "starting audio task %s", name.c_str());
|
ESP_LOGI(kTag, "starting audio task");
|
||||||
if (core_id) {
|
xTaskCreatePinnedToCore(&AudioTaskMain, "pipeline", kStackSize, args,
|
||||||
xTaskCreatePinnedToCore(&AudioTaskMain, name.c_str(),
|
kTaskPriorityAudio, NULL, kAudioCore);
|
||||||
element->StackSizeBytes(), args, kTaskPriorityAudio,
|
|
||||||
task_handle.get(), *core_id);
|
return new Handle(input_queue);
|
||||||
} else {
|
|
||||||
xTaskCreate(&AudioTaskMain, name.c_str(), element->StackSizeBytes(), args,
|
|
||||||
kTaskPriorityAudio, task_handle.get());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioTaskMain(void* args) {
|
void AudioTaskMain(void* args) {
|
||||||
@@ -51,113 +56,88 @@ void AudioTaskMain(void* args) {
|
|||||||
// called before the task quits.
|
// called before the task quits.
|
||||||
{
|
{
|
||||||
AudioTaskArgs* real_args = reinterpret_cast<AudioTaskArgs*>(args);
|
AudioTaskArgs* real_args = reinterpret_cast<AudioTaskArgs*>(args);
|
||||||
std::shared_ptr<IAudioElement> element = std::move(real_args->element);
|
std::unique_ptr<Pipeline> pipeline(real_args->pipeline);
|
||||||
|
QueueHandle_t input;
|
||||||
|
StreamBufferHandle_t output;
|
||||||
delete real_args;
|
delete real_args;
|
||||||
|
|
||||||
// Queue of events that we have received on our input queue, but not yet
|
std::vector<Pipeline*> elements = pipeline->GetIterationOrder();
|
||||||
// processed.
|
std::size_t max_inputs =
|
||||||
std::deque<std::unique_ptr<StreamEvent>> pending_events;
|
(*std::max_element(elements.begin(), elements.end(),
|
||||||
|
[](Pipeline const* first, Pipeline const* second) {
|
||||||
|
return first->NumInputs() < second->NumInputs();
|
||||||
|
}))
|
||||||
|
->NumInputs();
|
||||||
|
|
||||||
// TODO(jacqueline): quit event
|
// We need to be able to simultaneously map all of an element's inputs, plus
|
||||||
while (true) {
|
// its output. So preallocate that many ranges.
|
||||||
// First, we pull events from our input queue into pending_events. This
|
std::vector<MappableRegion<kPipelineBufferSize>> in_regions(max_inputs);
|
||||||
// keeps us responsive to any events that need to be handled immediately.
|
MappableRegion<kPipelineBufferSize> out_region;
|
||||||
// Then we check if there's any events to flush downstream.
|
std::for_each(in_regions.begin(), in_regions.end(),
|
||||||
// Then we pass anything requiring processing to the element.
|
[](const MappableRegion<kBufferSize>& region) {
|
||||||
|
assert(region.is_valid);
|
||||||
|
});
|
||||||
|
assert(out_region.is_valid);
|
||||||
|
|
||||||
bool has_work_to_do =
|
// Each element has exactly one output buffer.
|
||||||
(!pending_events.empty() || element->HasUnflushedOutput() ||
|
std::vector<HimemAlloc<kPipelineBufferSize>> buffers(elements.size());
|
||||||
element->HasUnprocessedInput()) &&
|
std::vector<StreamInfo> buffer_infos(buffers.size());
|
||||||
!element->IsOverBuffered();
|
std::for_each(buffers.begin(), buffers.end(),
|
||||||
|
[](const HimemAlloc<kPipelineBufferSize>& alloc) {
|
||||||
|
assert(alloc.is_valid);
|
||||||
|
});
|
||||||
|
|
||||||
if (has_work_to_do) {
|
bool playing = true;
|
||||||
ESP_LOGD(kTag, "checking for events");
|
bool quit = false;
|
||||||
} else {
|
while (!quit) {
|
||||||
ESP_LOGD(kTag, "waiting for events");
|
// TODO: full event here?
|
||||||
}
|
Command cmd;
|
||||||
|
bool has_cmd = xQueueReceive(input, &cmd, 0);
|
||||||
// If we have no new events to process and the element has nothing left to
|
if (has_cmd) {
|
||||||
// do, then just delay forever waiting for a new event.
|
switch (cmd) {
|
||||||
TickType_t ticks_to_wait = has_work_to_do ? 0 : portMAX_DELAY;
|
case PLAY:
|
||||||
|
playing = true;
|
||||||
StreamEvent* new_event = nullptr;
|
|
||||||
bool has_event =
|
|
||||||
xQueueReceive(element->InputEventQueue(), &new_event, ticks_to_wait);
|
|
||||||
|
|
||||||
if (has_event) {
|
|
||||||
if (new_event->tag == StreamEvent::UNINITIALISED) {
|
|
||||||
ESP_LOGE(kTag, "discarding invalid event!!");
|
|
||||||
} else if (new_event->tag == StreamEvent::CHUNK_NOTIFICATION) {
|
|
||||||
delete new_event;
|
|
||||||
} else if (new_event->tag == StreamEvent::LOG_STATUS) {
|
|
||||||
element->ProcessLogStatus();
|
|
||||||
if (element->OutputEventQueue() != nullptr) {
|
|
||||||
xQueueSendToFront(element->OutputEventQueue(), &new_event, 0);
|
|
||||||
} else {
|
|
||||||
delete new_event;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// This isn't an event that needs to be actioned immediately. Add it
|
|
||||||
// to our work queue.
|
|
||||||
pending_events.emplace_back(new_event);
|
|
||||||
ESP_LOGD(kTag, "deferring event");
|
|
||||||
}
|
|
||||||
// Loop again, so that we service all incoming events before doing our
|
|
||||||
// possibly expensive processing.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element->HasUnflushedOutput()) {
|
|
||||||
ESP_LOGD(kTag, "flushing output");
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have no new events. Next, see if there's anything that needs to be
|
|
||||||
// flushed.
|
|
||||||
if (element->HasUnflushedOutput() && !element->FlushBufferedOutput()) {
|
|
||||||
// We had things to flush, but couldn't send it all. This probably
|
|
||||||
// implies that the downstream element is having issues servicing its
|
|
||||||
// input queue, so hold off for a moment before retrying.
|
|
||||||
ESP_LOGW(kTag, "failed to flush buffered output");
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(100));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element->HasUnprocessedInput()) {
|
|
||||||
ESP_LOGD(kTag, "processing input events");
|
|
||||||
element->Process();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The element ran out of data, so now it's time to let it process more
|
|
||||||
// input.
|
|
||||||
while (!pending_events.empty()) {
|
|
||||||
std::unique_ptr<StreamEvent> event;
|
|
||||||
pending_events.front().swap(event);
|
|
||||||
pending_events.pop_front();
|
|
||||||
ESP_LOGD(kTag, "processing event, tag %i", event->tag);
|
|
||||||
|
|
||||||
if (event->tag == StreamEvent::STREAM_INFO) {
|
|
||||||
ESP_LOGD(kTag, "processing stream info");
|
|
||||||
|
|
||||||
element->ProcessStreamInfo(*event->stream_info);
|
|
||||||
|
|
||||||
} else if (event->tag == StreamEvent::ARENA_CHUNK) {
|
|
||||||
ESP_LOGD(kTag, "processing arena data");
|
|
||||||
|
|
||||||
memory::ArenaRef ref(event->arena_chunk);
|
|
||||||
auto callback =
|
|
||||||
StreamEvent::CreateChunkNotification(element->InputEventQueue());
|
|
||||||
if (!xQueueSend(event->source, &callback, 0)) {
|
|
||||||
ESP_LOGW(kTag, "failed to send chunk notif");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(jacqueline): Consider giving the element a full ArenaRef here,
|
|
||||||
// so that it can hang on to it and potentially save an alloc+copy.
|
|
||||||
element->ProcessChunk({ref.ptr.start, ref.ptr.used_size});
|
|
||||||
|
|
||||||
// TODO: think about whether to do the whole queue
|
|
||||||
break;
|
break;
|
||||||
|
case PAUSE:
|
||||||
|
playing = false;
|
||||||
|
break;
|
||||||
|
case QUIT:
|
||||||
|
quit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (quit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playing) {
|
||||||
|
for (int i = 0; i < elements.size(); i++) {
|
||||||
|
std::vector<MutableStream> in_streams;
|
||||||
|
elements.at(i)->InStreams(&in_regions, &in_streams);
|
||||||
|
MutableStream out_stream = elements.at(i)->OutStream(&out_region);
|
||||||
|
|
||||||
|
// Crop the input and output streams to the ranges that are safe to
|
||||||
|
// touch. For the input streams, this is the region that contains
|
||||||
|
// data. For the output stream, this is the region that does *not*
|
||||||
|
// already contain data.
|
||||||
|
std::vector<Stream> cropped_in_streams;
|
||||||
|
std::for_each(in_streams.begin(), in_streams.end(),
|
||||||
|
[&](MutableStream& s) {
|
||||||
|
cropped_in_streams.emplace_back(
|
||||||
|
s.info, s.data.first(s.info->bytes_in_stream));
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.at(i)->OutputElement()->Process(&cropped_in_streams,
|
||||||
|
&out_stream);
|
||||||
|
|
||||||
|
for (int stream = 0; stream < in_streams.size(); stream++) {
|
||||||
|
MutableStream& orig_stream = in_streams.at(stream);
|
||||||
|
Stream& cropped_stream = cropped_in_streams.at(stream);
|
||||||
|
std::move(cropped_stream.data.begin(), cropped_stream.data.end(),
|
||||||
|
orig_stream.data.begin());
|
||||||
|
orig_stream.info->bytes_in_stream =
|
||||||
|
cropped_stream.data.size_bytes();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,4 +145,6 @@ void AudioTaskMain(void* args) {
|
|||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace task
|
||||||
|
|
||||||
} // namespace audio
|
} // namespace audio
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
#include "arena.hpp"
|
#include "arena.hpp"
|
||||||
#include "esp_heap_caps.h"
|
#include "esp_heap_caps.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "ff.h"
|
||||||
#include "freertos/portmacro.h"
|
#include "freertos/portmacro.h"
|
||||||
|
|
||||||
#include "audio_element.hpp"
|
#include "audio_element.hpp"
|
||||||
@@ -15,43 +17,23 @@
|
|||||||
#include "stream_event.hpp"
|
#include "stream_event.hpp"
|
||||||
#include "stream_info.hpp"
|
#include "stream_info.hpp"
|
||||||
#include "stream_message.hpp"
|
#include "stream_message.hpp"
|
||||||
|
#include "types.hpp"
|
||||||
|
|
||||||
static const char* kTag = "SRC";
|
static const char* kTag = "SRC";
|
||||||
|
|
||||||
namespace audio {
|
namespace audio {
|
||||||
|
|
||||||
static const std::size_t kChunkSize = 24 * 1024;
|
FatfsAudioInput::FatfsAudioInput()
|
||||||
static const std::size_t kChunkReadahead = 2;
|
: IAudioElement(), current_file_(), is_file_open_(false) {}
|
||||||
|
|
||||||
FatfsAudioInput::FatfsAudioInput(std::shared_ptr<drivers::SdStorage> storage)
|
|
||||||
: IAudioElement(),
|
|
||||||
arena_(kChunkSize, kChunkReadahead, MALLOC_CAP_SPIRAM),
|
|
||||||
storage_(storage),
|
|
||||||
current_file_(),
|
|
||||||
is_file_open_(false) {}
|
|
||||||
|
|
||||||
FatfsAudioInput::~FatfsAudioInput() {}
|
FatfsAudioInput::~FatfsAudioInput() {}
|
||||||
|
|
||||||
auto FatfsAudioInput::HasUnprocessedInput() -> bool {
|
auto FatfsAudioInput::OpenFile(const std::string& path) -> void {
|
||||||
return is_file_open_;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto FatfsAudioInput::IsOverBuffered() -> bool {
|
|
||||||
return arena_.BlocksFree() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto FatfsAudioInput::ProcessStreamInfo(const StreamInfo& info) -> void {
|
|
||||||
if (is_file_open_) {
|
if (is_file_open_) {
|
||||||
f_close(¤t_file_);
|
f_close(¤t_file_);
|
||||||
is_file_open_ = false;
|
is_file_open_ = false;
|
||||||
}
|
}
|
||||||
|
ESP_LOGI(kTag, "opening file %s", path.c_str());
|
||||||
if (!info.path) {
|
|
||||||
// TODO(jacqueline): Handle errors.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ESP_LOGI(kTag, "opening file %s", info.path->c_str());
|
|
||||||
std::string path = *info.path;
|
|
||||||
FRESULT res = f_open(¤t_file_, path.c_str(), FA_READ);
|
FRESULT res = f_open(¤t_file_, path.c_str(), FA_READ);
|
||||||
if (res != FR_OK) {
|
if (res != FR_OK) {
|
||||||
ESP_LOGE(kTag, "failed to open file! res: %i", res);
|
ESP_LOGE(kTag, "failed to open file! res: %i", res);
|
||||||
@@ -60,52 +42,31 @@ auto FatfsAudioInput::ProcessStreamInfo(const StreamInfo& info) -> void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is_file_open_ = true;
|
is_file_open_ = true;
|
||||||
|
|
||||||
StreamInfo new_info(info);
|
|
||||||
new_info.chunk_size = kChunkSize;
|
|
||||||
ESP_LOGI(kTag, "chunk size: %u bytes", kChunkSize);
|
|
||||||
|
|
||||||
auto event = StreamEvent::CreateStreamInfo(input_events_, new_info);
|
|
||||||
SendOrBufferEvent(std::unique_ptr<StreamEvent>(event));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto FatfsAudioInput::ProcessChunk(const cpp::span<std::byte>& chunk) -> void {}
|
auto FatfsAudioInput::Process(std::vector<Stream>* inputs,
|
||||||
|
MutableStream* output) -> void {
|
||||||
auto FatfsAudioInput::ProcessEndOfStream() -> void {
|
if (!is_file_open_) {
|
||||||
if (is_file_open_) {
|
|
||||||
f_close(¤t_file_);
|
|
||||||
is_file_open_ = false;
|
|
||||||
SendOrBufferEvent(std::unique_ptr<StreamEvent>(
|
|
||||||
StreamEvent::CreateEndOfStream(input_events_)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto FatfsAudioInput::Process() -> void {
|
|
||||||
if (is_file_open_) {
|
|
||||||
auto dest_block = memory::ArenaRef::Acquire(&arena_);
|
|
||||||
if (!dest_block) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
FRESULT result = f_read(¤t_file_, dest_block->ptr.start,
|
FRESULT result =
|
||||||
dest_block->ptr.size, &dest_block->ptr.used_size);
|
f_read(¤t_file_, output->data.data(), output->data.size_bytes(),
|
||||||
|
&output->info->bytes_in_stream);
|
||||||
if (result != FR_OK) {
|
if (result != FR_OK) {
|
||||||
ESP_LOGE(kTag, "file I/O error %d", result);
|
ESP_LOGE(kTag, "file I/O error %d", result);
|
||||||
// TODO(jacqueline): Handle errors.
|
// TODO(jacqueline): Handle errors.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dest_block->ptr.used_size < dest_block->ptr.size ||
|
// TODO: read from filename?
|
||||||
|
output->info->data = StreamInfo::Encoded{codecs::STREAM_MP3};
|
||||||
|
|
||||||
|
if (output->info->bytes_in_stream < output->data.size_bytes() ||
|
||||||
f_eof(¤t_file_)) {
|
f_eof(¤t_file_)) {
|
||||||
f_close(¤t_file_);
|
f_close(¤t_file_);
|
||||||
is_file_open_ = false;
|
is_file_open_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto dest_event = std::unique_ptr<StreamEvent>(
|
|
||||||
StreamEvent::CreateArenaChunk(input_events_, dest_block->Release()));
|
|
||||||
|
|
||||||
SendOrBufferEvent(std::move(dest_event));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace audio
|
} // namespace audio
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "i2s_audio_output.hpp"
|
#include "i2s_audio_output.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "freertos/portmacro.h"
|
#include "freertos/portmacro.h"
|
||||||
@@ -10,14 +11,12 @@
|
|||||||
#include "freertos/projdefs.h"
|
#include "freertos/projdefs.h"
|
||||||
#include "gpio_expander.hpp"
|
#include "gpio_expander.hpp"
|
||||||
#include "result.hpp"
|
#include "result.hpp"
|
||||||
|
#include "stream_info.hpp"
|
||||||
|
|
||||||
static const TickType_t kIdleTimeBeforeMute = pdMS_TO_TICKS(1000);
|
|
||||||
static const char* kTag = "I2SOUT";
|
static const char* kTag = "I2SOUT";
|
||||||
|
|
||||||
namespace audio {
|
namespace audio {
|
||||||
|
|
||||||
static const std::size_t kDmaQueueLength = 8;
|
|
||||||
|
|
||||||
auto I2SAudioOutput::create(drivers::GpioExpander* expander)
|
auto I2SAudioOutput::create(drivers::GpioExpander* expander)
|
||||||
-> cpp::result<std::shared_ptr<I2SAudioOutput>, Error> {
|
-> cpp::result<std::shared_ptr<I2SAudioOutput>, Error> {
|
||||||
// First, we need to perform initial configuration of the DAC chip.
|
// First, we need to perform initial configuration of the DAC chip.
|
||||||
@@ -38,40 +37,26 @@ auto I2SAudioOutput::create(drivers::GpioExpander* expander)
|
|||||||
|
|
||||||
I2SAudioOutput::I2SAudioOutput(drivers::GpioExpander* expander,
|
I2SAudioOutput::I2SAudioOutput(drivers::GpioExpander* expander,
|
||||||
std::unique_ptr<drivers::AudioDac> dac)
|
std::unique_ptr<drivers::AudioDac> dac)
|
||||||
: expander_(expander),
|
: expander_(expander), dac_(std::move(dac)), current_config_() {}
|
||||||
dac_(std::move(dac)),
|
|
||||||
chunk_reader_(),
|
|
||||||
latest_chunk_() {}
|
|
||||||
|
|
||||||
I2SAudioOutput::~I2SAudioOutput() {}
|
I2SAudioOutput::~I2SAudioOutput() {}
|
||||||
|
|
||||||
auto I2SAudioOutput::HasUnprocessedInput() -> bool {
|
auto I2SAudioOutput::ProcessStreamInfo(const StreamInfo& info) -> bool {
|
||||||
return latest_chunk_.size() > 0;
|
if (!std::holds_alternative<StreamInfo::Pcm>(info.data)) {
|
||||||
}
|
|
||||||
|
|
||||||
auto I2SAudioOutput::IsOverBuffered() -> bool {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto I2SAudioOutput::ProcessStreamInfo(const StreamInfo& info) -> void {
|
StreamInfo::Pcm pcm = std::get<StreamInfo::Pcm>(info.data);
|
||||||
// TODO(jacqueline): probs do something with the channel hey
|
|
||||||
|
|
||||||
if (!info.bits_per_sample || !info.sample_rate) {
|
if (current_config_ && pcm == *current_config_) {
|
||||||
ESP_LOGE(kTag, "audio stream missing bits or sample rate");
|
return true;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!info.chunk_size) {
|
ESP_LOGI(kTag, "incoming audio stream: %u bpp @ %u Hz", pcm.bits_per_sample,
|
||||||
ESP_LOGE(kTag, "audio stream missing chunk size");
|
pcm.sample_rate);
|
||||||
return;
|
|
||||||
}
|
|
||||||
chunk_reader_.emplace(*info.chunk_size);
|
|
||||||
|
|
||||||
ESP_LOGI(kTag, "incoming audio stream: %u bpp @ %u Hz", *info.bits_per_sample,
|
|
||||||
*info.sample_rate);
|
|
||||||
|
|
||||||
drivers::AudioDac::BitsPerSample bps;
|
drivers::AudioDac::BitsPerSample bps;
|
||||||
switch (*info.bits_per_sample) {
|
switch (pcm.bits_per_sample) {
|
||||||
case 16:
|
case 16:
|
||||||
bps = drivers::AudioDac::BPS_16;
|
bps = drivers::AudioDac::BPS_16;
|
||||||
break;
|
break;
|
||||||
@@ -83,11 +68,11 @@ auto I2SAudioOutput::ProcessStreamInfo(const StreamInfo& info) -> void {
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
ESP_LOGE(kTag, "dropping stream with unknown bps");
|
ESP_LOGE(kTag, "dropping stream with unknown bps");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
drivers::AudioDac::SampleRate sample_rate;
|
drivers::AudioDac::SampleRate sample_rate;
|
||||||
switch (*info.sample_rate) {
|
switch (pcm.sample_rate) {
|
||||||
case 44100:
|
case 44100:
|
||||||
sample_rate = drivers::AudioDac::SAMPLE_RATE_44_1;
|
sample_rate = drivers::AudioDac::SAMPLE_RATE_44_1;
|
||||||
break;
|
break;
|
||||||
@@ -96,37 +81,25 @@ auto I2SAudioOutput::ProcessStreamInfo(const StreamInfo& info) -> void {
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
ESP_LOGE(kTag, "dropping stream with unknown rate");
|
ESP_LOGE(kTag, "dropping stream with unknown rate");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(jacqueline): probs do something with the channel hey
|
||||||
|
|
||||||
dac_->Reconfigure(bps, sample_rate);
|
dac_->Reconfigure(bps, sample_rate);
|
||||||
|
current_config_ = pcm;
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto I2SAudioOutput::ProcessChunk(const cpp::span<std::byte>& chunk) -> void {
|
auto I2SAudioOutput::Process(std::vector<Stream>* inputs, MutableStream* output)
|
||||||
latest_chunk_ = chunk_reader_->HandleNewData(chunk);
|
-> void {
|
||||||
|
std::for_each(inputs->begin(), inputs->end(), [&](Stream& s) {
|
||||||
|
if (ProcessStreamInfo(s.info)) {
|
||||||
|
std::size_t bytes_written = dac_->WriteData(s.data);
|
||||||
|
s.data = s.data.subspan(bytes_written);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
auto I2SAudioOutput::ProcessEndOfStream() -> void {
|
|
||||||
dac_->Stop();
|
|
||||||
SendOrBufferEvent(std::unique_ptr<StreamEvent>(
|
|
||||||
StreamEvent::CreateEndOfStream(input_events_)));
|
|
||||||
}
|
|
||||||
|
|
||||||
auto I2SAudioOutput::ProcessLogStatus() -> void {
|
|
||||||
dac_->LogStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto I2SAudioOutput::Process() -> void {
|
|
||||||
// Note: avoid logging here! We need to get bytes from the chunk buffer into
|
|
||||||
// the I2S DMA buffer as fast as possible, to avoid running out of samples.
|
|
||||||
std::size_t bytes_written = dac_->WriteData(latest_chunk_);
|
|
||||||
if (bytes_written == latest_chunk_.size_bytes()) {
|
|
||||||
latest_chunk_ = cpp::span<std::byte>();
|
|
||||||
chunk_reader_->HandleBytesLeftOver(0);
|
|
||||||
} else {
|
|
||||||
latest_chunk_ = latest_chunk_.subspan(bytes_written);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto I2SAudioOutput::SetVolume(uint8_t volume) -> void {
|
auto I2SAudioOutput::SetVolume(uint8_t volume) -> void {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "chunk.hpp"
|
#include "chunk.hpp"
|
||||||
#include "ff.h"
|
#include "ff.h"
|
||||||
@@ -10,6 +11,7 @@
|
|||||||
|
|
||||||
#include "audio_element.hpp"
|
#include "audio_element.hpp"
|
||||||
#include "codec.hpp"
|
#include "codec.hpp"
|
||||||
|
#include "stream_info.hpp"
|
||||||
|
|
||||||
namespace audio {
|
namespace audio {
|
||||||
|
|
||||||
@@ -22,28 +24,21 @@ class AudioDecoder : public IAudioElement {
|
|||||||
AudioDecoder();
|
AudioDecoder();
|
||||||
~AudioDecoder();
|
~AudioDecoder();
|
||||||
|
|
||||||
auto StackSizeBytes() const -> std::size_t override { return 10 * 1024; };
|
auto Process(std::vector<Stream>* inputs, MutableStream* output)
|
||||||
|
-> void override;
|
||||||
auto HasUnprocessedInput() -> bool override;
|
|
||||||
auto IsOverBuffered() -> bool override;
|
|
||||||
|
|
||||||
auto ProcessStreamInfo(const StreamInfo& info) -> void override;
|
|
||||||
auto ProcessChunk(const cpp::span<std::byte>& chunk) -> void override;
|
|
||||||
auto ProcessEndOfStream() -> void override;
|
|
||||||
auto Process() -> void override;
|
|
||||||
|
|
||||||
AudioDecoder(const AudioDecoder&) = delete;
|
AudioDecoder(const AudioDecoder&) = delete;
|
||||||
AudioDecoder& operator=(const AudioDecoder&) = delete;
|
AudioDecoder& operator=(const AudioDecoder&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
memory::Arena arena_;
|
|
||||||
std::unique_ptr<codecs::ICodec> current_codec_;
|
std::unique_ptr<codecs::ICodec> current_codec_;
|
||||||
std::optional<StreamInfo> stream_info_;
|
std::optional<StreamInfo> stream_info_;
|
||||||
std::optional<ChunkReader> chunk_reader_;
|
|
||||||
|
|
||||||
bool has_sent_stream_info_;
|
bool has_set_stream_info_;
|
||||||
bool has_samples_to_send_;
|
bool has_samples_to_send_;
|
||||||
bool needs_more_input_;
|
bool needs_more_input_;
|
||||||
|
|
||||||
|
auto ProcessStreamInfo(const StreamInfo& info) -> bool;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace audio
|
} // namespace audio
|
||||||
|
|||||||
@@ -40,62 +40,8 @@ class IAudioElement {
|
|||||||
IAudioElement();
|
IAudioElement();
|
||||||
virtual ~IAudioElement();
|
virtual ~IAudioElement();
|
||||||
|
|
||||||
/*
|
virtual auto Process(std::vector<Stream>* inputs, MutableStream* output)
|
||||||
* Returns the stack size in bytes that this element requires. This should
|
-> void = 0;
|
||||||
* be tuned according to the observed stack size of each element, as different
|
|
||||||
* elements have fairly different stack requirements (particular decoders).
|
|
||||||
*/
|
|
||||||
virtual auto StackSizeBytes() const -> std::size_t { return 4096; };
|
|
||||||
|
|
||||||
/* Returns this element's input buffer. */
|
|
||||||
auto InputEventQueue() const -> QueueHandle_t { return input_events_; }
|
|
||||||
/* Returns this element's output buffer. */
|
|
||||||
auto OutputEventQueue() const -> QueueHandle_t { return output_events_; }
|
|
||||||
auto OutputEventQueue(const QueueHandle_t q) -> void { output_events_ = q; }
|
|
||||||
|
|
||||||
virtual auto HasUnprocessedInput() -> bool = 0;
|
|
||||||
|
|
||||||
virtual auto IsOverBuffered() -> bool { return false; }
|
|
||||||
|
|
||||||
auto HasUnflushedOutput() -> bool { return !buffered_output_.empty(); }
|
|
||||||
auto FlushBufferedOutput() -> bool;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Called when a StreamInfo message is received. Used to configure this
|
|
||||||
* element in preperation for incoming chunks.
|
|
||||||
*/
|
|
||||||
virtual auto ProcessStreamInfo(const StreamInfo& info) -> void = 0;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Called when a ChunkHeader message is received. Includes the data associated
|
|
||||||
* with this chunk of stream data. This method should return the number of
|
|
||||||
* bytes in this chunk that were actually used; leftover bytes will be
|
|
||||||
* prepended to the next call.
|
|
||||||
*/
|
|
||||||
virtual auto ProcessChunk(const cpp::span<std::byte>& chunk) -> void = 0;
|
|
||||||
|
|
||||||
virtual auto ProcessEndOfStream() -> void = 0;
|
|
||||||
|
|
||||||
virtual auto ProcessLogStatus() -> void {}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Called when there has been no data received over the input buffer for some
|
|
||||||
* time. This could be used to synthesize output, or to save memory by
|
|
||||||
* releasing unused resources.
|
|
||||||
*/
|
|
||||||
virtual auto Process() -> void = 0;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
auto SendOrBufferEvent(std::unique_ptr<StreamEvent> event) -> bool;
|
|
||||||
|
|
||||||
// Queue for events coming into this element. Owned by us.
|
|
||||||
QueueHandle_t input_events_;
|
|
||||||
// Queue for events going into the next element. Not owned by us, may be null
|
|
||||||
// if we're not yet in a pipeline.
|
|
||||||
// FIXME: it would be nicer if this was non-nullable.
|
|
||||||
QueueHandle_t output_events_;
|
|
||||||
// Output events that have been generated, but are yet to be sent downstream.
|
|
||||||
std::deque<std::unique_ptr<StreamEvent>> buffered_output_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace audio
|
} // namespace audio
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "audio_task.hpp"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
|
#include "fatfs_audio_input.hpp"
|
||||||
#include "result.hpp"
|
#include "result.hpp"
|
||||||
#include "span.hpp"
|
#include "span.hpp"
|
||||||
|
|
||||||
@@ -23,12 +25,10 @@ namespace audio {
|
|||||||
class AudioPlayback {
|
class AudioPlayback {
|
||||||
public:
|
public:
|
||||||
enum Error { ERR_INIT_ELEMENT, ERR_MEM };
|
enum Error { ERR_INIT_ELEMENT, ERR_MEM };
|
||||||
static auto create(drivers::GpioExpander* expander,
|
static auto create(drivers::GpioExpander* expander)
|
||||||
std::shared_ptr<drivers::SdStorage> storage)
|
|
||||||
-> cpp::result<std::unique_ptr<AudioPlayback>, Error>;
|
-> cpp::result<std::unique_ptr<AudioPlayback>, Error>;
|
||||||
|
|
||||||
// TODO(jacqueline): configure on the fly once we have things to configure.
|
AudioPlayback(FatfsAudioInput *file_input);
|
||||||
AudioPlayback();
|
|
||||||
~AudioPlayback();
|
~AudioPlayback();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -44,9 +44,10 @@ class AudioPlayback {
|
|||||||
AudioPlayback& operator=(const AudioPlayback&) = delete;
|
AudioPlayback& operator=(const AudioPlayback&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
auto ConnectElements(IAudioElement* src, IAudioElement* sink) -> void;
|
FatfsAudioInput *file_source;
|
||||||
|
|
||||||
QueueHandle_t input_handle_;
|
std::vector<std::unique_ptr<IAudioElement>> all_elements_;
|
||||||
|
std::unique_ptr<task::Handle> pipeline_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace audio
|
} // namespace audio
|
||||||
|
|||||||
@@ -6,17 +6,38 @@
|
|||||||
|
|
||||||
#include "audio_element.hpp"
|
#include "audio_element.hpp"
|
||||||
#include "freertos/portmacro.h"
|
#include "freertos/portmacro.h"
|
||||||
|
#include "pipeline.hpp"
|
||||||
|
|
||||||
namespace audio {
|
namespace audio {
|
||||||
|
|
||||||
|
namespace task {
|
||||||
struct AudioTaskArgs {
|
struct AudioTaskArgs {
|
||||||
std::shared_ptr<IAudioElement>& element;
|
Pipeline* pipeline;
|
||||||
|
QueueHandle_t input;
|
||||||
};
|
};
|
||||||
|
|
||||||
auto StartAudioTask(const std::string& name,
|
extern "C" void AudioTaskMain(void* args);
|
||||||
std::optional<BaseType_t> core_id,
|
|
||||||
std::shared_ptr<IAudioElement> element) -> void;
|
|
||||||
|
|
||||||
void AudioTaskMain(void* args);
|
enum Command { PLAY, PAUSE, QUIT };
|
||||||
|
|
||||||
|
class Handle {
|
||||||
|
public:
|
||||||
|
explicit Handle(QueueHandle_t input);
|
||||||
|
~Handle();
|
||||||
|
|
||||||
|
auto SetStreamInfo() -> void;
|
||||||
|
auto Play() -> void;
|
||||||
|
auto Pause() -> void;
|
||||||
|
auto Quit() -> void;
|
||||||
|
|
||||||
|
auto OutputBuffer() -> StreamBufferHandle_t;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QueueHandle_t input;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto Start(Pipeline* pipeline) -> Handle*;
|
||||||
|
|
||||||
|
} // namespace task
|
||||||
|
|
||||||
} // namespace audio
|
} // namespace audio
|
||||||
|
|||||||
@@ -3,41 +3,36 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "arena.hpp"
|
#include "arena.hpp"
|
||||||
#include "chunk.hpp"
|
#include "chunk.hpp"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
|
|
||||||
|
#include "ff.h"
|
||||||
#include "freertos/message_buffer.h"
|
#include "freertos/message_buffer.h"
|
||||||
#include "freertos/queue.h"
|
#include "freertos/queue.h"
|
||||||
#include "span.hpp"
|
#include "span.hpp"
|
||||||
|
|
||||||
#include "audio_element.hpp"
|
#include "audio_element.hpp"
|
||||||
#include "storage.hpp"
|
|
||||||
#include "stream_buffer.hpp"
|
#include "stream_buffer.hpp"
|
||||||
|
|
||||||
namespace audio {
|
namespace audio {
|
||||||
|
|
||||||
class FatfsAudioInput : public IAudioElement {
|
class FatfsAudioInput : public IAudioElement {
|
||||||
public:
|
public:
|
||||||
explicit FatfsAudioInput(std::shared_ptr<drivers::SdStorage> storage);
|
explicit FatfsAudioInput();
|
||||||
~FatfsAudioInput();
|
~FatfsAudioInput();
|
||||||
|
|
||||||
auto HasUnprocessedInput() -> bool override;
|
auto OpenFile(const std::string& path) -> void;
|
||||||
auto IsOverBuffered() -> bool override;
|
|
||||||
|
|
||||||
auto ProcessStreamInfo(const StreamInfo& info) -> void override;
|
auto Process(std::vector<Stream>* inputs, MutableStream* output)
|
||||||
auto ProcessChunk(const cpp::span<std::byte>& chunk) -> void override;
|
-> void override;
|
||||||
auto ProcessEndOfStream() -> void override;
|
|
||||||
auto Process() -> void override;
|
|
||||||
|
|
||||||
FatfsAudioInput(const FatfsAudioInput&) = delete;
|
FatfsAudioInput(const FatfsAudioInput&) = delete;
|
||||||
FatfsAudioInput& operator=(const FatfsAudioInput&) = delete;
|
FatfsAudioInput& operator=(const FatfsAudioInput&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
memory::Arena arena_;
|
|
||||||
std::shared_ptr<drivers::SdStorage> storage_;
|
|
||||||
|
|
||||||
FIL current_file_;
|
FIL current_file_;
|
||||||
bool is_file_open_;
|
bool is_file_open_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "audio_element.hpp"
|
#include "audio_element.hpp"
|
||||||
#include "chunk.hpp"
|
#include "chunk.hpp"
|
||||||
@@ -9,6 +10,7 @@
|
|||||||
|
|
||||||
#include "dac.hpp"
|
#include "dac.hpp"
|
||||||
#include "gpio_expander.hpp"
|
#include "gpio_expander.hpp"
|
||||||
|
#include "stream_info.hpp"
|
||||||
|
|
||||||
namespace audio {
|
namespace audio {
|
||||||
|
|
||||||
@@ -22,14 +24,8 @@ class I2SAudioOutput : public IAudioElement {
|
|||||||
std::unique_ptr<drivers::AudioDac> dac);
|
std::unique_ptr<drivers::AudioDac> dac);
|
||||||
~I2SAudioOutput();
|
~I2SAudioOutput();
|
||||||
|
|
||||||
auto HasUnprocessedInput() -> bool override;
|
auto Process(std::vector<Stream>* inputs, MutableStream* output)
|
||||||
auto IsOverBuffered() -> bool override;
|
-> void override;
|
||||||
|
|
||||||
auto ProcessStreamInfo(const StreamInfo& info) -> void override;
|
|
||||||
auto ProcessChunk(const cpp::span<std::byte>& chunk) -> void override;
|
|
||||||
auto ProcessEndOfStream() -> void override;
|
|
||||||
auto ProcessLogStatus() -> void override;
|
|
||||||
auto Process() -> void override;
|
|
||||||
|
|
||||||
I2SAudioOutput(const I2SAudioOutput&) = delete;
|
I2SAudioOutput(const I2SAudioOutput&) = delete;
|
||||||
I2SAudioOutput& operator=(const I2SAudioOutput&) = delete;
|
I2SAudioOutput& operator=(const I2SAudioOutput&) = delete;
|
||||||
@@ -40,8 +36,9 @@ class I2SAudioOutput : public IAudioElement {
|
|||||||
drivers::GpioExpander* expander_;
|
drivers::GpioExpander* expander_;
|
||||||
std::unique_ptr<drivers::AudioDac> dac_;
|
std::unique_ptr<drivers::AudioDac> dac_;
|
||||||
|
|
||||||
std::optional<ChunkReader> chunk_reader_;
|
std::optional<StreamInfo::Pcm> current_config_;
|
||||||
cpp::span<std::byte> latest_chunk_;
|
|
||||||
|
auto ProcessStreamInfo(const StreamInfo& info) -> bool;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace audio
|
} // namespace audio
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "freertos/portmacro.h"
|
||||||
|
|
||||||
|
#include "audio_element.hpp"
|
||||||
|
#include "himem.hpp"
|
||||||
|
#include "stream_info.hpp"
|
||||||
|
|
||||||
|
namespace audio {
|
||||||
|
|
||||||
|
static const std::size_t kPipelineBufferSize = 32 * 1024;
|
||||||
|
|
||||||
|
class Pipeline {
|
||||||
|
public:
|
||||||
|
Pipeline(IAudioElement* output);
|
||||||
|
~Pipeline();
|
||||||
|
auto AddInput(IAudioElement* input) -> Pipeline*;
|
||||||
|
|
||||||
|
auto OutputElement() const -> IAudioElement*;
|
||||||
|
|
||||||
|
auto NumInputs() const -> std::size_t;
|
||||||
|
|
||||||
|
auto InStreams(std::vector<MappableRegion<kPipelineBufferSize>>*,
|
||||||
|
std::vector<MutableStream>*) -> void;
|
||||||
|
|
||||||
|
auto OutStream(MappableRegion<kPipelineBufferSize>*) -> MutableStream;
|
||||||
|
|
||||||
|
auto GetIterationOrder() -> std::vector<Pipeline*>;
|
||||||
|
|
||||||
|
private:
|
||||||
|
IAudioElement* root_;
|
||||||
|
std::vector<std::unique_ptr<Pipeline>> subtrees_;
|
||||||
|
|
||||||
|
HimemAlloc<kPipelineBufferSize> output_buffer_;
|
||||||
|
StreamInfo output_info_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace audio
|
||||||
@@ -4,19 +4,68 @@
|
|||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
#include "cbor.h"
|
|
||||||
#include "result.hpp"
|
#include "result.hpp"
|
||||||
#include "sys/_stdint.h"
|
#include "span.hpp"
|
||||||
|
#include "types.hpp"
|
||||||
|
|
||||||
namespace audio {
|
namespace audio {
|
||||||
|
|
||||||
struct StreamInfo {
|
struct StreamInfo {
|
||||||
std::optional<std::string> path;
|
// The number of bytes that are available for consumption within this
|
||||||
std::optional<uint8_t> channels;
|
// stream's buffer.
|
||||||
std::optional<uint8_t> bits_per_sample;
|
std::size_t bytes_in_stream;
|
||||||
std::optional<uint16_t> sample_rate;
|
|
||||||
std::optional<size_t> chunk_size;
|
// The total length of this stream, in case its source is finite (e.g. a
|
||||||
|
// file on disk). May be absent for endless streams (internet streams,
|
||||||
|
// generated audio, etc.)
|
||||||
|
std::optional<std::size_t> length_bytes;
|
||||||
|
|
||||||
|
struct Encoded {
|
||||||
|
// The codec that this stream is associated with.
|
||||||
|
codecs::StreamType type;
|
||||||
|
|
||||||
|
bool operator==(const Encoded&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Pcm {
|
||||||
|
// Number of channels in this stream.
|
||||||
|
uint8_t channels;
|
||||||
|
// Number of bits per sample.
|
||||||
|
uint8_t bits_per_sample;
|
||||||
|
// The sample rate.
|
||||||
|
uint16_t sample_rate;
|
||||||
|
|
||||||
|
bool operator==(const Pcm&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::variant<Encoded, Pcm> data;
|
||||||
|
|
||||||
|
bool operator==(const StreamInfo&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MutableStream {
|
||||||
|
public:
|
||||||
|
StreamInfo* info;
|
||||||
|
cpp::span<std::byte> data;
|
||||||
|
|
||||||
|
MutableStream(StreamInfo* i, cpp::span<std::byte> d)
|
||||||
|
: info(i), data(d) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A byte buffer + associated metadata, which is not allowed to modify any of
|
||||||
|
* the underlying data.
|
||||||
|
*/
|
||||||
|
class Stream {
|
||||||
|
public:
|
||||||
|
explicit Stream(const MutableStream& s) : info(*s.info), data(s.data) {}
|
||||||
|
|
||||||
|
const StreamInfo& info;
|
||||||
|
// `data` itself left mutable for signalling how much of the stream was
|
||||||
|
// consumed
|
||||||
|
cpp::span<const std::byte> data;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace audio
|
} // namespace audio
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#include "pipeline.hpp"
|
||||||
|
#include "stream_info.hpp"
|
||||||
|
|
||||||
|
namespace audio {
|
||||||
|
|
||||||
|
Pipeline::Pipeline(IAudioElement* output) : root_(output), subtrees_() {}
|
||||||
|
Pipeline::~Pipeline() {}
|
||||||
|
|
||||||
|
auto Pipeline::AddInput(IAudioElement* input) -> Pipeline* {
|
||||||
|
subtrees_.emplace_back(input);
|
||||||
|
return subtrees_.back().get();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Pipeline::OutputElement() const -> IAudioElement* {
|
||||||
|
return root_;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Pipeline::NumInputs() const -> std::size_t {
|
||||||
|
return subtrees_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Pipeline::InStreams(
|
||||||
|
std::vector<MappableRegion<kPipelineBufferSize>>* regions,
|
||||||
|
std::vector<MutableStream>* out) -> void {
|
||||||
|
for (int i = 0; i < subtrees_.size(); i++) {
|
||||||
|
MutableStream s = subtrees_[i]->OutStream(®ions->at(i));
|
||||||
|
out->push_back(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Pipeline::OutStream(MappableRegion<kPipelineBufferSize>* region)
|
||||||
|
-> MutableStream {
|
||||||
|
return {&output_info_, region->Map(output_buffer_)};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Pipeline::GetIterationOrder() -> std::vector<Pipeline*> {
|
||||||
|
std::vector<Pipeline*> to_search{this};
|
||||||
|
std::vector<Pipeline*> found;
|
||||||
|
|
||||||
|
while (!to_search.empty()) {
|
||||||
|
Pipeline* current = to_search.back();
|
||||||
|
to_search.pop_back();
|
||||||
|
found.push_back(current);
|
||||||
|
|
||||||
|
to_search.insert(to_search.end(), current->subtrees_.begin(),
|
||||||
|
current->subtrees_.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace audio
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include "result.hpp"
|
#include "result.hpp"
|
||||||
#include "span.hpp"
|
#include "span.hpp"
|
||||||
|
#include "types.hpp"
|
||||||
|
|
||||||
namespace codecs {
|
namespace codecs {
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ class ICodec {
|
|||||||
public:
|
public:
|
||||||
virtual ~ICodec() {}
|
virtual ~ICodec() {}
|
||||||
|
|
||||||
virtual auto CanHandleFile(const std::string& path) -> bool = 0;
|
virtual auto CanHandleType(StreamType type) -> bool = 0;
|
||||||
|
|
||||||
struct OutputFormat {
|
struct OutputFormat {
|
||||||
uint8_t num_channels;
|
uint8_t num_channels;
|
||||||
@@ -31,7 +32,7 @@ class ICodec {
|
|||||||
|
|
||||||
virtual auto ResetForNewStream() -> void = 0;
|
virtual auto ResetForNewStream() -> void = 0;
|
||||||
|
|
||||||
virtual auto SetInput(cpp::span<std::byte> input) -> void = 0;
|
virtual auto SetInput(cpp::span<const std::byte> input) -> void = 0;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Returns the codec's next read position within the input buffer. If the
|
* Returns the codec's next read position within the input buffer. If the
|
||||||
@@ -63,7 +64,7 @@ class ICodec {
|
|||||||
|
|
||||||
enum CreateCodecError { UNKNOWN_EXTENSION };
|
enum CreateCodecError { UNKNOWN_EXTENSION };
|
||||||
|
|
||||||
auto CreateCodecForFile(const std::string& file)
|
auto CreateCodecForType(StreamType type)
|
||||||
-> cpp::result<std::unique_ptr<ICodec>, CreateCodecError>;
|
-> cpp::result<std::unique_ptr<ICodec>, CreateCodecError>;
|
||||||
|
|
||||||
} // namespace codecs
|
} // namespace codecs
|
||||||
|
|||||||
+2
-1
@@ -192,7 +192,8 @@ auto AudioDac::Reconfigure(BitsPerSample bps, SampleRate rate) -> void {
|
|||||||
WriteRegister(Register::POWER_MODE, 0);
|
WriteRegister(Register::POWER_MODE, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto AudioDac::WriteData(cpp::span<std::byte> data) -> std::size_t {
|
auto AudioDac::WriteData(const cpp::span<const std::byte>& data)
|
||||||
|
-> std::size_t {
|
||||||
std::size_t bytes_written = 0;
|
std::size_t bytes_written = 0;
|
||||||
esp_err_t err = i2s_channel_write(i2s_handle_, data.data(), data.size_bytes(),
|
esp_err_t err = i2s_channel_write(i2s_handle_, data.data(), data.size_bytes(),
|
||||||
&bytes_written, 0);
|
&bytes_written, 0);
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class AudioDac {
|
|||||||
// TODO(jacqueline): worth supporting channels here as well?
|
// TODO(jacqueline): worth supporting channels here as well?
|
||||||
auto Reconfigure(BitsPerSample bps, SampleRate rate) -> void;
|
auto Reconfigure(BitsPerSample bps, SampleRate rate) -> void;
|
||||||
|
|
||||||
auto WriteData(cpp::span<std::byte> data) -> std::size_t;
|
auto WriteData(const cpp::span<const std::byte>& data) -> std::size_t;
|
||||||
|
|
||||||
auto Stop() -> void;
|
auto Stop() -> void;
|
||||||
auto LogStatus() -> void;
|
auto LogStatus() -> void;
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
idf_component_register(SRCS "arena.cpp" INCLUDE_DIRS "include" REQUIRES "span")
|
idf_component_register(SRCS "arena.cpp" INCLUDE_DIRS "include" REQUIRES "span" "esp_psram")
|
||||||
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})
|
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "esp32/himem.h"
|
||||||
|
#include "span.hpp"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Wrapper around an ESP-IDF himem allocation, which uses RAII to clean up after
|
||||||
|
* itself.
|
||||||
|
*/
|
||||||
|
template <std::size_t size>
|
||||||
|
class HimemAlloc {
|
||||||
|
public:
|
||||||
|
esp_himem_handle_t handle;
|
||||||
|
const bool is_valid;
|
||||||
|
|
||||||
|
HimemAlloc() : is_valid(esp_himem_alloc(size, &handle) == ESP_OK) {}
|
||||||
|
|
||||||
|
~HimemAlloc() {
|
||||||
|
if (is_valid) {
|
||||||
|
esp_himem_free(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not copyable or movable.
|
||||||
|
HimemAlloc(const HimemAlloc&) = delete;
|
||||||
|
HimemAlloc& operator=(const HimemAlloc&) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Wrapper around an ESP-IDF himem allocation, which maps a HimemAlloc into the
|
||||||
|
* usable address space. Instances always contain the last memory region that
|
||||||
|
* was mapped within them.
|
||||||
|
*/
|
||||||
|
template <std::size_t size>
|
||||||
|
class MappableRegion {
|
||||||
|
private:
|
||||||
|
std::byte* bytes_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
esp_himem_rangehandle_t range_handle;
|
||||||
|
const bool is_valid;
|
||||||
|
|
||||||
|
MappableRegion()
|
||||||
|
: bytes_(nullptr),
|
||||||
|
is_valid(esp_himem_alloc_map_range(size, &range_handle) == ESP_OK) {}
|
||||||
|
|
||||||
|
~MappableRegion() {
|
||||||
|
if (bytes_ != nullptr) {
|
||||||
|
esp_himem_unmap(range_handle, bytes_, size);
|
||||||
|
}
|
||||||
|
if (is_valid) {
|
||||||
|
esp_himem_free_map_range(range_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Get() -> cpp::span<std::byte> {
|
||||||
|
if (bytes_ != nullptr) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {bytes_, size};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Map(const HimemAlloc<size> &alloc) -> cpp::span<std::byte> {
|
||||||
|
if (bytes_ != nullptr) {
|
||||||
|
ESP_ERROR_CHECK(esp_himem_unmap(range_handle, bytes_, size));
|
||||||
|
}
|
||||||
|
ESP_ERROR_CHECK(esp_himem_map(alloc.handle, range_handle, 0, 0, size, 0,
|
||||||
|
reinterpret_cast<void**>(&bytes_)));
|
||||||
|
return Get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not copyable or movable.
|
||||||
|
MappableRegion(const MappableRegion&) = delete;
|
||||||
|
MappableRegion& operator=(const MappableRegion&) = delete;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user