summaryrefslogtreecommitdiff
path: root/src/audio
diff options
context:
space:
mode:
Diffstat (limited to 'src/audio')
-rw-r--r--src/audio/CMakeLists.txt1
-rw-r--r--src/audio/audio_decoder.cpp23
-rw-r--r--src/audio/audio_playback.cpp358
-rw-r--r--src/audio/audio_task.cpp40
-rw-r--r--src/audio/chunk.cpp50
-rw-r--r--src/audio/fatfs_audio_input.cpp26
-rw-r--r--src/audio/i2s_audio_output.cpp7
-rw-r--r--src/audio/include/audio_decoder.hpp13
-rw-r--r--src/audio/include/audio_element.hpp18
-rw-r--r--src/audio/include/audio_playback.hpp85
-rw-r--r--src/audio/include/audio_task.hpp3
-rw-r--r--src/audio/include/chunk.hpp13
-rw-r--r--src/audio/include/fatfs_audio_input.hpp11
-rw-r--r--src/audio/include/i2s_audio_output.hpp8
-rw-r--r--src/audio/include/stream_buffer.hpp37
-rw-r--r--src/audio/include/stream_info.hpp5
-rw-r--r--src/audio/stream_buffer.cpp26
17 files changed, 232 insertions, 492 deletions
diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt
index 306def86..cf58f00c 100644
--- a/src/audio/CMakeLists.txt
+++ b/src/audio/CMakeLists.txt
@@ -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")
diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp
index c48756ac..872b7ead 100644
--- a/src/audio/audio_decoder.cpp
+++ b/src/audio/audio_decoder.cpp
@@ -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
diff --git a/src/audio/audio_playback.cpp b/src/audio/audio_playback.cpp
index 300bf176..7b8418d7 100644
--- a/src/audio/audio_playback.cpp
+++ b/src/audio/audio_playback.cpp
@@ -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
diff --git a/src/audio/audio_task.cpp b/src/audio/audio_task.cpp
index 112f8f34..3512c96f 100644
--- a/src/audio/audio_task.cpp
+++ b/src/audio/audio_task.cpp
@@ -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) {
diff --git a/src/audio/chunk.cpp b/src/audio/chunk.cpp
index eb28d5a9..fbd795d9 100644
--- a/src/audio/chunk.cpp
+++ b/src/audio/chunk.cpp
@@ -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;
}
diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp
index 0f2fbe6d..9e8c5243 100644
--- a/src/audio/fatfs_audio_input.cpp
+++ b/src/audio/fatfs_audio_input.cpp
@@ -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:
diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp
index b6cf27f2..a51d6aa5 100644
--- a/src/audio/i2s_audio_output.cpp
+++ b/src/audio/i2s_audio_output.cpp
@@ -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,
diff --git a/src/audio/include/audio_decoder.hpp b/src/audio/include/audio_decoder.hpp
index a6b15d9e..eaef2f8c 100644
--- a/src/audio/include/audio_decoder.hpp
+++ b/src/audio/include/audio_decoder.hpp
@@ -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
diff --git a/src/audio/include/audio_element.hpp b/src/audio/include/audio_element.hpp
index 1973fccf..590889bd 100644
--- a/src/audio/include/audio_element.hpp
+++ b/src/audio/include/audio_element.hpp
@@ -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
diff --git a/src/audio/include/audio_playback.hpp b/src/audio/include/audio_playback.hpp
index 41ab46d2..9a7c5fc0 100644
--- a/src/audio/include/audio_playback.hpp
+++ b/src/audio/include/audio_playback.hpp
@@ -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
diff --git a/src/audio/include/audio_task.hpp b/src/audio/include/audio_task.hpp
index 05888170..ca75fbd2 100644
--- a/src/audio/include/audio_task.hpp
+++ b/src/audio/include/audio_task.hpp
@@ -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);
diff --git a/src/audio/include/chunk.hpp b/src/audio/include/chunk.hpp
index aadcbbdb..d55e5d9d 100644
--- a/src/audio/include/chunk.hpp
+++ b/src/audio/include/chunk.hpp
@@ -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;
};
diff --git a/src/audio/include/fatfs_audio_input.hpp b/src/audio/include/fatfs_audio_input.hpp
index 63555ddc..21c729be 100644
--- a/src/audio/include/fatfs_audio_input.hpp
+++ b/src/audio/include/fatfs_audio_input.hpp
@@ -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
diff --git a/src/audio/include/i2s_audio_output.hpp b/src/audio/include/i2s_audio_output.hpp
index 4b4a458d..9e59f8fd 100644
--- a/src/audio/include/i2s_audio_output.hpp
+++ b/src/audio/include/i2s_audio_output.hpp
@@ -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)
diff --git a/src/audio/include/stream_buffer.hpp b/src/audio/include/stream_buffer.hpp
new file mode 100644
index 00000000..cfb4bf9d
--- /dev/null
+++ b/src/audio/include/stream_buffer.hpp
@@ -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
diff --git a/src/audio/include/stream_info.hpp b/src/audio/include/stream_info.hpp
index 47bcaa45..45f10fc6 100644
--- a/src/audio/include/stream_info.hpp
+++ b/src/audio/include/stream_info.hpp
@@ -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_;
}
diff --git a/src/audio/stream_buffer.cpp b/src/audio/stream_buffer.cpp
new file mode 100644
index 00000000..740bea7f
--- /dev/null
+++ b/src/audio/stream_buffer.cpp
@@ -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