Audio System + Drivers (#562)
This commit is contained in:
@@ -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_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user