Audio prepare for Arduino3 (#19637)

This commit is contained in:
s-hadinger 2023-10-02 09:18:53 +02:00 committed by GitHub
parent 0be4613cf9
commit 795a194d65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1189 additions and 621 deletions

View File

@ -97,6 +97,35 @@ BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_init, "+.p", "");
extern void* be_audio_input_i2s_deinit(void* instance); extern void* be_audio_input_i2s_deinit(void* instance);
BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_deinit, "", "."); BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_deinit, "", ".");
// AudioInputI2S.begin() -> bool
extern int be_audio_input_i2s_begin(bvm *vm, void* in);
BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_begin, "b", "@.");
// AudioInputI2S.stop() -> bool
extern int be_audio_input_i2s_stop(void* in);
BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_stop, "b", ".");
extern int be_audio_input_i2s_read_bytes(bvm *vm);
// AudioInputI2S.set_gain(gain:real) -> bool
extern int be_audio_input_set_gain(void* in, float gain);
BE_FUNC_CTYPE_DECLARE(be_audio_input_set_gain, "b", ".f");
// AudioInputI2S.get_rate() -> int
extern int be_audio_input_i2s_get_rate(void* in);
BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_get_rate, "i", ".");
// AudioInputI2S.get_bits_per_sample() -> int
extern int be_audio_input_i2s_get_bits_per_sample(void* in);
BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_get_bits_per_sample, "i", ".");
// AudioInputI2S.get_channels() -> int
extern int be_audio_input_i2s_get_channels(void* in);
BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_get_channels, "i", ".");
// AudioInputI2S.get_gain() -> real
extern float be_audio_input_i2s_get_gain(void* in);
BE_FUNC_CTYPE_DECLARE(be_audio_input_i2s_get_gain, "f", ".");
#include "be_fixed_be_class_AudioOutputI2S.h" #include "be_fixed_be_class_AudioOutputI2S.h"
#include "be_fixed_be_class_AudioGenerator.h" #include "be_fixed_be_class_AudioGenerator.h"
@ -162,6 +191,17 @@ class be_class_AudioInputI2S (scope: global, name: AudioInputI2S, strings: weak)
.p, var .p, var
init, ctype_func(be_audio_input_i2s_init) init, ctype_func(be_audio_input_i2s_init)
deinit, ctype_func(be_audio_input_i2s_deinit) deinit, ctype_func(be_audio_input_i2s_deinit)
begin, ctype_func(be_audio_input_i2s_begin)
stop, ctype_func(be_audio_input_i2s_stop)
read_bytes, func(be_audio_input_i2s_read_bytes)
get_rate, ctype_func(be_audio_input_i2s_get_rate)
get_bits_per_sample, ctype_func(be_audio_input_i2s_get_bits_per_sample)
get_channels, ctype_func(be_audio_input_i2s_get_channels)
get_gain, ctype_func(be_audio_input_i2s_get_gain)
set_gain, ctype_func(be_audio_input_set_gain)
} }
@const_object_info_end */ @const_object_info_end */

View File

@ -0,0 +1,168 @@
/*
xdrv_42_0_i2s_0_config_idf51.ino - Simplified Audio library, general configuration
Copyright (C) 2021 Gerhard Mutz, Theo Arends, Staars, Stephan Hadinger
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#if defined(ESP32) && ESP_IDF_VERSION_MAJOR >= 5
#ifdef USE_I2S_AUDIO
#include "driver/i2s_std.h"
#include "driver/i2s_pdm.h"
#include "driver/gpio.h"
#include "soc/soc_caps.h"
#include "AudioFileSourcePROGMEM.h"
#include "AudioFileSourceID3.h"
#include "AudioGeneratorMP3.h"
#include <ESP8266SAM.h>
#include "AudioFileSourceFS.h"
#include "AudioGeneratorTalkie.h"
#include "AudioFileSourceICYStream.h"
#include "AudioFileSourceBuffer.h"
#include "AudioGeneratorAAC.h"
#include <layer3.h>
#include <types.h>
/*********************************************************************************************\
* Driver Settings in memory
\*********************************************************************************************/
// I2S communication mode
enum : int8_t {
I2S_MODE_STD = 0, // I2S mode standard
I2S_MODE_PDM = 1, // I2S mode PDM
I2S_MODE_TDM = 2, // I2S mode TDM
I2S_MODE_DAC = 3, // Using internal DAC - only available on ESP32
};
// I2S slot mask (left, right, both)
enum : int8_t {
I2S_SLOT_NOCHANGE = 0, // don't change default
I2S_SLOT_LEFT = 1, // left
I2S_SLOT_RIGHT = 2, // right
I2S_SLOT_BOTH = 3, // both
};
// I2S slot configuration
enum : int8_t {
I2S_SLOT_MSB = 0, // MSB
I2S_SLOT_PCM = 1, // PCM
I2S_SLOT_PHILIPS = 2, // Philips
};
#define I2S_SLOTS 2
typedef struct{
struct{
uint8_t version = 0; // B00
// runtime options, will be saved but ignored on setting read
bool duplex = 0; // B01 - depends on GPIO setting and SOC caps, DIN and DOUT on same port in GPIO means -> try to use duplex if possible
bool tx = 0; // B02 - depends on GPIO setting
bool rx = 0; // B03 - depends on GPIO setting
bool exclusive = 0; // B04 - depends on GPIO setting, if WS is shared between 2 ports, drivers needs to be reinstalled before being used (Yuck... but we don't have a choice)
bool mclk_inv[I2S_SLOTS] = {0}; // B05-06 - invert mclk
bool bclk_inv[I2S_SLOTS] = {0}; // B07-08 - invert bclk
bool ws_inv[I2S_SLOTS] = {0}; // B09-0A - invert ws
uint8_t spare[5]; // B0B-0F
} sys;
struct {
uint32_t sample_rate = 16000; // B00-03
uint8_t gain = 10; // B04 - was `volume`
uint8_t mode = I2S_MODE_STD; // B05 - I2S mode standard, PDM, TDM, DAC
uint8_t slot_mask = I2S_SLOT_NOCHANGE;// B06 - slot mask
uint8_t slot_config = I2S_SLOT_MSB;// B07 - slot configuration MSB = 0, PCM = 1, PHILIPS = 2
uint8_t channels = 2; // B08 - mono/stereo - 1 is added for both
bool apll = 1; // B09 - will be ignored on unsupported SOC's
// device specific
uint8_t mp3_preallocate = 0; // B0A - preallocate MP3 buffer for mp3 playing
uint8_t codec = 0; // B0B - S3 box only, unused for now
uint8_t spare[4]; // B0C-0F
} tx;
struct {
uint32_t sample_rate = 16000; // B00-03
uint8_t gain = 30; // B04
uint8_t mode = I2S_MODE_PDM; // B05 - I2S mode standard, PDM, TDM, DAC
uint8_t slot_mask = I2S_SLOT_NOCHANGE;// B06 - slot mask
uint8_t slot_config = I2S_SLOT_MSB;// B07 - slot configuration MSB = 0, PCM = 1, PHILIPS = 2
uint8_t channels = 1; // B08 - mono/stereo - 1 is added for both
bool apll = 1; // B09 - will be ignored on unsupported SOC's
// device specific
uint8_t codec = 0; // B0A - S3 box only, unused for now
uint8_t mp3_preallocate = 0; // B0B - will be ignored without PS-RAM
uint8_t spare[4]; // B0C-0F
} rx;
} tI2SSettings;
typedef union {
uint8_t data;
struct {
uint8_t master : 1;
uint8_t enabled : 1;
uint8_t swap_mic : 1;
uint8_t mode : 2;
};
} BRIDGE_MODE;
class TasmotaI2S;
struct AUDIO_I2S_t {
tI2SSettings *Settings;
i2s_chan_handle_t rx_handle = nullptr;
AudioGeneratorMP3 *mp3 = nullptr;
AudioFileSourceFS *file = nullptr;
TasmotaI2S *out = nullptr; // instance used for I2S output, or `nullptr` if none
TasmotaI2S *in = nullptr; // instance used for I2S input, or `nullptr` if none (it can be the same as `out` in case of full duplex)
AudioFileSourceID3 *id3 = nullptr;
AudioGeneratorMP3 *decoder = NULL;
void *mp3ram = NULL;
TaskHandle_t mp3_task_handle;
TaskHandle_t mic_task_handle;
char mic_path[32];
uint8_t mic_stop;
int8_t mic_error;
bool use_stream = false;
// SHINE
uint32_t recdur;
uint8_t stream_active;
uint8_t stream_enable;
WiFiClient client;
ESP8266WebServer *MP3Server;
// I2S_BRIDGE
BRIDGE_MODE bridge_mode;
WiFiUDP i2s_bridge_udp;
WiFiUDP i2s_bridgec_udp;
IPAddress i2s_bridge_ip;
TaskHandle_t i2s_bridge_h;
int8_t ptt_pin = -1;
} audio_i2s;
#endif // USE_I2S_AUDIO
#endif // defined(ESP32) && ESP_IDF_VERSION_MAJOR >= 5

View File

