better handling of chunk buffer

custom
jacqueline 2 years ago
parent f35bb64c2b
commit 01be69eca1
  1. 1
      src/audio/CMakeLists.txt
  2. 23
      src/audio/audio_decoder.cpp
  3. 358
      src/audio/audio_playback.cpp
  4. 40
      src/audio/audio_task.cpp
  5. 50
      src/audio/chunk.cpp
  6. 26
      src/audio/fatfs_audio_input.cpp
  7. 7
      src/audio/i2s_audio_output.cpp
  8. 13
      src/audio/include/audio_decoder.hpp
  9. 18
      src/audio/include/audio_element.hpp
  10. 85
      src/audio/include/audio_playback.hpp
  11. 3
      src/audio/include/audio_task.hpp
  12. 13
      src/audio/include/chunk.hpp
  13. 11
      src/audio/include/fatfs_audio_input.hpp
  14. 8
      src/audio/include/i2s_audio_output.hpp
  15. 37
      src/audio/include/stream_buffer.hpp
  16. 5
      src/audio/include/stream_info.hpp
  17. 26
      src/audio/stream_buffer.cpp
  18. 2
      src/codecs/codec.cpp
  19. 2
      src/codecs/include/codec.hpp
  20. 2
      src/drivers/include/storage.hpp
  21. 2
      src/drivers/storage.cpp
  22. 14
      src/main/app_console.cpp
  23. 7
      src/main/app_console.hpp
  24. 21
      src/main/main.cpp

