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( - 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 { @@ -75,7 +58,7 @@ auto AudioDecoder::ProcessChunk(const cpp::span& chunk) bool needs_more_input = false; std::optional error = std::nullopt; WriteChunksToStream( - output_buffer_, chunk_buffer_, + output_buffer_, [&](cpp::span 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 #include #include - -#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(status); - return static_cast(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 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 storage) -> cpp::result, 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(storage); + auto codec = std::make_shared(); - 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(); - 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(output, pipeline, fatfs_stream_reader, - event_interface); + return playback; } -AudioPlayback::AudioPlayback(std::unique_ptr& 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 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(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& element) -> void { + std::shared_ptr 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 data) -> std::optional { - 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 data) -> std::optional { + 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 working_buffer, +auto WriteChunksToStream(StreamBuffer* stream, std::function)> callback, TickType_t max_wait) -> ChunkWriteResult { + cpp::span 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( - 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 { - return working_buffer_.subspan(leftover_bytes_, last_message_size_); + return stream_->ReadBuffer().subspan(leftover_bytes_, last_message_size_); } auto ChunkReader::ReadChunkFromStream( std::function(cpp::span)> 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 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 leftover_data = working_buffer_.first(leftover_bytes_); + cpp::span leftover_data = + stream_->ReadBuffer().first(leftover_bytes_); cpp::span 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 storage) : IAudioElement(), @@ -29,24 +28,11 @@ FatfsAudioInput::FatfsAudioInput(std::shared_ptr 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( - heap_caps_malloc(kMaxChunkSize, MALLOC_CAP_SPIRAM))), - chunk_buffer_(raw_chunk_buffer_, kMaxChunkSize), current_file_(), - is_file_open_(false), - output_buffer_memory_(static_cast( - 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 { // 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 d) { return SendChunk(d); }, kServiceInterval); + output_buffer_, [&](cpp::span 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, Error> { + -> cpp::result, 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(expander, std::move(dac)); + return std::make_shared(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 override; auto ProcessChunk(const cpp::span& chunk) @@ -38,9 +42,6 @@ class AudioDecoder : public IAudioElement { private: std::unique_ptr current_codec_; std::optional stream_info_; - - std::byte* raw_chunk_buffer_; - cpp::span 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 +#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 = 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 #include #include +#include -#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 output) + enum Error { ERR_INIT_ELEMENT, ERR_MEM }; + static auto create(drivers::GpioExpander* expander, + std::shared_ptr storage) -> cpp::result, Error>; - AudioPlayback(std::unique_ptr& 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 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> 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& element; }; -auto StartAudioTask(std::shared_ptr& element) -> void; +auto StartAudioTask(const std::string& name, + std::shared_ptr 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 working_buffer, +auto WriteChunksToStream(StreamBuffer* stream, std::function)> 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 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 #include +#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 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::iterator pending_read_pos_; cpp::span::iterator file_buffer_write_pos_; - std::byte* raw_chunk_buffer_; - cpp::span 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, Error>; + -> cpp::result, Error>; I2SAudioOutput(drivers::GpioExpander* expander, std::unique_ptr 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 +#include + +#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 { return input_chunk_; } + auto WriteBuffer() -> cpp::span { 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 input_chunk_; + + std::byte* raw_output_chunk_; + cpp::span 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 #include #include +#include #include "cbor.h" #include "result.hpp" @@ -19,10 +20,14 @@ class StreamInfo { ~StreamInfo() = default; auto Path() const -> const std::optional& { return path_; } + auto Path(const std::string_view& d) -> void { path_ = d; } + auto Channels() const -> const std::optional& { return channels_; } + auto BitsPerSample() const -> const std::optional& { return bits_per_sample_; } + auto SampleRate() const -> const std::optional& { 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( + heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM))), + handle_( + xMessageBufferCreateStatic(buffer_size, + reinterpret_cast(raw_memory_), + &metadata_)), + raw_input_chunk_(static_cast( + heap_caps_malloc(chunk_size * 1.5, MALLOC_CAP_SPIRAM))), + input_chunk_(raw_input_chunk_, chunk_size * 1.5), + raw_output_chunk_(static_cast( + 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 diff --git a/src/codecs/codec.cpp b/src/codecs/codec.cpp index 2a66b5f1..4e9a6a47 100644 --- a/src/codecs/codec.cpp +++ b/src/codecs/codec.cpp @@ -5,7 +5,7 @@ namespace codecs { -auto CreateCodecForExtension(std::string extension) +auto CreateCodecForFile(const std::string& file) -> cpp::result, CreateCodecError> { return std::make_unique(); // TODO. } diff --git a/src/codecs/include/codec.hpp b/src/codecs/include/codec.hpp index bbb621f5..6897acf2 100644 --- a/src/codecs/include/codec.hpp +++ b/src/codecs/include/codec.hpp @@ -63,7 +63,7 @@ class ICodec { enum CreateCodecError { UNKNOWN_EXTENSION }; -auto CreateCodecForFile(const std::string& extension) +auto CreateCodecForFile(const std::string& file) -> cpp::result, CreateCodecError>; } // namespace codecs diff --git a/src/drivers/include/storage.hpp b/src/drivers/include/storage.hpp index aa736793..64ce4782 100644 --- a/src/drivers/include/storage.hpp +++ b/src/drivers/include/storage.hpp @@ -26,7 +26,7 @@ class SdStorage { }; static auto create(GpioExpander* gpio) - -> cpp::result, Error>; + -> cpp::result, Error>; SdStorage(GpioExpander* gpio, esp_err_t (*do_transaction)(sdspi_dev_handle_t, sdmmc_command_t*), diff --git a/src/drivers/storage.cpp b/src/drivers/storage.cpp index 2c2d7a5f..414bfd21 100644 --- a/src/drivers/storage.cpp +++ b/src/drivers/storage.cpp @@ -50,7 +50,7 @@ static esp_err_t do_transaction(sdspi_dev_handle_t handle, } // namespace callback auto SdStorage::create(GpioExpander* gpio) - -> cpp::result, Error> { + -> cpp::result, Error> { // Acquiring the bus will also flush the mux switch change. gpio->set_pin(GpioExpander::SD_MUX_SWITCH, GpioExpander::SD_MUX_ESP); diff --git a/src/main/app_console.cpp b/src/main/app_console.cpp index fbb8df87..281454dc 100644 --- a/src/main/app_console.cpp +++ b/src/main/app_console.cpp @@ -8,11 +8,14 @@ #include #include +#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(); diff --git a/src/main/app_console.hpp b/src/main/app_console.hpp index 155d8127..f94bcb51 100644 --- a/src/main/app_console.hpp +++ b/src/main/app_console.hpp @@ -2,6 +2,7 @@ #include +#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; diff --git a/src/main/main.cpp b/src/main/main.cpp index 3e073401..a923b683 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -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 storage = std::move(storage_res.value()); + std::shared_ptr 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 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 playback = + std::shared_ptr 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)); } }