@ -0,0 +1,603 @@
/*
xdrv_42_0_i2s_0_lib_idf51.ino - Simplified Audio library, core class
Copyright (C) 2021 Gerhard Mutz, Theo Arends, Staars, Stephan Hadinger
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#if defined(ESP32) && ESP_IDF_VERSION_MAJOR >= 5
#ifdef USE_I2S_AUDIO
/*********************************************************************************************\
* Driver Settings in memory
\*********************************************************************************************/
/*********************************************************************************************\
* Reminder of esp_err codes
*
* #define ESP_OK 0 - esp_err_t value indicating success (no error)
* #define ESP_FAIL -1 - Generic esp_err_t code indicating failure
*
* #define ESP_ERR_NO_MEM 0x101 257 - Out of memory
* #define ESP_ERR_INVALID_ARG 0x102 258 - Invalid argument
* #define ESP_ERR_INVALID_STATE 0x103 259 - Invalid state
* #define ESP_ERR_INVALID_SIZE 0x104 260 - Invalid size
* #define ESP_ERR_NOT_FOUND 0x105 261 - Requested resource not found
* #define ESP_ERR_NOT_SUPPORTED 0x106 262 - Operation or feature not supported
* #define ESP_ERR_TIMEOUT 0x107 263 - Operation timed out
* #define ESP_ERR_INVALID_RESPONSE 0x108 264 - Received response was invalid
* #define ESP_ERR_INVALID_CRC 0x109 265 - CRC or checksum was invalid
* #define ESP_ERR_INVALID_VERSION 0x10A 266 - Version was invalid
* #define ESP_ERR_INVALID_MAC 0x10B 267 - MAC address was invalid
* #define ESP_ERR_NOT_FINISHED 0x10C 268 - Operation has not fully completed
\*********************************************************************************************/
/*********************************************************************************************\
* This is the central class to acccess I2S in (rx) or out (tx)
*
* It inherits from AudioOutput so it can be used as output instance for ESP8266Audio library
*
* It also supports microphone input
*
\*********************************************************************************************/
class TasmotaI2S : public AudioOutput
{
public:
// Constructor takes no parameter, everything is configured from template and config file
TasmotaI2S() {
// set some defaults
hertz = 16000;
bps = I2S_DATA_BIT_WIDTH_16BIT;
channels = 2;
gainF2P6 = 32; // equivalent of 0.5
}
~TasmotaI2S() {
this->stop();
}
// Settings
void setPinout(int32_t bclk, int32_t ws, int32_t dout, int32_t mclk, int32_t din,
bool mclk_inv = false, bool bclk_inv = false, bool ws_inv = false, bool apll = false);
void setSlotConfig(i2s_port_t i2s_port, uint8_t tx_slot_config, uint8_t rx_slot_config,
uint8_t tx_slot_mask, uint8_t rx_slot_mask) {
_i2s_port = i2s_port;
_tx_slot_config = tx_slot_config;
_rx_slot_config = rx_slot_config;
}
void setRxFreq(uint16_t freq) { _rx_freq = freq; }
// ------------------------------------------------------------------------------------------
// Setters for configuration parameters
//
// TODO: not sure we still need them since all this should be set at initialiation
// ------------------------------------------------------------------------------------------
virtual bool SetBitsPerSample(int bits) {
AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: SetBitsPerSample: %i"), bits);
if ( (bits != 16) && (bits != 8) ) { return false; }
this->bps = bits;
return true;
}
virtual bool SetChannels(int channels) {
AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: SetChannels: %i"), channels);
if ((channels < 1) || (channels > 2)) { return false; }
if (channels == (int)this->channels) { return true; }
this->channels = channels;
return true;
}
virtual bool SetRate(int hz) {
AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: SetRate: %i was %i on=%i"), hz, this->hertz, _i2s_on);
if (hz == (int) this->hertz) { return true; }
this->hertz = hz;
if (_i2s_on) {
int result = updateClockConfig();
}
return true;
}
virtual bool SetGain(float f) {
AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: SetGain: %_f"), &f);
return AudioOutput::SetGain(f);
}
// ------------------------------------------------------------------------------------------
// Getters
inline bool isDuplex(void) const { return _tx_configured && _rx_configured; }
inline bool getExclusive(void) const {return _exclusive; }
inline uint8_t getTxMode(void) const { return _tx_mode; }
inline uint8_t getTxChannels(void) const { return channels; }
inline bool getTxRunning(void) const { return _tx_running; }
inline i2s_chan_handle_t getTxHandle(void) const { return _tx_handle; }
inline uint8_t getRxMode(void) const { return _rx_mode; }
inline uint8_t getRxBitsPerSample(void) const { return 16; } // TODO - hardcoded to 16 bits for recording
inline uint16_t getRxRate(void) const { return _rx_freq; }
inline uint8_t getRxChannels(void) const { return _rx_channels; }
inline float getRxGain(void) const { return 1.0f; } // TODO - hardcoded to 1.0 for recording
inline bool getRxRunning(void) const { return _rx_running; }
inline i2s_chan_handle_t getRxHandle(void) const { return _rx_handle; }
// ------------------------------------------------------------------------------------------
// Setters
inline void setExclusive(bool exclusive) { _exclusive = exclusive; }
inline void setTxMode(uint8_t mode) { _tx_mode = mode; }
inline void setTxChannels(uint8_t channels) { SetChannels(channels); }
inline void setTxRunning(bool running) { _tx_running = running; }
inline void setRxMode(uint8_t mode) { _rx_mode = mode; }
inline void setRxChannels(uint8_t channels) { _rx_channels = channels; }
inline void setRxRunning(bool running) { _rx_running = running; }
// ------------------------------------------------------------------------------------------
// AudioOutput has inconsistent case for methods, provide consistent setters for super class
inline void setRate(int hz) { SetRate(hz); }
inline void setBitsPerSample(int bits) { SetBitsPerSample(bits); }
inline void setChannels(int chan) { SetChannels(chan); }
inline void setGain(float f) { SetGain(f); }
bool begin(void);
bool stop(void);
bool ConsumeSample(int16_t sample[2]);
bool startI2SChannel(bool tx, bool rx);
int updateClockConfig(void);
// The following is now the preferred function
// and allows to send multiple samples at once
//
// Max 128 samples, it is clipped otherwise
// Returns: the number of samples actually consumed
// or -1 if an error occured
//
// The call is non blocking and does not wait
int32_t consumeSamples(int16_t *samples, size_t count);
// ------------------------------------------------------------------------------------------
// Microphone related methods
uint32_t I2sMicInit(void);
void I2sMicDeinit(void);
protected:
void loadSettings(void); // load all settings from Settings file and template - the goal is to have zero touch
protected:
bool _i2s_on = false; // is I2S audio active
bool _exclusive = false; // in exclusive mode, stopping this instance needs to uninstall driver, and reinstall for next use
i2s_port_t _i2s_port = I2S_NUM_AUTO; // I2S port, I2S_NUM_0/I2S_NUM_1/I2S_NUM_AUTO
// local copy of useful settings for audio
// TX
bool _tx_configured = false; // true = configured, false = not configured
uint8_t _tx_mode = I2S_MODE_STD; // I2S_MODE_STD / I2S_MODE_PDM / I2S_MODE_TDM / I2S_MODE_DAC
uint8_t _tx_slot_mask = I2S_SLOT_NOCHANGE;
bool _tx_running = false; // true = enabled, false = disabled
// uint8_t _tx_channels = 2; // number of channels, 1 = mono, 2 = stereo -- `channels`
i2s_chan_handle_t _tx_handle = nullptr; // I2S channel handle, automatically computed
uint8_t _tx_slot_config = I2S_SLOT_MSB;// I2S slot configuration
// RX
bool _rx_configured = false; // true = configured, false = not configured
uint8_t _rx_mode = I2S_MODE_STD; // I2S_MODE_STD / I2S_MODE_PDM / I2S_MODE_TDM / I2S_MODE_DAC
uint8_t _rx_slot_mask = I2S_SLOT_NOCHANGE;
bool _rx_running = false; // true = enabled, false = disabled
uint8_t _rx_channels = 2; // number of channels, 1 = mono, 2 = stereo
i2s_chan_handle_t _rx_handle = nullptr; // I2S channel handle, automatically computed
uint8_t _rx_slot_config = I2S_SLOT_MSB;// I2S slot configuration
uint16_t _rx_freq = 16000; // I2S Rx sampling frequency in Hz
// GPIOs for I2S
gpio_num_t _gpio_mclk = GPIO_NUM_NC; // GPIO for master clock
gpio_num_t _gpio_bclk = GPIO_NUM_NC; // GPIO for bit clock
gpio_num_t _gpio_ws = GPIO_NUM_NC; // GPIO for word select
gpio_num_t _gpio_dout = GPIO_NUM_NC; // GPIO for data out
gpio_num_t _gpio_din = GPIO_NUM_NC; // GPIO for data in
bool _gpio_mclk_inv = false; // invert master clock
bool _gpio_bclk_inv = false; // invert bit clock
bool _gpio_ws_inv = false; // invert word select
bool _apll = false; // use APLL instead of PLL
};
// ------------------------------------------------------------------------------------------
// Methods
// ------------------------------------------------------------------------------------------
void TasmotaI2S::setPinout(int32_t bclk, int32_t ws, int32_t dout, int32_t mclk, int32_t din,
bool mclk_inv, bool bclk_inv, bool ws_inv, bool apll) {
_gpio_mclk = (gpio_num_t) mclk;
_gpio_bclk = (gpio_num_t) bclk;
_gpio_ws = (gpio_num_t) ws;
_gpio_dout = (gpio_num_t) dout;
_gpio_din = (gpio_num_t) din;
_gpio_mclk_inv = mclk_inv;
_gpio_bclk_inv = bclk_inv;
_gpio_ws_inv = ws_inv;
_apll = apll;
_tx_configured = (_gpio_dout != GPIO_NUM_NC);
_rx_configured = (_gpio_din != GPIO_NUM_NC);
AddLog(LOG_LEVEL_DEBUG, "I2S: setPinout: gpios[%i,%i,%i,%i,%i] inv[%i,%i,%i] apll:%i _tx_configured:%i _rx_configured:%i",
_gpio_mclk, _gpio_bclk, _gpio_ws, _gpio_dout, _gpio_din,
_gpio_mclk_inv, _gpio_bclk_inv, _gpio_ws_inv, _apll,
_tx_configured, _rx_configured);
}
bool TasmotaI2S::begin() {
AddLog(LOG_LEVEL_DEBUG, "I2S: begin _tx_running:%i _i2s_on:%i", _tx_running, _i2s_on);
if (_tx_running) { return true; }
// if (!_i2s_on) {
// if ((!_rx_configured || !_tx_configured) && _rx_configured) { // not duplex -- TODO ?
// this->startI2SChannel();
// }
// }
int result = i2s_channel_enable(_tx_handle);
if (result != 0){
AddLog(LOG_LEVEL_INFO, "I2S: Could not enable i2s_channel: %i", result);
return false;
}
_tx_running = true;
AddLog(LOG_LEVEL_DEBUG, "I2S: begin _tx_running succeeded");
return true;
}
bool TasmotaI2S::stop() {
i2s_channel_disable(_tx_handle);
if ((!_rx_configured || !_tx_configured) && _rx_configured) { // not duplex -- TODO ?
i2s_del_channel(_tx_handle);
_i2s_on = false;
AddLog(LOG_LEVEL_INFO, "I2S: stop: I2S channel disabled");
}
_tx_running = false;
return true;
}
bool TasmotaI2S::ConsumeSample(int16_t sample[2]) {
return consumeSamples(sample, 1);
}
int32_t TasmotaI2S::consumeSamples(int16_t *samples, size_t count) {
if (!_tx_running) { return -1; }
if (count == 0) { return 0; }
if (count > 128) { count = 128; }
int16_t ms[count*2];
for (int32_t i = 0; i < count; i++) {
int16_t left = samples[i*2 + LEFTCHANNEL];
int16_t right = samples[i*2 + RIGHTCHANNEL];
if (channels == 1) { // if mono, average the two samples
// Average the two samples and overwrite
int32_t ttl = left + right;
left = right = (ttl>>1) & 0xffff;
}
if (bps == 8) {
left = (((int16_t)(left & 0xff)) - 128) << 8;
right = (((int16_t)(right & 0xff)) - 128) << 8;
}
if (_tx_mode == I2S_MODE_DAC) {
left = Amplify(left) + 0x8000;
right = Amplify(right) + 0x8000;
} else {
left = Amplify(left);
right = Amplify(right);
}
ms[i*2 + LEFTCHANNEL] = left;
ms[i*2 + RIGHTCHANNEL] = right;
}
// AddLog(LOG_LEVEL_DEBUG, "I2S: consumeSamples: left=%i right=%i", ms[0], ms[1]);
size_t i2s_bytes_written;
esp_err_t err = i2s_channel_write(_tx_handle, ms, sizeof(ms), &i2s_bytes_written, 0);
if (err && err != ESP_ERR_TIMEOUT) {
AddLog(LOG_LEVEL_INFO, "I2S: Could not write samples (count=%i): %i", count, err);
return -1;
}
return i2s_bytes_written;
}
// Initialize I2S channel
// return `true` if successful, `false` otherwise
bool TasmotaI2S::startI2SChannel(bool tx, bool rx) {
if (!tx) { _tx_configured = false; }
if (!rx) { _rx_configured = false; }
if (!_tx_configured && !_rx_configured) { return false; } // nothing configured
esp_err_t err = ESP_OK;
gpio_num_t _DIN = I2S_GPIO_UNUSED; // no input pin by default
if (_tx_configured) {
// default dma_desc_num = 6 (DMA buffers), dma_frame_num = 240 (frames per buffer)
i2s_chan_config_t tx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_1, I2S_ROLE_MASTER);
AddLog(LOG_LEVEL_DEBUG, "I2S: tx_chan_cfg id:%i role:%i dma_desc_num:%i dma_frame_num:%i auto_clear:%i",
tx_chan_cfg.id, tx_chan_cfg.role, tx_chan_cfg.dma_desc_num, tx_chan_cfg.dma_frame_num, tx_chan_cfg.auto_clear);
if (_tx_configured && _rx_configured) {
_DIN = (gpio_num_t)_gpio_din;
err = i2s_new_channel(&tx_chan_cfg, &_tx_handle, &audio_i2s.out->_rx_handle);
} else{
err = i2s_new_channel(&tx_chan_cfg, &_tx_handle, NULL);
}
AddLog(LOG_LEVEL_DEBUG, "I2S: i2s_new_channel err:%i", err);
// by default we configure for MSB 2 slots `I2S_STD_MSB_SLOT_DEFAULT_CONFIG`
i2s_std_config_t tx_std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(hertz),
.slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t)bps, (i2s_slot_mode_t)channels),
.gpio_cfg = {
.mclk = _gpio_mclk,
.bclk = _gpio_bclk,
.ws = _gpio_ws,
.dout = _gpio_dout,
.din = _DIN,
.invert_flags = {
.mclk_inv = _gpio_mclk_inv,
.bclk_inv = _gpio_bclk_inv,
.ws_inv = _gpio_ws_inv,
}
}
};
// change configuration if we are using PCM or PHILIPS
if (_tx_slot_config == I2S_SLOT_PCM) { // PCM
tx_std_cfg.slot_cfg = I2S_STD_PCM_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t)bps, (i2s_slot_mode_t)channels);
} else if (_tx_slot_config == I2S_SLOT_PHILIPS) { // PHILIPS
tx_std_cfg.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t)bps, (i2s_slot_mode_t)channels);
}
if (_tx_slot_mask != I2S_SLOT_NOCHANGE) { tx_std_cfg.slot_cfg.slot_mask = (i2s_std_slot_mask_t)_tx_slot_mask; }
// AddLog(LOG_LEVEL_INFO, ">>>: I2S tx_chan_cfg=%*_H", sizeof(tx_chan_cfg), &tx_chan_cfg);
// AddLog(LOG_LEVEL_INFO, ">>>: I2S tx_std_cfg=%*_H", sizeof(tx_std_cfg), &tx_std_cfg);
err = i2s_channel_init_std_mode(_tx_handle, &tx_std_cfg);
AddLog(LOG_LEVEL_DEBUG, "I2S: TX channel bits:%i channels:%i hertz:%i initialized err=0x%04X", bps, channels, hertz, err);
if (err != ERR_OK) {
_i2s_on = false;
return false;
}
_i2s_on = true;
if (_rx_configured) { // full duplex mode
err = i2s_channel_init_std_mode(audio_i2s.out->_rx_handle, &tx_std_cfg);
AddLog(LOG_LEVEL_DEBUG, "I2S: i2s_channel_init_std_mode err:%i", err);
AddLog(LOG_LEVEL_DEBUG, "I2S: RX channel added in full duplex mode");
}
} // if (tx)
// configure Rx Microphone
if (_rx_configured && !_tx_configured) { // if Microphone and not duplex
gpio_num_t clk_gpio;
i2s_slot_mode_t slot_mode = (_rx_channels == 1) ? I2S_SLOT_MODE_MONO : I2S_SLOT_MODE_STEREO;
AddLog(LOG_LEVEL_DEBUG, "I2S: mic init rx_channels:%i rx_running:%i rx_handle:%p", slot_mode, _rx_running, _rx_handle);
if (_tx_configured && _rx_running) { // duplex mode, mic was already initialized
AddLog(LOG_LEVEL_DEBUG, "I2S: mic init exit Rx already enabled");
return 0; // no need to en- or disable when in full duplex mode and already initialized
}
i2s_chan_config_t rx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
AddLog(LOG_LEVEL_DEBUG, "I2S: rx_chan_cfg id:%i role:%i dma_desc_num:%i dma_frame_num:%i auto_clear:%i",
rx_chan_cfg.id, rx_chan_cfg.role, rx_chan_cfg.dma_desc_num, rx_chan_cfg.dma_frame_num, rx_chan_cfg.auto_clear);
err = i2s_new_channel(&rx_chan_cfg, NULL, &_rx_handle);
AddLog(LOG_LEVEL_DEBUG, "I2S: mic init i2s_new_channel err=%i", err);
switch (_rx_mode){
case I2S_MODE_PDM:
{
i2s_pdm_rx_config_t rx_pdm_cfg = {
.clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG(_rx_freq),
/* The default mono slot is the left slot (whose 'select pin' of the PDM microphone is pulled down) */
.slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, (i2s_slot_mode_t)_rx_channels),
.gpio_cfg = {
.clk = _gpio_ws,
.din = _gpio_din,
.invert_flags = {
.clk_inv = _gpio_ws_inv,
},
},
};
// if (_rx_slot_mask != I2S_SLOT_NOCHANGE) { rx_pdm_cfg.slot_cfg.slot_mask = (i2s_pdm_slot_mask_t)_rx_slot_mask; }
// AddLog(LOG_LEVEL_DEBUG, "I2S: rx_pdm_cfg clk_cfg sample_rate_hz:%i clk_src:%i mclk_multiple:%i dn_sample_mode:%i",
// rx_pdm_cfg.clk_cfg.sample_rate_hz, rx_pdm_cfg.clk_cfg.clk_src, rx_pdm_cfg.clk_cfg.mclk_multiple, rx_pdm_cfg.clk_cfg.dn_sample_mode);
// AddLog(LOG_LEVEL_DEBUG, "I2S: rx_pdm_cfg slot_cfg data_bit_width:%i slot_bit_width:%i slot_mode:%i slot_mask:%i",
// rx_pdm_cfg.slot_cfg.data_bit_width, rx_pdm_cfg.slot_cfg.slot_bit_width, rx_pdm_cfg.slot_cfg.slot_mode, rx_pdm_cfg.slot_cfg.slot_mask);
// AddLog(LOG_LEVEL_DEBUG, "I2S: rx_pdm_cfg gpio_cfg clk:%i din:%i clk_inv:%i",
// rx_pdm_cfg.gpio_cfg.clk, rx_pdm_cfg.gpio_cfg.din, rx_pdm_cfg.gpio_cfg.invert_flags.clk_inv);
err = i2s_channel_init_pdm_rx_mode(_rx_handle, &rx_pdm_cfg);
AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: RX channel in PDM mode, CLK: %i, DIN: %i, 16 bit width, %i channel(s), err code: 0x%04X"),
_gpio_ws, _gpio_din, _rx_channels, err);
_i2s_on = true;
}
break;
case I2S_MODE_STD:
{
i2s_std_config_t rx_std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(_rx_freq),
.slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, slot_mode),
.gpio_cfg = {
.mclk = (gpio_num_t)Pin(GPIO_I2S_MCLK),
.bclk = (gpio_num_t)Pin(GPIO_I2S_BCLK),
.ws = (gpio_num_t)Pin(GPIO_I2S_WS),
.dout = I2S_GPIO_UNUSED,
.din = (gpio_num_t)Pin(GPIO_I2S_DIN),
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
if (_rx_slot_mask != I2S_SLOT_NOCHANGE) { rx_std_cfg.slot_cfg.slot_mask = (i2s_std_slot_mask_t)_rx_slot_mask; }
err = i2s_channel_init_std_mode(audio_i2s.rx_handle, &rx_std_cfg);
AddLog(LOG_LEVEL_DEBUG, "I2S: RX i2s_channel_init_std_mode err:%i", err);
AddLog(LOG_LEVEL_DEBUG, "I2S: RX channel in standard mode with 16 bit width on %i channel(s) initialized", slot_mode);
_i2s_on = true;
}
break;
default:
AddLog(LOG_LEVEL_INFO, "I2S: invalid rx mode=%i", _rx_mode);
}
}
return true;
}
int TasmotaI2S::updateClockConfig(void) {
i2s_channel_disable(_tx_handle);
i2s_std_clk_config_t clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(hertz);
#ifdef SOC_I2S_SUPPORTS_APLL
if (_apll) {
clk_cfg.clk_src = I2S_CLK_SRC_APLL;
}
#endif
int result = i2s_channel_reconfig_std_clock(_tx_handle, &clk_cfg );
if (_tx_running) { i2s_channel_enable(_tx_handle); }
AddLog(LOG_LEVEL_DEBUG, "I2S: Updating clock config");
return result;
}
/*********************************************************************************************\
* microphone related functions
\*********************************************************************************************/
uint32_t TasmotaI2S::I2sMicInit(void) {
if (!_rx_configured) { return 0; } // nothing configured
esp_err_t err = ESP_OK;
gpio_num_t clk_gpio;
i2s_slot_mode_t slot_mode = (_rx_channels == 1) ? I2S_SLOT_MODE_MONO : I2S_SLOT_MODE_STEREO;
AddLog(LOG_LEVEL_DEBUG, "I2S: mic init rx_channels:%i rx_running:%i rx_handle:%p", slot_mode, _rx_running, _rx_handle);
// if (_tx_configured && _rx_running) { // duplex mode, mic was already initialized
// AddLog(LOG_LEVEL_DEBUG, "I2S: mic init exit Rx already enabled");
// return 0; // no need to en- or disable when in full duplex mode and already initialized
// }
// if (_rx_handle == nullptr){
// i2s_chan_config_t rx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
// AddLog(LOG_LEVEL_DEBUG, "I2S: rx_chan_cfg id:%i role:%i dma_desc_num:%i dma_frame_num:%i auto_clear:%i",
// rx_chan_cfg.id, rx_chan_cfg.role, rx_chan_cfg.dma_desc_num, rx_chan_cfg.dma_frame_num, rx_chan_cfg.auto_clear);
// err = i2s_new_channel(&rx_chan_cfg, NULL, &_rx_handle);
// AddLog(LOG_LEVEL_DEBUG, "I2S: mic init i2s_new_channel err=%i", err);
// switch (_rx_mode){
// case I2S_MODE_PDM:
// {
// i2s_pdm_rx_config_t rx_pdm_cfg = {
// .clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG(_rx_freq),
// /* The default mono slot is the left slot (whose 'select pin' of the PDM microphone is pulled down) */
// .slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
// // .slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, slot_mode),
// .gpio_cfg = {
// .clk = _gpio_ws,
// .din = _gpio_din,
// .invert_flags = {
// .clk_inv = _gpio_ws_inv,
// },
// },
// };
// if (_rx_slot_mask != I2S_SLOT_NOCHANGE) { rx_pdm_cfg.slot_cfg.slot_mask = (i2s_pdm_slot_mask_t)_rx_slot_mask; }
// // AddLog(LOG_LEVEL_DEBUG, "I2S: rx_pdm_cfg clk_cfg sample_rate_hz:%i clk_src:%i mclk_multiple:%i dn_sample_mode:%i",
// // rx_pdm_cfg.clk_cfg.sample_rate_hz, rx_pdm_cfg.clk_cfg.clk_src, rx_pdm_cfg.clk_cfg.mclk_multiple, rx_pdm_cfg.clk_cfg.dn_sample_mode);
// // AddLog(LOG_LEVEL_DEBUG, "I2S: rx_pdm_cfg slot_cfg data_bit_width:%i slot_bit_width:%i slot_mode:%i slot_mask:%i",
// // rx_pdm_cfg.slot_cfg.data_bit_width, rx_pdm_cfg.slot_cfg.slot_bit_width, rx_pdm_cfg.slot_cfg.slot_mode, rx_pdm_cfg.slot_cfg.slot_mask);
// // AddLog(LOG_LEVEL_DEBUG, "I2S: rx_pdm_cfg gpio_cfg clk:%i din:%i clk_inv:%i",
// // rx_pdm_cfg.gpio_cfg.clk, rx_pdm_cfg.gpio_cfg.din, rx_pdm_cfg.gpio_cfg.invert_flags.clk_inv);
// err = i2s_channel_init_pdm_rx_mode(_rx_handle, &rx_pdm_cfg);
// AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: RX channel in PDM mode, CLK: %i, DIN: %i, 16 bit width, %i channel(s), err code: 0x%04X"),
// _gpio_ws, _gpio_din, slot_mode, err);
// }
// break;
// case I2S_MODE_STD:
// {
// i2s_std_config_t rx_std_cfg = {
// .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(_rx_freq),
// .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, slot_mode),
// .gpio_cfg = {
// .mclk = (gpio_num_t)Pin(GPIO_I2S_MCLK),
// .bclk = (gpio_num_t)Pin(GPIO_I2S_BCLK),
// .ws = (gpio_num_t)Pin(GPIO_I2S_WS),
// .dout = I2S_GPIO_UNUSED,
// .din = (gpio_num_t)Pin(GPIO_I2S_DIN),
// .invert_flags = {
// .mclk_inv = false,
// .bclk_inv = false,
// .ws_inv = false,
// },
// },
// };
// if (_rx_slot_mask != I2S_SLOT_NOCHANGE) { rx_std_cfg.slot_cfg.slot_mask = (i2s_std_slot_mask_t)_rx_slot_mask; }
// err = i2s_channel_init_std_mode(audio_i2s.rx_handle, &rx_std_cfg);
// AddLog(LOG_LEVEL_DEBUG, "I2S: RX i2s_channel_init_std_mode err:%i", err);
// AddLog(LOG_LEVEL_DEBUG, "I2S: RX channel in standard mode with 16 bit width on %i channel(s) initialized", slot_mode);
// }
// break;
// default:
// AddLog(LOG_LEVEL_INFO, "I2S: invalid rx mode=%i", _rx_mode);
// return -1;
// }
// }
if (!_rx_running) {
err = i2s_channel_enable(_rx_handle);
AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: RX channel enable err:0x%04X"),err);
_rx_running = true;
}
return err;
}
void TasmotaI2S::I2sMicDeinit(void) {
esp_err_t err = ESP_OK;
gpio_num_t clk_gpio;
AddLog(LOG_LEVEL_DEBUG, "I2S: mic deinit rx_running:%i rx_handle:%p", _rx_running, _rx_handle);
if (!_rx_handle) { return; }
if (!_tx_configured || !_rx_configured) { // if duplex mode, there is no mic channel - TODO check this
int err = i2s_channel_disable(_rx_handle);
i2s_del_channel(_rx_handle);
_rx_handle = nullptr;
_rx_running = false;
AddLog(LOG_LEVEL_DEBUG, "I2S: RX channel disable: %i", err);
}
}
#endif // USE_I2S_AUDIO
#endif // defined(ESP32) && ESP_IDF_VERSION_MAJOR >= 5

