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
+11
View File
@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(audio-codec-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel esp_codec_dev
)
@@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+19
View File
@@ -0,0 +1,19 @@
# Audio codec module
Defines `AUDIO_CODEC_TYPE` and the codec-agnostic `AudioCodecApi` (open/close/
read/write/volume/mute/native-rate/native-channels/capabilities) that every
audio codec driver in this repo implements (ES8311, ES8388, ES7210, AW88298,
the dummy I2S amps, the PDM mic). This is intentionally low-level: no
resampling or mixing, one codec device per chip. Most app code should go
through `audio_stream` (see `Drivers/audio-stream-module`) instead of talking
to a codec device directly.
Also provides `audio_codec_adapters.h`: glue that lets a codec driver build an
`esp_codec_dev` control/data/GPIO interface (`audio_codec_ctrl_if_t` /
`audio_codec_data_if_t` / `audio_codec_gpio_if_t`) directly from Tactility's
own I2C controller, I2S controller, and GPIO pin devices, so a codec driver
only ever needs `device_get_parent()`/`device_find_by_name()` plus a handful
of devicetree properties -- it never touches the platform I2C/I2S/GPIO APIs
directly.
License: [Apache v2.0](LICENSE-Apache-2.0.md)
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <tactility/drivers/gpio.h>
#include "audio_codec_data_if.h"
#include "audio_codec_ctrl_if.h"
#include "audio_codec_gpio_if.h"
struct Device;
/**
* @brief Creates an esp_codec_dev data interface backed by a Tactility I2S controller device.
* @param[in] i2s_controller the I2S controller device to read/write audio frames from/to
* @return the data interface, or NULL on failure. Free with audio_codec_delete_data_if().
*/
const audio_codec_data_if_t* audio_codec_adapter_new_i2s_data(struct Device* i2s_controller);
/**
* @brief Creates an esp_codec_dev data interface backed by a Tactility I2S controller device,
* configured for PDM RX mode (e.g. for a PDM MEMS microphone such as the SPM1423).
* @param[in] i2s_controller the I2S controller device to read PDM audio frames from. Must be
* backed by I2S controller 0 -- PDM RX is hardware-restricted to that controller.
* @return the data interface, or NULL on failure. Free with audio_codec_delete_data_if().
*/
const audio_codec_data_if_t* audio_codec_adapter_new_i2s_pdm_data(struct Device* i2s_controller);
/**
* @brief Creates an esp_codec_dev control interface backed by a Tactility I2C controller device.
* @param[in] i2c_controller the I2C controller device the codec chip is attached to
* @param[in] address the 7-bit I2C address of the codec chip
* @return the control interface, or NULL on failure. Free with audio_codec_delete_ctrl_if().
*/
const audio_codec_ctrl_if_t* audio_codec_adapter_new_i2c_ctrl(struct Device* i2c_controller, uint8_t address);
/**
* @brief Creates an esp_codec_dev GPIO interface backed by Tactility GpioPinSpec descriptors.
*
* The codec chip driver references GPIOs by small integer index (0, 1, ...); this adapter
* maps those indices to GpioPinSpec entries supplied at creation time (e.g. reset/PA-enable lines
* declared in devicetree). Indices outside the supplied array are treated as no-ops.
*
* @param[in] pins array of pin specs, indexed by the small integer GPIO id used by the codec chip driver
* @note The caller must keep `pins` valid for the lifetime of the returned interface (until
* audio_codec_adapter_delete_gpio() is called); the adapter stores the pointer, not a copy.
* @param[in] pin_count number of entries in `pins`
* @note Only a single instance is supported at a time; a second call before deleting the
* first returns NULL.
* @return the GPIO interface, or NULL on failure. Free with audio_codec_adapter_delete_gpio()
* (esp_codec_dev's generic audio_codec_delete_gpio_if() does not release our GPIO descriptors).
*/
const audio_codec_gpio_if_t* audio_codec_adapter_new_gpio(const struct GpioPinSpec* pins, size_t pin_count);
/**
* @brief Frees a GPIO interface created by audio_codec_adapter_new_gpio(), releasing its GPIO descriptors.
*/
int audio_codec_adapter_delete_gpio(const audio_codec_gpio_if_t* gpio_if);
#ifdef __cplusplus
}
#endif
@@ -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;
}