New pipeline building, still needs proper control

custom
jacqueline 2 years ago
parent 3817ec0c77
commit 7c6fd654f5
  1. 2
      src/audio/CMakeLists.txt
  2. 97
      src/audio/audio_decoder.cpp
  3. 56
      src/audio/audio_element.cpp
  4. 48
      src/audio/audio_playback.cpp
  5. 129
      src/audio/audio_task.cpp
  6. 20
      src/audio/fatfs_audio_input.cpp
  7. 23
      src/audio/i2s_audio_output.cpp
  8. 8
      src/audio/include/audio_decoder.hpp
  9. 8
      src/audio/include/audio_element.hpp
  10. 10
      src/audio/include/audio_playback.hpp
  11. 22
      src/audio/include/audio_sink.hpp
  12. 34
      src/audio/include/audio_task.hpp
  13. 3
      src/audio/include/fatfs_audio_input.hpp
  14. 11
      src/audio/include/i2s_audio_output.hpp
  15. 7
      src/audio/include/pipeline.hpp
  16. 69
      src/audio/include/stream_info.hpp
  17. 14
      src/audio/pipeline.cpp
  18. 2
      src/codecs/codec.cpp
  19. 4
      src/codecs/include/mad.hpp
  20. 7
      src/codecs/mad.cpp
  21. 6
      src/drivers/dac.cpp
  22. 2
      src/drivers/include/dac.hpp
  23. 4
      src/main/main.cpp
  24. 2
      src/memory/include/himem.hpp

@ -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" "pipeline.cpp" "audio_playback.cpp" "stream_event.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")

@ -26,22 +26,22 @@ static const char* kTag = "DEC";
AudioDecoder::AudioDecoder() AudioDecoder::AudioDecoder()
: IAudioElement(), : IAudioElement(),
stream_info_({}), current_codec_(),
has_samples_to_send_(false), current_input_format_(),
needs_more_input_(true) {} current_output_format_(),
has_samples_to_send_(false) {}
AudioDecoder::~AudioDecoder() {} AudioDecoder::~AudioDecoder() {}
auto AudioDecoder::ProcessStreamInfo(const StreamInfo& info) -> bool { auto AudioDecoder::ProcessStreamInfo(const StreamInfo& info) -> bool {
if (!std::holds_alternative<StreamInfo::Encoded>(info.data)) { if (!std::holds_alternative<StreamInfo::Encoded>(info.format)) {
return false; return false;
} }
const auto& encoded = std::get<StreamInfo::Encoded>(info.data); const auto& encoded = std::get<StreamInfo::Encoded>(info.format);
// 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_->CanHandleType(encoded.type)) { current_codec_->CanHandleType(encoded.type)) {
current_codec_->ResetForNewStream(); current_codec_->ResetForNewStream();
@ -60,43 +60,47 @@ auto AudioDecoder::ProcessStreamInfo(const StreamInfo& info) -> bool {
return true; return true;
} }
auto AudioDecoder::Process(std::vector<Stream>* inputs, MutableStream* output) auto AudioDecoder::Process(const std::vector<InputStream>& inputs,
-> void { OutputStream* output) -> void {
// We don't really expect multiple inputs, so just pick the first that // We don't really expect multiple inputs, so just pick the first that
// contains data. If none of them contain data, then we can still flush // contains data. If none of them contain data, then we can still flush
// pending samples. // pending samples.
auto input = auto input = std::find_if(
std::find_if(inputs->begin(), inputs->end(), inputs.begin(), inputs.end(),
[](const Stream& s) { return s.data.size_bytes() > 0; }); [](const InputStream& s) { return s.data().size_bytes() > 0; });
if (input == inputs.end()) {
if (input != inputs->end()) { input = inputs.begin();
const StreamInfo* info = input->info; }
if (!stream_info_ || *stream_info_ != *info) {
// The input stream has changed! Immediately throw everything away and const StreamInfo& info = input->info();
// start from scratch. if (!current_input_format_ || *current_input_format_ != info.format) {
// TODO: special case gapless playback? needs thought. // The input stream has changed! Immediately throw everything away and
stream_info_ = *info; // start from scratch.
has_samples_to_send_ = false; current_input_format_ = info.format;
has_set_stream_info_ = false; has_samples_to_send_ = false;
ProcessStreamInfo(*info);
}
current_codec_->SetInput(input->data); ProcessStreamInfo(info);
} }
current_codec_->SetInput(input->data());
while (true) { while (true) {
if (has_samples_to_send_) { if (has_samples_to_send_) {
if (!has_set_stream_info_) { if (!current_output_format_) {
has_set_stream_info_ = true;
auto format = current_codec_->GetOutputFormat(); auto format = current_codec_->GetOutputFormat();
output->info->data.emplace<StreamInfo::Pcm>( current_output_format_ = StreamInfo::Pcm{
format.bits_per_sample, format.sample_rate_hz, format.num_channels); .channels = format.num_channels,
.bits_per_sample = format.bits_per_sample,
.sample_rate = format.sample_rate_hz,
};
}
if (!output->prepare(*current_output_format_)) {
break;
} }
auto write_res = current_codec_->WriteOutputSamples( auto write_res = current_codec_->WriteOutputSamples(output->data());
output->data.subspan(output->info->bytes_in_stream)); output->add(write_res.first);
output->info->bytes_in_stream += write_res.first;
has_samples_to_send_ = !write_res.second; has_samples_to_send_ = !write_res.second;
if (has_samples_to_send_) { if (has_samples_to_send_) {
@ -106,26 +110,23 @@ auto AudioDecoder::Process(std::vector<Stream>* inputs, MutableStream* output)
} }
} }
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; }
}
input->data = input->data.subspan(current_codec_->GetInputPosition());
if (res.value()) { if (res.value()) {
// We're out of data in this buffer. Finish immediately; there's nothing // We're out of useable data in this buffer. Finish immediately; there's
// to send. // nothing to send.
break; input->mark_incomplete();
} else {
has_samples_to_send_ = true;
}
} else {
// No input; nothing to do.
break; break;
} else {
has_samples_to_send_ = true;
} }
} }
input->consume(current_codec_->GetInputPosition());
} }
} // namespace audio } // namespace audio

