basic i2s output element

custom
jacqueline 2 years ago
parent e0b2562cc4
commit f35bb64c2b
  1. 2
      src/audio/CMakeLists.txt
  2. 4
      src/audio/audio_decoder.cpp
  3. 15
      src/audio/chunk.cpp
  4. 7
      src/audio/fatfs_audio_input.cpp
  5. 147
      src/audio/i2s_audio_output.cpp
  6. 13
      src/audio/include/audio_decoder.hpp
  7. 12
      src/audio/include/audio_element.hpp
  8. 14
      src/audio/include/fatfs_audio_input.hpp
  9. 38
      src/audio/include/i2s_audio_output.hpp
  10. 1
      src/codecs/include/mad.hpp
  11. 3
      src/codecs/test/test.mp3.hpp
  12. 26
      src/codecs/test/test_mad.cpp
  13. 2
      src/drivers/CMakeLists.txt
  14. 80
      src/drivers/dac.cpp
  15. 21
      src/drivers/include/dac.hpp
  16. 2
      src/main/CMakeLists.txt
  17. 5
      test/main/main.cpp

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

@ -40,7 +40,7 @@ auto AudioDecoder::SetOutputBuffer(MessageBufferHandle_t* buffer) -> void {
output_buffer_ = buffer; output_buffer_ = buffer;
} }
auto AudioDecoder::ProcessStreamInfo(StreamInfo& info) auto AudioDecoder::ProcessStreamInfo(const StreamInfo& info)
-> cpp::result<void, AudioProcessingError> { -> cpp::result<void, AudioProcessingError> {
stream_info_ = info; stream_info_ = info;
@ -62,7 +62,7 @@ auto AudioDecoder::ProcessStreamInfo(StreamInfo& info)
return {}; return {};
} }
auto AudioDecoder::ProcessChunk(cpp::span<std::byte>& chunk) auto AudioDecoder::ProcessChunk(const cpp::span<std::byte>& chunk)
-> cpp::result<size_t, AudioProcessingError> { -> cpp::result<size_t, AudioProcessingError> {
if (current_codec_ == nullptr) { if (current_codec_ == nullptr) {
// Should never happen, but fail explicitly anyway. // Should never happen, but fail explicitly anyway.

@ -12,8 +12,15 @@
namespace audio { namespace audio {
// TODO: tune. /*
const std::size_t kMaxChunkSize = 512; * 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 // TODO: tune
static const std::size_t kWorkingBufferSize = kMaxChunkSize * 1.5; static const std::size_t kWorkingBufferSize = kMaxChunkSize * 1.5;
@ -60,11 +67,11 @@ ChunkReader::ChunkReader(MessageBufferHandle_t* stream)
: stream_(stream), : stream_(stream),
raw_working_buffer_(static_cast<std::byte*>( raw_working_buffer_(static_cast<std::byte*>(
heap_caps_malloc(kWorkingBufferSize, MALLOC_CAP_SPIRAM))), heap_caps_malloc(kWorkingBufferSize, MALLOC_CAP_SPIRAM))),
working_buffer_(raw_working_buffer_, kWorkingBufferSize){}; working_buffer_(raw_working_buffer_, kWorkingBufferSize) {}
ChunkReader::~ChunkReader() { ChunkReader::~ChunkReader() {
free(raw_working_buffer_); free(raw_working_buffer_);
}; }
auto ChunkReader::Reset() -> void { auto ChunkReader::Reset() -> void {
leftover_bytes_ = 0; leftover_bytes_ = 0;

@ -4,7 +4,6 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include "audio_element.hpp"
#include "esp_heap_caps.h" #include "esp_heap_caps.h"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
@ -50,7 +49,7 @@ FatfsAudioInput::~FatfsAudioInput() {
free(output_buffer_); free(output_buffer_);
} }
auto FatfsAudioInput::ProcessStreamInfo(StreamInfo& info) auto FatfsAudioInput::ProcessStreamInfo(const StreamInfo& info)
-> cpp::result<void, AudioProcessingError> { -> cpp::result<void, AudioProcessingError> {
if (is_file_open_) { if (is_file_open_) {
f_close(&current_file_); f_close(&current_file_);
@ -83,12 +82,12 @@ auto FatfsAudioInput::ProcessStreamInfo(StreamInfo& info)
return {}; return {};
} }
auto FatfsAudioInput::ProcessChunk(cpp::span<std::byte>& chunk) auto FatfsAudioInput::ProcessChunk(const cpp::span<std::byte>& chunk)
-> cpp::result<size_t, AudioProcessingError> { -> cpp::result<size_t, AudioProcessingError> {
return cpp::fail(UNSUPPORTED_STREAM); return cpp::fail(UNSUPPORTED_STREAM);
} }
auto FatfsAudioInput::GetRingBufferDistance() -> size_t { auto FatfsAudioInput::GetRingBufferDistance() const -> size_t {
if (file_buffer_read_pos_ == file_buffer_write_pos_) { if (file_buffer_read_pos_ == file_buffer_write_pos_) {
return 0; return 0;
} }

@ -2,18 +2,20 @@
#include <algorithm> #include <algorithm>
#include "audio_element.h"
#include "driver/i2s.h"
#include "esp_err.h" #include "esp_err.h"
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
#include "i2s_stream.h"
static const i2s_port_t kI2SPort = I2S_NUM_0; #include "audio_element.hpp"
#include "dac.hpp"
#include "gpio_expander.hpp"
#include "result.hpp"
static const TickType_t kIdleTimeBeforeMute = pdMS_TO_TICKS(1000);
static const char* kTag = "I2SOUT"; static const char* kTag = "I2SOUT";
namespace drivers { namespace audio {
auto I2SAudioOutput::create(GpioExpander* expander) auto I2SAudioOutput::create(drivers::GpioExpander* expander)
-> cpp::result<std::unique_ptr<I2SAudioOutput>, Error> { -> cpp::result<std::unique_ptr<I2SAudioOutput>, Error> {
// First, we need to perform initial configuration of the DAC chip. // First, we need to perform initial configuration of the DAC chip.
auto dac_result = drivers::AudioDac::create(expander); auto dac_result = drivers::AudioDac::create(expander);
@ -21,72 +23,82 @@ auto I2SAudioOutput::create(GpioExpander* expander)
ESP_LOGE(kTag, "failed to init dac: %d", dac_result.error()); ESP_LOGE(kTag, "failed to init dac: %d", dac_result.error());
return cpp::fail(DAC_CONFIG); return cpp::fail(DAC_CONFIG);
} }
std::unique_ptr<AudioDac> dac = std::move(dac_result.value()); std::unique_ptr<drivers::AudioDac> dac = std::move(dac_result.value());
// Soft mute immediately, in order to minimise any clicks and pops caused by // Soft mute immediately, in order to minimise any clicks and pops caused by
// the initial output element and pipeline configuration. // the initial output element and pipeline configuration.
dac->WriteVolume(255); dac->WriteVolume(255);
i2s_stream_cfg_t i2s_stream_config = i2s_stream_cfg_t{ return std::make_unique<I2SAudioOutput>(expander, std::move(dac));
.type = AUDIO_STREAM_WRITER, }
.i2s_config =
{ I2SAudioOutput::I2SAudioOutput(drivers::GpioExpander* expander,
// static_cast bc esp-adf uses enums incorrectly std::unique_ptr<drivers::AudioDac> dac)
.mode = static_cast<i2s_mode_t>(I2S_MODE_MASTER | I2S_MODE_TX), : expander_(expander),
.sample_rate = 44100, dac_(std::move(dac)),
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, volume_(255),
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, is_soft_muted_(false) {}
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LOWMED, I2SAudioOutput::~I2SAudioOutput() {
.dma_buf_count = 8, // TODO: power down the DAC.
.dma_buf_len = 64, }
.use_apll = false,
.tx_desc_auto_clear = false, auto I2SAudioOutput::ProcessStreamInfo(const StreamInfo& info)
.fixed_mclk = 0, -> cpp::result<void, AudioProcessingError> {
.mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT, // TODO(jacqueline): probs do something with the channel hey
.bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT,
}, if (!info.BitsPerSample() && !info.SampleRate()) {
.i2s_port = kI2SPort, return cpp::fail(UNSUPPORTED_STREAM);
.use_alc = false, }
.volume = 0, // Does nothing; use AudioDac to change this.
.out_rb_size = I2S_STREAM_RINGBUFFER_SIZE, drivers::AudioDac::BitsPerSample bps;
.task_stack = I2S_STREAM_TASK_STACK, switch (*info.BitsPerSample()) {
.task_core = I2S_STREAM_TASK_CORE, case 16:
.task_prio = I2S_STREAM_TASK_PRIO, bps = drivers::AudioDac::BPS_16;
.stack_in_ext = false, break;
.multi_out_num = 0, case 24:
.uninstall_drv = true, bps = drivers::AudioDac::BPS_24;
.need_expand = false, break;
.expand_src_bits = I2S_BITS_PER_SAMPLE_16BIT, case 32:
}; bps = drivers::AudioDac::BPS_32;
audio_element_handle_t i2s_stream_writer = break;
i2s_stream_init(&i2s_stream_config); default:
if (i2s_stream_writer == NULL) { return cpp::fail(UNSUPPORTED_STREAM);
return cpp::fail(Error::STREAM_INIT);
} }
// NOTE: i2s_stream_init does some additional setup that hardcodes MCK as drivers::AudioDac::SampleRate sample_rate;
// GPIO0. This happens to work fine for us, but be careful if changing. switch (*info.SampleRate()) {
i2s_pin_config_t pin_config = {.mck_io_num = GPIO_NUM_0, case 44100:
.bck_io_num = GPIO_NUM_26, sample_rate = drivers::AudioDac::SAMPLE_RATE_44_1;
.ws_io_num = GPIO_NUM_27, break;
.data_out_num = GPIO_NUM_5, case 48000:
.data_in_num = I2S_PIN_NO_CHANGE}; sample_rate = drivers::AudioDac::SAMPLE_RATE_48;
if (esp_err_t err = i2s_set_pin(kI2SPort, &pin_config) != ESP_OK) { break;
ESP_LOGE(kTag, "failed to configure i2s pins %x", err); default:
return cpp::fail(Error::I2S_CONFIG); return cpp::fail(UNSUPPORTED_STREAM);
} }
return std::make_unique<I2SAudioOutput>(dac, i2s_stream_writer); dac_->Reconfigure(bps, sample_rate);
return {};
} }
I2SAudioOutput::I2SAudioOutput(std::unique_ptr<AudioDac>& dac, auto I2SAudioOutput::ProcessChunk(const cpp::span<std::byte>& chunk)
audio_element_handle_t element) -> cpp::result<std::size_t, AudioProcessingError> {
: IAudioOutput(element), dac_(std::move(dac)) { SetSoftMute(false);
volume_ = 255; // TODO(jacqueline): write smaller parts with a small delay so that we can
// be responsive to pause and seek commands.
return dac_->WriteData(chunk, portMAX_DELAY);
} }
I2SAudioOutput::~I2SAudioOutput() {
// TODO: power down the DAC. auto I2SAudioOutput::IdleTimeout() const -> TickType_t {
return kIdleTimeBeforeMute;
}
auto I2SAudioOutput::ProcessIdle() -> cpp::result<void, AudioProcessingError> {
// TODO(jacqueline): Consider powering down the dac completely maybe?
SetSoftMute(true);
return {};
} }
auto I2SAudioOutput::SetVolume(uint8_t volume) -> void { auto I2SAudioOutput::SetVolume(uint8_t volume) -> void {
@ -97,18 +109,15 @@ auto I2SAudioOutput::SetVolume(uint8_t volume) -> void {
} }
auto I2SAudioOutput::SetSoftMute(bool enabled) -> void { auto I2SAudioOutput::SetSoftMute(bool enabled) -> void {
if (enabled) { if (enabled == is_soft_muted_) {
is_soft_muted_ = true; return;
}
is_soft_muted_ = enabled;
if (is_soft_muted_) {
dac_->WriteVolume(255); dac_->WriteVolume(255);
} else { } else {
is_soft_muted_ = false;
dac_->WriteVolume(volume_); dac_->WriteVolume(volume_);
} }
} }
auto I2SAudioOutput::Configure(audio_element_info_t& info) -> void { } // namespace audio
audio_element_setinfo(element_, &info);
i2s_stream_set_clk(element_, info.sample_rates, info.bits, info.channels);
}
} // namespace drivers

@ -2,6 +2,7 @@
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <memory>
#include "ff.h" #include "ff.h"
#include "span.hpp" #include "span.hpp"
@ -23,11 +24,13 @@ class AudioDecoder : public IAudioElement {
auto SetInputBuffer(MessageBufferHandle_t*) -> void; auto SetInputBuffer(MessageBufferHandle_t*) -> void;
auto SetOutputBuffer(MessageBufferHandle_t*) -> void; auto SetOutputBuffer(MessageBufferHandle_t*) -> void;
auto ProcessStreamInfo(StreamInfo& info) auto StackSizeBytes() const -> std::size_t override { return 8196; };
-> cpp::result<void, AudioProcessingError>;
auto ProcessChunk(cpp::span<std::byte>& chunk) auto ProcessStreamInfo(const StreamInfo& info)
-> cpp::result<std::size_t, AudioProcessingError>; -> cpp::result<void, AudioProcessingError> override;
auto ProcessIdle() -> cpp::result<void, AudioProcessingError>; auto ProcessChunk(const cpp::span<std::byte>& chunk)
-> cpp::result<std::size_t, AudioProcessingError> override;
auto ProcessIdle() -> cpp::result<void, AudioProcessingError> override;
AudioDecoder(const AudioDecoder&) = delete; AudioDecoder(const AudioDecoder&) = delete;
AudioDecoder& operator=(const AudioDecoder&) = delete; AudioDecoder& operator=(const AudioDecoder&) = delete;

@ -48,26 +48,26 @@ class IAudioElement {
* be tuned according to the observed stack size of each element, as different * be tuned according to the observed stack size of each element, as different
* elements have fairly different stack requirements. * elements have fairly different stack requirements.
*/ */
virtual auto StackSizeBytes() -> std::size_t { return 2048; }; virtual auto StackSizeBytes() const -> std::size_t { return 2048; };
/* /*
* How long to wait for new data on the input stream before triggering a call * How long to wait for new data on the input stream before triggering a call
* to ProcessIdle(). If this is portMAX_DELAY (the default), then ProcessIdle * to ProcessIdle(). If this is portMAX_DELAY (the default), then ProcessIdle
* will never be called. * will never be called.
*/ */
virtual auto IdleTimeout() -> TickType_t { return portMAX_DELAY; } virtual auto IdleTimeout() const -> TickType_t { return portMAX_DELAY; }
/* Returns this element's input buffer. */ /* Returns this element's input buffer. */
auto InputBuffer() -> MessageBufferHandle_t* { return input_buffer_; } auto InputBuffer() const -> MessageBufferHandle_t* { return input_buffer_; }
/* Returns this element's output buffer. */ /* Returns this element's output buffer. */
auto OutputBuffer() -> MessageBufferHandle_t* { return output_buffer_; } auto OutputBuffer() const -> MessageBufferHandle_t* { return output_buffer_; }
/* /*
* Called when a StreamInfo message is received. Used to configure this * Called when a StreamInfo message is received. Used to configure this
* element in preperation for incoming chunks. * element in preperation for incoming chunks.
*/ */
virtual auto ProcessStreamInfo(StreamInfo& info) virtual auto ProcessStreamInfo(const StreamInfo& info)
-> cpp::result<void, AudioProcessingError> = 0; -> cpp::result<void, AudioProcessingError> = 0;
/* /*
@ -76,7 +76,7 @@ class IAudioElement {
* bytes in this chunk that were actually used; leftover bytes will be * bytes in this chunk that were actually used; leftover bytes will be
* prepended to the next call. * prepended to the next call.
*/ */
virtual auto ProcessChunk(cpp::span<std::byte>& chunk) virtual auto ProcessChunk(const cpp::span<std::byte>& chunk)
-> cpp::result<std::size_t, AudioProcessingError> = 0; -> cpp::result<std::size_t, AudioProcessingError> = 0;
/* /*

@ -17,19 +17,19 @@ namespace audio {
class FatfsAudioInput : public IAudioElement { class FatfsAudioInput : public IAudioElement {
public: public:
FatfsAudioInput(std::shared_ptr<drivers::SdStorage> storage); explicit FatfsAudioInput(std::shared_ptr<drivers::SdStorage> storage);
~FatfsAudioInput(); ~FatfsAudioInput();
auto ProcessStreamInfo(StreamInfo& info) auto ProcessStreamInfo(const StreamInfo& info)
-> cpp::result<void, AudioProcessingError>; -> cpp::result<void, AudioProcessingError> override;
auto ProcessChunk(cpp::span<std::byte>& chunk) auto ProcessChunk(const cpp::span<std::byte>& chunk)
-> cpp::result<std::size_t, AudioProcessingError> = 0; -> cpp::result<std::size_t, AudioProcessingError> override;
auto ProcessIdle() -> cpp::result<void, AudioProcessingError>; auto ProcessIdle() -> cpp::result<void, AudioProcessingError> override;
auto SendChunk(cpp::span<std::byte> dest) -> size_t; auto SendChunk(cpp::span<std::byte> dest) -> size_t;
private: private:
auto GetRingBufferDistance() -> size_t; auto GetRingBufferDistance() const -> size_t;
std::shared_ptr<drivers::SdStorage> storage_; std::shared_ptr<drivers::SdStorage> storage_;

@ -3,30 +3,46 @@
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include "audio_element.hpp"
#include "result.hpp" #include "result.hpp"
#include "dac.hpp" #include "dac.hpp"
#include "gpio_expander.hpp" #include "gpio_expander.hpp"
#include "sys/_stdint.h"
namespace drivers { namespace audio {
class I2SAudioOutput : public IAudioOutput { class I2SAudioOutput : public IAudioElement {
public: public:
enum Error { DAC_CONFIG, I2S_CONFIG, STREAM_INIT }; enum Error { DAC_CONFIG, I2S_CONFIG, STREAM_INIT };
static auto create(GpioExpander* expander) static auto create(drivers::GpioExpander* expander)
-> cpp::result<std::unique_ptr<I2SAudioOutput>, Error>; -> cpp::result<std::unique_ptr<I2SAudioOutput>, Error>;
I2SAudioOutput(std::unique_ptr<AudioDac>& dac, I2SAudioOutput(drivers::GpioExpander* expander,
audio_element_handle_t element); std::unique_ptr<drivers::AudioDac> dac);
~I2SAudioOutput(); ~I2SAudioOutput();
virtual auto SetVolume(uint8_t volume) -> void; auto SetInputBuffer(MessageBufferHandle_t* in) -> void { input_buffer_ = in; }
virtual auto Configure(audio_element_info_t& info) -> void;
virtual auto SetSoftMute(bool enabled) -> void; auto IdleTimeout() const -> TickType_t override;
auto ProcessStreamInfo(const StreamInfo& info)
-> cpp::result<void, AudioProcessingError> override;
auto ProcessChunk(const cpp::span<std::byte>& chunk)
-> cpp::result<std::size_t, AudioProcessingError> override;
auto ProcessIdle() -> cpp::result<void, AudioProcessingError> override;
I2SAudioOutput(const I2SAudioOutput&) = delete;
I2SAudioOutput& operator=(const I2SAudioOutput&) = delete;
private: private:
std::unique_ptr<AudioDac> dac_; auto SetVolume(uint8_t volume) -> void;
bool is_soft_muted_ = false; auto SetSoftMute(bool enabled) -> void;
drivers::GpioExpander* expander_;
std::unique_ptr<drivers::AudioDac> dac_;
uint8_t volume_;
bool is_soft_muted_;
}; };
} // namespace drivers } // namespace audio

@ -3,6 +3,7 @@
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <string> #include <string>
#include <utility>
#include "mad.h" #include "mad.h"
#include "span.hpp" #include "span.hpp"

@ -96,6 +96,5 @@ std::uint8_t test_mp3[] = {
0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa};
};
std::size_t test_mp3_len = 1147; std::size_t test_mp3_len = 1147;

@ -1,15 +1,16 @@
#include "mad.hpp" #include "mad.hpp"
#include <cstdint>
#include <algorithm> #include <algorithm>
#include <cstdint>
#include "span.hpp"
#include "catch2/catch.hpp" #include "catch2/catch.hpp"
#include "span.hpp"
#include "test.mp3.hpp" #include "test.mp3.hpp"
void load_mp3(cpp::span<std::byte> dest) { void load_mp3(cpp::span<std::byte> dest) {
cpp::span<std::byte> src(reinterpret_cast<std::byte*>(test_mp3), test_mp3_len); cpp::span<std::byte> src(reinterpret_cast<std::byte*>(test_mp3),
test_mp3_len);
std::copy(src.begin(), src.begin() + dest.size(), dest.begin()); std::copy(src.begin(), src.begin() + dest.size(), dest.begin());
} }
@ -24,7 +25,7 @@ TEST_CASE("libmad mp3 decoder", "[unit]") {
} }
SECTION("processes streams correctly") { SECTION("processes streams correctly") {
std::array<std::byte, 256> input; std::array<std::byte, 2048> input;
input.fill(std::byte{0}); input.fill(std::byte{0});
decoder.ResetForNewStream(); decoder.ResetForNewStream();
@ -38,7 +39,7 @@ TEST_CASE("libmad mp3 decoder", "[unit]") {
} }
SECTION("invalid stream fails") { SECTION("invalid stream fails") {
input.fill(std::byte{0xFF}); input.fill(std::byte{0x69});
auto result = decoder.ProcessNextFrame(); auto result = decoder.ProcessNextFrame();
@ -65,23 +66,26 @@ TEST_CASE("libmad mp3 decoder", "[unit]") {
// Just check that some kind of data was written. We don't care // Just check that some kind of data was written. We don't care
// about what. // about what.
REQUIRE(std::to_integer<uint8_t>(output[0]) != 0); bool wrote_something = false;
REQUIRE(std::to_integer<uint8_t>(output[1]) != 0); for (int i = 0; i < output.size(); i++) {
REQUIRE(std::to_integer<uint8_t>(output[2]) != 0); if (std::to_integer<uint8_t>(output[0]) != 0) {
REQUIRE(std::to_integer<uint8_t>(output[3]) != 0); wrote_something = true;
break;
}
}
REQUIRE(wrote_something == true);
} }
SECTION("output format correct") { SECTION("output format correct") {
auto format = decoder.GetOutputFormat(); auto format = decoder.GetOutputFormat();
// Matches the test data. // Matches the test data.
REQUIRE(format.num_channels == 2); REQUIRE(format.num_channels == 1);
REQUIRE(format.sample_rate_hz == 44100); REQUIRE(format.sample_rate_hz == 44100);
// Matches libmad output // Matches libmad output
REQUIRE(format.bits_per_sample == 24); REQUIRE(format.bits_per_sample == 24);
} }
} }
} }
} }

@ -2,5 +2,5 @@ idf_component_register(
SRCS "dac.cpp" "gpio_expander.cpp" "battery.cpp" "storage.cpp" "i2c.cpp" SRCS "dac.cpp" "gpio_expander.cpp" "battery.cpp" "storage.cpp" "i2c.cpp"
"spi.cpp" "display.cpp" "display_init.cpp" "spi.cpp" "display.cpp" "display_init.cpp"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
REQUIRES "esp_adc_cal" "fatfs" "result" "lvgl") REQUIRES "esp_adc_cal" "fatfs" "result" "lvgl" "span")
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})

@ -4,11 +4,13 @@
#include "assert.h" #include "assert.h"
#include "driver/i2c.h" #include "driver/i2c.h"
#include "driver/i2s.h"
#include "esp_err.h" #include "esp_err.h"
#include "esp_log.h" #include "esp_log.h"
#include "hal/i2c_types.h" #include "hal/i2c_types.h"
#include "gpio_expander.hpp" #include "gpio_expander.hpp"
#include "hal/i2s_types.h"
#include "i2c.hpp" #include "i2c.hpp"
namespace drivers { namespace drivers {
@ -16,11 +18,55 @@ namespace drivers {
static const char* kTag = "AUDIODAC"; static const char* kTag = "AUDIODAC";
static const uint8_t kPcm5122Address = 0x4C; static const uint8_t kPcm5122Address = 0x4C;
static const uint8_t kPcm5122Timeout = 100 / portTICK_RATE_MS; static const uint8_t kPcm5122Timeout = 100 / portTICK_RATE_MS;
static const i2s_port_t kI2SPort = I2S_NUM_0;
static const AudioDac::SampleRate kDefaultSampleRate =
AudioDac::SAMPLE_RATE_44_1;
static const AudioDac::BitsPerSample kDefaultBps = AudioDac::BPS_16;
auto AudioDac::create(GpioExpander* expander) auto AudioDac::create(GpioExpander* expander)
-> cpp::result<std::unique_ptr<AudioDac>, Error> { -> cpp::result<std::unique_ptr<AudioDac>, Error> {
// First, instantiate the instance so it can do all of its power on
// configuration.
std::unique_ptr<AudioDac> dac = std::make_unique<AudioDac>(expander); std::unique_ptr<AudioDac> dac = std::make_unique<AudioDac>(expander);
// Whilst we wait for the initial boot, we can work on installing the I2S
// driver.
i2s_config_t i2s_config = {
// static_cast bc esp-adf uses enums incorrectly
.mode = static_cast<i2s_mode_t>(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = 44100,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LOWMED,
// TODO(jacqueline): tune dma buffer size. this seems very smol.
.dma_buf_count = 8,
.dma_buf_len = 64,
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT,
.bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT,
};
if (esp_err_t err =
i2s_driver_install(kI2SPort, &i2s_config, 0, NULL) != ESP_OK) {
ESP_LOGE(kTag, "failed to configure i2s pins %x", err);
return cpp::fail(Error::FAILED_TO_INSTALL_I2S);
}
i2s_pin_config_t pin_config = {.mck_io_num = GPIO_NUM_0,
.bck_io_num = GPIO_NUM_26,
.ws_io_num = GPIO_NUM_27,
.data_out_num = GPIO_NUM_5,
.data_in_num = I2S_PIN_NO_CHANGE};
if (esp_err_t err = i2s_set_pin(kI2SPort, &pin_config) != ESP_OK) {
ESP_LOGE(kTag, "failed to configure i2s pins %x", err);
return cpp::fail(Error::FAILED_TO_INSTALL_I2S);
}
// Now let's double check that the DAC itself came up whilst we we working.
bool is_booted = dac->WaitForPowerState( bool is_booted = dac->WaitForPowerState(
[](bool booted, PowerState state) { return booted; }); [](bool booted, PowerState state) { return booted; });
if (!is_booted) { if (!is_booted) {
@ -28,8 +74,9 @@ auto AudioDac::create(GpioExpander* expander)
return cpp::fail(Error::FAILED_TO_BOOT); return cpp::fail(Error::FAILED_TO_BOOT);
} }
// Write the initial configuration.
dac->WriteRegister(Register::DE_EMPHASIS, 1 << 4); dac->WriteRegister(Register::DE_EMPHASIS, 1 << 4);
dac->WriteVolume(100); dac->WriteVolume(255);
bool is_configured = bool is_configured =
dac->WaitForPowerState([](bool booted, PowerState state) { dac->WaitForPowerState([](bool booted, PowerState state) {
@ -43,14 +90,16 @@ auto AudioDac::create(GpioExpander* expander)
return dac; return dac;
} }
AudioDac::AudioDac(GpioExpander* gpio) { AudioDac::AudioDac(GpioExpander* gpio) : gpio_(gpio) {
this->gpio_ = gpio; gpio_->set_pin(GpioExpander::AUDIO_POWER_ENABLE, true);
}; gpio_->Write();
}
AudioDac::~AudioDac(){ AudioDac::~AudioDac() {
// TODO: reset stuff like de-emphasis? Reboot the whole dac? Need to think i2s_driver_uninstall(kI2SPort);
// about this. gpio_->set_pin(GpioExpander::AUDIO_POWER_ENABLE, false);
}; gpio_->Write();
}
void AudioDac::WriteVolume(uint8_t volume) { void AudioDac::WriteVolume(uint8_t volume) {
WriteRegister(Register::DIGITAL_VOLUME_L, volume); WriteRegister(Register::DIGITAL_VOLUME_L, volume);
@ -93,6 +142,21 @@ bool AudioDac::WaitForPowerState(
return has_matched; return has_matched;
} }
auto AudioDac::Reconfigure(BitsPerSample bps, SampleRate rate) -> bool {
// 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
// good enough.
i2s_set_clk(kI2SPort, rate, bps, I2S_CHANNEL_STEREO);
return true;
}
auto AudioDac::WriteData(const cpp::span<std::byte>& data, TickType_t max_wait)
-> std::size_t {
std::size_t res = 0;
i2s_write(kI2SPort, data.data(), data.size(), &res, max_wait);
return res;
}
void AudioDac::WriteRegister(Register reg, uint8_t val) { void AudioDac::WriteRegister(Register reg, uint8_t val) {
I2CTransaction transaction; I2CTransaction transaction;
transaction.start() transaction.start()

@ -5,7 +5,10 @@
#include <functional> #include <functional>
#include "esp_err.h" #include "esp_err.h"
#include "freertos/portmacro.h"
#include "hal/i2s_types.h"
#include "result.hpp" #include "result.hpp"
#include "span.hpp"
#include "gpio_expander.hpp" #include "gpio_expander.hpp"
@ -19,7 +22,9 @@ class AudioDac {
enum Error { enum Error {
FAILED_TO_BOOT, FAILED_TO_BOOT,
FAILED_TO_CONFIGURE, FAILED_TO_CONFIGURE,
FAILED_TO_INSTALL_I2S,
}; };
static auto create(GpioExpander* expander) static auto create(GpioExpander* expander)
-> cpp::result<std::unique_ptr<AudioDac>, Error>; -> cpp::result<std::unique_ptr<AudioDac>, Error>;
@ -47,6 +52,22 @@ class AudioDac {
/* Returns the current boot-up status and internal state of the DAC */ /* Returns the current boot-up status and internal state of the DAC */
std::pair<bool, PowerState> ReadPowerState(); std::pair<bool, PowerState> ReadPowerState();
enum BitsPerSample {
BPS_16 = I2S_BITS_PER_SAMPLE_16BIT,
BPS_24 = I2S_BITS_PER_SAMPLE_24BIT,
BPS_32 = I2S_BITS_PER_SAMPLE_32BIT
};
enum SampleRate {
SAMPLE_RATE_44_1 = 44100,
SAMPLE_RATE_48 = 48000,
};
// TODO(jacqueline): worth supporting channels here as well?
auto Reconfigure(BitsPerSample bps, SampleRate rate) -> bool;
auto WriteData(const cpp::span<std::byte>& data, TickType_t max_wait)
-> std::size_t;
// Not copyable or movable. // Not copyable or movable.
AudioDac(const AudioDac&) = delete; AudioDac(const AudioDac&) = delete;
AudioDac& operator=(const AudioDac&) = delete; AudioDac& operator=(const AudioDac&) = delete;

@ -1,5 +1,5 @@
idf_component_register( idf_component_register(
SRCS "main.cpp" "app_console.cpp" SRCS "main.cpp" "app_console.cpp"
INCLUDE_DIRS "." INCLUDE_DIRS "."
REQUIRES "drivers" "dev_console") REQUIRES "audio" "drivers" "dev_console" "drivers")
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})

@ -24,8 +24,9 @@ class TestConsole : public Console {
protected: protected:
virtual auto RegisterExtraComponents() -> void { RegisterCatch2(); } virtual auto RegisterExtraComponents() -> void { RegisterCatch2(); }
virtual auto GetStackSizeKiB() -> uint16_t { virtual auto GetStackSizeKiB() -> uint16_t {
// Catch2 requires a particularly large stack. // Catch2 requires a particularly large stack to begin with, and some of the
return 24; // tests (*cough*libmad*cough*) also use a lot of stack.
return 64;
} }
}; };

Loading…
Cancel
Save