Audio System + Drivers (#562)

This commit is contained in:
Shadowtrance
2026-07-14 16:34:29 +10:00
committed by GitHub
parent fa4a6e255c
commit 955416dac8
137 changed files with 8847 additions and 387 deletions
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/drivers/audio_codec_adapters.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/freertos/freertos.h>
#include <stdint.h>
#include <stdlib.h>
#define I2C_TIMEOUT_TICKS pdMS_TO_TICKS(100)
struct I2cCtrlContext {
audio_codec_ctrl_if_t base;
struct Device* i2c_controller;
uint8_t address;
bool is_open;
};
static bool ctrl_is_open(const audio_codec_ctrl_if_t* handle) {
const struct I2cCtrlContext* context = (const struct I2cCtrlContext*) handle;
return context->is_open;
}
static int ctrl_open(const audio_codec_ctrl_if_t* handle, void* config, int config_size) {
(void) config;
(void) config_size;
struct I2cCtrlContext* context = (struct I2cCtrlContext*) handle;
context->is_open = true;
return ESP_CODEC_DEV_OK;
}
static int ctrl_read_reg(const audio_codec_ctrl_if_t* handle, int reg, int reg_len, void* data, int data_len) {
struct I2cCtrlContext* context = (struct I2cCtrlContext*) handle;
if (!context->is_open || reg_len != 1) {
return ESP_CODEC_DEV_NOT_SUPPORT;
}
if (data_len < 0 || data_len > UINT16_MAX) {
return ESP_CODEC_DEV_NOT_SUPPORT;
}
error_t error = i2c_controller_read_register(context->i2c_controller, context->address, (uint8_t) reg, (uint8_t*) data, (size_t) data_len, I2C_TIMEOUT_TICKS);
return (error == ERROR_NONE) ? ESP_CODEC_DEV_OK : ESP_CODEC_DEV_READ_FAIL;
}
static int ctrl_write_reg(const audio_codec_ctrl_if_t* handle, int reg, int reg_len, void* data, int data_len) {
struct I2cCtrlContext* context = (struct I2cCtrlContext*) handle;
if (!context->is_open || reg_len != 1) {
return ESP_CODEC_DEV_NOT_SUPPORT;
}
if (data_len < 0 || data_len > UINT16_MAX) {
return ESP_CODEC_DEV_NOT_SUPPORT;
}
error_t error = i2c_controller_write_register(context->i2c_controller, context->address, (uint8_t) reg, (const uint8_t*) data, (uint16_t) data_len, I2C_TIMEOUT_TICKS);
return (error == ERROR_NONE) ? ESP_CODEC_DEV_OK : ESP_CODEC_DEV_WRITE_FAIL;
}
static int ctrl_close(const audio_codec_ctrl_if_t* handle) {
struct I2cCtrlContext* context = (struct I2cCtrlContext*) handle;
context->is_open = false;
return ESP_CODEC_DEV_OK;
}
const audio_codec_ctrl_if_t* audio_codec_adapter_new_i2c_ctrl(struct Device* i2c_controller, uint8_t address) {
if (i2c_controller == NULL) {
return NULL;
}
struct I2cCtrlContext* context = (struct I2cCtrlContext*) calloc(1, sizeof(struct I2cCtrlContext));
if (context == NULL) {
return NULL;
}
context->i2c_controller = i2c_controller;
context->address = address;
context->is_open = false;
context->base.open = ctrl_open;
context->base.is_open = ctrl_is_open;
context->base.read_reg = ctrl_read_reg;
context->base.write_reg = ctrl_write_reg;
context->base.close = ctrl_close;
return &context->base;
}
// Note: esp_codec_dev already provides audio_codec_delete_ctrl_if() (calls ->close then frees);
// no adapter-specific cleanup is needed, so we don't redefine it here.
@@ -0,0 +1,193 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/drivers/audio_codec_adapters.h>
#include <tactility/drivers/i2s_controller.h>
#include <tactility/freertos/freertos.h>
#include <stdlib.h>
// esp_codec_dev_read/write may issue several of these chunked I2S transfers per call and
// gives us no way to forward the caller's audio_stream timeout through. Keep this short so
// a stalled/underrunning I2S link returns promptly -- callers retry on failure, and a long
// per-chunk timeout here is what made AudioStreamTest's loopback task unkillable (it blocked
// far longer than its read/write timeout and the 2s drain wait in stopLoopback()).
#define I2S_TIMEOUT_TICKS pdMS_TO_TICKS(200)
struct I2sDataContext {
audio_codec_data_if_t base;
struct Device* i2s_controller;
bool is_open;
bool is_pdm; // true if created via audio_codec_adapter_new_i2s_pdm_data()
};
static bool data_is_open(const audio_codec_data_if_t* handle) {
const struct I2sDataContext* context = (const struct I2sDataContext*) handle;
return context->is_open;
}
static int data_open(const audio_codec_data_if_t* handle, void* data_cfg, int cfg_size) {
(void) data_cfg;
(void) cfg_size;
struct I2sDataContext* context = (struct I2sDataContext*) handle;
context->is_open = true;
return ESP_CODEC_DEV_OK;
}
static int data_enable(const audio_codec_data_if_t* handle, esp_codec_dev_type_t dev_type, bool enable) {
struct I2sDataContext* context = (struct I2sDataContext*) handle;
// esp_codec_dev_close() calls this with enable=false but never calls set_fmt() again
// before the next open -- without releasing the channel here, a TDM/PDM RX channel
// (e.g. ES7210, a PDM mic) or a TX channel stays enabled and holding its GPIOs/DMA
// channel indefinitely after the app that opened it closes, starving other I2S/DMA
// users system-wide. Only release the direction this codec actually owns -- a single
// I2S controller can carry an independent TX user (e.g. a speaker amp) and RX user
// (e.g. a PDM mic) at once, and tearing down the whole controller on one closing would
// also kill the other's still-active stream.
if (!enable) {
bool isInput = (dev_type & ESP_CODEC_DEV_TYPE_IN) != 0;
bool isOutput = (dev_type & ESP_CODEC_DEV_TYPE_OUT) != 0;
if (isInput) {
i2s_controller_disable_direction(context->i2s_controller, true);
}
if (isOutput) {
i2s_controller_disable_direction(context->i2s_controller, false);
}
}
return ESP_CODEC_DEV_OK;
}
static int data_set_fmt(const audio_codec_data_if_t* handle, esp_codec_dev_type_t dev_type, esp_codec_dev_sample_info_t* fs) {
struct I2sDataContext* context = (struct I2sDataContext*) handle;
if (!context->is_open || fs == NULL) {
return ESP_CODEC_DEV_INVALID_ARG;
}
// PDM RX is a distinct setup path chosen at adapter-construction time (see
// audio_codec_adapter_new_i2s_pdm_data()), not inferred from fs here -- PDM mics are
// mono/stereo just like standard mode, so there's no sample-info heuristic that could
// tell them apart the way fs->channel > 2 does for TDM.
if (context->is_pdm) {
// ESP-IDF's PDM RX mode only supports 16-bit samples -- silently configuring the
// controller anyway would leave the caller's chosen bit depth misapplied.
if (fs->bits_per_sample != 16) {
return ESP_CODEC_DEV_NOT_SUPPORT;
}
// PDM RX is fixed at 2 slots (mono or stereo) -- anything else doesn't map onto
// I2S_SLOT_MODE_MONO/STEREO and would silently get treated as stereo otherwise.
if (fs->channel != 1 && fs->channel != 2) {
return ESP_CODEC_DEV_NOT_SUPPORT;
}
struct I2sPdmRxConfig pdm_config = {
.sample_rate_hz = fs->sample_rate,
.stereo = fs->channel == 2,
};
error_t error = i2s_controller_set_rx_pdm_config(context->i2s_controller, &pdm_config);
if (error != ERROR_NONE) {
return ESP_CODEC_DEV_DRV_ERR;
}
return ESP_CODEC_DEV_OK;
}
// Standard (stereo) mode and TDM mode are alternative setup paths for the same I2S
// port, not sequential steps -- set_config() always allocates+enables a TX/RX channel
// pair, and calling it before set_rx_tdm_config() leaves that pair's TX channel bound
// to the shared BCLK/WS/MCLK pins, so the TDM path's fresh i2s_new_channel() can't
// claim those pins ("occupied by i2s_driver" / "GPIO not usable" at runtime).
if ((dev_type & ESP_CODEC_DEV_TYPE_IN) != 0 && fs->channel > 2) {
struct I2sTdmRxConfig tdm_config = {
.sample_rate_hz = fs->sample_rate,
.mclk_multiple = (fs->mclk_multiple != 0) ? (uint32_t) fs->mclk_multiple : 256,
.bclk_div = 8,
.slot_count = fs->channel,
.bits_per_sample = fs->bits_per_sample,
.slot_bit_width = 0,
};
error_t error = i2s_controller_set_rx_tdm_config(context->i2s_controller, &tdm_config);
if (error != ERROR_NONE) {
return ESP_CODEC_DEV_DRV_ERR;
}
return ESP_CODEC_DEV_OK;
}
struct I2sConfig config = {
.communication_format = I2S_FORMAT_STAND_I2S,
.sample_rate = fs->sample_rate,
.bits_per_sample = fs->bits_per_sample,
.channel_left = 0,
.channel_right = (fs->channel > 1) ? 1 : I2S_CHANNEL_NONE,
};
if (i2s_controller_set_config(context->i2s_controller, &config) != ERROR_NONE) {
return ESP_CODEC_DEV_DRV_ERR;
}
return ESP_CODEC_DEV_OK;
}
static int data_read(const audio_codec_data_if_t* handle, uint8_t* data, int size) {
struct I2sDataContext* context = (struct I2sDataContext*) handle;
if (!context->is_open) {
return ESP_CODEC_DEV_WRONG_STATE;
}
size_t bytes_read = 0;
error_t error = i2s_controller_read(context->i2s_controller, data, (size_t) size, &bytes_read, I2S_TIMEOUT_TICKS);
if (error != ERROR_NONE) {
return ESP_CODEC_DEV_READ_FAIL;
}
return (int) bytes_read;
}
static int data_write(const audio_codec_data_if_t* handle, uint8_t* data, int size) {
struct I2sDataContext* context = (struct I2sDataContext*) handle;
if (!context->is_open) {
return ESP_CODEC_DEV_WRONG_STATE;
}
size_t bytes_written = 0;
error_t error = i2s_controller_write(context->i2s_controller, data, (size_t) size, &bytes_written, I2S_TIMEOUT_TICKS);
if (error != ERROR_NONE) {
return ESP_CODEC_DEV_WRITE_FAIL;
}
return (int) bytes_written;
}
static int data_close(const audio_codec_data_if_t* handle) {
struct I2sDataContext* context = (struct I2sDataContext*) handle;
context->is_open = false;
return ESP_CODEC_DEV_OK;
}
static const audio_codec_data_if_t* new_i2s_data(struct Device* i2s_controller, bool is_pdm) {
if (i2s_controller == NULL) {
return NULL;
}
struct I2sDataContext* context = (struct I2sDataContext*) calloc(1, sizeof(struct I2sDataContext));
if (context == NULL) {
return NULL;
}
context->i2s_controller = i2s_controller;
context->is_open = false;
context->is_pdm = is_pdm;
context->base.open = data_open;
context->base.is_open = data_is_open;
context->base.enable = data_enable;
context->base.set_fmt = data_set_fmt;
context->base.read = data_read;
context->base.write = data_write;
context->base.close = data_close;
return &context->base;
}
const audio_codec_data_if_t* audio_codec_adapter_new_i2s_data(struct Device* i2s_controller) {
return new_i2s_data(i2s_controller, false);
}
const audio_codec_data_if_t* audio_codec_adapter_new_i2s_pdm_data(struct Device* i2s_controller) {
return new_i2s_data(i2s_controller, true);
}
// Note: esp_codec_dev already provides audio_codec_delete_data_if() (calls ->close then frees);
// no adapter-specific cleanup is needed, so we don't redefine it here.
@@ -0,0 +1,136 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/drivers/audio_codec_adapters.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/drivers/gpio_descriptor.h>
#include <tactility/log.h>
#include <stdlib.h>
#define TAG "AudioCodecGpio"
struct GpioAdapterContext {
audio_codec_gpio_if_t base;
const struct GpioPinSpec* pins;
size_t pin_count;
struct GpioDescriptor** descriptors;
};
static struct GpioAdapterContext* g_context = NULL;
static struct GpioDescriptor* acquire_descriptor(int16_t gpio) {
if (g_context == NULL || gpio < 0 || (size_t) gpio >= g_context->pin_count) {
return NULL;
}
if (g_context->descriptors[gpio] != NULL) {
return g_context->descriptors[gpio];
}
const struct GpioPinSpec* spec = &g_context->pins[gpio];
if (spec->gpio_controller == NULL) {
return NULL;
}
struct GpioDescriptor* descriptor = gpio_descriptor_acquire(spec->gpio_controller, spec->pin, GPIO_OWNER_GPIO);
if (descriptor != NULL) {
gpio_descriptor_set_flags(descriptor, spec->flags);
g_context->descriptors[gpio] = descriptor;
}
return descriptor;
}
static int gpio_setup(int16_t gpio, audio_gpio_dir_t dir, audio_gpio_mode_t mode) {
struct GpioDescriptor* descriptor = acquire_descriptor(gpio);
if (descriptor == NULL) {
return ESP_CODEC_DEV_NOT_FOUND;
}
gpio_flags_t flags = GPIO_FLAG_NONE;
gpio_descriptor_get_flags(descriptor, &flags);
flags &= ~(GPIO_FLAG_DIRECTION_INPUT | GPIO_FLAG_DIRECTION_OUTPUT | GPIO_FLAG_PULL_UP | GPIO_FLAG_PULL_DOWN);
flags |= (dir == AUDIO_GPIO_DIR_OUT) ? GPIO_FLAG_DIRECTION_OUTPUT : GPIO_FLAG_DIRECTION_INPUT;
if ((mode & AUDIO_GPIO_MODE_PULL_UP) != 0) {
flags |= GPIO_FLAG_PULL_UP;
}
if ((mode & AUDIO_GPIO_MODE_PULL_DOWN) != 0) {
flags |= GPIO_FLAG_PULL_DOWN;
}
return (gpio_descriptor_set_flags(descriptor, flags) == ERROR_NONE) ? ESP_CODEC_DEV_OK : ESP_CODEC_DEV_DRV_ERR;
}
static int gpio_set(int16_t gpio, bool high) {
struct GpioDescriptor* descriptor = acquire_descriptor(gpio);
if (descriptor == NULL) {
return ESP_CODEC_DEV_NOT_FOUND;
}
return (gpio_descriptor_set_level(descriptor, high) == ERROR_NONE) ? ESP_CODEC_DEV_OK : ESP_CODEC_DEV_DRV_ERR;
}
// esp_codec_dev's gpio_if_t::get returns plain bool with no error channel, so a failed
// acquire and a legitimate low reading are indistinguishable to the caller; log so the
// failure is at least visible.
static bool gpio_get(int16_t gpio) {
struct GpioDescriptor* descriptor = acquire_descriptor(gpio);
if (descriptor == NULL) {
LOG_E(TAG, "gpio_get: failed to acquire descriptor for pin %d", gpio);
return false;
}
bool high = false;
gpio_descriptor_get_level(descriptor, &high);
return high;
}
const audio_codec_gpio_if_t* audio_codec_adapter_new_gpio(const struct GpioPinSpec* pins, size_t pin_count) {
if (pins == NULL || pin_count == 0) {
return NULL;
}
// esp_codec_dev's gpio_if_t has no per-call user context -- only a single global instance
// is supported (see g_context usage in acquire_descriptor()/gpio_setup/etc). A second
// call would silently replace it underneath whatever codec already holds the first
// pointer, so refuse rather than corrupt that codec's GPIO access.
if (g_context != NULL) {
return NULL;
}
struct GpioAdapterContext* context = (struct GpioAdapterContext*) calloc(1, sizeof(struct GpioAdapterContext));
if (context == NULL) {
return NULL;
}
context->descriptors = (struct GpioDescriptor**) calloc(pin_count, sizeof(struct GpioDescriptor*));
if (context->descriptors == NULL) {
free(context);
return NULL;
}
context->pins = pins;
context->pin_count = pin_count;
context->base.setup = gpio_setup;
context->base.set = gpio_set;
context->base.get = gpio_get;
g_context = context;
return &context->base;
}
// Note: esp_codec_dev's generic audio_codec_delete_gpio_if() only frees the struct — it doesn't
// know about our descriptor array, so we provide our own cleanup under a non-colliding name.
int audio_codec_adapter_delete_gpio(const audio_codec_gpio_if_t* gpio_if) {
struct GpioAdapterContext* context = (struct GpioAdapterContext*) gpio_if;
for (size_t i = 0; i < context->pin_count; i++) {
if (context->descriptors[i] != NULL) {
gpio_descriptor_release(context->descriptors[i]);
}
}
free(context->descriptors);
if (g_context == context) {
g_context = NULL;
}
free(context);
return ESP_CODEC_DEV_OK;
}