better handling of chunk buffer
This commit is contained in:
@@ -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
|
||||
|
||||
+60
-294
@@ -4,322 +4,88 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#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"
|
||||
|
||||
#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"
|
||||
namespace audio {
|
||||
|
||||
#include "audio_output.hpp"
|
||||
// TODO: idk
|
||||
static const std::size_t kMinElementBufferSize = 1024;
|
||||
|
||||
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)
|
||||
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;
|
||||
// Create everything
|
||||
auto source = std::make_shared<FatfsAudioInput>(storage);
|
||||
auto codec = std::make_shared<AudioDecoder>();
|
||||
|
||||
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);
|
||||
auto sink_res = I2SAudioOutput::create(expander);
|
||||
if (sink_res.has_error()) {
|
||||
return cpp::fail(ERR_INIT_ELEMENT);
|
||||
}
|
||||
auto sink = sink_res.value();
|
||||
|
||||
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 playback = std::make_unique<AudioPlayback>();
|
||||
|
||||
audio_event_iface_cfg_t event_config = AUDIO_EVENT_IFACE_DEFAULT_CFG();
|
||||
event_interface = audio_event_iface_init(&event_config);
|
||||
// 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_set_listener(pipeline, event_interface);
|
||||
audio_element_msg_set_listener(fatfs_stream_reader, event_interface);
|
||||
audio_element_msg_set_listener(output->GetAudioElement(), event_interface);
|
||||
// Launch!
|
||||
StartAudioTask("src", source);
|
||||
StartAudioTask("dec", codec);
|
||||
StartAudioTask("sink", sink);
|
||||
|
||||
audio_pipeline_register(pipeline, fatfs_stream_reader, kSource);
|
||||
audio_pipeline_register(pipeline, output->GetAudioElement(), kSink);
|
||||
|
||||
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_);
|
||||
// TODO(jacqueline): signal the end of all things, and maybe wait for it?
|
||||
}
|
||||
|
||||
void AudioPlayback::Play(const std::string& filename) {
|
||||
output_->SetSoftMute(true);
|
||||
auto AudioPlayback::Play(const std::string& filename) -> void {
|
||||
StreamInfo info;
|
||||
info.Path(filename);
|
||||
|
||||
if (playback_state_ != STOPPED) {
|
||||
audio_pipeline_stop(pipeline_);
|
||||
audio_pipeline_wait_for_stop(pipeline_);
|
||||
audio_pipeline_terminate(pipeline_);
|
||||
}
|
||||
std::array<std::byte, 128> dest;
|
||||
auto len = WriteMessage(
|
||||
TYPE_STREAM_INFO, [&](auto enc) { return info.Encode(enc); }, dest);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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::GetPlaybackState() const -> PlaybackState {
|
||||
return playback_state_;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
// TODO: short delay, return error on fail
|
||||
xMessageBufferSend(*stream_start_.Handle(), dest.data(), len.value(),
|
||||
portMAX_DELAY);
|
||||
}
|
||||
|
||||
void AudioPlayback::SetNextFile(const std::string& filename) {
|
||||
next_filename_ = filename;
|
||||
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 buffer = std::make_unique<StreamBuffer>(chunk_size, buffer_size);
|
||||
src->OutputBuffer(buffer.get());
|
||||
sink->OutputBuffer(buffer.get());
|
||||
element_buffers_.push_back(std::move(buffer));
|
||||
}
|
||||
|
||||
void AudioPlayback::SetVolume(uint8_t volume) {
|
||||
output_->SetVolume(volume);
|
||||
}
|
||||
|
||||
auto AudioPlayback::GetVolume() const -> uint8_t {
|
||||
return output_->GetVolume();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace drivers
|
||||
} // namespace audio
|
||||
|
||||
+16
-22
@@ -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());
|
||||
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 (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) {
|
||||
|
||||
+17
-33
@@ -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_;
|
||||
auto ConnectElements(IAudioElement* src, IAudioElement* sink) -> void;
|
||||
|
||||
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_;
|
||||
|
||||
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;
|
||||
|
||||
+5
-16
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user