View File

@ -1,428 +0,0 @@
/*
xdrv_42_0_i2s__lib_idf51.ino - Simplified Audio library
Copyright (C) 2021 Gerhard Mutz, Theo Arends, Staars, Stephan Hadinger
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#if defined(ESP32) && ESP_IDF_VERSION_MAJOR >= 5
#ifdef USE_I2S_AUDIO
#include "driver/i2s_std.h"
#include "driver/i2s_pdm.h"
#include "driver/gpio.h"
#include "AudioFileSourcePROGMEM.h"
#include "AudioFileSourceID3.h"
#include "AudioGeneratorMP3.h"
#include <ESP8266SAM.h>
#include "AudioFileSourceFS.h"
#include "AudioGeneratorTalkie.h"
#include "AudioFileSourceICYStream.h"
#include "AudioFileSourceBuffer.h"
#include "AudioGeneratorAAC.h"
#include <layer3.h>
#include <types.h>
/*********************************************************************************************\
* Driver Settings in memory
\*********************************************************************************************/
// I2S communication mode
enum : int8_t {
I2S_MODE_STD = 0, // I2S mode standard
I2S_MODE_PDM = 1, // I2S mode PDM
I2S_MODE_TDM = 2, // I2S mode TDM
I2S_MODE_DAC = 3, // Using internal DAC - only available on ESP32
};
// I2S slot mask (left, right, both)
enum : int8_t {
I2S_SLOT_LEFT = 1, // left
I2S_SLOT_RIGHT = 2, // right
I2S_SLOT_BOTH = 3, // both
};
// I2S slot configuration
enum : int8_t {
I2S_SLOT_MSB = 0, // MSB
I2S_SLOT_PCM = 1, // PCM
I2S_SLOT_PHILIPS = 2, // Philips
};
typedef struct{
struct{
uint8_t version = 0; // B00
// runtime options, will be saved but ignored on setting read
bool duplex = 0; // B01 - depends on GPIO setting and SOC caps, DIN and DOUT on same port in GPIO means -> try to use duplex if possible
bool tx = 0; // B02 - depends on GPIO setting
bool rx = 0; // B03 - depends on GPIO setting
uint32_t spare01; // B04-07
} sys;
struct {
uint8_t mode = I2S_MODE_STD; // B00 - I2S mode standard, PDM, TDM, DAC
bool apll = 1; // B01 - will be ignored on unsupported SOC's
uint8_t channels = 2; // B02 - 1 = mono, 2 = stereo
uint8_t codec = 0; // B03 - S3 box only, unused for now
uint8_t slot_config = I2S_SLOT_MSB;// B04 - slot configuration MSB = 0, PCM = 1, PHILIPS = 2
uint8_t volume = 10; // B05
bool mclk_inv = 0; // B06 - invert mclk
bool bclk_inv = 0; // B07 - invert bclk
bool ws_inv = 0; // B08 - invert ws
uint8_t spare[7]; // B09-0F
} tx;
struct {
uint32_t sample_rate = 16000; // B00-03
uint8_t gain = 30; // B04
uint8_t mode = I2S_MODE_STD; // B05 - I2S mode standard, PDM, TDM, DAC
uint8_t slot_mask = I2S_SLOT_LEFT;// B06 - slot mask
uint8_t slot_mode = 0; // B07 - mono/stereo - 1 is added for both
uint8_t codec = 0; // B08 - unused for now
uint8_t mp3_encoder = 1; // B09 - will be ignored without PS-RAM
uint8_t spare[6]; // B0A-0F
} rx;
} tI2SSettings;
typedef union {
uint8_t data;
struct {
uint8_t master : 1;
uint8_t enabled : 1;
uint8_t swap_mic : 1;
uint8_t mode : 2;
};
} BRIDGE_MODE;
class TasmotaAudioOutputI2S;
struct AUDIO_I2S_t {
tI2SSettings *Settings;
i2s_chan_handle_t rx_handle = nullptr;
AudioGeneratorMP3 *mp3 = nullptr;
AudioFileSourceFS *file = nullptr;
TasmotaAudioOutputI2S *out = nullptr;
AudioFileSourceID3 *id3 = nullptr;
AudioGeneratorMP3 *decoder = NULL;
void *mp3ram = NULL;
TaskHandle_t mp3_task_handle;
TaskHandle_t mic_task_handle;
char mic_path[32];
uint8_t mic_stop;
int8_t mic_error;
bool use_stream = false;
// SHINE
uint32_t recdur;
uint8_t stream_active;
uint8_t stream_enable;
WiFiClient client;
ESP8266WebServer *MP3Server;
// I2S_BRIDGE
BRIDGE_MODE bridge_mode;
WiFiUDP i2s_bridge_udp;
WiFiUDP i2s_bridgec_udp;
IPAddress i2s_bridge_ip;
TaskHandle_t i2s_bridge_h;
int8_t ptt_pin = -1;
} audio_i2s;
/*********************************************************************************************\
* Class for outputting sound as endpoint for ESP8266Audio library
\*********************************************************************************************/
class TasmotaAudioOutputI2S : public AudioOutput
{
public:
// Constructor takes no parameter, everything is configured from template and config file
TasmotaAudioOutputI2S() {
loadSettings();
}
~TasmotaAudioOutputI2S() {
this->stop();
}
// ------------------------------------------------------------------------------------------
// Setters for configuration parameters
//
// TODO: not sure we still need them since all this should be set at initialiation
// ------------------------------------------------------------------------------------------
virtual bool SetBitsPerSample(int bits) {
AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: SetBitsPerSample: %i"), bits);
if ( (bits != 16) && (bits != 8) ) { return false; }
this->bps = bits;
return true;
}
virtual bool SetChannels(int channels) {
AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: SetChannels: %i"), channels);
if ((channels < 1) || (channels > 2)) { return false; }
if (channels == (int)this->channels) { return true; }
this->channels = channels;
return true;
}
virtual bool SetRate(int hz) {
AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: SetRate: %i was %i on=%i"), hz, this->hertz, _i2s_on);
if (hz == (int) this->hertz) { return true; }
this->hertz = hz;
if (_i2s_on) {
int result = updateClockConfig();
}
return true;
}
virtual bool SetGain(float f) {
AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: SetGain: %_f"), &f);
return AudioOutput::SetGain(f);
}
// ------------------------------------------------------------------------------------------
// Getters
inline uint8_t getTxMode(void) const { return _tx_mode; }
inline uint8_t getTxChannels(void) const { return _tx_channels; }
inline bool getTxEnabled(void) const { return _tx_enabled; }
// ------------------------------------------------------------------------------------------
// Setters
inline void setTxMode(uint8_t mode) { _tx_mode = mode; }
inline void setTxChannels(uint8_t channels) { _tx_channels = channels; }
inline void setTxEnabled(bool enabled) { _tx_enabled = enabled; }
// ------------------------------------------------------------------------------------------
// AudioOutput has inconsistent case for methods, provide consistent setters for super class
inline void setRate(int hz) { SetRate(hz); }
inline void setBitsPerSample(int bits) { SetBitsPerSample(bits); }
inline void setChannels(int chan) { SetChannels(chan); }
inline void setGain(float f) { SetGain(f); }
bool begin(void);
bool stop(void);
bool ConsumeSample(int16_t sample[2]);
bool startI2SChannel(void);
int updateClockConfig(void);
// The following is now the preferred function
// and allows to send multiple samples at once
//
// Max 128 samples, it is clipped otherwise
// Returns: the number of samples actually consumed
// or -1 if an error occured
//
// The call is non blocking and does not wait
int32_t consumeSamples(int16_t *samples, size_t count);
protected:
void loadSettings(void); // load all settings from Settings file and template - the goal is to have zero touch
protected:
bool _i2s_on = false; // is I2S audio active
// local copy of useful settings for audio
// TX
uint8_t _tx_mode = I2S_MODE_STD; // EXTERNAL_I2S = 0, INTERNAL_DAC = 1, INTERNAL_PDM = 2
bool _tx_enabled = false; // true = enabled, false = disabled
uint8_t _tx_channels = 2; // true = mono, false = stereo
i2s_chan_handle_t _tx_chan; // I2S channel handle, automatically computed
// GPIOs for I2S
gpio_num_t _gpio_mclk = GPIO_NUM_NC; // GPIO for master clock
gpio_num_t _gpio_bclk = GPIO_NUM_NC; // GPIO for bit clock
gpio_num_t _gpio_ws = GPIO_NUM_NC; // GPIO for word select
gpio_num_t _gpio_dout = GPIO_NUM_NC; // GPIO for data out
gpio_num_t _gpio_din = GPIO_NUM_NC; // GPIO for data in
bool _gpio_mclk_inv = false; // invert master clock
bool _gpio_bclk_inv = false; // invert bit clock
bool _gpio_ws_inv = false; // invert word select
};
// ------------------------------------------------------------------------------------------
// Methods
// ------------------------------------------------------------------------------------------
void TasmotaAudioOutputI2S::loadSettings(void) {
hertz = 16000;
_i2s_on = false;
bps = I2S_DATA_BIT_WIDTH_16BIT;
_tx_channels = audio_i2s.Settings->tx.channels;
if (_tx_channels == 0) { _tx_channels = 2; } // if zero channel default to stereo
if (_tx_channels > 2) { _tx_channels = 2; } // if > 2 channels default to stereo
channels = (_tx_channels == 1) ? I2S_SLOT_MODE_MONO : I2S_SLOT_MODE_STEREO;
_tx_mode = I2S_MODE_STD;
_tx_enabled = false;
_gpio_mclk = (gpio_num_t) Pin(GPIO_I2S_MCLK);
_gpio_bclk = (gpio_num_t) Pin(GPIO_I2S_BCLK);
_gpio_ws = (gpio_num_t) Pin(GPIO_I2S_WS);
_gpio_dout = (gpio_num_t) Pin(GPIO_I2S_DOUT);
_gpio_din = (gpio_num_t) Pin(GPIO_I2S_DIN);
_gpio_mclk_inv = audio_i2s.Settings->tx.mclk_inv;
_gpio_bclk_inv = audio_i2s.Settings->tx.bclk_inv;
_gpio_ws_inv = audio_i2s.Settings->tx.ws_inv;
}
bool TasmotaAudioOutputI2S::begin() {
if (_tx_enabled) { return true; }
if (!_i2s_on) {
if (audio_i2s.Settings->sys.duplex == 0 && audio_i2s.Settings->sys.rx == 1) {
this->startI2SChannel();
}
}
int result = i2s_channel_enable(_tx_chan);
if (result != 0){
AddLog(LOG_LEVEL_INFO, "I2S: Could not enable i2s_channel: %i", result);
return false;
}
_tx_enabled = true;
return true;
}
bool TasmotaAudioOutputI2S::stop() {
i2s_channel_disable(_tx_chan);
if (audio_i2s.Settings->sys.duplex == 0 && audio_i2s.Settings->sys.rx == 1) {
i2s_del_channel(_tx_chan);
_i2s_on = false;
AddLog(LOG_LEVEL_INFO, "I2S: stop: I2S channel disabled");
}
_tx_enabled = false;
return true;
}
bool TasmotaAudioOutputI2S::ConsumeSample(int16_t sample[2]) {
return consumeSamples(sample, 1);
}
int32_t TasmotaAudioOutputI2S::consumeSamples(int16_t *samples, size_t count) {
if (!_tx_enabled) { return -1; }
if (count == 0) { return 0; }
if (count > 128) { count = 128; }
int16_t ms[count*2];
for (int32_t i = 0; i < count; i++) {
int16_t left = samples[i*2 + LEFTCHANNEL];
int16_t right = samples[i*2 + RIGHTCHANNEL];
if (this->_tx_channels == 1) { // if mono, average the two samples
// Average the two samples and overwrite
int32_t ttl = left + right;
left = right = (ttl>>1) & 0xffff;
}
if (bps == 8) {
left = (((int16_t)(left & 0xff)) - 128) << 8;
right = (((int16_t)(right & 0xff)) - 128) << 8;
}
if (_tx_mode == I2S_MODE_DAC) {
left = Amplify(left) + 0x8000;
right = Amplify(right) + 0x8000;
} else {
left = Amplify(left);
right = Amplify(right);
}
ms[i*2 + LEFTCHANNEL] = left;
ms[i*2 + RIGHTCHANNEL] = right;
}
size_t i2s_bytes_written;
esp_err_t err = i2s_channel_write(_tx_chan, ms, sizeof(ms), &i2s_bytes_written, 0);
if (err && err != ESP_ERR_TIMEOUT) {
AddLog(LOG_LEVEL_INFO, "I2S: Could not write samples (count=%i): %i", count, err);
return -1;
}
return i2s_bytes_written;
}
// Initialize I2S channel
// return `true` if successful, `false` otherwise
bool TasmotaAudioOutputI2S::startI2SChannel(void) {
gpio_num_t _DIN = I2S_GPIO_UNUSED; // no input pin by default
// default dma_desc_num = 6 (DMA buffers), dma_frame_num = 240 (frames per buffer)
i2s_chan_config_t tx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
if (audio_i2s.Settings->sys.duplex == 1) {
_DIN = (gpio_num_t)_gpio_din;
i2s_new_channel(&tx_chan_cfg, &_tx_chan, &audio_i2s.rx_handle);
} else{
i2s_new_channel(&tx_chan_cfg, &_tx_chan, NULL);
}
// by default we configure for MSB 2 slots `I2S_STD_MSB_SLOT_DEFAULT_CONFIG`
i2s_std_config_t tx_std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(hertz),
.slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t)bps, (i2s_slot_mode_t)channels),
.gpio_cfg = {
.mclk = _gpio_mclk,
.bclk = _gpio_bclk,
.ws = _gpio_ws,
.dout = _gpio_dout,
.din = _DIN,
.invert_flags = {
.mclk_inv = _gpio_mclk_inv,
.bclk_inv = _gpio_bclk_inv,
.ws_inv = _gpio_ws_inv,
}
}
};
// change configuration if we are using PCM or PHILIPS
if (audio_i2s.Settings->tx.slot_config == I2S_SLOT_PCM) { // PCM
tx_std_cfg.slot_cfg = I2S_STD_PCM_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t)bps, (i2s_slot_mode_t)channels);
} else if (audio_i2s.Settings->tx.slot_config == I2S_SLOT_PHILIPS) { // PHILIPS
tx_std_cfg.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t)bps, (i2s_slot_mode_t)channels);
}
_i2s_on = (i2s_channel_init_std_mode(_tx_chan, &tx_std_cfg) == 0);
AddLog(LOG_LEVEL_DEBUG, "I2S: TX channel with %i bits width on %i channels initialized i2s_on=%i", bps, channels, _i2s_on);
if (audio_i2s.Settings->sys.duplex == 1) {
i2s_channel_init_std_mode(audio_i2s.rx_handle, &tx_std_cfg);
AddLog(LOG_LEVEL_DEBUG, "I2S: RX channel added in full duplex mode");
}
return _i2s_on;
}
int TasmotaAudioOutputI2S::updateClockConfig(void) {
i2s_channel_disable(_tx_chan);
i2s_std_clk_config_t clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(hertz);
#ifdef SOC_I2S_SUPPORTS_APLL
if (audio_i2s.Settings->tx.apll == 1) {
clk_cfg.clk_src = I2S_CLK_SRC_APLL;
}
#endif
int result = i2s_channel_reconfig_std_clock(_tx_chan, &clk_cfg );
if (_tx_enabled) { i2s_channel_enable(_tx_chan); }
AddLog(LOG_LEVEL_DEBUG, "I2S: Updating clock config");
return result;
}
#endif // USE_I2S_AUDIO
#endif // defined(ESP32) && ESP_IDF_VERSION_MAJOR >= 5