@ -1,56 +0,0 @@
#include "audio_element.hpp"
#include <memory>
namespace audio {
IAudioElement::IAudioElement()
: input_events_(xQueueCreate(kEventQueueSize, sizeof(void*))),
output_events_(nullptr),
buffered_output_() {}
IAudioElement::~IAudioElement() {
// Ensure we don't leak any memory from events leftover in the queue.
while (uxQueueSpacesAvailable(input_events_) < kEventQueueSize) {
StreamEvent* event;
if (xQueueReceive(input_events_, &event, 0)) {
free(event);
} else {
break;
}
}
// Technically there's a race here if someone is still adding to the queue,
// but hopefully the whole pipeline is stopped if an element is being
// destroyed.
vQueueDelete(input_events_);
}
auto IAudioElement::SendOrBufferEvent(std::unique_ptr<StreamEvent> event)
-> bool {
if (!buffered_output_.empty()) {
// To ensure we send data in order, don't try to send if we've already
// failed to send something.
buffered_output_.push_back(std::move(event));
return false;
}
StreamEvent* raw_event = event.release();
if (!xQueueSend(output_events_, &raw_event, 0)) {
event.reset(raw_event);
buffered_output_.push_back(std::move(event));
return false;
}
return true;
}
auto IAudioElement::FlushBufferedOutput() -> bool {
while (!buffered_output_.empty()) {
StreamEvent* raw_event = buffered_output_.front().release();
buffered_output_.pop_front();
if (!xQueueSend(output_events_, &raw_event, 0)) {
buffered_output_.emplace_front(raw_event);
return false;
}
}
return true;
}
} // namespace audio