@ -1,6 +1,7 @@
idf_component_register(
SRCS "audio_decoder.cpp" "audio_task.cpp" "chunk.cpp" "fatfs_audio_input.cpp"
"stream_info.cpp" "stream_message.cpp" "i2s_audio_output.cpp"
"stream_buffer.cpp" "audio_playback.cpp"
INCLUDE_DIRS "include"
REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span")

@ -19,26 +19,9 @@ static const char* kTag = "DEC";
namespace audio {
AudioDecoder::AudioDecoder()
: IAudioElement(),
stream_info_({}),
raw_chunk_buffer_(static_cast<std::byte*>(
heap_caps_malloc(kMaxChunkSize, MALLOC_CAP_SPIRAM))),
chunk_buffer_(raw_chunk_buffer_, kMaxChunkSize)
AudioDecoder::AudioDecoder() : IAudioElement(), stream_info_({}) {}
{}
AudioDecoder::~AudioDecoder() {
free(raw_chunk_buffer_);
}
auto AudioDecoder::SetInputBuffer(MessageBufferHandle_t* buffer) -> void {
input_buffer_ = buffer;
}
auto AudioDecoder::SetOutputBuffer(MessageBufferHandle_t* buffer) -> void {
output_buffer_ = buffer;
}
AudioDecoder::~AudioDecoder() {}
auto AudioDecoder::ProcessStreamInfo(const StreamInfo& info)
-> cpp::result<void, AudioProcessingError> {
@ -75,7 +58,7 @@ auto AudioDecoder::ProcessChunk(const cpp::span<std::byte>& chunk)
bool needs_more_input = false;
std::optional<codecs::ICodec::ProcessingError> error = std::nullopt;
WriteChunksToStream(
output_buffer_, chunk_buffer_,
output_buffer_,
[&](cpp::span<std::byte> buffer) -> std::size_t {
std::size_t bytes_written = 0;
// Continue filling up the output buffer so long as we have samples

@ -4,322 +4,88 @@
#include <cstdint>
#include <memory>
#include <string_view>
#include "aac_decoder.h"
#include "amr_decoder.h"
#include "audio_element.h"
#include "audio_event_iface.h"
#include "audio_pipeline.h"
#include "esp_err.h"
#include "flac_decoder.h"
#include "mp3_decoder.h"
#include "ogg_decoder.h"
#include "opus_decoder.h"
#include "wav_decoder.h"
#include "audio_output.hpp"
static const char* kTag = "PLAYBACK";
static const char* kSource = "src";
static const char* kDecoder = "dec";
static const char* kSink = "sink";
static audio_element_status_t toStatus(void* status) {
uintptr_t as_pointer_int = reinterpret_cast<uintptr_t>(status);
return static_cast<audio_element_status_t>(as_pointer_int);
}
static bool endsWith(std::string_view str, std::string_view suffix) {
return str.size() >= suffix.size() &&
0 == str.compare(str.size() - suffix.size(), suffix.size(), suffix);
}
static void toLower(std::string& str) {
std::transform(str.begin(), str.end(), str.begin(),
[](unsigned char c) { return std::tolower(c); });
}
namespace drivers {
auto AudioPlayback::create(std::unique_ptr<IAudioOutput> output)
#include "audio_decoder.hpp"
#include "audio_task.hpp"
#include "chunk.hpp"
#include "fatfs_audio_input.hpp"
#include "freertos/portmacro.h"
#include "gpio_expander.hpp"
#include "i2s_audio_output.hpp"
#include "storage.hpp"
#include "stream_buffer.hpp"
#include "stream_info.hpp"
#include "stream_message.hpp"
namespace audio {
// TODO: idk
static const std::size_t kMinElementBufferSize = 1024;
auto AudioPlayback::create(drivers::GpioExpander* expander,
std::shared_ptr<drivers::SdStorage> storage)
-> cpp::result<std::unique_ptr<AudioPlayback>, Error> {
audio_pipeline_handle_t pipeline;
audio_element_handle_t fatfs_stream_reader;
audio_event_iface_handle_t event_interface;
audio_pipeline_cfg_t pipeline_config =
audio_pipeline_cfg_t(DEFAULT_AUDIO_PIPELINE_CONFIG());
pipeline = audio_pipeline_init(&pipeline_config);
if (pipeline == NULL) {
return cpp::fail(Error::PIPELINE_INIT);
}
// Create everything
auto source = std::make_shared<FatfsAudioInput>(storage);
auto codec = std::make_shared<AudioDecoder>();
fatfs_stream_cfg_t fatfs_stream_config =
fatfs_stream_cfg_t(FATFS_STREAM_CFG_DEFAULT());
fatfs_stream_config.type = AUDIO_STREAM_READER;
fatfs_stream_reader = fatfs_stream_init(&fatfs_stream_config);
if (fatfs_stream_reader == NULL) {
return cpp::fail(Error::FATFS_INIT);
auto sink_res = I2SAudioOutput::create(expander);
if (sink_res.has_error()) {
return cpp::fail(ERR_INIT_ELEMENT);
}
auto sink = sink_res.value();
audio_event_iface_cfg_t event_config = AUDIO_EVENT_IFACE_DEFAULT_CFG();
event_interface = audio_event_iface_init(&event_config);
auto playback = std::make_unique<AudioPlayback>();
audio_pipeline_set_listener(pipeline, event_interface);
audio_element_msg_set_listener(fatfs_stream_reader, event_interface);
audio_element_msg_set_listener(output->GetAudioElement(), event_interface);
// Configure the pipeline
source->InputBuffer(&playback->stream_start_);
sink->OutputBuffer(&playback->stream_end_);
playback->ConnectElements(source.get(), codec.get());
playback->ConnectElements(codec.get(), sink.get());
audio_pipeline_register(pipeline, fatfs_stream_reader, kSource);
audio_pipeline_register(pipeline, output->GetAudioElement(), kSink);
// Launch!
StartAudioTask("src", source);
StartAudioTask("dec", codec);
StartAudioTask("sink", sink);
return std::make_unique<AudioPlayback>(output, pipeline, fatfs_stream_reader,
event_interface);
return playback;
}
AudioPlayback::AudioPlayback(std::unique_ptr<IAudioOutput>& output,
audio_pipeline_handle_t pipeline,
audio_element_handle_t source_element,
audio_event_iface_handle_t event_interface)
: output_(std::move(output)),
pipeline_(pipeline),
source_element_(source_element),
event_interface_(event_interface) {}
// TODO(jacqueline): think about sizes
AudioPlayback::AudioPlayback()
: stream_start_(128, 128), stream_end_(128, 128) {}
AudioPlayback::~AudioPlayback() {
audio_pipeline_remove_listener(pipeline_);
audio_element_msg_remove_listener(source_element_, event_interface_);
audio_element_msg_remove_listener(output_->GetAudioElement(),
event_interface_);
audio_pipeline_stop(pipeline_);
audio_pipeline_wait_for_stop(pipeline_);
audio_pipeline_terminate(pipeline_);
ReconfigurePipeline(NONE);
audio_pipeline_unregister(pipeline_, source_element_);
audio_pipeline_unregister(pipeline_, output_->GetAudioElement());
audio_event_iface_destroy(event_interface_);
audio_pipeline_deinit(pipeline_);
audio_element_deinit(source_element_);
}
void AudioPlayback::Play(const std::string& filename) {
output_->SetSoftMute(true);
if (playback_state_ != STOPPED) {
audio_pipeline_stop(pipeline_);
audio_pipeline_wait_for_stop(pipeline_);
audio_pipeline_terminate(pipeline_);
}
playback_state_ = PLAYING;
Decoder decoder = GetDecoderForFilename(filename);
ReconfigurePipeline(decoder);
audio_element_set_uri(source_element_, filename.c_str());
audio_pipeline_reset_ringbuffer(pipeline_);
audio_pipeline_reset_elements(pipeline_);
audio_pipeline_run(pipeline_);
output_->SetSoftMute(false);
}
void AudioPlayback::Toggle() {
if (playback_state_ == PLAYING) {
Pause();
} else if (playback_state_ == PAUSED) {
Resume();
}
// TODO(jacqueline): signal the end of all things, and maybe wait for it?
}
void AudioPlayback::Resume() {
if (playback_state_ == PAUSED) {
ESP_LOGI(kTag, "resuming");
playback_state_ = PLAYING;
audio_pipeline_resume(pipeline_);
output_->SetSoftMute(false);
}
}
void AudioPlayback::Pause() {
if (GetPlaybackState() == PLAYING) {
ESP_LOGI(kTag, "pausing");
output_->SetSoftMute(true);
playback_state_ = PAUSED;
audio_pipeline_pause(pipeline_);
}
}
auto AudioPlayback::Play(const std::string& filename) -> void {
StreamInfo info;
info.Path(filename);
auto AudioPlayback::GetPlaybackState() const -> PlaybackState {
return playback_state_;
}
std::array<std::byte, 128> dest;
auto len = WriteMessage(
TYPE_STREAM_INFO, [&](auto enc) { return info.Encode(enc); }, dest);
void AudioPlayback::ProcessEvents(uint16_t max_time_ms) {
if (playback_state_ == STOPPED) {
if (len.has_error()) {
// TODO.
return;
}
while (1) {
audio_event_iface_msg_t event;
esp_err_t err = audio_event_iface_listen(event_interface_, &event,
pdMS_TO_TICKS(max_time_ms));
if (err != ESP_OK) {
// Error should only be timeouts, so use a 'failure' as an indication that
// we're out of events to process.
break;
}
if (event.source_type == AUDIO_ELEMENT_TYPE_ELEMENT &&
event.source == (void*)decoder_ &&
event.cmd == AEL_MSG_CMD_REPORT_MUSIC_INFO) {
audio_element_info_t music_info;
audio_element_getinfo(decoder_, &music_info);
ESP_LOGI(kTag, "sample_rate=%d, bits=%d, ch=%d", music_info.sample_rates,
music_info.bits, music_info.channels);
}
if (event.source_type == AUDIO_ELEMENT_TYPE_ELEMENT &&
event.source == (void*)source_element_ &&
event.cmd == AEL_MSG_CMD_REPORT_STATUS) {
audio_element_status_t status = toStatus(event.data);
if (status == AEL_STATUS_STATE_FINISHED) {
// TODO: Could we change the uri here? hmm.
ESP_LOGI(kTag, "finished reading input.");
}
}
if (event.source_type == AUDIO_ELEMENT_TYPE_ELEMENT &&
event.source == (void*)output_->GetAudioElement() &&
event.cmd == AEL_MSG_CMD_REPORT_STATUS) {
audio_element_status_t status = toStatus(event.data);
if (status == AEL_STATUS_STATE_FINISHED) {
if (next_filename_ != "") {
ESP_LOGI(kTag, "finished writing output. enqueing next.");
Decoder decoder = GetDecoderForFilename(next_filename_);
if (decoder == decoder_type_) {
output_->SetSoftMute(true);
audio_element_set_uri(source_element_, next_filename_.c_str());
audio_pipeline_reset_ringbuffer(pipeline_);
audio_pipeline_reset_elements(pipeline_);
audio_pipeline_change_state(pipeline_, AEL_STATE_INIT);
audio_pipeline_run(pipeline_);
output_->SetSoftMute(true);
} else {
Play(next_filename_);
}
next_filename_ = "";
} else {
ESP_LOGI(kTag, "finished writing output. stopping.");
audio_pipeline_wait_for_stop(pipeline_);
audio_pipeline_terminate(pipeline_);
playback_state_ = STOPPED;
}
return;
}
}
if (event.need_free_data) {
// AFAICT this never happens in practice, but it doesn't hurt to follow
// the api here anyway.
free(event.data);
}
}
}
void AudioPlayback::SetNextFile(const std::string& filename) {
next_filename_ = filename;
}
void AudioPlayback::SetVolume(uint8_t volume) {
output_->SetVolume(volume);
// TODO: short delay, return error on fail
xMessageBufferSend(*stream_start_.Handle(), dest.data(), len.value(),
portMAX_DELAY);
}
auto AudioPlayback::GetVolume() const -> uint8_t {
return output_->GetVolume();
}
auto AudioPlayback::ConnectElements(IAudioElement* src, IAudioElement* sink)
-> void {
std::size_t chunk_size =
std::max(src->InputMinChunkSize(), sink->InputMinChunkSize());
std::size_t buffer_size = std::max(kMinElementBufferSize, chunk_size * 2);
auto AudioPlayback::GetDecoderForFilename(std::string filename) const
-> Decoder {
toLower(filename);
if (endsWith(filename, "mp3")) {
return MP3;
}
if (endsWith(filename, "amr") || endsWith(filename, "wamr")) {
return AMR;
}
if (endsWith(filename, "opus")) {
return OPUS;
}
if (endsWith(filename, "ogg")) {
return OGG;
}
if (endsWith(filename, "flac")) {
return FLAC;
}
if (endsWith(filename, "wav")) {
return WAV;
}
if (endsWith(filename, "aac") || endsWith(filename, "m4a") ||
endsWith(filename, "ts") || endsWith(filename, "mp4")) {
return AAC;
}
return NONE;
}
auto AudioPlayback::CreateDecoder(Decoder decoder) const
-> audio_element_handle_t {
if (decoder == MP3) {
mp3_decoder_cfg_t config = DEFAULT_MP3_DECODER_CONFIG();
return mp3_decoder_init(&config);
}
if (decoder == AMR) {
amr_decoder_cfg_t config = DEFAULT_AMR_DECODER_CONFIG();
return amr_decoder_init(&config);
}
if (decoder == OPUS) {
opus_decoder_cfg_t config = DEFAULT_OPUS_DECODER_CONFIG();
return decoder_opus_init(&config);
}
if (decoder == OGG) {
ogg_decoder_cfg_t config = DEFAULT_OGG_DECODER_CONFIG();
return ogg_decoder_init(&config);
}
if (decoder == FLAC) {
flac_decoder_cfg_t config = DEFAULT_FLAC_DECODER_CONFIG();
return flac_decoder_init(&config);
}
if (decoder == WAV) {
wav_decoder_cfg_t config = DEFAULT_WAV_DECODER_CONFIG();
return wav_decoder_init(&config);
}
if (decoder == AAC) {
aac_decoder_cfg_t config = DEFAULT_AAC_DECODER_CONFIG();
return aac_decoder_init(&config);
}
return nullptr;
}
void AudioPlayback::ReconfigurePipeline(Decoder decoder) {
if (decoder_type_ == decoder) {
return;
}
if (decoder_type_ != NONE) {
audio_pipeline_unlink(pipeline_);
audio_element_msg_remove_listener(decoder_, event_interface_);
audio_pipeline_unregister(pipeline_, decoder_);
audio_element_deinit(decoder_);
}
if (decoder != NONE) {
decoder_ = CreateDecoder(decoder);
decoder_type_ = decoder;
audio_pipeline_register(pipeline_, decoder_, kDecoder);
audio_element_msg_set_listener(decoder_, event_interface_);
static const char* link_tag[3] = {kSource, kDecoder, kSink};
audio_pipeline_link(pipeline_, &link_tag[0], 3);
}
auto buffer = std::make_unique<StreamBuffer>(chunk_size, buffer_size);
src->OutputBuffer(buffer.get());
sink->OutputBuffer(buffer.get());
element_buffers_.push_back(std::move(buffer));
}
} // namespace drivers
} // namespace audio

@ -19,12 +19,8 @@
namespace audio {
static const TickType_t kCommandWaitTicks = 1;
static const TickType_t kIdleTaskDelay = 1;
static const size_t kChunkBufferSize = kMaxChunkSize * 1.5;
auto StartAudioTask(const std::string& name,
std::shared_ptr<IAudioElement>& element) -> void {
std::shared_ptr<IAudioElement> element) -> void {
AudioTaskArgs* args = new AudioTaskArgs{.element = element};
xTaskCreate(&AudioTaskMain, name.c_str(), element->StackSizeBytes(), args,
kTaskPriorityAudio, NULL);
@ -45,24 +41,22 @@ void AudioTaskMain(void* args) {
// processing any chunks from it. Try doing this first, then fall back to
// the other cases.
bool has_received_message = false;
if (element->InputBuffer() != nullptr) {
ChunkReadResult chunk_res = chunk_reader.ReadChunkFromStream(
[&](cpp::span<std::byte> data) -> std::optional<size_t> {
process_res = element->ProcessChunk(data);
if (process_res.has_value()) {
return process_res.value();
} else {
return {};
}
},
element->IdleTimeout());
if (chunk_res == CHUNK_PROCESSING_ERROR ||
chunk_res == CHUNK_DECODING_ERROR) {
break; // TODO.
} else if (chunk_res == CHUNK_STREAM_ENDED) {
has_received_message = true;
}
ChunkReadResult chunk_res = chunk_reader.ReadChunkFromStream(
[&](cpp::span<std::byte> data) -> std::optional<size_t> {
process_res = element->ProcessChunk(data);
if (process_res.has_value()) {
return process_res.value();
} else {
return {};
}
},
element->IdleTimeout());
if (chunk_res == CHUNK_PROCESSING_ERROR ||
chunk_res == CHUNK_DECODING_ERROR) {
break; // TODO.
} else if (chunk_res == CHUNK_STREAM_ENDED) {
has_received_message = true;
}
if (has_received_message) {

@ -8,31 +8,19 @@
#include "cbor.h"
#include "stream_buffer.hpp"
#include "stream_message.hpp"
namespace audio {
/*
* The maximum size of a single chunk of stream data. This should be comfortably
* larger than the largest size of a frame of audio we should expect to handle.
*
* 128 kbps MPEG-1 @ 44.1 kHz is approx. 418 bytes according to the internet.
*
* TODO(jacqueline): tune as more codecs are added.
*/
const std::size_t kMaxChunkSize = 2048;
// TODO: tune
static const std::size_t kWorkingBufferSize = kMaxChunkSize * 1.5;
auto WriteChunksToStream(MessageBufferHandle_t* stream,
cpp::span<std::byte> working_buffer,
auto WriteChunksToStream(StreamBuffer* stream,
std::function<size_t(cpp::span<std::byte>)> callback,
TickType_t max_wait) -> ChunkWriteResult {
cpp::span<std::byte> write_buffer = stream->WriteBuffer();
while (1) {
// First, write out our chunk header so we know how much space to give to
// the callback.
auto header_size = WriteTypeOnlyMessage(TYPE_CHUNK_HEADER, working_buffer);
auto header_size = WriteTypeOnlyMessage(TYPE_CHUNK_HEADER, write_buffer);
if (header_size.has_error()) {
return CHUNK_ENCODING_ERROR;
}
@ -40,8 +28,8 @@ auto WriteChunksToStream(MessageBufferHandle_t* stream,
// Now we can ask the callback to fill the remaining space.
size_t chunk_size = std::invoke(
callback,
working_buffer.subspan(header_size.value(),
working_buffer.size() - header_size.value()));
write_buffer.subspan(header_size.value(),
write_buffer.size() - header_size.value()));
if (chunk_size == 0) {
// They had nothing for us, so bail out.
@ -51,7 +39,7 @@ auto WriteChunksToStream(MessageBufferHandle_t* stream,
// Try to write to the buffer. Note the return type here will be either 0 or
// header_size + chunk_size, as MessageBuffer doesn't allow partial writes.
size_t actual_write_size =
xMessageBufferSend(*stream, working_buffer.data(),
xMessageBufferSend(stream->Handle(), write_buffer.data(),
header_size.value() + chunk_size, max_wait);
if (actual_write_size == 0) {
@ -63,15 +51,9 @@ auto WriteChunksToStream(MessageBufferHandle_t* stream,
}
}
ChunkReader::ChunkReader(MessageBufferHandle_t* stream)
: stream_(stream),
raw_working_buffer_(static_cast<std::byte*>(
heap_caps_malloc(kWorkingBufferSize, MALLOC_CAP_SPIRAM))),
working_buffer_(raw_working_buffer_, kWorkingBufferSize) {}
ChunkReader::ChunkReader(StreamBuffer* stream) : stream_(stream) {}
ChunkReader::~ChunkReader() {
free(raw_working_buffer_);
}
ChunkReader::~ChunkReader() {}
auto ChunkReader::Reset() -> void {
leftover_bytes_ = 0;
@ -79,16 +61,17 @@ auto ChunkReader::Reset() -> void {
}
auto ChunkReader::GetLastMessage() -> cpp::span<std::byte> {
return working_buffer_.subspan(leftover_bytes_, last_message_size_);
return stream_->ReadBuffer().subspan(leftover_bytes_, last_message_size_);
}
auto ChunkReader::ReadChunkFromStream(
std::function<std::optional<size_t>(cpp::span<std::byte>)> callback,
TickType_t max_wait) -> ChunkReadResult {
// First, wait for a message to arrive over the buffer.
last_message_size_ =
xMessageBufferReceive(*stream_, raw_working_buffer_ + leftover_bytes_,
working_buffer_.size() - leftover_bytes_, max_wait);
cpp::span<std::byte> new_data_dest = stream_->ReadBuffer().last(
stream_->ReadBuffer().size() - leftover_bytes_);
last_message_size_ = xMessageBufferReceive(
stream_->Handle(), new_data_dest.data(), new_data_dest.size(), max_wait);
if (last_message_size_ == 0) {
return CHUNK_READ_TIMEOUT;
@ -109,7 +92,8 @@ auto ChunkReader::ReadChunkFromStream(
// Now we need to stick the end of the last chunk (if it exists) onto the
// front of the new chunk. Do it this way around bc we assume the old chunk
// is shorter, and therefore faster to move.
cpp::span<std::byte> leftover_data = working_buffer_.first(leftover_bytes_);
cpp::span<std::byte> leftover_data =
stream_->ReadBuffer().first(leftover_bytes_);
cpp::span<std::byte> combined_data(chunk_data.data() - leftover_data.size(),
leftover_data.size() + chunk_data.size());
if (leftover_bytes_ > 0) {
@ -127,7 +111,7 @@ auto ChunkReader::ReadChunkFromStream(
leftover_bytes_ = combined_data.size() - amount_processed.value();
if (leftover_bytes_ > 0) {
std::copy(combined_data.begin() + amount_processed.value(),
combined_data.end(), working_buffer_.begin());
combined_data.end(), stream_->ReadBuffer().begin());
return CHUNK_LEFTOVER_DATA;
}

@ -19,7 +19,6 @@ static const TickType_t kServiceInterval = pdMS_TO_TICKS(50);
static const std::size_t kFileBufferSize = 1024 * 128;
static const std::size_t kMinFileReadSize = 1024 * 4;
static const std::size_t kOutputBufferSize = 1024 * 4;
FatfsAudioInput::FatfsAudioInput(std::shared_ptr<drivers::SdStorage> storage)
: IAudioElement(),
@ -29,24 +28,11 @@ FatfsAudioInput::FatfsAudioInput(std::shared_ptr<drivers::SdStorage> storage)
file_buffer_(raw_file_buffer_, kFileBufferSize),
file_buffer_read_pos_(file_buffer_.begin()),
file_buffer_write_pos_(file_buffer_.begin()),
raw_chunk_buffer_(static_cast<std::byte*>(
heap_caps_malloc(kMaxChunkSize, MALLOC_CAP_SPIRAM))),
chunk_buffer_(raw_chunk_buffer_, kMaxChunkSize),
current_file_(),
is_file_open_(false),
output_buffer_memory_(static_cast<uint8_t*>(
heap_caps_malloc(kOutputBufferSize, MALLOC_CAP_SPIRAM))) {
output_buffer_ = new MessageBufferHandle_t;
*output_buffer_ = xMessageBufferCreateStatic(
kOutputBufferSize, output_buffer_memory_, &output_buffer_metadata_);
}
is_file_open_(false) {}
FatfsAudioInput::~FatfsAudioInput() {
free(raw_file_buffer_);
free(raw_chunk_buffer_);
vMessageBufferDelete(output_buffer_);
free(output_buffer_memory_);
free(output_buffer_);
}
auto FatfsAudioInput::ProcessStreamInfo(const StreamInfo& info)
@ -70,13 +56,13 @@ auto FatfsAudioInput::ProcessStreamInfo(const StreamInfo& info)
auto write_size =
WriteMessage(TYPE_STREAM_INFO,
std::bind(&StreamInfo::Encode, info, std::placeholders::_1),
chunk_buffer_);
output_buffer_->WriteBuffer());
if (write_size.has_error()) {
return cpp::fail(IO_ERROR);
} else {
xMessageBufferSend(output_buffer_, chunk_buffer_.data(), write_size.value(),
portMAX_DELAY);
xMessageBufferSend(output_buffer_, output_buffer_->WriteBuffer().data(),
write_size.value(), portMAX_DELAY);
}
return {};
@ -142,8 +128,8 @@ auto FatfsAudioInput::ProcessIdle() -> cpp::result<void, AudioProcessingError> {
// Now stream data into the output buffer until it's full.
pending_read_pos_ = file_buffer_read_pos_;
ChunkWriteResult result = WriteChunksToStream(
output_buffer_, chunk_buffer_,
[&](cpp::span<std::byte> d) { return SendChunk(d); }, kServiceInterval);
output_buffer_, [&](cpp::span<std::byte> d) { return SendChunk(d); },
kServiceInterval);
switch (result) {
case CHUNK_WRITE_TIMEOUT:

@ -16,7 +16,7 @@ static const char* kTag = "I2SOUT";
namespace audio {
auto I2SAudioOutput::create(drivers::GpioExpander* expander)
-> cpp::result<std::unique_ptr<I2SAudioOutput>, Error> {
-> cpp::result<std::shared_ptr<I2SAudioOutput>, Error> {
// First, we need to perform initial configuration of the DAC chip.
auto dac_result = drivers::AudioDac::create(expander);
if (dac_result.has_error()) {
@ -27,9 +27,10 @@ auto I2SAudioOutput::create(drivers::GpioExpander* expander)
// Soft mute immediately, in order to minimise any clicks and pops caused by
// the initial output element and pipeline configuration.
dac->WriteVolume(255);
// dac->WriteVolume(255);
dac->WriteVolume(120); // for testing
return std::make_unique<I2SAudioOutput>(expander, std::move(dac));
return std::make_shared<I2SAudioOutput>(expander, std::move(dac));
}
I2SAudioOutput::I2SAudioOutput(drivers::GpioExpander* expander,

@ -21,11 +21,15 @@ class AudioDecoder : public IAudioElement {
AudioDecoder();
~AudioDecoder();
auto SetInputBuffer(MessageBufferHandle_t*) -> void;
auto SetOutputBuffer(MessageBufferHandle_t*) -> void;
auto StackSizeBytes() const -> std::size_t override { return 8196; };
auto InputMinChunkSize() const -> std::size_t override {
// 128 kbps MPEG-1 @ 44.1 kHz is approx. 418 bytes according to the
// internet.
// TODO(jacqueline): tune as more codecs are added.
return 1024;
}
auto ProcessStreamInfo(const StreamInfo& info)
-> cpp::result<void, AudioProcessingError> override;
auto ProcessChunk(const cpp::span<std::byte>& chunk)
@ -38,9 +42,6 @@ class AudioDecoder : public IAudioElement {
private:
std::unique_ptr<codecs::ICodec> current_codec_;
std::optional<StreamInfo> stream_info_;
std::byte* raw_chunk_buffer_;
cpp::span<std::byte> chunk_buffer_;
};
} // namespace audio

@ -2,6 +2,7 @@
#include <cstdint>
#include "chunk.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/message_buffer.h"
@ -9,6 +10,7 @@
#include "result.hpp"
#include "span.hpp"
#include "stream_buffer.hpp"
#include "stream_info.hpp"
#include "types.hpp"
@ -41,7 +43,7 @@ enum AudioProcessingError {
class IAudioElement {
public:
IAudioElement() : input_buffer_(nullptr), output_buffer_(nullptr) {}
virtual ~IAudioElement();
virtual ~IAudioElement() {}
/*
* Returns the stack size in bytes that this element requires. This should
@ -57,11 +59,17 @@ class IAudioElement {
*/
virtual auto IdleTimeout() const -> TickType_t { return portMAX_DELAY; }
virtual auto InputMinChunkSize() const -> std::size_t { return 0; }
/* Returns this element's input buffer. */
auto InputBuffer() const -> MessageBufferHandle_t* { return input_buffer_; }
auto InputBuffer() const -> StreamBuffer* { return input_buffer_; }
/* Returns this element's output buffer. */
auto OutputBuffer() const -> MessageBufferHandle_t* { return output_buffer_; }
auto OutputBuffer() const -> StreamBuffer* { return output_buffer_; }
auto InputBuffer(StreamBuffer* b) -> void { input_buffer_ = b; }
auto OutputBuffer(StreamBuffer* b) -> void { output_buffer_ = b; }
/*
* Called when a StreamInfo message is received. Used to configure this
@ -87,8 +95,8 @@ class IAudioElement {
virtual auto ProcessIdle() -> cpp::result<void, AudioProcessingError> = 0;
protected:
MessageBufferHandle_t* input_buffer_;
MessageBufferHandle_t* output_buffer_;
StreamBuffer* input_buffer_;
StreamBuffer* output_buffer_;
};
} // namespace audio

@ -3,95 +3,44 @@
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include "audio_common.h"
#include "audio_element.h"
#include "audio_event_iface.h"
#include "audio_pipeline.h"
#include "audio_element.hpp"
#include "esp_err.h"
#include "fatfs_stream.h"
#include "i2s_stream.h"
#include "mp3_decoder.h"
#include "gpio_expander.hpp"
#include "result.hpp"
#include "audio_output.hpp"
#include "dac.hpp"
#include "span.hpp"
#include "storage.hpp"
#include "stream_buffer.hpp"
namespace drivers {
namespace audio {
/*
* Sends an I2S audio stream to the DAC. Includes basic controls for pausing
* and resuming the stream, as well as support for gapless playback of the next
* queued song, but does not implement any kind of sophisticated queing or
* playback control; these should be handled at a higher level.
* TODO.
*/
class AudioPlayback {
public:
enum Error { FATFS_INIT, I2S_INIT, PIPELINE_INIT };
static auto create(std::unique_ptr<IAudioOutput> output)
enum Error { ERR_INIT_ELEMENT, ERR_MEM };
static auto create(drivers::GpioExpander* expander,
std::shared_ptr<drivers::SdStorage> storage)
-> cpp::result<std::unique_ptr<AudioPlayback>, Error>;
AudioPlayback(std::unique_ptr<IAudioOutput>& output,
audio_pipeline_handle_t pipeline,
audio_element_handle_t source_element,
audio_event_iface_handle_t event_interface);
// TODO(jacqueline): configure on the fly once we have things to configure.
AudioPlayback();
~AudioPlayback();
/*
* Replaces any currently playing file with the one given, and begins
* playback.
*
* Any value set in `set_next_file` is cleared by this method.
*/
auto Play(const std::string& filename) -> void;
/* Toogle between resumed and paused. */
auto Toggle() -> void;
auto Resume() -> void;
auto Pause() -> void;
enum PlaybackState { PLAYING, PAUSED, STOPPED };
auto GetPlaybackState() const -> PlaybackState;
/*
* Handles any pending events from the underlying audio pipeline. This must
* be called regularly in order to handle configuring the I2S stream for
* different audio types (e.g. sample rate, bit depth), and for gapless
* playback.
*/
auto ProcessEvents(uint16_t max_time_ms) -> void;
/*
* Sets the file that should be played immediately after the current file
* finishes. This is used for gapless playback
*/
auto SetNextFile(const std::string& filename) -> void;
auto SetVolume(uint8_t volume) -> void;
auto GetVolume() const -> uint8_t;
// Not copyable or movable.
AudioPlayback(const AudioPlayback&) = delete;
AudioPlayback& operator=(const AudioPlayback&) = delete;
private:
PlaybackState playback_state_;
enum Decoder { NONE, MP3, AMR, OPUS, OGG, FLAC, WAV, AAC };
auto GetDecoderForFilename(std::string filename) const -> Decoder;
auto CreateDecoder(Decoder decoder) const -> audio_element_handle_t;
auto ReconfigurePipeline(Decoder decoder) -> void;
std::unique_ptr<IAudioOutput> output_;
std::string next_filename_ = "";
audio_pipeline_handle_t pipeline_;
audio_element_handle_t source_element_;
audio_event_iface_handle_t event_interface_;
auto ConnectElements(IAudioElement* src, IAudioElement* sink) -> void;
audio_element_handle_t decoder_ = nullptr;
Decoder decoder_type_ = NONE;
StreamBuffer stream_start_;
StreamBuffer stream_end_;
std::vector<std::unique_ptr<StreamBuffer>> element_buffers_;
};
} // namespace drivers
} // namespace audio

@ -10,7 +10,8 @@ struct AudioTaskArgs {
std::shared_ptr<IAudioElement>& element;
};
auto StartAudioTask(std::shared_ptr<IAudioElement>& element) -> void;
auto StartAudioTask(const std::string& name,
std::shared_ptr<IAudioElement> element) -> void;
void AudioTaskMain(void* args);

@ -13,11 +13,10 @@
#include "freertos/queue.h"
#include "result.hpp"
#include "span.hpp"
#include "stream_buffer.hpp"
namespace audio {
extern const std::size_t kMaxChunkSize;
enum ChunkWriteResult {
// Returned when the callback does not write any data.
CHUNK_OUT_OF_DATA,
@ -37,8 +36,7 @@ enum ChunkWriteResult {
* number of bytes it wrote. Return a value of 0 to indicate that there is no
* more input to read.
*/
auto WriteChunksToStream(MessageBufferHandle_t* stream,
cpp::span<std::byte> working_buffer,
auto WriteChunksToStream(StreamBuffer* stream,
std::function<size_t(cpp::span<std::byte>)> callback,
TickType_t max_wait) -> ChunkWriteResult;
@ -59,7 +57,7 @@ enum ChunkReadResult {
class ChunkReader {
public:
ChunkReader(MessageBufferHandle_t* stream);
explicit ChunkReader(StreamBuffer* buffer);
~ChunkReader();
auto Reset() -> void;
@ -83,10 +81,7 @@ class ChunkReader {
TickType_t max_wait) -> ChunkReadResult;
private:
MessageBufferHandle_t* stream_;
std::byte* raw_working_buffer_;
cpp::span<std::byte> working_buffer_;
StreamBuffer* stream_;
std::size_t leftover_bytes_ = 0;
std::size_t last_message_size_ = 0;
};

@ -4,6 +4,7 @@
#include <memory>
#include <string>
#include "chunk.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/message_buffer.h"
@ -12,6 +13,7 @@
#include "audio_element.hpp"
#include "storage.hpp"
#include "stream_buffer.hpp"
namespace audio {
@ -28,6 +30,9 @@ class FatfsAudioInput : public IAudioElement {
auto SendChunk(cpp::span<std::byte> dest) -> size_t;
FatfsAudioInput(const FatfsAudioInput&) = delete;
FatfsAudioInput& operator=(const FatfsAudioInput&) = delete;
private:
auto GetRingBufferDistance() const -> size_t;
@ -39,14 +44,8 @@ class FatfsAudioInput : public IAudioElement {
cpp::span<std::byte>::iterator pending_read_pos_;
cpp::span<std::byte>::iterator file_buffer_write_pos_;
std::byte* raw_chunk_buffer_;
cpp::span<std::byte> chunk_buffer_;
FIL current_file_;
bool is_file_open_;
uint8_t* output_buffer_memory_;
StaticMessageBuffer_t output_buffer_metadata_;
};
} // namespace audio

@ -16,13 +16,17 @@ class I2SAudioOutput : public IAudioElement {
public:
enum Error { DAC_CONFIG, I2S_CONFIG, STREAM_INIT };
static auto create(drivers::GpioExpander* expander)
-> cpp::result<std::unique_ptr<I2SAudioOutput>, Error>;
-> cpp::result<std::shared_ptr<I2SAudioOutput>, Error>;
I2SAudioOutput(drivers::GpioExpander* expander,
std::unique_ptr<drivers::AudioDac> dac);
~I2SAudioOutput();
auto SetInputBuffer(MessageBufferHandle_t* in) -> void { input_buffer_ = in; }
auto InputMinChunkSize() const -> std::size_t override {
// TODO(jacqueline): work out a good value here. Maybe similar to the total
// DMA buffer size?
return 128;
}
auto IdleTimeout() const -> TickType_t override;
auto ProcessStreamInfo(const StreamInfo& info)

@ -0,0 +1,37 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "freertos/FreeRTOS.h"
#include "freertos/message_buffer.h"
#include "span.hpp"
namespace audio {
class StreamBuffer {
public:
explicit StreamBuffer(std::size_t chunk_size, std::size_t buffer_size);
~StreamBuffer();
auto Handle() -> MessageBufferHandle_t* { return &handle_; }
auto ReadBuffer() -> cpp::span<std::byte> { return input_chunk_; }
auto WriteBuffer() -> cpp::span<std::byte> { return output_chunk_; }
StreamBuffer(const StreamBuffer&) = delete;
StreamBuffer& operator=(const StreamBuffer&) = delete;
private:
std::byte* raw_memory_;
StaticMessageBuffer_t metadata_;
MessageBufferHandle_t handle_;
std::byte* raw_input_chunk_;
cpp::span<std::byte> input_chunk_;
std::byte* raw_output_chunk_;
cpp::span<std::byte> output_chunk_;
};
} // namespace audio

@ -3,6 +3,7 @@
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include "cbor.h"
#include "result.hpp"
@ -19,10 +20,14 @@ class StreamInfo {
~StreamInfo() = default;
auto Path() const -> const std::optional<std::string>& { return path_; }
auto Path(const std::string_view& d) -> void { path_ = d; }
auto Channels() const -> const std::optional<uint8_t>& { return channels_; }
auto BitsPerSample() const -> const std::optional<uint8_t>& {
return bits_per_sample_;
}
auto SampleRate() const -> const std::optional<uint16_t>& {
return sample_rate_;
}

@ -0,0 +1,26 @@
#include "stream_buffer.hpp"
namespace audio {
StreamBuffer::StreamBuffer(std::size_t chunk_size, std::size_t buffer_size)
: raw_memory_(static_cast<std::byte*>(
heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM))),
handle_(
xMessageBufferCreateStatic(buffer_size,
reinterpret_cast<uint8_t*>(raw_memory_),
&metadata_)),
raw_input_chunk_(static_cast<std::byte*>(
heap_caps_malloc(chunk_size * 1.5, MALLOC_CAP_SPIRAM))),
input_chunk_(raw_input_chunk_, chunk_size * 1.5),
raw_output_chunk_(static_cast<std::byte*>(
heap_caps_malloc(chunk_size, MALLOC_CAP_SPIRAM))),
output_chunk_(raw_output_chunk_, chunk_size) {}
StreamBuffer::~StreamBuffer() {
vMessageBufferDelete(handle_);
free(raw_memory_);
free(raw_input_chunk_);
free(raw_output_chunk_);
}
} // namespace audio

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

@ -63,7 +63,7 @@ class ICodec {
enum CreateCodecError { UNKNOWN_EXTENSION };
auto CreateCodecForFile(const std::string& extension)
auto CreateCodecForFile(const std::string& file)
-> cpp::result<std::unique_ptr<ICodec>, CreateCodecError>;
} // namespace codecs

@ -26,7 +26,7 @@ class SdStorage {
};
static auto create(GpioExpander* gpio)
-> cpp::result<std::unique_ptr<SdStorage>, Error>;
-> cpp::result<std::shared_ptr<SdStorage>, Error>;
SdStorage(GpioExpander* gpio,
esp_err_t (*do_transaction)(sdspi_dev_handle_t, sdmmc_command_t*),

@ -50,7 +50,7 @@ static esp_err_t do_transaction(sdspi_dev_handle_t handle,
} // namespace callback
auto SdStorage::create(GpioExpander* gpio)
-> cpp::result<std::unique_ptr<SdStorage>, Error> {
-> cpp::result<std::shared_ptr<SdStorage>, Error> {
// Acquiring the bus will also flush the mux switch change.
gpio->set_pin(GpioExpander::SD_MUX_SWITCH, GpioExpander::SD_MUX_ESP);

@ -8,11 +8,14 @@
#include <iostream>
#include <string>
#include "audio_playback.hpp"
#include "esp_console.h"
namespace console {
std::string toSdPath(std::string filepath) {
static AppConsole* sInstance = nullptr;
std::string toSdPath(const std::string& filepath) {
return std::string(drivers::kStoragePath) + "/" + filepath;
}
@ -57,12 +60,7 @@ int CmdPlayFile(int argc, char** argv) {
return 1;
}
/*
sInstance->playback_->Play(toSdPath(argv[1]));
if (argc == 3) {
sInstance->playback_->SetNextFile(toSdPath(argv[2]));
}
*/
return 0;
}
@ -125,14 +123,12 @@ void RegisterVolume() {
esp_console_cmd_register(&cmd);
}
/*
AppConsole::AppConsole() {
AppConsole::AppConsole(audio::AudioPlayback* playback) : playback_(playback) {
sInstance = this;
}
AppConsole::~AppConsole() {
sInstance = nullptr;
}
*/
auto AppConsole::RegisterExtraComponents() -> void {
RegisterListDir();

@ -2,6 +2,7 @@
#include <memory>
#include "audio_playback.hpp"
#include "console.hpp"
#include "storage.hpp"
@ -9,8 +10,10 @@ namespace console {
class AppConsole : public Console {
public:
AppConsole() {}
virtual ~AppConsole() {}
explicit AppConsole(audio::AudioPlayback* playback);
virtual ~AppConsole();
audio::AudioPlayback* playback_;
protected:
virtual auto RegisterExtraComponents() -> void;

@ -24,6 +24,7 @@
#include "widgets/lv_label.h"
#include "app_console.hpp"
#include "audio_playback.hpp"
#include "battery.hpp"
#include "dac.hpp"
#include "display.hpp"
@ -102,7 +103,7 @@ extern "C" void app_main(void) {
ESP_LOGE(TAG, "Failed: %d", storage_res.error());
return;
}
std::unique_ptr<drivers::SdStorage> storage = std::move(storage_res.value());
std::shared_ptr<drivers::SdStorage> storage = std::move(storage_res.value());
LvglArgs* lvglArgs = (LvglArgs*)calloc(1, sizeof(LvglArgs));
lvglArgs->gpio_expander = expander;
@ -110,32 +111,20 @@ extern "C" void app_main(void) {
(void*)lvglArgs, 1, sLvglStack,
&sLvglTaskBuffer, 1);
/*
ESP_LOGI(TAG, "Init audio output (I2S)");
auto sink_res = drivers::I2SAudioOutput::create(expander);
if (sink_res.has_error()) {
ESP_LOGE(TAG, "Failed: %d", sink_res.error());
return;
}
std::unique_ptr<drivers::IAudioOutput> sink = std::move(sink_res.value());
ESP_LOGI(TAG, "Init audio pipeline");
auto playback_res = drivers::AudioPlayback::create(std::move(sink));
auto playback_res = audio::AudioPlayback::create(expander, storage);
if (playback_res.has_error()) {
ESP_LOGE(TAG, "Failed: %d", playback_res.error());
return;
}
std::unique_ptr<drivers::AudioPlayback> playback =
std::shared_ptr<audio::AudioPlayback> playback =
std::move(playback_res.value());
playback->SetVolume(130);
*/
ESP_LOGI(TAG, "Launch console");
console::AppConsole console;
console::AppConsole console(playback.get());
console.Launch();
while (1) {
// playback->ProcessEvents(5);
vTaskDelay(pdMS_TO_TICKS(100));
}
}

Loading…
Cancel
Save