View File

@ -26,6 +26,7 @@
#define USE_I2S_SAY_TIME #define USE_I2S_SAY_TIME
#define USE_I2S_RTTTL #define USE_I2S_RTTTL
#define USE_I2S_WEBRADIO #define USE_I2S_WEBRADIO
#define USE_I2S_DEBUG // remove before flight
// Macros used in audio sub-functions // Macros used in audio sub-functions
#undef AUDIO_PWR_ON #undef AUDIO_PWR_ON
@ -54,6 +55,9 @@ void I2sWebRadioStopPlaying(void);
const char kI2SAudio_Commands[] PROGMEM = "I2S|" const char kI2SAudio_Commands[] PROGMEM = "I2S|"
"Gain|Play|Rec|MGain|Stop" "Gain|Play|Rec|MGain|Stop"
#ifdef USE_I2S_DEBUG
"|Mic" // debug only
#endif // USE_I2S_DEBUG
#ifdef USE_I2S_WEBRADIO #ifdef USE_I2S_WEBRADIO
"|WR" "|WR"
#endif // USE_I2S_WEBRADIO #endif // USE_I2S_WEBRADIO
@ -76,6 +80,9 @@ const char kI2SAudio_Commands[] PROGMEM = "I2S|"
void (* const I2SAudio_Command[])(void) PROGMEM = { void (* const I2SAudio_Command[])(void) PROGMEM = {
&CmndI2SGain, &CmndI2SPlay, &CmndI2SMicRec, &CmndI2SMicGain, &CmndI2SStop, &CmndI2SGain, &CmndI2SPlay, &CmndI2SMicRec, &CmndI2SMicGain, &CmndI2SStop,
#ifdef USE_I2S_DEBUG
&CmndI2SMic,
#endif // USE_I2S_DEBUG
#ifdef USE_I2S_WEBRADIO #ifdef USE_I2S_WEBRADIO
&CmndI2SWebRadio, &CmndI2SWebRadio,
#endif // USE_I2S_WEBRADIO #endif // USE_I2S_WEBRADIO
@ -100,94 +107,6 @@ void (* const I2SAudio_Command[])(void) PROGMEM = {
* microphone related functions * microphone related functions
\*********************************************************************************************/ \*********************************************************************************************/
uint32_t I2sMicInit(void) {
esp_err_t err = ESP_OK;
gpio_num_t clk_gpio;
i2s_slot_mode_t slot_mode = (audio_i2s.Settings->rx.slot_mode == 0) ? I2S_SLOT_MODE_MONO : I2S_SLOT_MODE_STEREO;
AddLog(LOG_LEVEL_DEBUG, "I2S: mic init rx_channels=%i", slot_mode);
if (audio_i2s.Settings->sys.duplex == 1 && audio_i2s.rx_handle){
return 0; // no need to en- or disable when in full duplex mode and already initialized
}
if (audio_i2s.rx_handle == nullptr){
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
err = i2s_new_channel(&chan_cfg, NULL, &audio_i2s.rx_handle);
AddLog(LOG_LEVEL_DEBUG, "I2S: mic init i2s_new_channel err=%i", err);
switch (audio_i2s.Settings->rx.mode){
case I2S_MODE_PDM:
{
clk_gpio = (gpio_num_t)Pin(GPIO_I2S_WS,1); //legacy setting for Core2, might be wrong
if (clk_gpio == -1){
clk_gpio = (gpio_num_t)Pin(GPIO_I2S_WS); //fallback to other port, might be wrong
}
i2s_pdm_rx_config_t pdm_rx_cfg = {
.clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG(audio_i2s.Settings->rx.sample_rate),
/* The default mono slot is the left slot (whose 'select pin' of the PDM microphone is pulled down) */
.slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, slot_mode),
.gpio_cfg = {
.clk = clk_gpio,
.din = (gpio_num_t)Pin(GPIO_I2S_DIN),
.invert_flags = {
.clk_inv = false,
},
},
};
pdm_rx_cfg.slot_cfg.slot_mask = (i2s_pdm_slot_mask_t)audio_i2s.Settings->rx.slot_mask;
err = i2s_channel_init_pdm_rx_mode(audio_i2s.rx_handle, &pdm_rx_cfg);
AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: RX channel in PDM mode, CLK: %i, DIN: %i, 16 bit width, %i channel(s), err code: %u"),clk_gpio, Pin(GPIO_I2S_DIN), slot_mode, err);
}
break;
case I2S_MODE_STD:
{
i2s_std_config_t rx_std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(audio_i2s.Settings->rx.sample_rate),
.slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, slot_mode),
.gpio_cfg = {
.mclk = (gpio_num_t)Pin(GPIO_I2S_MCLK),
.bclk = (gpio_num_t)Pin(GPIO_I2S_BCLK),
.ws = (gpio_num_t)Pin(GPIO_I2S_WS),
.dout = I2S_GPIO_UNUSED,
.din = (gpio_num_t)Pin(GPIO_I2S_DIN),
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
rx_std_cfg.slot_cfg.slot_mask = (i2s_std_slot_mask_t)audio_i2s.Settings->rx.slot_mask;
i2s_channel_init_std_mode(audio_i2s.rx_handle, &rx_std_cfg);
AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: RX channel in standard mode with 16 bit width on %i channel(s) initialized"),slot_mode);
}
break;
default:
AddLog(LOG_LEVEL_INFO, "I2S: invalid rx mode=%i", audio_i2s.Settings->rx.mode);
}
}
err = i2s_channel_enable(audio_i2s.rx_handle);
AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: RX channel enable: %i"),err);
return err;
}
void I2sMicDeinit(void) {
esp_err_t err = ESP_OK;
gpio_num_t clk_gpio;
if (!audio_i2s.rx_handle) { return; }
if (!audio_i2s.Settings->sys.duplex) { // if duplex mode, there is no mic channel - TODO check this
int err = i2s_channel_disable(audio_i2s.rx_handle);
i2s_del_channel(audio_i2s.rx_handle);
audio_i2s.rx_handle = nullptr;
AddLog(LOG_LEVEL_DEBUG, "I2S: RX channel disable: %i", err);
}
}
// micro to mp3 file or stream // micro to mp3 file or stream
void I2sMicTask(void *arg){ void I2sMicTask(void *arg){
int8_t error = 0; int8_t error = 0;
@ -226,14 +145,14 @@ void I2sMicTask(void *arg){
shine_set_config_mpeg_defaults(&config.mpeg); shine_set_config_mpeg_defaults(&config.mpeg);
if (audio_i2s.Settings->rx.slot_mode == 0) { if (audio_i2s.Settings->rx.channels == 1) {
config.mpeg.mode = MONO; config.mpeg.mode = MONO;
} else { } else {
config.mpeg.mode = STEREO; config.mpeg.mode = STEREO;
} }
config.mpeg.bitr = 128; config.mpeg.bitr = 128;
config.wave.samplerate = audio_i2s.Settings->rx.sample_rate; config.wave.samplerate = audio_i2s.Settings->rx.sample_rate;
config.wave.channels = (channels)(audio_i2s.Settings->rx.slot_mode + 1); config.wave.channels = (channels)(audio_i2s.Settings->rx.channels);
if (shine_check_config(config.wave.samplerate, config.mpeg.bitr) < 0) { if (shine_check_config(config.wave.samplerate, config.mpeg.bitr) < 0) {
error = 3; error = 3;
@ -247,7 +166,7 @@ void I2sMicTask(void *arg){
} }
samples_per_pass = shine_samples_per_pass(s); samples_per_pass = shine_samples_per_pass(s);
bytesize = samples_per_pass * 2 * (audio_i2s.Settings->rx.slot_mode + 1); bytesize = samples_per_pass * 2 * (audio_i2s.Settings->rx.channels);
buffer = (int16_t*)malloc(bytesize); buffer = (int16_t*)malloc(bytesize);
if (!buffer) { if (!buffer) {
@ -310,7 +229,7 @@ exit:
audio_i2s.client.stop(); audio_i2s.client.stop();
} }
I2sMicDeinit(); audio_i2s.out->I2sMicDeinit();
audio_i2s.mic_stop = 0; audio_i2s.mic_stop = 0;
audio_i2s.mic_error = error; audio_i2s.mic_error = error;
AddLog(LOG_LEVEL_INFO, PSTR("mp3task result code: %d"), error); AddLog(LOG_LEVEL_INFO, PSTR("mp3task result code: %d"), error);
@ -334,7 +253,7 @@ int32_t I2sRecordShine(char *path) {
if (audio_i2s.use_stream) { if (audio_i2s.use_stream) {
stack = 8000; stack = 8000;
} }
I2sMicInit(); audio_i2s.out->I2sMicInit();
err = xTaskCreatePinnedToCore(I2sMicTask, "MIC", stack, NULL, 3, &audio_i2s.mic_task_handle, 1); err = xTaskCreatePinnedToCore(I2sMicTask, "MIC", stack, NULL, 3, &audio_i2s.mic_task_handle, 1);
@ -347,7 +266,8 @@ int32_t I2sRecordShine(char *path) {
// error codes // error codes
enum { enum {
I2S_OK = 0, I2S_OK = 0,
I2S_ERR_OUTPUT_NOT_CONFIGURE, I2S_ERR_OUTPUT_NOT_CONFIGURED,
I2S_ERR_INPUT_NOT_CONFIGURED,
I2S_ERR_DECODER_IN_USE, I2S_ERR_DECODER_IN_USE,
I2S_ERR_FILE_NOT_FOUND, I2S_ERR_FILE_NOT_FOUND,
}; };
@ -369,7 +289,6 @@ void I2SSettingsLoad(const char * config_filename, bool erase) {
AddLog(LOG_LEVEL_ERROR, "I2S: ERROR memory allocation failed"); AddLog(LOG_LEVEL_ERROR, "I2S: ERROR memory allocation failed");
return; return;
} }
if (!config_filename) { return; } // if no filename, use defaults if (!config_filename) { return; } // if no filename, use defaults
#ifndef USE_UFILESYS #ifndef USE_UFILESYS
@ -406,53 +325,20 @@ void I2SSettingsSave(const char * config_filename) {
* Driver init * Driver init
\*********************************************************************************************/ \*********************************************************************************************/
//
// I2sCheckCfg
//
// Multiple checks
void I2sCheckCfg(void){
// din and dout must be configured on port 0 for full duplex
bool useDuplexMode = ((Pin(GPIO_I2S_DIN) != -1) && (Pin(GPIO_I2S_DOUT) != -1));
// AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: DIN %i , DOUT %i"),Pin(GPIO_I2S_DIN),Pin(GPIO_I2S_DOUT) );
if (useDuplexMode){
if (audio_i2s.Settings->rx.mode == I2S_MODE_PDM || audio_i2s.Settings->tx.mode == I2S_MODE_PDM ){
useDuplexMode = false;
AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: PDM forbids full duplex mode"));
}
audio_i2s.Settings->sys.duplex = useDuplexMode ? 1 : 0;
if(useDuplexMode){
AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: will try to use full duplex mode"));
}
}
if (Pin(GPIO_I2S_DIN) != -1 || Pin(GPIO_I2S_DIN, 1) != -1){ // micro could be port 0 or 1
audio_i2s.Settings->sys.rx = 1;
AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: RX config = mode: %i, channels: %i, gain: %i, sample rate: %i"), audio_i2s.Settings->rx.mode, (uint8_t)(audio_i2s.Settings->rx.slot_mode + 1), audio_i2s.Settings->rx.gain, audio_i2s.Settings->rx.sample_rate);
}
else{
audio_i2s.Settings->sys.rx = 0;
audio_i2s.Settings->rx.mp3_encoder = 0; // do not allocate buffer
}
if (Pin(GPIO_I2S_DOUT) != -1){ // output is only supported on port 0
audio_i2s.Settings->sys.tx = 1;
}
else{
audio_i2s.Settings->sys.tx = 0;
}
AddLog(LOG_LEVEL_INFO, PSTR("I2S: init pins bclk=%d, ws=%d, dout=%d, mclk=%d, din=%d [tx=%i, rx=%i, duplex=%i]"),
Pin(GPIO_I2S_BCLK) , Pin(GPIO_I2S_WS), Pin(GPIO_I2S_DOUT), Pin(GPIO_I2S_MCLK), Pin(GPIO_I2S_DIN),
audio_i2s.Settings->sys.tx, audio_i2s.Settings->sys.tx, audio_i2s.Settings->sys.duplex);
}
// //
// I2sInit // I2sInit
// //
// Initialize I2S driver for input and output // Initialize I2S driver for input and output
void I2sInit(void) { void I2sInit(void) {
int32_t gpio_din_0 = Pin(GPIO_I2S_DIN, 0);
int32_t gpio_din_1 = Pin(GPIO_I2S_DIN, 1);
int32_t gpio_dout_0 = Pin(GPIO_I2S_DOUT, 0);
int32_t gpio_dout_1 = Pin(GPIO_I2S_DOUT, 1);
int32_t gpio_ws_0 = Pin(GPIO_I2S_WS, 0);
// we need at least one pin configured // we need at least one pin configured
if(Pin(GPIO_I2S_DIN) + Pin(GPIO_I2S_DIN,1) + Pin(GPIO_I2S_DOUT) + Pin(GPIO_I2S_DOUT,1) == -4){ // Note: in case of ESP32 DAC output we may have only WS_0 configured. DAC is only supported on port 0
if ((gpio_din_0 < 0) && (gpio_din_1 < 0) && (gpio_dout_0 < 0) && (gpio_dout_1 < 0) && (gpio_ws_0 < 0)) {
AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: no pin configured")); AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: no pin configured"));
return; return;
} }
@ -460,37 +346,204 @@ void I2sInit(void) {
I2SSettingsLoad("/.drvset042", false); // load configuration (no-erase) I2SSettingsLoad("/.drvset042", false); // load configuration (no-erase)
if (!audio_i2s.Settings) { return; } // fatal error, could not allocate memory for configuration if (!audio_i2s.Settings) { return; } // fatal error, could not allocate memory for configuration
// check configuration is valid // detect if we need full-duplex on port 0
I2sCheckCfg(); bool duplex = false;
if ((gpio_din_0 >= 0) && (gpio_dout_0 >= 0)) {
// TODO // conditions are potentially favorable for duplex
int result = 0; if (audio_i2s.Settings->rx.mode == I2S_MODE_PDM || audio_i2s.Settings->tx.mode == I2S_MODE_PDM ){
if (audio_i2s.Settings->sys.rx == 1 && audio_i2s.Settings->sys.duplex == 0){ AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: PDM forbids full duplex mode, ignoring 'I2S DIN 1'"));
I2sMicDeinit(); gpio_din_0 = -1; // hence deconfigure DIN_0 which can't be used
} else {
AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: will try to use full duplex mode"));
duplex = true;
}
} }
if(audio_i2s.Settings->sys.tx == 1){ // AddLog(LOG_LEVEL_INFO, PSTR("I2S: init pins bclk=%d, ws=%d, dout=%d, mclk=%d, din=%d"),
audio_i2s.out = new TasmotaAudioOutputI2S; // Pin(GPIO_I2S_BCLK, 0) , Pin(GPIO_I2S_WS, 0), Pin(GPIO_I2S_DOUT, 0), Pin(GPIO_I2S_MCLK, 0), Pin(GPIO_I2S_DIN, 0));
int err = audio_i2s.out->startI2SChannel() ? 0 : 1;
result += err; audio_i2s.Settings->sys.duplex = false;
audio_i2s.Settings->sys.tx = false;
audio_i2s.Settings->sys.rx = false;
audio_i2s.Settings->sys.exclusive = false;
bool exclusive = false; // signals that in/out have a shared GPIO and need to un/install driver before use
for (uint32_t port = 0; port < SOC_I2S_NUM; port++) {
int32_t bclk = Pin(GPIO_I2S_BCLK, port);
int32_t ws = Pin(GPIO_I2S_WS, port);
int32_t dout = Pin(GPIO_I2S_DOUT, port);
int32_t mclk = Pin(GPIO_I2S_MCLK, port);
int32_t din = Pin(GPIO_I2S_DIN, port);
bool tx = false; // is Tx enabled for this port
bool rx = false; // is Rx enabled for this port
AddLog(LOG_LEVEL_DEBUG, "I2S: I2S%i bclk=%i, ws=%i, dout=%i, mclk=%i, din=%i", port, bclk, ws, dout, mclk, din);
// if neither input, nor output, nor DAC/ADC skip (WS could is only needed for DAC but supports only port 0)
if (din < 0 && dout < 0 && (ws < 0 || port !=0)) { continue; }
const char *err_msg = nullptr; // to save code, we indicate an error with a message configured
bool duplex = (din >= 0) && (dout >= 0);
bool dac_mode = false;
if (din >= 0 || dout >= 0) {
// we have regular I2S configuration
// do multiple checks
// 1. check that WS is configured
if (ws < 0) {
// WS may be shared between both ports, so if it is configured on port 0, we accept it on port 1
int32_t ws0 = Pin(GPIO_I2S_WS, 0);
if (ws0 >= 0) {
ws = ws0;
AddLog(LOG_LEVEL_DEBUG, "I2S: I2S%i WS is shared, using WS from port 0 (%i)", port, ws);
exclusive = true;
}
if (ws < 0) {
err_msg = "no WS pin configured";
}
}
// 2. check that DAC mode is not enabled for output(incompatible with DIN/DOUT)
else if (dout >= 0 && audio_i2s.Settings->tx.mode == I2S_MODE_DAC) {
err_msg = "DAC mode is not compatible with DOUT";
}
// 3. check that ADC mode is not enabled for output (incompatible with DIN/DOUT)
else if (din >= 0 && audio_i2s.Settings->rx.mode == I2S_MODE_DAC) {
err_msg = "ADC mode is not compatible with DIN";
}
// 4. check that output is not already configured
else if (dout >= 0 && audio_i2s.out) {
err_msg = "output already configured";
}
// 5. check that input is not already configured
else if (din >= 0 && audio_i2s.in) {
err_msg = "input already configured";
}
// 6. check that we don't try PDM on port 1
else if (port != 0 && din >= 0 && audio_i2s.Settings->rx.mode == I2S_MODE_PDM) {
err_msg = "PDM Rx is not supported";
}
// 7. check that we don't try PDM on port 1
else if (port != 0 && dout >= 0 && audio_i2s.Settings->tx.mode == I2S_MODE_PDM) {
err_msg = "PDM Tx is not supported";
}
// 8. check that we don't try full-duplex with PDM in either direction
else if (duplex && (audio_i2s.Settings->rx.mode == I2S_MODE_PDM || audio_i2s.Settings->tx.mode == I2S_MODE_PDM )) {
err_msg = "PDM forbids full duplex mode";
duplex = false;
din = -1; // deconfigure DIN_0 which can't be used
}
} else {
dac_mode = true;
// no DIN/DOUT, try DAC mode
// 1. Check that tx.mode is I2S_MODE_DAC
if (audio_i2s.Settings->tx.mode != I2S_MODE_DAC) {
err_msg = "DAC mode is not enabled";
}
}
// is there any error?
if (err_msg) {
AddLog(LOG_LEVEL_DEBUG, "I2S: Error: %s for I2S%i, skipping", err_msg, port);
continue; // skip this port
}
tx = (dout >= 0) || dac_mode;
rx = (din >= 0);
if (duplex) {
AddLog(LOG_LEVEL_DEBUG, PSTR("I2S: will try to use full duplex mode"));
}
TasmotaI2S * i2s = new TasmotaI2S;
i2s->setPinout(bclk, ws, dout, mclk, din,
audio_i2s.Settings->sys.mclk_inv[0], audio_i2s.Settings->sys.bclk_inv[0],
audio_i2s.Settings->sys.ws_inv[0], audio_i2s.Settings->tx.apll);
i2s->setSlotConfig((i2s_port_t)port, audio_i2s.Settings->tx.slot_config, audio_i2s.Settings->rx.slot_config,
audio_i2s.Settings->tx.slot_mask, audio_i2s.Settings->rx.slot_mask);
if (tx) {
i2s->setTxMode(audio_i2s.Settings->tx.mode);
i2s->setTxChannels(audio_i2s.Settings->tx.channels);
i2s->setRate(audio_i2s.Settings->tx.sample_rate);
}
if (rx) {
i2s->setRxMode(audio_i2s.Settings->rx.mode);
i2s->setRxFreq(audio_i2s.Settings->rx.sample_rate);
i2s->setRxChannels(audio_i2s.Settings->rx.channels);
i2s->setRate(audio_i2s.Settings->rx.sample_rate);
}
if (i2s->startI2SChannel(tx, rx)) {
// succesful, register handlers
if (tx) {
audio_i2s.out = i2s;
audio_i2s.Settings->sys.tx = true;
}
if (rx) {
audio_i2s.in = i2s;
audio_i2s.Settings->sys.rx = true;
}
if (duplex) {
audio_i2s.Settings->sys.duplex = true;
}
}
} }
// do we have exclusive mode?
audio_i2s.Settings->sys.exclusive = exclusive;
if (audio_i2s.out) { audio_i2s.out->setExclusive(exclusive); }
if (audio_i2s.in) { audio_i2s.in->setExclusive(exclusive); }
if(audio_i2s.out != nullptr){ if(audio_i2s.out != nullptr){
audio_i2s.out->SetGain(((float)audio_i2s.Settings->tx.volume / 100.0) * 4.0); audio_i2s.out->SetGain(((float)audio_i2s.Settings->tx.gain / 100.0) * 4.0);
audio_i2s.out->begin(); audio_i2s.out->begin();
audio_i2s.out->stop(); audio_i2s.out->stop();
} }
audio_i2s.mp3ram = nullptr; audio_i2s.mp3ram = nullptr;
if(audio_i2s.Settings->rx.mp3_encoder == 1){ if (audio_i2s.Settings->rx.mp3_preallocate == 1){
// if (UsePSRAM()) {
AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: will allocate buffer for mp3 encoder")); AddLog(LOG_LEVEL_DEBUG,PSTR("I2S: will allocate buffer for mp3 encoder"));
if (UsePSRAM()) { audio_i2s.mp3ram = special_malloc(preallocateCodecSize);
audio_i2s.mp3ram = heap_caps_malloc(preallocateCodecSize, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); // }
} // else{
else{ // audio_i2s.Settings->rx.mp3_preallocate = 0; // no PS-RAM -> no MP3 encoding
audio_i2s.Settings->rx.mp3_encoder = 0; // no PS-RAM -> no MP3 encoding // }
}
} }
AddLog(LOG_LEVEL_DEBUG, "I2S: I2sInit done");
}
/*********************************************************************************************\
* General functions
\*********************************************************************************************/
//
// I2SPrepareTx() -> int
//
// Prepare I2S for output, handle exclusive access if necessary
//
// Returns `I2S_OK` if ok to send to output or error code
int32_t I2SPrepareTx(void) {
AddLog(LOG_LEVEL_DEBUG, "I2S: I2SPrepareTx out=%p", audio_i2s.out);
if (!audio_i2s.out) { return I2S_ERR_OUTPUT_NOT_CONFIGURED; }
if (audio_i2s.Settings->sys.exclusive) {
// TODO - deconfigure input driver
}
return I2S_OK;
}
//
// I2SPrepareRx() -> int
//
// Prepare I2S for input, handle exclusive access if necessary
//
// Returns `I2S_OK` if ok to record input or error code
int32_t I2SPrepareRx(void) {
if (!audio_i2s.in) return I2S_ERR_OUTPUT_NOT_CONFIGURED;
if (audio_i2s.Settings->sys.exclusive) {
// TODO - deconfigure input driver
}
return I2S_OK;
} }
/*********************************************************************************************\ /*********************************************************************************************\
@ -558,7 +611,8 @@ void I2sStopPlaying() {
// //
// Returns I2S_error_t // Returns I2S_error_t
int32_t I2SPlayMp3(const char *path) { int32_t I2SPlayMp3(const char *path) {
if (!audio_i2s.out) return I2S_ERR_OUTPUT_NOT_CONFIGURE; int32_t i2s_err = I2S_OK;
if ((i2s_err = I2SPrepareTx()) != I2S_OK) { return i2s_err; }
if (audio_i2s.decoder || audio_i2s.mp3) return I2S_ERR_DECODER_IN_USE; if (audio_i2s.decoder || audio_i2s.mp3) return I2S_ERR_DECODER_IN_USE;
// check if the filename starts with '/', if not add it // check if the filename starts with '/', if not add it
@ -598,7 +652,7 @@ void mp3_delete(void) {
void Say(char *text) { void Say(char *text) {
if (!audio_i2s.out) return; if (I2SPrepareTx()) { return; }
I2SAudioPower(true); I2SAudioPower(true);
@ -615,12 +669,44 @@ void Say(char *text) {
* Commands * Commands
\*********************************************************************************************/ \*********************************************************************************************/
void CmndI2SMic(void) {
if (I2SPrepareRx()) {
ResponseCmndChar("I2S Mic not configured");
return;
}
esp_err_t err = ESP_OK;
if (audio_i2s.decoder || audio_i2s.mp3) return;
audio_i2s.in->I2sMicInit();
if (audio_i2s.in->getRxRunning()) {
uint8_t buf[128];
size_t bytes_read = 0;
esp_err_t err = i2s_channel_read(audio_i2s.in->getRxHandle(), buf, sizeof(buf), &bytes_read, 0);
if (err == ESP_ERR_TIMEOUT) { err = ESP_OK; }
AddLog(LOG_LEVEL_INFO, "I2S: Mic (err:%i size:%i) %*_H", err, bytes_read, bytes_read, buf);
}
// err = xTaskCreatePinnedToCore(I2sMicTask, "MIC", stack, NULL, 3, &audio_i2s.mic_task_handle, 1);
ResponseCmndDone();
}
void CmndI2SStop(void) { void CmndI2SStop(void) {
if (!I2SPrepareTx()) {
ResponseCmndChar("I2S output not configured");
return;
}
I2sStopPlaying(); I2sStopPlaying();
ResponseCmndDone(); ResponseCmndDone();
} }
void CmndI2SPlay(void) { void CmndI2SPlay(void) {
if (I2SPrepareTx()) {
ResponseCmndChar("I2S output not configured");
return;
}
if (XdrvMailbox.data_len > 0) { if (XdrvMailbox.data_len > 0) {
int32_t err = I2SPlayMp3(XdrvMailbox.data); int32_t err = I2SPlayMp3(XdrvMailbox.data);
// display return message // display return message
@ -628,7 +714,7 @@ void CmndI2SPlay(void) {
case I2S_OK: case I2S_OK:
ResponseCmndDone(); ResponseCmndDone();
break; break;
case I2S_ERR_OUTPUT_NOT_CONFIGURE: case I2S_ERR_OUTPUT_NOT_CONFIGURED:
ResponseCmndChar("I2S output not configured"); ResponseCmndChar("I2S output not configured");
break; break;
case I2S_ERR_DECODER_IN_USE: case I2S_ERR_DECODER_IN_USE:
@ -649,14 +735,18 @@ void CmndI2SPlay(void) {
void CmndI2SGain(void) { void CmndI2SGain(void) {
if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 100)) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 100)) {
if (audio_i2s.out) { if (audio_i2s.out) {
audio_i2s.Settings->tx.volume = XdrvMailbox.payload; audio_i2s.Settings->tx.gain = XdrvMailbox.payload;
audio_i2s.out->SetGain(((float)(audio_i2s.Settings->tx.volume-2)/100.0)*4.0); audio_i2s.out->SetGain(((float)(audio_i2s.Settings->tx.gain-2)/100.0)*4.0);
} }
} }
ResponseCmndNumber(audio_i2s.Settings->tx.volume); ResponseCmndNumber(audio_i2s.Settings->tx.gain);
} }
void CmndI2SSay(void) { void CmndI2SSay(void) {
if (I2SPrepareTx()) {
ResponseCmndChar("I2S output not configured");
return;
}
if (XdrvMailbox.data_len > 0) { if (XdrvMailbox.data_len > 0) {
Say(XdrvMailbox.data); Say(XdrvMailbox.data);
} }
@ -664,6 +754,10 @@ void CmndI2SSay(void) {
} }
void CmndI2SI2SRtttl(void) { void CmndI2SI2SRtttl(void) {
if (I2SPrepareTx()) {
ResponseCmndChar("I2S output not configured");
return;
}
if (XdrvMailbox.data_len > 0) { if (XdrvMailbox.data_len > 0) {
Rtttl(XdrvMailbox.data); Rtttl(XdrvMailbox.data);
} }
@ -671,37 +765,38 @@ void CmndI2SI2SRtttl(void) {
} }
void CmndI2SMicRec(void) { void CmndI2SMicRec(void) {
if (audio_i2s.Settings->rx.mp3_encoder == 1) { if (audio_i2s.Settings->rx.mp3_preallocate == 1) {
if (XdrvMailbox.data_len > 0) { if (XdrvMailbox.data_len > 0) {
if (!strncmp(XdrvMailbox.data, "-?", 2)) { if (!strncmp(XdrvMailbox.data, "-?", 2)) {
Response_P("{\"I2SREC-duration\":%d}", audio_i2s.recdur); Response_P("{\"I2SREC-duration\":%d}", audio_i2s.recdur);
} else { } else {
I2sRecordShine(XdrvMailbox.data); I2sRecordShine(XdrvMailbox.data);
ResponseCmndChar(XdrvMailbox.data); ResponseCmndChar(XdrvMailbox.data);
} }
} else { } else {
if (audio_i2s.mic_task_handle) { if (audio_i2s.mic_task_handle) {
// stop task // stop task
audio_i2s.mic_stop = 1; audio_i2s.mic_stop = 1;
while (audio_i2s.mic_stop) { while (audio_i2s.mic_stop) {
delay(1); delay(1);
}
ResponseCmndChar_P(PSTR("Stopped"));
} }
ResponseCmndChar_P(PSTR("Stopped"));
} }
} }
}
else{
if(audio_i2s.Settings->sys.rx == 1){
ResponseCmndChar_P(PSTR("need PSRAM for MP3 recording"));
}
else{ else{
ResponseCmndChar_P(PSTR("no mic configured")); if (audio_i2s.in){
ResponseCmndChar_P(PSTR("need PSRAM for MP3 recording"));
}
else{
ResponseCmndChar_P(PSTR("no mic configured"));
}
} }
} }
}
// mic gain in factor not percent // mic gain in factor not percent
void CmndI2SMicGain(void) { void CmndI2SMicGain(void) {
// TODO - does nothing for now
if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 256)) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 256)) {
audio_i2s.Settings->rx.gain = XdrvMailbox.payload; audio_i2s.Settings->rx.gain = XdrvMailbox.payload;
} }

View File

@ -40,21 +40,22 @@ extern "C" {
// AudioOutputI2S.init() -> instance // AudioOutputI2S.init() -> instance
void* be_audio_output_i2s_init(void) { void* be_audio_output_i2s_init(void) {
return audio_i2s.out; // return the singleton of TasmotaAudioOutputI2S which is already initialized return audio_i2s.out; // return the singleton of TasmotaI2S which is already initialized
} }
// AudioOutputI2S.deinit()-> void // AudioOutputI2S.deinit()-> void
void be_audio_output_i2s_deinit(TasmotaAudioOutputI2S * out) { void be_audio_output_i2s_deinit(TasmotaI2S * out) {
out->stop(); out->stop();
} }
// AudioOutputI2S.begin() -> bool // AudioOutputI2S.begin() -> bool
int be_audio_output_i2s_begin(TasmotaAudioOutputI2S* out) { int be_audio_output_i2s_begin(TasmotaI2S* out) {
if (I2SPrepareTx()) { return false; }
return out->begin(); return out->begin();
} }
// AudioOutputI2S.stop() -> bool // AudioOutputI2S.stop() -> bool
int be_audio_output_i2s_stop(TasmotaAudioOutputI2S* out) { int be_audio_output_i2s_stop(TasmotaI2S* out) {
return out->stop(); return out->stop();
} }
@ -125,7 +126,7 @@ extern "C" {
// AudioOutputI2S() // AudioOutputI2S()
// //
int i2s_output_i2s_init(bvm *vm) { int i2s_output_i2s_init(bvm *vm) {
TasmotaAudioOutputI2S * audio = new TasmotaAudioOutputI2S(); TasmotaI2S * audio = new TasmotaI2S();
be_pushcomptr(vm, (void*) audio); be_pushcomptr(vm, (void*) audio);
be_setmember(vm, 1, ".p"); be_setmember(vm, 1, ".p");
be_return_nil(vm); be_return_nil(vm);
@ -134,7 +135,7 @@ extern "C" {
int i2s_output_i2s_deinit(bvm *vm) { int i2s_output_i2s_deinit(bvm *vm) {
int argc = be_top(vm); int argc = be_top(vm);
be_getmember(vm, 1, ".p"); be_getmember(vm, 1, ".p");
TasmotaAudioOutputI2S * audio = (TasmotaAudioOutputI2S *) be_tocomptr(vm, -1); TasmotaI2S * audio = (TasmotaI2S *) be_tocomptr(vm, -1);
if (audio) { if (audio) {
delete audio; delete audio;
// clear // clear
@ -177,6 +178,7 @@ extern "C" {
int i2s_generator_wav_begin(bvm *vm) { int i2s_generator_wav_begin(bvm *vm) {
int argc = be_top(vm); int argc = be_top(vm);
if (argc > 2) { if (argc > 2) {
if (I2SPrepareTx()) { be_raisef(vm, "internal_error", "I2SPrepareTx() failed"); be_return_nil(vm); }
AudioGeneratorWAV * wav = i2s_generator_wav_get(vm); AudioGeneratorWAV * wav = i2s_generator_wav_get(vm);
be_getmember(vm, 2, ".p"); be_getmember(vm, 2, ".p");
AudioFileSource * source = (AudioFileSource*) be_tocomptr(vm, -1); AudioFileSource * source = (AudioFileSource*) be_tocomptr(vm, -1);
@ -244,6 +246,7 @@ extern "C" {
int i2s_generator_mp3_begin(bvm *vm) { int i2s_generator_mp3_begin(bvm *vm) {
int argc = be_top(vm); int argc = be_top(vm);
if (argc > 2) { if (argc > 2) {
if (I2SPrepareTx()) { be_raisef(vm, "internal_error", "I2SPrepareTx() failed"); be_return_nil(vm); }
AudioGeneratorMP3 * mp3 = i2s_generator_mp3_get(vm); AudioGeneratorMP3 * mp3 = i2s_generator_mp3_get(vm);
be_getmember(vm, 2, ".p"); be_getmember(vm, 2, ".p");
AudioFileSource * source = (AudioFileSource*) be_tocomptr(vm, -1); AudioFileSource * source = (AudioFileSource*) be_tocomptr(vm, -1);
@ -313,12 +316,99 @@ extern "C" {
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// AudioInputI2S.init() -> instance // AudioInputI2S.init() -> instance
void* be_audio_input_i2s_init(void) { void* be_audio_input_i2s_init(void) {
return audio_i2s.out; // return the singleton of TasmotaAudioOutputI2S which is already initialized return audio_i2s.in; // return the singleton of TasmotaI2S which is already initialized
} }
// AudioInputI2S.deinit()-> void // AudioInputI2S.deinit()-> void
void be_audio_input_i2s_deinit(TasmotaAudioOutputI2S * out) { void be_audio_input_i2s_deinit(TasmotaI2S * in) {
out->stop(); in->stop();
}
// AudioInputI2S.begin() -> bool
int be_audio_input_i2s_begin(bvm *vm, TasmotaI2S* in) {
if (I2SPrepareRx()) { be_raisef(vm, "internal_error", "I2SPrepareRx() failed"); be_return_nil(vm); }
in->I2sMicInit();
return in->getRxRunning();
}
// AudioInputI2S.stop() -> bool
int be_audio_input_i2s_stop(TasmotaI2S* in) {
in->I2sMicDeinit();
return true;
}
// AudioInputI2S.get_rate() -> int
int be_audio_input_i2s_get_rate(TasmotaI2S* in) {
return in->getRxRate();
}
// AudioInputI2S.get_bits_per_sample() -> int
int be_audio_input_i2s_get_bits_per_sample(TasmotaI2S* in) {
return in->getRxBitsPerSample();
}
// AudioInputI2S.get_channels() -> int
int be_audio_input_i2s_get_channels(TasmotaI2S* in) {
return in->getRxChannels();
}
// AudioInputI2S.get_gain() -> real
float be_audio_input_i2s_get_gain(TasmotaI2S* in) {
return in->getRxGain();
}
// AudioInputI2S.set_gain(gain:real) -> bool
int be_audio_input_set_gain(TasmotaI2S* in, float gain) {
return true; // TODO
// return in->SetGain(gain);
}
// AudioInputI2S.read_bytes() -> bytes()
//
// Reads bytes in the input buffer
// Pre-condition: microphone but be recording (call begin() first)
//
// Returns `nil` if not configured or buffer empty
// Returns `bytes()` instance with 16-bits audio
int be_audio_input_i2s_read_bytes(bvm *vm) {
int argc = be_top(vm);
if (!audio_i2s.in->getRxRunning()) { be_return_nil(vm); }
uint16_t buf_audio[256];
size_t bytes_read = 0;
esp_err_t err = i2s_channel_read(audio_i2s.in->getRxHandle(), buf_audio, sizeof(buf_audio), &bytes_read, 0);
// AddLog(LOG_LEVEL_DEBUG, "BRY: be_audio_input_i2s_read_bytes - err=%i bytes=%i", err, bytes_read);
if ((err != ESP_OK) && (err != ESP_ERR_TIMEOUT)) { be_return_nil(vm); }
if (bytes_read == 0) { be_return_nil(vm); }
// we received some data
if (argc >= 2 && be_isbytes(vm, 2)) {
// we have already a bytes() buffer
be_pushvalue(vm, 2); // push on top
// resize to btr
be_getmember(vm, -1, "resize");
be_pushvalue(vm, -2);
be_pushint(vm, bytes_read);
be_call(vm, 2);
be_pop(vm, 3);
} else {
be_pushbytes(vm, nullptr, bytes_read); // allocate a buffer of size btr filled with zeros
}
// get the address of the buffer
be_getmember(vm, -1, "_buffer");
be_pushvalue(vm, -2);
be_call(vm, 1);
uint8_t * buf = (uint8_t*) be_tocomptr(vm, -2);
be_pop(vm, 2);
// copy
memmove(buf, buf_audio, bytes_read);
be_return(vm); /* return code */
} }
} }

View File

@ -136,7 +136,7 @@ extern "C" {
be_setmember(vm, 1, "remote_port"); be_setmember(vm, 1, "remote_port");
be_pop(vm, 1); be_pop(vm, 1);
be_return(vm); /* return code */ be_return(vm);
} else { } else {
be_return_nil(vm); be_return_nil(vm);
} }