@ -8,6 +8,7 @@
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
#include "audio_decoder.hpp" #include "audio_decoder.hpp"
#include "audio_element.hpp"
#include "audio_task.hpp" #include "audio_task.hpp"
#include "chunk.hpp" #include "chunk.hpp"
#include "fatfs_audio_input.hpp" #include "fatfs_audio_input.hpp"
@ -23,55 +24,30 @@ namespace audio {
auto AudioPlayback::create(drivers::GpioExpander* expander) auto AudioPlayback::create(drivers::GpioExpander* expander)
-> cpp::result<std::unique_ptr<AudioPlayback>, Error> { -> cpp::result<std::unique_ptr<AudioPlayback>, Error> {
// Create everything
auto source = std::make_shared<FatfsAudioInput>();
auto codec = std::make_shared<AudioDecoder>();
auto sink_res = I2SAudioOutput::create(expander); auto sink_res = I2SAudioOutput::create(expander);
if (sink_res.has_error()) { if (sink_res.has_error()) {
return cpp::fail(ERR_INIT_ELEMENT); return cpp::fail(ERR_INIT_ELEMENT);
} }
auto sink = sink_res.value(); return std::make_unique<AudioPlayback>(std::move(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() { AudioPlayback::AudioPlayback(std::unique_ptr<I2SAudioOutput> output)
// Create everything : file_source_(), i2s_output_(std::move(output)) {
auto source = std::make_shared<FatfsAudioInput>(); AudioDecoder* codec = new AudioDecoder();
auto codec = std::make_shared<AudioDecoder>(); elements_.emplace_back(codec);
auto sink_res = I2SAudioOutput::create(expander); Pipeline* pipeline = new Pipeline(elements_.front().get());
if (sink_res.has_error()) { pipeline->AddInput(file_source_.get());
return cpp::fail(ERR_INIT_ELEMENT);
}
auto sink = sink_res.value();
auto playback = std::make_unique<AudioPlayback>();
Pipeline *pipeline = new Pipeline(sink.get()); task::StartPipeline(pipeline, i2s_output_.get());
pipeline->AddInput(codec.get())->AddInput(source.get()); task::StartDrain(i2s_output_.get());
task::Start(pipeline);
return playback;
} }
AudioPlayback::~AudioPlayback() { AudioPlayback::~AudioPlayback() {}
pipeline_->Quit();
}
auto AudioPlayback::Play(const std::string& filename) -> void { auto AudioPlayback::Play(const std::string& filename) -> void {
// TODO: concurrency, yo! // TODO: concurrency, yo!
file_source->OpenFile(filename); file_source_->OpenFile(filename);
pipeline_->Play();
} }
auto AudioPlayback::LogStatus() -> void { auto AudioPlayback::LogStatus() -> void {

@ -8,7 +8,9 @@
#include <deque> #include <deque>
#include <memory> #include <memory>
#include "audio_sink.hpp"
#include "cbor.h" #include "cbor.h"
#include "dac.hpp"
#include "esp_err.h" #include "esp_err.h"
#include "esp_heap_caps.h" #include "esp_heap_caps.h"
#include "esp_log.h" #include "esp_log.h"
@ -33,22 +35,29 @@ namespace task {
static const char* kTag = "task"; static const char* kTag = "task";
static const std::size_t kStackSize = 24 * 1024; static const std::size_t kStackSize = 24 * 1024;
static const std::size_t kDrainStackSize = 1024;
static const uint8_t kAudioCore = 0; static const uint8_t kAudioCore = 0;
auto Start(Pipeline* pipeline) -> Handle* { auto StartPipeline(Pipeline* pipeline, IAudioSink* sink) -> void {
auto input_queue = xQueueCreate(8, 1);
// Newly created task will free this. // Newly created task will free this.
AudioTaskArgs* args = new AudioTaskArgs{ AudioTaskArgs* args = new AudioTaskArgs{.pipeline = pipeline, .sink = sink};
.pipeline = pipeline,
.input = input_queue,
};
ESP_LOGI(kTag, "starting audio task"); ESP_LOGI(kTag, "starting audio pipeline task");
xTaskCreatePinnedToCore(&AudioTaskMain, "pipeline", kStackSize, args, xTaskCreatePinnedToCore(&AudioTaskMain, "pipeline", kStackSize, args,
kTaskPriorityAudio, NULL, kAudioCore); kTaskPriorityAudio, NULL, kAudioCore);
}
return new Handle(input_queue); auto StartDrain(IAudioSink* sink) -> void {
auto command = new std::atomic<Command>(PLAY);
// Newly created task will free this.
AudioDrainArgs* drain_args = new AudioDrainArgs{
.sink = sink,
.command = command,
};
ESP_LOGI(kTag, "starting audio drain task");
xTaskCreatePinnedToCore(&AudioDrainMain, "drain", kDrainStackSize, drain_args,
kTaskPriorityAudio, NULL, kAudioCore);
} }
void AudioTaskMain(void* args) { void AudioTaskMain(void* args) {
@ -57,10 +66,11 @@ void AudioTaskMain(void* args) {
{ {
AudioTaskArgs* real_args = reinterpret_cast<AudioTaskArgs*>(args); AudioTaskArgs* real_args = reinterpret_cast<AudioTaskArgs*>(args);
std::unique_ptr<Pipeline> pipeline(real_args->pipeline); std::unique_ptr<Pipeline> pipeline(real_args->pipeline);
QueueHandle_t input; IAudioSink* sink = real_args->sink;
StreamBufferHandle_t output;
delete real_args; delete real_args;
std::optional<StreamInfo::Format> output_format;
std::vector<Pipeline*> elements = pipeline->GetIterationOrder(); std::vector<Pipeline*> elements = pipeline->GetIterationOrder();
std::size_t max_inputs = std::size_t max_inputs =
(*std::max_element(elements.begin(), elements.end(), (*std::max_element(elements.begin(), elements.end(),
@ -74,9 +84,7 @@ void AudioTaskMain(void* args) {
std::vector<MappableRegion<kPipelineBufferSize>> in_regions(max_inputs); std::vector<MappableRegion<kPipelineBufferSize>> in_regions(max_inputs);
MappableRegion<kPipelineBufferSize> out_region; MappableRegion<kPipelineBufferSize> out_region;
std::for_each(in_regions.begin(), in_regions.end(), std::for_each(in_regions.begin(), in_regions.end(),
[](const MappableRegion<kBufferSize>& region) { [](const auto& region) { assert(region.is_valid); });
assert(region.is_valid);
});
assert(out_region.is_valid); assert(out_region.is_valid);
// Each element has exactly one output buffer. // Each element has exactly one output buffer.
@ -90,55 +98,68 @@ void AudioTaskMain(void* args) {
bool playing = true; bool playing = true;
bool quit = false; bool quit = false;
while (!quit) { while (!quit) {
// TODO: full event here?
Command cmd;
bool has_cmd = xQueueReceive(input, &cmd, 0);
if (has_cmd) {
switch (cmd) {
case PLAY:
playing = true;
break;
case PAUSE:
playing = false;
break;
case QUIT:
quit = true;
break;
}
}
if (quit) {
break;
}
if (playing) { if (playing) {
for (int i = 0; i < elements.size(); i++) { for (int i = 0; i < elements.size(); i++) {
std::vector<MutableStream> in_streams; std::vector<RawStream> raw_in_streams;
elements.at(i)->InStreams(&in_regions, &in_streams); elements.at(i)->InStreams(&in_regions, &raw_in_streams);
MutableStream out_stream = elements.at(i)->OutStream(&out_region); RawStream raw_out_stream = elements.at(i)->OutStream(&out_region);
// Crop the input and output streams to the ranges that are safe to // Crop the input and output streams to the ranges that are safe to
// touch. For the input streams, this is the region that contains // touch. For the input streams, this is the region that contains
// data. For the output stream, this is the region that does *not* // data. For the output stream, this is the region that does *not*
// already contain data. // already contain data.
std::vector<Stream> cropped_in_streams; std::vector<InputStream> in_streams;
std::for_each(in_streams.begin(), in_streams.end(), std::for_each(raw_in_streams.begin(), raw_in_streams.end(),
[&](MutableStream& s) { [&](RawStream& s) { in_streams.emplace_back(&s); });
cropped_in_streams.emplace_back( OutputStream out_stream(&raw_out_stream);
s.info, s.data.first(s.info->bytes_in_stream));
}); elements.at(i)->OutputElement()->Process(in_streams, &out_stream);
}
elements.at(i)->OutputElement()->Process(&cropped_in_streams,
&out_stream); RawStream raw_sink_stream = elements.back()->OutStream(&out_region);
InputStream sink_stream(&raw_sink_stream);
for (int stream = 0; stream < in_streams.size(); stream++) {
MutableStream& orig_stream = in_streams.at(stream); if (!output_format || output_format != sink_stream.info().format) {
Stream& cropped_stream = cropped_in_streams.at(stream); // The format of the stream within the sink stream has changed. We
std::move(cropped_stream.data.begin(), cropped_stream.data.end(), // need to reconfigure the sink, but shouldn't do so until we've fully
orig_stream.data.begin()); // drained the current buffer.
orig_stream.info->bytes_in_stream = if (xStreamBufferIsEmpty(sink->buffer())) {
cropped_stream.data.size_bytes(); output_format = sink_stream.info().format;
sink->Configure(*output_format);
} }
} }
// We've reconfigured the sink, or it was already configured correctly.
// Send through some data.
if (output_format == sink_stream.info().format) {
// TODO: tune the delay on this, as it's currently the only way to
// throttle this task's CPU time. Maybe also hold off on the pipeline
// if the buffer is already close to full?
std::size_t sent = xStreamBufferSend(
sink->buffer(), sink_stream.data().data(),
sink_stream.data().size_bytes(), pdMS_TO_TICKS(10));
sink_stream.consume(sent);
}
}
}
}
vTaskDelete(NULL);
}
void AudioDrainMain(void* args) {
{
AudioDrainArgs* real_args = reinterpret_cast<AudioDrainArgs*>(args);
IAudioSink* sink = real_args->sink;
std::atomic<Command>* command = real_args->command;
delete real_args;
// TODO(jacqueline): implement PAUSE without busy-waiting.
while (*command != QUIT) {
std::byte buf[64];
std::size_t len =
xStreamBufferReceive(sink->buffer(), buf, sizeof(buf), portMAX_DELAY);
if (len > 0) {
sink->Send({buf, len});
} }
} }
} }

@ -44,26 +44,30 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> void {
is_file_open_ = true; is_file_open_ = true;
} }
auto FatfsAudioInput::Process(std::vector<Stream>* inputs, auto FatfsAudioInput::Process(const std::vector<InputStream>& inputs,
MutableStream* output) -> void { OutputStream* output) -> void {
if (!is_file_open_) { if (!is_file_open_) {
return; return;
} }
StreamInfo::Format format = StreamInfo::Encoded{codecs::STREAM_MP3};
if (!output->prepare(format)) {
return;
}
std::size_t max_size = output->data().size_bytes();
std::size_t size = 0;
FRESULT result = FRESULT result =
f_read(&current_file_, output->data.data(), output->data.size_bytes(), f_read(&current_file_, output->data().data(), max_size, &size);
&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;
} }
// TODO: read from filename? output->add(size);
output->info->data = StreamInfo::Encoded{codecs::STREAM_MP3};
if (output->info->bytes_in_stream < output->data.size_bytes() || if (size < max_size || f_eof(&current_file_)) {
f_eof(&current_file_)) {
f_close(&current_file_); f_close(&current_file_);
is_file_open_ = false; is_file_open_ = false;
} }

@ -1,6 +1,7 @@
#include "i2s_audio_output.hpp" #include "i2s_audio_output.hpp"
#include <algorithm> #include <algorithm>
#include <cstddef>
#include <variant> #include <variant>
#include "esp_err.h" #include "esp_err.h"
@ -18,7 +19,7 @@ static const char* kTag = "I2SOUT";
namespace audio { namespace audio {
auto I2SAudioOutput::create(drivers::GpioExpander* expander) auto I2SAudioOutput::create(drivers::GpioExpander* expander)
-> cpp::result<std::shared_ptr<I2SAudioOutput>, Error> { -> cpp::result<std::unique_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.
auto dac_result = drivers::AudioDac::create(expander); auto dac_result = drivers::AudioDac::create(expander);
if (dac_result.has_error()) { if (dac_result.has_error()) {
@ -32,7 +33,7 @@ auto I2SAudioOutput::create(drivers::GpioExpander* expander)
// dac->WriteVolume(255); // dac->WriteVolume(255);
dac->WriteVolume(120); // for testing dac->WriteVolume(120); // for testing
return std::make_shared<I2SAudioOutput>(expander, std::move(dac)); return std::make_unique<I2SAudioOutput>(expander, std::move(dac));
} }
I2SAudioOutput::I2SAudioOutput(drivers::GpioExpander* expander, I2SAudioOutput::I2SAudioOutput(drivers::GpioExpander* expander,
@ -41,18 +42,18 @@ I2SAudioOutput::I2SAudioOutput(drivers::GpioExpander* expander,
I2SAudioOutput::~I2SAudioOutput() {} I2SAudioOutput::~I2SAudioOutput() {}
auto I2SAudioOutput::ProcessStreamInfo(const StreamInfo& info) -> bool { auto I2SAudioOutput::Configure(const StreamInfo::Format& format) -> bool {
if (!std::holds_alternative<StreamInfo::Pcm>(info.data)) { if (!std::holds_alternative<StreamInfo::Pcm>(format)) {
return false; return false;
} }
StreamInfo::Pcm pcm = std::get<StreamInfo::Pcm>(info.data); StreamInfo::Pcm pcm = std::get<StreamInfo::Pcm>(format);
if (current_config_ && pcm == *current_config_) { if (current_config_ && pcm == *current_config_) {
return true; return true;
} }
ESP_LOGI(kTag, "incoming audio stream: %u bpp @ %u Hz", pcm.bits_per_sample, ESP_LOGI(kTag, "incoming audio stream: %u bpp @ %lu Hz", pcm.bits_per_sample,
pcm.sample_rate); pcm.sample_rate);
drivers::AudioDac::BitsPerSample bps; drivers::AudioDac::BitsPerSample bps;
@ -92,14 +93,8 @@ auto I2SAudioOutput::ProcessStreamInfo(const StreamInfo& info) -> bool {
return true; return true;
} }
auto I2SAudioOutput::Process(std::vector<Stream>* inputs, MutableStream* output) auto I2SAudioOutput::Send(const cpp::span<std::byte>& data) -> void {
-> void { dac_->WriteData(data);
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::SetVolume(uint8_t volume) -> void { auto I2SAudioOutput::SetVolume(uint8_t volume) -> void {

@ -24,7 +24,7 @@ class AudioDecoder : public IAudioElement {
AudioDecoder(); AudioDecoder();
~AudioDecoder(); ~AudioDecoder();
auto Process(std::vector<Stream>* inputs, MutableStream* output) auto Process(const std::vector<InputStream>& inputs, OutputStream* output)
-> void override; -> void override;
AudioDecoder(const AudioDecoder&) = delete; AudioDecoder(const AudioDecoder&) = delete;
@ -32,11 +32,9 @@ class AudioDecoder : public IAudioElement {
private: private:
std::unique_ptr<codecs::ICodec> current_codec_; std::unique_ptr<codecs::ICodec> current_codec_;
std::optional<StreamInfo> stream_info_; std::optional<StreamInfo::Format> current_input_format_;
std::optional<StreamInfo::Format> current_output_format_;
bool has_set_stream_info_;
bool has_samples_to_send_; bool has_samples_to_send_;
bool needs_more_input_;
auto ProcessStreamInfo(const StreamInfo& info) -> bool; auto ProcessStreamInfo(const StreamInfo& info) -> bool;
}; };

@ -37,11 +37,11 @@ static const size_t kEventQueueSize = 8;
*/ */
class IAudioElement { class IAudioElement {
public: public:
IAudioElement(); IAudioElement() {}
virtual ~IAudioElement(); virtual ~IAudioElement() {}
virtual auto Process(std::vector<Stream>* inputs, MutableStream* output) virtual auto Process(const std::vector<InputStream>& inputs,
-> void = 0; OutputStream* output) -> void = 0;
}; };
} // namespace audio } // namespace audio

@ -8,6 +8,7 @@
#include "audio_task.hpp" #include "audio_task.hpp"
#include "esp_err.h" #include "esp_err.h"
#include "fatfs_audio_input.hpp" #include "fatfs_audio_input.hpp"
#include "i2s_audio_output.hpp"
#include "result.hpp" #include "result.hpp"
#include "span.hpp" #include "span.hpp"
@ -28,7 +29,7 @@ class AudioPlayback {
static auto create(drivers::GpioExpander* expander) static auto create(drivers::GpioExpander* expander)
-> cpp::result<std::unique_ptr<AudioPlayback>, Error>; -> cpp::result<std::unique_ptr<AudioPlayback>, Error>;
AudioPlayback(FatfsAudioInput *file_input); explicit AudioPlayback(std::unique_ptr<I2SAudioOutput> output);
~AudioPlayback(); ~AudioPlayback();
/* /*
@ -44,10 +45,9 @@ class AudioPlayback {
AudioPlayback& operator=(const AudioPlayback&) = delete; AudioPlayback& operator=(const AudioPlayback&) = delete;
private: private:
FatfsAudioInput *file_source; std::unique_ptr<FatfsAudioInput> file_source_;
std::unique_ptr<I2SAudioOutput> i2s_output_;
std::vector<std::unique_ptr<IAudioElement>> all_elements_; std::vector<std::unique_ptr<IAudioElement>> elements_;
std::unique_ptr<task::Handle> pipeline_;
}; };
} // namespace audio } // namespace audio

@ -0,0 +1,22 @@
#pragma once
#include "audio_element.hpp"
#include "stream_info.hpp"
namespace audio {
class IAudioSink {
private:
static const std::size_t kDrainBufferSize = 8 * 1024;
StreamBufferHandle_t buffer_;
public:
IAudioSink() : buffer_(xStreamBufferCreate(kDrainBufferSize, 1)) {}
virtual ~IAudioSink() { vStreamBufferDelete(buffer_); }
virtual auto Configure(const StreamInfo::Format& format) -> bool = 0;
virtual auto Send(const cpp::span<std::byte>& data) -> void = 0;
auto buffer() const -> StreamBufferHandle_t { return buffer_; }
};
} // namespace audio

@ -5,38 +5,32 @@
#include <string> #include <string>
#include "audio_element.hpp" #include "audio_element.hpp"
#include "audio_sink.hpp"
#include "dac.hpp"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
#include "pipeline.hpp" #include "pipeline.hpp"
#include "stream_buffer.hpp"
namespace audio { namespace audio {
namespace task { namespace task {
enum Command { PLAY, PAUSE, QUIT };
struct AudioTaskArgs { struct AudioTaskArgs {
Pipeline* pipeline; Pipeline* pipeline;
QueueHandle_t input; IAudioSink* sink;
};
struct AudioDrainArgs {
IAudioSink* sink;
std::atomic<Command>* command;
}; };
extern "C" void AudioTaskMain(void* args); extern "C" void AudioTaskMain(void* args);
extern "C" void AudioDrainMain(void* args);
enum Command { PLAY, PAUSE, QUIT }; auto StartPipeline(Pipeline* pipeline, IAudioSink* sink) -> void;
auto StartDrain(IAudioSink* sink) -> void;
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 task

@ -16,6 +16,7 @@
#include "audio_element.hpp" #include "audio_element.hpp"
#include "stream_buffer.hpp" #include "stream_buffer.hpp"
#include "stream_info.hpp"
namespace audio { namespace audio {
@ -26,7 +27,7 @@ class FatfsAudioInput : public IAudioElement {
auto OpenFile(const std::string& path) -> void; auto OpenFile(const std::string& path) -> void;
auto Process(std::vector<Stream>* inputs, MutableStream* output) auto Process(const std::vector<InputStream>& inputs, OutputStream* output)
-> void override; -> void override;
FatfsAudioInput(const FatfsAudioInput&) = delete; FatfsAudioInput(const FatfsAudioInput&) = delete;

@ -5,6 +5,7 @@
#include <vector> #include <vector>
#include "audio_element.hpp" #include "audio_element.hpp"
#include "audio_sink.hpp"
#include "chunk.hpp" #include "chunk.hpp"
#include "result.hpp" #include "result.hpp"
@ -14,18 +15,18 @@
namespace audio { namespace audio {
class I2SAudioOutput : public IAudioElement { class I2SAudioOutput : public IAudioSink {
public: public:
enum Error { DAC_CONFIG, I2S_CONFIG, STREAM_INIT }; enum Error { DAC_CONFIG, I2S_CONFIG, STREAM_INIT };
static auto create(drivers::GpioExpander* expander) static auto create(drivers::GpioExpander* expander)
-> cpp::result<std::shared_ptr<I2SAudioOutput>, Error>; -> cpp::result<std::unique_ptr<I2SAudioOutput>, Error>;
I2SAudioOutput(drivers::GpioExpander* expander, I2SAudioOutput(drivers::GpioExpander* expander,
std::unique_ptr<drivers::AudioDac> dac); std::unique_ptr<drivers::AudioDac> dac);
~I2SAudioOutput(); ~I2SAudioOutput();
auto Process(std::vector<Stream>* inputs, MutableStream* output) auto Configure(const StreamInfo::Format& format) -> bool override;
-> void override; auto Send(const cpp::span<std::byte>& data) -> void override;
I2SAudioOutput(const I2SAudioOutput&) = delete; I2SAudioOutput(const I2SAudioOutput&) = delete;
I2SAudioOutput& operator=(const I2SAudioOutput&) = delete; I2SAudioOutput& operator=(const I2SAudioOutput&) = delete;
@ -37,8 +38,6 @@ class I2SAudioOutput : public IAudioElement {
std::unique_ptr<drivers::AudioDac> dac_; std::unique_ptr<drivers::AudioDac> dac_;
std::optional<StreamInfo::Pcm> current_config_; std::optional<StreamInfo::Pcm> current_config_;
auto ProcessStreamInfo(const StreamInfo& info) -> bool;
}; };
} // namespace audio } // namespace audio

@ -3,6 +3,7 @@
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <string> #include <string>
#include <vector>
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
@ -16,7 +17,7 @@ static const std::size_t kPipelineBufferSize = 32 * 1024;
class Pipeline { class Pipeline {
public: public:
Pipeline(IAudioElement* output); explicit Pipeline(IAudioElement* output);
~Pipeline(); ~Pipeline();
auto AddInput(IAudioElement* input) -> Pipeline*; auto AddInput(IAudioElement* input) -> Pipeline*;
@ -25,9 +26,9 @@ class Pipeline {
auto NumInputs() const -> std::size_t; auto NumInputs() const -> std::size_t;
auto InStreams(std::vector<MappableRegion<kPipelineBufferSize>>*, auto InStreams(std::vector<MappableRegion<kPipelineBufferSize>>*,
std::vector<MutableStream>*) -> void; std::vector<RawStream>*) -> void;
auto OutStream(MappableRegion<kPipelineBufferSize>*) -> MutableStream; auto OutStream(MappableRegion<kPipelineBufferSize>*) -> RawStream;
auto GetIterationOrder() -> std::vector<Pipeline*>; auto GetIterationOrder() -> std::vector<Pipeline*>;

@ -4,6 +4,8 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <type_traits>
#include <utility>
#include <variant> #include <variant>
#include "result.hpp" #include "result.hpp"
@ -35,37 +37,82 @@ struct StreamInfo {
// Number of bits per sample. // Number of bits per sample.
uint8_t bits_per_sample; uint8_t bits_per_sample;
// The sample rate. // The sample rate.
uint16_t sample_rate; uint32_t sample_rate;
bool operator==(const Pcm&) const = default; bool operator==(const Pcm&) const = default;
}; };
std::variant<Encoded, Pcm> data; typedef std::variant<Encoded, Pcm> Format;
Format format;
bool operator==(const StreamInfo&) const = default; bool operator==(const StreamInfo&) const = default;
}; };
class MutableStream { class RawStream {
public: public:
StreamInfo* info; StreamInfo* info;
cpp::span<std::byte> data; cpp::span<std::byte> data;
bool is_incomplete;
MutableStream(StreamInfo* i, cpp::span<std::byte> d) RawStream(StreamInfo* i, cpp::span<std::byte> d)
: info(i), data(d) {} : info(i), data(d), is_incomplete(false) {}
}; };
/* /*
* A byte buffer + associated metadata, which is not allowed to modify any of * A byte buffer + associated metadata, which is not allowed to modify any of
* the underlying data. * the underlying data.
*/ */
class Stream { class InputStream {
public: public:
explicit Stream(const MutableStream& s) : info(*s.info), data(s.data) {} explicit InputStream(RawStream* s) : raw_(s) {}
const StreamInfo& info; void consume(std::size_t bytes) const {
// `data` itself left mutable for signalling how much of the stream was auto new_data = raw_->data.subspan(bytes);
// consumed std::move(new_data.begin(), new_data.end(), raw_->data.begin());
cpp::span<const std::byte> data; raw_->info->bytes_in_stream = new_data.size_bytes();
}
void mark_incomplete() const { raw_->is_incomplete = true; }
const StreamInfo& info() const { return *raw_->info; }
cpp::span<const std::byte> data() const {
return raw_->data.first(raw_->info->bytes_in_stream);
}
private:
RawStream* raw_;
};
class OutputStream {
public:
explicit OutputStream(RawStream* s) : raw_(s) {}
void add(std::size_t bytes) const { raw_->info->bytes_in_stream += bytes; }
bool prepare(const StreamInfo::Format& new_format) {
if (new_format == raw_->info->format) {
raw_->info->format = new_format;
return true;
}
if (raw_->is_incomplete) {
raw_->info->format = new_format;
raw_->info->bytes_in_stream = 0;
return true;
}
return false;
}
const StreamInfo& info() const { return *raw_->info; }
cpp::span<std::byte> data() const {
return raw_->data.subspan(raw_->info->bytes_in_stream);
}
bool is_incomplete() const { return raw_->is_incomplete; }
private:
RawStream* raw_;
}; };
} // namespace audio } // namespace audio

@ -1,4 +1,5 @@
#include "pipeline.hpp" #include "pipeline.hpp"
#include <memory>
#include "stream_info.hpp" #include "stream_info.hpp"
namespace audio { namespace audio {
@ -7,7 +8,7 @@ Pipeline::Pipeline(IAudioElement* output) : root_(output), subtrees_() {}
Pipeline::~Pipeline() {} Pipeline::~Pipeline() {}
auto Pipeline::AddInput(IAudioElement* input) -> Pipeline* { auto Pipeline::AddInput(IAudioElement* input) -> Pipeline* {
subtrees_.emplace_back(input); subtrees_.push_back(std::make_unique<Pipeline>(input));
return subtrees_.back().get(); return subtrees_.back().get();
} }
@ -21,15 +22,15 @@ auto Pipeline::NumInputs() const -> std::size_t {
auto Pipeline::InStreams( auto Pipeline::InStreams(
std::vector<MappableRegion<kPipelineBufferSize>>* regions, std::vector<MappableRegion<kPipelineBufferSize>>* regions,
std::vector<MutableStream>* out) -> void { std::vector<RawStream>* out) -> void {
for (int i = 0; i < subtrees_.size(); i++) { for (int i = 0; i < subtrees_.size(); i++) {
MutableStream s = subtrees_[i]->OutStream(&regions->at(i)); RawStream s = subtrees_[i]->OutStream(&regions->at(i));
out->push_back(s); out->push_back(s);
} }
} }
auto Pipeline::OutStream(MappableRegion<kPipelineBufferSize>* region) auto Pipeline::OutStream(MappableRegion<kPipelineBufferSize>* region)
-> MutableStream { -> RawStream {
return {&output_info_, region->Map(output_buffer_)}; return {&output_info_, region->Map(output_buffer_)};
} }
@ -42,8 +43,9 @@ auto Pipeline::GetIterationOrder() -> std::vector<Pipeline*> {
to_search.pop_back(); to_search.pop_back();
found.push_back(current); found.push_back(current);
to_search.insert(to_search.end(), current->subtrees_.begin(), for (const auto& i : current->subtrees_) {
current->subtrees_.end()); to_search.push_back(i.get());
}
} }
return found; return found;

@ -5,7 +5,7 @@
namespace codecs { namespace codecs {
auto CreateCodecForFile(const std::string& file) auto CreateCodecForType(StreamType type)
-> cpp::result<std::unique_ptr<ICodec>, CreateCodecError> { -> cpp::result<std::unique_ptr<ICodec>, CreateCodecError> {
return std::make_unique<MadMp3Decoder>(); // TODO. return std::make_unique<MadMp3Decoder>(); // TODO.
} }

@ -17,10 +17,10 @@ class MadMp3Decoder : public ICodec {
MadMp3Decoder(); MadMp3Decoder();
~MadMp3Decoder(); ~MadMp3Decoder();
auto CanHandleFile(const std::string& path) -> bool override; auto CanHandleType(StreamType type) -> bool override;
auto GetOutputFormat() -> OutputFormat override; auto GetOutputFormat() -> OutputFormat override;
auto ResetForNewStream() -> void override; auto ResetForNewStream() -> void override;
auto SetInput(cpp::span<std::byte> input) -> void override; auto SetInput(cpp::span<const std::byte> input) -> void override;
auto GetInputPosition() -> std::size_t override; auto GetInputPosition() -> std::size_t override;
auto ProcessNextFrame() -> cpp::result<bool, ProcessingError> override; auto ProcessNextFrame() -> cpp::result<bool, ProcessingError> override;
auto WriteOutputSamples(cpp::span<std::byte> output) auto WriteOutputSamples(cpp::span<std::byte> output)

@ -5,6 +5,7 @@
#include "mad.h" #include "mad.h"
#include "codec.hpp" #include "codec.hpp"
#include "types.hpp"
namespace codecs { namespace codecs {
@ -35,8 +36,8 @@ MadMp3Decoder::~MadMp3Decoder() {
mad_header_finish(&header_); mad_header_finish(&header_);
} }
auto MadMp3Decoder::CanHandleFile(const std::string& path) -> bool { auto MadMp3Decoder::CanHandleType(StreamType type) -> bool {
return true; // TODO. return type == STREAM_MP3;
} }
auto MadMp3Decoder::GetOutputFormat() -> OutputFormat { auto MadMp3Decoder::GetOutputFormat() -> OutputFormat {
@ -52,7 +53,7 @@ auto MadMp3Decoder::ResetForNewStream() -> void {
has_decoded_header_ = false; has_decoded_header_ = false;
} }
auto MadMp3Decoder::SetInput(cpp::span<std::byte> input) -> void { auto MadMp3Decoder::SetInput(cpp::span<const std::byte> input) -> void {
mad_stream_buffer(&stream_, mad_stream_buffer(&stream_,
reinterpret_cast<const unsigned char*>(input.data()), reinterpret_cast<const unsigned char*>(input.data()),
input.size()); input.size());

@ -192,15 +192,13 @@ auto AudioDac::Reconfigure(BitsPerSample bps, SampleRate rate) -> void {
WriteRegister(Register::POWER_MODE, 0); WriteRegister(Register::POWER_MODE, 0);
} }
auto AudioDac::WriteData(const cpp::span<const std::byte>& data) auto AudioDac::WriteData(const cpp::span<const std::byte>& data) -> void {
-> 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, portMAX_DELAY);
if (err != ESP_ERR_TIMEOUT) { if (err != ESP_ERR_TIMEOUT) {
ESP_ERROR_CHECK(err); ESP_ERROR_CHECK(err);
} }
return bytes_written;
} }
auto AudioDac::Stop() -> void { auto AudioDac::Stop() -> void {

@ -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(const cpp::span<const std::byte>& data) -> std::size_t; auto WriteData(const cpp::span<const std::byte>& data) -> void;
auto Stop() -> void; auto Stop() -> void;
auto LogStatus() -> void; auto LogStatus() -> void;

@ -133,10 +133,10 @@ extern "C" void app_main(void) {
(void*)lvglArgs, 1, sLvglStack, (void*)lvglArgs, 1, sLvglStack,
&sLvglTaskBuffer, 1); &sLvglTaskBuffer, 1);
std::shared_ptr<audio::AudioPlayback> playback; std::unique_ptr<audio::AudioPlayback> playback;
if (storage) { if (storage) {
ESP_LOGI(TAG, "Init audio pipeline"); ESP_LOGI(TAG, "Init audio pipeline");
auto playback_res = audio::AudioPlayback::create(expander, storage); auto playback_res = audio::AudioPlayback::create(expander);
if (playback_res.has_error()) { if (playback_res.has_error()) {
ESP_LOGE(TAG, "Failed! Playback will not work."); ESP_LOGE(TAG, "Failed! Playback will not work.");
} else { } else {

@ -63,7 +63,7 @@ class MappableRegion {
return {bytes_, size}; return {bytes_, size};
} }
auto Map(const HimemAlloc<size> &alloc) -> cpp::span<std::byte> { auto Map(const HimemAlloc<size>& alloc) -> cpp::span<std::byte> {
if (bytes_ != nullptr) { if (bytes_ != nullptr) {
ESP_ERROR_CHECK(esp_himem_unmap(range_handle, bytes_, size)); ESP_ERROR_CHECK(esp_himem_unmap(range_handle, bytes_, size));
} }

Loading…
Cancel
Save