WIP use result<> and RAII
This commit is contained in:
+15
-5
@@ -1,5 +1,15 @@
|
|||||||
idf_component_register(
|
idf_component_register(SRCS
|
||||||
SRCS "gay-ipod-fw.cpp" "dac.cpp" "gpio-expander.cpp" "battery.cpp" "storage.cpp" "i2c.cpp"
|
"gay-ipod-fw.cpp"
|
||||||
INCLUDE_DIRS "."
|
"dac.cpp"
|
||||||
REQUIRES "esp_adc_cal" "fatfs" "audio_pipeline" "audio_stream" "result"
|
"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)
|
||||||
|
|||||||
+3
-3
@@ -11,8 +11,8 @@ static esp_adc_cal_characteristics_t calibration;
|
|||||||
esp_err_t init_adc(void) {
|
esp_err_t init_adc(void) {
|
||||||
// Calibration should already be fused into the chip from the factory, so
|
// Calibration should already be fused into the chip from the factory, so
|
||||||
// we should only need to read it back out again.
|
// we should only need to read it back out again.
|
||||||
esp_adc_cal_characterize(
|
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 0,
|
||||||
ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 0, &calibration);
|
&calibration);
|
||||||
|
|
||||||
// Max battery voltage should be a little over 2V due to our divider, so
|
// 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.
|
// 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);
|
return esp_adc_cal_raw_to_voltage(raw, &calibration);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace gay_ipod
|
} // namespace gay_ipod
|
||||||
|
|||||||
+1
-1
@@ -13,4 +13,4 @@ esp_err_t init_adc(void);
|
|||||||
*/
|
*/
|
||||||
uint32_t read_battery_voltage(void);
|
uint32_t read_battery_voltage(void);
|
||||||
|
|
||||||
} // namespace gay_ipod
|
} // namespace gay_ipod
|
||||||
|
|||||||
+58
-43
@@ -1,40 +1,55 @@
|
|||||||
#include "dac.h"
|
#include "dac.h"
|
||||||
|
|
||||||
#include "esp_err.h"
|
#include <cstdint>
|
||||||
#include "i2c.h"
|
|
||||||
#include "esp_log.h"
|
|
||||||
#include "assert.h"
|
#include "assert.h"
|
||||||
#include "driver/i2c.h"
|
#include "driver/i2c.h"
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "esp_log.h"
|
||||||
#include "gpio-expander.h"
|
#include "gpio-expander.h"
|
||||||
#include "hal/i2c_types.h"
|
#include "hal/i2c_types.h"
|
||||||
#include <cstdint>
|
#include "i2c.h"
|
||||||
|
|
||||||
namespace gay_ipod {
|
namespace gay_ipod {
|
||||||
|
|
||||||
static const char* TAG = "AUDIODAC";
|
static const char* kTag = "AUDIODAC";
|
||||||
|
static const uint8_t kPcm5122Address = 0x4C;
|
||||||
|
static const uint8_t kPcm5122Timeout = 100 / portTICK_RATE_MS;
|
||||||
|
|
||||||
AudioDac::AudioDac(GpioExpander *gpio) {
|
auto AudioDac::create(GpioExpander* expander)
|
||||||
|
-> cpp::result<std::unique_ptr<AudioDac>, Error> {
|
||||||
|
std::unique_ptr<AudioDac> dac = std::make_unique<AudioDac>(expander);
|
||||||
|
|
||||||
|
bool is_booted = dac->WaitForPowerState(
|
||||||
|
[](bool booted, PowerState state) { return booted; });
|
||||||
|
if (!is_booted) {
|
||||||
|
ESP_LOGE(kTag, "Timed out waiting for boot");
|
||||||
|
return cpp::fail(Error::FAILED_TO_BOOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
dac->WriteRegister(Register::DE_EMPHASIS, 1 << 4);
|
||||||
|
dac->WriteVolume(100);
|
||||||
|
|
||||||
|
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 dac;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioDac::AudioDac(GpioExpander* gpio) {
|
||||||
this->gpio_ = gpio;
|
this->gpio_ = gpio;
|
||||||
};
|
};
|
||||||
|
|
||||||
AudioDac::~AudioDac() {};
|
AudioDac::~AudioDac(){
|
||||||
|
// TODO: reset stuff like de-emphasis? Reboot the whole dac? Need to think
|
||||||
esp_err_t AudioDac::Start() {
|
// about this.
|
||||||
bool is_booted = WaitForPowerState([](bool booted, PowerState state){ return booted; });
|
};
|
||||||
if (!is_booted) {
|
|
||||||
ESP_LOGE(TAG, "Timed out waiting for boot");
|
|
||||||
return ESP_ERR_TIMEOUT;
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteRegister(Register::DE_EMPHASIS, 1 << 4);
|
|
||||||
WriteVolume(100);
|
|
||||||
|
|
||||||
WaitForPowerState([](bool booted, PowerState state){
|
|
||||||
return state == WAIT_FOR_CP || state == RAMP_UP || state == RUN || state == STANDBY;
|
|
||||||
});
|
|
||||||
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AudioDac::WriteVolume(uint8_t volume) {
|
void AudioDac::WriteVolume(uint8_t volume) {
|
||||||
WriteRegister(Register::DIGITAL_VOLUME_L, volume);
|
WriteRegister(Register::DIGITAL_VOLUME_L, volume);
|
||||||
@@ -45,31 +60,32 @@ std::pair<bool, AudioDac::PowerState> AudioDac::ReadPowerState() {
|
|||||||
uint8_t result = 0;
|
uint8_t result = 0;
|
||||||
|
|
||||||
I2CTransaction transaction;
|
I2CTransaction transaction;
|
||||||
transaction
|
transaction.start()
|
||||||
.start()
|
.write_addr(kPcm5122Address, I2C_MASTER_WRITE)
|
||||||
.write_addr(kPCM5122Address, I2C_MASTER_WRITE)
|
.write_ack(DSP_BOOT_POWER_STATE)
|
||||||
.write_ack(DSP_BOOT_POWER_STATE)
|
.start()
|
||||||
.start()
|
.write_addr(kPcm5122Address, I2C_MASTER_READ)
|
||||||
.write_addr(kPCM5122Address, I2C_MASTER_READ)
|
.read(&result, I2C_MASTER_NACK)
|
||||||
.read(&result, I2C_MASTER_NACK)
|
.stop();
|
||||||
.stop();
|
|
||||||
|
|
||||||
ESP_ERROR_CHECK(transaction.Execute());
|
ESP_ERROR_CHECK(transaction.Execute());
|
||||||
|
|
||||||
bool is_booted = result >> 7;
|
bool is_booted = result >> 7;
|
||||||
PowerState detail = (PowerState) (result & 0b1111);
|
PowerState detail = (PowerState)(result & 0b1111);
|
||||||
return std::pair(is_booted, detail);
|
return std::pair(is_booted, detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AudioDac::WaitForPowerState(std::function<bool(bool,AudioDac::PowerState)> predicate) {
|
bool AudioDac::WaitForPowerState(
|
||||||
|
std::function<bool(bool, AudioDac::PowerState)> predicate) {
|
||||||
bool has_matched = false;
|
bool has_matched = false;
|
||||||
for (int i=0; i<10; i++) {
|
for (int i = 0; i < 10; i++) {
|
||||||
std::pair<bool, PowerState> result = ReadPowerState();
|
std::pair<bool, PowerState> result = ReadPowerState();
|
||||||
has_matched = predicate(result.first, result.second);
|
has_matched = predicate(result.first, result.second);
|
||||||
if (has_matched) {
|
if (has_matched) {
|
||||||
break;
|
break;
|
||||||
} else {
|
} 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));
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,14 +93,13 @@ bool AudioDac::WaitForPowerState(std::function<bool(bool,AudioDac::PowerState)>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void AudioDac::WriteRegister(Register reg, uint8_t val) {
|
void AudioDac::WriteRegister(Register reg, uint8_t val) {
|
||||||
I2CTransaction transaction;
|
I2CTransaction transaction;
|
||||||
transaction
|
transaction.start()
|
||||||
.start()
|
.write_addr(kPcm5122Address, I2C_MASTER_WRITE)
|
||||||
.write_addr(kPCM5122Address, I2C_MASTER_WRITE)
|
|
||||||
.write_ack(reg, val)
|
.write_ack(reg, val)
|
||||||
.stop();
|
.stop();
|
||||||
// TODO: Retry once?
|
// TODO: Retry once?
|
||||||
ESP_ERROR_CHECK(transaction.Execute());
|
ESP_ERROR_CHECK(transaction.Execute());
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace gay_ipod
|
} // namespace gay_ipod
|
||||||
|
|||||||
+51
-50
@@ -1,72 +1,73 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "gpio-expander.h"
|
#include "gpio-expander.h"
|
||||||
#include <stdint.h>
|
#include "result.hpp"
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
namespace gay_ipod {
|
namespace gay_ipod {
|
||||||
|
|
||||||
static const uint8_t kPCM5122Address = 0x4C;
|
|
||||||
static const uint8_t kPCM5122Timeout = 100 / portTICK_RATE_MS;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for a PCM5122PWR DAC, configured over I2C.
|
* Interface for a PCM5122PWR DAC, configured over I2C.
|
||||||
*/
|
*/
|
||||||
class AudioDac {
|
class AudioDac {
|
||||||
public:
|
public:
|
||||||
AudioDac(GpioExpander *gpio);
|
enum Error {
|
||||||
~AudioDac();
|
FAILED_TO_BOOT,
|
||||||
|
FAILED_TO_CONFIGURE,
|
||||||
|
};
|
||||||
|
static auto create(GpioExpander* expander)
|
||||||
|
-> cpp::result<std::unique_ptr<AudioDac>, Error>;
|
||||||
|
|
||||||
/**
|
AudioDac(GpioExpander* gpio);
|
||||||
* Performs initial configuration of the DAC and sets it to begin expecting
|
~AudioDac();
|
||||||
* I2S audio data.
|
|
||||||
*/
|
|
||||||
esp_err_t Start();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the volume on a scale from 0 (loudest) to 254 (quietest). A value of
|
* Sets the volume on a scale from 0 (loudest) to 254 (quietest). A value of
|
||||||
* 255 engages the soft mute function.
|
* 255 engages the soft mute function.
|
||||||
*/
|
*/
|
||||||
void WriteVolume(uint8_t volume);
|
void WriteVolume(uint8_t volume);
|
||||||
|
|
||||||
enum PowerState {
|
enum PowerState {
|
||||||
POWERDOWN = 0b0,
|
POWERDOWN = 0b0,
|
||||||
WAIT_FOR_CP = 0b1,
|
WAIT_FOR_CP = 0b1,
|
||||||
CALIBRATION_1 = 0b10,
|
CALIBRATION_1 = 0b10,
|
||||||
CALIBRATION_2 = 0b11,
|
CALIBRATION_2 = 0b11,
|
||||||
RAMP_UP = 0b100,
|
RAMP_UP = 0b100,
|
||||||
RUN = 0b101,
|
RUN = 0b101,
|
||||||
SHORT = 0b110,
|
SHORT = 0b110,
|
||||||
RAMP_DOWN = 0b111,
|
RAMP_DOWN = 0b111,
|
||||||
STANDBY = 0b1000,
|
STANDBY = 0b1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
/* 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();
|
||||||
|
|
||||||
// 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;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
GpioExpander *gpio_;
|
GpioExpander* gpio_;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 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.
|
||||||
*/
|
*/
|
||||||
bool WaitForPowerState(std::function<bool(bool,PowerState)> predicate);
|
bool WaitForPowerState(std::function<bool(bool, PowerState)> predicate);
|
||||||
|
|
||||||
enum Register {
|
enum Register {
|
||||||
PAGE_SELECT = 0,
|
PAGE_SELECT = 0,
|
||||||
DE_EMPHASIS = 7,
|
DE_EMPHASIS = 7,
|
||||||
DIGITAL_VOLUME_L = 61,
|
DIGITAL_VOLUME_L = 61,
|
||||||
DIGITAL_VOLUME_R = 62,
|
DIGITAL_VOLUME_R = 62,
|
||||||
DSP_BOOT_POWER_STATE = 118,
|
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
|
||||||
|
|||||||
+61
-154
@@ -1,7 +1,12 @@
|
|||||||
#include <cstdint>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "audio_common.h"
|
||||||
|
#include "audio_element.h"
|
||||||
|
#include "audio_pipeline.h"
|
||||||
#include "battery.h"
|
#include "battery.h"
|
||||||
#include "dac.h"
|
#include "dac.h"
|
||||||
#include "driver/adc.h"
|
#include "driver/adc.h"
|
||||||
@@ -14,19 +19,16 @@
|
|||||||
#include "esp_adc_cal.h"
|
#include "esp_adc_cal.h"
|
||||||
#include "esp_intr_alloc.h"
|
#include "esp_intr_alloc.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
|
#include "fatfs_stream.h"
|
||||||
#include "gpio-expander.h"
|
#include "gpio-expander.h"
|
||||||
#include "hal/adc_types.h"
|
#include "hal/adc_types.h"
|
||||||
#include "hal/gpio_types.h"
|
#include "hal/gpio_types.h"
|
||||||
#include "hal/i2s_types.h"
|
#include "hal/i2s_types.h"
|
||||||
#include "hal/spi_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 "i2s_stream.h"
|
||||||
#include "mp3_decoder.h"
|
#include "mp3_decoder.h"
|
||||||
|
#include "playback.h"
|
||||||
|
#include "storage.h"
|
||||||
|
|
||||||
#define I2C_SDA_IO (GPIO_NUM_2)
|
#define I2C_SDA_IO (GPIO_NUM_2)
|
||||||
#define I2C_SCL_IO (GPIO_NUM_4)
|
#define I2C_SCL_IO (GPIO_NUM_4)
|
||||||
@@ -48,16 +50,17 @@ static const char* TAG = "MAIN";
|
|||||||
esp_err_t init_i2c(void) {
|
esp_err_t init_i2c(void) {
|
||||||
i2c_port_t port = I2C_NUM_0;
|
i2c_port_t port = I2C_NUM_0;
|
||||||
i2c_config_t config = {
|
i2c_config_t config = {
|
||||||
.mode = I2C_MODE_MASTER,
|
.mode = I2C_MODE_MASTER,
|
||||||
.sda_io_num = I2C_SDA_IO,
|
.sda_io_num = I2C_SDA_IO,
|
||||||
.scl_io_num = I2C_SCL_IO,
|
.scl_io_num = I2C_SCL_IO,
|
||||||
.sda_pullup_en = GPIO_PULLUP_ENABLE,
|
.sda_pullup_en = GPIO_PULLUP_ENABLE,
|
||||||
.scl_pullup_en = GPIO_PULLUP_ENABLE,
|
.scl_pullup_en = GPIO_PULLUP_ENABLE,
|
||||||
.master = {
|
.master =
|
||||||
.clk_speed = I2C_CLOCK_HZ,
|
{
|
||||||
},
|
.clk_speed = I2C_CLOCK_HZ,
|
||||||
// No requirements for the clock.
|
},
|
||||||
.clk_flags = 0,
|
// No requirements for the clock.
|
||||||
|
.clk_flags = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
ESP_ERROR_CHECK(i2c_param_config(port, &config));
|
ESP_ERROR_CHECK(i2c_param_config(port, &config));
|
||||||
@@ -70,22 +73,22 @@ esp_err_t init_i2c(void) {
|
|||||||
|
|
||||||
esp_err_t init_spi(void) {
|
esp_err_t init_spi(void) {
|
||||||
spi_bus_config_t config = {
|
spi_bus_config_t config = {
|
||||||
.mosi_io_num = SPI_SDO_IO,
|
.mosi_io_num = SPI_SDO_IO,
|
||||||
.miso_io_num = SPI_SDI_IO,
|
.miso_io_num = SPI_SDI_IO,
|
||||||
.sclk_io_num = SPI_SCLK_IO,
|
.sclk_io_num = SPI_SCLK_IO,
|
||||||
.quadwp_io_num = SPI_QUADWP_IO,
|
.quadwp_io_num = SPI_QUADWP_IO,
|
||||||
.quadhd_io_num = SPI_QUADHD_IO,
|
.quadhd_io_num = SPI_QUADHD_IO,
|
||||||
|
|
||||||
// Unused
|
// Unused
|
||||||
.data4_io_num = -1,
|
.data4_io_num = -1,
|
||||||
.data5_io_num = -1,
|
.data5_io_num = -1,
|
||||||
.data6_io_num = -1,
|
.data6_io_num = -1,
|
||||||
.data7_io_num = -1,
|
.data7_io_num = -1,
|
||||||
|
|
||||||
// Use the DMA default size.
|
// Use the DMA default size.
|
||||||
.max_transfer_sz = 0,
|
.max_transfer_sz = 0,
|
||||||
.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_IOMUX_PINS,
|
.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_IOMUX_PINS,
|
||||||
.intr_flags = 0,
|
.intr_flags = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
ESP_ERROR_CHECK(spi_bus_initialize(VSPI_HOST, &config, SPI_DMA_CH_AUTO));
|
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;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" void app_main(void)
|
extern "C" void app_main(void) {
|
||||||
{
|
|
||||||
ESP_LOGI(TAG, "Initialising peripherals");
|
ESP_LOGI(TAG, "Initialising peripherals");
|
||||||
|
|
||||||
ESP_ERROR_CHECK(gpio_install_isr_service(ESP_INTR_FLAG_LOWMED));
|
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;
|
gay_ipod::GpioExpander expander;
|
||||||
|
|
||||||
// for debugging usb ic
|
// 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_LOGI(TAG, "Init ADC");
|
||||||
ESP_ERROR_CHECK(gay_ipod::init_adc());
|
ESP_ERROR_CHECK(gay_ipod::init_adc());
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Init SD card");
|
ESP_LOGI(TAG, "Init SD card");
|
||||||
gay_ipod::SdStorage storage(&expander);
|
auto storage_res = gay_ipod::SdStorage::create(&expander);
|
||||||
gay_ipod::SdStorage::Error err = storage.Acquire();
|
if (storage_res.has_error()) {
|
||||||
if (err != gay_ipod::SdStorage::Error::OK) {
|
ESP_LOGE(TAG, "Failed: %d", storage_res.error());
|
||||||
ESP_LOGE(TAG, "Failed to acquire storage!");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
std::unique_ptr<gay_ipod::SdStorage> storage = std::move(storage_res.value());
|
||||||
|
|
||||||
|
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<gay_ipod::AudioDac> dac = std::move(dac_res.value());
|
||||||
|
|
||||||
|
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<gay_ipod::DacAudioPlayback> playback =
|
||||||
|
std::move(playback_res.value());
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Everything looks good! Waiting a mo for debugger.");
|
ESP_LOGI(TAG, "Everything looks good! Waiting a mo for debugger.");
|
||||||
vTaskDelay(pdMS_TO_TICKS(1500));
|
vTaskDelay(pdMS_TO_TICKS(1500));
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Trying to init DAC");
|
playback->Play("/sdcard/test.mp3");
|
||||||
gay_ipod::AudioDac dac(&expander);
|
playback->set_volume(100);
|
||||||
dac.Start();
|
|
||||||
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
playback->WaitForSongEnd();
|
||||||
|
|
||||||
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_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,
|
|
||||||
.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, "Trying to play something??");
|
|
||||||
audio_element_set_uri(fatfs_stream_reader, "/sdcard/test.mp3");
|
|
||||||
audio_pipeline_run(pipeline);
|
|
||||||
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
|
||||||
ESP_LOGI(TAG, "It could be playing? idk");
|
|
||||||
|
|
||||||
gay_ipod::AudioDac::PowerState state = dac.ReadPowerState().second;
|
|
||||||
ESP_LOGI(TAG, "dac power state: %x", (uint8_t)state);
|
|
||||||
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(120000));
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Time to deinit.");
|
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!");
|
ESP_LOGI(TAG, "Hooray!");
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-18
@@ -1,9 +1,9 @@
|
|||||||
#include "gpio-expander.h"
|
#include "gpio-expander.h"
|
||||||
|
|
||||||
#include "i2c.h"
|
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "i2c.h"
|
||||||
|
|
||||||
namespace gay_ipod {
|
namespace gay_ipod {
|
||||||
|
|
||||||
GpioExpander::GpioExpander() {
|
GpioExpander::GpioExpander() {
|
||||||
@@ -32,9 +32,9 @@ esp_err_t GpioExpander::Write() {
|
|||||||
|
|
||||||
I2CTransaction transaction;
|
I2CTransaction transaction;
|
||||||
transaction.start()
|
transaction.start()
|
||||||
.write_addr(kPca8575Address, I2C_MASTER_WRITE)
|
.write_addr(kPca8575Address, I2C_MASTER_WRITE)
|
||||||
.write_ack(ports_ab.first, ports_ab.second)
|
.write_ack(ports_ab.first, ports_ab.second)
|
||||||
.stop();
|
.stop();
|
||||||
|
|
||||||
return transaction.Execute();
|
return transaction.Execute();
|
||||||
}
|
}
|
||||||
@@ -44,10 +44,10 @@ esp_err_t GpioExpander::Read() {
|
|||||||
|
|
||||||
I2CTransaction transaction;
|
I2CTransaction transaction;
|
||||||
transaction.start()
|
transaction.start()
|
||||||
.write_addr(kPca8575Address, I2C_MASTER_READ)
|
.write_addr(kPca8575Address, I2C_MASTER_READ)
|
||||||
.read(&input_a, I2C_MASTER_ACK)
|
.read(&input_a, I2C_MASTER_ACK)
|
||||||
.read(&input_b, I2C_MASTER_LAST_NACK)
|
.read(&input_b, I2C_MASTER_LAST_NACK)
|
||||||
.stop();
|
.stop();
|
||||||
|
|
||||||
esp_err_t ret = transaction.Execute();
|
esp_err_t ret = transaction.Execute();
|
||||||
inputs_ = pack(input_a, input_b);
|
inputs_ = pack(input_a, input_b);
|
||||||
@@ -55,7 +55,7 @@ esp_err_t GpioExpander::Read() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GpioExpander::set_pin(ChipSelect cs, bool value) {
|
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) {
|
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)
|
GpioExpander::SpiLock::SpiLock(GpioExpander& gpio, ChipSelect cs)
|
||||||
: lock_(gpio.cs_mutex_), gpio_(gpio), cs_(cs) {
|
: lock_(gpio.cs_mutex_), gpio_(gpio), cs_(cs) {
|
||||||
gpio_.with([&](auto& gpio) {
|
gpio_.with([&](auto& gpio) { gpio.set_pin(cs_, 0); });
|
||||||
gpio.set_pin(cs_, 0);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GpioExpander::SpiLock::~SpiLock() {
|
GpioExpander::SpiLock::~SpiLock() {
|
||||||
gpio_.with([&](auto& gpio) {
|
gpio_.with([&](auto& gpio) { gpio.set_pin(cs_, 1); });
|
||||||
gpio.set_pin(cs_, 1);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace gay_ipod
|
} // namespace gay_ipod
|
||||||
|
|||||||
+161
-161
@@ -1,16 +1,17 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <stdint.h>
|
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "esp_check.h"
|
|
||||||
#include "esp_log.h"
|
|
||||||
#include "esp_err.h"
|
|
||||||
#include "driver/i2c.h"
|
#include "driver/i2c.h"
|
||||||
|
#include "esp_check.h"
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "esp_log.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
|
|
||||||
namespace gay_ipod {
|
namespace gay_ipod {
|
||||||
@@ -26,184 +27,183 @@ namespace gay_ipod {
|
|||||||
* should be done whilst holding `cs_lock` (preferably via the helper methods).
|
* should be done whilst holding `cs_lock` (preferably via the helper methods).
|
||||||
*/
|
*/
|
||||||
class GpioExpander {
|
class GpioExpander {
|
||||||
public:
|
public:
|
||||||
GpioExpander();
|
GpioExpander();
|
||||||
~GpioExpander();
|
~GpioExpander();
|
||||||
|
|
||||||
static const uint8_t kPca8575Address = 0x20;
|
static const uint8_t kPca8575Address = 0x20;
|
||||||
static const uint8_t kPca8575Timeout = 100 / portTICK_RATE_MS;
|
static const uint8_t kPca8575Timeout = 100 / portTICK_RATE_MS;
|
||||||
|
|
||||||
// Port A:
|
// Port A:
|
||||||
// 0 - audio power enable
|
// 0 - audio power enable
|
||||||
// 1 - usb interface power enable
|
// 1 - usb interface power enable
|
||||||
// 2 - display power enable
|
// 2 - display power enable
|
||||||
// 3 - sd card power enable
|
// 3 - sd card power enable
|
||||||
// 4 - charge power ok (active low)
|
// 4 - charge power ok (active low)
|
||||||
// 5 - sd mux switch
|
// 5 - sd mux switch
|
||||||
// 6 - sd chip select
|
// 6 - sd chip select
|
||||||
// 7 - display chip select
|
// 7 - display chip select
|
||||||
// All power switches low, chip selects high, active-low charge power high
|
// All power switches low, chip selects high, active-low charge power high
|
||||||
static const uint8_t kPortADefault = 0b11010001;
|
static const uint8_t kPortADefault = 0b11010001;
|
||||||
|
|
||||||
// Port B:
|
// Port B:
|
||||||
// 0 - 3.5mm jack detect (active low)
|
// 0 - 3.5mm jack detect (active low)
|
||||||
// 1 - dac soft mute switch
|
// 1 - dac soft mute switch
|
||||||
// 2 - GPIO
|
// 2 - GPIO
|
||||||
// 3 - GPIO
|
// 3 - GPIO
|
||||||
// 4 - GPIO
|
// 4 - GPIO
|
||||||
// 5 - GPIO
|
// 5 - GPIO
|
||||||
// 6 - GPIO
|
// 6 - GPIO
|
||||||
// 7 - GPIO
|
// 7 - GPIO
|
||||||
// DAC mute output low, everything else is active-low inputs.
|
// DAC mute output low, everything else is active-low inputs.
|
||||||
static const uint8_t kPortBDefault = 0b11111111;
|
static const uint8_t kPortBDefault = 0b11111111;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Convenience mehod for packing the port a and b bytes into a single 16 bit
|
* Convenience mehod for packing the port a and b bytes into a single 16 bit
|
||||||
* value.
|
* value.
|
||||||
*/
|
*/
|
||||||
static uint16_t pack(uint8_t a, uint8_t b) {
|
static uint16_t pack(uint8_t a, uint8_t b) { return ((uint16_t)b) << 8 | a; }
|
||||||
return ((uint16_t) b) << 8 | a;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Convenience mehod for unpacking the result of `pack` back into two single
|
* Convenience mehod for unpacking the result of `pack` back into two single
|
||||||
* byte port datas.
|
* byte port datas.
|
||||||
*/
|
*/
|
||||||
static std::pair<uint8_t, uint8_t> unpack(uint16_t ba) {
|
static std::pair<uint8_t, uint8_t> unpack(uint16_t ba) {
|
||||||
return std::pair((uint8_t) ba, (uint8_t) (ba >> 8));
|
return std::pair((uint8_t)ba, (uint8_t)(ba >> 8));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Convenience function for running some arbitrary pin writing code, then
|
* Convenience function for running some arbitrary pin writing code, then
|
||||||
* flushing a `Write()` to the expander. Example usage:
|
* flushing a `Write()` to the expander. Example usage:
|
||||||
*
|
*
|
||||||
* ```
|
* ```
|
||||||
* gpio_.with([&](auto& gpio) {
|
* gpio_.with([&](auto& gpio) {
|
||||||
* gpio.set_pin(AUDIO_POWER_ENABLE, true);
|
* gpio.set_pin(AUDIO_POWER_ENABLE, true);
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
void with(std::function<void(GpioExpander&)> f);
|
void with(std::function<void(GpioExpander&)> f);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the ports on the GPIO expander to the values currently represented
|
* Sets the ports on the GPIO expander to the values currently represented
|
||||||
* in `ports`.
|
* in `ports`.
|
||||||
*/
|
*/
|
||||||
esp_err_t Write(void);
|
esp_err_t Write(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads from the GPIO expander, populating `inputs` with the most recent
|
* Reads from the GPIO expander, populating `inputs` with the most recent
|
||||||
* values.
|
* values.
|
||||||
*/
|
*/
|
||||||
esp_err_t Read(void);
|
esp_err_t Read(void);
|
||||||
|
|
||||||
/* Maps each pin of the expander to its number in a `pack`ed uint16. */
|
/* Maps each pin of the expander to its number in a `pack`ed uint16. */
|
||||||
enum Pin {
|
enum Pin {
|
||||||
// Port A
|
// Port A
|
||||||
AUDIO_POWER_ENABLE = 0,
|
AUDIO_POWER_ENABLE = 0,
|
||||||
USB_INTERFACE_POWER_ENABLE = 1,
|
USB_INTERFACE_POWER_ENABLE = 1,
|
||||||
DISPLAY_POWER_ENABLE = 2,
|
DISPLAY_POWER_ENABLE = 2,
|
||||||
SD_CARD_POWER_ENABLE = 3,
|
SD_CARD_POWER_ENABLE = 3,
|
||||||
CHARGE_POWER_OK = 4, // Active-low input
|
CHARGE_POWER_OK = 4, // Active-low input
|
||||||
SD_MUX_SWITCH = 5,
|
SD_MUX_SWITCH = 5,
|
||||||
SD_CHIP_SELECT = 6,
|
SD_CHIP_SELECT = 6,
|
||||||
DISPLAY_CHIP_SELECT = 7,
|
DISPLAY_CHIP_SELECT = 7,
|
||||||
|
|
||||||
// Port B
|
// Port B
|
||||||
PHONE_DETECT = 8, // Active-high input
|
PHONE_DETECT = 8, // Active-high input
|
||||||
DAC_MUTE = 9,
|
DAC_MUTE = 9,
|
||||||
GPIO_1 = 10,
|
GPIO_1 = 10,
|
||||||
GPIO_2 = 11,
|
GPIO_2 = 11,
|
||||||
GPIO_3 = 12,
|
GPIO_3 = 12,
|
||||||
GPIO_4 = 13,
|
GPIO_4 = 13,
|
||||||
GPIO_5 = 14,
|
GPIO_5 = 14,
|
||||||
GPIO_6 = 15,
|
GPIO_6 = 15,
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Pins whose access should be guarded by `cs_lock`. */
|
/* Pins whose access should be guarded by `cs_lock`. */
|
||||||
enum ChipSelect {
|
enum ChipSelect {
|
||||||
SD_CARD = SD_CHIP_SELECT,
|
SD_CARD = SD_CHIP_SELECT,
|
||||||
DISPLAY = DISPLAY_CHIP_SELECT,
|
DISPLAY = DISPLAY_CHIP_SELECT,
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Nicer value names for use with the SD_MUX_SWITCH pin. */
|
/* Nicer value names for use with the SD_MUX_SWITCH pin. */
|
||||||
enum SdController {
|
enum SdController {
|
||||||
SD_MUX_ESP = 0,
|
SD_MUX_ESP = 0,
|
||||||
SD_MUX_USB = 1,
|
SD_MUX_USB = 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current driven status of each of the ports. The first byte is
|
* Returns the current driven status of each of the ports. The first byte is
|
||||||
* port a, and the second byte is port b.
|
* port a, and the second byte is port b.
|
||||||
*/
|
*/
|
||||||
std::atomic<uint16_t>& ports() { return ports_; }
|
std::atomic<uint16_t>& ports() { return ports_; }
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Sets a single specific pin to the given value. `true` corresponds to
|
* Sets a single specific pin to the given value. `true` corresponds to
|
||||||
* HIGH, and `false` corresponds to LOW.
|
* HIGH, and `false` corresponds to LOW.
|
||||||
*
|
*
|
||||||
* Calls to this method will be buffered in memory until a call to `Write()`
|
* Calls to this method will be buffered in memory until a call to `Write()`
|
||||||
* is made.
|
* is made.
|
||||||
*/
|
*/
|
||||||
void set_pin(Pin pin, bool value);
|
void set_pin(Pin pin, bool value);
|
||||||
void set_pin(ChipSelect cs, bool value);
|
void set_pin(ChipSelect cs, bool value);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the input status of each of the ports. The first byte is port a,
|
* Returns the input status of each of the ports. The first byte is port a,
|
||||||
* and the second byte is port b.
|
* and the second byte is port b.
|
||||||
*/
|
*/
|
||||||
const std::atomic<uint16_t>& inputs() const { return inputs_; }
|
const std::atomic<uint16_t>& inputs() const { return inputs_; }
|
||||||
|
|
||||||
/* Returns the most recently cached value of the given pin. Only valid for
|
/* 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
|
* pins used as inputs; to check what value we're driving a pin, use
|
||||||
* `ports()`.
|
* `ports()`.
|
||||||
*/
|
*/
|
||||||
bool get_input(Pin pin) const;
|
bool get_input(Pin pin) const;
|
||||||
|
|
||||||
/* Returns the mutex that must be held whilst pulling a CS pin low. */
|
/* Returns the mutex that must be held whilst pulling a CS pin low. */
|
||||||
std::mutex& cs_mutex() { return cs_mutex_; }
|
std::mutex& cs_mutex() { return cs_mutex_; }
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Helper class containing an active `cs_mutex` lock. When an instance of
|
* Helper class containing an active `cs_mutex` lock. When an instance of
|
||||||
* this class is destroyed (usually by falling out of scope), the associated
|
* this class is destroyed (usually by falling out of scope), the associated
|
||||||
* CS pin will be driven high before the lock is released.
|
* CS pin will be driven high before the lock is released.
|
||||||
*/
|
*/
|
||||||
class SpiLock {
|
class SpiLock {
|
||||||
public:
|
public:
|
||||||
SpiLock(GpioExpander &gpio, ChipSelect cs);
|
SpiLock(GpioExpander& gpio, ChipSelect cs);
|
||||||
~SpiLock();
|
~SpiLock();
|
||||||
|
|
||||||
SpiLock(const SpiLock&) = delete;
|
SpiLock(const SpiLock&) = delete;
|
||||||
private:
|
|
||||||
std::scoped_lock<std::mutex> lock_;
|
|
||||||
GpioExpander &gpio_;
|
|
||||||
ChipSelect cs_;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
private:
|
||||||
* Pulls the given CS pin low to signal that we are about to communicate
|
std::scoped_lock<std::mutex> lock_;
|
||||||
* with a particular device, after acquiring a lock on `cs_mutex`. The
|
GpioExpander& gpio_;
|
||||||
* recommended way to safely interact with devices on the SPI bus is to have
|
ChipSelect cs_;
|
||||||
* 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.
|
* Pulls the given CS pin low to signal that we are about to communicate
|
||||||
GpioExpander(const GpioExpander&) = delete;
|
* with a particular device, after acquiring a lock on `cs_mutex`. The
|
||||||
GpioExpander& operator=(const GpioExpander&) = delete;
|
* 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);
|
||||||
|
|
||||||
private:
|
// Not copyable or movable. There should usually only ever be once instance
|
||||||
std::mutex cs_mutex_;
|
// of this class, and that instance will likely have a static lifetime.
|
||||||
std::atomic<uint16_t> ports_;
|
GpioExpander(const GpioExpander&) = delete;
|
||||||
std::atomic<uint16_t> inputs_;
|
GpioExpander& operator=(const GpioExpander&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::mutex cs_mutex_;
|
||||||
|
std::atomic<uint16_t> ports_;
|
||||||
|
std::atomic<uint16_t> inputs_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace gay_ipod
|
} // namespace gay_ipod
|
||||||
|
|||||||
+3
-2
@@ -1,4 +1,5 @@
|
|||||||
#include "i2c.h"
|
#include "i2c.h"
|
||||||
|
|
||||||
#include "assert.h"
|
#include "assert.h"
|
||||||
|
|
||||||
namespace gay_ipod {
|
namespace gay_ipod {
|
||||||
@@ -36,9 +37,9 @@ I2CTransaction& I2CTransaction::write_ack(uint8_t data) {
|
|||||||
return *this;
|
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));
|
ESP_ERROR_CHECK(i2c_master_read_byte(handle_, dest, ack));
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace gay_ipod
|
} // namespace gay_ipod
|
||||||
|
|||||||
+57
-54
@@ -1,8 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
#include "driver/i2c.h"
|
#include "driver/i2c.h"
|
||||||
#include "hal/i2c_types.h"
|
#include "hal/i2c_types.h"
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
namespace gay_ipod {
|
namespace gay_ipod {
|
||||||
|
|
||||||
@@ -15,66 +16,68 @@ namespace gay_ipod {
|
|||||||
* typically represent invalid arguments or OOMs.
|
* typically represent invalid arguments or OOMs.
|
||||||
*/
|
*/
|
||||||
class I2CTransaction {
|
class I2CTransaction {
|
||||||
public:
|
public:
|
||||||
static const uint8_t kI2CTimeout = 100 / portTICK_RATE_MS;
|
static const uint8_t kI2CTimeout = 100 / portTICK_RATE_MS;
|
||||||
|
|
||||||
I2CTransaction();
|
I2CTransaction();
|
||||||
~I2CTransaction();
|
~I2CTransaction();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Executes all enqueued commands, returning the result code. Possible error
|
* Executes all enqueued commands, returning the result code. Possible error
|
||||||
* codes, per the ESP-IDF docs:
|
* codes, per the ESP-IDF docs:
|
||||||
*
|
*
|
||||||
* ESP_OK Success
|
* ESP_OK Success
|
||||||
* ESP_ERR_INVALID_ARG Parameter error
|
* ESP_ERR_INVALID_ARG Parameter error
|
||||||
* ESP_FAIL Sending command error, slave doesn’t ACK the transfer.
|
* 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_INVALID_STATE I2C driver not installed or not in master mode.
|
||||||
* ESP_ERR_TIMEOUT Operation timeout because the bus is busy.
|
* ESP_ERR_TIMEOUT Operation timeout because the bus is busy.
|
||||||
*/
|
*/
|
||||||
esp_err_t Execute();
|
esp_err_t Execute();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Enqueues a start condition. May also be used for repeated start conditions.
|
* Enqueues a start condition. May also be used for repeated start
|
||||||
*/
|
* conditions.
|
||||||
I2CTransaction& start();
|
*/
|
||||||
/* Enqueues a stop condition. */
|
I2CTransaction& start();
|
||||||
I2CTransaction& stop();
|
/* Enqueues a stop condition. */
|
||||||
|
I2CTransaction& stop();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Enqueues writing the given 7 bit address, followed by one bit indicating
|
* Enqueues writing the given 7 bit address, followed by one bit indicating
|
||||||
* whether this is a read or write request.
|
* whether this is a read or write request.
|
||||||
*
|
*
|
||||||
* This command will expect an ACK before continuing.
|
* This command will expect an ACK before continuing.
|
||||||
*/
|
*/
|
||||||
I2CTransaction& write_addr(uint8_t addr, uint8_t op);
|
I2CTransaction& write_addr(uint8_t addr, uint8_t op);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Enqueues one or more bytes to be written. The transaction will wait for
|
* Enqueues one or more bytes to be written. The transaction will wait for
|
||||||
* an ACK to be returned before writing the next byte.
|
* an ACK to be returned before writing the next byte.
|
||||||
*/
|
*/
|
||||||
I2CTransaction& write_ack(uint8_t data);
|
I2CTransaction& write_ack(uint8_t data);
|
||||||
template <typename ...More>
|
template <typename... More>
|
||||||
I2CTransaction& write_ack(uint8_t data, More... more) {
|
I2CTransaction& write_ack(uint8_t data, More... more) {
|
||||||
write_ack(data);
|
write_ack(data);
|
||||||
write_ack(more...);
|
write_ack(more...);
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Enqueues a read of one byte into the given uint8. Responds with the given
|
* Enqueues a read of one byte into the given uint8. Responds with the given
|
||||||
* ACK/NACK type.
|
* ACK/NACK type.
|
||||||
*/
|
*/
|
||||||
I2CTransaction& read(uint8_t *dest, i2c_ack_type_t ack);
|
I2CTransaction& read(uint8_t* dest, i2c_ack_type_t ack);
|
||||||
|
|
||||||
/* Returns the underlying command buffer. */
|
/* Returns the underlying command buffer. */
|
||||||
i2c_cmd_handle_t handle() { return handle_; }
|
i2c_cmd_handle_t handle() { return handle_; }
|
||||||
|
|
||||||
// Cannot be moved or copied, since doing so is probably an error. Pass a
|
// Cannot be moved or copied, since doing so is probably an error. Pass a
|
||||||
// reference instead.
|
// reference instead.
|
||||||
I2CTransaction(const I2CTransaction&) = delete;
|
I2CTransaction(const I2CTransaction&) = delete;
|
||||||
I2CTransaction& operator=(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
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
#include "playback.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#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<std::unique_ptr<DacAudioPlayback>, 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_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,
|
||||||
|
.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<DacAudioPlayback>(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
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#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<std::unique_ptr<DacAudioPlayback>, 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
|
||||||
+87
-33
@@ -1,5 +1,6 @@
|
|||||||
#include "storage.h"
|
#include "storage.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
||||||
#include "diskio_impl.h"
|
#include "diskio_impl.h"
|
||||||
@@ -15,77 +16,130 @@
|
|||||||
#include "hal/spi_types.h"
|
#include "hal/spi_types.h"
|
||||||
#include "sdmmc_cmd.h"
|
#include "sdmmc_cmd.h"
|
||||||
|
|
||||||
|
static const char* kTag = "SDSTORAGE";
|
||||||
|
static const uint8_t kMaxOpenFiles = 8;
|
||||||
|
|
||||||
namespace gay_ipod {
|
namespace gay_ipod {
|
||||||
|
|
||||||
static const char* TAG = "SDSTORAGE";
|
const char* kStoragePath = "/sdcard";
|
||||||
|
|
||||||
SdStorage::SdStorage(GpioExpander *gpio) {
|
// Static functions for interrop with the ESP IDF API, which requires a
|
||||||
this->gpio_ = gpio;
|
// function pointer.
|
||||||
|
namespace callback {
|
||||||
|
static std::atomic<SdStorage*> instance = nullptr;
|
||||||
|
static std::atomic<esp_err_t (*)(sdspi_dev_handle_t, sdmmc_command_t*)>
|
||||||
|
bootstrap = nullptr;
|
||||||
|
|
||||||
|
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::~SdStorage() {}
|
auto SdStorage::create(GpioExpander* gpio)
|
||||||
|
-> cpp::result<std::unique_ptr<SdStorage>, Error> {
|
||||||
SdStorage::Error SdStorage::Acquire(void) {
|
|
||||||
// Acquiring the bus will also flush the mux switch change.
|
// 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.
|
// Now we can init the driver and set up the SD card into SPI mode.
|
||||||
sdspi_host_init();
|
sdspi_host_init();
|
||||||
|
|
||||||
sdspi_device_config_t config = {
|
sdspi_device_config_t config = {
|
||||||
.host_id = VSPI_HOST,
|
.host_id = VSPI_HOST,
|
||||||
// CS handled manually bc it's on the GPIO expander
|
// CS handled manually bc it's on the GPIO expander
|
||||||
.gpio_cs = GPIO_NUM_2,
|
.gpio_cs = GPIO_NUM_2,
|
||||||
.gpio_cd = SDSPI_SLOT_NO_CD,
|
.gpio_cd = SDSPI_SLOT_NO_CD,
|
||||||
.gpio_wp = SDSPI_SLOT_NO_WP,
|
.gpio_wp = SDSPI_SLOT_NO_WP,
|
||||||
.gpio_int = GPIO_NUM_NC,
|
.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
|
// 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
|
// 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
|
// with our own that acquires the CS mutex for the duration of the SPI
|
||||||
// transaction.
|
// transaction.
|
||||||
auto src = host_.do_transaction;
|
auto do_transaction = host.do_transaction;
|
||||||
sdspi::do_transaction_wrapper = [=](sdspi_dev_handle_t handle, sdmmc_command_t *cmd) -> esp_err_t {
|
host.do_transaction = &callback::do_transaction;
|
||||||
auto lock = gpio_->AcquireSpiBus(GpioExpander::SD_CARD);
|
host.slot = handle;
|
||||||
return src(handle, cmd);
|
callback::bootstrap = do_transaction;
|
||||||
};
|
|
||||||
host_.do_transaction = &sdspi::do_transaction;
|
|
||||||
|
|
||||||
host_.slot = handle_;
|
auto lock = gpio->AcquireSpiBus(GpioExpander::SD_CARD);
|
||||||
// Will return ESP_ERR_INVALID_RESPONSE if there is no 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) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGW(TAG, "Failed to read, err: %d", err);
|
ESP_LOGW(kTag, "Failed to read, err: %d", err);
|
||||||
return Error::FAILED_TO_READ;
|
return cpp::fail(Error::FAILED_TO_READ);
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_ERROR_CHECK(esp_vfs_fat_register(kStoragePath, "", kMaxOpenFiles, &fs_));
|
ESP_ERROR_CHECK(esp_vfs_fat_register(kStoragePath, "", kMaxOpenFiles, &fs));
|
||||||
ff_diskio_register_sdmmc(fs_->pdrv, &card_);
|
ff_diskio_register_sdmmc(fs->pdrv, &card);
|
||||||
|
|
||||||
// Mount right now, not on first operation.
|
// Mount right now, not on first operation.
|
||||||
FRESULT ferr = f_mount(fs_, "", 1);
|
FRESULT ferr = f_mount(fs, "", 1);
|
||||||
if (ferr != FR_OK) {
|
if (ferr != FR_OK) {
|
||||||
ESP_LOGW(TAG, "Failed to mount, err: %d", ferr);
|
ESP_LOGW(kTag, "Failed to mount, err: %d", ferr);
|
||||||
return Error::FAILED_TO_MOUNT;
|
return cpp::fail(Error::FAILED_TO_MOUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Error::OK;
|
return std::make_unique<SdStorage>(gpio, do_transaction, handle, host, card,
|
||||||
|
fs);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdStorage::Release(void) {
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
SdStorage::~SdStorage() {
|
||||||
// Unmount and unregister the filesystem
|
// Unmount and unregister the filesystem
|
||||||
f_unmount("");
|
f_unmount("");
|
||||||
ff_diskio_register(fs_->pdrv, NULL);
|
ff_diskio_register(fs_->pdrv, NULL);
|
||||||
esp_vfs_fat_unregister_path(kStoragePath);
|
esp_vfs_fat_unregister_path(kStoragePath);
|
||||||
fs_ = nullptr;
|
fs_ = nullptr;
|
||||||
|
|
||||||
|
callback::instance = nullptr;
|
||||||
|
|
||||||
// Uninstall the SPI driver
|
// Uninstall the SPI driver
|
||||||
sdspi_host_remove_device(this->handle_);
|
sdspi_host_remove_device(this->handle_);
|
||||||
sdspi_host_deinit();
|
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
|
||||||
|
|||||||
+37
-51
@@ -1,73 +1,59 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include "driver/sdmmc_types.h"
|
#include "driver/sdmmc_types.h"
|
||||||
#include "driver/sdspi_host.h"
|
#include "driver/sdspi_host.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "esp_vfs_fat.h"
|
#include "esp_vfs_fat.h"
|
||||||
#include "gpio-expander.h"
|
#include "gpio-expander.h"
|
||||||
|
#include "result.hpp"
|
||||||
|
|
||||||
namespace gay_ipod {
|
namespace gay_ipod {
|
||||||
|
|
||||||
// Static functions for interrop with the ESP IDF API, which requires a function
|
extern const char* kStoragePath;
|
||||||
// pointer.
|
|
||||||
namespace sdspi {
|
|
||||||
// Holds a lambda created by SdStorage.
|
|
||||||
static std::function<esp_err_t(sdspi_dev_handle_t,sdmmc_command_t*)> 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;
|
|
||||||
|
|
||||||
class SdStorage {
|
class SdStorage {
|
||||||
public:
|
public:
|
||||||
SdStorage(GpioExpander *gpio);
|
enum Error {
|
||||||
~SdStorage();
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
enum Error {
|
static auto create(GpioExpander* gpio)
|
||||||
OK,
|
-> cpp::result<std::unique_ptr<SdStorage>, Error>;
|
||||||
/** 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
|
SdStorage(GpioExpander* gpio,
|
||||||
// down once we have that capability.
|
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)
|
||||||
* Initialises the SDSPI driver and mounts the SD card for reading and
|
-> esp_err_t;
|
||||||
* writing. This must be called before any interactions with the underlying
|
|
||||||
* storage.
|
|
||||||
*/
|
|
||||||
Error Acquire(void);
|
|
||||||
|
|
||||||
/**
|
// Not copyable or movable.
|
||||||
* Unmounts the SD card and frees memory associated with the SDSPI driver.
|
// TODO: maybe this could be movable?
|
||||||
*/
|
SdStorage(const SdStorage&) = delete;
|
||||||
void Release(void);
|
SdStorage& operator=(const SdStorage&) = delete;
|
||||||
|
|
||||||
// Not copyable or movable.
|
private:
|
||||||
// TODO: maybe this could be movable?
|
GpioExpander* gpio_;
|
||||||
SdStorage(const SdStorage&) = delete;
|
|
||||||
SdStorage& operator=(const SdStorage&) = delete;
|
|
||||||
|
|
||||||
private:
|
esp_err_t (*do_transaction_)(sdspi_dev_handle_t, sdmmc_command_t*) = nullptr;
|
||||||
GpioExpander *gpio_;
|
|
||||||
|
|
||||||
// SPI and SD driver info
|
// SPI and SD driver info
|
||||||
sdspi_dev_handle_t handle_;
|
sdspi_dev_handle_t handle_;
|
||||||
sdmmc_host_t host_;
|
sdmmc_host_t host_;
|
||||||
sdmmc_card_t card_;
|
sdmmc_card_t card_;
|
||||||
|
|
||||||
// Filesystem info
|
// Filesystem info
|
||||||
FATFS *fs_ = nullptr;
|
FATFS* fs_ = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace gay_ipod
|
} // namespace gay_ipod
|
||||||
|
|||||||
Reference in New Issue
Block a user