Use the sync apis for I2S output

custom
jacqueline 2 years ago
parent fa1f1cd9ab
commit a65d996583
  1. 7
      src/audio/audio_decoder.cpp
  2. 5
      src/audio/audio_task.cpp
  3. 6
      src/audio/fatfs_audio_input.cpp
  4. 94
      src/audio/i2s_audio_output.cpp
  5. 2
      src/audio/include/audio_task.hpp
  6. 4
      src/audio/include/i2s_audio_output.hpp
  7. 61
      src/drivers/dac.cpp
  8. 9
      src/drivers/include/dac.hpp

@ -21,7 +21,7 @@ namespace audio {
static const char* kTag = "DEC"; static const char* kTag = "DEC";
static const std::size_t kSamplesPerChunk = 256; static const std::size_t kSamplesPerChunk = 1024;
AudioDecoder::AudioDecoder() AudioDecoder::AudioDecoder()
: IAudioElement(), : IAudioElement(),
@ -89,14 +89,12 @@ auto AudioDecoder::ProcessEndOfStream() -> void {
needs_more_input_ = true; needs_more_input_ = true;
current_codec_.reset(); current_codec_.reset();
SendOrBufferEvent( SendOrBufferEvent(std::unique_ptr<StreamEvent>(
std::unique_ptr<StreamEvent>(
StreamEvent::CreateEndOfStream(input_events_))); StreamEvent::CreateEndOfStream(input_events_)));
} }
auto AudioDecoder::Process() -> cpp::result<void, AudioProcessingError> { auto AudioDecoder::Process() -> cpp::result<void, AudioProcessingError> {
if (has_samples_to_send_) { if (has_samples_to_send_) {
ESP_LOGI(kTag, "sending samples");
// Writing samples is relatively quick (it's just a bunch of memcopy's), so // Writing samples is relatively quick (it's just a bunch of memcopy's), so
// do them all at once. // do them all at once.
while (has_samples_to_send_ && !IsOverBuffered()) { while (has_samples_to_send_ && !IsOverBuffered()) {
@ -132,7 +130,6 @@ auto AudioDecoder::Process() -> cpp::result<void, AudioProcessingError> {
} }
if (!needs_more_input_) { if (!needs_more_input_) {
ESP_LOGI(kTag, "decoding frame");
auto res = current_codec_->ProcessNextFrame(); auto res = current_codec_->ProcessNextFrame();
if (res.has_error()) { if (res.has_error()) {
// todo // todo

@ -38,8 +38,9 @@ auto StartAudioTask(const std::string& name,
ESP_LOGI(kTag, "starting audio task %s", name.c_str()); ESP_LOGI(kTag, "starting audio task %s", name.c_str());
if (core_id) { if (core_id) {
xTaskCreatePinnedToCore(&AudioTaskMain, name.c_str(), element->StackSizeBytes(), args, xTaskCreatePinnedToCore(&AudioTaskMain, name.c_str(),
kTaskPriorityAudio, task_handle.get(), *core_id); element->StackSizeBytes(), args, kTaskPriorityAudio,
task_handle.get(), *core_id);
} else { } else {
xTaskCreate(&AudioTaskMain, name.c_str(), element->StackSizeBytes(), args, xTaskCreate(&AudioTaskMain, name.c_str(), element->StackSizeBytes(), args,
kTaskPriorityAudio, task_handle.get()); kTaskPriorityAudio, task_handle.get());

@ -73,8 +73,7 @@ auto FatfsAudioInput::ProcessEndOfStream() -> void {
if (is_file_open_) { if (is_file_open_) {
f_close(&current_file_); f_close(&current_file_);
is_file_open_ = false; is_file_open_ = false;
SendOrBufferEvent( SendOrBufferEvent(std::unique_ptr<StreamEvent>(
std::unique_ptr<StreamEvent>(
StreamEvent::CreateEndOfStream(input_events_))); StreamEvent::CreateEndOfStream(input_events_)));
} }
} }
@ -85,7 +84,6 @@ auto FatfsAudioInput::Process() -> cpp::result<void, AudioProcessingError> {
StreamEvent::CreateChunkData(input_events_, kChunkSize)); StreamEvent::CreateChunkData(input_events_, kChunkSize));
UINT bytes_read = 0; UINT bytes_read = 0;
ESP_LOGI(kTag, "reading from file");
FRESULT result = f_read(&current_file_, dest_event->chunk_data.raw_bytes, FRESULT result = f_read(&current_file_, dest_event->chunk_data.raw_bytes,
kChunkSize, &bytes_read); kChunkSize, &bytes_read);
if (result != FR_OK) { if (result != FR_OK) {
@ -93,13 +91,11 @@ auto FatfsAudioInput::Process() -> cpp::result<void, AudioProcessingError> {
return cpp::fail(IO_ERROR); return cpp::fail(IO_ERROR);
} }
ESP_LOGI(kTag, "sending file data (%u bytes)", bytes_read);
dest_event->chunk_data.bytes = dest_event->chunk_data.bytes =
dest_event->chunk_data.bytes.first(bytes_read); dest_event->chunk_data.bytes.first(bytes_read);
SendOrBufferEvent(std::move(dest_event)); SendOrBufferEvent(std::move(dest_event));
if (bytes_read < kChunkSize || f_eof(&current_file_)) { if (bytes_read < kChunkSize || f_eof(&current_file_)) {
ESP_LOGI(kTag, "closing file");
f_close(&current_file_); f_close(&current_file_);
is_file_open_ = false; is_file_open_ = false;
} }

@ -43,22 +43,12 @@ I2SAudioOutput::I2SAudioOutput(drivers::GpioExpander* expander,
volume_(255), volume_(255),
is_soft_muted_(false), is_soft_muted_(false),
chunk_reader_(), chunk_reader_(),
latest_chunk_(), latest_chunk_() {}
dma_size_(),
dma_queue_(nullptr) {}
I2SAudioOutput::~I2SAudioOutput() { I2SAudioOutput::~I2SAudioOutput() {}
if (dma_queue_ != nullptr) {
ClearDmaQueue();
}
// TODO: power down the DAC.
}
auto I2SAudioOutput::HasUnprocessedInput() -> bool { auto I2SAudioOutput::HasUnprocessedInput() -> bool {
if (dma_queue_ == nullptr || !dma_size_) { return latest_chunk_.size() > 0;
return false;
}
return latest_chunk_.size() >= *dma_size_;
} }
auto I2SAudioOutput::ProcessStreamInfo(const StreamInfo& info) auto I2SAudioOutput::ProcessStreamInfo(const StreamInfo& info)
@ -108,77 +98,30 @@ auto I2SAudioOutput::ProcessStreamInfo(const StreamInfo& info)
return cpp::fail(UNSUPPORTED_STREAM); return cpp::fail(UNSUPPORTED_STREAM);
} }
QueueHandle_t new_dma_queue = dac_->Reconfigure(bps, sample_rate);
xQueueCreate(kDmaQueueLength, sizeof(std::byte*));
dma_size_ = dac_->Reconfigure(bps, sample_rate, new_dma_queue);
if (dma_queue_ != nullptr) {
ClearDmaQueue();
}
dma_queue_ = new_dma_queue;
return {}; return {};
} }
auto I2SAudioOutput::ProcessChunk(const cpp::span<std::byte>& chunk) auto I2SAudioOutput::ProcessChunk(const cpp::span<std::byte>& chunk)
-> cpp::result<std::size_t, AudioProcessingError> { -> cpp::result<std::size_t, AudioProcessingError> {
ESP_LOGI(kTag, "received new samples");
latest_chunk_ = chunk_reader_->HandleNewData(chunk); latest_chunk_ = chunk_reader_->HandleNewData(chunk);
return 0; return 0;
} }
auto I2SAudioOutput::ProcessEndOfStream() -> void { auto I2SAudioOutput::ProcessEndOfStream() -> void {
if (chunk_reader_ && dma_size_) { SendOrBufferEvent(std::unique_ptr<StreamEvent>(
auto leftovers = chunk_reader_->GetLeftovers();
if (leftovers.size() > 0 && leftovers.size() < *dma_size_) {
std::byte* dest = static_cast<std::byte*>(malloc(*dma_size_));
cpp::span dest_span(dest, *dma_size_);
std::copy(leftovers.begin(), leftovers.end(), dest_span.begin());
std::fill(dest_span.begin() + leftovers.size(), dest_span.end(), static_cast<std::byte>(0));
xQueueSend(dma_queue_, &dest, portMAX_DELAY);
}
}
SendOrBufferEvent(
std::unique_ptr<StreamEvent>(
StreamEvent::CreateEndOfStream(input_events_))); StreamEvent::CreateEndOfStream(input_events_)));
chunk_reader_.reset();
dma_size_.reset();
} }
auto I2SAudioOutput::Process() -> cpp::result<void, AudioProcessingError> { auto I2SAudioOutput::Process() -> cpp::result<void, AudioProcessingError> {
std::size_t spaces_available = uxQueueSpacesAvailable(dma_queue_); // Note: no logging here!
if (spaces_available == 0) { std::size_t bytes_written = dac_->WriteData(latest_chunk_);
// TODO: think about this more. can this just be the output event queue? if (bytes_written == latest_chunk_.size_bytes()) {
vTaskDelay(pdMS_TO_TICKS(100)); latest_chunk_ = cpp::span<std::byte>();
return {}; chunk_reader_->HandleBytesLeftOver(0);
} } else {
latest_chunk_ = latest_chunk_.subspan(bytes_written);
// Fill the queue as much as possible, since we need to be able to stream
// FAST.
while (latest_chunk_.size() >= *dma_size_ && spaces_available > 0) {
// TODO: small memory arena for this?
std::byte* dest = static_cast<std::byte*>(malloc(*dma_size_));
cpp::span dest_span(dest, *dma_size_);
cpp::span src_span = latest_chunk_.first(*dma_size_);
std::copy(src_span.begin(), src_span.end(), dest_span.begin());
if (!xQueueSend(dma_queue_, &dest, 0)) {
// TODO: calculate how often we expect this to happen.
free(dest);
break;
}
latest_chunk_ = latest_chunk_.subspan(*dma_size_);
ESP_LOGI(kTag, "wrote dma buffer of size %u", *dma_size_);
}
if (latest_chunk_.size() < *dma_size_) {
// TODO: if this is the end of the stream, then we should be sending this
// with zero padding. hmm. i guess we need an explicit EOF event?
chunk_reader_->HandleBytesLeftOver(latest_chunk_.size());
ESP_LOGI(kTag, "not enough samples for dma buffer");
} }
return {}; return {};
} }
@ -202,17 +145,4 @@ auto I2SAudioOutput::SetSoftMute(bool enabled) -> void {
} }
} }
auto I2SAudioOutput::ClearDmaQueue() -> void {
// Ensure we don't leak any memory from events leftover in the queue.
while (uxQueueSpacesAvailable(dma_queue_) < kDmaQueueLength) {
std::byte* data = nullptr;
if (xQueueReceive(input_events_, &data, 0)) {
free(data);
} else {
break;
}
}
vQueueDelete(dma_queue_);
}
} // namespace audio } // namespace audio

@ -1,8 +1,8 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include <string>
#include <optional> #include <optional>
#include <string>
#include "audio_element.hpp" #include "audio_element.hpp"
#include "audio_element_handle.hpp" #include "audio_element_handle.hpp"

@ -38,8 +38,6 @@ class I2SAudioOutput : public IAudioElement {
auto SetVolume(uint8_t volume) -> void; auto SetVolume(uint8_t volume) -> void;
auto SetSoftMute(bool enabled) -> void; auto SetSoftMute(bool enabled) -> void;
auto ClearDmaQueue() -> void;
drivers::GpioExpander* expander_; drivers::GpioExpander* expander_;
std::unique_ptr<drivers::AudioDac> dac_; std::unique_ptr<drivers::AudioDac> dac_;
@ -48,8 +46,6 @@ class I2SAudioOutput : public IAudioElement {
std::optional<ChunkReader> chunk_reader_; std::optional<ChunkReader> chunk_reader_;
cpp::span<std::byte> latest_chunk_; cpp::span<std::byte> latest_chunk_;
std::optional<std::size_t> dma_size_;
QueueHandle_t dma_queue_;
}; };
} // namespace audio } // namespace audio

@ -8,9 +8,11 @@
#include "driver/i2s_common.h" #include "driver/i2s_common.h"
#include "driver/i2s_std.h" #include "driver/i2s_std.h"
#include "driver/i2s_types.h" #include "driver/i2s_types.h"
#include "esp_attr.h"
#include "esp_err.h" #include "esp_err.h"
#include "esp_log.h" #include "esp_log.h"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
#include "freertos/projdefs.h"
#include "hal/i2c_types.h" #include "hal/i2c_types.h"
#include "gpio_expander.hpp" #include "gpio_expander.hpp"
@ -28,22 +30,13 @@ static const AudioDac::SampleRate kDefaultSampleRate =
AudioDac::SAMPLE_RATE_44_1; AudioDac::SAMPLE_RATE_44_1;
static const AudioDac::BitsPerSample kDefaultBps = AudioDac::BPS_16; static const AudioDac::BitsPerSample kDefaultBps = AudioDac::BPS_16;
extern "C" {
bool dma_callback(i2s_chan_handle_t handle,
i2s_event_data_t* event,
void* user_ctx) {
AudioDac* dac = static_cast<AudioDac*>(user_ctx);
return dac->WriteDataFromISR(static_cast<std::byte*>(event->data),
event->size);
}
}
auto AudioDac::create(GpioExpander* expander) auto AudioDac::create(GpioExpander* expander)
-> cpp::result<std::unique_ptr<AudioDac>, Error> { -> cpp::result<std::unique_ptr<AudioDac>, Error> {
// TODO: tune. // TODO: tune.
i2s_chan_handle_t i2s_handle; i2s_chan_handle_t i2s_handle;
i2s_chan_config_t channel_config = i2s_chan_config_t channel_config =
I2S_CHANNEL_DEFAULT_CONFIG(kI2SPort, I2S_ROLE_MASTER); I2S_CHANNEL_DEFAULT_CONFIG(kI2SPort, I2S_ROLE_MASTER);
ESP_ERROR_CHECK(i2s_new_channel(&channel_config, &i2s_handle, NULL)); ESP_ERROR_CHECK(i2s_new_channel(&channel_config, &i2s_handle, NULL));
// //
// First, instantiate the instance so it can do all of its power on // First, instantiate the instance so it can do all of its power on
@ -108,8 +101,7 @@ AudioDac::AudioDac(GpioExpander* gpio, i2s_chan_handle_t i2s_handle)
i2s_handle_(i2s_handle), i2s_handle_(i2s_handle),
clock_config_(I2S_STD_CLK_DEFAULT_CONFIG(44100)), clock_config_(I2S_STD_CLK_DEFAULT_CONFIG(44100)),
slot_config_(I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, slot_config_(I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT,
I2S_SLOT_MODE_STEREO)), I2S_SLOT_MODE_STEREO)) {
dma_queue_(nullptr) {
gpio_->set_pin(GpioExpander::AUDIO_POWER_ENABLE, true); gpio_->set_pin(GpioExpander::AUDIO_POWER_ENABLE, true);
gpio_->Write(); gpio_->Write();
} }
@ -167,9 +159,7 @@ bool AudioDac::WaitForPowerState(
return has_matched; return has_matched;
} }
auto AudioDac::Reconfigure(BitsPerSample bps, auto AudioDac::Reconfigure(BitsPerSample bps, SampleRate rate) -> void {
SampleRate rate,
QueueHandle_t dma_queue) -> std::size_t {
// TODO(jacqueline): investigate how reliable the auto-clocking of the dac // TODO(jacqueline): investigate how reliable the auto-clocking of the dac
// is. We might need to explicit reconfigure the dac here as well if it's not // is. We might need to explicit reconfigure the dac here as well if it's not
// good enough. // good enough.
@ -183,44 +173,17 @@ auto AudioDac::Reconfigure(BitsPerSample bps,
bps == BPS_24 ? I2S_MCLK_MULTIPLE_384 : I2S_MCLK_MULTIPLE_256; bps == BPS_24 ? I2S_MCLK_MULTIPLE_384 : I2S_MCLK_MULTIPLE_256;
ESP_ERROR_CHECK(i2s_channel_reconfig_std_clock(i2s_handle_, &clock_config_)); ESP_ERROR_CHECK(i2s_channel_reconfig_std_clock(i2s_handle_, &clock_config_));
dma_queue_ = dma_queue;
// TODO: less spooky action here plz.
// dma_buffer_size = dma_frame_num (channel config) * slot_num (always 2?) *
// slot_bit_width / 8
//size_t dma_size = 240 * 2 * slot_config_.slot_bit_width / 8;
size_t dma_size = 960;
ESP_LOGI(kTag, "new dma size: %u bytes", dma_size);
i2s_event_callbacks_t callbacks = {
.on_recv = NULL,
.on_recv_q_ovf = NULL,
.on_sent = &dma_callback,
.on_send_q_ovf = NULL,
};
ESP_ERROR_CHECK(
i2s_channel_register_event_callback(i2s_handle_, &callbacks, this));
ESP_ERROR_CHECK(i2s_channel_enable(i2s_handle_)); ESP_ERROR_CHECK(i2s_channel_enable(i2s_handle_));
return dma_size;
} }
auto AudioDac::WriteDataFromISR(std::byte* data, std::size_t size) -> bool { auto AudioDac::WriteData(cpp::span<std::byte> data) -> std::size_t {
std::byte* new_data; std::size_t bytes_written = 0;
BaseType_t high_priority_task_awoken = pdFALSE; esp_err_t err = i2s_channel_write(i2s_handle_, data.data(), data.size_bytes(),
&bytes_written, 0);
if (xQueueReceiveFromISR(dma_queue_, &new_data, &high_priority_task_awoken)) { if (err != ESP_ERR_TIMEOUT) {
// Item was received. Copy it into the DMA buffer. ESP_ERROR_CHECK(err);
memcpy(data, new_data, size);
free(new_data);
ESP_DRAM_LOGI(kTag, "wrote dma");
} else {
// No item was received. Write empty data.
memset(data, 0, size);
} }
return bytes_written;
return high_priority_task_awoken;
} }
void AudioDac::WriteRegister(Register reg, uint8_t val) { void AudioDac::WriteRegister(Register reg, uint8_t val) {

@ -10,6 +10,7 @@
#include "driver/i2s_std.h" #include "driver/i2s_std.h"
#include "driver/i2s_types.h" #include "driver/i2s_types.h"
#include "esp_err.h" #include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
#include "result.hpp" #include "result.hpp"
#include "span.hpp" #include "span.hpp"
@ -67,10 +68,9 @@ class AudioDac {
}; };
// TODO(jacqueline): worth supporting channels here as well? // TODO(jacqueline): worth supporting channels here as well?
auto Reconfigure(BitsPerSample bps, SampleRate rate, QueueHandle_t dma_queue) auto Reconfigure(BitsPerSample bps, SampleRate rate) -> void;
-> std::size_t;
auto WriteDataFromISR(std::byte* data, std::size_t size) -> bool; auto WriteData(cpp::span<std::byte> data) -> std::size_t;
// Not copyable or movable. // Not copyable or movable.
AudioDac(const AudioDac&) = delete; AudioDac(const AudioDac&) = delete;
@ -83,9 +83,6 @@ class AudioDac {
i2s_std_clk_config_t clock_config_; i2s_std_clk_config_t clock_config_;
i2s_std_slot_config_t slot_config_; i2s_std_slot_config_t slot_config_;
// TODO: volatile?
volatile QueueHandle_t dma_queue_;
/* /*
* Pools the power state for up to 10ms, waiting for the given predicate to * Pools the power state for up to 10ms, waiting for the given predicate to
* be true. * be true.

Loading…
Cancel
Save