From 4fc5f931acf5bdc582fdad3cb48e6810964198c5 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 12 Oct 2022 11:59:42 +1100 Subject: [PATCH] WIP use result<> and RAII --- main/CMakeLists.txt | 20 ++- main/battery.cpp | 6 +- main/battery.h | 2 +- main/dac.cpp | 93 ++++++----- main/dac.h | 101 ++++++------ main/gay-ipod-fw.cpp | 219 +++++++------------------ main/gpio-expander.cpp | 34 ++-- main/gpio-expander.h | 364 ++++++++++++++++++++--------------------- main/i2c.cpp | 5 +- main/i2c.h | 111 +++++++------ main/playback.cpp | 224 +++++++++++++++++++++++++ main/playback.h | 68 ++++++++ main/storage.cpp | 120 ++++++++++---- main/storage.h | 104 +++++------- 14 files changed, 868 insertions(+), 603 deletions(-) create mode 100644 main/playback.cpp create mode 100644 main/playback.h diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index ec2bc4f9..7ab321c3 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,5 +1,15 @@ -idf_component_register( - SRCS "gay-ipod-fw.cpp" "dac.cpp" "gpio-expander.cpp" "battery.cpp" "storage.cpp" "i2c.cpp" - INCLUDE_DIRS "." - REQUIRES "esp_adc_cal" "fatfs" "audio_pipeline" "audio_stream" "result" -) +idf_component_register(SRCS + "gay-ipod-fw.cpp" + "dac.cpp" + "gpio-expander.cpp" + "battery.cpp" + "storage.cpp" + "i2c.cpp" + "playback.cpp" INCLUDE_DIRS "." REQUIRES + "esp_adc_cal" + "fatfs" + "audio_pipeline" + "audio_stream" + "result") + target_compile_options(${COMPONENT_LIB} PRIVATE - + DRESULT_DISABLE_EXCEPTIONS) diff --git a/main/battery.cpp b/main/battery.cpp index 179a6439..d175ab67 100644 --- a/main/battery.cpp +++ b/main/battery.cpp @@ -11,8 +11,8 @@ static esp_adc_cal_characteristics_t calibration; esp_err_t init_adc(void) { // Calibration should already be fused into the chip from the factory, so // we should only need to read it back out again. - esp_adc_cal_characterize( - ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 0, &calibration); + esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 0, + &calibration); // Max battery voltage should be a little over 2V due to our divider, so // we need the max attenuation to properly handle the full range. @@ -28,4 +28,4 @@ uint32_t read_battery_voltage(void) { return esp_adc_cal_raw_to_voltage(raw, &calibration); } -} // namespace gay_ipod +} // namespace gay_ipod diff --git a/main/battery.h b/main/battery.h index 4b7bece0..399e866f 100644 --- a/main/battery.h +++ b/main/battery.h @@ -13,4 +13,4 @@ esp_err_t init_adc(void); */ uint32_t read_battery_voltage(void); -} // namespace gay_ipod +} // namespace gay_ipod diff --git a/main/dac.cpp b/main/dac.cpp index 513793db..0e34c98f 100644 --- a/main/dac.cpp +++ b/main/dac.cpp @@ -1,41 +1,56 @@ #include "dac.h" -#include "esp_err.h" -#include "i2c.h" -#include "esp_log.h" +#include + #include "assert.h" #include "driver/i2c.h" +#include "esp_err.h" +#include "esp_log.h" #include "gpio-expander.h" #include "hal/i2c_types.h" -#include +#include "i2c.h" namespace gay_ipod { -static const char* TAG = "AUDIODAC"; - -AudioDac::AudioDac(GpioExpander *gpio) { - this->gpio_ = gpio; -}; +static const char* kTag = "AUDIODAC"; +static const uint8_t kPcm5122Address = 0x4C; +static const uint8_t kPcm5122Timeout = 100 / portTICK_RATE_MS; -AudioDac::~AudioDac() {}; +auto AudioDac::create(GpioExpander* expander) + -> cpp::result, Error> { + std::unique_ptr dac = std::make_unique(expander); -esp_err_t AudioDac::Start() { - bool is_booted = WaitForPowerState([](bool booted, PowerState state){ return booted; }); + bool is_booted = dac->WaitForPowerState( + [](bool booted, PowerState state) { return booted; }); if (!is_booted) { - ESP_LOGE(TAG, "Timed out waiting for boot"); - return ESP_ERR_TIMEOUT; + ESP_LOGE(kTag, "Timed out waiting for boot"); + return cpp::fail(Error::FAILED_TO_BOOT); } - WriteRegister(Register::DE_EMPHASIS, 1 << 4); - WriteVolume(100); + dac->WriteRegister(Register::DE_EMPHASIS, 1 << 4); + dac->WriteVolume(100); - WaitForPowerState([](bool booted, PowerState state){ - return state == WAIT_FOR_CP || state == RAMP_UP || state == RUN || state == STANDBY; - }); + bool is_configured = + dac->WaitForPowerState([](bool booted, PowerState state) { + return state == WAIT_FOR_CP || state == RAMP_UP || state == RUN || + state == STANDBY; + }); + if (!is_configured) { + return cpp::fail(Error::FAILED_TO_CONFIGURE); + } - return ESP_OK; + return dac; } +AudioDac::AudioDac(GpioExpander* gpio) { + this->gpio_ = gpio; +}; + +AudioDac::~AudioDac(){ + // TODO: reset stuff like de-emphasis? Reboot the whole dac? Need to think + // about this. +}; + void AudioDac::WriteVolume(uint8_t volume) { WriteRegister(Register::DIGITAL_VOLUME_L, volume); WriteRegister(Register::DIGITAL_VOLUME_R, volume); @@ -45,31 +60,32 @@ std::pair AudioDac::ReadPowerState() { uint8_t result = 0; I2CTransaction transaction; - transaction - .start() - .write_addr(kPCM5122Address, I2C_MASTER_WRITE) - .write_ack(DSP_BOOT_POWER_STATE) - .start() - .write_addr(kPCM5122Address, I2C_MASTER_READ) - .read(&result, I2C_MASTER_NACK) - .stop(); + transaction.start() + .write_addr(kPcm5122Address, I2C_MASTER_WRITE) + .write_ack(DSP_BOOT_POWER_STATE) + .start() + .write_addr(kPcm5122Address, I2C_MASTER_READ) + .read(&result, I2C_MASTER_NACK) + .stop(); ESP_ERROR_CHECK(transaction.Execute()); bool is_booted = result >> 7; - PowerState detail = (PowerState) (result & 0b1111); + PowerState detail = (PowerState)(result & 0b1111); return std::pair(is_booted, detail); } -bool AudioDac::WaitForPowerState(std::function predicate) { +bool AudioDac::WaitForPowerState( + std::function predicate) { bool has_matched = false; - for (int i=0; i<10; i++) { + for (int i = 0; i < 10; i++) { std::pair result = ReadPowerState(); has_matched = predicate(result.first, result.second); if (has_matched) { break; } else { - ESP_LOGI(TAG, "Waiting for power state (was %d %x)", result.first, (uint8_t) result.second); + ESP_LOGI(kTag, "Waiting for power state (was %d %x)", result.first, + (uint8_t)result.second); vTaskDelay(pdMS_TO_TICKS(1)); } } @@ -77,14 +93,13 @@ bool AudioDac::WaitForPowerState(std::function } void AudioDac::WriteRegister(Register reg, uint8_t val) { - I2CTransaction transaction; - transaction - .start() - .write_addr(kPCM5122Address, I2C_MASTER_WRITE) + I2CTransaction transaction; + transaction.start() + .write_addr(kPcm5122Address, I2C_MASTER_WRITE) .write_ack(reg, val) .stop(); - // TODO: Retry once? - ESP_ERROR_CHECK(transaction.Execute()); + // TODO: Retry once? + ESP_ERROR_CHECK(transaction.Execute()); } -} // namespace gay_ipod +} // namespace gay_ipod diff --git a/main/dac.h b/main/dac.h index bbc8485d..c4e6b9a4 100644 --- a/main/dac.h +++ b/main/dac.h @@ -1,72 +1,73 @@ #pragma once -#include "esp_err.h" -#include "gpio-expander.h" #include + #include -namespace gay_ipod { +#include "esp_err.h" +#include "gpio-expander.h" +#include "result.hpp" - static const uint8_t kPCM5122Address = 0x4C; - static const uint8_t kPCM5122Timeout = 100 / portTICK_RATE_MS; +namespace gay_ipod { /** * Interface for a PCM5122PWR DAC, configured over I2C. */ class AudioDac { - public: - AudioDac(GpioExpander *gpio); - ~AudioDac(); + public: + enum Error { + FAILED_TO_BOOT, + FAILED_TO_CONFIGURE, + }; + static auto create(GpioExpander* expander) + -> cpp::result, Error>; - /** - * Performs initial configuration of the DAC and sets it to begin expecting - * I2S audio data. - */ - esp_err_t Start(); + AudioDac(GpioExpander* gpio); + ~AudioDac(); - /** - * Sets the volume on a scale from 0 (loudest) to 254 (quietest). A value of - * 255 engages the soft mute function. - */ - void WriteVolume(uint8_t volume); + /** + * Sets the volume on a scale from 0 (loudest) to 254 (quietest). A value of + * 255 engages the soft mute function. + */ + void WriteVolume(uint8_t volume); - enum PowerState { - POWERDOWN = 0b0, - WAIT_FOR_CP = 0b1, - CALIBRATION_1 = 0b10, - CALIBRATION_2 = 0b11, - RAMP_UP = 0b100, - RUN = 0b101, - SHORT = 0b110, - RAMP_DOWN = 0b111, - STANDBY = 0b1000, - }; + enum PowerState { + POWERDOWN = 0b0, + WAIT_FOR_CP = 0b1, + CALIBRATION_1 = 0b10, + CALIBRATION_2 = 0b11, + RAMP_UP = 0b100, + RUN = 0b101, + SHORT = 0b110, + RAMP_DOWN = 0b111, + STANDBY = 0b1000, + }; - /* Returns the current boot-up status and internal state of the DAC */ - std::pair ReadPowerState(); + /* Returns the current boot-up status and internal state of the DAC */ + std::pair ReadPowerState(); - // Not copyable or movable. - AudioDac(const AudioDac&) = delete; - AudioDac& operator=(const AudioDac&) = delete; + // Not copyable or movable. + AudioDac(const AudioDac&) = delete; + AudioDac& operator=(const AudioDac&) = delete; - private: - GpioExpander *gpio_; + private: + GpioExpander* gpio_; - /* - * Pools the power state for up to 10ms, waiting for the given predicate to - * be true. - */ - bool WaitForPowerState(std::function predicate); + /* + * Pools the power state for up to 10ms, waiting for the given predicate to + * be true. + */ + bool WaitForPowerState(std::function predicate); - enum Register { - PAGE_SELECT = 0, - DE_EMPHASIS = 7, - DIGITAL_VOLUME_L = 61, - DIGITAL_VOLUME_R = 62, - DSP_BOOT_POWER_STATE = 118, - }; + enum Register { + PAGE_SELECT = 0, + DE_EMPHASIS = 7, + DIGITAL_VOLUME_L = 61, + DIGITAL_VOLUME_R = 62, + DSP_BOOT_POWER_STATE = 118, + }; - void WriteRegister(Register reg, uint8_t val); + void WriteRegister(Register reg, uint8_t val); }; -} // namespace gay_ipod +} // namespace gay_ipod diff --git a/main/gay-ipod-fw.cpp b/main/gay-ipod-fw.cpp index dfae9c7b..b883d072 100644 --- a/main/gay-ipod-fw.cpp +++ b/main/gay-ipod-fw.cpp @@ -1,7 +1,12 @@ -#include -#include #include +#include + +#include +#include +#include "audio_common.h" +#include "audio_element.h" +#include "audio_pipeline.h" #include "battery.h" #include "dac.h" #include "driver/adc.h" @@ -14,19 +19,16 @@ #include "esp_adc_cal.h" #include "esp_intr_alloc.h" #include "esp_log.h" +#include "fatfs_stream.h" #include "gpio-expander.h" #include "hal/adc_types.h" #include "hal/gpio_types.h" #include "hal/i2s_types.h" #include "hal/spi_types.h" -#include "storage.h" - -#include "audio_element.h" -#include "audio_pipeline.h" -#include "audio_common.h" -#include "fatfs_stream.h" #include "i2s_stream.h" #include "mp3_decoder.h" +#include "playback.h" +#include "storage.h" #define I2C_SDA_IO (GPIO_NUM_2) #define I2C_SCL_IO (GPIO_NUM_4) @@ -48,16 +50,17 @@ static const char* TAG = "MAIN"; esp_err_t init_i2c(void) { i2c_port_t port = I2C_NUM_0; i2c_config_t config = { - .mode = I2C_MODE_MASTER, - .sda_io_num = I2C_SDA_IO, - .scl_io_num = I2C_SCL_IO, - .sda_pullup_en = GPIO_PULLUP_ENABLE, - .scl_pullup_en = GPIO_PULLUP_ENABLE, - .master = { - .clk_speed = I2C_CLOCK_HZ, - }, - // No requirements for the clock. - .clk_flags = 0, + .mode = I2C_MODE_MASTER, + .sda_io_num = I2C_SDA_IO, + .scl_io_num = I2C_SCL_IO, + .sda_pullup_en = GPIO_PULLUP_ENABLE, + .scl_pullup_en = GPIO_PULLUP_ENABLE, + .master = + { + .clk_speed = I2C_CLOCK_HZ, + }, + // No requirements for the clock. + .clk_flags = 0, }; ESP_ERROR_CHECK(i2c_param_config(port, &config)); @@ -70,22 +73,22 @@ esp_err_t init_i2c(void) { esp_err_t init_spi(void) { spi_bus_config_t config = { - .mosi_io_num = SPI_SDO_IO, - .miso_io_num = SPI_SDI_IO, - .sclk_io_num = SPI_SCLK_IO, - .quadwp_io_num = SPI_QUADWP_IO, - .quadhd_io_num = SPI_QUADHD_IO, - - // Unused - .data4_io_num = -1, - .data5_io_num = -1, - .data6_io_num = -1, - .data7_io_num = -1, - - // Use the DMA default size. - .max_transfer_sz = 0, - .flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_IOMUX_PINS, - .intr_flags = 0, + .mosi_io_num = SPI_SDO_IO, + .miso_io_num = SPI_SDI_IO, + .sclk_io_num = SPI_SCLK_IO, + .quadwp_io_num = SPI_QUADWP_IO, + .quadhd_io_num = SPI_QUADHD_IO, + + // Unused + .data4_io_num = -1, + .data5_io_num = -1, + .data6_io_num = -1, + .data7_io_num = -1, + + // Use the DMA default size. + .max_transfer_sz = 0, + .flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_IOMUX_PINS, + .intr_flags = 0, }; ESP_ERROR_CHECK(spi_bus_initialize(VSPI_HOST, &config, SPI_DMA_CH_AUTO)); @@ -93,8 +96,7 @@ esp_err_t init_spi(void) { return ESP_OK; } -extern "C" void app_main(void) -{ +extern "C" void app_main(void) { ESP_LOGI(TAG, "Initialising peripherals"); ESP_ERROR_CHECK(gpio_install_isr_service(ESP_INTR_FLAG_LOWMED)); @@ -105,140 +107,45 @@ extern "C" void app_main(void) gay_ipod::GpioExpander expander; // for debugging usb ic - //expander.set_sd_mux(gay_ipod::GpioExpander::USB); + // expander.set_sd_mux(gay_ipod::GpioExpander::USB); ESP_LOGI(TAG, "Init ADC"); ESP_ERROR_CHECK(gay_ipod::init_adc()); ESP_LOGI(TAG, "Init SD card"); - gay_ipod::SdStorage storage(&expander); - gay_ipod::SdStorage::Error err = storage.Acquire(); - if (err != gay_ipod::SdStorage::Error::OK) { - ESP_LOGE(TAG, "Failed to acquire storage!"); + auto storage_res = gay_ipod::SdStorage::create(&expander); + if (storage_res.has_error()) { + ESP_LOGE(TAG, "Failed: %d", storage_res.error()); return; } + std::unique_ptr storage = std::move(storage_res.value()); - ESP_LOGI(TAG, "Everything looks good! Waiting a mo for debugger."); - vTaskDelay(pdMS_TO_TICKS(1500)); - - ESP_LOGI(TAG, "Trying to init DAC"); - gay_ipod::AudioDac dac(&expander); - dac.Start(); - - vTaskDelay(pdMS_TO_TICKS(1000)); - - ESP_LOGI(TAG, "Looks okay? Let's play some music!"); - - i2s_port_t port = I2S_NUM_0; - - i2s_config_t i2s_config = { - // Weird enum usage in ESP IDF. - .mode = static_cast(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, - .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, - }; - - //ESP_ERROR_CHECK(i2s_driver_install(port, &i2s_config, 0, NULL)); - - audio_pipeline_handle_t pipeline; - audio_element_handle_t fatfs_stream_reader, i2s_stream_writer, audio_decoder; - - audio_pipeline_cfg_t pipeline_config = audio_pipeline_cfg_t(DEFAULT_AUDIO_PIPELINE_CONFIG()); - pipeline = audio_pipeline_init(&pipeline_config); - assert(pipeline != NULL); - ESP_LOGI(TAG, "Made pipeline okay."); - - 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); - assert(fatfs_stream_reader != NULL); - ESP_LOGI(TAG, "Made reader okay."); - - i2s_stream_cfg_t i2s_stream_config = i2s_stream_cfg_t{ - .type = AUDIO_STREAM_WRITER, - .i2s_config = i2s_config, - .i2s_port = port, - .use_alc = false, - .volume = 0, // Does nothing; use the dac - .out_rb_size = I2S_STREAM_RINGBUFFER_SIZE, - .task_stack = I2S_STREAM_TASK_STACK, - .task_core = I2S_STREAM_TASK_CORE, - .task_prio = I2S_STREAM_TASK_PRIO, - .stack_in_ext = false, - .multi_out_num = 0, - .uninstall_drv = true, - .need_expand = false, - .expand_src_bits = I2S_BITS_PER_SAMPLE_16BIT, - }; - // TODO fix hardcoded mclk :( - i2s_stream_writer = i2s_stream_init(&i2s_stream_config); - assert(i2s_stream_writer != NULL); - ESP_LOGI(TAG, "Made i2s stream okay."); - - - ESP_LOGI(TAG, "Init i2s pins"); - 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 - }; - - ESP_ERROR_CHECK(i2s_set_pin(port, &pin_config)); - - mp3_decoder_cfg_t decoder_config = mp3_decoder_cfg_t(DEFAULT_MP3_DECODER_CONFIG()); - audio_decoder = mp3_decoder_init(&decoder_config); - assert(audio_decoder != NULL); - ESP_LOGI(TAG, "Made mp3 decoder okay."); - - vTaskDelay(pdMS_TO_TICKS(500)); - ESP_LOGI(TAG, "Putting it all together"); - - audio_pipeline_register(pipeline, fatfs_stream_reader, "file"); - audio_pipeline_register(pipeline, audio_decoder, "dec"); - audio_pipeline_register(pipeline, i2s_stream_writer, "i2s"); - - const char *link_tag[3] = {"file", "dec", "i2s"}; - audio_pipeline_link(pipeline, &link_tag[0], 3); + ESP_LOGI(TAG, "Init DAC"); + auto dac_res = gay_ipod::AudioDac::create(&expander); + if (storage_res.has_error()) { + ESP_LOGE(TAG, "Failed: %d", dac_res.error()); + return; + } + std::unique_ptr dac = std::move(dac_res.value()); - ESP_LOGI(TAG, "Trying to play something??"); - audio_element_set_uri(fatfs_stream_reader, "/sdcard/test.mp3"); - audio_pipeline_run(pipeline); + ESP_LOGI(TAG, "Init Audio Pipeline"); + auto playback_res = gay_ipod::DacAudioPlayback::create(dac.get()); + if (playback_res.has_error()) { + ESP_LOGE(TAG, "Failed: %d", playback_res.error()); + return; + } + std::unique_ptr playback = + std::move(playback_res.value()); - vTaskDelay(pdMS_TO_TICKS(1000)); - ESP_LOGI(TAG, "It could be playing? idk"); + ESP_LOGI(TAG, "Everything looks good! Waiting a mo for debugger."); + vTaskDelay(pdMS_TO_TICKS(1500)); - gay_ipod::AudioDac::PowerState state = dac.ReadPowerState().second; - ESP_LOGI(TAG, "dac power state: %x", (uint8_t)state); + playback->Play("/sdcard/test.mp3"); + playback->set_volume(100); - vTaskDelay(pdMS_TO_TICKS(120000)); + playback->WaitForSongEnd(); ESP_LOGI(TAG, "Time to deinit."); - audio_pipeline_stop(pipeline); - audio_pipeline_wait_for_stop(pipeline); - audio_pipeline_terminate(pipeline); - - audio_pipeline_unregister(pipeline, fatfs_stream_reader); - audio_pipeline_unregister(pipeline, audio_decoder); - audio_pipeline_unregister(pipeline, i2s_stream_writer); - - audio_pipeline_deinit(pipeline); - audio_element_deinit(fatfs_stream_reader); - audio_element_deinit(i2s_stream_writer); - audio_element_deinit(audio_decoder); - - storage.Release(); ESP_LOGI(TAG, "Hooray!"); } diff --git a/main/gpio-expander.cpp b/main/gpio-expander.cpp index 389fb333..36cf37b0 100644 --- a/main/gpio-expander.cpp +++ b/main/gpio-expander.cpp @@ -1,9 +1,9 @@ #include "gpio-expander.h" -#include "i2c.h" - #include +#include "i2c.h" + namespace gay_ipod { GpioExpander::GpioExpander() { @@ -32,9 +32,9 @@ esp_err_t GpioExpander::Write() { I2CTransaction transaction; transaction.start() - .write_addr(kPca8575Address, I2C_MASTER_WRITE) - .write_ack(ports_ab.first, ports_ab.second) - .stop(); + .write_addr(kPca8575Address, I2C_MASTER_WRITE) + .write_ack(ports_ab.first, ports_ab.second) + .stop(); return transaction.Execute(); } @@ -44,10 +44,10 @@ esp_err_t GpioExpander::Read() { I2CTransaction transaction; transaction.start() - .write_addr(kPca8575Address, I2C_MASTER_READ) - .read(&input_a, I2C_MASTER_ACK) - .read(&input_b, I2C_MASTER_LAST_NACK) - .stop(); + .write_addr(kPca8575Address, I2C_MASTER_READ) + .read(&input_a, I2C_MASTER_ACK) + .read(&input_b, I2C_MASTER_LAST_NACK) + .stop(); esp_err_t ret = transaction.Execute(); inputs_ = pack(input_a, input_b); @@ -55,7 +55,7 @@ esp_err_t GpioExpander::Read() { } void GpioExpander::set_pin(ChipSelect cs, bool value) { - set_pin((Pin) cs, value); + set_pin((Pin)cs, value); } void GpioExpander::set_pin(Pin pin, bool value) { @@ -75,16 +75,12 @@ GpioExpander::SpiLock GpioExpander::AcquireSpiBus(ChipSelect cs) { } GpioExpander::SpiLock::SpiLock(GpioExpander& gpio, ChipSelect cs) - : lock_(gpio.cs_mutex_), gpio_(gpio), cs_(cs) { - gpio_.with([&](auto& gpio) { - gpio.set_pin(cs_, 0); - }); + : lock_(gpio.cs_mutex_), gpio_(gpio), cs_(cs) { + gpio_.with([&](auto& gpio) { gpio.set_pin(cs_, 0); }); } -GpioExpander::SpiLock::~SpiLock() { - gpio_.with([&](auto& gpio) { - gpio.set_pin(cs_, 1); - }); +GpioExpander::SpiLock::~SpiLock() { + gpio_.with([&](auto& gpio) { gpio.set_pin(cs_, 1); }); } -} // namespace gay_ipod +} // namespace gay_ipod diff --git a/main/gpio-expander.h b/main/gpio-expander.h index f3b5b804..4589f93f 100644 --- a/main/gpio-expander.h +++ b/main/gpio-expander.h @@ -1,16 +1,17 @@ #pragma once +#include + #include #include #include -#include #include #include +#include "driver/i2c.h" #include "esp_check.h" -#include "esp_log.h" #include "esp_err.h" -#include "driver/i2c.h" +#include "esp_log.h" #include "freertos/FreeRTOS.h" namespace gay_ipod { @@ -26,184 +27,183 @@ namespace gay_ipod { * should be done whilst holding `cs_lock` (preferably via the helper methods). */ class GpioExpander { - public: - GpioExpander(); - ~GpioExpander(); - - static const uint8_t kPca8575Address = 0x20; - static const uint8_t kPca8575Timeout = 100 / portTICK_RATE_MS; - - // Port A: - // 0 - audio power enable - // 1 - usb interface power enable - // 2 - display power enable - // 3 - sd card power enable - // 4 - charge power ok (active low) - // 5 - sd mux switch - // 6 - sd chip select - // 7 - display chip select - // All power switches low, chip selects high, active-low charge power high - static const uint8_t kPortADefault = 0b11010001; - - // Port B: - // 0 - 3.5mm jack detect (active low) - // 1 - dac soft mute switch - // 2 - GPIO - // 3 - GPIO - // 4 - GPIO - // 5 - GPIO - // 6 - GPIO - // 7 - GPIO - // DAC mute output low, everything else is active-low inputs. - static const uint8_t kPortBDefault = 0b11111111; - - /* - * Convenience mehod for packing the port a and b bytes into a single 16 bit - * value. - */ - static uint16_t pack(uint8_t a, uint8_t b) { - return ((uint16_t) b) << 8 | a; - } - - /* - * Convenience mehod for unpacking the result of `pack` back into two single - * byte port datas. - */ - static std::pair unpack(uint16_t ba) { - return std::pair((uint8_t) ba, (uint8_t) (ba >> 8)); - } - - /* - * Convenience function for running some arbitrary pin writing code, then - * flushing a `Write()` to the expander. Example usage: - * - * ``` - * gpio_.with([&](auto& gpio) { - * gpio.set_pin(AUDIO_POWER_ENABLE, true); - * }); - * ``` - */ - void with(std::function f); - - /** - * Sets the ports on the GPIO expander to the values currently represented - * in `ports`. - */ - esp_err_t Write(void); - - /** - * Reads from the GPIO expander, populating `inputs` with the most recent - * values. - */ - esp_err_t Read(void); - - /* Maps each pin of the expander to its number in a `pack`ed uint16. */ - enum Pin { - // Port A - AUDIO_POWER_ENABLE = 0, - USB_INTERFACE_POWER_ENABLE = 1, - DISPLAY_POWER_ENABLE = 2, - SD_CARD_POWER_ENABLE = 3, - CHARGE_POWER_OK = 4, // Active-low input - SD_MUX_SWITCH = 5, - SD_CHIP_SELECT = 6, - DISPLAY_CHIP_SELECT = 7, - - // Port B - PHONE_DETECT = 8, // Active-high input - DAC_MUTE = 9, - GPIO_1 = 10, - GPIO_2 = 11, - GPIO_3 = 12, - GPIO_4 = 13, - GPIO_5 = 14, - GPIO_6 = 15, - }; - - /* Pins whose access should be guarded by `cs_lock`. */ - enum ChipSelect { - SD_CARD = SD_CHIP_SELECT, - DISPLAY = DISPLAY_CHIP_SELECT, - }; - - /* Nicer value names for use with the SD_MUX_SWITCH pin. */ - enum SdController { - SD_MUX_ESP = 0, - SD_MUX_USB = 1, - }; - - /** - * Returns the current driven status of each of the ports. The first byte is - * port a, and the second byte is port b. - */ - std::atomic& ports() { return ports_; } - - /* - * Sets a single specific pin to the given value. `true` corresponds to - * HIGH, and `false` corresponds to LOW. - * - * Calls to this method will be buffered in memory until a call to `Write()` - * is made. - */ - void set_pin(Pin pin, bool value); - void set_pin(ChipSelect cs, bool value); - - /** - * Returns the input status of each of the ports. The first byte is port a, - * and the second byte is port b. - */ - const std::atomic& inputs() const { return inputs_; } - - /* Returns the most recently cached value of the given pin. Only valid for - * pins used as inputs; to check what value we're driving a pin, use - * `ports()`. - */ - bool get_input(Pin pin) const; - - /* Returns the mutex that must be held whilst pulling a CS pin low. */ - std::mutex& cs_mutex() { return cs_mutex_; } - - /* - * Helper class containing an active `cs_mutex` lock. When an instance of - * this class is destroyed (usually by falling out of scope), the associated - * CS pin will be driven high before the lock is released. - */ - class SpiLock { - public: - SpiLock(GpioExpander &gpio, ChipSelect cs); - ~SpiLock(); - - SpiLock(const SpiLock&) = delete; - private: - std::scoped_lock lock_; - GpioExpander &gpio_; - ChipSelect cs_; - }; - - /* - * Pulls the given CS pin low to signal that we are about to communicate - * with a particular device, after acquiring a lock on `cs_mutex`. The - * recommended way to safely interact with devices on the SPI bus is to have - * a self-contained block like so: - * - * ``` - * { - * auto lock = AcquireSpiBus(WHATEVER); - * // Do some cool things here. - * } - * ``` - */ - SpiLock AcquireSpiBus(ChipSelect cs); - - // Not copyable or movable. There should usually only ever be once instance - // of this class, and that instance will likely have a static lifetime. - GpioExpander(const GpioExpander&) = delete; - GpioExpander& operator=(const GpioExpander&) = delete; - - private: - std::mutex cs_mutex_; - std::atomic ports_; - std::atomic inputs_; + public: + GpioExpander(); + ~GpioExpander(); + + static const uint8_t kPca8575Address = 0x20; + static const uint8_t kPca8575Timeout = 100 / portTICK_RATE_MS; + + // Port A: + // 0 - audio power enable + // 1 - usb interface power enable + // 2 - display power enable + // 3 - sd card power enable + // 4 - charge power ok (active low) + // 5 - sd mux switch + // 6 - sd chip select + // 7 - display chip select + // All power switches low, chip selects high, active-low charge power high + static const uint8_t kPortADefault = 0b11010001; + + // Port B: + // 0 - 3.5mm jack detect (active low) + // 1 - dac soft mute switch + // 2 - GPIO + // 3 - GPIO + // 4 - GPIO + // 5 - GPIO + // 6 - GPIO + // 7 - GPIO + // DAC mute output low, everything else is active-low inputs. + static const uint8_t kPortBDefault = 0b11111111; + + /* + * Convenience mehod for packing the port a and b bytes into a single 16 bit + * value. + */ + static uint16_t pack(uint8_t a, uint8_t b) { return ((uint16_t)b) << 8 | a; } + + /* + * Convenience mehod for unpacking the result of `pack` back into two single + * byte port datas. + */ + static std::pair unpack(uint16_t ba) { + return std::pair((uint8_t)ba, (uint8_t)(ba >> 8)); + } + + /* + * Convenience function for running some arbitrary pin writing code, then + * flushing a `Write()` to the expander. Example usage: + * + * ``` + * gpio_.with([&](auto& gpio) { + * gpio.set_pin(AUDIO_POWER_ENABLE, true); + * }); + * ``` + */ + void with(std::function f); + + /** + * Sets the ports on the GPIO expander to the values currently represented + * in `ports`. + */ + esp_err_t Write(void); + + /** + * Reads from the GPIO expander, populating `inputs` with the most recent + * values. + */ + esp_err_t Read(void); + + /* Maps each pin of the expander to its number in a `pack`ed uint16. */ + enum Pin { + // Port A + AUDIO_POWER_ENABLE = 0, + USB_INTERFACE_POWER_ENABLE = 1, + DISPLAY_POWER_ENABLE = 2, + SD_CARD_POWER_ENABLE = 3, + CHARGE_POWER_OK = 4, // Active-low input + SD_MUX_SWITCH = 5, + SD_CHIP_SELECT = 6, + DISPLAY_CHIP_SELECT = 7, + + // Port B + PHONE_DETECT = 8, // Active-high input + DAC_MUTE = 9, + GPIO_1 = 10, + GPIO_2 = 11, + GPIO_3 = 12, + GPIO_4 = 13, + GPIO_5 = 14, + GPIO_6 = 15, + }; + + /* Pins whose access should be guarded by `cs_lock`. */ + enum ChipSelect { + SD_CARD = SD_CHIP_SELECT, + DISPLAY = DISPLAY_CHIP_SELECT, + }; + + /* Nicer value names for use with the SD_MUX_SWITCH pin. */ + enum SdController { + SD_MUX_ESP = 0, + SD_MUX_USB = 1, + }; + + /** + * Returns the current driven status of each of the ports. The first byte is + * port a, and the second byte is port b. + */ + std::atomic& ports() { return ports_; } + + /* + * Sets a single specific pin to the given value. `true` corresponds to + * HIGH, and `false` corresponds to LOW. + * + * Calls to this method will be buffered in memory until a call to `Write()` + * is made. + */ + void set_pin(Pin pin, bool value); + void set_pin(ChipSelect cs, bool value); + + /** + * Returns the input status of each of the ports. The first byte is port a, + * and the second byte is port b. + */ + const std::atomic& inputs() const { return inputs_; } + + /* Returns the most recently cached value of the given pin. Only valid for + * pins used as inputs; to check what value we're driving a pin, use + * `ports()`. + */ + bool get_input(Pin pin) const; + + /* Returns the mutex that must be held whilst pulling a CS pin low. */ + std::mutex& cs_mutex() { return cs_mutex_; } + + /* + * Helper class containing an active `cs_mutex` lock. When an instance of + * this class is destroyed (usually by falling out of scope), the associated + * CS pin will be driven high before the lock is released. + */ + class SpiLock { + public: + SpiLock(GpioExpander& gpio, ChipSelect cs); + ~SpiLock(); + + SpiLock(const SpiLock&) = delete; + + private: + std::scoped_lock lock_; + GpioExpander& gpio_; + ChipSelect cs_; + }; + + /* + * Pulls the given CS pin low to signal that we are about to communicate + * with a particular device, after acquiring a lock on `cs_mutex`. The + * recommended way to safely interact with devices on the SPI bus is to have + * a self-contained block like so: + * + * ``` + * { + * auto lock = AcquireSpiBus(WHATEVER); + * // Do some cool things here. + * } + * ``` + */ + SpiLock AcquireSpiBus(ChipSelect cs); + + // Not copyable or movable. There should usually only ever be once instance + // of this class, and that instance will likely have a static lifetime. + GpioExpander(const GpioExpander&) = delete; + GpioExpander& operator=(const GpioExpander&) = delete; + + private: + std::mutex cs_mutex_; + std::atomic ports_; + std::atomic inputs_; }; -} // namespace gay_ipod +} // namespace gay_ipod diff --git a/main/i2c.cpp b/main/i2c.cpp index 2cb8420f..ae313b33 100644 --- a/main/i2c.cpp +++ b/main/i2c.cpp @@ -1,4 +1,5 @@ #include "i2c.h" + #include "assert.h" namespace gay_ipod { @@ -36,9 +37,9 @@ I2CTransaction& I2CTransaction::write_ack(uint8_t data) { return *this; } -I2CTransaction& I2CTransaction::read(uint8_t *dest, i2c_ack_type_t ack) { +I2CTransaction& I2CTransaction::read(uint8_t* dest, i2c_ack_type_t ack) { ESP_ERROR_CHECK(i2c_master_read_byte(handle_, dest, ack)); return *this; } -} // namespace gay_ipod +} // namespace gay_ipod diff --git a/main/i2c.h b/main/i2c.h index 20b6491b..6b2de577 100644 --- a/main/i2c.h +++ b/main/i2c.h @@ -1,8 +1,9 @@ #pragma once +#include + #include "driver/i2c.h" #include "hal/i2c_types.h" -#include namespace gay_ipod { @@ -15,66 +16,68 @@ namespace gay_ipod { * typically represent invalid arguments or OOMs. */ class I2CTransaction { - public: - static const uint8_t kI2CTimeout = 100 / portTICK_RATE_MS; + public: + static const uint8_t kI2CTimeout = 100 / portTICK_RATE_MS; + + I2CTransaction(); + ~I2CTransaction(); - I2CTransaction(); - ~I2CTransaction(); + /* + * Executes all enqueued commands, returning the result code. Possible error + * codes, per the ESP-IDF docs: + * + * ESP_OK Success + * ESP_ERR_INVALID_ARG Parameter error + * ESP_FAIL Sending command error, slave doesn’t ACK the transfer. + * ESP_ERR_INVALID_STATE I2C driver not installed or not in master mode. + * ESP_ERR_TIMEOUT Operation timeout because the bus is busy. + */ + esp_err_t Execute(); - /* - * Executes all enqueued commands, returning the result code. Possible error - * codes, per the ESP-IDF docs: - * - * ESP_OK Success - * ESP_ERR_INVALID_ARG Parameter error - * ESP_FAIL Sending command error, slave doesn’t ACK the transfer. - * ESP_ERR_INVALID_STATE I2C driver not installed or not in master mode. - * ESP_ERR_TIMEOUT Operation timeout because the bus is busy. - */ - esp_err_t Execute(); + /* + * Enqueues a start condition. May also be used for repeated start + * conditions. + */ + I2CTransaction& start(); + /* Enqueues a stop condition. */ + I2CTransaction& stop(); - /* - * Enqueues a start condition. May also be used for repeated start conditions. - */ - I2CTransaction& start(); - /* Enqueues a stop condition. */ - I2CTransaction& stop(); + /* + * Enqueues writing the given 7 bit address, followed by one bit indicating + * whether this is a read or write request. + * + * This command will expect an ACK before continuing. + */ + I2CTransaction& write_addr(uint8_t addr, uint8_t op); - /* - * Enqueues writing the given 7 bit address, followed by one bit indicating - * whether this is a read or write request. - * - * This command will expect an ACK before continuing. - */ - I2CTransaction& write_addr(uint8_t addr, uint8_t op); + /* + * Enqueues one or more bytes to be written. The transaction will wait for + * an ACK to be returned before writing the next byte. + */ + I2CTransaction& write_ack(uint8_t data); + template + I2CTransaction& write_ack(uint8_t data, More... more) { + write_ack(data); + write_ack(more...); + return *this; + } - /* - * Enqueues one or more bytes to be written. The transaction will wait for - * an ACK to be returned before writing the next byte. - */ - I2CTransaction& write_ack(uint8_t data); - template - I2CTransaction& write_ack(uint8_t data, More... more) { - write_ack(data); - write_ack(more...); - return *this; - } + /* + * Enqueues a read of one byte into the given uint8. Responds with the given + * ACK/NACK type. + */ + I2CTransaction& read(uint8_t* dest, i2c_ack_type_t ack); - /* - * Enqueues a read of one byte into the given uint8. Responds with the given - * ACK/NACK type. - */ - I2CTransaction& read(uint8_t *dest, i2c_ack_type_t ack); + /* Returns the underlying command buffer. */ + i2c_cmd_handle_t handle() { return handle_; } - /* Returns the underlying command buffer. */ - i2c_cmd_handle_t handle() { return handle_; } + // Cannot be moved or copied, since doing so is probably an error. Pass a + // reference instead. + I2CTransaction(const I2CTransaction&) = delete; + I2CTransaction& operator=(const I2CTransaction&) = delete; - // Cannot be moved or copied, since doing so is probably an error. Pass a - // reference instead. - I2CTransaction(const I2CTransaction&) = delete; - I2CTransaction& operator=(const I2CTransaction&) = delete; - private: - i2c_cmd_handle_t handle_; + private: + i2c_cmd_handle_t handle_; }; -} // namespace gay_ipod +} // namespace gay_ipod diff --git a/main/playback.cpp b/main/playback.cpp new file mode 100644 index 00000000..7b58f099 --- /dev/null +++ b/main/playback.cpp @@ -0,0 +1,224 @@ +#include "playback.h" + +#include + +#include "audio_element.h" +#include "audio_event_iface.h" +#include "audio_pipeline.h" +#include "dac.h" +#include "driver/i2s.h" +#include "esp_err.h" +#include "mp3_decoder.h" + +static const char* kTag = "PLAYBACK"; +static const i2s_port_t kI2SPort = I2S_NUM_0; + +namespace gay_ipod { + +// Static functions for interrop with the ESP IDF API, which requires a +// function pointer. +namespace callback { +static DacAudioPlayback* instance = nullptr; + +// Fits the required function pointer signature, but just delegates to the +// wrapper function. Does that make this the wrapper? Who knows. +__attribute__((unused)) // (gcc incorrectly thinks this is unused) +static esp_err_t +on_event(audio_event_iface_msg_t* event, void* data) { + if (instance == nullptr) { + ESP_LOGW(kTag, "uncaught ADF event, type %x", event->cmd); + return ESP_OK; + } + return instance->HandleEvent(event, data); +} +} // namespace callback + +auto DacAudioPlayback::create(AudioDac* dac) + -> cpp::result, Error> { + assert(callback::instance == nullptr); + + // Ensure we're soft-muted before initialising, in order to reduce protential + // clicks and pops. + dac->WriteVolume(255); + + audio_pipeline_handle_t pipeline; + audio_element_handle_t fatfs_stream_reader; + audio_element_handle_t i2s_stream_writer; + 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); + } + + 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::PIPELINE_INIT); + } + + i2s_stream_cfg_t i2s_stream_config = i2s_stream_cfg_t{ + .type = AUDIO_STREAM_WRITER, + .i2s_config = + { + // static_cast bc esp-adf uses enums incorrectly + .mode = static_cast(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, + .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, + }, + .i2s_port = kI2SPort, + .use_alc = false, + .volume = 0, // Does nothing; use AudioDac to change this. + .out_rb_size = I2S_STREAM_RINGBUFFER_SIZE, + .task_stack = I2S_STREAM_TASK_STACK, + .task_core = I2S_STREAM_TASK_CORE, + .task_prio = I2S_STREAM_TASK_PRIO, + .stack_in_ext = false, + .multi_out_num = 0, + .uninstall_drv = true, + .need_expand = false, + .expand_src_bits = I2S_BITS_PER_SAMPLE_16BIT, + }; + i2s_stream_writer = i2s_stream_init(&i2s_stream_config); + if (i2s_stream_writer == NULL) { + return cpp::fail(Error::PIPELINE_INIT); + } + + // NOTE: i2s_stream_init does some additional setup that hardcodes MCK as + // GPIO0. This happens to work fine for us, but be careful if changing. + 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::PIPELINE_INIT); + } + + // TODO: Create encoders dynamically when we need them. + audio_element_handle_t mp3_decoder; + mp3_decoder_cfg_t mp3_config = + mp3_decoder_cfg_t(DEFAULT_MP3_DECODER_CONFIG()); + mp3_decoder = mp3_decoder_init(&mp3_config); + assert(mp3_decoder != NULL); + + audio_event_iface_cfg_t event_config = AUDIO_EVENT_IFACE_DEFAULT_CFG(); + event_config.on_cmd = &callback::on_event; + event_interface = audio_event_iface_init(&event_config); + audio_pipeline_set_listener(pipeline, event_interface); + + // TODO: most of this is likely post-init, since it involves a decoder. + // All the elements of our pipeline have been initialised. Now switch them + // together. + audio_pipeline_register(pipeline, fatfs_stream_reader, "file"); + audio_pipeline_register(pipeline, mp3_decoder, "dec"); + audio_pipeline_register(pipeline, i2s_stream_writer, "i2s"); + + const char* link_tag[3] = {"file", "dec", "i2s"}; + audio_pipeline_link(pipeline, &link_tag[0], 3); + + return std::make_unique(dac, pipeline, fatfs_stream_reader, + i2s_stream_writer, event_interface, + mp3_decoder); +} + +DacAudioPlayback::DacAudioPlayback(AudioDac* dac, + audio_pipeline_handle_t pipeline, + audio_element_handle_t fatfs_stream_reader, + audio_element_handle_t i2s_stream_writer, + audio_event_iface_handle_t event_interface, + audio_element_handle_t mp3_decoder) + : dac_(dac), + pipeline_(pipeline), + fatfs_stream_reader_(fatfs_stream_reader), + i2s_stream_writer_(i2s_stream_writer), + event_interface_(event_interface), + mp3_decoder_(mp3_decoder) { + callback::instance = this; +} + +DacAudioPlayback::~DacAudioPlayback() { + dac_->WriteVolume(255); + + audio_pipeline_remove_listener(pipeline_); + callback::instance = nullptr; + + audio_pipeline_stop(pipeline_); + audio_pipeline_wait_for_stop(pipeline_); + audio_pipeline_terminate(pipeline_); + + audio_pipeline_unregister(pipeline_, fatfs_stream_reader_); + audio_pipeline_unregister(pipeline_, mp3_decoder_); + audio_pipeline_unregister(pipeline_, i2s_stream_writer_); + + audio_event_iface_destroy(event_interface_); + + audio_pipeline_deinit(pipeline_); + audio_element_deinit(fatfs_stream_reader_); + audio_element_deinit(i2s_stream_writer_); + audio_element_deinit(mp3_decoder_); +} + +void DacAudioPlayback::Play(const std::string& filename) { + dac_->WriteVolume(255); + // TODO: handle reconfiguring the pipeline if needed. + audio_element_set_uri(fatfs_stream_reader_, filename.c_str()); + audio_pipeline_run(pipeline_); + dac_->WriteVolume(volume_); +} + +void DacAudioPlayback::Resume() { + // TODO. +} +void DacAudioPlayback::Pause() { + // TODO. +} + +void DacAudioPlayback::WaitForSongEnd() { + while (1) { + audio_element_state_t state = audio_element_get_state(i2s_stream_writer_); + if (state == AEL_STATE_FINISHED) { + return; + } + + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} + +/* for gapless */ +void DacAudioPlayback::set_next_file(const std::string& filename) { + next_filename_ = filename; +} + +void DacAudioPlayback::set_volume(uint8_t volume) { + volume_ = volume; + // TODO: don't write immediately if we're muting to change track or similar. + dac_->WriteVolume(volume); +} + +auto DacAudioPlayback::volume() -> uint8_t { + return volume_; +} + +auto DacAudioPlayback::HandleEvent(audio_event_iface_msg_t* event, void* data) + -> esp_err_t { + ESP_LOGI(kTag, "got event!"); + return ESP_OK; +} + +} // namespace gay_ipod diff --git a/main/playback.h b/main/playback.h new file mode 100644 index 00000000..bd65579f --- /dev/null +++ b/main/playback.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include + +#include "audio_common.h" +#include "audio_element.h" +#include "audio_event_iface.h" +#include "audio_pipeline.h" +#include "dac.h" +#include "esp_err.h" +#include "fatfs_stream.h" +#include "i2s_stream.h" +#include "result.hpp" +#include "storage.h" + +namespace gay_ipod { + +class DacAudioPlayback { + public: + enum Error { PIPELINE_INIT }; + static auto create(AudioDac* dac) + -> cpp::result, Error>; + + DacAudioPlayback(AudioDac* dac, + audio_pipeline_handle_t pipeline, + audio_element_handle_t fatfs_stream_reader, + audio_element_handle_t i2s_stream_writer, + audio_event_iface_handle_t event_interface, + audio_element_handle_t mp3_decoder); + ~DacAudioPlayback(); + + void Play(const std::string& filename); + void Resume(); + void Pause(); + + // For debug :) + void WaitForSongEnd(); + + /* for gapless */ + void set_next_file(const std::string& filename); + + void set_volume(uint8_t volume); + auto volume() -> uint8_t; + + auto HandleEvent(audio_event_iface_msg_t* event, void* data) -> esp_err_t; + + // Not copyable or movable. + DacAudioPlayback(const DacAudioPlayback&) = delete; + DacAudioPlayback& operator=(const DacAudioPlayback&) = delete; + + private: + AudioDac* dac_; + std::mutex playback_lock_; + + std::string next_filename_; + uint8_t volume_; + + audio_pipeline_handle_t pipeline_; + audio_element_handle_t fatfs_stream_reader_; + audio_element_handle_t i2s_stream_writer_; + audio_event_iface_handle_t event_interface_; + + audio_element_handle_t mp3_decoder_; +}; + +} // namespace gay_ipod diff --git a/main/storage.cpp b/main/storage.cpp index d2f88894..9e34ade0 100644 --- a/main/storage.cpp +++ b/main/storage.cpp @@ -1,5 +1,6 @@ #include "storage.h" +#include #include #include "diskio_impl.h" @@ -15,77 +16,130 @@ #include "hal/spi_types.h" #include "sdmmc_cmd.h" +static const char* kTag = "SDSTORAGE"; +static const uint8_t kMaxOpenFiles = 8; + namespace gay_ipod { -static const char* TAG = "SDSTORAGE"; +const char* kStoragePath = "/sdcard"; -SdStorage::SdStorage(GpioExpander *gpio) { - this->gpio_ = gpio; -} +// Static functions for interrop with the ESP IDF API, which requires a +// function pointer. +namespace callback { +static std::atomic instance = nullptr; +static std::atomic + bootstrap = nullptr; -SdStorage::~SdStorage() {} +static esp_err_t do_transaction(sdspi_dev_handle_t handle, + sdmmc_command_t* cmdinfo) { + auto bootstrap_fn = bootstrap.load(); + if (bootstrap_fn != nullptr) { + return bootstrap_fn(handle, cmdinfo); + } + auto instance_unwrapped = instance.load(); + if (instance_unwrapped == nullptr) { + ESP_LOGW(kTag, "uncaught sdspi transaction"); + return ESP_OK; + } + // TODO: what if a transaction comes in right now? + return instance_unwrapped->HandleTransaction(handle, cmdinfo); +} +} // namespace callback -SdStorage::Error SdStorage::Acquire(void) { +auto SdStorage::create(GpioExpander* gpio) + -> cpp::result, Error> { // Acquiring the bus will also flush the mux switch change. - gpio_->set_pin(GpioExpander::SD_MUX_SWITCH, GpioExpander::SD_MUX_ESP); + gpio->set_pin(GpioExpander::SD_MUX_SWITCH, GpioExpander::SD_MUX_ESP); + + sdspi_dev_handle_t handle; + sdmmc_host_t host; + sdmmc_card_t card; + FATFS* fs = nullptr; // Now we can init the driver and set up the SD card into SPI mode. sdspi_host_init(); sdspi_device_config_t config = { - .host_id = VSPI_HOST, - // CS handled manually bc it's on the GPIO expander - .gpio_cs = GPIO_NUM_2, - .gpio_cd = SDSPI_SLOT_NO_CD, - .gpio_wp = SDSPI_SLOT_NO_WP, - .gpio_int = GPIO_NUM_NC, + .host_id = VSPI_HOST, + // CS handled manually bc it's on the GPIO expander + .gpio_cs = GPIO_NUM_2, + .gpio_cd = SDSPI_SLOT_NO_CD, + .gpio_wp = SDSPI_SLOT_NO_WP, + .gpio_int = GPIO_NUM_NC, }; - ESP_ERROR_CHECK(sdspi_host_init_device(&config, &handle_)); + if (esp_err_t err = sdspi_host_init_device(&config, &handle) != ESP_OK) { + ESP_LOGE(kTag, "Failed to init, err %d", err); + return cpp::fail(Error::FAILED_TO_INIT); + } - host_ = sdmmc_host_t SDSPI_HOST_DEFAULT(); + host = sdmmc_host_t SDSPI_HOST_DEFAULT(); // We manage the CS pin ourselves via the GPIO expander. To do this safely in // a multithreaded environment, we wrap the ESP IDF do_transaction function // with our own that acquires the CS mutex for the duration of the SPI // transaction. - auto src = host_.do_transaction; - sdspi::do_transaction_wrapper = [=](sdspi_dev_handle_t handle, sdmmc_command_t *cmd) -> esp_err_t { - auto lock = gpio_->AcquireSpiBus(GpioExpander::SD_CARD); - return src(handle, cmd); - }; - host_.do_transaction = &sdspi::do_transaction; + auto do_transaction = host.do_transaction; + host.do_transaction = &callback::do_transaction; + host.slot = handle; + callback::bootstrap = do_transaction; - host_.slot = handle_; + auto lock = gpio->AcquireSpiBus(GpioExpander::SD_CARD); // Will return ESP_ERR_INVALID_RESPONSE if there is no card - esp_err_t err = sdmmc_card_init(&host_, &card_); + esp_err_t err = sdmmc_card_init(&host, &card); if (err != ESP_OK) { - ESP_LOGW(TAG, "Failed to read, err: %d", err); - return Error::FAILED_TO_READ; + ESP_LOGW(kTag, "Failed to read, err: %d", err); + return cpp::fail(Error::FAILED_TO_READ); } - ESP_ERROR_CHECK(esp_vfs_fat_register(kStoragePath, "", kMaxOpenFiles, &fs_)); - ff_diskio_register_sdmmc(fs_->pdrv, &card_); + ESP_ERROR_CHECK(esp_vfs_fat_register(kStoragePath, "", kMaxOpenFiles, &fs)); + ff_diskio_register_sdmmc(fs->pdrv, &card); // Mount right now, not on first operation. - FRESULT ferr = f_mount(fs_, "", 1); + FRESULT ferr = f_mount(fs, "", 1); if (ferr != FR_OK) { - ESP_LOGW(TAG, "Failed to mount, err: %d", ferr); - return Error::FAILED_TO_MOUNT; + ESP_LOGW(kTag, "Failed to mount, err: %d", ferr); + return cpp::fail(Error::FAILED_TO_MOUNT); } - return Error::OK; + return std::make_unique(gpio, do_transaction, handle, host, card, + fs); +} + +SdStorage::SdStorage(GpioExpander* gpio, + esp_err_t (*do_transaction)(sdspi_dev_handle_t, + sdmmc_command_t*), + sdspi_dev_handle_t handle, + sdmmc_host_t host, + sdmmc_card_t card, + FATFS* fs) + : gpio_(gpio), + do_transaction_(do_transaction), + handle_(handle), + host_(host), + card_(card), + fs_(fs) { + callback::instance = this; + callback::bootstrap = nullptr; } -void SdStorage::Release(void) { +SdStorage::~SdStorage() { // Unmount and unregister the filesystem f_unmount(""); ff_diskio_register(fs_->pdrv, NULL); esp_vfs_fat_unregister_path(kStoragePath); fs_ = nullptr; + callback::instance = nullptr; + // Uninstall the SPI driver sdspi_host_remove_device(this->handle_); sdspi_host_deinit(); } -} // namespace gay_ipod +auto SdStorage::HandleTransaction(sdspi_dev_handle_t handle, + sdmmc_command_t* cmdinfo) -> esp_err_t { + auto lock = gpio_->AcquireSpiBus(GpioExpander::SD_CARD); + return do_transaction_(handle, cmdinfo); +} + +} // namespace gay_ipod diff --git a/main/storage.h b/main/storage.h index c93a426b..64eab5dd 100644 --- a/main/storage.h +++ b/main/storage.h @@ -1,73 +1,59 @@ #pragma once +#include + #include "driver/sdmmc_types.h" #include "driver/sdspi_host.h" #include "esp_err.h" #include "esp_vfs_fat.h" #include "gpio-expander.h" +#include "result.hpp" namespace gay_ipod { -// Static functions for interrop with the ESP IDF API, which requires a function -// pointer. -namespace sdspi { - // Holds a lambda created by SdStorage. - static std::function do_transaction_wrapper; - - // Fits the required function pointer signature, but just delegates to the - // wrapper function. Does that make this the wrapper? Who knows. - __attribute__ ((unused)) // (gcc incorrectly thinks this is unused) - static esp_err_t do_transaction(sdspi_dev_handle_t handle, sdmmc_command_t *cmdinfo) { - return do_transaction_wrapper(handle, cmdinfo); - } -} // namespace sdspi - -static const char *kStoragePath = "/sdcard"; -static const uint8_t kMaxOpenFiles = 8; +extern const char* kStoragePath; class SdStorage { - public: - SdStorage(GpioExpander *gpio); - ~SdStorage(); - - enum Error { - OK, - /** We couldn't interact with the SD card at all. Is it missing? */ - FAILED_TO_READ, - /** We couldn't mount the SD card. Is it formatted? */ - FAILED_TO_MOUNT, - }; - - // FIXME: these methods should also handling powering the SD card up and - // down once we have that capability. - - /** - * Initialises the SDSPI driver and mounts the SD card for reading and - * writing. This must be called before any interactions with the underlying - * storage. - */ - Error Acquire(void); - - /** - * Unmounts the SD card and frees memory associated with the SDSPI driver. - */ - void Release(void); - - // Not copyable or movable. - // TODO: maybe this could be movable? - SdStorage(const SdStorage&) = delete; - SdStorage& operator=(const SdStorage&) = delete; - - private: - GpioExpander *gpio_; - - // SPI and SD driver info - sdspi_dev_handle_t handle_; - sdmmc_host_t host_; - sdmmc_card_t card_; - - // Filesystem info - FATFS *fs_ = nullptr; + public: + enum Error { + FAILED_TO_INIT, + /** We couldn't interact with the SD card at all. Is it missing? */ + FAILED_TO_READ, + /** We couldn't mount the SD card. Is it formatted? */ + FAILED_TO_MOUNT, + }; + + static auto create(GpioExpander* gpio) + -> cpp::result, Error>; + + SdStorage(GpioExpander* gpio, + esp_err_t (*do_transaction)(sdspi_dev_handle_t, sdmmc_command_t*), + sdspi_dev_handle_t handle_, + sdmmc_host_t host_, + sdmmc_card_t card_, + FATFS* fs_); + ~SdStorage(); + + auto HandleTransaction(sdspi_dev_handle_t handle, sdmmc_command_t* cmdinfo) + -> esp_err_t; + + // Not copyable or movable. + // TODO: maybe this could be movable? + SdStorage(const SdStorage&) = delete; + SdStorage& operator=(const SdStorage&) = delete; + + private: + GpioExpander* gpio_; + + esp_err_t (*do_transaction_)(sdspi_dev_handle_t, sdmmc_command_t*) = nullptr; + + // SPI and SD driver info + sdspi_dev_handle_t handle_; + sdmmc_host_t host_; + sdmmc_card_t card_; + + // Filesystem info + FATFS* fs_ = nullptr; }; -} // namespace gay_ipod +} // namespace gay_ipod